Artifact Preview
A file the agent generated or found, previewed in the chat, with download and open.
≈ 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
srcandcontentcome 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:
csvfor comma-separated data,jsonfor JSON,codefor source files withlanguageset. - Keep
summaryto 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
supersededByon the old call. Whensrcis a signed URL that expires, setexpiresAtto its expiry.
How it works
- Header: the name and one meta line (
PDF · 4 pages · 212 KB · v2). The size is computed fromcontentwhensizeBytesis 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.
| Kind | Preview |
|---|---|
pdf | The 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 |
image | The image on a plain background, with a Fit / 100% zoom toggle |
code, text | Monospace 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 |
csv | A 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 |
json | A 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 |
markdown | Rendered headings, paragraphs, lists, code, quotes, bold, italic and links, plus a Raw view |
other | A 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 adownloadattribute, or builds the file fromcontent. Actions that can't work are hidden: Open without an http(s) or blob: URL. A host can passlinkto 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
srcmust be an http(s) URL, a blob: URL from an http(s) page, or a data: URL whose MIME type matches the kind (for exampleimage/pngfor an image,text/csvfor a CSV). Ajavascript: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
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
| Prop | Type | Notes |
|---|---|---|
name | string | File name with extension |
kind | "pdf" | "image" | "code" | "text" | "csv" | "json" | "markdown" | "other" | Picks the preview |
src | string? | Data-bound. http(s), blob:, or a data: URL of a matching MIME type |
content | string? | Data-bound. Inline text for code, text, csv, json and markdown, up to 200 KB |
language | string? | For code: a label only, e.g. SQL |
sizeBytes | number? | Data-bound. Computed from content when omitted |
pages | number? | Data-bound. For pdf |
version | string? | 2 shows as v2 |
summary | string? | 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.