Skip to content
kitn AI/UI

Open an artifact from a message

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.

<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.

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:

chat.messages = [
{
id: 'm2',
role: 'assistant',
content: "Here's a landing page for Starboard…",
// the generated file, shown as a chip in the message
attachments: [
{ id: 'starboard-html', type: 'file', filename: 'index.html', title: 'Starboard landing page' },
],
// 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:

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:

<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.

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.