Artifact panel The conversation stays about the work and the panel holds the work. A generated document gets its own version pager, its own actions and its own scroll, while the transcript keeps a short reference to what was produced. npx shadcn@latest add resizable button npm i lucide-react Tokens this needs: --foreground, --background, --card, --card-foreground, --accent, --accent-foreground, --muted, --muted-foreground, --border, --input, --ring, --chart-1 ──────────────────────────────────────────────────────────────────────── // ArtifactPanel.tsx ──────────────────────────────────────────────────────────────────────── import { useState, type ReactNode } from "react"; import { Copy, Download, Play, Eye, Globe, FileText } from "lucide-react"; import { Button } from "@/components/ui/button"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; export type Artifact = { title: string; /** Document, code or page. The actions follow the type. */ kind: "document" | "code" | "page"; /** Every version, oldest first. Six near-identical drafts in a transcript * have to be diffed by eye; here they page. */ versions: string[]; }; /** Copy and download for a document, run and preview for code, publish for * a page. A panel with the wrong verbs is a panel nobody uses. */ const ACTIONS = { document: [{ label: "copy", Icon: Copy }, { label: "download", Icon: Download }], code: [{ label: "run", Icon: Play }, { label: "preview", Icon: Eye }], page: [{ label: "publish", Icon: Globe }], } as const; /** The transcript keeps a short pointer, so the conversation stays about the * work and stays navigable. The work itself lives in the panel. */ export function ArtifactReference({ artifact, version, onOpen }: { artifact: Artifact; version: number; onOpen: () => void }) { return ( ); } export function ArtifactPanel({ artifact, onAction, onEdit, defaultVersion }: { artifact: Artifact; /** Which version opens first. Defaults to the newest. */ defaultVersion?: number; onAction: (label: string, version: number) => void; /** Fires on every edit made in the panel. The model has to see this next * turn; a panel whose contents diverge from what the model holds is the * worst failure here. */ onEdit: (version: number, text: string) => void; }) { const n = artifact.versions.length; const [v, setV] = useState(defaultVersion ?? n); const [dirty, setDirty] = useState(false); return (

{artifact.title}

v{v} of {n}
{ACTIONS[artifact.kind].map(({ label, Icon }) => ( ))}
{/* The panel has its own scroll, and its body is the editable thing. */}
{ setDirty(true); onEdit(v, e.currentTarget.innerText); }} className="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap p-3 text-sm leading-relaxed text-card-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring" > {artifact.versions[v - 1]}
{dirty && (

edited here, and the model sees it next turn

)}
); } /** Transcript on the left, work on the right, and the reader sets the split. */ export function ArtifactLayout({ transcript, panel }: { transcript: ReactNode; panel: ReactNode }) { return ( {transcript} {panel} ); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { ArtifactLayout, ArtifactPanel, ArtifactReference, type Artifact } from "./ArtifactPanel"; const PLANS = "Starter: three seats, unlimited projects, community support. Free.\nTeam: £12 a seat a month, shared workspaces, priority support, SSO.\nEnterprise: custom seat pricing, audit log, dedicated support, invoicing."; const CLOSE = "Every plan includes unlimited viewers, and you can move between plans at any time without losing work."; /** Five drafts of the same page. v3 is the one the transcript points at. */ const draft: Artifact = { title: "Pricing page draft", kind: "document", versions: [ `Our pricing is designed to grow with you. Whether you are a solo founder or a global enterprise, we have a plan that fits your needs and your budget, with transparent pricing and no hidden fees.\n\n${PLANS}`, `Pricing that grows with you. One plan per team size, metered by seats, with the first three seats free so you can try it with real colleagues before you pay anything.\n\n${PLANS}\n\n${CLOSE}`, `Pay for what you use. One plan, metered by seats, with the first three free.\n\n${PLANS}\n\n${CLOSE}`, `Pay for what you use. Seats, not tiers.\n\n${PLANS}\n\n${CLOSE}`, `Seats, not tiers. The first three are free.\n\n${PLANS}\n\n${CLOSE}`, ], }; export default function Demo() { const [open, setOpen] = useState(true); return (

make the intro shorter

Cut it to two sentences and moved the plan comparison up. v3 is in the panel.

setOpen(true)} />
} panel={ open ? (
{}} onEdit={() => {}} />
) : null } /> ); }