UNPKG

@replyke/core

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

161 lines 6.65 kB
import { useCallback, useEffect, useRef, useState } from "react"; import useProject from "../projects/useProject"; import { useReplykeSelector } from "../../store/hooks"; import { selectAccessToken } from "../../store/slices/authSlice"; import { BASE_URL } from "../../config/axios"; /** * Parses raw SSE text from a ReadableStream chunk. Because TCP packets don't * align with SSE event boundaries, we maintain a buffer of incomplete text * across calls and return the leftover for the next iteration. */ function parseSseChunk(buffer) { const events = []; // Events are delimited by a blank line (\n\n) const blocks = buffer.split("\n\n"); // The last element is either empty (buffer ended cleanly) or an incomplete // block that hasn't received its terminating \n\n yet — keep it for next call. const remainder = blocks.pop() ?? ""; for (const block of blocks) { let event = "message"; let data = ""; for (const line of block.split("\n")) { if (line.startsWith("event:")) event = line.slice(6).trim(); else if (line.startsWith("data:")) data = line.slice(5).trim(); } if (data) events.push({ event, data }); } return { events, remainder }; } export default function useAskContent() { const { projectId } = useProject(); const accessToken = useReplykeSelector(selectAccessToken); const [answer, setAnswer] = useState(""); const [sources, setSources] = useState([]); const [streaming, setStreaming] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // Held across renders so reset() and unmount cleanup can abort an in-flight stream const abortControllerRef = useRef(null); // Abort stream on unmount useEffect(() => { return () => { abortControllerRef.current?.abort(); }; }, []); const reset = useCallback(() => { abortControllerRef.current?.abort(); abortControllerRef.current = null; setAnswer(""); setSources([]); setStreaming(false); setLoading(false); setError(null); }, []); const ask = useCallback(({ query, sourceTypes, spaceId, conversationId, limit }) => { if (!projectId) return; if (!query.trim()) return; // Cancel any previous stream before starting a new one abortControllerRef.current?.abort(); const controller = new AbortController(); abortControllerRef.current = controller; // Reset output state for the new query setAnswer(""); setSources([]); setError(null); setLoading(true); setStreaming(false); const body = JSON.stringify({ query, ...(sourceTypes && { sourceTypes }), ...(spaceId && { spaceId }), ...(conversationId && { conversationId }), ...(limit && { limit }), }); const headers = { "Content-Type": "application/json", Accept: "text/event-stream", }; if (accessToken) { headers["Authorization"] = `Bearer ${accessToken}`; } // Run async without blocking the render cycle — errors are surfaced via state (async () => { try { const response = await fetch(`${BASE_URL}/${projectId}/search/ask`, { method: "POST", headers, body, signal: controller.signal, }); if (!response.ok) { const text = await response.text().catch(() => ""); const message = (() => { try { return JSON.parse(text)?.error ?? `Request failed (${response.status})`; } catch { return `Request failed (${response.status})`; } })(); setError(message); setLoading(false); return; } if (!response.body) { setError("Streaming is not supported in this environment. " + "In React Native, install react-native-fetch-api, web-streams-polyfill, and react-native-polyfill-globals, " + "then call polyfillGlobals() at app startup."); setLoading(false); return; } setStreaming(true); setLoading(false); 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 }); const { events, remainder } = parseSseChunk(buffer); buffer = remainder; for (const { event, data } of events) { if (event === "token") { const parsed = JSON.parse(data); setAnswer((prev) => prev + parsed.content); } else if (event === "sources") { const parsed = JSON.parse(data); setSources(parsed); } else if (event === "done") { setStreaming(false); } else if (event === "error") { const parsed = JSON.parse(data); setError(parsed.error); setStreaming(false); } } } // Ensure streaming is cleared if the connection closes without a done event setStreaming(false); } catch (err) { if (err.name === "AbortError") return; // user called reset() setError("An unexpected error occurred"); setStreaming(false); setLoading(false); } })(); }, [projectId, accessToken]); return { answer, sources, streaming, loading, error, ask, reset }; } //# sourceMappingURL=useAskContent.js.map