"use client"; // Client chat engine: optimistic sends through the same-origin gateway // proxy, incremental SSE parsing, reducer-owned streaming state. import { useCallback, useReducer, useRef } from "react"; import { parseEnvelope } from "./events"; import { createSseParser } from "./stream-parser"; import { initialTranscript, transcriptReducer, type TranscriptState, type UiMessage, } from "./transcript"; export interface ChatHandle { state: TranscriptState; send: (text: string) => Promise; } export function useChat( clawId: string, sessionKey: string, initialMessages: UiMessage[], ): ChatHandle { const [state, dispatch] = useReducer( transcriptReducer, undefined, (): TranscriptState => ({ ...initialTranscript(), messages: initialMessages, }), ); const abortRef = useRef(null); const send = useCallback( async (text: string) => { abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; dispatch({ kind: "send", text }); try { const response = await fetch( `/api/gateway?clawId=${encodeURIComponent(clawId)}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ sessionKey, message: text }), signal: controller.signal, }, ); if (!response.ok || !response.body) { dispatch({ kind: "transport_error", message: `gateway returned ${response.status}`, }); return; } const parser = createSseParser(); const reader = response.body.getReader(); for (;;) { const { done, value } = await reader.read(); const frames = done ? parser.flush() : parser.push(value); for (const frame of frames) { dispatch({ kind: "gateway", envelope: parseEnvelope(frame.id, frame.data), }); } if (done) { break; } } } catch (error) { if (!controller.signal.aborted) { dispatch({ kind: "transport_error", message: String(error) }); } } }, [clawId, sessionKey], ); return { state, send }; }