Skip to content
KONIGI Patterns

AI Assistants / Answer formats / Artifact panel

1 of 4 ›

Artifact panel

People need room to read and revise generated documents without losing the conversation.

Updated September 12, 2026

Problem

Long documents and code are hard to work with inside a chat. Each revision adds another copy, making it difficult to follow the conversation and find the latest version.

Solution

Show documents, code, and other reusable output in a separate panel beside the conversation. Give the panel its own scroll area so people can read the output while keeping their place in the chat.

Keep revisions together, with the latest version displayed and earlier versions available to review or restore. This saves people from searching through messages for a previous draft.

Use the panel for content people will edit or use elsewhere. Keep short answers in the conversation. Let people move content into or out of the panel when the automatic choice doesn’t suit their task.

Include actions that fit the content, such as downloading a document or previewing a page. If people can edit in the panel, make sure the AI uses those edits in its next response.

Use when

The AI produces a document, code, or other content that people will keep, revise, or use elsewhere.

Don’t use when

The answer is an answer. Explanations, comparisons and short code fragments belong in the conversation, and moving them out adds a click and removes them from the context the reader is holding.

Trade-offs

A side panel leaves less room for the conversation. People may need to look back and forth to connect an explanation with the content it describes.

Revisions can also make earlier messages confusing if they refer to content that has changed. Keep previous versions accessible.

On small screens, people may need to switch between the conversation and the panel. Preserve their place in both views.

Checklist

  • What promotes output into the panel, and can the viewer override it in both directions?
  • Are previous versions reachable, and is the current one labelled?
  • If the viewer edits in the panel, does the model see the edit next turn?
  • What actions does the panel carry, and do they match the output’s type?
  • Does the transcript keep a readable reference to what was produced?
  • What happens on a phone?
  • Can the artifact be exported or shared on its own?
  • Is content still streaming into the panel visible without switching to it?
  • What happens when one conversation produces several different artifacts?
  • Does closing the panel destroy anything?

Compare

Claude opens a side panel for documents and code and keeps a version history on it, so the conversation stays a conversation while the output accumulates revisions in one place. ChatGPT offers a canvas that turns the output into a directly editable surface with inline controls, moving the panel from a viewer toward an editor. Gemini routes long output into Google Docs instead. The container problem goes away because the artifact lands in a product built to hold one. v0 inverts the arrangement entirely and makes the preview the main surface with the conversation as a narrow rail. When every turn produces a new version of one thing, that’s the right call.

Streaming response shows output as it is generated, including inside a panel. Code block actions provide controls for code within a chat message. Response collapse keeps long messages out of the way when a separate panel isn’t needed.

Assistant sidebar places the conversation beside an existing workspace. Message turn structures the chat messages that refer to the panel’s content.

Artifact panel anatomy A conversation on the left and a generated document in its own panel on the right, carrying a version pager, its own actions, and its own scroll, with the transcript keeping a short reference to what was produced. The conversation stays about the work; the panel holds the work make the intro shorter Pricing page draft · v3 Pricing page draft ‹ v3 of 5 › copy · download edited here, and the model sees it next turn 1 2 3 4 1 A REFERENCE, NOT A COPY The transcript keeps a short pointer so the conversation stays navigable. 2 VERSIONS Six near-identical drafts in a transcript have to be diffed by eye. Here they page. 3 ACTIONS THE TYPE NEEDS Copy and download for a document, run and preview for code, publish for a page. 4 EDITS HAVE TO PROPAGATE A panel whose contents diverge from what the model holds is the worst failure here. Promotion is the hard part. A two-line snippet exiled to a panel costs a click to read three words.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

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.

shadcn
npx shadcn@latest add resizable button
npm
lucide-react
Tokens
--foreground--background--card--card-foreground--accent--accent-foreground--muted--muted-foreground--border--input--ring--chart-1

make the intro shorter

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

Pricing page draft

v3 of 5
Pay for what you use. One plan, metered by seats, with the first three free. Starter: three seats, unlimited projects, community support. Free. Team: £12 a seat a month, shared workspaces, priority support, SSO. Enterprise: custom seat pricing, audit log, dedicated support, invoicing. Every plan includes unlimited viewers, and you can move between plans at any time without losing work.

ArtifactPanel.tsxThe reference chip, the panel with its pager, type-specific actions and an editable body that reports every edit, and the resizable layout.

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 (
    <Button variant="outline" size="sm" className="w-full justify-start font-normal" onClick={onOpen}>
      <FileText className="size-4" /> {artifact.title} · v{version}
    </Button>
  );
}

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 (
    <div className="flex h-full min-w-0 flex-col overflow-hidden rounded-lg border bg-muted">
      <div className="border-b px-3 py-2">
        <p className="text-sm text-card-foreground">{artifact.title}</p>
        <div className="mt-1 flex flex-wrap items-center gap-2">
        <div className="flex items-center gap-1 text-xs">
          <Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setV(v - 1)} disabled={v === 1} aria-label="previous version">‹</Button>
          <span className="tabular-nums text-chart-1">v{v} of {n}</span>
          <Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setV(v + 1)} disabled={v === n} aria-label="next version">›</Button>
        </div>
        <div className="ml-auto flex gap-1">
          {ACTIONS[artifact.kind].map(({ label, Icon }) => (
            <Button key={label} variant="ghost" size="sm" className="h-7 text-xs" onClick={() => onAction(label, v)}>
              <Icon className="size-3.5" /> {label}
            </Button>
          ))}
        </div>
        </div>
      </div>

      {/* The panel has its own scroll, and its body is the editable thing. */}
      <div
        contentEditable
        suppressContentEditableWarning
        onInput={(e) => { 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]}
      </div>

      {dirty && (
        <p className="border-t px-3 py-1.5 text-xs text-muted-foreground">edited here, and the model sees it next turn</p>
      )}
    </div>
  );
}

/** Transcript on the left, work on the right, and the reader sets the split. */
export function ArtifactLayout({ transcript, panel }: { transcript: ReactNode; panel: ReactNode }) {
  return (
    <ResizablePanelGroup direction="horizontal">
      <ResizablePanel defaultSize={45} minSize={30}>{transcript}</ResizablePanel>
      <ResizableHandle />
      <ResizablePanel defaultSize={55} minSize={35}>{panel}</ResizablePanel>
    </ResizablePanelGroup>
  );
}

demo.tsxHow it is called: five drafts, v3 open, the transcript holding only the pointer. Drag the split.

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 (
    <div className="h-[420px] overflow-hidden rounded-lg border bg-card">
      <ArtifactLayout
        transcript={
          <div className="h-full p-4">
            <div className="flex justify-end">
              <p className="rounded-xl bg-muted px-3 py-1.5 text-sm text-foreground">make the intro shorter</p>
            </div>
            <p className="mt-4 text-sm leading-relaxed text-card-foreground">Cut it to two sentences and moved the plan comparison up. v3 is in the panel.</p>
            <div className="mt-4">
              <ArtifactReference artifact={draft} version={3} onOpen={() => setOpen(true)} />
            </div>
          </div>
        }
        panel={
          open ? (
            <div className="h-full p-4">
              <ArtifactPanel artifact={draft} defaultVersion={3} onAction={() => {}} onEdit={() => {}} />
            </div>
          ) : null
        }
      />
    </div>
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

No screenshots yet for this pattern. Browse Products to see other examples.