Skip to content
kitn AI/UI

Workspace app

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.

<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 needs id, title, scope ({ type: 'collection' }), messageCount, lastMessageAt, and updatedAt. The rail uses the timestamps to bucket rows into “Today”, “Yesterday”, “Last 7 days”, etc.
  • messages (on <kai-chat>) — the standard ChatMessage[] shape: id, role, an ordered parts array (text, reasoning, tool, card, source, file), plus optional actions.

Sidebar layout:

The shell owns the rail’s geometry. Widths are CSS custom properties on <kai-workspace>, not props:

Custom propertyDefaultEffect
--kai-workspace-start-width280pxInitial rail width
--kai-workspace-start-min-width200pxMinimum rail width
--kai-workspace-start-max-width480pxMaximum 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.

  • 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-workspace reference — every slot, prop, and event on the layout shell.
  • kai-conversations reference — the rail element, usable in any layout you build.
  • Theming — brand the sidebar (--color-sidebar) and chat thread in one pass.