feat: Nachrichten als wichtig markieren (Stern-Button)

- 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
This commit is contained in:
Meleki committed 2026-03-01 01:30:21 +01:00
1 parent 21ca3a451b
commit 19f32442b1
11 files changed
+124 -13

No files matched your search

+2 -1
View File
@@ -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");
+14
View File
@@ -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();
+2 -1
View File
@@ -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);
+1
View File
@@ -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; }
}
+25
View File
@@ -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<bool> 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<bool>(
"SELECT is_marked FROM messages WHERE id = @messageId",
new { messageId });
}
public async Task<IEnumerable<MessageRecord>> GetMarkedMessages(string sessionId)
{
using var conn = CreateConnection();
return await conn.QueryAsync<MessageRecord>(@"
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 });
}
}
+14 -1
View File
@@ -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<MessageRecord> markedMessages = null)
{
var parts = new List<string>();
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<string> { "## 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);
}
}
+4
View File
@@ -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?
+25 -4
View File
@@ -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 {
+5
View File
@@ -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;
+2 -2
View File
@@ -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;
+30 -4
View File
@@ -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 ? '<i class="fa fa-star"></i>' : '<i class="fa fa-star-o"></i>';
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 ? '<i class="fa fa-star"></i>' : '<i class="fa fa-star-o"></i>';
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;