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

code
<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:

code
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:

code
// 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.

code
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.

code
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.