Skip to content
kitn AI/UI

Artifacts canvas beside chat

A coding-agent layout in the shape of Claude Artifacts or v0: the conversation drives the work on the left, and the right side pairs a <kai-file-tree> with a <kai-artifact> canvas. Pick a file in the tree and it loads into the canvas — HTML pages frame live in the sandboxed preview, every file opens its source on the Code tab.

Three elements compose the shell. <kai-resizable> splits chat from the canvas column; a nested vertical <kai-resizable> stacks the tree above the artifact. Selecting a file in <kai-file-tree> fires kai-select; the handler sets the artifact’s activeFile (and, for files with a hosted url, its src) so the canvas follows the tree.

<kai-resizable orientation="horizontal" style="display:block;height:620px">
<!-- left: the conversation -->
<kai-resizable-item size="42%" min="280px">
<kai-chat id="chat" chat-title="Build agent"></kai-chat>
</kai-resizable-item>
<!-- right: file tree above the artifact canvas -->
<kai-resizable-item min="320px">
<kai-resizable orientation="vertical" style="display:block;height:100%">
<kai-resizable-item size="34%" min="120px" max="60%">
<kai-file-tree id="tree"></kai-file-tree>
</kai-resizable-item>
<kai-resizable-item min="200px">
<kai-artifact
id="canvas"
iframe-title="Project preview"
open-in-tab
sandbox="allow-scripts allow-forms allow-same-origin"
></kai-artifact>
</kai-resizable-item>
</kai-resizable>
</kai-resizable-item>
</kai-resizable>
<script type="module">
import '@kitn.ai/ui/elements'; // registers the custom elements
const tree = document.getElementById('tree');
const canvas = document.getElementById('canvas');
// Each file: { path, url?, code?, language?, type? }. `url` points at a page
// your backend hosts; `code` feeds the Code tab. Folders derive from `/`.
const files = [
{ path: 'index.html', type: 'html', language: 'html', code: '<!DOCTYPE …', url: 'https://your-backend.example/artifacts/abc/index.html' },
{ path: 'about.html', type: 'html', language: 'html', code: '<!DOCTYPE …', url: 'https://your-backend.example/artifacts/abc/about.html' },
{ path: 'styles.css', type: 'other', language: 'css', code: ':root { … }', url: 'https://your-backend.example/artifacts/abc/styles.css' },
{ path: 'src/theme.ts', type: 'other', language: 'ts', code: 'export const theme = …' },
];
tree.files = files; // both take the same array, set in JS
tree.activeFile = 'index.html';
canvas.files = files;
canvas.activeFile = 'index.html';
canvas.src = files[0].url; // the preview frames this URL
// Tree → canvas: load the picked file into the artifact.
tree.addEventListener('kai-select', (e) => {
const file = files.find((f) => f.path === e.detail.path);
tree.activeFile = e.detail.path;
canvas.activeFile = e.detail.path;
if (file.url) { canvas.src = file.url; canvas.tab = 'preview'; }
else { canvas.tab = 'code'; } // code-only file → show the source
});
</script>

<kai-artifact> self-navigates its sandboxed iframe. The back/forward/reload/home toolbar and the address field always track navigations the canvas starts — picking a file, editing the path, home, reload. Tracking in-frame relative-link clicks (clicking “About →” inside the preview) also needs allow-same-origin, because the default allow-scripts allow-forms sandbox gives the framed document an opaque origin the component cannot read. This canvas opts in with sandbox="allow-scripts allow-forms allow-same-origin", so it observes those clicks and keeps the address field and back/forward truthful. Add allow-same-origin only for first-party artifacts you trust — it lets the framed document reach its own origin.