kai-chat
Ships the thread, the prompt box, and a header already wired, so a chat surface needs no assembly.
- Shadow DOM
- 8 events
- Markdown + syntax highlighting
- Model switcher
- Entity pills (/ skills, @ agents)
- Starter suggestions
Preview
Section titled “Preview”Set rich data (arrays, objects) in JavaScript; scalar props work as attributes or properties:
<kai-chat id="chat" style="display:block; height:100vh;"></kai-chat>
<script type="module"> import '@kitn.ai/ui/web-components';
await customElements.whenDefined('kai-chat');
const chat = document.getElementById('chat');
chat.messages = [ { id: '1', role: 'user', parts: [{ type: 'text', text: 'How do I center a div?' }] }, { id: '2', role: 'assistant', parts: [{ type: 'text', text: 'Use `display: grid; place-items: center;`' }], actions: ['copy', 'like', 'dislike'] }, ];
chat.addEventListener('kai-submit', (e) => { console.log('user sent:', e.detail.value, 'attachments:', e.detail.attachments); }); chat.addEventListener('kai-message-action', (e) => { console.log(e.detail.messageId, e.detail.action); });</script>messages:id,role('user' | 'assistant'), an orderedpartsarray, plus optionalactions,avatar, andfeedback.loading: disables the input and shows a typing indicator while you await a reply.
Message actions
Section titled “Message actions”Add actions: ['copy', 'like', 'dislike'] and the action row wires itself: copy writes to the clipboard and shows a check, a vote hides the other side and toggles off on a second tap, and each raises a toast. See Message.
kai-message-action reports votes with a state field — 'on' when set, 'off' when cleared:
chat.addEventListener('kai-message-action', (e) => { const { messageId, action, state } = e.detail; if ((action === 'like' || action === 'dislike') && state === 'on') { saveVote(messageId, action); }});Set feedback: 'like' | 'dislike' on a message to re-hydrate a persisted vote; it wins over the element’s optimistic state.
- Model switcher — set
models({ id, name, provider? }[]) andcurrentModel; listen forkai-model-change. - Context meter — set
context({ usedTokens, maxTokens, … }) to show a token-usage gauge in the header.
Examples
Section titled “Examples”Basic Thread
Section titled “Basic Thread”Loading State
Section titled “Loading State”Starter Suggestions
Section titled “Starter Suggestions”suggestion-mode="fill" populates the input instead of submitting.
Model Switcher and Header
Section titled “Model Switcher and Header”Context Token Meter
Section titled “Context Token Meter”Entity Pills
Section titled “Entity Pills”triggers turns / into a skill pill and @ into a sectioned menu of agents and plugins; kai-submit carries the structured doc + entities.
A frame: keep the built-in thread and project your own chrome into named slots. inject adds to a region, replace stands in for it, and you own that region’s data and events. See Compose your own shell.
| Slot | Mode | Purpose |
|---|---|---|
| header-start | inject | Leading header controls, left of the title. |
| header-end | inject | Trailing header controls. |
header· ::part | replace | Full custom header; replaces the built-in title/model/context bar. |
sidebar· ::part | inject | Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable. |
| home | replace | Custom home-tab content in place of the built-in home screen (greeting, recent-conversation card, links). Rendered only while the home view is showing, so it needs the `home` property set; the tab bar and navigation stay built in. |
| empty | replace | Custom zero-state rendered in the message area while the thread is empty. Replaces the empty message list only; the composer and any suggestions still render. |
| composer | replace | Full custom composer; you own submit + loading, drive the thread via messages. |
| composer-actions | inject | Accessory row above the composer. |
footer· ::part | inject | Row below the composer (disclaimers, token meter). |
Styling
Section titled “Styling”| Part | Purpose | Example |
|---|---|---|
| header-bar | The built-in header bar (the title / model-switcher / context row that hosts the header-start/header-end inject slots). Restyle its height, padding, or gap from outside without replacing the whole header via the `header` slot. | |
| header | Full custom header; replaces the built-in title/model/context bar. | — |
| sidebar | Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable. | — |
| footer | Row below the composer (disclaimers, token meter). | — |
| Property | Type | Default | Notes |
|---|---|---|---|
| theme | "light" | "dark" | "auto" | 'auto' | Color mode (`auto` follows prefers-color-scheme). |
| value | — | Value of the input: a string is controlled, a `ComposerDoc` is a one-time seed that pre-populates pills, unset is uncontrolled. | |
| placeholder | string | 'Send a message...' | Placeholder text shown in the empty input. |
| loading | boolean | false | Disables submit and shows the streaming state. |
| suggestions | string[] | — | Starter prompts shown above the input while the thread is empty. |
| suggestionMode | "submit" | "fill" | 'submit' | What clicking a suggestion does. Default sends it immediately; `'fill'` places it in the input without sending. |
| persistSuggestions | boolean | false | Keep suggestions visible after the conversation starts; they otherwise hide once `messages` is non-empty. Default false. |
| proseSize | "xs" | "sm" | "base" | "lg" | 'sm' | Body/prose font scale for rendered markdown. Defaults to `'sm'`. |
| codeTheme | string | 'github-dark-dimmed' | Shiki theme name for syntax-highlighted code blocks (e.g. `'github-dark-dimmed'`). |
| imagePreview | "hover" | "lightbox" | — | How an image tile reveals full size. `'lightbox'` is the only value keyboard and touch can reach. Default `'hover'`. |
| codeHighlight | boolean | true | Renders plain `<pre>` blocks with no highlighter load when false. Default true. |
| reasoning | "full" | "compact" | "off" | — | How reasoning parts render. Default is the collapsible disclosure. |
| reasoningOpen | boolean | — | Seeds the reasoning disclosure open and keeps it tracking the stream. Default false; inert unless `reasoning` is `'full'`. |
| chatTitle | string | — | Title shown at the start of the header bar. |
| models | — | Model list; more than one renders a switcher in the header. | |
| currentModel | string | — | The currently selected model id (pairs with `models`). |
| context | — | Token usage, shown as a context meter in the header. | |
| scrollButton | boolean | true | Show the scroll-to-bottom button inside the scroll area. Default true. |
| headerStart | boolean | — | Whether `slot="header-start"` content is projected, which forces the header row open. |
| headerEnd | boolean | — | Whether the host has `slot="header-end"` content (right of the controls). |
| headerFull | boolean | — | Replaces the built-in header bar with `slot="header"` content. |
| homeFull | boolean | — | Replaces the built-in home screen with `slot="home"` content, while the home view shows. |
| sidebar | boolean | — | Whether `slot="sidebar"` content is projected, which shows the left sidebar column. |
| empty | boolean | — | Replaces the empty-state message area with `slot="empty"` content. The composer still renders. |
| composer | boolean | — | Replaces the built-in composer with `slot="composer"` content, which wires its own submit. |
| composerActions | boolean | — | Whether `slot="composer-actions"` content is projected, which shows the row above the composer. |
| footer | boolean | — | Whether `slot="footer"` content is projected, which shows the footer row below the composer. |
| attach | boolean | true | Hides the built-in paperclip attach button; only an explicit `false` hides it. Default true. |
| webSearch | boolean | false | Show a web-search (Globe) button in the input toolbar; calls `onWebSearch`. |
| voice | boolean | false | Show a voice-input button in the input toolbar; calls `onVoice`. |
| triggers | — | Rich entity triggers. Each opens a menu at the caret that inserts an atomic pill. | |
| kindIcons | Record<string, string> | — | Default icon per entity kind (kind → image src) for pills/menu items. |
| actionsReveal | "always" | "hover" | 'always' | Whether each message's action bar is visible at rest or revealed on pointer-over. Visible at rest by default. |
| userActions | — | Default action bar for user messages that have no `actions` of their own; a message's own `actions` replaces it. | |
| assistantActions | — | Default action bar for assistant messages, as `userActions` is for user ones. | |
| hideSources | boolean | false | Hide the citations row that consecutive `source` parts collapse into; absent or `false` renders it. |
| accept | string | — | Which attachment media types the user may stage, in HTML `accept` syntax. Omitted = no filter; media types only, an extension THROWS. |
| messages | [] | The message thread to render, newest last. JS property; pass a NEW array per streaming chunk. Omit for an empty thread. | |
| cardTypes | Record<string, string> | — | Card type → custom-element tag overrides/additions, merged over the built-ins. JS property: `el.cardTypes`. |
| cardSchemas | Record<string, object> | — | Card-type JSON Schemas keyed by envelope type; validates each card's `data`. JS property: `el.cardSchemas`. |
| conversations | boolean | false | Turns on the prior-conversations list. Requires `store`; default `false`; a load arrives as `kai-conversation-load` -- set `el.messages` yourself. |
| store | — | The persistence adapter: `{ list, load, save }`. JS property only (`el.store = myAdapter`). | |
| home | — | Turns on the Home screen (greeting, recent conversation, links, Home/Messages tabs). JS property; omit for the chat-only widget. | |
| hostOpen | boolean | true | Whether the chrome hosting this element is visible (e.g. a launcher's open state). JS property only; `false` has no attribute form. |
Events
Section titled “Events”| Event | Detail | Notes |
|---|---|---|
| kai-attachments-change | | The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time. |
| kai-attachments-rejected | | One or more picked files were refused because `accept` excluded them. Renders no message of its own; only fires when `accept` is set. |
| kai-conversation-load | | A conversation's history loaded. Set `el.messages` from `detail.messages` -- the element does not render it for you. |
| kai-home-link | | A `home.links` entry with no `href` was activated (tapped/clicked/Enter). Meaningful only when `home` is set. |
| kai-message-action | | An action button on a message was clicked. `action` is the built-in name or a custom id. |
| kai-model-change | | The header model switcher changed. |
| kai-submit | | User submitted a message. |
| kai-suggestion-click | | A suggestion chip was clicked (only in `suggestion-mode="fill"`). |
| kai-unread-change | | Whether a conversation OTHER than the one on screen is unread. Mirror it onto a launcher badge (`dock.unread = detail.unread`). |
| kai-value-change | | Fired on every input change. |
| kai-voice | Record<string, never> | The Mic / voice button was clicked. |
| kai-web-search | Record<string, never> | The web-search (Globe) toolbar button was clicked. |
Methods
Section titled “Methods”| Method | Signature | Notes |
|---|---|---|
| focus | (options?: FocusOptions): void | Focus the composer, meaning the contenteditable (or textarea) inside the shadow root. A native `focus()` on the host lands on the host itself and never reaches it, so this is the only way to focus the input programmatically. |
| blur | (): void | Blur whatever currently holds focus inside the shadow root. The companion to `focus()`, for the same reason: a native `blur()` on the host misses the real focus target. |
| clear | (): void | Empty the COMPOSER: drops the draft text and every staged attachment, then fires `kai-value-change` with `''`. It does NOT touch the thread. `messages` is the consumer's own state, so clearing history stays the consumer's call. |
| send | (): void | Submit whatever the composer currently holds, on the same path as Enter or the send button: fires `kai-submit` with that value plus the staged attachments, then drops the attachments. It takes no argument, so to send text the user never typed, set `el.value` first. There is no empty-check, so an empty composer still fires. The draft is cleared afterwards only when `value` is uncontrolled; a controlled host owns its value and clears it itself. Named `send`, not `submit`, to match the shared vocabulary. |
| scrollToBottom | (behavior?: ScrollBehavior): void | Scroll the message viewport to the newest message. Defaults to `'smooth'`; pass `'instant'` to jump without animating. |
| closeConversationsList | (): void | Force the widget back to its default landing view: `'home'` when the `home` property is set, `'chat'` otherwise (a no-op if already there, or if neither `home` nor `conversations` is on). This element has no knowledge of whatever chrome hosts it, so it cannot know when that host closes; a composed launcher/dock calls this on every hide so the NEXT open lands on the default screen rather than wherever the conversations list was left. `kai-dock`'s `kai-open-change` fires on every close path (header X, launcher toggle, Escape), so one listener covers all three. |
| startNewConversation | (): void | Start a fresh conversation, on the same path as the list view's "+ New conversation" row: clears the active conversation id, returns to the chat view, and delivers `[]` through `kai-conversation-load` (set `el.messages = event.detail.messages` like every other load; this element never updates `messages` for you). The seam a composed app's own "New conversation" control drives; the construct shell palette's entry rides the same controller call). No id is minted until the first message, so calling this on an already-empty new conversation is a harmless no-op. |
Composed from
Section titled “Composed from”This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.