Skip to content
kitn AI/UI

Chat

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

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 ordered parts array, plus optional actions, avatar, and feedback.
  • loading: disables the input and shows a typing indicator while you await a reply.

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? }[]) and currentModel; listen for kai-model-change.
  • Context meter — set context ({ usedTokens, maxTokens, … }) to show a token-usage gauge in the header.

suggestion-mode="fill" populates the input instead of submitting.

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.

SlotModePurpose
injectLeading header controls, left of the title.
injectTrailing header controls.
replaceFull custom header; replaces the built-in title/model/context bar.
injectLeft column (your nav / conversation list). Fixed width; use compose-your-own for resizable.
replaceCustom 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.
replaceCustom 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.
replaceFull custom composer; you own submit + loading, drive the thread via messages.
injectAccessory row above the composer.
injectRow below the composer (disclaimers, token meter).
PartPurposeExample
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.
kai-chat::part(header-bar) { height: 3.5rem; padding-inline: 1rem; gap: 0.5rem }
Full custom header; replaces the built-in title/model/context bar.—
Left column (your nav / conversation list). Fixed width; use compose-your-own for resizable.—
Row below the composer (disclaimers, token meter).—
PropertyTypeDefaultNotes
theme'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'Send a message...'Placeholder text shown in the empty input.
loadingfalseDisables submit and shows the streaming state.
suggestions—Starter prompts shown above the input while the thread is empty.
suggestionMode'submit'What clicking a suggestion does. Default sends it immediately; `'fill'` places it in the input without sending.
persistSuggestionsfalseKeep suggestions visible after the conversation starts; they otherwise hide once `messages` is non-empty. Default false.
proseSize'sm'Body/prose font scale for rendered markdown. Defaults to `'sm'`.
codeTheme'github-dark-dimmed'Shiki theme name for syntax-highlighted code blocks (e.g. `'github-dark-dimmed'`).
imagePreview—How an image tile reveals full size. `'lightbox'` is the only value keyboard and touch can reach. Default `'hover'`.
codeHighlighttrueRenders plain `<pre>` blocks with no highlighter load when false. Default true.
reasoning—How reasoning parts render. Default is the collapsible disclosure.
reasoningOpen—Seeds the reasoning disclosure open and keeps it tracking the stream. Default false; inert unless `reasoning` is `'full'`.
chatTitle—Title shown at the start of the header bar.
models—Model list; more than one renders a switcher in the header.
currentModel—The currently selected model id (pairs with `models`).
context—Token usage, shown as a context meter in the header.
scrollButtontrueShow the scroll-to-bottom button inside the scroll area. Default true.
headerStart—Whether `slot="header-start"` content is projected, which forces the header row open.
headerEnd—Whether the host has `slot="header-end"` content (right of the controls).
headerFull—Replaces the built-in header bar with `slot="header"` content.
homeFull—Replaces the built-in home screen with `slot="home"` content, while the home view shows.
sidebar—Whether `slot="sidebar"` content is projected, which shows the left sidebar column.
empty—Replaces the empty-state message area with `slot="empty"` content. The composer still renders.
composer—Replaces the built-in composer with `slot="composer"` content, which wires its own submit.
composerActions—Whether `slot="composer-actions"` content is projected, which shows the row above the composer.
footer—Whether `slot="footer"` content is projected, which shows the footer row below the composer.
attachtrueHides the built-in paperclip attach button; only an explicit `false` hides it. Default true.
webSearchfalseShow a web-search (Globe) button in the input toolbar; calls `onWebSearch`.
voicefalseShow 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—Default icon per entity kind (kind → image src) for pills/menu items.
actionsReveal'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.
hideSourcesfalseHide the citations row that consecutive `source` parts collapse into; absent or `false` renders it.
accept—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—Card type → custom-element tag overrides/additions, merged over the built-ins. JS property: `el.cardTypes`.
cardSchemas—Card-type JSON Schemas keyed by envelope type; validates each card's `data`. JS property: `el.cardSchemas`.
conversationsfalseTurns 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.
hostOpentrueWhether the chrome hosting this element is visible (e.g. a launcher's open state). JS property only; `false` has no attribute form.
EventDetailNotes
The staged attachments changed (file added or removed). Carries the full current list so a consumer can react in real time.
One or more picked files were refused because `accept` excluded them. Renders no message of its own; only fires when `accept` is set.
A conversation's history loaded. Set `el.messages` from `detail.messages` -- the element does not render it for you.
A `home.links` entry with no `href` was activated (tapped/clicked/Enter). Meaningful only when `home` is set.
An action button on a message was clicked. `action` is the built-in name or a custom id.
The header model switcher changed.
User submitted a message.
A suggestion chip was clicked (only in `suggestion-mode="fill"`).
Whether a conversation OTHER than the one on screen is unread. Mirror it onto a launcher badge (`dock.unread = detail.unread`).
Fired on every input change.
Record<string, never> The Mic / voice button was clicked.
Record<string, never> The web-search (Globe) toolbar button was clicked.
MethodSignatureNotes
(options?: FocusOptions): voidFocus 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.
(): voidBlur 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.
(): voidEmpty 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.
(): voidSubmit 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.
(behavior?: ScrollBehavior): voidScroll the message viewport to the newest message. Defaults to `'smooth'`; pass `'instant'` to jump without animating.
(): voidForce 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.
(): voidStart 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.

This element wraps these SolidJS components — reach for them directly when you need finer control than the props expose.

ChatThread