# Open an artifact from a message

Reveal a kai-artifact canvas on demand from an assistant message — the v0 / Lovable / Replit model, where your backend serves the generated output and the canvas frames it live.

When the assistant generates a runnable result — a web page, an app, a component — your backend serves it at a URL and `<kai-artifact>` frames it live. You don't have to show it the moment it's ready: keep the conversation full-width, reference the generated file in the message, and let the reader open the preview when they want it. This is the v0 / Lovable / Replit model — generate, serve, then frame the running result in a side panel on demand.

<p>`<kai-artifact>` frames a URL your backend serves (a preview sandbox, a dev server, object storage) — it doesn't render files inline. Wire the reveal action to point the canvas at that URL.</p>

## How it works

The reveal runs on a custom message action, not a special element. Give the assistant message an action descriptor and (optionally) attach the generated file so it reads as a real artifact:

```js
chat.messages = [
  {
    id: 'm2',
    role: 'assistant',
    parts: [
      // the generated file, shown as a chip in the message (a `file` part)
      { type: 'file', attachment: { id: 'starboard-html', type: 'file', filename: 'index.html', title: 'Starboard landing page' } },
      { type: 'text', text: "Here's a landing page for Starboard…" },
    ],
    // a custom action button that opens the canvas. Omit `icon` for a
    // label-only text button, or pass a curated icon name (e.g. 'link').
    actions: [
      { id: 'open-preview', label: 'Preview' },
      'copy',
    ],
  },
];
```

`kai-chat` fires `kai-message-action` with `{ messageId, action }` when the button is clicked. Match your action `id` and reveal the artifact:

```js
const artifact = document.getElementById('artifact');

chat.addEventListener('kai-message-action', (e) => {
  if (e.detail.action !== 'open-preview') return;
  artifact.files = projectFiles;       // source for the Code tab
  artifact.src = '/artifacts/starboard/index.html'; // your hosted preview
  panel.hidden = false;                // reveal the side panel
});
```

Lay the chat and the canvas side by side with `<kai-resizable>`, and keep the artifact panel hidden until it's needed. Toggling the panel's `<kai-resizable-item hidden>` lets the chat flex to full width when it's closed:

```html
<kai-resizable orientation="horizontal" style="display:block;height:100%">
  <kai-resizable-item>
    <kai-chat id="chat"></kai-chat>
  </kai-resizable-item>

  <!-- starts hidden; revealed by the open-preview action -->
  <kai-resizable-item id="panel" size="54%" min="360px" hidden>
    <kai-artifact id="artifact" iframe-title="Preview"></kai-artifact>
  </kai-resizable-item>
</kai-resizable>
```

Add a close control in the panel that flips `hidden` back on, and the conversation returns to full width — the artifact's state is preserved for the next open.

## Why an action, not a clickable chip

Attachment chips render the file in the message, but `kai-chat` doesn't emit a click event for them — they're a visual reference. The clickable trigger is the action button, which fires `kai-message-action`. Pairing the two gives you the familiar artifact card: the file is named in the message, and a clear control opens it.

## Next steps

- **[Artifacts canvas](/examples/artifacts-canvas/)** — the always-on version: a file tree and canvas docked beside the chat.
- **[`kai-artifact` reference](/components/artifact/)** — the Preview/Code toggle, sandbox flags, and navigation toolbar.
- **[`kai-message` reference](/components/message/)** — custom actions, attachments, and the `kai-message-action` event.
- **[Resizable split](/patterns/resizable-split/)** — sizing and dragging the two panes.
