From 19f32442b1e3d5ac9477fb3f88095f0c18a137a3 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sun, 1 Mar 2026 01:30:21 +0100 Subject: [PATCH] feat: Nachrichten als wichtig markieren (Stern-Button) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MessageRecord: neues Property IsMarked - DatabaseService: Migration is_marked, ToggleMessageMark(), GetMarkedMessages() - HistoryEndpoint: PUT /api/messages/{id}/mark (Toggle) - SystemPromptBuilder: markierte Nachrichten als Referenz im System-Prompt - ChatEndpoint + SessionEndpoint: markierte Nachrichten laden und an Builder übergeben - Frontend: Stern-Button (☆/★) auf jeder Nachricht, Toggle per API - CSS: .marked-Klasse mit gelbem Seitenrand, Button-Styling --- Endpoints/ChatEndpoint.cs | 3 ++- Endpoints/HistoryEndpoint.cs | 14 ++++++++++++++ Endpoints/SessionEndpoint.cs | 3 ++- Models/MessageRecord.cs | 1 + Services/DatabaseService.cs | 25 ++++++++++++++++++++++++ Services/SystemPromptBuilder.cs | 15 ++++++++++++++- TODO.md | 4 ++++ wwwroot/css/chat.css | 29 ++++++++++++++++++++++++---- wwwroot/js/api.js | 5 +++++ wwwroot/js/history.js | 4 ++-- wwwroot/js/ui.js | 34 +++++++++++++++++++++++++++++---- 11 files changed, 124 insertions(+), 13 deletions(-) diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 1f83a33..5b8f6e3 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -77,7 +77,8 @@ public static class ChatEndpoint res.Headers["Cache-Control"] = "no-cache"; res.Headers["Connection"] = "keep-alive"; - var systemPromptText = SystemPromptBuilder.Build(agent, session); + var markedMessages = await db.GetMarkedMessages(sessionId); + var systemPromptText = SystemPromptBuilder.Build(agent, session, markedMessages); var chainId = Guid.NewGuid().ToString("N"); diff --git a/Endpoints/HistoryEndpoint.cs b/Endpoints/HistoryEndpoint.cs index b842e8a..4667c88 100644 --- a/Endpoints/HistoryEndpoint.cs +++ b/Endpoints/HistoryEndpoint.cs @@ -19,6 +19,7 @@ public static class HistoryEndpoint app.MapGet("/api/archives", HandleListArchives); app.MapGet("/api/archive/{archiveId}", HandleGetArchive); app.MapGet("/api/attachments/{messageId}", HandleGetAttachments); + app.MapPut("/api/messages/{messageId}/mark", HandleToggleMark); app.MapGet("/api/chains/{chainId}", HandleGetChain); } @@ -119,6 +120,19 @@ public static class HistoryEndpoint await ctx.Response.WriteAsJsonAsync(result); } + private static async Task HandleToggleMark(HttpContext ctx, DatabaseService db) + { + var messageIdStr = ctx.Request.RouteValues["messageId"]?.ToString(); + if (!long.TryParse(messageIdStr, out var messageId)) + { + ctx.Response.StatusCode = 400; + return; + } + + var isMarked = await db.ToggleMessageMark(messageId); + await ctx.Response.WriteAsJsonAsync(new { messageId, isMarked }); + } + private static async Task HandleGetChain(HttpContext ctx, DatabaseService db) { var chainId = ctx.Request.RouteValues["chainId"]?.ToString(); diff --git a/Endpoints/SessionEndpoint.cs b/Endpoints/SessionEndpoint.cs index b35952b..a083117 100644 --- a/Endpoints/SessionEndpoint.cs +++ b/Endpoints/SessionEndpoint.cs @@ -122,7 +122,8 @@ public static class SessionEndpoint return; } - var systemPrompt = SystemPromptBuilder.Build(agent, session); + var markedMessages = await db.GetMarkedMessages(sessionId); + var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages); ctx.Response.ContentType = "text/plain; charset=utf-8"; await ctx.Response.WriteAsync(systemPrompt); diff --git a/Models/MessageRecord.cs b/Models/MessageRecord.cs index 0f153d8..5e10c48 100644 --- a/Models/MessageRecord.cs +++ b/Models/MessageRecord.cs @@ -13,6 +13,7 @@ public class MessageRecord public int Iteration { get; set; } // welche Loop-Iteration 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 CreatedAt { get; set; } } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 0039c56..2085ece 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -128,6 +128,7 @@ public class DatabaseService TryAddColumn(conn, "messages", "attachment_names", "TEXT"); TryAddColumn(conn, "agents", "tool_choice", "TEXT DEFAULT 'any'"); TryAddColumn(conn, "agents", "custom_commands", "TEXT"); + TryAddColumn(conn, "messages", "is_marked", "INTEGER NOT NULL DEFAULT 0"); } private void TryAddColumn(SqliteConnection conn, string table, string column, string type) @@ -494,4 +495,28 @@ public class DatabaseService ORDER BY m.id LIMIT @limit", new { sessionId, afterId, limit }); } + + // ── Message Marking ── + + public async Task ToggleMessageMark(long messageId) + { + using var conn = CreateConnection(); + await conn.ExecuteAsync( + "UPDATE messages SET is_marked = CASE WHEN is_marked = 1 THEN 0 ELSE 1 END WHERE id = @messageId", + new { messageId }); + return await conn.ExecuteScalarAsync( + "SELECT is_marked FROM messages WHERE id = @messageId", + new { messageId }); + } + + public async Task> GetMarkedMessages(string sessionId) + { + using var conn = CreateConnection(); + return await conn.QueryAsync(@" + SELECT m.* FROM messages m + JOIN chains c ON m.chain_id = c.chain_id + WHERE c.session_id = @sessionId AND m.is_marked = 1 + ORDER BY m.id", + new { sessionId }); + } } diff --git a/Services/SystemPromptBuilder.cs b/Services/SystemPromptBuilder.cs index 7c972c8..51bdcb8 100644 --- a/Services/SystemPromptBuilder.cs +++ b/Services/SystemPromptBuilder.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; +using System.Linq; using Gott.Models; namespace Gott.Services; public static class SystemPromptBuilder { - public static string Build(AgentRecord agent, SessionRecord session) + public static string Build(AgentRecord agent, SessionRecord session, IEnumerable markedMessages = null) { var parts = new List(); parts.Add($"Du bist {agent.Name}."); @@ -50,6 +51,18 @@ Wenn der User auf eine bestimmte Zeile in einer Datei verweist (z.B. `[Datei: fo Diese Regel hat höchste Priorität und gilt für JEDE Antwort: Wenn eine Datei gelesen, geschrieben, bearbeitet, erstellt oder gelöscht werden soll, oder ein Befehl ausgeführt werden soll, MUSS der entsprechende Tool-Aufruf erfolgen. Ohne Tool-Aufruf ist NICHTS passiert. Beschreibe keine Änderungen - führe sie durch."); } + var marked = markedMessages?.Where(m => !string.IsNullOrWhiteSpace(m.Content)).ToList(); + if (marked != null && marked.Count > 0) + { + var lines = new List { "## Markierte Nachrichten\nDer User hat folgende Nachrichten als wichtig markiert:" }; + foreach (var m in marked) + { + var role = m.Role == "user" ? "User" : "Assistant"; + lines.Add($"- [{role}]: {m.Content.Trim()}"); + } + parts.Add(string.Join("\n", lines)); + } + return string.Join("\n\n", parts); } } diff --git a/TODO.md b/TODO.md index aae955d..6944936 100644 --- a/TODO.md +++ b/TODO.md @@ -3,6 +3,7 @@ ## Features - [ ] Skills: In Datenbank anlegen können, mit Pfad zu Skill-Dateien; im Frontend am Dashboard dargestellt. Zuweisung an Agent und/oder Session. - [ ] System-Prompt: Nachrichten einer Session als Referenz und somit in den System-Prompt aufnehmen + ## 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 - [x] Größere Tasks brechen irgendwann ab, könnte an der Limitierung von Antrophic/Claude liegen. Jedoch sollte wird es scheinbar nicht behandelt: Der Status bleibt auf "Streaming..." aber es passiert nichts mehr @@ -14,3 +15,6 @@ - [x] Chat: Es scrollt beim Laden nicht ganz nach unten, ich denke weil im DOM noch etwas nachgeladen wird (ich denke es ist die Schriftart), und die Höhe sich beim Laden nochmal verändert - [x] Chat: User-Nachrichten mit dem Tag [Datei: pfat/zur/datei.xyz:1-10] wird nicht als HTML geparsed - [x] Chat: Wird zwar erkannt und ist klickbar, aber es öffnet sich einfach nur der File Explorer, ohne dass sich die Datei öffnet und die Darstellung in Monospace ist auch nicht schön + +## Backlog +- [ ] Gibt es eine Möglichkeit Subagents zu starten? diff --git a/wwwroot/css/chat.css b/wwwroot/css/chat.css index 17e3116..a075d21 100644 --- a/wwwroot/css/chat.css +++ b/wwwroot/css/chat.css @@ -268,10 +268,10 @@ position: relative; } -.msg-reply-btn { +.msg-reply-btn, +.msg-mark-btn { position: absolute; top: var(--badge-spacing); - right: var(--badge-spacing); opacity: 0; transition: opacity .15s; padding: .15em .35em; @@ -281,14 +281,35 @@ border-radius: var(--border-radius); } -.msg:hover .msg-reply-btn { +.msg-reply-btn { + right: var(--badge-spacing); +} + +.msg-mark-btn { + right: calc(var(--badge-spacing) + 2em); +} + +.msg:hover .msg-reply-btn, +.msg:hover .msg-mark-btn { opacity: var(--semi-transparent); } -.msg-reply-btn:hover { +.msg-reply-btn:hover, +.msg-mark-btn:hover { opacity: 1 !important; } +.msg-mark-btn.marked { + opacity: 1 !important; + color: var(--warning); +} + +/* Markierte Nachrichten */ + +.msg.marked { + border-left: 3px solid var(--warning); +} + /* Reply Quote in User Bubble */ .reply-quote { diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index a0cb08b..9ea1bcb 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -131,6 +131,11 @@ async function apiGetChain(chainId) { return resp.json(); } +async function apiToggleMessageMark(messageId) { + const resp = await fetch(`/api/messages/${messageId}/mark`, { 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 edcb361..5bc1258 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -35,11 +35,11 @@ function renderMessages(msgs, addToConversation) { switch (msg.type) { case 'text': if (msg.role === 'user') { - addUserBubble(msg.content, false, msg.createdAt); + addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked); if (msg.attachmentCount > 0) addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames); if (addToConversation) messages.push({ role: 'user', content: msg.content }); } else if (msg.role === 'assistant') { - addAssistantBubble(msg.content, msg.createdAt); + addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked); if (addToConversation) messages.push({ role: 'assistant', content: msg.content }); } break; diff --git a/wwwroot/js/ui.js b/wwwroot/js/ui.js index 2e376a3..b1b6d12 100644 --- a/wwwroot/js/ui.js +++ b/wwwroot/js/ui.js @@ -33,6 +33,28 @@ function truncate(s, max) { return s && s.length > max ? s.substring(0, max) + '...' : (s || ''); } +// ── Mark Button ── + +function _addMarkButton(div, messageId, isMarked) { + if (!messageId) return; // kein DB-Eintrag → kein Button + const btn = document.createElement('button'); + btn.className = 'msg-mark-btn icon-btn' + (isMarked ? ' marked' : ''); + btn.innerHTML = isMarked ? '' : ''; + btn.title = 'Als wichtig markieren'; + btn.addEventListener('click', async (e) => { + e.stopPropagation(); + try { + const result = await apiToggleMessageMark(messageId); + btn.classList.toggle('marked', result.isMarked); + btn.innerHTML = result.isMarked ? '' : ''; + div.classList.toggle('marked', result.isMarked); + } catch (err) { + console.error('Mark toggle failed:', err); + } + }); + div.prepend(btn); +} + // ── Reply Button ── function _addReplyButton(div) { @@ -129,25 +151,29 @@ function _renderUserTextContent(container, raw) { container.appendChild(frag); } -function addUserBubble(text, streamed = false, timestamp = '') { +function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false) { const div = document.createElement('div'); - div.className = 'msg user'; + div.className = 'msg user' + (isMarked ? ' marked' : ''); 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); + _addMarkButton(div, messageId, isMarked); chatEl().appendChild(div); scrollDown(); } -function addAssistantBubble(text, timestamp = '') { +function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false) { const div = document.createElement('div'); - div.className = 'msg assistant markdown'; + div.className = 'msg assistant markdown' + (isMarked ? ' marked' : ''); div.dataset.role = 'assistant'; div.dataset.timestamp = timestamp || new Date().toISOString(); + if (messageId) div.dataset.messageId = messageId; div.innerHTML = renderMarkdown(text); _addReplyButton(div); + _addMarkButton(div, messageId, isMarked); chatEl().appendChild(div); scrollDown(); return div;