Building a chat widget
The SDK's MVP use case — a streaming, cited chat widget with thumbs up/down feedback, in ~40 lines. Framework-agnostic core plus a React example.
The first thing most teams build on the SDK is a chat widget: a text box
that streams a grounded answer token-by-token, shows its sources, and lets the
user rate it. Every piece the widget needs is on the client — answerStream
for the stream, Citation[] on the terminal frame for sources, and feedback
for the rating.
The loop
import { Molecule, type AnswerStreamEvent, type Turn } from "@molecule/sdk";
const molecule = new Molecule({
apiKey: process.env.NEXT_PUBLIC_MOLECULE_KEY!, // a PUBLIC key — see the note
baseUrl: "https://brain.example.com",
});
const history: Turn[] = [];
async function ask(question: string, onToken: (t: string) => void) {
history.push({ role: "user", content: question });
let answer = "";
let decisionId = "";
for await (const ev of molecule.answerStream(question, {
surface: "site_chatbot",
channel: "chat",
history,
})) {
if (ev.type === "token") {
answer += ev.delta;
onToken(ev.delta); // render incrementally
} else if (ev.type === "done") {
decisionId = ev.decisionId; // keep it for feedback
renderSources(ev.citations);
} else if (ev.type === "error") {
throw new Error(ev.error);
}
}
history.push({ role: "assistant", content: answer });
return decisionId;
}
// When the user clicks 👍 / 👎:
async function rate(decisionId: string, rating: "up" | "down") {
await molecule.feedback({ decisionId, rating });
}Ship a PUBLIC key in the browser
A key exposed in page JavaScript must be a public key. By construction it
can only unlock the tenant's public namespace (end_customer audience), so a
site visitor can never reach private knowledge — even though the key is
visible. Never put an internal_agent key in a browser bundle.
React example
A minimal streaming chat component. useRef holds the mutable history and the
current decisionId; state drives the render.
"use client";
import { useRef, useState } from "react";
import { Molecule, type Turn } from "@molecule/sdk";
const molecule = new Molecule({
apiKey: process.env.NEXT_PUBLIC_MOLECULE_KEY!,
baseUrl: process.env.NEXT_PUBLIC_BRAIN_URL!,
});
export function Chat() {
const [messages, setMessages] = useState<{ role: "user" | "assistant"; text: string }[]>([]);
const [streaming, setStreaming] = useState(false);
const history = useRef<Turn[]>([]);
const lastDecision = useRef<string>("");
const abort = useRef<AbortController | null>(null);
async function send(q: string) {
abort.current?.abort(); // cancel any in-flight answer
abort.current = new AbortController();
setMessages((m) => [...m, { role: "user", text: q }, { role: "assistant", text: "" }]);
history.current.push({ role: "user", content: q });
setStreaming(true);
let answer = "";
try {
for await (const ev of molecule.answerStream(q, {
surface: "site_chatbot",
channel: "chat",
history: history.current,
signal: abort.current.signal,
})) {
if (ev.type === "token") {
answer += ev.delta;
setMessages((m) => {
const next = [...m];
next[next.length - 1] = { role: "assistant", text: answer };
return next;
});
} else if (ev.type === "done") {
lastDecision.current = ev.decisionId;
} else if (ev.type === "error") {
answer = "Sorry — something went wrong.";
}
}
history.current.push({ role: "assistant", content: answer });
} finally {
setStreaming(false);
}
}
const rate = (rating: "up" | "down") =>
lastDecision.current && molecule.feedback({ decisionId: lastDecision.current, rating });
return (
<div>
{messages.map((m, i) => (
<p key={i} data-role={m.role}>{m.text}</p>
))}
{!streaming && lastDecision.current && (
<div>
<button onClick={() => rate("up")}>👍</button>
<button onClick={() => rate("down")}>👎</button>
</div>
)}
<input
placeholder="Ask a question…"
onKeyDown={(e) => {
if (e.key === "Enter" && e.currentTarget.value.trim()) {
send(e.currentTarget.value.trim());
e.currentTarget.value = "";
}
}}
/>
</div>
);
}Not ready to stream?
If you don't want to handle frames, answer() returns the whole composition in
one call, and answerStreamToResponse() collects a stream into that same shape:
const res = await molecule.answer("what are your pricing plans?", {
surface: "site_chatbot",
});
// res.answer, res.citations, res.decisionIdWhat the endpoints do
The widget rides on two endpoints added for exactly this use case:
Install from GitHub
Consume @molecule/sdk in another repo straight from GitHub — via GitHub Packages (recommended) or a git dependency — with the scope and token setup that actually works for a monorepo.
Retrieve
Policy-scoped hybrid retrieval — returns ranked, scored chunks the caller is allowed to see.