AI Chat
A conversation with a model — the thread, the streaming reply, the composer, the stop button — wired to whichever model you have.
Walkthrough
1. 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?' ]}/> 2. 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."/> 3. 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 /> 4. 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}/> 5. 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> 6. 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."/> 7. 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>