Recipe: streaming UI
Rendering an answer as it arrives, with citations and a cancel button.
import { useCallback, useRef, useState } from "react";
import { createClient, type CitationSource } from "@oprag/sdk";
const oprag = createClient({
apiUrl: import.meta.env.VITE_OPRAG_API_URL,
apiKey: import.meta.env.VITE_OPRAG_EMBED_KEY,
});
export function useAsk() {
const [answer, setAnswer] = useState("");
const [sources, setSources] = useState<CitationSource[]>([]);
const [pending, setPending] = useState(false);
const controller = useRef<AbortController>();
const ask = useCallback(async (question: string, sessionId?: string) => {
controller.current?.abort(); // one question at a time
const next = new AbortController();
controller.current = next;
setAnswer("");
setSources([]);
setPending(true);
try {
const final = await oprag.chat.stream(
{ question, sessionId },
{
// `accumulated` is the whole answer so far — no buffer needed here.
onToken: (_token, accumulated) => setAnswer(accumulated),
onSources: (next) => setSources(next ?? []),
signal: next.signal,
},
);
if (final.type === "lead_capture_prompt") {
return { kind: "lead" as const, prompt: final.promptMessage, fields: final.fields };
}
return { kind: "answer" as const };
} catch (err) {
// An abort is not a failure — it is the user asking something else.
if (next.signal.aborted) return { kind: "aborted" as const };
throw err;
} finally {
setPending(false);
}
}, []);
return { ask, answer, sources, pending, cancel: () => controller.current?.abort() };
} Citations arrive through onSources once, not per token, so render them when they land rather than on every frame.
Ready to ship?
Get started free