Menu
Public repository / master
AkurAI-Framework / site/content/docs/ai-usage.md
Latest change b04f9a417788a170e630729967686f543cb2f532 - docs: add ai-usage.md — end-to-end AI chat usage guide by Ólafur Búi Ólafsson
# Using AI components
End-to-end guide: configure the LLM backend, mount a streaming chat, add generative
UI, and send receipts back to the assistant — all in native ESM, no build step.
> **Before you start.** This guide assumes a working project (`akurai new myapp`).
> For the component catalogue and provenance, see
> [AI components](/docs/ai-components). This page is the practical how-to.
---
## 1. Setup — point the framework at an LLM
The AI layer needs an OpenAI-compatible `/v1/chat/completions` endpoint. Configure
it with two environment variables:
```sh
export AKURAI_LLM_URL=http://localhost:11434 # any OpenAI-compatible host
export AKURAI_LLM_MODEL=llama3.2 # model name the host recognises
```
`AKURAI_LLM_URL` is **plain HTTP only** — the framework's built-in LLM client uses
`std::net::TcpStream` and has no TLS. Point it at a local model server
([Ollama](https://ollama.com), LM Studio, vLLM, your homelab proxy) or a gateway
that terminates TLS upstream. Pointing it at `https://` will be rejected at startup.
Start the server:
```sh
akurai serve
```
**No LLM configured?** The framework starts fine. Requests to `POST /api/assistant`
return a structured error the client displays as an inline message — nothing crashes,
nothing hangs. You can develop the rest of your UI without a running model.
---
## 2. Mount a chat on a page
Chat primitives are native ES module classes in `akurai-ui`. Load them directly in
your page script — no bundler, no import map gymnastics beyond what the framework
already injects.
### Minimal thread
```html
<!-- frontend/pages/chat/index.html -->
<div id="chat-root"></div>
<script type="module">
import { Thread, Message, Composer, ActionBar } from '/ui/ai/chat.js';
const root = document.getElementById('chat-root');
const thread = new Thread(root, {
endpoint: '/api/assistant',
// optional — initial system message
system: 'You are a helpful assistant for the Acme store.',
});
// Compose the inner layout
thread.mount([
new Message(), // renders each turn (user + assistant)
new Composer(), // input row
new ActionBar(), // copy / retry / edit per message
]);
</script>
```
`Thread` owns the conversation state, auto-scrolls to the latest token, and exposes
keyboard shortcuts out of the box. `Message` handles Markdown + syntax-highlighted
code via `akurai-markdown`. `Composer` handles multiline, `Enter` to send,
`Shift+Enter` for newline, and optional voice dictation (Web Speech API — no server
STT).
For the full API surface, see [Chat primitives](/docs/chat).
### How streaming works
When the user sends a message, `Thread` opens a `POST /api/assistant` request. The
server proxies the conversation to the configured model and streams back
newline-delimited SSE events:
| Event type | Payload | What the client does |
|---|---|---|
| `delta` | `{"content": "..."}` | Appends tokens to the in-progress message. |
| `tool_call` | `{"name": "...", "arguments": {...}}` | Starts a tool-result slot in the message. |
| `done` | `{}` | Finalises the assistant turn. |
| `error` | `{"message": "..."}` | Displays the error inline; does not clear the conversation. |
The client never sees a raw HTTP body — everything arrives as typed events. If the
connection drops mid-stream, the partial message is preserved and marked with a
retry affordance.
For the full SSE contract, see [Assistant API](/docs/assistant-api).
---
## 3. Generative UI — tool results as components
The assistant can render **live UI inside the chat** when a tool call returns a
structured payload. The runtime matches that payload against a registered component;
if there is no match it falls back gracefully to a pretty-printed JSON view.
### The component contract
A tool-result component is a plain object with six fields:
```js
const weatherWidget = {
name: 'weather_widget', // unique ID — must match your tool name
category: 'display', // one of the six categories
title: 'Weather', // display name in the gallery
match: (payload) => // return true if this payload is yours
payload?.type === 'weather' && typeof payload.location === 'string',
render: (payload, container) => { // write DOM into container
container.innerHTML = `
<div class="weather-card">
<h2>${payload.location}</h2>
<p>${payload.temperature}°C — ${payload.condition}</p>
</div>`;
},
example: { // used in the /ai-components gallery
type: 'weather', location: 'Akureyri', temperature: 4, condition: 'Cloudy',
},
};
```
`match` is called before `render`. Keep it cheap — a type-tag check is enough. If
`match` returns false for every registered component, the runtime renders the payload
as formatted JSON so the developer can see what shape is arriving.
### Register a component
```js
import { registerToolComponent } from '/ui/ai/tool-runtime.js';
registerToolComponent(weatherWidget);
```
Call `registerToolComponent` before you mount the `Thread`. Order does not matter
beyond that — the runtime re-evaluates the registry on every new `tool_call` event.
For the full runtime API, see [Generative UI](/docs/generative-ui).
### Payload validation with `akurai-schema`
If you want to validate the shape rather than inspect it manually in `match`, use
the framework's lightweight schema validator:
```js
import { schema } from '/ui/schema.js';
const weatherSchema = schema.object({
type: schema.literal('weather'),
location: schema.string(),
temperature: schema.number(),
condition: schema.string(),
});
const weatherWidget = {
name: 'weather_widget',
match: (p) => weatherSchema.check(p).ok,
render: (p, el) => { /* ... */ },
// ...
};
```
`schema.check` returns `{ ok: true }` or `{ ok: false, errors: [...] }` — no
exceptions, no external validator. See [Schema](/docs/schema).
### Receipts — sending interaction results back
Interactive components (approvals, forms, option lists) can send a **receipt** back
to the assistant. A receipt is a structured result that flows into the model's next
turn as the tool's return value, closing the round-trip.
```js
import { sendReceipt } from '/ui/ai/tool-runtime.js';
const approvalCard = {
name: 'approval_card',
category: 'confirmation',
title: 'Approval',
match: (p) => p?.type === 'approval_request',
render: (payload, container, context) => {
container.innerHTML = `
<div class="approval-card">
<p>${payload.prompt}</p>
<button id="approve">Approve</button>
<button id="deny">Deny</button>
</div>`;
container.querySelector('#approve').onclick = () =>
sendReceipt(context, { approved: true, action: payload.action });
container.querySelector('#deny').onclick = () =>
sendReceipt(context, { approved: false, action: payload.action });
},
example: { type: 'approval_request', prompt: 'Send the email?', action: 'send_email' },
};
```
`sendReceipt(context, result)` posts the result to `/api/assistant` as a tool result
message. The model receives it, continues the conversation, and the stream resumes.
The component's container is automatically locked (pointer-events off) once a
receipt is sent — the user cannot approve twice.
---
## 4. Worked example — weather widget + approval round-trip
The following shows both patterns together: a `get_weather` tool whose result renders
the `weather_widget` component, and a `send_alert` tool that requires approval.
### Server side (your tool handlers)
The assistant endpoint relays model tool calls to your backend. Register handlers in
`backend/routes.json` or in a custom route file:
```json
{
"POST /api/tools/get_weather": { "handler": "tools/get_weather.js" },
"POST /api/tools/send_alert": { "handler": "tools/send_alert.js" }
}
```
`tools/get_weather.js` returns:
```json
{
"type": "weather",
"location": "Akureyri",
"temperature": 4,
"condition": "Cloudy"
}
```
`tools/send_alert.js` first returns an approval request:
```json
{
"type": "approval_request",
"prompt": "Send SMS alert to the on-call team?",
"action": "sms_oncall"
}
```
After the receipt arrives with `{ "approved": true }`, it fires the SMS and returns
a confirmation payload.
### Client side
```js
import { Thread, Message, Composer, ActionBar } from '/ui/ai/chat.js';
import { registerToolComponent } from '/ui/ai/tool-runtime.js';
import { sendReceipt } from '/ui/ai/tool-runtime.js';
// --- register components ---
registerToolComponent({
name: 'weather_widget',
category: 'display',
title: 'Weather',
match: (p) => p?.type === 'weather',
render: (p, el) => {
el.innerHTML = `
<div class="weather-card">
<h2>${p.location}</h2>
<p class="temp">${p.temperature}°C</p>
<p class="cond">${p.condition}</p>
</div>`;
},
example: { type: 'weather', location: 'Akureyri', temperature: 4, condition: 'Cloudy' },
});
registerToolComponent({
name: 'approval_card',
category: 'confirmation',
title: 'Approval',
match: (p) => p?.type === 'approval_request',
render: (p, el, ctx) => {
el.innerHTML = `
<div class="approval-card">
<p>${p.prompt}</p>
<button id="yes">Approve</button>
<button id="no">Deny</button>
</div>`;
el.querySelector('#yes').onclick = () => sendReceipt(ctx, { approved: true });
el.querySelector('#no').onclick = () => sendReceipt(ctx, { approved: false });
},
example: { type: 'approval_request', prompt: 'Send SMS?', action: 'sms_oncall' },
});
// --- mount the thread ---
const thread = new Thread(document.getElementById('chat-root'), {
endpoint: '/api/assistant',
system: 'Use get_weather for any weather query. Use send_alert to page on-call.',
tools: ['get_weather', 'send_alert'], // declare tools the model may call
});
thread.mount([ new Message(), new Composer(), new ActionBar() ]);
```
**What happens at runtime:**
1. User types "What's the weather in Akureyri?"
2. `Thread` posts to `/api/assistant`; the model emits a `tool_call` for `get_weather`.
3. The SSE event arrives; the runtime calls `match` on every registered component.
4. `weather_widget.match` returns `true`; `render` fires and a weather card appears
inline in the assistant turn, mid-stream.
5. The stream resumes with the model's text summary.
For the approval round-trip, the model emits `tool_call` for `send_alert`. The
`approval_card` renders. The user clicks **Approve**; `sendReceipt` posts `{ approved: true }`.
The model receives the result and continues.
---
## 5. Component catalogue
Browse every component with live examples at [/ai-components](/ai-components).
| Category | What it renders |
|---|---|
| **Progress** | Multi-step plan trackers, streaming progress bars. |
| **Input** | Option pickers, parameter sliders, preference panels, branching question flows. |
| **Display** | Citations, geo maps, carousels, link previews, stats, terminal output, weather. |
| **Artifacts** | Charts, code blocks, diffs, data tables, message drafts, social post cards. |
| **Confirmation** | Approval cards, order summaries with receipt round-trips. |
| **Media** | Audio players, images, image galleries, video. |
Per-category reference pages are linked from [AI components](/docs/ai-components) as
they ship. Each page documents the payload shape, the `name` field to match, and
the interactions (receipts) the component can emit.
---
## 6. Provenance
The component set and streaming UX are a **1:1 port in spirit** of two excellent
open-source libraries:
- **[assistant-ui](https://github.com/assistant-ui/assistant-ui)** — composable chat
primitives: Thread, Message, Composer, ActionBar, ThreadList, BranchPicker,
Reasoning, Suggestions.
- **[assistant-ui/tool-ui](https://github.com/assistant-ui/tool-ui)** — the
tool-payload → UI mapping, the six component categories, and the receipt protocol.
Both are React + Tailwind + Zod. This framework is the opposite: native ES modules
the browser loads directly, a pure-`std` Rust server, and a from-scratch schema
validator. We port the **components and behaviours**, not the stack. The result is
the same chat and generative-UI experience with zero bundler, zero runtime
dependencies, and no React.
---
## See also
- [AI components](/docs/ai-components) — overview, provenance, full component list.
- [Assistant API](/docs/assistant-api) — the `POST /api/assistant` SSE contract.
- [Chat primitives](/docs/chat) — Thread / Message / Composer / ActionBar API.
- [Generative UI](/docs/generative-ui) — tool-call runtime, receipts, approvals, ThreadList, branches, reasoning, suggestions.
- [Schema](/docs/schema) — `akurai-schema` validation.