Changeset 0.37.8 (#220)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 18:50:04 +00:00
committed by xcaliber
parent b6152fbf5e
commit 2695bb3bdc
18 changed files with 1151 additions and 299 deletions

View File

@@ -0,0 +1,103 @@
// ==========================================
// ChatPane Kit — useStream Hook
// ==========================================
// SSE ReadableStream parser with rAF-batched state updates.
// Independently importable — no ChatPane dependency.
//
// Usage:
// const { streamContent, streaming, startStream, stopStream } = useStream();
// // Pass a Response from sw.api.channels.complete():
// startStream(response);
const { useState, useRef, useCallback } = window.hooks;
/**
* Hook that parses an SSE stream from a fetch Response.
*
* @returns {{ streamContent: string, streaming: boolean, startStream: (resp: Response) => void, stopStream: () => void }}
*/
export function useStream() {
const [streamContent, setStreamContent] = useState('');
const [streaming, setStreaming] = useState(false);
const contentRef = useRef('');
const abortRef = useRef(null);
const rafRef = useRef(null);
const _flush = useCallback(() => {
setStreamContent(contentRef.current);
rafRef.current = null;
}, []);
const _scheduleFlush = useCallback(() => {
if (!rafRef.current) {
rafRef.current = requestAnimationFrame(_flush);
}
}, [_flush]);
const stopStream = useCallback(() => {
if (abortRef.current) {
abortRef.current.abort();
abortRef.current = null;
}
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
setStreamContent(contentRef.current);
setStreaming(false);
}, []);
const startStream = useCallback(async (response) => {
// Reset
contentRef.current = '';
setStreamContent('');
setStreaming(true);
const controller = new AbortController();
abortRef.current = controller;
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (controller.signal.aborted) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') continue;
try {
const delta = JSON.parse(data).choices?.[0]?.delta?.content;
if (delta) {
contentRef.current += delta;
_scheduleFlush();
}
} catch (_) { /* skip malformed JSON */ }
}
}
} catch (e) {
if (e.name !== 'AbortError') {
contentRef.current += '\n\n**[Stream error: ' + e.message + ']**';
}
}
// Final flush
if (rafRef.current) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
setStreamContent(contentRef.current);
setStreaming(false);
abortRef.current = null;
}, [_scheduleFlush]);
return { streamContent, streaming, startStream, stopStream };
}