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.
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
nameandfolder. 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,onDoneandonError. 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/*.