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