Use the chat app
<kai-chat> is the batteries-included element: a message thread with markdown, reasoning, and tool calls, a composer, optional header chrome, and a streaming-ready render loop. You feed it data and listen for events; it handles the UI. Send a message in the demo below — the reply streams in.
This guide assumes you’ve installed the package and registered the elements. If not, do the Getting Started quickstart first, then come back here to go deeper.
Drop it in
Section titled “Drop it in”<kai-chat> works in two motions you’ll repeat all over this page: set rich data on a JavaScript property, and listen for kai-* events. Render the element, seed the thread, and handle a submit.
<kai-chat id="chat" style="display:block; height:100dvh;"></kai-chat>
<script type="module"> import '@kitn.ai/ui/elements';
await customElements.whenDefined('kai-chat');
const chat = document.getElementById('chat');
// Seed the thread. Arrays can't be attributes, so this lives in JavaScript. chat.messages = [ { id: '1', role: 'assistant', content: 'Hello! How can I help?' }, ];
chat.addEventListener('kai-submit', (e) => { const userMessage = { id: crypto.randomUUID(), role: 'user', content: e.detail.value }; chat.messages = [...chat.messages, userMessage]; // Call your model here, then append an assistant message. });</script>A few things that pay off everywhere:
messagesis an array of{ id, role, content }. Every entry needs a stableid(it keys the render and message actions);roleis'user'or'assistant'. Optional fields —reasoning,tools,attachments,actions,avatar— light up the matching UI when present.- Assign a new array to re-render. Pushing into the existing one won’t trigger an update.
kai-submitcarries the input one.detail.valueand any staged files one.detail.attachments.- Give the element an explicit height. It fills its block and owns its own scrolling.
Stream a reply
Section titled “Stream a reply”<kai-chat> is transport-agnostic — it renders whatever you hand it, so it doesn’t care whether tokens come from your own backend, a proxy, or a mock. The pattern: append an empty assistant message, then replace it with a fresh object on every chunk.
chat.addEventListener('kai-submit', async (e) => { const prompt = e.detail.value.trim(); if (!prompt) return;
// 1. Show the user's turn and an empty assistant placeholder. const assistantId = crypto.randomUUID(); chat.messages = [ ...chat.messages, { id: crypto.randomUUID(), role: 'user', content: prompt }, { id: assistantId, role: 'assistant', content: '' }, ]; chat.loading = true;
// 2. Stream from your endpoint and grow the assistant message. const res = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message: prompt }), }); const reader = res.body.getReader(); const decoder = new TextDecoder(); let answer = '';
while (true) { const { value, done } = await reader.read(); if (done) break; answer += decoder.decode(value, { stream: true });
// New object per chunk — mutating in place won't re-render. chat.messages = chat.messages.map((m) => m.id === assistantId ? { ...m, content: answer } : m, ); }
chat.loading = false;});loading toggles the streaming indicator and disables submit while the reply is in flight. Set it back to false when the stream ends. For a full backend recipe — parsing SSE, error handling, the abort path — see the streaming recipe.
Offer models
Section titled “Offer models”Pass a models array and <kai-chat> shows a switcher in its header. Track the selection with currentModel and update it from kai-model-change.
chat.models = [ { id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' }, { id: 'claude-sonnet', name: 'Claude Sonnet', provider: 'Anthropic' },];chat.currentModel = 'gpt-4o';
chat.addEventListener('kai-model-change', (e) => { chat.currentModel = e.detail.modelId; // Route your next request to e.detail.modelId.});Each model is { id, name, provider? } — id is what you get back on kai-model-change, name is the label, and provider groups the menu. The switcher only appears when there’s more than one model.
Starter suggestions and the empty state
Section titled “Starter suggestions and the empty state”An empty thread reads as a dead end. Hand the user a few suggestions and <kai-chat> renders them as starter chips in its own empty state, then hides them once the conversation begins.
chat.messages = [];chat.suggestions = [ 'What can you help me with?', 'Show me a streaming example', 'Theme the components to match my brand',];Clicking a chip submits it immediately by default, firing kai-submit like a typed message — so the streaming loop above just works. To prefill the input instead and let the user edit first, set suggestionMode = 'fill' and read the chosen text from kai-suggestion-click. For a custom greeting with chips of your own, see the empty-state pattern.
The events you’ll wire
Section titled “The events you’ll wire”Every interaction surfaces as a non-bubbling kai-* CustomEvent. Listen on the element itself and read the payload off e.detail.
kai-submit— the user sent a message.detail: { value, attachments }. This is the one you always handle.kai-model-change— the header switcher changed.detail: { modelId }.kai-suggestion-click— a starter chip was chosen in'fill'mode (in'submit'mode you getkai-submitinstead).detail: { value }.kai-message-action— a message action button was clicked.detail: { messageId, action, state? }, whereactionis a built-in (copy,like,dislike,regenerate,edit) or your custom id;stateis'on'/'off'for the like/dislike toggles. Show actions by adding anactionsarray to a message.kai-value-change— fires on every keystroke; useful for draft persistence or a controlled input.kai-search/kai-voice— the toolbar Search and Mic buttons, shown when you set thesearchandvoiceprops. Each fires with an empty detail; you own what happens next.
chat.addEventListener('kai-message-action', (e) => { const { messageId, action } = e.detail; if (action === 'regenerate') regenerate(messageId);});In React
Section titled “In React”The element is identical under React; the wrapper just gives you typed props and on* handlers.
import { Chat } from '@kitn.ai/ui/react';import { useState } from 'react';
export function App() { const [messages, setMessages] = useState([ { id: '1', role: 'assistant', content: 'Hello! How can I help?' }, ]);
return ( <Chat messages={messages} style={{ display: 'block', height: '100dvh' }} onSubmit={(e) => { const turn = { id: crypto.randomUUID(), role: 'user', content: e.detail.value }; setMessages((prev) => [...prev, turn]); // …call your model, then append an assistant message. }} /> );}For state that’s tedious to thread by hand — the message list, the streaming loop, optimistic turns — the useKaiChat hook manages it for you.
What’s next
Section titled “What’s next”- Compose your own shell — when one element isn’t enough control, swap in slots and assemble the thread, header, and composer yourself.
- State & hooks —
useKaiChat/createKaiChatfor managing the conversation without wiringmessagesby hand. kai-chatreference — every prop, event, and slot in one place.