Artifact Preview

A file the agent generated or found, previewed in the chat, with download and open.

Pattern

≈ 1,160 tokens per use (tool 557 · guidance 245 · typical payload 358)How token costs are estimated

Example: Artifact Preview

Write the SQL for the monthly revenue report.

monthly-revenue-report.sql

SQL43 lines1 KBv2

Monthly revenue by region, net of refunds.

-- Monthly revenue report-- Revenue, orders and average order value by month and region,-- with month-over-month change. Refunds are netted out. WITH paid_orders AS (  SELECT    o.id,    o.region,    DATE_TRUNC('month', o.paid_at) AS month,    o.total_cents - COALESCE(r.refunded_cents, 0) AS net_cents  FROM orders o  LEFT JOIN (    SELECT order_id, SUM(amount_cents) AS refunded_cents    FROM refunds    GROUP BY order_id  ) r ON r.order_id = o.id  WHERE o.status = 'paid'    AND o.paid_at >= DATE_TRUNC('month', CURRENT_DATE) - INTERVAL '12 months'), monthly AS (  SELECT    month,    region,    COUNT(*) AS orders,    SUM(net_cents) / 100.0 AS revenue  FROM paid_orders  GROUP BY month, region) SELECT  month,  region,  orders,  ROUND(revenue, 2) AS revenue,  ROUND(revenue / NULLIF(orders, 0), 2) AS avg_order_value,  ROUND(    100.0 * (revenue - LAG(revenue) OVER w) / NULLIF(LAG(revenue) OVER w, 0),    1  ) AS revenue_change_pctFROM monthlyWINDOW w AS (PARTITION BY region ORDER BY month)ORDER BY month DESC, revenue DESC;

When to use

  • The agent generated or found a file the user will inspect, keep or share: a PDF, an image, a script, a CSV export, a JSON response, a Markdown doc.
  • The user asked to export or download something.

When not to use

  • A short snippet answers the question. Put it in a code block in the reply.
  • The data is for reading in the chat, not a file. Use a Data Table or an Article.
  • The agent doesn't have the file's real content or URL. It must never invent either.

