Dashboardblocks
Components

AI Assistant

A chat panel docked beside a page, a conversation with code and feedback, questions about a usage card with the cost of each answer, and a failed reply with retry.

The assistant works with any model and any API. Messages are plain objects with an id, a role of user or assistant, and content text, and status is ready, submitted, streaming or error: the same shapes as the AI SDK's useChat. Replies are formatted safely, with no HTML, for paragraphs, lists, bold, code and fenced code blocks. The log follows a reply as it streams unless you scroll up, and screen readers hear the whole reply once it's done, not every word. The examples stream canned replies; see Usage to connect your own.

Side Panel

An assistant docked beside the page, with suggested prompts to start from, the page as context, and new chat and close buttons. On narrow screens it covers the page.

Conversation

A conversation with a list and a SQL query. Copy a reply or its code, rate it, regenerate the last one, or pick a follow-up question.

Analytics assistant
Ask questions about your product analytics
You:

Why did checkout conversion drop last week?

Assistant:

Checkout conversion fell from 3.8% to 3.1% between Sep 15 and Sep 21. Three things changed that week:

  • Safari 17 payment errors. 41% of failed checkouts were on Safari 17, starting with the Sep 15 release.
  • Longer shipping estimates. Standard delivery showed 5–7 days instead of 3–5 for EU addresses.
  • More paid social traffic. Sessions from paid social doubled, and they convert at 1.2%.

The Safari errors explain most of the drop. Fixing them should win back about half a point.

You:

Can you give me the SQL to check the Safari errors?

Assistant:

This counts started and failed checkouts by browser for that week, from the events table:

sql
select
  browser,
  count(*) filter (where name = 'checkout_started') as started,
  count(*) filter (where name = 'checkout_failed') as failed,
  round(100.0 * count(*) filter (where name = 'checkout_failed')
    / nullif(count(*) filter (where name = 'checkout_started'), 0), 1) as failure_rate
from events
where occurred_at >= '2026-09-15' and occurred_at < '2026-09-22'
group by browser
order by failed desc;

If Safari's failure_rate is well above the other browsers, the release is the likely cause.

Ask About Data

Questions about an AI Usage card, with the tokens and cost of each answer.

Token usage
All models, this month
Tokens
305.5M
Cost
$998
Cache hits
30%
  • Input 197M
  • Cached 83M
  • Output 25.5M
You:

What drove the cost this month?

Assistant:

Output tokens: they're 8% of the tokens but 38% of the cost, at five times the input price. Most come from the support summaries feature, which writes about 900 tokens per ticket.

Capping summaries at 400 tokens would save roughly $150 a month.

2K in · 96 out · $0.0037

Press Enter to send and Shift+Enter for a new line.

Retry

A reply that failed, kept in the conversation with a button to try again.

Ops assistant
Answers from incidents, deploys and runbooks
You:

Is checkout healthy right now?

Assistant:

Yes. The checkout error rate is 0.3% over the last hour, under the 1% alert threshold, and p95 latency is 420 ms. The last deploy, web@4.18.2, went out at 09:12 UTC with no new errors.

You:

Summarise last night’s incidents

Usage

The examples run on useChatStream, a small hook in the primitives. Give it onSend, a function that streams the reply to the conversation so far as chunks of text, and it keeps the messages and status, and handles stop, regenerate and new chat. Stop aborts the signal.

async function* onSend(messages: ChatMessageData[], signal: AbortSignal) {
  const response = await fetch('/api/chat', {
    body: JSON.stringify({ messages }),
    method: 'POST',
    signal,
  })
  const reader = response.body!.pipeThrough(new TextDecoderStream()).getReader()
  while (true) {
    const { done, value } = await reader.read()
    if (done) return
    yield value
  }
}

const chat = useChatStream({ onSend })

With the AI SDK, use useChat instead and pass its values to the same primitives. Its messages keep text in parts, so join those into content:

const { messages, regenerate, sendMessage, status, stop } = useChat()

const chatMessages = messages.flatMap((message) =>
  message.role === 'system'
    ? []
    : [
        {
          content: message.parts.map((part) => (part.type === 'text' ? part.text : '')).join(''),
          id: message.id,
          role: message.role,
        },
      ],
)

<ChatComposer status={status} onSubmit={(text) => sendMessage({ text })} onStop={stop} />

On this page