Skip to content
Communication

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.

Answers can be wrong. Check anything that matters.
<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.

You said:
How do I stream a reply?
Assistant said:

Return the Response from your fetch as is. The component reads the AI SDK stream, plain text, or JSON — whichever the route sends.

send={(messages, { signal }) =>  fetch('/api/chat', { method: 'POST', body: JSON.stringify({ messages }), signal })}
<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.

Docs assistant online

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>