UNPKG

@replyke/core

Version:

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

167 lines 7.02 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = useAskContent; const react_1 = require("react"); const useProject_1 = __importDefault(require("../projects/useProject")); const hooks_1 = require("../../store/hooks"); const authSlice_1 = require("../../store/slices/authSlice"); const axios_1 = require("../../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 }; } function useAskContent() { const { projectId } = (0, useProject_1.default)(); const accessToken = (0, hooks_1.useReplykeSelector)(authSlice_1.selectAccessToken); const [answer, setAnswer] = (0, react_1.useState)(""); const [sources, setSources] = (0, react_1.useState)([]); const [streaming, setStreaming] = (0, react_1.useState)(false); const [loading, setLoading] = (0, react_1.useState)(false); const [error, setError] = (0, react_1.useState)(null); // Held across renders so reset() and unmount cleanup can abort an in-flight stream const abortControllerRef = (0, react_1.useRef)(null); // Abort stream on unmount (0, react_1.useEffect)(() => { return () => { abortControllerRef.current?.abort(); }; }, []); const reset = (0, react_1.useCallback)(() => { abortControllerRef.current?.abort(); abortControllerRef.current = null; setAnswer(""); setSources([]); setStreaming(false); setLoading(false); setError(null); }, []); const ask = (0, react_1.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(`${axios_1.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