History Polling: Message-ID-basiert, neuer /api/history/poll Endpoint

This commit is contained in:
Meleki committed 2026-02-26 15:54:01 +01:00
1 parent 19f926da05
commit fb2c3a0720
5 files changed
+90 -25

No files matched your search

+10
View File
@@ -14,6 +14,7 @@ public static class HistoryEndpoint
public static void MapHistoryEndpoint(this IEndpointRouteBuilder app) public static void MapHistoryEndpoint(this IEndpointRouteBuilder app)
{ {
app.MapGet("/api/history", HandleHistory); app.MapGet("/api/history", HandleHistory);
app.MapGet("/api/history/poll", HandlePoll);
} }
private static async Task HandleHistory(HttpContext ctx, DatabaseService db) private static async Task HandleHistory(HttpContext ctx, DatabaseService db)
@@ -37,4 +38,13 @@ public static class HistoryEndpoint
await ctx.Response.WriteAsJsonAsync(result); 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);
}
} }
+9
View File
@@ -135,4 +135,13 @@ public class DatabaseService
"SELECT * FROM messages WHERE chain_id = @chainId ORDER BY id", new { chainId } "SELECT * FROM messages WHERE chain_id = @chainId ORDER BY id", new { chainId }
); );
} }
public async Task<IEnumerable<MessageRecord>> GetMessagesAfterId(long afterId, int limit = 200)
{
using var conn = CreateConnection();
return await conn.QueryAsync<MessageRecord>(
"SELECT * FROM messages WHERE id > @afterId ORDER BY id LIMIT @limit",
new { afterId, limit }
);
}
} }
+5
View File
@@ -12,3 +12,8 @@ async function apiHistory(sessionId = 'null', limit = 50) {
const resp = await fetch(`/api/history?sessionId=${sessionId}&limit=${limit}`); const resp = await fetch(`/api/history?sessionId=${sessionId}&limit=${limit}`);
return resp.json(); return resp.json();
} }
async function apiHistoryPoll(afterId) {
const resp = await fetch(`/api/history/poll?afterId=${afterId}`);
return resp.json();
}
+3
View File
@@ -1,6 +1,7 @@
// ── Chat State ── // ── Chat State ──
const messages = []; const messages = [];
let isStreaming = false;
function clearChat() { function clearChat() {
messages.length = 0; messages.length = 0;
@@ -24,6 +25,7 @@ async function sendMessage() {
input.value = ''; input.value = '';
input.style.height = 'auto'; input.style.height = 'auto';
btn.disabled = true; btn.disabled = true;
isStreaming = true;
setMetaHtml('<span style="color:#7c83ff">⏳ Streaming...</span>'); setMetaHtml('<span style="color:#7c83ff">⏳ Streaming...</span>');
let fullText = ''; let fullText = '';
@@ -103,6 +105,7 @@ async function sendMessage() {
addErrorBubble('Netzwerk-Fehler: ' + e.message); addErrorBubble('Netzwerk-Fehler: ' + e.message);
} finally { } finally {
btn.disabled = false; btn.disabled = false;
isStreaming = false;
endStreamBubble(); endStreamBubble();
} }
} }
+63 -25
View File
@@ -1,40 +1,78 @@
// ── History Loading ── // ── History Loading ──
let lastKnownMessageId = 0;
let historyPollInterval = null;
async function loadHistory() { async function loadHistory() {
try { try {
const data = await apiHistory('null'); const data = await apiHistory('null');
data.reverse(); // DESC → chronologisch data.reverse(); // DESC → chronologisch
for (const entry of data) { for (const entry of data) {
for (const msg of entry.messages) { renderMessages(entry.messages, true);
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;
}
}
} }
scrollDown(); scrollDown();
startHistoryPolling();
} catch (e) { } catch (e) {
console.error('History laden fehlgeschlagen:', 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);
}
}