AI UX extras
These client affordances make the assistant easier to enter, continue, and trust. They do not move model policy or identity into the browser.
Starter prompts
<ChatWidget
apiBase="/api/chat"
config={{
schemaVersion: 1,
runtime: { model: 'anthropic/claude-sonnet-4-5' },
client: {
starterPrompts: [
{ title: 'Summarize my open tickets', subtitle: 'Across all projects' },
{ title: 'Draft a reply to the latest thread' },
],
display: { starterPromptsLayout: 'grid' },
},
}}
/>Use getStarterPrompts() for route-, record-, or role-dependent suggestions. A
non-empty runtime result wins over starterPrompts; errors and empty results
fall back to the static list. capabilitiesPrompt adds a final "what can you do?"
onramp.
Follow-up chips
Follow-ups are generated server-side only:
createChatHandler({
getUserId: getChatUserId,
model,
store,
followUps: true, // or { max: 3, timeoutMs: 5_000 }
});After the main answer settles, the handler makes a small structured second call
with the same resolved model and appends the suggestions as a data-follow-ups
part. They persist with the message, so a history reload does not regenerate
them, and the call is included in the turn's usage totals.
The client renders whatever it is served — there is no client-side switch, count, or generator. Both settings live in one place because both describe the same list, and a second copy could only disagree with the first:
// hosted config — the only place follow-ups are configured
runtime: { followUps: { enabled: true, max: 3 } }Note
ChatWidget's followUps prop and the FollowUpConfig type were removed in
0.17.0, along with the client generate(messages) fallback. Turning chips off
belongs in runtime.followUps.enabled — doing it client-side would pay for the
generation and then discard the result.
Action-result cards
Use actionRenderers to derive visible status from real tool output rather than
model prose.
const actionRenderers = {
createTicket: (part: ToolPartLike): ActionResult | null => {
if (part.state !== 'output-available') return null;
const output = part.output as { ok: boolean; url?: string };
return {
status: output.ok ? 'success' : 'error',
title: output.ok ? 'Ticket created' : 'Ticket creation failed',
link: output.url ? { label: 'View ticket', href: output.url } : undefined,
};
},
};Rendering precedence is toolRenderers, then actionRenderers, then the default
tool row.
Panel persistence and imperative control
Uncontrolled popup state persists under the scope the server returns from
bootstrap. Set persistState: false in config.client to disable it.
Controlled mode, inline, and page layouts do not use built-in persistence.
const ref = useRef<ChatWidgetHandle>(null);
<button onClick={() => ref.current?.open()}>Need help?</button>
<ChatWidget
ref={ref}
apiBase="/api/chat"
config={{ schemaVersion: 1, client: { allowAutoReopen: true } }}
/>Opening after explicit dismissal requires allowAutoReopen; closing is always
allowed.
Human-in-the-loop approval
AI SDK tool parts can move through approval-requested,
approval-responded, output-available, or output-denied. The widget renders
Approve/Deny for pending approvals and resumes the turn automatically once all
pending approvals on the last assistant message are answered. Derive final cards
from actual state/output, including denial and partial failure.