diff --git a/Endpoints/HistoryEndpoint.cs b/Endpoints/HistoryEndpoint.cs index 8894cfa..bd0c1c7 100644 --- a/Endpoints/HistoryEndpoint.cs +++ b/Endpoints/HistoryEndpoint.cs @@ -14,6 +14,7 @@ public static class HistoryEndpoint public static void MapHistoryEndpoint(this IEndpointRouteBuilder app) { app.MapGet("/api/history", HandleHistory); + app.MapGet("/api/history/poll", HandlePoll); } private static async Task HandleHistory(HttpContext ctx, DatabaseService db) @@ -37,4 +38,13 @@ public static class HistoryEndpoint await ctx.Response.WriteAsJsonAsync(result); } + + private static async Task HandlePoll(HttpContext ctx, DatabaseService db) + { + var afterStr = ctx.Request.Query["afterId"].FirstOrDefault(); + var afterId = long.TryParse(afterStr, out var a) ? a : 0; + + var messages = await db.GetMessagesAfterId(afterId); + await ctx.Response.WriteAsJsonAsync(messages); + } } diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 8c4c526..a67a376 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -135,4 +135,13 @@ public class DatabaseService "SELECT * FROM messages WHERE chain_id = @chainId ORDER BY id", new { chainId } ); } + + public async Task> GetMessagesAfterId(long afterId, int limit = 200) + { + using var conn = CreateConnection(); + return await conn.QueryAsync( + "SELECT * FROM messages WHERE id > @afterId ORDER BY id LIMIT @limit", + new { afterId, limit } + ); + } } diff --git a/wwwroot/js/api.js b/wwwroot/js/api.js index d36a71c..c55798d 100644 --- a/wwwroot/js/api.js +++ b/wwwroot/js/api.js @@ -12,3 +12,8 @@ async function apiHistory(sessionId = 'null', limit = 50) { const resp = await fetch(`/api/history?sessionId=${sessionId}&limit=${limit}`); return resp.json(); } + +async function apiHistoryPoll(afterId) { + const resp = await fetch(`/api/history/poll?afterId=${afterId}`); + return resp.json(); +} diff --git a/wwwroot/js/chat.js b/wwwroot/js/chat.js index f3fb416..41656b1 100644 --- a/wwwroot/js/chat.js +++ b/wwwroot/js/chat.js @@ -1,6 +1,7 @@ // ── Chat State ── const messages = []; +let isStreaming = false; function clearChat() { messages.length = 0; @@ -24,6 +25,7 @@ async function sendMessage() { input.value = ''; input.style.height = 'auto'; btn.disabled = true; + isStreaming = true; setMetaHtml('⏳ Streaming...'); let fullText = ''; @@ -103,6 +105,7 @@ async function sendMessage() { addErrorBubble('Netzwerk-Fehler: ' + e.message); } finally { btn.disabled = false; + isStreaming = false; endStreamBubble(); } } diff --git a/wwwroot/js/history.js b/wwwroot/js/history.js index 3f02b3b..5f942b9 100644 --- a/wwwroot/js/history.js +++ b/wwwroot/js/history.js @@ -1,40 +1,78 @@ // ── History Loading ── +let lastKnownMessageId = 0; +let historyPollInterval = null; + async function loadHistory() { try { const data = await apiHistory('null'); data.reverse(); // DESC → chronologisch for (const entry of data) { - for (const msg of entry.messages) { - switch (msg.type) { - case 'text': - if (msg.role === 'user') { - addUserBubble(msg.content); - messages.push({ role: 'user', content: msg.content }); - } else if (msg.role === 'assistant') { - addAssistantBubble(msg.content); - messages.push({ role: 'assistant', content: msg.content }); - } - break; - - case 'tool_use': - addToolBoxStatic(msg.toolName, msg.content, null, false); - break; - - case 'tool_result': - addToolBoxStatic(null, null, msg.content, msg.isError); - break; - - case 'error': - addErrorBubble(msg.content); - break; - } - } + renderMessages(entry.messages, true); } scrollDown(); + startHistoryPolling(); } catch (e) { console.error('History laden fehlgeschlagen:', e); } } + +function renderMessages(msgs, addToConversation) { + for (const msg of msgs) { + if (msg.id > lastKnownMessageId) lastKnownMessageId = msg.id; + + switch (msg.type) { + case 'text': + if (msg.role === 'user') { + addUserBubble(msg.content); + if (addToConversation) messages.push({ role: 'user', content: msg.content }); + } else if (msg.role === 'assistant') { + addAssistantBubble(msg.content); + if (addToConversation) messages.push({ role: 'assistant', content: msg.content }); + } + break; + + case 'tool_use': + addToolBoxStatic(msg.toolName, msg.content, null, false); + break; + + case 'tool_result': + addToolBoxStatic(null, null, msg.content, msg.isError); + break; + + case 'error': + addErrorBubble(msg.content); + break; + } + } +} + +// ── Polling ── + +function startHistoryPolling(intervalMs = 3000) { + if (historyPollInterval) return; + historyPollInterval = setInterval(pollHistory, intervalMs); +} + +function stopHistoryPolling() { + if (historyPollInterval) { + clearInterval(historyPollInterval); + historyPollInterval = null; + } +} + +async function pollHistory() { + if (isStreaming) return; + + try { + const newMessages = await apiHistoryPoll(lastKnownMessageId); + if (newMessages.length > 0) { + renderMessages(newMessages, true); + scrollDown(); + } + } catch (e) { + console.error('History poll fehlgeschlagen:', e); + } +}