@replyke/core
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
161 lines • 6.65 kB
JavaScript
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