State helpers & hooks
The elements are controlled: you own the data, pass it in, listen for events. To make
that ergonomic, @kitn.ai/ui/state ships immutable helpers and a streaming handle, and
each framework gets a batteries-included store that owns the state for you.
The setter contract
Section titled “The setter contract”Everything drives one shape — a functional-updater setter, (prev) => next. React’s
setState, a Solid signal setter, and one line for plain HTML all satisfy it:
const set = (fn) => { el.messages = fn(el.messages ?? []); };The helpers and the streaming handle accept any setter that conforms to this shape, so they work the same way regardless of which framework is in play.
Helpers (@kitn.ai/ui/state)
Section titled “Helpers (@kitn.ai/ui/state)”Six pure functions for common mutations. Each takes the current array and returns a new one:
import { appendMessage, upsertMessage, updateMessage, removeMessage, appendText, addSuggestion, removeSuggestion,} from '@kitn.ai/ui/state';
// Append a user turnsetMessages((m) => appendMessage(m, { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text }] }));
// Patch a field on an existing message (e.g. correct the text after an edit)setMessages((m) => updateMessage(m, id, { parts: [{ type: 'text', text: corrected }] }));
// Insert-or-replace by id (useful for streaming placeholders)setMessages((m) => upsertMessage(m, { id, role: 'assistant', parts: [{ type: 'text', text: partial }] }));
// Append a delta onto an existing message's trailing text part (streaming primitive)setMessages((m) => appendText(m, id, delta));ChatMessage is exported from @kitn.ai/ui (and re-exported from @kitn.ai/ui/react) if
you need the type:
import type { ChatMessage } from '@kitn.ai/ui';Streaming (createAssistantStream)
Section titled “Streaming (createAssistantStream)”createAssistantStream inserts an assistant placeholder and returns a handle with methods that
grow it in place. Pass your setter; it does the rest.
import { createAssistantStream } from '@kitn.ai/ui/state';
const s = createAssistantStream(setMessages);
for await (const part of backend(prompt)) { s.appendText(part);}
s.done(); // marks the message finishedThe full handle:
| Method | Purpose |
|---|---|
appendText(chunk) | Append a text delta to the trailing text part |
appendReasoning(chunk, opts?) | Append a reasoning delta (opts can set label, provider index, or signature) |
upsertTool(toolCallId, patch) | Insert a tool part (first call) or merge patch onto its ToolPart (later calls), keyed by toolCallId |
addCard(envelope) | Insert a generative-UI card part (first call) or replace it in place on later calls, keyed by envelope.id |
addSource(source) | Append a citation part |
addFile(attachment) | Append a file/attachment part |
done() | Mark the message complete |
abort(reason?) | Mark the message aborted; flips any in-flight tool parts to output-error |
Re-send a card with the same envelope.id to revise it in place — a card can fill in as the model works instead of stacking duplicates.
You can pass an optional init object (a Partial<ChatMessage>) as the second argument to
seed the message before the stream starts — for example a stable id you generated up front so
you can correlate it with your backend request:
const s = createAssistantStream(setMessages, { id: requestId });React: useKaiChat
Section titled “React: useKaiChat”useKaiChat is a React hook (from @kitn.ai/ui/react) that owns the message list, wires the
submit handler, and returns a bind object you spread directly onto <Chat>:
import { Chat, useKaiChat } from '@kitn.ai/ui/react';
function App() { const chat = useKaiChat({ async onSubmit({ value }) { chat.append({ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: value }] }); const s = chat.streamAssistant(); for await (const part of backend(value)) s.appendText(part); s.done(); }, });
return <Chat {...chat.bind} />;}chat.bind carries messages, onSubmit (the element event handler), and any other props
the hook manages — spread it and you’re done. If you need to read or set the message list
directly, chat.messages and chat.setMessages are also exposed.
Solid: createKaiChat
Section titled “Solid: createKaiChat”For SolidJS, createKaiChat (from @kitn.ai/ui) returns the same shape. Because the
element is a web component in a Solid template, you spread chat.bind as JSX props and
attach the submit handler separately:
import { createKaiChat } from '@kitn.ai/ui';
const chat = createKaiChat({ async onSubmit({ value }) { chat.append({ id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: value }] }); const s = chat.streamAssistant(); for await (const part of backend(value)) s.appendText(part); s.done(); },});
<kai-chat {...chat.bind} on:kai-submit={chat.handleSubmit} />;chat.handleSubmit is a stable reference to the event handler — pass it directly to the
on:kai-submit prop rather than an inline arrow so Solid’s event delegation can clean it up.
Multiple chats per page
Section titled “Multiple chats per page”Each useKaiChat / createKaiChat call creates its own independent state. There is no
global store — instantiate as many as you need and they run in isolation:
const left = useKaiChat({ onSubmit: askLeft });const right = useKaiChat({ onSubmit: askRight });
<Chat {...left.bind} /><Chat {...right.bind} />Vanilla / plain HTML
Section titled “Vanilla / plain HTML”Without a framework hook, wire the setter contract manually and use the helpers directly:
import '@kitn.ai/ui/elements';import { appendMessage, createAssistantStream } from '@kitn.ai/ui/state';
await customElements.whenDefined('kai-chat');const el = document.querySelector('kai-chat');
const set = (fn) => { el.messages = fn(el.messages ?? []); };
el.addEventListener('kai-submit', async (e) => { const id = crypto.randomUUID(); set((m) => appendMessage(m, { id: crypto.randomUUID(), role: 'user', parts: [{ type: 'text', text: e.detail.value }] }));
const s = createAssistantStream(set); for await (const part of backend(e.detail.value)) s.appendText(part); s.done();});Next steps
Section titled “Next steps”- Getting Started — install, register, and render your first
<kai-chat>. - Streaming recipe — full server-side streaming patterns.
- React framework guide — React-specific wiring and tips.