Embed the widget
Voice, leads & persistence
The widget is more than a chat box. It captures leads inline, transcribes voice, persists across reloads, and shows a live "human is here" state when an operator takes over. This page covers each feature and how to configure it.
Conversation persistence
Every visitor gets an anon_id (a random string written to
localStorage on first visit). On reload, the widget calls
/v1/widget/init with the same anon_id and the
server resumes the most recent conversation if it's less than 24 hours
old.
The init response includes the last 30 messages so the chat log
rehydrates in the order the visitor left it. The "Clear conversation"
button (in the widget header) writes a cleared_at timestamp
on the conversation row — past messages stay in the database for
analytics + lead linkage but stop being shown to the visitor.
Voice mic
A microphone button in the input area lets visitors dictate instead of
type. The widget uses the browser's built-in
SpeechRecognition API — no server-side speech model. While
recording the mic shows a sonar-ring animation; clicking again stops
and inserts the transcript into the input box (appended if there's
already text there, so you can dictate, edit, then dictate more).
Browsers without SpeechRecognition (Firefox, older Safari)
don't show the mic button. There's no fallback — the feature is
opportunistic.
Lead capture
The widget can collect contact info inline without forcing the visitor away from the conversation. Lead capture fires when:
- A behavior rule of
kind=lead_capturematches. - The visitor explicitly asks to be contacted ("can someone call me?", "email me a quote").
- You wire a CTA button to the
lead_captureaction.
The form fields are configured per agent. The default is name + email;
you can add phone, company, and custom fields. Submitted leads are
POSTed to /v1/widget/leads (rate-limited per JWT) and
appear immediately in /app/inbox (the per-workspace lead list).
Smart capture from intent
The latest update (commit 9190aa5) adds intent-based capture:
the widget watches the conversation for phrases that suggest a real
sales intent — pricing questions, "is this right for…", "can I demo…" —
and offers the lead form proactively after a few turns. Threshold and
phrase list are tunable per agent.
Human takeover
When a workspace operator claims a conversation in /app/inbox,
the widget receives a Reverb event (conversation.takeover)
and updates the chat header to show a "Human is here" badge. From that
point, the AI stays paused — every visitor message goes to the operator,
every operator reply streams to the visitor. The visitor sees one
continuous thread; under the hood the message role flips
from assistant to human-agent and back.
See Inbox & human takeover for the operator side.
Citations
Whenever the agent answers from retrieved sources, citation chips appear
below the message. Click one to open the source URL in a new tab. The
chips are numbered ([1], [2]) matching inline
references in the response text — visitors who care can verify the
answer; visitors who don't see a clean reply.
Curated answers can include an optional citation URL too — useful when the canned answer is sourced from a specific page.
Streaming
Messages stream token-by-token over Server-Sent Events. The widget
reads the stream and appends tokens to the DOM in real time. If the
stream errors mid-flight (network blip, LLM timeout), the widget
auto-retries up to 3 times before showing an error state — and only
one user bubble appears even on retries (commit a576e1c).
Branding
The widget footer shows a "Powered by Pitchbar" link by default. It's
hidden for workspaces on a plan with the remove_branding
feature flag enabled — see Billing &
plans.
The brand label, URL, and logo all come from platform-admin
configuration (config('branding.*') + the optional
app_settings singleton overrides), so a self-hosted
deployment can rebrand the footer entirely.
Storage
The widget uses localStorage for:
anon_id— persistent visitor identifier.- Conversation cleared-state (which message IDs the visitor has hidden via "Clear").
No personally identifiable data is stored client-side. The JWT itself lives in memory — it's re-issued on every init.