Changeset 0.37.8 (#220)
Co-authored-by: gobha <jasafpro@gmail.com> Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
103
src/js/sw/components/chat-pane/use-stream.js
Normal file
103
src/js/sw/components/chat-pane/use-stream.js
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user