Skip to content

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.