@myflowai/web-sdk
v0.5.0
Published
Browser SDK for embedding the FlowAI agent orchestrator (chat + voice) on any webpage.
Readme
@myflowai/web-sdk
Browser SDK for embedding the FlowAI agent orchestrator on any webpage. Wraps the orchestrator's WebSocket chat transport and ElevenLabs-powered voice flow behind a single typed event-emitter surface.
Distribution: published to public npmjs.com as
@myflowai/web-sdkand served as a UMD bundle (window.FlowAI) via unpkg / jsDelivr. The orchestrator endpoint is baked into the bundle — integrators do not configure URLs.
Versioned (recommended):
https://unpkg.com/@myflowai/[email protected]/dist/flowai.min.js@latest works too but pins your page to whatever ships next — not
recommended for production.
Quick start — chat
<script src="https://unpkg.com/@myflowai/[email protected]/dist/flowai.min.js"></script>
<script>
const chat = new FlowAI.ChatClient({
tenantId: "acme",
agentId: "support-v1",
});
// Assistant reply streams token-by-token. A turn can produce several
// message segments (see "public event surface" below); `message` closes
// each and `final` ends the turn.
chat.on("token", (t) => appendDelta(t.turnId, t.text));
chat.on("message", (m) => sealSegment(m.turnId, m.text));
chat.on("final", () => endTurn());
// `agent_message` is a human supervisor message (warm handoff). It may
// also carry `attachments[]` when the supervisor sent files / images.
chat.on("agent_message", (m) => {
render({ from: "human", text: m.text });
for (const a of m.attachments ?? []) renderAttachment(a);
});
chat.on("transfer_to_human", (e) => showHandoffNotice(e));
chat.on("error", (e) => console.error(e.message));
await chat.connect();
chat.send("Hello, I need help with my order.");
</script>Quick start — file & image attachments
Two-step upload: Upload the attachment and refer the attachment in the user message.
<input type="file" id="picker" accept="image/*,application/pdf" />
<script>
document.getElementById("picker").addEventListener("change", async (ev) => {
const file = ev.target.files[0];
if (!file) return;
// 1. Upload (presign + PUT). Returns an AttachmentRef including a
// client-side SHA-256 for integrity tracking.
const ref = await chat.uploadAttachment(file, {
onProgress: (f) => setBar(`${Math.round(f * 100)}%`),
});
// 2. Send a turn referencing the attachment. Pass an empty text for
// image-only sends, or text + attachments together.
chat.send("What do you see in this picture?", [ref]);
});
</script>Rendering inbound attachments
When a supervisor sends a file, the agent_message event carries
attachments[]. Each ref includes a short-TTL viewUrl for immediate
rendering, plus an s3Key you can use with the GET fallback if the URL
is stale (e.g. after a page reload).
chat.on("agent_message", (m) => {
for (const a of m.attachments ?? []) {
const src = chat.attachmentUrl(a);
if (a.mimeType.startsWith("image/")) {
const img = document.createElement("img");
img.src = src;
img.alt = a.filename ?? a.attachmentId;
transcriptEl.appendChild(img);
} else {
const link = document.createElement("a");
link.href = src;
link.target = "_blank";
link.textContent = a.filename ?? a.attachmentId;
transcriptEl.appendChild(link);
}
}
});Supported types & limits
- Images: JPEG, PNG, WebP, GIF, HEIC, HEIF.
- Documents: PDF.
- Max size: 20 MB.
Quick start — voice (ElevenLabs)
<script src="https://unpkg.com/@myflowai/[email protected]/dist/flowai.min.js"></script>
<script src="https://unpkg.com/@elevenlabs/client@latest/dist/lib.umd.js"></script>
<script>
const voice = new FlowAI.VoiceClient({
tenantId: "acme",
agentId: "support-v1", // optional
elevenlabsAgentId: "el_abc",
// The chat bundle externalizes @elevenlabs/client; load it yourself
// (above) and pass a tiny loader that returns it from window.
elevenlabsClientLoader: () => Promise.resolve(window.ElevenLabs),
});
voice.on("transcript", (t) => console.log(`${t.role}: ${t.text}`));
voice.on("audioState", (s) => console.log("audio:", s.state));
voice.on("start", (s) => console.log("session:", s.sessionId));
await voice.start();
</script>On start(), the SDK first calls POST /v1/sessions to create the
orchestrator session (seeded with the tenant's config), then starts the
ElevenLabs call — injecting session_id, tenant_id, and agent_id as
dynamic variables. The tenant's ElevenLabs agent must:
- Use the orchestrator's
/v1/chat/completionsendpoint as its Custom LLM. - Forward those dynamic variables as request headers — configure
X-Session-Id: {{session_id}},X-Tenant-Id: {{tenant_id}}, andX-Agent-Id: {{agent_id}}in the Custom LLM "Request headers" section so the orchestrator can resolve the session per turn.
To attach to a session you created yourself, pass sessionId (and agentId)
and the SDK skips the create call.
ChatClient — public event surface
The SDK exposes the assistant's streamed reply (token / final) plus a
minimal control-plane surface. Tool calls, KB retrievals, and other
wire-internal events are consumed internally and not surfaced.
| Event | Payload | When |
|---|---|---|
| ready | { sessionId } | Init handshake completed |
| token | { turnId, text } | Incremental assistant delta for the current message segment |
| message | { turnId, text } | A completed assistant message segment — its authoritative full text |
| final | { turnId, text, stopReason } | Turn boundary (no per-segment content; use for typing/inflight reset) |
| agent_message | { text, source: "human", at, messageId, attachments? } | A human supervisor sent a message (possibly with files/images) |
| transfer_to_human | { reason?, destination?, transferNote? } | Warm handoff to a human agent |
| error | { message } | Protocol-level error |
| close | { code, reason } | Socket closed |
A single turn can produce multiple assistant messages (one per graph node that yields text — e.g. a pre-tool "let me check…" followed by the answer). Render each as its own bubble:
- On
token, append the delta to the current bubble. - On
message, snap that bubble to the authoritativetextand seal it — the nexttokenstarts a fresh bubble. - On
final, the turn is over: clear the typing indicator / inflight state. Do not renderfinal.text(segment content already arrived viamessage).
let bubble = null;
chat.on("token", (t) => { (bubble ??= newBubble()).textContent += t.text; });
chat.on("message", (m) => { (bubble ??= newBubble()).textContent = m.text; bubble = null; });
chat.on("final", () => { bubble = null; stopTypingIndicator(); });Do not rely on
final.textbeing the whole turn — in a multi-node turn it carries only the last node's text. Usemessagefor content.
Behavior
Barge-in: calling
chat.send(text)while a turn is still streaming on the server first emits acancelframe, then submits the new message. The previous turn's events are dropped.Reconnect: transient closes (network drop, server shutdown 4503) trigger automatic reconnect with exponential backoff using the saved
sessionId— the conversation continues on a fresh task. Terminal closes (4400 / 4404 / 4422) do not retry.Persist session across page reloads — the SDK doesn't store anything; wire it yourself:
const saved = sessionStorage.getItem("flowai.sessionId") ?? undefined; const chat = new FlowAI.ChatClient({ tenantId, agentId, sessionId: saved }); chat.on("ready", (e) => sessionStorage.setItem("flowai.sessionId", e.sessionId));
