feat: Nachrichten als gelöscht markieren

- DB: deleted_on TEXT Spalte in messages (Migration)
- Backend: ToggleMessageDelete(), includeDeleted-Parameter in allen History-Queries
- Neuer Endpoint: PUT /api/messages/{id}/delete
- Frontend: Trash-Button zwischen Reply- und Mark-Button
- Gelöschte Nachrichten: durchgestrichen + roter Rand (oder versteckt je nach Einstellung)
- Einstellungen: Toggle-Switch 'Gelöschte Nachrichten anzeigen'
- KI-Payload: gelöschte Nachrichten werden nicht mitgeschickt
This commit is contained in:
Meleki committed 2026-03-03 11:45:52 +01:00
1 parent 493dfcda1c
commit a14a991a0f
11 files changed
+248 -39

No files matched your search

+21 -5
View File
@@ -21,6 +21,7 @@ public static class HistoryEndpoint
app.MapGet("/api/archive/{archiveId}", HandleGetArchive);
app.MapGet("/api/attachments/{messageId}", HandleGetAttachments);
app.MapPut("/api/messages/{messageId}/mark", HandleToggleMark);
app.MapPut("/api/messages/{messageId}/delete", HandleToggleDelete);
app.MapGet("/api/chains/{chainId}", HandleGetChain);
}
@@ -44,23 +45,24 @@ public static class HistoryEndpoint
const int messageLimit = 500;
var beforeStr = ctx.Request.Query["beforeId"].FirstOrDefault();
var beforeId = long.TryParse(beforeStr, out var b) ? b : 0;
var includeDeleted = ctx.Request.Query["includeDeleted"].FirstOrDefault() == "true";
var totalCount = await db.GetActiveMessageCount(sessionId);
var totalCount = await db.GetActiveMessageCount(sessionId, includeDeleted);
List<MessageRecord> messages;
if (beforeId > 0)
{
messages = await db.GetActiveMessagesBeforeId(sessionId, beforeId, messageLimit);
messages = await db.GetActiveMessagesBeforeId(sessionId, beforeId, messageLimit, includeDeleted);
}
else
{
messages = await db.GetLatestActiveMessages(sessionId, messageLimit);
messages = await db.GetLatestActiveMessages(sessionId, messageLimit, includeDeleted);
}
// Zähle wie viele Nachrichten es VOR der ältesten geladenen noch gibt
var oldestLoadedId = messages.Count > 0 ? messages[0].Id : 0;
var remainingOlder = oldestLoadedId > 0
? await db.GetActiveMessageCountBeforeId(sessionId, oldestLoadedId)
? await db.GetActiveMessageCountBeforeId(sessionId, oldestLoadedId, includeDeleted)
: 0;
// Nach Chain gruppieren (Reihenfolge beibehalten)
@@ -91,6 +93,7 @@ public static class HistoryEndpoint
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
var afterStr = ctx.Request.Query["afterId"].FirstOrDefault();
var afterId = long.TryParse(afterStr, out var a) ? a : 0;
var includeDeleted = ctx.Request.Query["includeDeleted"].FirstOrDefault() == "true";
if (string.IsNullOrEmpty(sessionId) || !await VerifySessionOwner(ctx, db, sessionId))
{
@@ -98,7 +101,7 @@ public static class HistoryEndpoint
return;
}
var messages = await db.GetActiveMessagesAfterId(sessionId, afterId);
var messages = await db.GetActiveMessagesAfterId(sessionId, afterId, includeDeleted: includeDeleted);
await ctx.Response.WriteAsJsonAsync(messages);
}
@@ -174,6 +177,19 @@ public static class HistoryEndpoint
await ctx.Response.WriteAsJsonAsync(new { messageId, isMarked });
}
private static async Task HandleToggleDelete(HttpContext ctx, DatabaseService db)
{
var messageIdStr = ctx.Request.RouteValues["messageId"]?.ToString();
if (!long.TryParse(messageIdStr, out var messageId))
{
ctx.Response.StatusCode = 400;
return;
}
var deletedOn = await db.ToggleMessageDelete(messageId);
await ctx.Response.WriteAsJsonAsync(new { messageId, deletedOn });
}
private static async Task HandleGetChain(HttpContext ctx, DatabaseService db)
{
var chainId = ctx.Request.RouteValues["chainId"]?.ToString();
+1
View File
@@ -14,6 +14,7 @@ public class MessageRecord
public int AttachmentCount { get; set; } // Anzahl angehängter Dateien
public string AttachmentNames { get; set; } // JSON-Array mit Dateinamen
public bool IsMarked { get; set; } // als wichtig markiert
public string DeletedOn { get; set; } // null = aktiv, sonst Zeitstempel der Löschung
public string CreatedAt { get; set; }
}
+46 -15
View File
@@ -146,6 +146,7 @@ public class DatabaseService
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "messages", "deleted_on", "TEXT");
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "web_search_version", "TEXT DEFAULT ''");
@@ -560,57 +561,62 @@ public class DatabaseService
);
}
public async Task<int> GetActiveMessageCount(string sessionId)
public async Task<int> GetActiveMessageCount(string sessionId, bool includeDeleted = false)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
return await conn.ExecuteScalarAsync<int>($@"
SELECT COUNT(*) FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE c.archive_id IS NULL AND c.session_id = @sessionId",
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}",
new { sessionId });
}
public async Task<List<MessageRecord>> GetLatestActiveMessages(string sessionId, int limit)
public async Task<List<MessageRecord>> GetLatestActiveMessages(string sessionId, int limit, bool includeDeleted = false)
{
using var conn = CreateConnection();
var messages = await conn.QueryAsync<MessageRecord>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
var messages = await conn.QueryAsync<MessageRecord>($@"
SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE c.archive_id IS NULL AND c.session_id = @sessionId
WHERE c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
ORDER BY m.id DESC LIMIT @limit",
new { sessionId, limit });
return messages.Reverse().ToList();
}
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId)
public async Task<int> GetActiveMessageCountBeforeId(string sessionId, long beforeId, bool includeDeleted = false)
{
using var conn = CreateConnection();
return await conn.ExecuteScalarAsync<int>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
return await conn.ExecuteScalarAsync<int>($@"
SELECT COUNT(*) FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId",
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}",
new { sessionId, beforeId });
}
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit)
public async Task<List<MessageRecord>> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit, bool includeDeleted = false)
{
using var conn = CreateConnection();
var messages = await conn.QueryAsync<MessageRecord>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
var messages = await conn.QueryAsync<MessageRecord>($@"
SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId
WHERE m.id < @beforeId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
ORDER BY m.id DESC LIMIT @limit",
new { sessionId, beforeId, limit });
return messages.Reverse().ToList();
}
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200)
public async Task<IEnumerable<MessageRecord>> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200, bool includeDeleted = false)
{
using var conn = CreateConnection();
return await conn.QueryAsync<MessageRecord>(@"
var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL";
return await conn.QueryAsync<MessageRecord>($@"
SELECT m.* FROM messages m
JOIN chains c ON m.chain_id = c.chain_id
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId
WHERE m.id > @afterId AND c.archive_id IS NULL AND c.session_id = @sessionId {deletedFilter}
ORDER BY m.id LIMIT @limit",
new { sessionId, afterId, limit });
}
@@ -628,6 +634,31 @@ public class DatabaseService
new { messageId });
}
// ── Message Deletion ──
public async Task<string> ToggleMessageDelete(long messageId)
{
using var conn = CreateConnection();
var current = await conn.ExecuteScalarAsync<string>(
"SELECT deleted_on FROM messages WHERE id = @messageId", new { messageId });
string newValue;
if (current == null)
{
newValue = DateTime.UtcNow.ToString("o");
await conn.ExecuteAsync(
"UPDATE messages SET deleted_on = @newValue WHERE id = @messageId",
new { messageId, newValue });
}
else
{
newValue = null;
await conn.ExecuteAsync(
"UPDATE messages SET deleted_on = NULL WHERE id = @messageId",
new { messageId });
}
return newValue;
}
public async Task<IEnumerable<MessageRecord>> GetMarkedMessages(string sessionId)
{
using var conn = CreateConnection();
+9 -2
View File
@@ -7,14 +7,13 @@
- [x] Agents-Formular (nur beim Bearbeiten von vorhandenen Agent): Button der das File-Explorer Modal öffnet bei dem ich den Agent-Ordner sehe.
- [x] Chat: Maximal 500 Nachrichten laden. Dauert sonst zu lange und der Browser wird langsam
- [x] User-Management: Tabelle in Datenbank mit username / password. Wenn kein User vorhanden: dann admin mit password "admin" anlegen
- [ ] Neuer Message-Tag: `[canvas:<title>:<height (optional)>]...[/canvas]`
- [x] Websearch Tool
- [x] Anfrage/Stream abbrechen
- [x] Telegram-Intergration
- [x] Refactoring: Agent/Session-Einstellungen, die den System-Prompt bzw. das Tool Use bearbeiten
- [ ] Cronjobs: Planung, ob cron von Debian/Linux verwendet werden soll, oder ein eigener Worker/Service in der App selbst, der das übernimmt
- [x] Weiteres Modell hinzufügen: "Haiku 4.5"
- [ ] Weitere Tools: Glob? Search?
- [ ] Weitere Tools: Glob? Search? Todo List?
## Bugs
- [x] Wenn gerade gestreamt wird und man refreshed die Seite, dann verliert das Frontend den Status "Streaming..." und ich kann eine Nachricht schreiben, obwohl das nicht möglich sein sollte
@@ -33,7 +32,15 @@
- [x] File Explorer: CodeMirror lässt sich horizontal nicht scroll
- [ ] Chat: Datum und Uhrzeit bei den Nachrichten anzeigen (Format: dd.MM.yyyy HH:mm:ss)
- [x] Dashboard: Menu-Dropdown bei Cards sind abgeschnitten wenn die Card-Höhe nicht ausreicht (logisch, weil das Menu-Dropdown ein Child von der Card ist)
- [ ] Auto-scrolling deaktivieren, wenn man weiterhoch gescrollt hat, und er wieder fortsetzen wenn man ganz nach unten gescrollt hat
## Ideas / Backlog
- [ ] Memory: Embedding-Modell für semantische Suche über Memory-Dateien (Vektor-basiert + BM25 Hybrid Search)
- [ ] Subagents: Möglichkeit Subagents zu starten und zu orchestrieren
## Features aus Devy übernehmen
- [ ] 💬 Typing-Indicator - Animierter Indikator statt statischem "denkt nach..."
- [ ] 🎨 Chat-Hintergründe - Benutzerdefinierte Hintergründe pro Session
- [ ] 👤 User-Management - Admin-only: Benutzer CRUD, Login-as-User, Default-Pfade
- [ ] 🖥️ Windows Desktop App - WPF + WebView2 Wrapper mit konfigurierbarer URL
- [ ] 🎨 Canvas - HTML/CSS/JS inline rendern in sandboxed iframes mit Resize und Source-View (Message-Tag: `[canvas:<title>:<height (optional)>]...[/canvas]`)
+25
View File
@@ -280,6 +280,7 @@
}
.msg-reply-btn,
.msg-delete-btn,
.msg-mark-btn {
position: absolute;
top: var(--badge-spacing);
@@ -294,6 +295,10 @@
}
.msg-reply-btn {
right: calc(var(--badge-spacing) + 4em + var(--badge-spacing) * 2);
}
.msg-delete-btn {
right: calc(var(--badge-spacing) + 2em + var(--badge-spacing));
}
@@ -302,11 +307,13 @@
}
.msg:hover .msg-reply-btn,
.msg:hover .msg-delete-btn,
.msg:hover .msg-mark-btn {
opacity: var(--semi-transparent);
}
.msg-reply-btn:hover,
.msg-delete-btn:hover,
.msg-mark-btn:hover {
opacity: 1 !important;
}
@@ -316,12 +323,30 @@
color: var(--warning);
}
.msg-delete-btn.deleted {
opacity: 1 !important;
color: var(--dangerous);
}
/* Markierte Nachrichten */
.msg.marked {
border-left: 3px solid var(--warning);
}
/* Gelöschte Nachrichten */
.msg.msg-deleted {
opacity: 0.45;
text-decoration: line-through;
text-decoration-color: var(--dangerous);
border-left: 3px solid var(--dangerous);
}
.msg.msg-deleted-hidden {
display: none;
}
/* Reply Quote in User Bubble */
.reply-quote {
+45
View File
@@ -228,3 +228,48 @@ label.skill-item {
background: var(--dangerous);
color: var(--f1-bg);
}
/* Toggle Switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 2.4em;
height: 1.4em;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
inset: 0;
background: var(--f2-border);
border-radius: 1em;
transition: background var(--transition-duration);
}
.toggle-slider::before {
content: '';
position: absolute;
width: 1em;
height: 1em;
left: .2em;
top: .2em;
background: var(--f1-bg);
border-radius: 50%;
transition: transform var(--transition-duration);
}
.toggle-switch input:checked + .toggle-slider {
background: var(--secondary);
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(1em);
}
+12
View File
@@ -273,6 +273,18 @@
</select>
</div>
</div>
<div class="setting-row">
<div class="setting-info">
<label>Gelöschte Nachrichten</label>
<span class="setting-hint">Gelöschte Nachrichten durchgestrichen anzeigen (werden nicht an die KI gesendet)</span>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="settingShowDeletedMessages">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</section>
<section id="settingsBackupSection" class="settings-section" style="display:none">
<h2>Daten</h2>
+11 -3
View File
@@ -85,15 +85,18 @@ async function apiDeleteAgent(agentId) {
return fetch(`/api/agents/${agentId}`, { method: 'DELETE' });
}
async function apiHistory(sessionId, beforeId) {
async function apiHistory(sessionId, beforeId, includeDeleted = false) {
let url = `/api/history?sessionId=${sessionId}`;
if (beforeId) url += `&beforeId=${beforeId}`;
if (includeDeleted) url += `&includeDeleted=true`;
const resp = await fetch(url);
return resp.json();
}
async function apiHistoryPoll(sessionId, afterId) {
const resp = await fetch(`/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`);
async function apiHistoryPoll(sessionId, afterId, includeDeleted = false) {
let url = `/api/history/poll?sessionId=${sessionId}&afterId=${afterId}`;
if (includeDeleted) url += `&includeDeleted=true`;
const resp = await fetch(url);
return resp.json();
}
@@ -186,6 +189,11 @@ async function apiToggleMessageMark(messageId) {
return resp.json();
}
async function apiToggleMessageDelete(messageId) {
const resp = await fetch(`/api/messages/${messageId}/delete`, { method: 'PUT' });
return resp.json();
}
async function apiGitStatus(sessionId) {
const resp = await fetch(`/api/sessions/${sessionId}/git-status`);
if (!resp.ok) return null;
+11 -7
View File
@@ -10,7 +10,7 @@ async function loadHistory() {
try {
const sid = getCurrentSessionId();
const data = await apiHistory(sid);
const data = await apiHistory(sid, null, isDeletedVisible());
const entries = data.entries || [];
// Älteste geladene Message-ID merken für Nachladen
@@ -51,7 +51,7 @@ async function loadOlderMessages() {
try {
const sid = getCurrentSessionId();
const data = await apiHistory(sid, _oldestLoadedMessageId);
const data = await apiHistory(sid, _oldestLoadedMessageId, isDeletedVisible());
const entries = data.entries || [];
_hasMoreOlderMessages = data.hasMore;
@@ -146,11 +146,13 @@ function _renderMessagesIntoFragment(fragment, msgs) {
case 'text':
if (msg.role === 'user') {
div.className = 'msg user markdown' + (msg.isMarked ? ' marked' : '');
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'user';
div.dataset.timestamp = msg.createdAt || '';
if (msg.id) div.dataset.messageId = msg.id;
renderUserContent(div, msg.content);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_addMarkButton(div, msg.id, msg.isMarked);
fragment.appendChild(div);
if (msg.attachmentCount > 0) {
@@ -167,12 +169,14 @@ function _renderMessagesIntoFragment(fragment, msgs) {
}
} else if (msg.role === 'assistant') {
div.className = 'msg assistant markdown' + (msg.isMarked ? ' marked' : '');
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'assistant';
div.dataset.timestamp = msg.createdAt || '';
if (msg.id) div.dataset.messageId = msg.id;
div.innerHTML = renderMarkdown(msg.content);
parseFluentEmojis(div);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_addMarkButton(div, msg.id, msg.isMarked);
fragment.appendChild(div);
}
@@ -215,12 +219,12 @@ function renderMessages(msgs, addToConversation) {
switch (msg.type) {
case 'text':
if (msg.role === 'user') {
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked);
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
if (addToConversation) messages.push({ role: 'user', content: msg.content });
if (addToConversation && !msg.deletedOn) messages.push({ role: 'user', content: msg.content });
} else if (msg.role === 'assistant') {
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked);
if (addToConversation) messages.push({ role: 'assistant', content: msg.content });
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
if (addToConversation && !msg.deletedOn) messages.push({ role: 'assistant', content: msg.content });
}
break;
@@ -388,7 +392,7 @@ async function pollHistory(force = false) {
_pollLock = true;
try {
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId);
const newMessages = await apiHistoryPoll(getCurrentSessionId(), lastKnownMessageId, isDeletedVisible());
if (newMessages.length > 0) {
renderMessages(newMessages, true);
scrollDown();
+11 -1
View File
@@ -5,7 +5,8 @@ const SETTINGS_KEY = 'settings';
const SETTINGS_DEFAULTS = {
pageWidth: 900,
toolCallDisplay: 'modifications', // 'all' | 'modifications' | 'hidden'
chainMetaDisplay: 'sometimes' // 'always' | 'sometimes' | 'never'
chainMetaDisplay: 'sometimes', // 'always' | 'sometimes' | 'never'
showDeletedMessages: false
};
function loadSettings() {
@@ -38,6 +39,7 @@ registerPage('settings', {
const display = document.getElementById('settingPageWidthValue');
const toolCallSelect = document.getElementById('settingToolCallDisplay');
const chainMetaSelect = document.getElementById('settingChainMetaDisplay');
const showDeletedToggle = document.getElementById('settingShowDeletedMessages');
const resetBtn = document.getElementById('settingsResetBtn');
const backupSection = document.getElementById('settingsBackupSection');
const telegramSection = document.getElementById('settingsTelegramSection');
@@ -53,6 +55,7 @@ registerPage('settings', {
display.textContent = s.pageWidth + 'px';
toolCallSelect.value = s.toolCallDisplay;
chainMetaSelect.value = s.chainMetaDisplay;
showDeletedToggle.checked = s.showDeletedMessages;
slider.addEventListener('input', () => {
display.textContent = slider.value + 'px';
@@ -77,6 +80,12 @@ registerPage('settings', {
saveSettings(s);
});
showDeletedToggle.addEventListener('change', () => {
const s = loadSettings();
s.showDeletedMessages = showDeletedToggle.checked;
saveSettings(s);
});
const backupBtn = document.getElementById('settingsBackupBtn');
if (backupBtn) backupBtn.addEventListener('click', async () => {
backupBtn.disabled = true;
@@ -100,6 +109,7 @@ registerPage('settings', {
display.textContent = SETTINGS_DEFAULTS.pageWidth + 'px';
toolCallSelect.value = SETTINGS_DEFAULTS.toolCallDisplay;
chainMetaSelect.value = SETTINGS_DEFAULTS.chainMetaDisplay;
showDeletedToggle.checked = SETTINGS_DEFAULTS.showDeletedMessages;
});
function initTelegramSection() {
+56 -6
View File
@@ -115,6 +115,46 @@ function truncate(s, max) {
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
}
// ── Delete Button ──
function _addDeleteButton(div, messageId, deletedOn) {
if (!messageId) return;
const isDeleted = !!deletedOn;
const btn = document.createElement('button');
btn.className = 'msg-delete-btn icon-btn' + (isDeleted ? ' deleted' : '');
btn.innerHTML = '<i class="fa fa-trash-o"></i>';
btn.title = isDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
btn.addEventListener('click', async (e) => {
e.stopPropagation();
try {
const result = await apiToggleMessageDelete(messageId);
const nowDeleted = !!result.deletedOn;
btn.classList.toggle('deleted', nowDeleted);
btn.title = nowDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
if (nowDeleted && !isDeletedVisible()) {
// Nachricht ausblenden
div.classList.add('msg-deleted-hidden');
} else {
div.classList.remove('msg-deleted-hidden');
div.classList.toggle('msg-deleted', nowDeleted);
}
} catch (err) {
console.error('Delete toggle failed:', err);
}
});
// Nach dem Reply-Button einfügen
const replyBtn = div.querySelector('.msg-reply-btn');
if (replyBtn) {
replyBtn.after(btn);
} else {
div.prepend(btn);
}
}
function isDeletedVisible() {
return loadSettings().showDeletedMessages === true;
}
// ── Mark Button ──
function _addMarkButton(div, messageId, isMarked) {
@@ -134,11 +174,17 @@ function _addMarkButton(div, messageId, isMarked) {
console.error('Mark toggle failed:', err);
}
});
const replyBtn = div.querySelector('.msg-reply-btn');
if (replyBtn) {
replyBtn.after(btn);
// Nach dem Delete-Button einfügen (oder nach Reply-Button, oder am Anfang)
const deleteBtn = div.querySelector('.msg-delete-btn');
if (deleteBtn) {
deleteBtn.after(btn);
} else {
div.prepend(btn);
const replyBtn = div.querySelector('.msg-reply-btn');
if (replyBtn) {
replyBtn.after(btn);
} else {
div.prepend(btn);
}
}
}
@@ -238,23 +284,26 @@ function _renderUserTextContent(container, raw) {
parseFluentEmojis(wrap);
}
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false) {
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
const div = document.createElement('div');
div.className = 'msg user markdown' + (isMarked ? ' marked' : '');
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'user';
div.dataset.timestamp = timestamp || new Date().toISOString();
if (messageId) div.dataset.messageId = messageId;
if (streamed) div.classList.add('streamed');
renderUserContent(div, text);
_addReplyButton(div);
_addDeleteButton(div, messageId, deletedOn);
_addMarkButton(div, messageId, isMarked);
chatEl().appendChild(div);
scrollDown();
}
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false) {
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
const div = document.createElement('div');
div.className = 'msg assistant markdown' + (isMarked ? ' marked' : '');
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = 'assistant';
div.dataset.timestamp = timestamp || new Date().toISOString();
if (messageId) div.dataset.messageId = messageId;
@@ -262,6 +311,7 @@ function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = f
parseFluentEmojis(div);
wrapKreaGalleries(div);
_addReplyButton(div);
_addDeleteButton(div, messageId, deletedOn);
_addMarkButton(div, messageId, isMarked);
chatEl().appendChild(div);
scrollDown();