toruk-embed
v0.3.0
Published
TypeScript SDK for TORUK — typed client for prediction APIs, plus an embeddable chat widget
Readme
toruk-embed
The official TORUK SDK and embeddable chat widget for the web.
Starting in 0.2.x, toruk-embed ships two surfaces from the same package:
TorukClient— a typed TypeScript SDK for TORUK's prediction APIs. Unifiedemployees.*namespace for chatflows and agentflows (execute,stream,feedback,attach,vectorUpsert,getConfig,isStreamAvailable), explicitAuthConfig, response-envelope normalization, error mapping. First-class React bindings viatoruk-embed/react.- Embeddable chat widget (preserved from 0.1.x) —
registerWebComponents,init,initFull, etc. The SolidJS-based Web Component. Existing 0.1.x code keeps working without modification.
Table of Contents
- Installation
- SDK —
TorukClient← 0.3.0 rename:workflows.*→employees.* - PreviewerAPI —
createTorukPreviewer - React Bindings —
toruk-embed/react - Widget vs Headless
- Legacy Widget API ← preserved from 0.1.x
- Demo Server
- License
Installation
npm install toruk-embed
# or
pnpm add toruk-embed
# or
yarn add toruk-embedSDK — TorukClient
0.3.0 — rename:
workflows.*→employees.*. The typed TypeScript client for TORUK's prediction APIs. Old names (workflows,predict,workflowId,WORKFLOW_NOT_FOUND, …) remain as deprecated aliases for one minor cycle and emit a one-timeconsole.warnon first use. See Migration from 0.2.x andMIGRATION.mdfor the full mapping.
The SDK is a client for TORUK-CORE's prediction endpoint family. It handles authentication, header construction, response envelope normalization, and error mapping, so consumers don't have to wire raw HTTP calls.
Migration from 0.2.x
- await toruk.workflows.predict({ workflowId: 'chatflow_abc', message: 'Hi' });
+ await toruk.employees.execute({ taskId: 'chatflow_abc', message: 'Hi' });Quick reference:
| 0.2.x | 0.3.0 |
| ----------------------- | ---------------------- |
| toruk.workflows.* | toruk.employees.* |
| .predict(…) | .execute(…) |
| workflowId | taskId |
| WORKFLOW_NOT_FOUND | TASK_NOT_FOUND |
| WORKFLOW_BUILD_FAILED | TASK_BUILD_FAILED |
| WorkflowPredictInput | EmployeeExecuteInput |
Wire body and URL paths are unchanged — question, chatId, /api/v1/prediction/:id, etc. all stay the same. Only SDK names changed.
SDK Quick Start
import { TorukClient } from 'toruk-embed';
const toruk = new TorukClient({
baseUrl: 'https://toruk.company.com',
auth: { type: 'apiKey', apiKey: process.env.TORUK_API_KEY! },
});
// Run a chatflow OR an agentflow — same call, same shape
const result = await toruk.employees.execute({
taskId: 'chatflow_abc', // or 'agentflow_xyz'
message: 'Summarize this document',
chatId: 'chat_xyz', // optional — auto-generated if omitted
});
if (result.success) {
console.log(result.data.text); // bot reply
console.log(result.data.chatId); // session id
console.log(result.data.messageId); // optional message id
} else {
console.error(result.error.code, result.message);
}In Node-only contexts (server-to-server, CLIs, scripts), import from the dedicated entry to skip the browser-only widget code:
import { TorukClient, fromEnv } from 'toruk-embed/node';
const toruk = fromEnv();
// Reads TORUK_BASE_URL + TORUK_API_KEY (or TORUK_JWT) from process.envAuthentication
The SDK supports the three authentication modes that TORUK-CORE accepts on its prediction routes:
| Mode | When to use | Wire behavior |
| -------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| apiKey | Browser apps (task-bound key) or server-to-server (org-scoped key). The primary path. | Sends x-api-key. On GET requests, also appends ?apikey= as a query-param fallback so browser clients survive proxies that strip custom headers during CORS preflight. |
| jwt | Apps where the user is already logged into TORUK and the host has an access token. | Sends Authorization: Bearer <token>. Calls a host-supplied refresh callback once on a 401 and retries. |
| none | Calls a public task that doesn't require credentials. | Sends no auth headers. |
// API key (primary)
new TorukClient({
baseUrl: 'https://toruk.company.com',
auth: { type: 'apiKey', apiKey: 'tk_live_…' },
});
// JWT bearer with token rotation
new TorukClient({
baseUrl: 'https://toruk.company.com',
auth: {
type: 'jwt',
token: accessToken,
refresh: async () => await refreshAccessToken(), // called once on 401
},
});
// Public task (no credentials)
new TorukClient({
baseUrl: 'https://toruk.company.com',
auth: { type: 'none' },
});Note on organization scoping. The SDK does not send
x-organization-idheaders. TORUK-CORE derives the organization server-side from the API key or JWT membership — the client never needs to supply it.
Browser-distributed API keys
API keys can be used in the browser (TORUK-CORE accepts them via header and query-param fallback). When distributing keys to browsers:
- Use task-bound keys, not organization-scoped — a leaked key can only access that one task.
- Set a short
expiresAton the key and rotate via a host-side endpoint.
employees.execute
Run a chatflow or an agentflow without streaming. TORUK-CORE serves both entity types through the same endpoint, so the SDK accepts either UUID through the same method.
const result = await toruk.employees.execute({
taskId: 'chatflow_abc', // or 'agentflow_xyz'
message: 'Explain the uploaded document',
chatId: 'chat_xyz', // optional; UUID auto-generated if omitted
overrideConfig: { variables: { region: 'eu-west-1' } }, // optional passthrough
history: [{ role: 'user', content: 'Earlier turn' }], // optional
signal: controller.signal, // optional AbortSignal
});
if (result.success) {
result.data.chatId; // string — always present
result.data.text; // string | undefined — bot reply
result.data.messageId; // string | undefined
result.data.sourceDocuments; // unknown[] | undefined
result.data.usedTools; // unknown[] | undefined
result.data.agentReasoning; // unknown | undefined
} else {
result.error.code; // 'TASK_NOT_FOUND' | 'FORBIDDEN' | ...
result.error.details; // unknown — backend-supplied detail payload
result.message; // human-readable
}Input fields
| Field | Type | Notes |
| ---------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- |
| taskId | string | UUID of the chatflow or agentflow. Sent as the URL path parameter, not a body field. |
| message | string | The user message. Mapped to wire field question (TORUK-CORE's name for it). |
| chatId | string | Optional. Auto-generated as a UUID if omitted. |
| overrideConfig | Record<string, unknown> | Optional engine-side passthrough (variables, session overrides). |
| history | ChatHistoryItem[] | Optional turn history. |
| uploads | EmployeeUpload[] | Optional JSON uploads (URL or data-URI). For binary file uploads, use employees.attach. |
| leadEmail | string | Optional lead-capture email. |
| action | EmployeeAction | Optional humanInput continuation payload. |
| signal | AbortSignal | Optional. Cancels the request. |
| headers | Record<string, string> | Optional per-call custom headers. |
employees.stream
Stream tokens as they are generated via Server-Sent Events. Returns a Promise that resolves with { chatId, messageId } when the stream ends.
const controller = new AbortController();
const result = await toruk.employees.stream({
taskId: 'chatflow_abc',
message: 'Tell me a story',
chatId: 'chat_xyz', // optional — resume a session
signal: controller.signal, // optional — cancel mid-stream
onStart: ({ chatId, messageId }) => {
console.log('Stream started', chatId, messageId);
},
onToken: (token) => {
process.stdout.write(token); // called for each text chunk
},
onDone: ({ chatId, messageId }) => {
console.log('\nDone', chatId, messageId);
},
onError: (err) => {
console.error(err.code, err.message);
},
onEvent: (ev) => {
// catch-all — fires for every SSE frame: 'start', 'token', 'end'
},
});
console.log(result.chatId, result.messageId);
// Cancel at any time:
controller.abort();Non-streaming fallback. If the backend returns JSON instead of an SSE stream (task has isStreaming: false), employees.stream() automatically synthesizes onStart, onToken, and onDone from the JSON response — no change to the calling code needed.
All errors are surfaced as TorukSdkError. See Error Handling for the full code table.
employees.getConfig
Fetch the public chatbot config for a task (theme, welcome message, etc.). Maps to GET /api/v1/public-chatbotConfig/:id.
const result = await toruk.employees.getConfig('chatflow_abc');
if (result.success) {
console.log(result.data); // arbitrary config object
}employees.isStreamAvailable
Pre-flight check for whether a task supports streaming. Maps to GET /api/v1/chatflows-streaming/:id.
const result = await toruk.employees.isStreamAvailable('chatflow_abc');
if (result.success && result.data.isStreaming) {
// safe to call employees.stream
}employees.feedback
Submit thumbs-up / thumbs-down (with optional free-text content) for a specific message. Maps to POST /api/v1/feedback/:id.
const result = await toruk.employees.feedback({
taskId: 'chatflow_abc',
chatId: 'chat_xyz',
messageId: 'msg_123',
rating: 'thumbsUp', // or 'thumbsDown'
content: 'Nailed the cite.', // optional free-text
});
if (result.success) {
result.data.id; // backend-assigned feedback id
}employees.attach
Upload one or more files into a chat session (multipart). Maps to POST /api/v1/attachments/:id/:chatId.
Pass File (browser) or Blob (Node 18+) instances. The optional filenames array overrides per-position when passing raw Blobs without a name.
const result = await toruk.employees.attach({
taskId: 'chatflow_abc',
chatId: 'chat_xyz',
files: [pdfFile, screenshotBlob],
filenames: ['contract.pdf', 'screenshot.png'], // optional
});employees.vectorUpsert
Upsert one or more documents into the task's vector store for RAG (multipart). Maps to POST /api/v1/vector/upsert/:id.
Engine-specific fields (e.g. splitter, chunkSize) are passed through via fields.
const result = await toruk.employees.vectorUpsert({
taskId: 'chatflow_abc',
files: [docFile],
fields: { splitter: 'recursive', chunkSize: '1000' }, // optional
});Error Handling
The SDK distinguishes predictable errors (returned in the response envelope) from structural failures (thrown).
Predictable errors — response envelope
Returned without a throw. The result.success === false branch carries a typed error code.
const result = await toruk.employees.execute({ taskId: 'missing', message: 'hi' });
if (!result.success) {
switch (result.error.code) {
case 'TASK_NOT_FOUND':
// 404 — task ID is wrong
break;
case 'UNAUTHORIZED':
// 401 — bad/missing credentials
break;
case 'FORBIDDEN':
// 403 — auth OK but task not accessible
break;
case 'RATE_LIMITED':
// 429 — slow down
break;
// ... see below for the full list
}
}Known codes:
| Code | When |
| ----------------------- | ------------------------------------------- |
| TASK_NOT_FOUND | 404 — task does not exist |
| UNAUTHORIZED | 401 — auth failed |
| FORBIDDEN | 403 — auth OK but task not accessible |
| BAD_REQUEST | 400 — malformed request body |
| TASK_BUILD_FAILED | 500 — backend build error |
| STREAMING_UNAVAILABLE | 503 — streaming requested but not available |
| RATE_LIMITED | 429 — too many requests |
| UNKNOWN | other 4xx/5xx without a known backend code |
The legacy codes WORKFLOW_NOT_FOUND and WORKFLOW_BUILD_FAILED remain in the TorukSdkErrorCode union as deprecated string aliases so existing consumer switch statements still compile — but the SDK emits only the new codes. Update your switches before the next major release.
Structural failures — thrown
A TorukSdkError is thrown when the failure is structural and can't be expressed in the envelope: network down, request aborted, response parse failure.
import { TorukSdkError } from 'toruk-embed';
try {
await toruk.employees.execute({ taskId: 'x', message: 'hi' });
} catch (err) {
if (err instanceof TorukSdkError) {
err.code; // 'NETWORK' | 'ABORTED' | 'TIMEOUT' | 'MALFORMED_RESPONSE' | ...
err.status; // HTTP status if applicable
err.details; // backend-supplied detail payload if applicable
}
}Response Envelope
Every SDK method returns a discriminated union:
type TorukApiResponse<T> = { success: true; data: T } | { success: false; message: string; error: { code: string; details?: unknown } };Success responses from TORUK-CORE arrive unwrapped (just the bare execution result); the SDK wraps them into { success: true, data }. Error responses already carry the envelope shape; the SDK normalizes the error.code against the table above.
Subpath Entries
| Entry | Use when | What's exported |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| toruk-embed | Browser-bundler consumers (Vite, Next.js, Webpack). | TorukClient, TorukSdkError, types, AND the legacy widget surface (registerWebComponents, init, …). |
| toruk-embed/browser | Browser consumers who only want the SDK — no widget transitive imports. Smaller bundle. | TorukClient, TorukSdkError, types. |
| toruk-embed/node | Pure-Node consumers (server-to-server, CLIs, tests). Skips the widget code that loads solid-element at module top-level. | Same as /browser + the fromEnv() helper. |
| toruk-embed/previewer | Embed the TORUK chat widget with typed lifecycle controls. Prefer over legacy init() for new projects. | createTorukPreviewer, TorukPreviewerInstance, PreviewerMode, PreviewerMountOptions |
| toruk-embed/react | React apps — context provider + hook + <TorukPreviewer /> component. SSR-safe (no-ops on the server). | TorukProvider, useTorukClient, TorukPreviewer |
fromEnv helper (Node)
In Node-only contexts you can construct a client from environment variables:
import { fromEnv } from 'toruk-embed/node';
// Reads:
// TORUK_BASE_URL (required)
// TORUK_API_KEY (preferred for server-to-server) OR
// TORUK_JWT (alternative)
const toruk = fromEnv();Throws a plain Error with a clear message if either variable is missing — misconfiguration fails loudly at boot instead of silently at the first request.
SDK ↔ Backend wire mapping
The SDK uses developer-friendly field names; TORUK-CORE uses different names on the wire. The translation happens at the boundary.
| SDK input field | Wire body field |
| ------------------------------------------------------------- | -------------------------------------------------- |
| taskId | (path :id — not a body field) |
| message | question |
| stream (set by employees.stream) | streaming |
| chatId | chatId (passthrough; auto-generated if absent) |
| overrideConfig, history, uploads, leadEmail, action | (passthrough verbatim) |
| Wire response field | SDK envelope path |
| ------------------------------------------------------ | --------------------------------------------------- |
| chatId | result.data.chatId |
| text | result.data.text |
| chatMessageId | result.data.messageId (renamed at the boundary) |
| sourceDocuments, usedTools, agentReasoning, etc. | result.data.<same name> (passthrough) |
This mapping is the spec — it does not change in patch releases.
PreviewerAPI — createTorukPreviewer
New in 0.2.3. Prefer this over the legacy
init()/initFull()helpers for new projects.
createTorukPreviewer mounts the TORUK chat widget with typed lifecycle controls, without requiring you to call registerWebComponents() manually.
import { createTorukPreviewer } from 'toruk-embed/previewer';
const previewer = createTorukPreviewer({
baseUrl: 'https://toruk.company.com',
auth: { type: 'apiKey', apiKey: 'tk_live_…' },
});previewer.mount(target, options) — returns Promise<void>
Awaiting the returned Promise ensures the widget element is in the DOM before you inspect or interact with it.
// Floating bubble (default) — appended to document.body
await previewer.mount(null, {
taskId: 'chatflow_abc',
mode: 'floating', // default; 'inline' | 'modal' | 'floating'
});
// Full-page inline inside a container
await previewer.mount('#chat-host', {
taskId: 'chatflow_abc',
mode: 'inline',
variant: 'neptune', // 'luna' | 'erebus' | 'neptune'
withSidebar: true,
});
// Modal overlay with backdrop
await previewer.mount(null, {
taskId: 'chatflow_abc',
mode: 'modal',
});Lifecycle methods
previewer.unmount(); // remove widget from DOM
previewer.open(); // show widget (floating/modal)
previewer.close(); // hide widget (floating/modal)
previewer.updateConfig({ taskId: 'xyz' }); // hot-update props in place
const el = previewer.element; // mounted DOM element, or undefinedMode reference
| mode | DOM element | target |
| ------------ | -------------------------------------------- | -------------------------------------------- |
| 'floating' | <toruk-chatbot> (bubble) | ignored — mounted on document.body |
| 'inline' | <toruk-fullchatbot> | CSS selector or Element |
| 'modal' | <toruk-fullchatbot> inside a fixed overlay | ignored — overlay mounted on document.body |
Mount options
| field | type | description |
| ------------- | ------------------------------------- | -------------------------------------------- |
| taskId | string | Chatflow or agentflow UUID |
| mode | 'floating' \| 'inline' \| 'modal' | Defaults to 'floating' |
| variant | 'luna' \| 'erebus' \| 'neptune' | Full-page visual variant (inline/modal only) |
| withSidebar | boolean | Show sidebar in full-page modes |
| theme | Record<string, unknown> | Theme override passed to the widget |
| taskConfig | Record<string, unknown> | Per-task engine config |
| onRequest | (req: RequestInit) => Promise<void> | Extra request hook (composes with auth) |
React Bindings — toruk-embed/react
First-class React surface: a context provider for sharing a TorukClient, a hook to access it, and a <TorukPreviewer /> component that wraps the imperative previewer. SSR-safe (no-ops on the server; mounts in useEffect after hydration).
react and react-dom are optional peer dependencies. Only consumers who import toruk-embed/react need them installed.
TorukProvider
Wrap your tree once. Pass either config (the provider constructs the client) or client (you manage it):
import { TorukProvider } from 'toruk-embed/react';
export function App() {
return (
<TorukProvider config={{ baseUrl: 'https://toruk.company.com', auth: { type: 'apiKey', apiKey: 'tk_live_…' } }}>
<Page />
</TorukProvider>
);
}useTorukClient
Access the shared client anywhere below the provider. Throws if used outside a TorukProvider.
import { useTorukClient } from 'toruk-embed/react';
function SendButton() {
const toruk = useTorukClient();
const onClick = async () => {
const r = await toruk.employees.execute({ taskId: 'chatflow_abc', message: 'Hi' });
// ...
};
return <button onClick={onClick}>Send</button>;
}<TorukPreviewer />
Drop-in component that mounts the chat widget. Sources auth from the surrounding provider.
import { TorukPreviewer } from 'toruk-embed/react';
// Floating bubble — appended to <body>
<TorukPreviewer taskId="chatflow_abc" />
// Inline, full-page
<TorukPreviewer
taskId="chatflow_abc"
mode="inline"
variant="neptune"
withSidebar
className="chat-host"
style={{ height: 600 }}
fallback={<Skeleton />}
/>| Prop | Type | Description |
| ------------- | ------------------------------------- | ----------------------------------------------------------- |
| taskId | string | Required — chatflow or agentflow UUID. |
| mode | 'floating' \| 'inline' \| 'modal' | Defaults to 'floating'. |
| variant | 'luna' \| 'erebus' \| 'neptune' | Full-page variant for inline/modal. |
| withSidebar | boolean | Show sidebar in full-page modes. |
| className | string | Class forwarded onto the host <div> (inline/modal only). |
| style | CSSProperties | Style forwarded onto the host <div> (inline/modal only). |
| fallback | ReactNode | Rendered while the widget mounts (skeleton, spinner, etc.). |
| theme | Record<string, unknown> | Theme override passed to the widget. |
| taskConfig | Record<string, unknown> | Per-task engine config. |
| onRequest | (req: RequestInit) => Promise<void> | Extra request hook (composes with auth). |
Widget vs Headless
| | TorukClient (headless) | createTorukPreviewer / init (widget) |
| ------------------ | -------------------------------------------- | -------------------------------------------------------------------- |
| DOM dependency | None — Node.js, edge, workers | Browser only |
| Auth | Typed AuthConfig — no localStorage | Reads from onRequest hook or localStorage |
| UI | You own the UI | Ships the TORUK SolidJS chat UI |
| Use case | Custom UI, server-side calls, data pipelines | Drop-in embeds for web pages |
| Entry point | toruk-embed or toruk-embed/node | toruk-embed/previewer (preferred) or toruk-embed (legacy init) |
| Mount control | n/a | mount() / unmount() / open() / close() |
| Streaming | employees.stream() with callbacks | Handled internally by the widget UI |
Use TorukClient when you need to call prediction APIs directly — from a Node.js server, a custom React/Vue UI, or a data pipeline. Use createTorukPreviewer when you want to drop the full chat UI into a web page with minimal code.
Legacy Widget API
Preserved unchanged from
[email protected]. The widget renders a fully-featured chat UI as a Web Component, built with SolidJS and compiled to native Custom Elements. Existing widget code works in 0.2.x without modification.
Usage — Script Tag (CDN)
Embed the chatbot on any page with a single script tag. The script registers the Web Components and exposes window.Chatbot.
<script type="module">
import Chatbot from 'https://unpkg.com/toruk-embed/dist/web.js';
Chatbot.init({
chatflowid: 'YOUR_CHATFLOW_ID',
apiHost: 'https://your-toruk-api.com',
});
</script>Or with UMD (no ES modules required):
<script src="https://unpkg.com/toruk-embed/dist/web.umd.js"></script>
<script>
window.TorukEmbed.init({
chatflowid: 'YOUR_CHATFLOW_ID',
apiHost: 'https://your-toruk-api.com',
});
</script>Usage — npm Module
Import and use inside any JS/TS bundler project (Vite, Next.js, webpack, etc.).
import { registerWebComponents, init } from 'toruk-embed';
// Register custom elements once — SSR-safe (no-op on server)
registerWebComponents();
// Mount the bubble chatbot
init({
chatflowid: 'YOUR_CHATFLOW_ID',
apiHost: 'https://your-toruk-api.com',
theme: {
button: { backgroundColor: '#3385FF' },
chatWindow: {
title: 'TORUK Assistant',
welcomeMessage: 'Hello! How can I help?',
},
},
});Next.js (SSR) — use dynamic import:
// app/components/ChatWidget.tsx
'use client';
import { useEffect } from 'react';
export default function ChatWidget() {
useEffect(() => {
import('toruk-embed').then(({ registerWebComponents, init }) => {
registerWebComponents();
init({
chatflowid: 'YOUR_CHATFLOW_ID',
apiHost: 'https://your-toruk-api.com',
});
});
}, []);
return null;
}Templates
Three display modes are available:
| Template | Description |
| -------- | -------------------------------------------- |
| bubble | Floating chat bubble in the corner (default) |
| full | Full-page embedded chat with sidebar |
| popup | Popup overlay triggered by user action |
Use init() for bubble, initFull() for full-page, or initFromConfig() for flexible config-driven setup.
API Reference
init()
Mounts the bubble chatbot. Removes any existing widget first.
init(props: BotProps): voidimport { registerWebComponents, init } from 'toruk-embed';
registerWebComponents();
init({
chatflowid: 'abc-123',
apiHost: 'https://api.example.com',
});initFull()
Mounts the full-page chatbot.
initFull(props: BotProps & {
id?: string;
variant?: 'luna' | 'erebus' | 'neptune';
fullPageStyle?: 'luna' | 'erebus' | 'neptune';
withSidebar?: boolean;
}): voidimport { registerWebComponents, initFull } from 'toruk-embed';
registerWebComponents();
initFull({
chatflowid: 'abc-123',
apiHost: 'https://api.example.com',
variant: 'neptune',
withSidebar: true,
});initFromConfig()
Flexible initializer — accepts a TemplateConfig object. Used for dynamic/server-driven configuration.
initFromConfig(config: {
template: 'bubble' | 'chatbubble' | 'full' | 'fullPage';
flowId: string;
apiHost?: string;
theme?: BubbleTheme;
variant?: 'luna' | 'erebus' | 'neptune';
fullPageStyle?: 'luna' | 'erebus' | 'neptune';
withSidebar?: boolean;
chatflowConfig?: Record<string, unknown>;
observersConfig?: observersConfigType;
onRequest?: (request: RequestInit) => Promise<void>;
}): voidimport { registerWebComponents, initFromConfig } from 'toruk-embed';
registerWebComponents();
initFromConfig({
template: 'fullPage',
flowId: 'abc-123',
apiHost: 'https://api.example.com',
variant: 'luna',
withSidebar: true,
});destroy()
Removes the mounted widget from the DOM.
destroy(): voidimport { destroy } from 'toruk-embed';
destroy();registerWebComponents()
Registers the <toruk-chatbot> and <toruk-fullchatbot> custom elements (plus the deprecated <flowise-chatbot> / <flowise-fullchatbot> aliases for backward compatibility). Must be called once before init(), initFull(), or initFromConfig().
- Safe to call on SSR/Node — is a no-op when
windowis undefined. - Safe to call multiple times — custom elements self-skip re-registration.
import { registerWebComponents } from 'toruk-embed';
registerWebComponents();BotProps
All properties accepted by init() and initFull().
| Prop | Type | Required | Description |
| ---------------------------------------------- | ------------------------------------- | -------- | -------------------------------------------------------------- |
| chatflowid | string | ✅ | The chatflow UUID from your TORUK instance |
| apiHost | string | — | Base URL of the TORUK API (e.g. https://api.example.com) |
| onRequest | (req: RequestInit) => Promise<void> | — | Hook to modify every outgoing request (add auth headers, etc.) |
| chatflowConfig | Record<string, unknown> | — | Extra config passed to the chatflow |
| observersConfig | observersConfigType | — | Reactive callbacks for messages, loading state |
| theme.button.backgroundColor | string | — | Bubble button background color (hex) |
| theme.button.iconColor | string | — | Bubble button icon color (hex) |
| theme.chatWindow.title | string | — | Chat window header title |
| theme.chatWindow.titleBackgroundColor | string | — | Header background color |
| theme.chatWindow.titleTextColor | string | — | Header text color |
| theme.chatWindow.titleAvatarSrc | string | — | URL for the avatar shown in the header |
| theme.chatWindow.welcomeMessage | string | — | First message shown before user types |
| theme.chatWindow.heroGreeting | string | — | Large greeting text in the hero section |
| theme.chatWindow.heroHeadline | string | — | Subtitle under the greeting |
| theme.chatWindow.backgroundColor | string | — | Chat window background color |
| theme.chatWindow.fontSize | number | — | Base font size (px) |
| theme.chatWindow.userMessage.backgroundColor | string | — | User bubble background |
| theme.chatWindow.userMessage.textColor | string | — | User bubble text color |
| theme.chatWindow.botMessage.backgroundColor | string | — | Bot bubble background |
| theme.chatWindow.botMessage.textColor | string | — | Bot bubble text color |
| theme.chatWindow.textInput.backgroundColor | string | — | Input area background |
| theme.chatWindow.textInput.textColor | string | — | Input text color |
| theme.chatWindow.textInput.sendButtonColor | string | — | Send button color |
| theme.chatWindow.textInput.placeholder | string | — | Input placeholder text |
| theme.chatWindow.showTitle | boolean | — | Show/hide the title bar |
| theme.chatWindow.showAgentMessages | boolean | — | Show/hide agent reasoning messages |
| theme.chatWindow.starterPrompts | string[] | — | Suggested prompts shown before first message |
| theme.chatWindow.clearChatOnReload | boolean | — | Clear chat history on page reload |
| theme.chatWindow.renderHTML | boolean | — | Allow HTML rendering in bot messages |
| theme.customCSS | string | — | Raw CSS injected into the widget shadow scope |
| isFullPage | boolean | — | Internal flag; set automatically by initFull() |
Theme Variants
Full-page mode (initFull / initFromConfig with template: 'fullPage') supports three visual variants:
| Variant | Description |
| --------- | ---------------------------------------------------------------------- |
| luna | Clean light/dark default layout |
| erebus | Dark gradient with glowing accents |
| neptune | Deep-space dark with animated WebGL laser beam (Three.js; lazy-loaded) |
initFull({
chatflowid: 'abc-123',
apiHost: 'https://api.example.com',
variant: 'neptune',
withSidebar: true,
});Note: The Neptune variant loads Three.js dynamically only when rendered. Users of the
lunaanderebusvariants do not download Three.js.
observersConfig
Subscribe to reactive state changes inside the widget.
import { registerWebComponents, init } from 'toruk-embed';
registerWebComponents();
init({
chatflowid: 'abc-123',
apiHost: 'https://api.example.com',
observersConfig: {
observeUserInput: (input) => {
console.log('User typed:', input);
},
observeLoading: (isLoading) => {
console.log('Loading:', isLoading);
},
observeMessages: (messages) => {
console.log('Messages updated:', messages);
},
},
});| Observer | Payload type | Fires when |
| ------------------ | --------------- | ----------------------------------------- |
| observeUserInput | string | User types in the input box |
| observeLoading | boolean | Bot starts or stops generating a response |
| observeMessages | MessageType[] | Any message is added or updated |
Demo Server
A local proxy server for development (keeps your API key out of the browser) lives in demo/.
cd demo
cp .env.example .env
# Fill in API_HOST and TORUK_API_KEY in .env
npm install
npm start
# Opens http://localhost:3001License
MIT — see LICENSE.
