Communication
AI Chat
A conversation with a model — the thread, the streaming reply, the composer, the stop button — wired to whichever model you have.
import { AiChat } from 'omaris' Examples
Basic
One prop. mockChat() stands in for a model on this page; in an app, chatEndpoint('/api/chat') points it at a route of yours and the rest is the same — the chips, the streaming reply, the stop button, the markdown, the thread that follows the reply until you scroll up.
How can I help?
<script lang="ts"> import { AiChat, mockChat } from 'omaris';</script><AiChat send={mockChat()} suggestions={[ 'Explain runes in one paragraph', 'Write a Svelte counter', 'What makes a chat feel right?' ]}/> Assistant
An assistant with a job. title and hint set the empty state, avatar puts its mark beside each reply, assistantName is what screen readers call it, and disclaimer sits under the composer.
Hi, I'm the order helper
I can find an order, or draft a reply to a customer.
<script lang="ts"> import { AiChat, mockChat } from 'omaris';</script><AiChat send={mockChat()} avatar assistantName="Order helper" title="Hi, I'm the order helper" hint="I can find an order, or draft a reply to a customer." suggestions={['Where is order #4821?', 'Draft a refund reply']} disclaimer="Answers can be wrong. Check anything that matters."/> History
A conversation resumed. bind:messages is the whole state — load it from wherever you keep it, and save it in onchange. The reply's markdown is rendered, code blocks included, with copy on each.
<script lang="ts"> import { AiChat, mockChat, type AiChatMessage } from 'omaris'; let messages = $state<AiChatMessage[]>([ { id: '1', role: 'user', content: 'How do I stream a reply?', createdAt: 1, status: 'done' }, { id: '2', role: 'assistant', content: "Return the `Response` from your `fetch` as is. The component reads the AI SDK stream, plain text, or JSON — whichever the route sends.\n\n```ts\nsend={(messages, { signal }) =>\n fetch('/api/chat', { method: 'POST', body: JSON.stringify({ messages }), signal })}\n```", createdAt: 2, status: 'done' } ]);</script><AiChat send={mockChat()} bind:messages /> Error
When it fails. The message lands where the reply would have been, with a retry; what streamed before the failure stays. The stop button keeps what arrived rather than throwing it away.
How can I help?
Every second reply fails here, on purpose.
<script lang="ts"> import { AiChat, mockChat } from 'omaris';</script><AiChat send={mockChat({ fail: 2, latency: 400 })} hint="Every second reply fails here, on purpose." suggestions={['Say hello', 'Say it again']} height={360}/> Headless
Your own composer. An empty composer snippet takes the bar away; bind:this gives you send(), stop(), regenerate() and clear(), and bind:status says where it is.
How can I help?
<script lang="ts"> import { AiChat, Button, Input, mockChat, type AiChatStatus } from 'omaris'; let chat = $state<AiChat | null>(null); let draft = $state(''); let status = $state<AiChatStatus>('idle');</script><div class="flex flex-col gap-3"> <AiChat bind:this={chat} bind:status send={mockChat()} height={320}> {#snippet composer()}{/snippet} </AiChat> <form class="flex items-end gap-2" onsubmit={(event) => { event.preventDefault(); void chat?.send(draft); draft = ''; }} > <Input label="Message" bind:value={draft} class="flex-1" /> {#if status === 'streaming'} <Button type="button" variant="tonal" onclick={() => chat?.stop()}>Stop</Button> {:else} <Button type="submit" disabled={!draft.trim()}>Send</Button> {/if} <Button type="button" variant="text" tone="secondary" onclick={() => chat?.clear()}> Clear </Button> </form></div> Server
The real wiring. The key stays on the server: chatEndpoint() posts the conversation to a route of yours and streams the reply back. With omaris/ai and the Vercel AI SDK the route is two lines, and switching models is switching the import: ``ts // src/routes/api/chat/+server.ts import { chatRoute } from 'omaris/ai'; import { anthropic } from '@ai-sdk/anthropic'; // import { openai } from '@ai-sdk/openai'; → openai('gpt-5') // import { google } from '@ai-sdk/google'; → google('gemini-2.5-pro') export const POST = chatRoute({ model: anthropic('claude-sonnet-5'), system: 'You are the help desk for a bakery. Be brief.' }); ` Any other route works too: the component reads the AI SDK's stream, plain streamed text, or a JSON { text }. send can also be your own function that returns a fetch Response`, a string, or an async iterable.
How can I help?
<script lang="ts"> import { AiChat, chatEndpoint } from 'omaris';</script><AiChat send={chatEndpoint('/api/chat', { body: { conversation: 'demo' } })} system="Answer in one short paragraph."/> Overridden
Every part is reachable. classes names the slots, class covers the root and wins last, height="100%" lets a parent set the frame, and --chat-measure narrows the reading column. The header snippet sits above the thread and the icon snippet replaces the assistant's mark.
Ask the docs
Answers cite the page they came from.
<script lang="ts"> import { AiChat, Badge, mockChat } from 'omaris';</script><div class="h-112 max-w-md"> <AiChat send={mockChat()} tone="tertiary" height="100%" avatar title="Ask the docs" hint="Answers cite the page they came from." placeholder="Ask about the design system…" class="rounded-shape-lg bg-surface-container-lowest [--chat-measure:32rem]" classes={{ bubble: 'rounded-shape-md bg-tertiary-container text-tertiary-container-foreground', composer: 'rounded-shape-lg', tools: 'pointer-fine:opacity-100' }} > {#snippet header()} <span class="text-sm font-medium text-foreground">Docs assistant</span> <Badge tone="success" variant="tonal" size="sm">online</Badge> {/snippet} {#snippet icon()} ... {/snippet} </AiChat></div> When to use it
Use it for
- A conversation with a model, anywhere one belongs: a help desk in a drawer, an assistant in a dashboard's sidebar, a whole chat page. One
sendprop and it streams, renders markdown, scrolls, stops and retries. - An assistant with a job.
systemsays what it is for,titleandhintgreet the person,suggestionsare the three things people ask it. - A conversation that comes back.
bind:messagesis the whole state; save it inonchange, load it on the way in. - A reply that shows its work. Reasoning a model streams is folded above its answer, with nothing to turn on.
- A composer of your own. An empty
composersnippet takes the bar away, andsend(),stop(),regenerate()andclear()come throughbind:this.
Not for
- Chat between people → List with your own transport. This one has a person and a model, and the tools on a message (regenerate, edit) assume it.
- One question with one answer, with no thread → a Text Field and a Card for the reply.
- Making a picture → AI Image. A model that replies with one can still be shown here through the
messagesnippet. - Long-form output to be edited → Rich Text Editor, with the model writing into it.
Do
- Keep the key on the server.
chatEndpoint('/api/chat')here andchatRoute()fromomaris/aiin the route, or any route that streams. The component reads the AI SDK's stream, plain streamed text, or a JSON{ text }. - Return the
fetchResponseas is from asendof your own. There is nothing to parse on your side. - Give it a height it can scroll in.
heightorclass="h-full"inside a flex column; a chat that grows with its thread pushes the composer off the screen. - Put a
disclaimerunder it when the answers matter. - Leave the reading column alone in a wide frame. It stops at 48rem and centres, which is what keeps a reply readable on a full page; set
--chat-measureon the root only for a different measure. - Use
mockChat()while the route is not there yet. It streams markdown a word at a time after a pause, and can fail on demand.
Don't
- Fight the scroll from outside. The thread follows the reply until the person scrolls up, and the jump button brings them back; a
scrollToof your own on every change drags them down mid-read. - Put the model's reply in a bubble through
classes.bubble. That slot is the person's message; the reply is plain text on purpose, so long answers and code blocks read like a document. - Store secrets in
bodyofchatEndpoint(). It is the request body, in the network tab. - Drop
status: 'error'messages frommessagesbefore resending them.chatEndpoint()already leaves them out.
Quick reference
tone primary(default)secondarytertiarydestructivesuccesswarninginfo
API
AiChat
A conversation with a model — the thread, the streaming reply, the composer, the stop button — wired to whichever model you have.
The smallest call is one prop:
The person's messages sit in a compact bubble on the end side; the model's reply runs as plain text the width of the column, markdown and code blocks rendered. The thread follows the reply as it streams until the person scrolls up, and a button brings them back. Replies can be stopped, retried, regenerated and copied, and the person's own messages edited. send is any (messages, ctx) => reply, and a reply can be a string, an async iterable, a ReadableStream or the raw Response from a fetch — the component reads all of them.
The column is 48rem at most, centred; set --chat-measure on the root for another width.
import { AiChat } from 'omaris' <AiChat send={chatEndpoint('/api/chat')} /> Parts
Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.
root- No description in the source yet.
header- Above the thread — a title, a model picker. Only rendered with a
headersnippet. thread- The scrolling area, full width so its scrollbar sits at the edge.
content- The centred column inside the thread.
user- One of the person's messages, on the end side: the bubble, and its tools — beside it on hover with a mouse, under it on touch.
bubble- The bubble around the person's message.
assistant- One reply: the optional avatar and the text column.
avatar- The circle beside a reply, with
avatar. reply- The reply's column: reasoning, text, error, tools.
prose- Rendered markdown.
cursor- The dot at the end of a reply still arriving.
typing- The three dots before the first word.
tools- The copy / regenerate / edit row under a message.
reasoning- The model's reasoning, folded above its answer when it sent any.
alert- A failed reply's message and retry.
empty- Before the first message.
mark- The circle above the headline.
title- No description in the source yet.
description- No description in the source yet.
suggestions- No description in the source yet.
footer- The composer, the jump button over it and the disclaimer under it.
jump- The "jump to latest" button, floating over the composer.
composer- The single rounded surface: text field and send button.
input- No description in the source yet.
disclaimer- The line under the composer: "Can make mistakes".
Props
send SendFn Answers the conversation. chatEndpoint('/api/chat'), mockChat(), or any (messages, ctx) => reply of your own — a fetch whose Response you return as is will do.
messages bindableDefaults to []
AiChatMessage[] The conversation. Bindable; give it a history to resume one.
prompt bindableDefaults to ''
string The text in the composer. Bindable.
system string A system prompt, carried in the request by chatEndpoint().
status bindableDefaults to 'idle'
AiChatStatus Where it is. Bindable, read-only in practice.
tone Defaults to 'primary'
AiChatTone Colours the send button, the focus ring, links and the assistant's mark.
primarysecondarytertiarydestructivesuccesswarninginfo
height Defaults to '32rem'
number | string Height of the whole thing, in px or any CSS length. class="h-full" also works.
avatar Defaults to false
boolean A small mark beside each reply. The icon snippet replaces what is in it.
suggestions Defaults to []
string[] Prompts offered as chips before the first message.
assistantName Defaults to 'Assistant'
string The name the assistant goes by, for screen readers.
title Defaults to 'How can I help?'
string The headline before the first message.
hint string The line under it.
placeholder Defaults to 'Message…'
string disclaimer string The line under the composer.
disabled Defaults to false
boolean onsend (message: AiChatMessage) => void Called with the message the person sent.
onfinish (message: AiChatMessage) => void Called with the finished reply.
onerror (error: Error) => void onchange (messages: AiChatMessage[]) => void Called after any change to the conversation.
class string classes AiChatClasses Per-part Tailwind overrides. class still covers the root.
header Snippet Above the thread.
icon Snippet The assistant's mark, in the empty state and the avatar. Defaults to sparkles.
empty Snippet<[AiChatEmptyContext]> Replaces the empty thread.
message Snippet<[AiChatMessageContext]> Replaces what a message shows — the bubble's text, or the reply's.
actions Snippet<[AiChatMessageContext]> Extra buttons under a message, after copy, regenerate and edit.
composer Snippet<[AiChatComposerContext]> Replaces the composer. An empty snippet takes it away.