This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/src/js/sw/components/chat-pane/use-stream.js
gobha b7746c3004 Changeset 0.37.14 (#226)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-23 16:47:48 +00:00

122 lines
4.3 KiB
JavaScript

// ==========================================
// 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 [streamThinking, setStreamThinking] = useState('');
const [streaming, setStreaming] = useState(false);
const contentRef = useRef('');
const thinkingRef = useRef('');
const abortRef = useRef(null);
const rafRef = useRef(null);
const _flush = useCallback(() => {
setStreamContent(contentRef.current);
setStreamThinking(thinkingRef.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);
setStreamThinking(thinkingRef.current);
setStreaming(false);
}, []);
const startStream = useCallback(async (response) => {
// Reset
contentRef.current = '';
thinkingRef.current = '';
setStreamContent('');
setStreamThinking('');
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 parsed = JSON.parse(data).choices?.[0]?.delta;
if (parsed?.content) {
let chunk = parsed.content;
// Run pipe stream filters (extensions can transform or suppress chunks)
if (window.sw?.pipe?._runStream) {
const pipeCtx = window.sw.pipe._runStream({ content: chunk });
if (pipeCtx === null) continue;
chunk = pipeCtx.content;
}
contentRef.current += chunk;
_scheduleFlush();
}
if (parsed?.reasoning_content) {
thinkingRef.current += parsed.reasoning_content;
_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);
setStreamThinking(thinkingRef.current);
setStreaming(false);
abortRef.current = null;
}, [_scheduleFlush]);
return { streamContent, streamThinking, streaming, startStream, stopStream };
}