How the agent should use it

  • src and content come from a tool result. Never write a file's content from memory as if it were the real file.
  • Send the whole content. Every line and up to 200 CSV rows are shown, with the preview scrolling inside its frame.
  • Pick the kind that matches the file: csv for comma-separated data, json for JSON, code for source files with language set.
  • Keep summary to one line and don't repeat the file name. It's read to screen readers (and is the image's alt text), not shown: say what the file is in the reply.
  • Lifecycle: a file is a snapshot, so it usually needs none. When a newer version replaces it, set supersededBy on the old call. When src is a signed URL that expires, set expiresAt to its expiry.

How it works

  • Header: the name and one meta line (PDF · 4 pages · 212 KB · v2). The size is computed from content when sizeBytes is omitted. When the file has no view switch (code, text, PDF), its icon buttons (wrap, expand) sit at the right of the meta line rather than on a row of their own.
  • View switch: CSV, JSON, Markdown and images get a toolbar row with a full-width segmented control (Table / Raw, Tree / Raw, Preview / Raw, Fit / 100%), with the icon buttons after it.
  • Preview: capped at about 480px with its own scroll. Any edge with more content past it fades: the sides into the preview's own surface, the top and bottom into the card (black in dark mode, white in light), so more lines above or below read without a scrollbar. The top fade starts under a CSV table's sticky header. Expand (an icon button) opens the same view in a native <dialog> (Escape or Close returns focus to the Expand button). View settings, column order and sort carry over between the two.
  • Code surface: code, raw views and the JSON tree sit on their own grey surface (a clear grey on dark cards, a faint one on light), with the line-number gutter matching it.
KindPreview
pdfThe browser's built-in viewer in an <iframe>, for http(s) and blob: URLs when navigator.pdfViewerEnabled is true. Otherwise, and always for data: PDFs (many browsers block them in frames), a "No inline preview" placeholder; Open and Download stay in the footer
imageThe image on a plain background, with a Fit / 100% zoom toggle
code, textMonospace with line numbers (hidden from screen readers) and a wrap toggle (icon button). SQL (by language or a .sql name) is coloured; other code stays plain. Every line is shown
csvA table of the first 200 rows with a sticky header, plus a Raw view. The row count is in the meta line; "Showing 200 of 1,240 rows" appears only when rows are cut. Numeric columns are right-aligned, with digit grouping (121250 → 121,250). Click a column label to sort (ascending, descending, off; numbers by value, text with numbers inside in natural order, empty cells last). Drag a column header left or right to move it, or press Alt with ← or → on its label; a screen reader hears where it went
jsonA collapsible tree, open to depth 2, with a toggle per node and an Expand all / Collapse all icon button, plus a pretty-printed Raw view. Keys, strings, numbers and true/false/null are coloured in both. Invalid JSON shows the error with its line and column and marks that line
markdownRendered headings, paragraphs, lists, code, quotes, bold, italic and links, plus a Raw view
otherA neutral "No preview" placeholder; the type and size are in the meta line
  • Footer actions (a closed set, in a fixed order): Open (an icon button; a new tab) and Download. Every file whose text is in the call also gets Copy (hollow, before Download), which shows "Copied" for a moment and reports { action: "copy" }. Download uses the file's URL with a download attribute, or builds the file from content. Actions that can't work are hidden: Open without an http(s) or blob: URL. A host can pass link to put its own main action in the footer instead of Download, such as a link to a details page.
  • Syntax colours follow editor conventions: keywords and JSON keys blue, functions teal, strings green, numbers orange, true/false/null rose, comments and punctuation muted. Each is a darker step on light cards and a lighter one on dark, at least 4.5:1 against the code surface.
  • Streaming: the header fills in first. The preview stays a skeleton until the call finishes, so a half-received CSV or JSON is never parsed, and the actions are disabled until then.
  • It never locks. Copy, download and open can be used any number of times.

Safety

  • src must be an http(s) URL, a blob: URL from an http(s) page, or a data: URL whose MIME type matches the kind (for example image/png for an image, text/csv for a CSV). A javascript: URL, a relative path or a data: URL of another type is ignored.
  • Open is limited to http(s) and blob: URLs; data: URLs are never opened as pages.
  • Markdown is parsed by a small tokenizer and rendered as React elements, never injected as HTML. Only http(s) links become links; anything else renders as its text. Markdown images show their alt text and are never loaded.
  • The JSON parser builds its own tree, so keys like __proto__ are plain data.
  • External links open in a new tab with rel="noopener noreferrer nofollow". Download file names are stripped of paths and reserved characters.

Installation

npx shadcn@latest add https://your-site.com/r/artifact-preview.json

Usage

app/api/chat/route.ts
import { tool } from "ai"
import { artifactPreviewTool } from "@/components/genui/artifact-preview.schema"

const tools = {
  [artifactPreviewTool.name]: tool({
    description: artifactPreviewTool.description,
    inputSchema: artifactPreviewTool.inputSchema,
  }),
}

Nothing goes back to the agent, so give the tool an execute that returns immediately. onResult reports the file actions, for analytics:

<ArtifactPreview
  {...part.input}
  status={part.state === "input-streaming" ? "streaming" : "ready"}
  onResult={(r) => track("artifact_action", r)} // { action: "download" | "open" }
/>

With LangGraph

Push it from a graph node with ui.push({ name: "show_artifact", props: { toolCallId, ...args } }, { message }), and render the thread's UI messages with the Renderer.

Restoring from history

The preview never locks, so there is nothing to restore: render it from its stored input.

Schema

PropTypeNotes
namestringFile name with extension
kind"pdf" | "image" | "code" | "text" | "csv" | "json" | "markdown" | "other"Picks the preview
srcstring?Data-bound. http(s), blob:, or a data: URL of a matching MIME type
contentstring?Data-bound. Inline text for code, text, csv, json and markdown, up to 200 KB
languagestring?For code: a label only, e.g. SQL
sizeBytesnumber?Data-bound. Computed from content when omitted
pagesnumber?Data-bound. For pdf
versionstring?2 shows as v2
summarystring?One line. Read by screen readers and used as the image's alt text; not shown
actions("download" | "open")[]Default: download for text kinds, open and download otherwise

Host props: status (streaming or ready), onResult, link ({ label, href }, replaces Download), result and className.

Source

On this page