@darqlabs/curator-sdk-react
v0.1.4
Published
Official React components for Curator — drop-in chat widget and headless hook backed by @darqlabs/curator-sdk.
Maintainers
Readme
@darqlabs/curator-sdk-react
Official React components for Curator. Drop in a ready-to-use chat widget that talks to your deployed agents, or wire up your own UI with the headless hook.
Powered by assistant-ui internals — token-by-token streaming, edit/regenerate primitives, and tool-call extension points are available to consumers who want to go deeper than the drop-in widget.
// once, at the app root
import { Curator } from "@darqlabs/curator-sdk"
import { CuratorProvider } from "@darqlabs/curator-sdk-react"
const curator = new Curator({
apiKey: "CURATOR_API_KEY",
project: "default",
environment: "production",
})
export function App() {
return (
<CuratorProvider client={curator}>
<Routes />
</CuratorProvider>
)
}
// anywhere inside
import { CuratorChat } from "@darqlabs/curator-sdk-react"
export function Support() {
return <CuratorChat agent="support-bot" />
}Install
npm install @darqlabs/curator-sdk @darqlabs/curator-sdk-reactReact 18 or 19. Works with Vite, Next.js, Remix, anything with react and react-dom.
Why pass the client?
The components consume a Curator instance (built from @darqlabs/curator-sdk) rather than an API key. That keeps auth, project / environment defaults, custom fetch, and headers in one place — the same instance can power multiple chats, file uploads, or your own SDK calls.
Wrap your tree once with <CuratorProvider> and components further down read the client from context automatically. Or pass curator explicitly to override (e.g. when one screen needs to talk to a different client).
Drop-in component
<CuratorChat
agent="support-bot"
// optional — overrides the provider's client
curator={otherCurator}
// optional UX
title="Support"
placeholder="Ask anything…"
welcomeMessage="Hi! How can I help?"
// optional locator overrides
project="default"
environment="production"
conversationId={savedConversationId}
// optional message actions (both default off)
enableRetry // ↻ regenerate button under the latest assistant reply
enableCopy // copy button on assistant messages
// optional styling
theme="light" // or "dark"
className="my-chat"
style={{ height: 520 }}
// optional events
onMessage={(msg) => console.log(msg)} // final assistant message (msg.metadata = last state snapshot)
onState={(s) => console.log(s.metadata)} // live structured state, every turn (see below)
onError={(err) => console.error(err)}
/>Live structured state (onState)
If your agent produces a structured result — a booking draft, an extracted form, a plan — it can report that result as it works, so your app renders progress live instead of waiting for the final message. On the agent side this is the reserved __emit_state tool (call it each turn with the complete current object); on the client side it surfaces as onState.
onState fires on every turn the agent reports state (including mid-run, while it's still calling tools), before onMessage. state.metadata is the complete current snapshot — schema-agnostic; validate/shape it in your app.
function BookingCopilot() {
const [draft, setDraft] = useState(null)
return (
<>
<CuratorChat
agent="booking-copilot"
onState={(s) => setDraft(s.metadata?.bookingDraft)} // updates as the agent fills it in
/>
<BookingPreview draft={draft} />
</>
)
}The same snapshot also arrives on the final message as msg.metadata via onMessage, so a non-streaming consumer still gets the last state without handling onState.
Message actions
| Prop | Default | Effect |
| --- | --- | --- |
| enableRetry | false | Shows a ↻ button under the latest assistant message that re-sends the preceding user turn. |
| enableCopy | false | Shows a copy button on assistant messages (with a brief check-mark confirmation). |
Theming
Two built-in palettes via the theme prop ("light" / "dark"). For finer control, override the design-token CSS variables through the style prop:
<CuratorChat
agent="support-bot"
style={{
["--curator-accent"]: "#2563eb", // agent bubble + send button background
["--curator-accent-text"]: "#ffffff", // agent bubble + send button text/icon
["--curator-user-bubble"]: "#f1f5f9", // user bubble background
["--curator-bg"]: "#fafafa", // chat background
["--curator-text"]: "#18181b", // primary text
["--curator-muted"]: "#71717a", // action icons, timestamps, hints
["--curator-border"]: "rgba(0,0,0,0.08)",
["--curator-error"]: "#b91c1c",
["--curator-radius"]: "16px", // container corner radius
} as React.CSSProperties}
/>| Token | Controls |
| --- | --- |
| --curator-accent / --curator-accent-text | Agent (assistant) bubble and the send button |
| --curator-user-bubble | User bubble background (text uses --curator-text) |
| --curator-bg / --curator-surface | Chat background / composer + input background |
| --curator-muted | Action icons, welcome text, timestamps |
| --curator-border, --curator-error, --curator-radius | Borders, error text, container radius |
Use the style prop (not a CSS class or an ancestor rule): the widget sets these variables inline on its own root for the active theme, and inline styles win over a class — the style prop is applied last, so it's the one override that reliably takes effect. The widget ships no external stylesheet.
Floating launcher
For a chat-bubble-in-the-corner widget instead of an inline panel, use <CuratorChatLauncher />. Same props as <CuratorChat /> plus placement controls:
import { CuratorChatLauncher } from "@darqlabs/curator-sdk-react"
<CuratorChatLauncher
agent="support-bot"
placement="bottom-right" // or bottom-left / top-right / top-left
offset={24} // px from the docked corner
panelWidth={380}
panelHeight={560}
defaultOpen={false}
// all <CuratorChat /> props are forwarded:
title="Support"
theme="light"
welcomeMessage="Hi! How can I help?"
/>Renders via a portal to document.body, so it escapes stacking contexts in the host app. ESC closes the panel; clicking outside doesn't (intentional — keeps chat state from getting lost on a stray click).
Headless hook
For full UI control, use useCuratorChat directly. curator is optional when a <CuratorProvider> is in scope:
import { useCuratorChat } from "@darqlabs/curator-sdk-react"
function MyChat() {
const { messages, send, sending, error, conversationId } = useCuratorChat({
agent: "support-bot",
})
return (
<div>
{messages.map((m) =>
m.kind === "user" ? <UserBubble key={m.localId} text={m.content} />
: m.kind === "assistant" ? <BotBubble key={m.runId} text={m.content} />
: <ErrorRow key={m.localId} text={m.message} />,
)}
{sending && <Typing />}
<Composer onSubmit={(text) => send(text)} />
</div>
)
}The hook returns:
| | |
| --- | --- |
| messages | Chronological list of bubbles (user / assistant / error). |
| send(content, opts?) | Append a user message and await the assistant's reply. opts.files for attachments, opts.timeoutMs to override the wait. |
| sending | True while a send is in flight. |
| error | Most recent error, or null. Cleared on next successful send. |
| chat | Underlying SDK Chat instance. |
| conversationId | Conversation id once one has been created. |
| reset() | Clear visible messages (keeps the same conversation). |
Reading the client directly
import { useCurator } from "@darqlabs/curator-sdk-react"
function UploadButton() {
const curator = useCurator()
return (
<button onClick={async () => {
const fileRef = await curator.files.upload({ filename, mimeType, data })
// …
}}>
Upload
</button>
)
}Throws a clear error if no <CuratorProvider> is mounted higher in the tree.
Errors
The hook captures all SDK errors into a friendly kind: "error" bubble — auth, agent failures, timeouts, approval-required pauses, etc. — and also exposes the raw error via error and the onError prop so you can log or surface them however you like.
Security in the browser
The chat runs wherever your Curator client runs. If you instantiate the client in browser code, the API key ships in your bundle. Recommended patterns:
- Backend proxy. Run the SDK on your server; have your frontend call your server. Your server holds the key.
- Trusted internal tools. Employee dashboards behind SSO are usually fine.
- Server-issued tokens. Not supported yet — reach out if you need them.
