kai-chat
A full-featured chat UI — thread, composer, and optional header — packaged as a single Shadow DOM web component for any framework.
- 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/elements';
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— each entry:id,role('user' | 'assistant'), an orderedpartsarray (textrendered as markdown for assistant, plusreasoning,tool,card,source,file), and optionalactions,avatar, andfeedback.loading— flip totruewhile awaiting a reply; the input disables and a typing indicator appears.
Message actions
Section titled “Message actions”Add actions: ['copy', 'like', 'dislike'] to a message and the action row is wired for you. copy writes to the clipboard and shows a check; like / dislike mark the vote, hide the other, and toggle off on a second tap. Each raises a toast. See Message for the full behavior.
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”suggestions — clickable prompts above an empty thread; suggestion-mode="fill" populates the input instead of submitting.
Model Switcher and Header
Section titled “Model Switcher and Header”models + chat-title activates the header bar with a model-switcher dropdown.
Context Token Meter
Section titled “Context Token Meter”Add context to show a live token-usage gauge — yellow near the warning threshold, red near danger.
Entity Pills
Section titled “Entity Pills”Set triggers so / inserts a skill pill and @ opens a sectioned menu of agents and plugins. kai-submit carries the structured doc + entities for your backend to expand.
<kai-chat> is a frame: keep the built-in thread and project your own chrome into named slots. An inject slot adds to a region; a replace slot stands in for it (you own that region’s data and events). See Compose your own shell for a worked example.
| 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. |
| 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”Restyle the built-in regions from outside via ::part — no shadow piercing.
| 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). |
| search | boolean | false | Show a Search (Globe) button in the input toolbar; fires a `search` event. |
| value | — | Value of the input. A **string** is controlled (the host owns the text and updates it on `kai-value-change`). A **ComposerDoc** is a one-time seed that pre-populates pills; the user then edits freely. Leave unset for uncontrolled. | |
| placeholder | string | 'Send a message...' | Placeholder text shown in the empty input. |
| loading | boolean | false | When true, shows the loading/streaming state and disables submit (use while awaiting the assistant's reply). |
| suggestions | string[] | — | Starter prompts shown above the input when the thread is empty. Clicking one follows `suggestionMode`. Set as a JS property. |
| suggestionMode | "submit" | "fill" | 'submit' | What clicking a suggestion does: `'submit'` (default) sends it immediately as if typed and submitted; `'fill'` just places it in the input. |
| persistSuggestions | boolean | false | Keep suggestions visible after the conversation starts. By default suggestions are conversation starters and hide once `messages` is non-empty; set this to keep them always shown. Default false. |
| proseSize | "xs" | "sm" | "base" | "lg" | 'sm' | Body/prose font scale for rendered markdown (`'xs' | 'sm' | 'base' | 'lg'`). Defaults to `'sm'`. |
| codeTheme | string | 'github-dark-dimmed' | Shiki theme name for syntax-highlighted code blocks (e.g. `'github-dark-dimmed'`). |
| codeHighlight | boolean | true | Enable Shiki syntax highlighting in code blocks. Turn off to render plain `<pre>` blocks (lighter, no highlighter load). Default true. |
| chatTitle | string | — | Optional header title shown on the left of the header. |
| models | — | Optional model list. When set (>1 model) a ModelSwitcher is shown in the header and a `kai-model-change` event fires on selection. | |
| currentModel | string | — | The currently selected model id (pairs with `models`). |
| context | — | Optional context-window token usage. When set, a Context token meter is shown in the header. | |
| scrollButton | boolean | true | Show the scroll-to-bottom button inside the scroll area. Default true. |
| headerStart | boolean | — | Whether the host has `slot="header-start"` content (left of the title). Set by the `<kai-chat>` facade so a custom control forces the header open. |
| headerEnd | boolean | — | Whether the host has `slot="header-end"` content (right of the controls). |
| headerFull | boolean | — | REPLACE: full custom header in place of the built-in title/model/context bar. |
| sidebar | boolean | — | INJECT: left sidebar column (e.g. a conversation list / your own nav). |
| empty | boolean | — | REPLACE: custom zero-state rendered in the message area while the thread is empty (replaces the empty message list only; the composer and its suggestions still render). |
| composer | boolean | — | REPLACE: full custom composer in place of the built-in prompt input. The projected content wires its own submit (the data-flow boundary). |
| composerActions | boolean | — | INJECT: accessory row just above the composer (e.g. extra actions). |
| footer | boolean | — | INJECT: footer row below the composer (disclaimers, token meter, …). |
| voice | boolean | false | Show a Voice (Mic) button in the input toolbar; fires a `voice` event. |
| triggers | — | Rich entity triggers. Each `{ char, kind, items }` opens a caret-anchored menu that inserts an atomic pill (`/` skills, `@` agents/plugins). Set as a JS property; forwarded to the input. | |
| 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 always visible (`'always'`, default) or only revealed on hover of that message row (`'hover'`). |
| messages | [] | The full message thread to render, newest last. Each entry carries its role, ordered `parts`, and optional actions/avatar/feedback. Set as a JS property (`el.messages = [...]`); a NEW array reference per streaming chunk re-renders (mutating in place does not). Omit for an empty thread. Re-declared here (rather than inherited from `ChatThreadProps`) because the ELEMENT registers a `[]` default and renders the empty state without it, while the SolidJS `<ChatThread>` component still requires it. The facade hands it a validated array either way. Matches `<kai-thread>`. | |
| cardTypes | Record<string, string> | — | Optional card type -> custom-element tag overrides/additions for `card` parts (merged over the built-ins). Property: `el.cardTypes`. Typed as a plain string map (not the `CardTagMap` alias) so the generated React wrapper inlines it instead of emitting an unresolved named type. |
| cardSchemas | Record<string, object> | — | JSON Schemas for the card types this app renders, keyed by envelope type. The companion of `cardTypes`, which says what DRAWS a card while this says what a VALID one looks like. An OBJECT, so it is a JS property only: `el.cardSchemas = { 'pricing-table': pricingSchema }`, never an attribute. `createCardRegistry(...).validationSchemas` is exactly this shape. Without it the kit validates its own seven built-ins and leaves your own card type, the one your app actually cares about, as the only unchecked thing on screen. A schema here WINS over a built-in of the same name. Typed `Record<string, object>` rather than `Record<string, JsonSchema>` deliberately: an imported `.json` schema widens `"type"` to `string`, and an authored one carries `$schema`/`title`/`description`/`additionalProperties`, so the tighter type would reject both of the normal ways to supply one. |
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-message-action | | An action button on a message was clicked. `action` is the built-in name or custom id. `state` is present only for the toggleable feedback votes: `'on'` when a like/dislike is set, `'off'` when re-tapped to clear. |
| kai-model-change | | The header model switcher changed. |
| kai-search | Record<string, never> | The Search button was clicked. |
| kai-submit | | User submitted a message. |
| kai-suggestion-click | | A suggestion chip was clicked (only in `suggestion-mode="fill"`). |
| kai-value-change | | Fired on every input change. |
| kai-voice | Record<string, never> | The Mic / voice 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. |
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.