1. Create typeset.css
Run the CLI, or copy the stylesheet next to your global CSS and import it after Tailwind.
npx zard-cli@latest add typeset@import 'tailwindcss';
@import './typeset.css';2. Add the fonts
The faces are self-hosted through @fontsource — no request to a third-party CDN.
npm install @fontsource-variable/geist @fontsource-variable/geist-mono@import '@fontsource-variable/geist';
@import '@fontsource-variable/geist-mono';
:root {
--font-geist: 'Geist Variable', sans-serif;
--font-geist-mono: 'Geist Mono Variable', monospace;
}3. Add the preset
Six variables, in your global stylesheet.
.typeset-docs {
--typeset-font-body: var(--font-geist);
--typeset-font-heading: var(--font-geist);
--typeset-font-mono: var(--font-geist-mono);
--typeset-size: 15px;
--typeset-leading: 1.75;
--typeset-flow: 1.25em;
}4. Wrap your content
typeset turns the styles on; typeset-docs carries your choices.
<div class="typeset typeset-docs max-w-[37em]">
<!-- rendered markdown -->
</div>Building a Streaming Chatbot
The injectChat function makes it effortless to create a conversational user interface for your chatbot application. It streams chat messages from your AI provider, holds the chat state in signals, and updates the UI automatically as new messages arrive.
To summarize, injectChat provides the following features:
- Message Streaming: All the messages from the AI provider are streamed to the chat UI in real-time.
- Managed Signals: The function manages the signals for input, messages, status, error and more for you.
- Seamless Integration: Easily integrate your chat AI into any design or layout with minimal effort.
In this guide, you will learn how to use injectChat to create a chatbot application with real-time message streaming. Check out our chatbot with tools guide to learn how to use tools in your chatbot.
Example
The request flow works like this:
- The user submits a message and
sendMessageposts it to your API route. - Your transport calls the provider and returns a UI message stream.
- The chat appends chunks to the last message as they arrive, re-rendering as it goes.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { injectChat } from '@acme/chat';
@Component({
selector: 'app-chat',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (message of chat.messages(); track message.id) {
<app-message [message]="message" />
}
<app-chat-input
[disabled]="chat.status() !== 'ready'"
(submitted)="chat.sendMessage({ text: $event })"
/>
`,
})
export class ChatComponent {
protected readonly chat = injectChat();
}
import { Injectable } from '@angular/core';
import { readUIMessageStream } from '@acme/chat';
@Injectable({ providedIn: 'root' })
export class ChatTransport {
async *stream(messages: UIMessage[]) {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ messages }),
});
yield* readUIMessageStream(response.body);
}
}
The UI messages have a
partsproperty that contains the message parts. We recommend rendering the messages using thepartsproperty instead of thecontentproperty. The parts property supports different message types, including text, tool invocation, and tool result, and allows for more flexible and complex chat UIs.
In the ChatComponent, injectChat will request your AI provider endpoint whenever the user sends a message using sendMessage. The messages are then streamed back in real-time and displayed in the chat UI.
Customized UI
injectChat also provides ways to manage the chat message state in code, show status, and update messages without being triggered by user interactions.
Status
injectChat returns a status signal. It has the following possible values:
submitted: The message has been sent to the API and we're awaiting the start of the response stream.streaming: The response is actively streaming in from the API, receiving chunks of data.ready: The full response has been received and processed; a new user message can be submitted.error: An error occurred during the request, preventing successful completion.
protected readonly chat = injectChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
<!-- ... -->
@if (chat.status() === 'submitted' || chat.status() === 'streaming') {
<div>
@if (chat.status() === 'submitted') {
<app-spinner />
}
<button type="button" (click)="chat.stop()">Stop</button>
</div>
}
Error State
Similarly, the error signal holds the error thrown during the request. It can be used to display an error message, disable the submit button, or show a retry button:
We recommend showing a generic error message to the user, such as "Something went wrong." This is a good practice to avoid leaking information from the server.
protected readonly chat = injectChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
<!-- ... -->
@if (chat.error()) {
<div>An error occurred.</div>
<button type="button" (click)="chat.regenerate()">Retry</button>
}
Cancellation and regeneration
It's also a common use case to abort the response message while it's still streaming back from the AI provider. You can do this by calling the stop method returned by injectChat.
<button
[disabled]="chat.status() !== 'streaming' && chat.status() !== 'submitted'"
(click)="chat.stop()"
>
Stop
</button>
API reference
injectChat(options)
Creates a chat helper. All options are optional; the defaults talk to /api/chat and render at native stream speed.
| Prop | Type | Description |
|---|---|---|
transport | ChatTransport<UIMessage> | How messages reach your API route |
messages | UIMessage[] | Initial messages to seed the conversation |
onFinish | (event: FinishEvent) => void | Runs when the assistant response completes |
onError | (error: Error) => void | Runs when the request fails |
throttle | number | Milliseconds between signal updates while streaming |
Event Callbacks
injectChat provides optional event callbacks that you can use to handle different stages of the chatbot lifecycle:
onFinish: Called when the assistant response is completed. The event includes the response message, all messages, and flags for abort, disconnect, and errors.onError: Called when an error occurs during the request.onData: Called whenever a data part is received.
These callbacks can be used to trigger additional actions, such as logging, analytics, or custom UI updates.
protected readonly chat = injectChat({
onFinish: ({ message }) => this.history.save(message),
onError: error => console.error(error),
});
Math
Display math sits in the flow rhythm and scrolls when it runs long. Inline math like rides the line without stretching it.
Display
The quadratic formula, as a block:
Prose continues after the block at the normal distance, so equations read as part of the argument, not decoration.
Overflow
A long expansion scrolls inside its own box instead of breaking the column: