Skip to content
mordn
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.