Three elements give you the full workspace: the <kai-workspace> layout shell, a resizable, collapsible <kai-conversations> rail listing past conversations, and a streaming <kai-chat> thread. Select a conversation, start a new one, or send a message below.
How it works
Section titled “How it works”<kai-workspace> is the layout: it arranges its slots, handles resize and collapse, and nothing else. You slot <kai-conversations> into start and <kai-chat> into the main region, then drive each part directly. The rail gets a flat conversations array (add groups when you want section headers), the chat gets the active thread’s messages, and three events drive all the interactivity:
<kai-workspace collapse-below="720" drawer-below="640" style="display:block; height:100vh;"> <kai-conversations slot="start"></kai-conversations> <kai-chat></kai-chat></kai-workspace>
<script type="module"> import '@kitn.ai/ui/elements';
const rail = document.querySelector('kai-conversations'); const chat = document.querySelector('kai-chat');
// Seed the rail (assign in JavaScript — arrays can't be attributes). rail.conversations = [ { id: 'c1', title: 'Migrating the dashboard to web components', scope: { type: 'collection' }, messageCount: 4, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, // … more conversations ]; rail.activeId = 'c1';
// Seed the active thread. chat.messages = [ { id: 'u1', role: 'user', parts: [{ type: 'text', text: 'How do I share state across the panels?' }] }, { id: 'a1', role: 'assistant', parts: [{ type: 'text', text: 'Keep one source of truth and reassign it on each change …' }], actions: ['copy', 'like', 'dislike'], }, ];
// Swap the thread when the user picks a conversation. rail.addEventListener('kai-conversation-select', (e) => { rail.activeId = e.detail.id; chat.messages = threadFor(e.detail.id); // load from your store });
// Clear the thread and start fresh. rail.addEventListener('kai-new-chat', () => { chat.messages = []; rail.activeId = undefined; });
// Stream a reply — identical pattern to standalone <kai-chat>. chat.addEventListener('kai-submit', async (e) => { const { value } = e.detail; const aId = crypto.randomUUID(); chat.messages = [ ...chat.messages, { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: value }] }, { id: aId, role: 'assistant', parts: [] }, ]; chat.loading = true; let answer = ''; for await (const token of streamFromYourModel(value)) { answer += token; chat.messages = chat.messages.map((m) => m.id === aId ? { ...m, parts: [{ type: 'text', text: answer }] } : m, ); } chat.loading = false; });</script>Key data shapes:
conversations(on<kai-conversations>) — each entry needsid,title,scope({ type: 'collection' }),messageCount,lastMessageAt, andupdatedAt. The rail uses the timestamps to bucket rows into “Today”, “Yesterday”, “Last 7 days”, etc.messages(on<kai-chat>) — the standardChatMessage[]shape:id,role, an orderedpartsarray (text,reasoning,tool,card,source,file), plus optionalactions.
Sidebar layout:
The shell owns the rail’s geometry. Widths are CSS custom properties on <kai-workspace>, not props:
| Custom property | Default | Effect |
|---|---|---|
--kai-workspace-start-width | 280px | Initial rail width |
--kai-workspace-start-min-width | 200px | Minimum rail width |
--kai-workspace-start-max-width | 480px | Maximum rail width |
The user can drag the handle to resize (the shell fires kai-aside-resize) or collapse the rail. Start collapsed with the default-start-collapsed attribute, and listen to kai-aside-toggle (detail.side, detail.collapsed) on the shell to persist the state.
Next steps
Section titled “Next steps”- Drop-in chat — the simpler single-thread starting point and streaming loop.
- Use a workspace — the full composition guide, including the migration table from the old single-element chat preset.
kai-workspacereference — every slot, prop, and event on the layout shell.kai-conversationsreference — the rail element, usable in any layout you build.- Theming — brand the sidebar (
--color-sidebar) and chat thread in one pass.