@waniwani/sdk/chat. Pick based on who runs the chat backend:
The React components mount inline: they render wherever you place them in your layout, and you control the container. There is no launcher, floating bubble, or overlay panel in the React path. Those live in the embed script, as
floating mode (a docked bar) and composer mode (an in-page input that opens the panel on send). To put the React chat in a sidebar or drawer of your own that opens from your buttons, see In a sidebar or drawer.
Platform feature. Requires a Waniwani project token (
wwp_...). Get one at app.waniwani.ai or read more about the Platform.Install
_app.tsx or your root layout):
WaniwaniChat
The hosted chat. Pass your project token and channel ID and it talks toapp.waniwani.ai directly — no API route, no proxy, no backend work on your side.
overrides.appearance.
Sizing and placement
WaniwaniChat fills its parent. Put it wherever you want it and size the container:
className is applied to the root element if you’d rather style it directly.
Per-page overrides
The dashboard is the source of truth. Useoverrides only for a local tweak that doesn’t justify a separate channel:
Control the chat from your page
Pass aref to drive the chat from anywhere else on the page: a call to action in your hero, a link in your navigation, a pricing row that asks about itself. The ref holds a ChatHandle.
Calls made before the chat has loaded its channel config are dropped, so wire the handle to a visitor’s click rather than calling it during the first render. On a page that the dashboard’s per-URL rules hide, the component renders nothing and the chat methods do nothing.
In a sidebar or drawer
The React handle has noopen() or close(), because WaniwaniChat is on screen wherever you mount it. A sidebar, drawer or modal around it is your own component, and its open state lives in your code. A button anywhere on the page flips that state, and can send a first message through the ref in the same click:
open is true would mount it on the very click that sends the question, before it has loaded, and that first message would be dropped. Keeping it mounted also means the visitor finds their conversation where they left it when they reopen the sidebar.
If you’d rather use the Waniwani panel than build your own, load the embed script in floating or composer mode and call window.WaniWani.chat.open() from your React components. See Control the chat from your page.
Tracking from the host page
The sameChatHandle carries track and identify. identify takes two positional arguments, the user id first and optional traits second:
WaniwaniChat only — the bare ChatEmbed primitive has no Waniwani credential to send events with. See Identify.
The page the visitor is on
From 0.24.1,WaniwaniChat sends the address of the current page with each message, and your agent is told which page the visitor is on. There is nothing to set up. The page the visitor is on shows what the agent gets and how to tell it what your pages are.
ChatEmbed
A bare-bones primitive for when you run the chat backend yourself.api is required and there is no default; pass any endpoint implementing the Vercel AI SDK chat protocol and the component handles the rest.
Reach for
ChatEmbed only when self-hosting. If you’re using the Waniwani Platform, WaniwaniChat gives you the same UI plus dashboard configuration, visitor correlation, and tracking.Link visitors to your analytics
Both components accept avisitorId prop to override the anonymous id the widget generates with one you already track (a PostHog / Amplitude / Segment distinct id, your own cookie). Waniwani correlates its sessions and events to the same visitor your analytics sees, and your server-side MCP tools and flows read it back as context.waniwani.visitorId. Leave it unset to keep the auto-generated, localStorage-persisted id.
visitorId accepts a string, or a resolver that returns one. The resolver may be sync or async, which is handy when your analytics SDK only exposes its id after it bootstraps:
Attachments
ChatEmbed takes the documents module’s state as a prop, since it fetches no remote config of its own:
enabled is true, and the upload endpoint enforces the same flag. WaniwaniChat and the script embed read all of this from the dashboard, so they need none of it. See Documents for what an agent does with the files.
Authentication
headers goes out with every chat request, and document uploads reuse it, so one bearer usually covers both.
uploadHeaders when your upload endpoint takes a different credential than your chat endpoint. It is merged over headers on the document upload and discard calls, and leaves the chat request alone.
X-Tenant-Id. Only the names you repeat are replaced, and the match ignores case, so authorization overrides Authorization.
Both land in the browser, so put only public credentials in them.
Theming
Both components take an appearance preset (light, dark, or auto) with optional per-property overrides layered on top:
auto follows the host’s prefers-color-scheme. variables accepts every ChatTheme token — colors, message-bubble radius/padding/max-width, base font size and line height. The full token and CSS-variable list lives on Theming & customization.
Deep customization
Two escape hatches when tokens aren’t enough:appearance.assistantBubble— opt-in filled bubble for assistant replies (plain text by default), styled byassistantBubbleColor/assistantBubbleTextColor.classNames— per-slot class overrides merged onto the widget’s own classes:root,header,message,userBubble,assistantBubble,input. Pass it directly onChatEmbed, or asoverrides.classNamesonWaniwaniChat.
Mirroring events into your analytics
The hostedWaniwaniChat component accepts an onEvent prop that fires on chat lifecycle events (messages, session start, errors, link clicks) so you can mirror widget activity into your own analytics — Segment, Amplitude, gtag — with the page’s identity attached automatically. ChatEmbed, the bring-your-own-backend primitive, does not expose it. See Widget events (onEvent).