AI UX extras
Starter prompts, server-first follow-ups, action-result cards, panel persistence, and tool approval.
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 } }
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.