arizuko

arizukohowto › Embed chat (SDK)

embed chat with the SDK

The plain chat link (put a chat link on your site) drops a ready-made page at /chat/<token>/ with zero code. When you want the conversation inside your own UI — your fonts, your layout, your message bubbles — use arizuko-client.js, the browser SDK that wraps the same token surface.

Same primitive, two front ends. The SDK talks to the exact /chat/<token>/ POST + SSE endpoints the built-in page uses — see reference › route tokens for the wire shapes and concepts › tokens for the bearer model.

get a token

Mint a web: chat token for the folder you want to talk to:

arizuko token issue acme chat
# https://acme.fiu.wtf/chat/Yp3v...Q2/

The token is the bit after /chat/. Treat it as a shareable bearer credential — anyone holding it can chat as an anonymous visitor against that folder's agent.

load the SDK

Pull arizuko-client.js from the same host that serves the docs and chat. It exposes one global, Arizuko.

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

connect, send, stream

Three calls carry the whole conversation:

complete sample

A self-contained chat page — header, scrolling thread, input box — built on the three calls above. Paste your token (or pass ?token=<t> in the URL), open the file in a browser, and you have a working chat client styled however you like.

<!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.fiu.wtf/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 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);
          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);
              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>

reconnection

stream() handles dropped connections the same way the built-in page does: it reconnects with Last-Event-Id, the server replays missed frames, and the turn resumes live. You don't manage the EventSource yourself.

go deeper