mirror of
https://github.com/lrnselfreliance/wrolpi.git
synced 2026-10-26 19:01:39 -04:00
The app's first streaming client: sse.js reads the /api/chat POST body with fetch + a stream reader (apiCall's 60s JSON path cannot stream), parsing SSE events with abort support. AiChatContext holds the conversation (browser-only, per design) so it survives visiting the Manage tab; switching modes starts a fresh conversation. The Chat tab has the three-button mode picker, per-mode suggestion chips, streamed answers with clickable relative WROLPi links, tool-activity indicators, a slow-hardware banner, and a "not ready" state pointing at Manage. ChatMessage states are in the theme-sample gallery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
71 lines
2.4 KiB
JavaScript
71 lines
2.4 KiB
JavaScript
// The app's only streaming client. `apiCall` is JSON-only with a 60-second timeout, which is
|
|
// wrong for a chat answer that streams for minutes — so the AI chat uses fetch + a stream
|
|
// reader, parsing Server-Sent Events.
|
|
import {API_URI} from './components/Vars';
|
|
|
|
// Parse one SSE block ("event: name\ndata: {...}") into {event, data}.
|
|
export function parseSSEBlock(block) {
|
|
let event = null;
|
|
let data = null;
|
|
for (const line of block.split('\n')) {
|
|
if (line.startsWith('event:')) {
|
|
event = line.slice(6).trim();
|
|
} else if (line.startsWith('data:')) {
|
|
try {
|
|
data = JSON.parse(line.slice(5).trim());
|
|
} catch (e) {
|
|
data = null;
|
|
}
|
|
}
|
|
}
|
|
return event ? {event, data} : null;
|
|
}
|
|
|
|
/*
|
|
* POST /api/chat and stream its SSE events to `onEvent(event, data)`.
|
|
*
|
|
* Events: status, token, tool_call, tool_result, done, error (see modules/ai/chat.py).
|
|
* Throws on a non-OK response (e.g. AI disabled) with the API's message; abort with `signal`.
|
|
*/
|
|
export async function streamChat({mode, messages}, onEvent, signal = undefined) {
|
|
const response = await fetch(`${API_URI}/chat/`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({mode, messages}),
|
|
signal,
|
|
});
|
|
if (!response.ok) {
|
|
let message = `Chat failed: HTTP ${response.status}`;
|
|
let code = null;
|
|
try {
|
|
const content = await response.json();
|
|
message = content.summary || content.message || message;
|
|
code = content.code;
|
|
} catch (e) {
|
|
// Not JSON; keep the HTTP message.
|
|
}
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
throw error;
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
let buffer = '';
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
buffer += decoder.decode(value, {stream: true});
|
|
let index;
|
|
while ((index = buffer.indexOf('\n\n')) >= 0) {
|
|
const block = buffer.slice(0, index);
|
|
buffer = buffer.slice(index + 2);
|
|
const parsed = parseSSEBlock(block);
|
|
if (parsed) {
|
|
onEvent(parsed.event, parsed.data);
|
|
}
|
|
}
|
|
}
|
|
}
|