diff --git a/Endpoints/HistoryEndpoint.cs b/Endpoints/HistoryEndpoint.cs index 547a00c..a870f02 100644 --- a/Endpoints/HistoryEndpoint.cs +++ b/Endpoints/HistoryEndpoint.cs @@ -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 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(); diff --git a/Models/MessageRecord.cs b/Models/MessageRecord.cs index 5e10c48..5397399 100644 --- a/Models/MessageRecord.cs +++ b/Models/MessageRecord.cs @@ -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; } } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index b96a0e1..b615ce2 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -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 GetActiveMessageCount(string sessionId) + public async Task GetActiveMessageCount(string sessionId, bool includeDeleted = false) { using var conn = CreateConnection(); - return await conn.ExecuteScalarAsync(@" + var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL"; + return await conn.ExecuteScalarAsync($@" 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> GetLatestActiveMessages(string sessionId, int limit) + public async Task> GetLatestActiveMessages(string sessionId, int limit, bool includeDeleted = false) { using var conn = CreateConnection(); - var messages = await conn.QueryAsync(@" + var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL"; + var messages = await conn.QueryAsync($@" 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 GetActiveMessageCountBeforeId(string sessionId, long beforeId) + public async Task GetActiveMessageCountBeforeId(string sessionId, long beforeId, bool includeDeleted = false) { using var conn = CreateConnection(); - return await conn.ExecuteScalarAsync(@" + var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL"; + return await conn.ExecuteScalarAsync($@" 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> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit) + public async Task> GetActiveMessagesBeforeId(string sessionId, long beforeId, int limit, bool includeDeleted = false) { using var conn = CreateConnection(); - var messages = await conn.QueryAsync(@" + var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL"; + var messages = await conn.QueryAsync($@" 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> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200) + public async Task> GetActiveMessagesAfterId(string sessionId, long afterId, int limit = 200, bool includeDeleted = false) { using var conn = CreateConnection(); - return await conn.QueryAsync(@" + var deletedFilter = includeDeleted ? "" : "AND m.deleted_on IS NULL"; + return await conn.QueryAsync($@" 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 ToggleMessageDelete(long messageId) + { + using var conn = CreateConnection(); + var current = await conn.ExecuteScalarAsync( + "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> GetMarkedMessages(string sessionId) { using var conn = CreateConnection(); diff --git a/TODO.md b/TODO.md index d5a6acf..816c276 100644 --- a/TODO.md +++ b/TODO.md @@ -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::<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]`) diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index fd4d230..4caab6c 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -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 { diff --git a/wwwroot/css/form.css b/wwwroot/css/form.css index e736b40..d7465be 100644 --- a/wwwroot/css/form.css +++ b/wwwroot/css/form.css @@ -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); +} + diff --git a/wwwroot/index.html b/wwwroot/index.html index 9835cb5..1c48b4f 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -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> diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index d1c6f6e..b06757e 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -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; diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index 8dba9df..ec35aef 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -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(); diff --git a/wwwroot/js/pages/settings-page.js b/wwwroot/js/pages/settings-page.js index 4e43017..252fa63 100644 --- a/wwwroot/js/pages/settings-page.js +++ b/wwwroot/js/pages/settings-page.js @@ -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() { diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 646802d..3ead21a 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -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();