AkurAI Build
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.