# Help AI Elements: the full SDK docs (@helpai/elements@1.15.2)
# Quickstart
From zero to working chat widget in under 5 minutes.
---
## 1. Get your browser deployment credentials
1. Sign up at [help.ai](https://help.ai) if you haven't.
2. Go to **Dashboard → Deployments**.
3. Click **New deployment** — give it a name (e.g. "Marketing site").
4. Copy the public key (`agent_pk_abc123...`) and deployment UUID from the
generated install snippet.
> The dashboard generates a copy-paste snippet for you — feel free to use
> that directly. The rest of this guide assumes you want to know what
> each piece does.
---
## 2. Pick your install path
The widget works four ways. Pick whichever matches your site:
- **Static HTML** (marketing site, WordPress, landing page) → [path A](#path-a--static-html).
- **Async-loading site** (uses Google Tag Manager, Segment, etc.) → [path B](#path-b--async-loader).
- **React / Next.js / Remix** → the React kit in [path C](#path-c--npm).
- **Vue / Svelte / Nuxt** (any framework with a build step) → [path C](#path-c--npm).
- **Plain HTML but prefer a declarative tag** → [path D](#path-d--web-component).
All four paths land on the same widget with the same behavior. Mix and
match as needed.
---
## Path A — static HTML
Paste this into your `
`:
```html
```
That's it. Reload your page and look at the bottom-right corner.
**What `defer` does** — the script downloads in the background and runs
after the HTML parses. Zero impact on first contentful paint. Always
use `defer`.
**Available attributes** — every config option is available as a
kebab-case `data-*` attribute. Common ones:
```html
```
Full list: [Installation § script data attributes](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/installation.md#path-a--script-data--auto-boot).
---
## Path B — async loader
If your site already uses a tag-manager-style loader pattern (Google
Analytics, Segment, Intercom, etc.), you'll recognize this shape:
```html
```
**What this does:**
1. Defines `window.HelpAI` as a queue until the real bundle loads.
2. Kicks off the async script download.
3. Queues an `init` call — it'll run as soon as the bundle is ready.
You can stack more queued calls:
```js
HelpAI('init', { publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
HelpAI('open'); // auto-opens the panel once init finishes
```
Same behavior as path A, just split into two script tags so you can
load asynchronously.
---
## Path C — NPM
For sites with a build step (Next.js, Vite, Remix, Nuxt, SvelteKit,
Astro…):
```bash
npm install @helpai/elements
# or: yarn add @helpai/elements
# or: pnpm add @helpai/elements
# or: bun add @helpai/elements
```
### React, Next.js, Remix
The React kit renders on the server safely and is already `'use client'`; put it in the root layout:
```tsx
// app/layout.tsx
import { HelpAI } from '@helpai/elements/react';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
`useHelpAI()` in any component returns `{ state, ask, open, close, reset }`.
### Vue / Svelte / Angular / any bundler
Call `init(...)` once on mount and `destroy()` on unmount, or use the `` element (path D).
Full framework-by-framework walkthrough: [Framework integrations](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/frameworks.md).
---
## Path D — web component
Declarative tag that works in any framework or plain HTML:
```html
```
Attributes mirror the options. Changing an attribute at runtime
re-renders the widget with the new value.
Web components shine for:
- In-app help centers (`mode="inline"` inside a sidebar).
- CMS-built pages where you can paste HTML but not run JS.
- Declarative "just put the widget here" integrations.
---
## 3. Verify it's working
Open your page and look at the bottom-right. You should see a small
round chat bubble with an accent color.
- **Click it** → a panel opens with a greeting from the assistant.
- **Type a message** → the assistant replies token-by-token.
- **Close the panel** → the bubble returns.
- **Reload the page** → your chat history stays intact.
Something not showing up? → [Troubleshooting](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/troubleshooting.md).
---
## 4. Common next steps
- **Match your brand** — [Theming](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/theming.md) has every color,
radius, and font knob.
- **Change the launcher label / icon** — [`launcher` options](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/configuration.md#launcher).
- **Know who the visitor is** — pass `userContext: { id, email, name }`
(and `pageContext: { area }`) for personalization.
- **Forward chat events to analytics** — [Events & hooks](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/events-and-hooks.md).
- **Add a second widget** (one for sales, one for support) — [Recipes
§ Multiple widgets](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/recipes.md#multiple-widgets-on-one-page).
---
## TypeScript users
Types ship with the NPM package — no separate `@types` install needed:
```ts
import { init, mount, destroy, hook, type ChatWidgetOptions, type ChatWidgetInstance, type EventMap, type EventName } from '@helpai/elements';
```
Every option is autocomplete-friendly. Hover a field in your IDE to see
its doc.
---
## Environment variables
For server-rendered apps, expose the public key and deployment ID through the
framework's public environment variables. The values are designed for browser
installation; the backend accepts the authenticated pair only when both match
the same deployment:
| Framework | Env var prefix |
| ----------------------- | ---------------------- |
| Next.js | `NEXT_PUBLIC_*` |
| Vite / SvelteKit / Nuxt | `VITE_*` or `PUBLIC_*` |
| Remix | read from `window.ENV` |
| Create React App | `REACT_APP_*` |
Full security notes: [Security & privacy](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/security-privacy.md).
---
# Installation
Deep-dive for every install path. See [Quickstart](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/quickstart.md)
for the 5-minute version.
---
## Choose your path
| Path | Best for | Delivery |
| ----------------------------------------------------------------- | --------------------------------------------------- | ---------- |
| [A. `
```
### Attributes
The zero-JavaScript path intentionally exposes a small, flat subset of
`ChatWidgetOptions`. These are the attributes parsed by the current bundle:
| Attribute | Maps to | Example |
| ---------------------------------------------- | --------------------------------- | ------------------------------- |
| `data-public-key` | `publicKey` | `agent_pk_...` |
| `data-ai-agent-deployment-id` | `aiAgentDeploymentId` | deployment UUID |
| `data-base-url` | `baseUrl` | `https://help.ai` |
| `data-agent-api-base-url` | `agentApiBaseUrl` | `http://localhost:3069` |
| `data-api-base-url` | `dataApiBaseUrl` | `http://localhost:3071` |
| `data-theme` | `theme` | `auto` / `light` / `dark` |
| `data-mode` / `data-position` | `presentation` | `floating` / `bottom-right` |
| `data-iframe` | `presentation.iframe` | `true` |
| `data-width` / `data-height` | `presentation.size` | `420px` / `640px` |
| `data-response-mode` | `behavior.responseMode` | `streaming` / `buffered` |
| `data-start-minimized` | `behavior.startMinimized` | `false` |
| `data-show-reasoning` | `behavior.showReasoning` | `true` |
| `data-show-tool-calls` | `behavior.showToolCalls` | `true` |
| `data-show-sources` | `behavior.showSources` | `true` |
| `data-disable-message-feedback` | `behavior.disableMessageFeedback` | `true` |
| `data-auto-send-suggestions` | `behavior.autoSendSuggestions` | `true` |
| `data-pop-out-url` | `behavior.popOutUrl` | `https://chat.example.com` |
| `data-locale` | `i18n.locale` | `fr-FR` |
| `data-launcher-variant` / `-size` / `-icon` | `launcher` fields | `pill` / `lg` / `spark` |
| `data-launcher-label` / `-hidden` | `launcher` fields | `Ask AI` / `true` |
| `data-callout-text` / `-delay` | `launcher.callout` | `Hi! 👋` / `2000` |
| `data-actions` | `header.actions` (CSV) | `theme,history,close` |
| `data-features-disable-file-upload` | `features.disableFileUpload` | `true` |
| `data-features-voice` | `features.voice` | `local` / `server` / `disabled` |
| `data-endpoint-upload` / `-transcribe` | `endpoints` | `/pai/upload` |
| `data-user-id` / `-email` / `-name` | `userContext` scalar fields | `user_42` |
| `data-user-context` | `userContext` extras (JSON) | `'{"plan":"pro"}'` |
| `data-page-area` / `-url` / `-path` / `-title` | `pageContext` scalar fields | `support` |
| `data-page-context` | `pageContext` extras (JSON) | `'{"experiment":"b"}'` |
Unknown attributes are ignored. Full option semantics:
[Configuration](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/configuration.md).
> Explicit scalar attributes (`data-user-id`, `data-page-area`, …) win
> over keys in the `data-user-context` / `data-page-context` JSON. For
> SPAs where the visitor logs in after load or navigates between pages,
> set context dynamically with
> [`instance.setUserContext(...)` / `setPageContext(...)`](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/configuration.md#setting-context-after-login--on-navigation).
> The `data-user-*` / `data-page-*` attributes are reactive — updating
> one re-handshakes with the same anonymous visitor/conversation. This
> context is informational for the AI, never an auth credential.
### Load without mounting
The script tag mounts the widget only when it carries `data-ai-agent-deployment-id`. Leave that attribute out to
load the bundle **without** mounting, then call `init()` from JavaScript when you're ready:
```html
```
### Multi-widget
For two widgets on one page, load the bundle once and call `mount()` with
distinct `widgetId` values:
```js
HelpAI.mount({ widgetId: 'support', publicKey: SUPPORT_KEY, aiAgentDeploymentId: SUPPORT_DEPLOYMENT_ID });
HelpAI.mount({ widgetId: 'sales', publicKey: SALES_KEY, aiAgentDeploymentId: SALES_DEPLOYMENT_ID });
```
---
## Path B — async loader snippet
Tag-manager-style snippet. Defers the bundle download without blocking
first paint, while letting your page queue commands immediately.
```html
```
### Queueable commands
| Call | What it does |
| ----------------------------------------- | ---------------------------------------------------------------- |
| `HelpAI('init', opts)` | Mount once; update the same widget id |
| `HelpAI('mount', opts)` | Mount fresh; replace the same widget id |
| `HelpAI('destroy')` | Unmount all live widgets |
| `HelpAI('open')` | Open the default widget's panel |
| `HelpAI('close')` | Close the default widget's panel |
| `HelpAI('expand')` | Toggle the default widget's expanded tier |
| `HelpAI('ask', text, { send })` | Open the chat with `text` in the composer; `send: true` sends it |
| `HelpAI('on', eventName, fn)` | Subscribe to a default-widget event |
| `HelpAI('update', { /* patch */ })` | Update the default widget's options |
| `HelpAI('hook', widgetId, eventName, fn)` | Register a global lifecycle hook |
### Pre-boot hooks
`window.HelpAIHooks` is a pre-boot queue for event listeners. Push
arrays or objects to it before the bundle arrives:
```html
```
Full details: [Events & hooks](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/events-and-hooks.md#global-lifecycle-hooks).
### After boot
Once the bundle loads, `window.HelpAI` becomes a real API:
```js
window.HelpAI.init(opts)
window.HelpAI.destroy(widgetId?)
window.HelpAI.getInstance(widgetId?) // returns handle or null
window.HelpAI.getAllInstances()
window.HelpAI.hook(widgetId, event, fn)
window.HelpAI.open() / close() / expand()
window.HelpAI.version // semver string
window.HelpAI.brand // { id, displayName, cdnUrl, ... }
```
---
## Path C — NPM package
For sites with a bundler. Full framework-specific walkthroughs:
[Framework integrations](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/frameworks.md).
### Install
```bash
npm install @helpai/elements
# or
yarn add @helpai/elements
# or
pnpm add @helpai/elements
# or
bun add @helpai/elements
```
### Entry points
The package ships two entries:
```ts
// Main entry — everything except the web component.
import { init, mount, destroy, hook } from '@helpai/elements';
// Sub-entry — registers as a custom element.
import '@helpai/elements/web-component';
```
Tree-shaking: the main entry has zero mount-on-import side effects.
Unused exports are dropped by your bundler. Importing only types costs
zero runtime bytes.
### Minimal React usage
```tsx
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { useEffect, useRef } from 'react';
export function Chat() {
const instance = useRef(null);
useEffect(() => {
instance.current = init({
publicKey: import.meta.env.VITE_HELPAI_KEY,
aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID,
});
return () => instance.current?.destroy();
}, []);
return null; // the widget renders its own DOM outside your tree
}
```
### Typed options
Autocomplete for every option:
```ts
import type { ChatWidgetOptions } from '@helpai/elements';
const opts: ChatWidgetOptions = {
publicKey: 'agent_pk_...',
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
presentation: { mode: 'floating' },
theme: { accent: '#ff3e00' },
launcher: { variant: 'pill', label: 'Ask AI' },
// ...
};
```
### SSR
The widget is client-only. Never call `init()` on the server. In
Next.js app router, wrap your component with `'use client'` (shown
above). In Remix / SvelteKit, use the `onMount` / `useEffect`
equivalent to defer init until the browser.
---
## Path D — Web Component
Declarative tag. Works in any framework or plain HTML.
### Load + use
```html
```
### Attributes
Same parser fields as `
```
Pinning means you control upgrade timing. The floating URL means you
get new features + bug fixes automatically. Both are supported; most
customers use the floating URL for marketing sites and pinned
versions for mission-critical apps.
---
## Path E — Standalone ESM bundle (no build, no NPM install)
For pages without a build step (plain HTML, MDX, sandbox demos, code
playgrounds, AI-generated UIs), load the pre-bundled ESM file shipped
with every release. `elements.esm.js` is fully self-contained — every
dependency (preact, signals, streaming-markdown) is inlined. No bare
specifiers, no import maps, no `+esm` magic. Drop into any page with
`
```
### Web component (declarative tag)
The web-component bundle self-registers `` on import:
```html
```
### File catalog (every `dist//` ships these)
| File | Purpose | Use with |
| --------------------------------- | ------------------------------------------------------ | --------------------------------------- |
| `elements.js` | IIFE bundle, defines `window.HelpAI` | Plain `
```
- jsDelivr serves a pinned version as `Cache-Control: public, max-age=31536000,
immutable` — cached for a year, global edge, free.
- **Always pin an exact version** for production. `@latest` / ranges (`@0.56`)
are mutable (jsDelivr revalidates ~daily) **and** auto-update — a published
patch can change behavior under you.
- Trade-off: the customer must **bump the version in their snippet to update**,
and every release re-downloads the whole ~107 KB (one self-contained file).
- Speed it up on pages you control: `` (and optionally `rel="modulepreload"`).
### Option 2 — your own CDN behind a stable URL (recommended; simplest for customers)
Point a CDN (`cdn.help.ai`, a Cloudflare/CloudFront origin, …) at the built
`dist//` assets and give customers **one URL that never changes**:
```html
```
You ship updates **behind** that stable URL — the customer's page is frozen. The
caching rule is the whole trick:
| Asset | `Cache-Control` | Why |
| ------------------------------------- | -------------------------------------- | -------------------------------------------- |
| The stable entry (`elements.js`) | `public, max-age=300, must-revalidate` | Short TTL so a deploy propagates in minutes. |
| Content-hashed chunks (`*..js`) | `public, max-age=31536000, immutable` | Hash in the name → safe to cache forever. |
Plus Brotli/Gzip, HTTP/2+3, and (optionally) `integrity="sha384-…"` SRI hashes.
### Automatic chunking (zero customer changes)
The value of Option 2 is the **stable URL** above — the customer's snippet never
changes and you own caching + region. Chunking is a further, _optional_ step on
top (usually **not** worth it — see the caveat below). With **content-hashed
chunk filenames served from a version-less path**, an unchanged chunk keeps the
**same URL** across releases, so the browser reuses it — only the chunks that
actually changed re-download. To get there:
1. Build ESM with code-splitting on (esbuild/tsdown `splitting: true`, `format:
"esm"`) and `[hash]` filenames. The thin entry imports the chunks.
2. Add lazy `import()` boundaries at the **optional surfaces** — messenger tabs
(Home/Help/News), forms/HITL, voice. esbuild emits each as its own chunk and
hoists shared code (e.g. Preact) into a common chunk automatically. Core chat
loads first → smaller first paint; each feature caches independently.
3. The customer keeps the single stable `
```
```vue
```
### Web component approach
Vue 3 supports custom elements natively. Register the web-component
sub-entry, then use the tag:
```ts
// main.ts
import { createApp } from 'vue';
import '@helpai/elements/web-component';
import App from './App.vue';
const app = createApp(App);
// Tell Vue to treat `web-ai-chat` as a custom element (not a Vue component)
app.config.compilerOptions.isCustomElement = (tag) => tag === 'web-ai-chat';
app.mount('#app');
```
```vue
```
---
## Nuxt 3
```ts
// plugins/web-ai-chat.client.ts
import { init } from '@helpai/elements';
export default defineNuxtPlugin((nuxtApp) => {
const chat = init({
publicKey: useRuntimeConfig().public.helpaiKey,
aiAgentDeploymentId: useRuntimeConfig().public.helpaiDeploymentId,
});
nuxtApp.hook('app:beforeMount', () => {
// Widget already mounted by init(). This hook is just a reference.
});
// Clean up on app tear-down (rarely triggered in SPA navigation)
nuxtApp.hook('app:suspense:resolve', () => {
// No-op — widget stays across navigations
});
return {
provide: {
chat,
},
};
});
```
```ts
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
helpaiKey: process.env.NUXT_PUBLIC_HELPAI_KEY,
helpaiDeploymentId: process.env.NUXT_PUBLIC_HELPAI_DEPLOYMENT_ID,
},
},
});
```
Note the `.client.ts` suffix — ensures the plugin only runs in the
browser.
---
## Svelte / SvelteKit
**SvelteKit**:
```svelte
```
**Plain Svelte** (no SvelteKit):
```svelte
```
### Web component in Svelte
Svelte handles custom elements natively:
```svelte
```
---
## Astro
Astro is SSR-first; wrap the widget in a client directive:
```astro
---
// src/components/Chat.astro
const publicKey = import.meta.env.PUBLIC_HELPAI_KEY;
const aiAgentDeploymentId = import.meta.env.PUBLIC_HELPAI_DEPLOYMENT_ID;
---
```
```astro
---
// src/layouts/Layout.astro
import Chat from '../components/Chat.astro';
---
```
Or use the web component (even simpler — no JS needed):
```astro
---
import '@helpai/elements/web-component';
---
```
---
## Remix
Remix (and React Router 7) render on the server, which the [React kit](#react) handles: pass the keys from the loader.
```tsx
// app/root.tsx
import { HelpAI } from '@helpai/elements/react';
import { Outlet, useLoaderData } from '@remix-run/react';
export async function loader() {
return {
helpaiKey: process.env.HELPAI_PUBLIC_KEY ?? '',
helpaiDeploymentId: process.env.HELPAI_DEPLOYMENT_ID ?? '',
};
}
export default function App() {
const { helpaiKey, helpaiDeploymentId } = useLoaderData();
return (
{helpaiKey && helpaiDeploymentId && }
);
}
```
---
## SolidJS
```tsx
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { onMount, onCleanup } from 'solid-js';
export function Chat() {
let instance: ChatWidgetInstance | null = null;
onMount(() => {
instance = init({ publicKey: import.meta.env.VITE_HELPAI_KEY, aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID });
});
onCleanup(() => {
instance?.destroy();
});
return null;
}
```
Solid also supports custom elements natively:
```tsx
import '@helpai/elements/web-component';
export default function App() {
return ;
}
```
---
## Angular
Angular needs `CUSTOM_ELEMENTS_SCHEMA` to accept the custom tag:
```ts
// src/app/app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@helpai/elements/web-component';
@NgModule({
// ...
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}
```
```html
```
For programmatic control:
```ts
// chat.service.ts
import { Injectable } from '@angular/core';
import { init, type ChatWidgetInstance } from '@helpai/elements';
import { environment } from '../environments/environment';
@Injectable({ providedIn: 'root' })
export class ChatService {
private instance: ChatWidgetInstance | null = null;
mount() {
if (!this.instance) {
this.instance = init({ publicKey: environment.helpaiKey, aiAgentDeploymentId: environment.helpaiDeploymentId });
}
}
destroy() {
this.instance?.destroy();
this.instance = null;
}
open() {
this.instance?.open();
}
}
```
---
## WordPress
Add the snippet via the Theme Header/Footer plugin OR directly in
your theme's `header.php`:
```html
```
### Via a plugin
Any "Insert Headers and Footers" plugin works:
1. Install the plugin (e.g., "Insert Headers and Footers" by WPBeginner).
2. Settings → Insert Headers and Footers.
3. Paste the script tag into the "Scripts in Header" box.
4. Save.
### Via functions.php
```php
ID) {
$attrs .= ' data-user-id="' . esc_attr($user->ID) . '"';
$attrs .= ' data-user-email="' . esc_attr($user->user_email) . '"';
$attrs .= ' data-user-name="' . esc_attr($user->display_name) . '"';
$attrs .= ' data-user-context=\'' . esc_attr(json_encode(array("plan" => "pro"))) . '\'';
}
?>
Note: `data-user-*` / `data-page-*` are host-asserted context for the
> AI, never an auth credential — the browser deployment is selected by
> `data-public-key` + `data-ai-agent-deployment-id`. Explicit
> scalar attributes win over keys in the `data-user-context` /
> `data-page-context` JSON.
---
## Shopify
Shopify has a "custom pixels" and "additional scripts" workflow.
Easiest path:
1. In your admin: **Online Store → Themes → Edit code**.
2. Open `theme.liquid`.
3. Paste before ``:
```html
```
### With Shopify customer data
```html
{% if customer %}
{% else %}
{% endif %}
```
---
## Webflow
Webflow lets you add site-wide custom code:
1. Site settings → **Custom code → Head code**.
2. Paste:
```html
```
3. Save + publish.
Page-specific customization: open the page → **Page settings →
Inside `` tag** and paste a page-specific snippet.
---
## Squarespace / Wix
**Squarespace:**
1. Settings → **Advanced → Code Injection**.
2. Paste the script tag into the **Header** section.
**Wix:**
1. Settings → **Custom Code**.
2. Add new snippet.
3. Paste + choose "All pages" + "Head" placement.
---
## Framework-agnostic troubleshooting
**The widget doesn't appear in SSR previews** — that's expected. The
widget is client-only. It appears after hydration / mount.
**The widget flashes open then closes on SPA navigation** — you're
probably calling `destroy()` then `init()` on every route change. Use
`init()` alone — it's idempotent. If there's an existing instance at
the same `widgetId`, it merges the new options in.
**Hot module replacement breaks the widget** — Next.js / Vite HMR
sometimes mounts duplicate instances. Always pair your `useEffect` /
`onMounted` with a cleanup that calls `destroy()`.
**CSP blocks the widget** — the widget inlines styles inside a shadow
root (requires `style-src 'unsafe-inline'` or a nonce pattern). See
[Security & privacy § CSP](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/security-privacy.md#content-security-policy).
More: [Troubleshooting](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/troubleshooting.md).
---
## Next
- [Recipes](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/recipes.md) — programmatic control patterns.
- [Events & hooks](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/events-and-hooks.md) — analytics integration.
- [Configuration](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/configuration.md) — every option in detail.
---
# Configuration reference
Every option you can pass to `init(...)` / `mount(...)` / set via
`data-*` attributes / set via web-component attributes.
Group-by-group walkthrough with realistic examples. For an at-a-glance
lookup, see [API reference](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/api-reference.md).
---
## How options arrive
Options resolve in this order (highest priority wins):
1. **Options you pass to `init()`** — always win.
2. **Server-pushed settings** — your Help AI dashboard settings for this
deployment. Fill in anything you left unset.
3. **Built-in defaults** — sensible fallbacks shipped with the widget.
This means you can:
- Hardcode `publicKey` in code, tune the rest from the dashboard.
- Ship all config in `init()` and ignore the dashboard.
- Override a specific field (e.g., force `theme: "dark"`) while letting
the dashboard control everything else.
### Canonical option shape
Embed identity, routing, context, endpoint overrides, storage, and callbacks stay
at the top level. Serializable widget settings are grouped by ownership:
| Section | Fields described in this guide |
| -------------- | ------------------------------------------------------------------------------------ |
| `presentation` | `mode`, `iframe`, `position`, `size` |
| `behavior` | `startMinimized`, `responseMode`, transcript visibility, feedback, suggestion policy |
| `header` | `actions` |
| `composer` | `sendButton`, `attachments` |
| `feedback` | `sound`, `haptics` |
| `i18n` | `locale`, `defaultLocale`, `availableLocales`, `strings` |
| `footer` | `composerDisclaimer`, `poweredBy` |
The flat HTML attributes remain intentionally short (`data-mode`, `data-actions`,
and so on); the attribute parser maps them into this nested JavaScript shape.
---
## Required
### `publicKey`
`string` · required for authenticated browser deployments
Your deployment's public key from the Help AI dashboard. Looks like
`agent_pk_...`.
```ts
init({ publicKey: 'agent_pk_abc123...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
```
The public key authorizes the public widget surface. Pair it with that same
deployment's `aiAgentDeploymentId`; the backend requires both values to match.
Intentional guest mode omits the key but still supplies the deployment ID.
There is no per-end-user token — see
[`userContext`](#usercontext) below.
### `aiAgentDeploymentId`
`string` · required for browser deployments
The deployment UUID from the same Dashboard → Deployments install panel as the
public key. It selects the exact agent/configuration and acts as a
defense-in-depth match for authenticated mounts. Send it on every browser
mount; also send the public key unless guest mode is intentional.
---
## Identity + routing
### `widgetId`
`string` · default: `"default"`
A name you pick for this widget instance. Matters when you run more
than one widget on a page — each widgetId gets its own chat thread,
settings, and storage.
```ts
init({ widgetId: 'support', publicKey: SUPPORT_KEY, aiAgentDeploymentId: SUPPORT_DEPLOYMENT_ID });
init({ widgetId: 'sales', publicKey: SALES_KEY, aiAgentDeploymentId: SALES_DEPLOYMENT_ID });
```
See [Recipes § Multiple widgets](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/recipes.md#multiple-widgets-on-one-page).
### `baseUrl`
`string` · default: `https://help.ai`
The MAIN site origin the widget calls — the API modules derive from it
(agent API at `${baseUrl}/api/agent`, data API at `${baseUrl}/api/data`).
Override if you use:
- A custom domain (`your-company.com`).
- A local dev backend.
### `userToken`
`string | () => string | Promise` · default: none (anonymous)
Signs the visitor in as your logged-in user: a JWT your server signs with the deployment's secret key, or a function
returning a fresh one. Their conversations then follow them to any device, and the AI gets their name and email as
verified facts. Runtime-only: not an attribute or `data-*` option, so the token stays out of the page's markup. See
[Security → Verified users](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/security-privacy.md#verified-users-usertoken) for signing it.
```ts
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', userToken: () => fetch('/api/help-ai-token').then((res) => res.text()) });
```
### `tools`
`ModelContextTool[]` · default: none
Tools the AI agent can use on your page, in the WebMCP tool shape: a name, a description, a JSON Schema of its input
and an `execute` function the widget runs in the visitor's browser when the agent calls it
(`annotations: { consequentialHint: true }` asks the visitor first). Runtime-only, like `userToken`;
`chat.update({ tools })` replaces them. `chat.registerTool(tool)` adds one for every widget on the page instead. Full
guide: [Page tools](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/page-tools.md).
```ts
init({
publicKey: 'agent_pk_...',
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
tools: [{ name: 'open_booking', description: 'Open the booking form. Use it whenever the visitor wants to book a call.', execute: () => (bookingForm.open(), { opened: true }) }],
});
```
### `userContext`
`object | undefined`
Host-asserted information about the end user, passed to the AI as
**context only — never an authentication credential.** The backend does
not register or authenticate these users. Optional — omit it for
anonymous chats. Recommended for logged-in users so the AI has context.
```ts
init({
publicKey: 'agent_pk_...',
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
userContext: {
id: 'user_42', // your user id
email: 'jane@help.ai',
name: 'Jane Doe',
plan: 'pro',
role: 'admin',
phone: '+15551234567',
seats: 12, // arbitrary custom scalar field
},
});
```
A flat object. Recognised fields (all optional):
- `id` — your user's stable identifier.
- `email` — for personalization.
- `name` — for personalization (also drives the Home tab greeting,
e.g. "Hi Jane 👋").
- `plan` — subscription / pricing tier (e.g. `"pro"`).
- `role` — the user's role on your site (e.g. `"admin"`).
- `phone` — phone number.
Plus any arbitrary custom scalar fields (`string | number | boolean`) —
account age, feature flags, etc.
Because this is untrusted external input, it is sanitized client-side
before it leaves the page: non-scalar values are dropped, reserved keys
(`visitorId`, `conversationId`, `conversationId`, `jwt`, `token`, `authorization`,
`password`) are stripped, a malformed `email` is dropped, strings are
trimmed and capped at 1024 chars, max 50 keys. Sanitization warns to the
debug log and never throws.
> **This is YOUR site's logged-in user — not the widget's own visitor.**
> The widget independently mints and persists an anonymous
> `visitorId` / `conversationId` for the device; `userContext` never overrides
> those. Setting `userContext` simply attaches your context to the same
> visitor.
### `pageContext`
`object | undefined`
Host-asserted context about the page/visit (separate from `userContext`),
so the AI can adapt to where the visitor is — a pricing visitor is not a
docs reader is not a help-desk ticket.
```ts
init({
publicKey: 'agent_pk_...',
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
pageContext: {
area: 'pricing', // host-defined surface
path: '/pricing/enterprise',
title: 'Enterprise plans',
// url auto-filled from location.href when omitted
},
});
```
Recognised fields (all optional):
- `area` — host-defined surface the visitor is on. Free-form string;
suggested values: `home`, `docs`, `help-desk`, `support`, `pricing`,
`checkout`, `orders`, `account`, `blog` (any string accepted).
- `url` — full page URL. Auto-filled from the host page's
`location.href` when omitted.
- `path` — page path, e.g. `/pricing/enterprise`.
- `title` — page title.
Plus arbitrary custom scalar fields. Sanitized client-side with the same
rules as `userContext`.
> **On the wire:** `userContext` + `pageContext` are bundled into one
> `context` field — `base64url(JSON.stringify({ user, page }))` — sent on
> **every** request (body for POSTs, `?context=` for GETs), so the backend
> always has the visitor's latest context as they navigate. base64url is
> encoding, **not encryption** (the transport is already TLS); it just keeps
> PII out of plaintext access logs / the devtools network tab.
### Setting context after login / on navigation
In a typical SPA the visitor is anonymous on first load and logs in
later. Update context dynamically via the instance handle. The new context
rides the **next** request automatically (no reconnect) — and a
`setUserContext` identity change additionally re-personalizes the greeting,
keeping the **same** visitor/conversation so the conversation is continuous:
```ts
const widget = HelpAI.init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
// after your app authenticates the user:
widget.setUserContext({
id: 'user_42',
email: 'jane@help.ai',
name: 'Jane Doe',
plan: 'pro',
});
// on SPA navigation:
widget.setPageContext({ area: 'checkout', path: '/checkout' });
// on logout:
widget.clearUserContext();
```
You can also seed context at load time via `data-user-*` /
`data-page-*` tag attributes — see [Installation](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/installation.md) and
the framework recipes in [`frameworks.md`](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/frameworks.md).
---
## Rendering
### `presentation.mode`
`"floating" | "inline" | "standalone" | "modal" | "drawer" | "page"` · default: `"floating"`
How the widget renders on the page.
- **`"floating"`** (default) — floating chat bubble in a corner.
Clicking opens the panel. Best for marketing sites, support chat.
- **`"inline"`** — a block element that fills its parent container. No
bubble, no popover. Best for in-app assistants, sidebar chats.
- **`"standalone"`** — full viewport. The dedicated chat tab; renders
the full module surface (no sidebar).
- **`"modal"`** — a centered dialog over a backdrop. The host opens it
(no launcher); call `open()` from your own trigger.
- **`"drawer"`** — docks to a screen edge; the launcher becomes a
vertical edge tab.
- **`"page"`** — full-viewport split: a conversation **sidebar** plus
the module surface in the main area.
`target` controls where every mode's shadow host is inserted. It is most
important for `inline` / `standalone` / `page`; overlay modes remain
viewport-positioned even when their host is inserted under a custom target.
See `../features/modes.md` for the full behavior matrix.
```ts
init({ presentation: { mode: 'inline' }, target: document.getElementById('chat')! });
```
### `target`
`HTMLElement | undefined`
Where to insert the widget's shadow host. Recommended for `inline` /
`standalone` / `page` modes. Floating, drawer, and modal hosts also use
it, while their panels remain viewport-positioned. Defaults to ``.
### `presentation.iframe`
`boolean` · default: `false`
Wrap the widget in a same-origin iframe for strict JS + CSS isolation.
Useful when the host page has aggressive global scripts that mangle
`window.*` or inject hostile CSS.
```ts
init({ presentation: { mode: 'floating', iframe: true }, publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
```
Composes with any `mode`. Default is `false` — shadow DOM alone is
enough for 95% of deployments. [Security § Iframe isolation](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/security-privacy.md#iframe-isolation).
### `presentation.position`
`"bottom-right" | "bottom-left" | "top-right" | "top-left"` · default: `"bottom-right"`
Launcher corner. Only applies in `floating` mode.
```ts
init({ presentation: { position: 'bottom-left' } });
```
### `behavior.startMinimized`
`boolean` · default: `true`
Whether the floating panel starts closed (showing only the launcher).
Only applies in `floating` mode.
```ts
// Start open on every fresh page load
init({ behavior: { startMinimized: false } });
```
User's last open/close choice is remembered per device — so even with
`behavior.startMinimized: false`, closing the widget and reloading keeps it
closed.
---
## Theming
### `theme`
`"auto" | "light" | "dark" | ThemeOverrides` · default: `"auto"`
Three forms:
**1. Preset mode:**
```ts
init({ theme: 'auto' }); // follows OS prefers-color-scheme
init({ theme: 'light' }); // force light
init({ theme: 'dark' }); // force dark
```
**2. Color overrides (stays in auto mode):**
```ts
init({
theme: {
accent: '#ff3e00', // primary brand color
accentText: '#ffffff', // text on the accent color
radius: '12px', // corner rounding
fontFamily: 'Inter, system-ui, sans-serif',
},
});
```
**3. Both at once** — not supported in one call, but users can pick a
mode via the header picker regardless of palette overrides.
End users can flip the theme via the header picker. Their choice is
persisted per device + synced cross-device.
Full customization guide: [Theming](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/theming.md).
---
## Sizing
### `presentation.size`
`SizeOptions | undefined`
Control the panel's dimensions across three tiers:
```ts
init({
presentation: {
size: {
// Normal tier (default panel size)
width: '420px',
height: '640px',
// Expanded tier (when user clicks the ⤢ button)
expanded: { width: '80vw', height: '80vh' },
// User drag-resize
resize: {
enabled: true,
minWidth: '360px',
maxWidth: '900px',
minHeight: '480px',
maxHeight: '90vh',
},
// First-render pick
initialSize: 'auto', // auto / normal / expanded / fullscreen
autoSizeBreakpoint: 900, // "auto" → expanded if viewport ≥ 900px
},
},
});
```
### Size tiers
- **Normal** — `width`/`height` or mode-specific CSS defaults.
- **Expanded** — ⤢ header button, configurable dimensions.
- **Fullscreen** — ⛶ header button, always 100vw × 100vh.
### Drag-resize
`presentation.size.resize.enabled: true` shows a corner grip on the panel. Users drag
it to resize; dimensions persist per device + sync cross-device.
`presentation.size.resize.minWidth` etc. clamp the drag range.
### `presentation.size.initialSize`
Which tier to open at on the user's **first** mount. After that, user
interactions own the size.
- `"normal"` — force the normal tier.
- `"expanded"` — open roomy.
- `"fullscreen"` — open full-screen.
- `"auto"` (default) — `fullscreen` below 640 px, `normal` from 640 px
to `autoSizeBreakpoint`, then `expanded` on wider viewports.
---
## Launcher
### `launcher`
`LauncherOptions | undefined` · only applies in `presentation.mode: "floating"`
The FAB and its nudges.
```ts
init({
launcher: {
variant: 'pill', // circle / pill / bar / minimal
size: 'md', // sm / md / lg
label: 'Ask us anything',
icon: 'chat', // chat / message / bubbles / spark / wand / bot / help / lifebuoy / headset / search / sparkles / concierge / wave / raw SVG
hidden: false, // true = no FAB
// Unified pre-engagement bubble (optional). Same component
// for greetings, nudges, onboarding tips — pick the
// visuals + dismissal lifecycle for the use case.
callout: {
text: '👋 Hi! Need any help?',
shape: 'pill', // pill / bubble / callout
position: 'auto', // auto / horizontal / vertical
delayMs: 1200,
autoDismissMs: 0,
animated: true,
persistent: true, // true (default) = re-show on every visit
// false = dismiss-once, hidden across visits
},
},
});
```
Variants + callout fully explained in
[Theming § Launcher](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/theming.md#launcher).
### `launcher.hidden`
Hide the launcher FAB entirely. Use when you want to trigger the widget
from your own UI:
```ts
const chat = init({ launcher: { hidden: true } });
document.getElementById('my-custom-chat-button').onclick = () => chat.open();
```
---
## Header actions
### `header.actions`
`ActionName[]` · per-mode defaults
The list is a flat enabled set. `history` and `close` render as dedicated
buttons; the rest live in the ⋯ menu in a fixed internal order. Unlisted actions
are hidden, so input order does not choose placement.
```ts
init({
header: {
actions: ['theme', 'textSize', 'history', 'close', 'clear', 'fullscreen', 'popOut', 'sound', 'language'],
},
});
```
Available actions:
| Name | What it does |
| ------------ | --------------------------------------------------------------------------- |
| `clear` | Start a new conversation (trash icon) |
| `expand` | Toggle normal ↔ expanded tier (⤢) |
| `fullscreen` | Toggle normal ↔ fullscreen (⛶) |
| `popOut` | Open the active conversation in `behavior.popOutUrl` |
| `sound` | Toggle notification sounds (switch-style) |
| `history` | Toggle the chat history pane |
| `language` | Open the language picker (renders as globe icon or inline rows in overflow) |
| `theme` | Open the theme picker (auto/light/dark) |
| `textSize` | Cycle the conversation text size |
| `close` | Close the panel (X) |
`popOut` renders only when `behavior.popOutUrl` is configured. The new tab
receives `chat`, `widgetId`, `public_key` (when present), and
`ai_agent_deployment_id` query parameters so it can resume the same thread.
Per-mode defaults and visibility rules are maintained in
Header actions.
> `header.actions` is always a complete replacement for the per-mode default;
> arrays are never merged. An explicit client list wins over the server list.
---
## Composer
### `composer.sendButton`
Send-button chrome.
```ts
init({
composer: {
sendButton: {
shape: 'pill', // circle / square / pill
icon: 'arrowUp', // arrowUp / paperPlane / send / chevron
variant: 'filled', // filled / outline / ghost
},
},
});
```
3 shapes × 4 icons × 3 variants = 36 combinations. Pick what matches
your brand.
---
## Footer
### `footer.composerDisclaimer`
Small grey text below the textarea. Classic "AI can make mistakes"
pattern.
```ts
init({ footer: { composerDisclaimer: 'AI can make mistakes. Check important info.' } });
```
Empty / omitted = no disclaimer shown.
### `footer.poweredBy`
Attribution row below the disclaimer. Defaults to the active
brand: `Powered by ` linking to `BRAND.siteUrl`
(e.g. `Powered by Help AI` → `https://help.ai` for `@helpai/elements`).
Override fields per-deployment when you want different copy or to add a logo;
omit fields to inherit the brand defaults.
```ts
init({
footer: {
poweredBy: {
text: 'Powered by Acme', // overrides brand default
href: 'https://acme.com', // overrides brand default
logoUrl: 'https://yoursite.com/helpai-logo.svg',
},
},
});
```
To opt out of the attribution entirely, pass an empty `text` (and no
logo): `footer: { poweredBy: { text: "" } }` collapses the element.
---
## Attachments + features
### `composer.attachments`
Client-side limits. Server enforces its own — these are to give users
fast feedback before upload.
```ts
init({
composer: {
attachments: {
maxBytes: 10 * 1024 * 1024, // 10 MB, default
maxCount: 10, // default
accept: 'image/*,application/pdf', // comma-separated MIME globs
},
},
});
```
### `features.disableFileUpload`
`boolean` · default: `false`
Turn OFF file uploads — hide the paperclip button and reject drag-drop / paste.
Unset/`false` = enabled (default); set `true` to disable attachments entirely.
### `features.enableOperatorRequest`
`boolean` · default: `false`
Opt IN **operator control** — exposes a "Talk to a person" control in the
composer and the connected-to-operator states (a status banner while the conversation
is routed to the operator, and operator reply bubbles). This is a **positive opt-in**
for a new capability (unlike the `disable*` flags): unset/`false` = off (default), `true` = on.
Every deployment holds one long-lived `GET /pai/stream-resume`; this flag does not gate
the stream. It only exposes the visitor's `request-operator` control. AI replies, operator
replies, and control-state changes continue to arrive on the resume channel and deduplicate
by message id whether the request control is visible or not. The composer stays open while
operator-routed so the visitor can keep typing. Operator requests require backend support.
```ts
init({ features: { enableOperatorRequest: true } });
```
### `features.voice`
`"local" | "server" | "disabled"` · default: `"local"`
How voice input works:
- `"local"` — uses the browser's Web Speech API. Zero upload, free,
quality varies by browser.
- `"server"` — records audio via `MediaRecorder`, uploads to
`/pai/transcribe-audio`, receives text. Consistent quality, uses your
upload quota.
- `"disabled"` — hides the mic button entirely.
## Messenger modules
### `modules`
Turns the panel into an Intercom-style **messenger** with a bottom tab
bar. `modules` is an **ordered array of up to 4 tabs**; each tab picks a UI
`layout`, the content collections it shows (`contentCollectionIds`), and a
translatable `label`. The surface renders in
**every** mode (in `page` mode it fills the main area beside the sidebar). With nothing set, just the **chat** shows —
today's single-chat widget, no tab bar.
```ts
init({
publicKey: 'agent_pk_...',
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
modules: [
{ label: 'tabHome', layout: 'home', contentCollectionIds: [GUIDES_ID], greetingText: 'Hi {name} 👋', brandName: 'Acme' }, // "Popular articles" = the collections' pinned items
{ label: 'tabConversations', layout: 'chat' }, // the conversation
{ id: 'help', label: 'tabHelp', layout: 'help', contentCollectionIds: [GUIDES_ID, BILLING_ID] },
{ id: 'news', label: 'tabNews', layout: 'news', contentCollectionIds: [NEWS_ID] },
],
});
```
| Field | Required | Meaning |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| `label` | yes | Tab label — an i18n key (e.g. `tabHome`) or literal; translated via `i18n.strings`. |
| `layout` | yes | `chat` (the conversation), `home`, `help`, or `news`. |
| `contentCollectionIds` | no | Content collection ids (UUIDv7, max 20) this tab shows, in order; Help sections follow this order. |
| `id` | no | Stable id for nav + last-tab memory (defaults to the first `contentCollectionIds`, else `layout`). |
| home extras | no | `greetingText` (supports `{name}`), `hideSearchBar`, `brandName`, … (`home` layout only). |
- **Order is the array order** (left → right). One tab → no tab bar; zero
tabs (`[]`) → an empty state.
- The first tab is the first-open default; the widget then remembers the
user's last tab (per device).
- The deployment can also push `modules` from the dashboard via the
handshake (`config.modules`); your `init({ modules })` wins (replace-whole).
Each tab's content is fetched lazily from `GET /content` (data-api) on first open.
Full behavior + wire contract:
`features/messenger-modules.md`.
---
## Response mode
### `behavior.responseMode`
`"streaming" | "buffered"` · default: `"streaming"`
- `"streaming"` — paint replies token-by-token as they arrive. Default.
- `"buffered"` — show a neutral loading indicator until the reply
finishes, then render it whole. The backend always streams; this is a
client-rendering choice. Useful when token-by-token feels stuttery.
### `behavior.disableMessageFeedback`
`boolean` · default: `false`
Turn OFF the 👍/👎 feedback controls under each rateable reply (AI or operator) — unset/`false` shows them (default). While
shown, a choice fires the [`messageFeedback`](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/events-and-hooks.md) event and is sent to the backend.
```ts
init({ behavior: { disableMessageFeedback: true } }); // hide the controls
```
### `behavior.autoSendSuggestions`
`boolean` · default: `false`
Controls what tapping a suggestion chip does. Unset/`false` (default) **fills the composer** with the chip's
text so the visitor can edit before sending; `true` **sends** it immediately.
```ts
init({ behavior: { autoSendSuggestions: true } }); // send on tap instead of filling
```
---
## Internationalization
### `i18n.locale`
`string | undefined`
Force a specific BCP-47 locale, overriding the visitor's browser
setting.
```ts
init({ i18n: { locale: 'fr-CA' } });
```
### `i18n.defaultLocale`
`"auto" | string` · default: `"auto"`
Initial / fallback locale when the user hasn't picked one.
- `"auto"` — sniff `navigator.language`.
- Explicit tag (e.g., `"es-MX"`) — force that one.
### `i18n.availableLocales`
`string[]` · optional
Locales offered in the language switcher. Omission defaults to the active
locale as a one-entry list; zero or one entries hide the picker.
```ts
init({
i18n: {
availableLocales: ['en', 'fr', 'es', 'de'],
defaultLocale: 'auto',
},
});
```
### `i18n.strings`
Per-key overrides for any UI string. Two shapes:
**Flat** (applies to active locale):
```ts
init({
i18n: {
strings: {
send: 'Go',
composerPlaceholder: 'Ask me anything…',
newConversation: 'Start fresh',
},
},
});
```
**Nested** (per-locale):
```ts
init({
i18n: {
strings: {
en: { send: 'Go' },
fr: { send: 'Envoyer' },
es: { send: 'Enviar' },
},
},
});
```
Full key list: [i18n](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/theming.md#internationalization) or
`src/i18n/strings.ts` in the source.
---
## Sound + haptics
### `feedback.sound`
Notification sounds.
```ts
init({
feedback: {
sound: {
enabled: true, // default
volume: 0.3, // 0..1, default 0.3
events: {
messageReceived: true, // built-in synth tone
messageSent: true,
error: 'https://cdn.your-site.com/error.mp3', // custom audio file
voiceStart: false, // silence this one
voiceStop: false,
},
},
},
});
```
Per-event values:
- `true` — use the built-in synthesized tone.
- `false` — silence this event.
- `"url"` — URL to a custom audio file (decoded once + cached).
End users can mute via the sound action in the header. Their choice
is remembered.
### `feedback.haptics`
Tactile feedback on touch devices.
```ts
init({
feedback: {
haptics: {
enabled: true, // default: true on touch devices
events: {
messageSent: [6], // single 6ms tap
error: [10, 50, 10], // two taps with a 50ms pause
},
},
},
});
```
Per-event values:
- `true` — default pattern.
- `false` — silence.
- number — single vibration of N ms.
- number[] — alternating `[vibrate, pause, vibrate, …]`.
iOS: WebKit only supports a single tick; arrays are flattened.
---
## Callbacks (shortcut for events)
Quick inline callbacks for common events. Same as calling `.on(...)`
on the returned instance.
```ts
init({
publicKey: 'agent_pk_...',
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
onMessage: (msg) => console.log(msg),
onOpen: () => console.log('opened'),
onClose: () => console.log('closed'),
onError: (err) => Sentry.captureException(err),
});
```
For richer event handling, see [Events & hooks](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/events-and-hooks.md).
---
## Storage
### `storage`
`ChatWidgetOptions["storage"]`
Custom synchronous storage adapter. Default is `localStorage`. Swap for:
- `sessionStorage`.
- Encrypted wrapper.
- In-memory shim (for environments without `localStorage`).
```ts
import type { ChatWidgetOptions } from '@helpai/elements';
type StorageAdapter = NonNullable;
const myStorage: StorageAdapter = {
get: (key) => sessionStorage.getItem(key),
set: (key, value) => sessionStorage.setItem(key, value),
remove: (key) => sessionStorage.removeItem(key),
};
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', storage: myStorage });
```
The adapter also receives `draft.` keys containing exact
unsent textarea text and owns their storage, retention, and privacy behavior.
The widget itself does not include drafts in Help AI network requests before
Send. Use an encrypted adapter when same-origin plaintext storage is not
acceptable; an in-memory adapter disables reload restoration.
Full adapter contract: [Security & privacy § custom storage](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/security-privacy.md#custom-storage).
---
## Runtime changes
You can change most options after mount via `chat.update(...)`:
```ts
const chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
// Later — flip to dark mode
chat.update({ theme: 'dark' });
// Or change the launcher label
chat.update({ launcher: { variant: 'pill', label: 'New message!' } });
```
`update()` is a **shallow merge** at the top level. Nested objects are
replaced, not merged:
```ts
chat.update({ presentation: { size: { expanded: { width: '800px' } } } });
// This replaces the whole `presentation` section — mode, iframe, and the other size fields are gone!
// Keep the current section in your application state and spread each nested level you preserve:
chat.update({
presentation: {
...currentPresentation,
size: {
...currentPresentation.size,
expanded: { width: '800px' },
},
},
});
```
The `publicKey`, `widgetId`, `presentation.mode`, and `presentation.iframe` can't change at runtime
— they require a full remount (`destroy()` + `mount()`).
---
## Putting it all together — full example
```ts
import { init } from '@helpai/elements';
const chat = init({
// Identity
publicKey: process.env.NEXT_PUBLIC_HELPAI_KEY!,
aiAgentDeploymentId: process.env.NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID!,
widgetId: 'support',
userContext: { id: currentUser.id, email: currentUser.email, name: currentUser.name },
pageContext: { area: 'support', path: location.pathname },
// Presentation + behavior
presentation: {
mode: 'floating',
position: 'bottom-right',
size: {
expanded: { width: '720px', height: '85vh' },
resize: { enabled: true, minWidth: '360px', maxWidth: '900px' },
initialSize: 'auto',
autoSizeBreakpoint: 1024,
},
},
behavior: { startMinimized: true },
theme: { accent: '#ff3e00', radius: '12px' },
// Launcher
launcher: {
variant: 'pill',
label: 'Ask us anything',
icon: 'chat',
callout: { text: 'Hey there 👋', delayMs: 2000 },
},
// i18n
i18n: {
availableLocales: ['en', 'fr', 'es'],
defaultLocale: 'auto',
strings: {
en: { composerPlaceholder: 'Ask about anything…' },
fr: { composerPlaceholder: 'Demandez-moi…' },
},
},
// Feedback
feedback: { sound: { enabled: true, volume: 0.25 } },
// Footer
footer: {
composerDisclaimer: 'AI-generated answers may contain mistakes.',
poweredBy: { text: 'Powered by Help AI', href: 'https://help.ai' },
},
// Events
onMessage: (m) => analytics.track('chat_message', m),
onError: (err) => Sentry.captureException(err),
});
```
---
## Not covered here
- **Events** → [Events & hooks](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/events-and-hooks.md)
- **Theming deep-dive** → [Theming](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/theming.md)
- **Framework-specific wiring** → [Frameworks](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/frameworks.md)
- **Backend API** → Backend contract (internal)
---
# Configuration — `ChatWidgetOptions`
The canonical surface lives in `src/schema/widget.ts` and
`src/schema/deployment.ts` as Zod schemas. Every field carries a
`.describe(...)` string and (where useful) `.meta({ examples })` —
this page is a flat reference auto-generated from those annotations.
When the schema and this doc disagree, **the schema wins**. Run
`bun run gen:docs` after editing a schema to refresh the tables; CI
fails if this file is out of sync.
For longer-form prose on what each subsystem does, see the
`features/` directory:
- Theming
- Sizing
- Launcher
- Header actions
- Composer
- Feedback
- i18n
- Modes
---
## Conventions
- Every field is optional unless its row says otherwise.
- "Default" is what the runtime applies when the field is absent.
`—` means there's no schema-level default; the resolver falls
through to runtime behaviour or leaves the field undefined.
- Field names use the schema (camelCase) form. The web-component +
`
```
Full attribute list: [Installation § script data attributes](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/installation.md#attributes).
Leave out `data-ai-agent-deployment-id` to load the bundle without mounting, then call
`HelpAI.init({ … })` from JavaScript ([Installation § Load without mounting](https://cdn.help.ai/sdk/js/elements/1.15.2/docs/installation.md#load-without-mounting)).
---
## Global `window.HelpAI` API
After the IIFE boots:
```ts
// Direct methods
window.HelpAI.init(opts)
window.HelpAI.mount(opts)
window.HelpAI.destroy(widgetId?)
window.HelpAI.getInstance(widgetId?)
window.HelpAI.getAllInstances()
window.HelpAI.hook(widgetId, event, fn)
// Shortcuts for the default widget
window.HelpAI.open() // or .open({ to: 'news', id? })
window.HelpAI.ask(text, { send }) // send defaults to false: fill the composer
await window.HelpAI.ready() // the default widget, once connected (also when it mounts later)
window.HelpAI.reset() // a new anonymous visitor (on logout)
window.HelpAI.close()
window.HelpAI.expand()
window.HelpAI.on(event, fn)
window.HelpAI.update(patch)
// Metadata
window.HelpAI.version // semver string
window.HelpAI.brand // { id, displayName, cssPrefix, cdnUrl, ... }
// Callable dispatch (for queued calls)
window.HelpAI('init', opts)
window.HelpAI('open')
window.HelpAI('open', { to: 'help' })
window.HelpAI('ask', 'How do I book?') // or ('ask', 'How do I book?', { send: true })
window.HelpAI('ready', (chat) => chat.on('unread', showUnread)) // queue-able before the bundle loads
window.HelpAI('reset')
window.HelpAI('hook', '*', 'message', fn)
```
---
## Pre-boot queues
Used with the async loader snippet — write calls before the bundle
arrives:
```html
```
The IIFE drains both on boot.
---
## Web component
Custom element tag name: ``.
The Web Component exposes the same curated parser fields without the
script dataset's outer `data-` prefix:
```html
```
Changing a reactive attribute re-resolves the live widget. `iframe`
and `start-minimized` are mount-time-only.
The element has the instance's commands, state and events, so a page drives it directly:
```js
const chat = document.querySelector('web-ai-chat');
chat.open(); // or chat.open({ to: 'help' })
chat.ask('How do I book a session?');
chat.close();
chat.expand();
chat.openForm('lead');
await chat.ready;
chat.on('unread', (count) => console.log(count, chat.state));
chat.reset(); // on logout
```
`on()` subscriptions end when the element leaves the page. Importing `@helpai/elements/web-component` types
`document.querySelector('web-ai-chat')` as `WebAiChatElement` in TypeScript; the
[JSX typings](#jsx-typings) do the same for React, Vue and Svelte markup.
### No-script triggers
Mark any element on the page and a click drives the default widget, with no script. A link keeps working without the
widget; with it, the click opens the widget instead of following the link.
| Attribute | Click does |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `data-helpai-open` | Opens the panel; a value opens a tab (`home`, `chats`, `help`, `news`) or one article or post (`help/`) |
| `data-helpai-ask=""` | Opens the chat with the text in the composer (empty value: the element's own text); add `data-helpai-send` to send it |
| `data-helpai-form="