arizuko

arizuko › howto › Embed chat (SDK)

chat SDK

The chat link gives a ready chat page at /chat/<token>/ without code. To put the conversation inside your own page, with your fonts, layout and message bubbles, use arizuko-client.js. This browser SDK uses the same token URLs.

The SDK and the built-in page use the same chat token. The SDK uses the token chat POST and turn SSE endpoints. The built-in page uses a topic stream. Route token reference shows the wire shapes. Route tokens explains the bearer model.

A web chat token opens the conversation

Make a web: chat token for the folder of the agent. Here the instance is demo and the folder is acme:

arizuko token -i demo issue chat acme
# jid:   web:acme
# token: Yp3v...Q2

The token is a bearer credential that anyone can use. A person with the token can chat with the agent of that folder as an anonymous visitor.

webd serves the SDK file

Load arizuko-client.js from the host that serves the chat. The file defines one global object, Arizuko.

<script src="https://acme.example.com/assets/arizuko-client.js"></script>

Three calls carry the conversation

Arizuko.connect(token)
Returns a session with name and folder.
session.send(content)
Posts a message and returns { turn_id }.
session.stream(turn_id, handlers)
Opens the SSE stream of that turn and calls onMessage, onStatus, onDone and onError. It returns a function that closes the stream early.

One HTML file is a complete client

The sample is a full chat page with a header, a thread and an input box, built on the three calls. Put your token in the file, or add ?token=<t> to the URL. Set baseURL to your chat host. The sample keeps one topic for each page load so its messages share a conversation.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>chat-sdk sample</title>
    <script src="https://acme.example.com/assets/arizuko-client.js"></script>
    <style>
      body { font: 14px/1.5 -apple-system, sans-serif; display: flex; flex-direction: column; height: 100dvh; }
      #thread { flex: 1; overflow-y: auto; padding: 1rem; display: flex; flex-direction: column; gap: .5rem; }
      .msg { max-width: 75%; padding: .55rem .85rem; border-radius: 12px; white-space: pre-wrap; }
      .msg.user { align-self: flex-end; background: #4ade80; color: #0a0a0a; }
      .msg.assistant { align-self: flex-start; border: 1px solid #222; }
      .msg.status { align-self: center; color: #666; font-style: italic; font-size: .85em; }
      footer form { display: flex; gap: .5rem; padding: .6rem 1rem; border-top: 1px solid #222; }
      footer input { flex: 1; padding: .5rem .7rem; }
    </style>
  </head>
  <body>
    <header><span id="name">connecting…</span> <span id="folder"></span></header>
    <div id="thread"></div>
    <footer><form id="f"><input id="m" placeholder="type a message…" autofocus /><button id="b" disabled>send</button></form></footer>
    <script>
      // Paste your chat token here, or override via ?token=<t> in the URL.
      const TOKEN = new URLSearchParams(location.search).get('token') || 'REPLACE_WITH_CHAT_TOKEN';
      const topic = crypto.randomUUID();
      const thread = document.getElementById('thread');
      const btn = document.getElementById('b');
      const input = document.getElementById('m');

      function add(role, content) {
        const d = document.createElement('div');
        d.className = 'msg ' + role;
        d.textContent = content;
        thread.appendChild(d);
        d.scrollIntoView({ behavior: 'smooth' });
        return d;
      }

      (async () => {
        try {
          const slink = await Arizuko.connect(TOKEN, { baseURL: 'https://acme.example.com' });
          document.getElementById('name').textContent = slink.name;
          document.getElementById('folder').textContent = slink.folder;
          btn.disabled = false;

          document.getElementById('f').onsubmit = async (e) => {
            e.preventDefault();
            const content = input.value.trim();
            if (!content) return;
            input.value = '';
            btn.disabled = true;
            add('user', content);
            try {
              const turn = await slink.send(content, { topic });
              const typing = add('status', 'thinking…');
              slink.stream(turn.turn_id, {
                onMessage: (f) => { typing.remove(); add('assistant', f.content); },
                onStatus:  (f) => { typing.textContent = f.content; },
                onDone:    (_) => { typing.remove(); },
                onError:   (err) => { typing.textContent = 'stream error'; console.error(err); },
              });
            } catch (err) {
              add('status', 'error: ' + err.message);
            } finally {
              btn.disabled = false;
              input.focus();
            }
          };
        } catch (err) {
          document.getElementById('name').textContent = 'connect failed';
          add('status', err.message);
        }
      })();
    </script>
  </body>
</html>

The stream connects again after a lost connection

stream() uses the browser EventSource. After a lost connection, the browser connects again with Last-Event-Id. webd sends the missed frames, and the turn continues live. You do not manage the EventSource yourself. The stream closes when the turn ends.

Four pages give the details

chat link
The same chat without code.
token reference
The /chat/<token>/ POST and SSE surface under the SDK.
route tokens
The bearer model and the JID encoding.
webd
The daemon that serves /chat/*.