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:
1 parent
21ca3a451b
commit
19f32442b1
11 files changed
+124
-13
No files matched your search
@@ -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");
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user