@helpai/elements 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,626 @@
1
+ # Installation
2
+
3
+ Deep-dive for every install path. See [Quickstart](./quickstart.md)
4
+ for the 5-minute version.
5
+
6
+ ---
7
+
8
+ ## Choose your path
9
+
10
+ | Path | Best for | Delivery |
11
+ | ----------------------------------------------------------------- | --------------------------------------------------- | ---------- |
12
+ | [A. `<script data-*>` auto-boot](#path-a--script-data--auto-boot) | Static HTML, WordPress, no build step | CDN |
13
+ | [B. Async loader snippet](#path-b--async-loader-snippet) | Tag managers, analytics pipelines | CDN |
14
+ | [C. NPM package](#path-c--npm-package) | React, Next.js, Vue, Svelte, SolidJS, any bundler | NPM |
15
+ | [React kit](./frameworks.md#react) | React, Next.js, Remix: `<HelpAI />` + `useHelpAI()` | NPM |
16
+ | [D. Web Component](#path-d--web-component) | Declarative tags in any framework | CDN or NPM |
17
+
18
+ > ### CDN URL
19
+ >
20
+ > The bundle is served from a **versioned** path — the bare root 404s:
21
+ >
22
+ > ```
23
+ > https://cdn.help.ai/sdk/js/elements/<version>/<file>
24
+ > ```
25
+ >
26
+ > - `…/elements/<version>/elements.js` — **pinned** to an exact release (immutable, recommended for production).
27
+ > - `…/elements/latest/elements.js` — always the newest release (a 302 to its pinned path, cached up to 4 hours).
28
+ >
29
+ > Files: `elements.js` (auto-boot + queue + tag), `elements-web-component.esm.js`
30
+ > (standalone web-component ESM). The examples below use `latest`; **pin a version
31
+ > in production** so a release never changes behaviour under you.
32
+
33
+ All four paths:
34
+
35
+ - Load the same JS bundle.
36
+ - Accept the same [configuration options](./configuration.md).
37
+ - Emit the same [events](./events-and-hooks.md).
38
+ - Respect the same [isolation guarantees](./security-privacy.md#shadow-dom--iframe).
39
+
40
+ You can combine them — e.g., a Next.js page can use path C, and a
41
+ marketing landing can use path A, both pointing at the same deployment.
42
+
43
+ ---
44
+
45
+ ## Path A — `<script data-*>` auto-boot
46
+
47
+ Fastest path. Zero custom JavaScript.
48
+
49
+ ```html
50
+ <!-- In your HTML <head> -->
51
+ <script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_abc123..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
52
+ ```
53
+
54
+ ### Attributes
55
+
56
+ The zero-JavaScript path intentionally exposes a small, flat subset of
57
+ `ChatWidgetOptions`. These are the attributes parsed by the current bundle:
58
+
59
+ | Attribute | Maps to | Example |
60
+ | ---------------------------------------------- | --------------------------------- | ------------------------------- |
61
+ | `data-public-key` | `publicKey` | `agent_pk_...` |
62
+ | `data-ai-agent-deployment-id` | `aiAgentDeploymentId` | deployment UUID |
63
+ | `data-base-url` | `baseUrl` | `https://help.ai` |
64
+ | `data-agent-api-base-url` | `agentApiBaseUrl` | `http://localhost:3069` |
65
+ | `data-api-base-url` | `dataApiBaseUrl` | `http://localhost:3071` |
66
+ | `data-theme` | `theme` | `auto` / `light` / `dark` |
67
+ | `data-mode` / `data-position` | `presentation` | `floating` / `bottom-right` |
68
+ | `data-iframe` | `presentation.iframe` | `true` |
69
+ | `data-width` / `data-height` | `presentation.size` | `420px` / `640px` |
70
+ | `data-response-mode` | `behavior.responseMode` | `streaming` / `buffered` |
71
+ | `data-start-minimized` | `behavior.startMinimized` | `false` |
72
+ | `data-show-reasoning` | `behavior.showReasoning` | `true` |
73
+ | `data-show-tool-calls` | `behavior.showToolCalls` | `true` |
74
+ | `data-show-sources` | `behavior.showSources` | `true` |
75
+ | `data-disable-message-feedback` | `behavior.disableMessageFeedback` | `true` |
76
+ | `data-auto-send-suggestions` | `behavior.autoSendSuggestions` | `true` |
77
+ | `data-pop-out-url` | `behavior.popOutUrl` | `https://chat.example.com` |
78
+ | `data-locale` | `i18n.locale` | `fr-FR` |
79
+ | `data-launcher-variant` / `-size` / `-icon` | `launcher` fields | `pill` / `lg` / `spark` |
80
+ | `data-launcher-label` / `-hidden` | `launcher` fields | `Ask AI` / `true` |
81
+ | `data-callout-text` / `-delay` | `launcher.callout` | `Hi! 👋` / `2000` |
82
+ | `data-actions` | `header.actions` (CSV) | `theme,history,close` |
83
+ | `data-features-disable-file-upload` | `features.disableFileUpload` | `true` |
84
+ | `data-features-voice` | `features.voice` | `local` / `server` / `disabled` |
85
+ | `data-endpoint-upload` / `-transcribe` | `endpoints` | `/pai/upload` |
86
+ | `data-user-id` / `-email` / `-name` | `userContext` scalar fields | `user_42` |
87
+ | `data-user-context` | `userContext` extras (JSON) | `'{"plan":"pro"}'` |
88
+ | `data-page-area` / `-url` / `-path` / `-title` | `pageContext` scalar fields | `support` |
89
+ | `data-page-context` | `pageContext` extras (JSON) | `'{"experiment":"b"}'` |
90
+
91
+ Unknown attributes are ignored. Full option semantics:
92
+ [Configuration](./configuration.md).
93
+
94
+ > Explicit scalar attributes (`data-user-id`, `data-page-area`, …) win
95
+ > over keys in the `data-user-context` / `data-page-context` JSON. For
96
+ > SPAs where the visitor logs in after load or navigates between pages,
97
+ > set context dynamically with
98
+ > [`instance.setUserContext(...)` / `setPageContext(...)`](./configuration.md#setting-context-after-login--on-navigation).
99
+ > The `data-user-*` / `data-page-*` attributes are reactive — updating
100
+ > one re-handshakes with the same anonymous visitor/conversation. This
101
+ > context is informational for the AI, never an auth credential.
102
+
103
+ ### `data-no-autoboot`
104
+
105
+ Add this flag to load the bundle **without** automatically mounting.
106
+ Useful when you want to `init()` later from JavaScript:
107
+
108
+ ```html
109
+ <script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-no-autoboot></script>
110
+ <script>
111
+ // Mount later, conditionally
112
+ document.addEventListener('DOMContentLoaded', () => {
113
+ if (shouldShowChat()) {
114
+ HelpAI.init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
115
+ }
116
+ });
117
+ </script>
118
+ ```
119
+
120
+ ### Multi-widget
121
+
122
+ For two widgets on one page, load the bundle once and call `mount()` with
123
+ distinct `widgetId` values:
124
+
125
+ ```js
126
+ HelpAI.mount({ widgetId: 'support', publicKey: SUPPORT_KEY, aiAgentDeploymentId: SUPPORT_DEPLOYMENT_ID });
127
+ HelpAI.mount({ widgetId: 'sales', publicKey: SALES_KEY, aiAgentDeploymentId: SALES_DEPLOYMENT_ID });
128
+ ```
129
+
130
+ ---
131
+
132
+ ## Path B — async loader snippet
133
+
134
+ Tag-manager-style snippet. Defers the bundle download without blocking
135
+ first paint, while letting your page queue commands immediately.
136
+
137
+ ```html
138
+ <script>
139
+ (function (w, d) {
140
+ w.HelpAI =
141
+ w.HelpAI ||
142
+ function () {
143
+ (w.HelpAI.q = w.HelpAI.q || []).push(arguments);
144
+ };
145
+ w.HelpAIHooks = w.HelpAIHooks || [];
146
+ var s = d.createElement('script');
147
+ s.async = 1;
148
+ s.src = 'https://cdn.help.ai/sdk/js/elements/latest/elements.js';
149
+ d.head.appendChild(s);
150
+ })(window, document);
151
+
152
+ // Queue commands — they run in order once the bundle arrives.
153
+ HelpAI('init', { publicKey: 'agent_pk_abc123...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
154
+ </script>
155
+ ```
156
+
157
+ ### Queueable commands
158
+
159
+ | Call | What it does |
160
+ | ----------------------------------------- | ---------------------------------------------------------------- |
161
+ | `HelpAI('init', opts)` | Mount once; update the same widget id |
162
+ | `HelpAI('mount', opts)` | Mount fresh; replace the same widget id |
163
+ | `HelpAI('destroy')` | Unmount all live widgets |
164
+ | `HelpAI('open')` | Open the default widget's panel |
165
+ | `HelpAI('close')` | Close the default widget's panel |
166
+ | `HelpAI('expand')` | Toggle the default widget's expanded tier |
167
+ | `HelpAI('ask', text, { send })` | Open the chat with `text` in the composer; `send: true` sends it |
168
+ | `HelpAI('on', eventName, fn)` | Subscribe to a default-widget event |
169
+ | `HelpAI('update', { /* patch */ })` | Update the default widget's options |
170
+ | `HelpAI('hook', widgetId, eventName, fn)` | Register a global lifecycle hook |
171
+
172
+ ### Pre-boot hooks
173
+
174
+ `window.HelpAIHooks` is a pre-boot queue for event listeners. Push
175
+ arrays or objects to it before the bundle arrives:
176
+
177
+ ```html
178
+ <script>
179
+ window.HelpAIHooks = window.HelpAIHooks || [];
180
+
181
+ // Fire for every event on every widget
182
+ window.HelpAIHooks.push([
183
+ '*',
184
+ '*',
185
+ (inst, event, data) => {
186
+ console.log('chat event:', event, 'from', inst.widgetId, data);
187
+ },
188
+ ]);
189
+
190
+ // Or target one widget + one event
191
+ window.HelpAIHooks.push([
192
+ 'support',
193
+ 'message',
194
+ (inst, _event, msg) => {
195
+ analytics.track('chat_message', msg);
196
+ },
197
+ ]);
198
+ </script>
199
+ ```
200
+
201
+ Full details: [Events & hooks](./events-and-hooks.md#global-lifecycle-hooks).
202
+
203
+ ### After boot
204
+
205
+ Once the bundle loads, `window.HelpAI` becomes a real API:
206
+
207
+ ```js
208
+ window.HelpAI.init(opts)
209
+ window.HelpAI.destroy(widgetId?)
210
+ window.HelpAI.getInstance(widgetId?) // returns handle or null
211
+ window.HelpAI.getAllInstances()
212
+ window.HelpAI.hook(widgetId, event, fn)
213
+ window.HelpAI.open() / close() / expand()
214
+ window.HelpAI.version // semver string
215
+ window.HelpAI.brand // { id, displayName, cdnUrl, ... }
216
+ ```
217
+
218
+ ---
219
+
220
+ ## Path C — NPM package
221
+
222
+ For sites with a bundler. Full framework-specific walkthroughs:
223
+ [Framework integrations](./frameworks.md).
224
+
225
+ ### Install
226
+
227
+ ```bash
228
+ npm install @helpai/elements
229
+ # or
230
+ yarn add @helpai/elements
231
+ # or
232
+ pnpm add @helpai/elements
233
+ # or
234
+ bun add @helpai/elements
235
+ ```
236
+
237
+ ### Entry points
238
+
239
+ The package ships two entries:
240
+
241
+ ```ts
242
+ // Main entry — everything except the web component.
243
+ import { init, mount, destroy, hook } from '@helpai/elements';
244
+ // Sub-entry — registers <web-ai-chat> as a custom element.
245
+ import '@helpai/elements/web-component';
246
+ ```
247
+
248
+ Tree-shaking: the main entry has zero mount-on-import side effects.
249
+ Unused exports are dropped by your bundler. Importing only types costs
250
+ zero runtime bytes.
251
+
252
+ ### Minimal React usage
253
+
254
+ ```tsx
255
+ import { init, type ChatWidgetInstance } from '@helpai/elements';
256
+ import { useEffect, useRef } from 'react';
257
+
258
+ export function Chat() {
259
+ const instance = useRef<ChatWidgetInstance | null>(null);
260
+
261
+ useEffect(() => {
262
+ instance.current = init({
263
+ publicKey: import.meta.env.VITE_HELPAI_KEY,
264
+ aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID,
265
+ });
266
+ return () => instance.current?.destroy();
267
+ }, []);
268
+
269
+ return null; // the widget renders its own DOM outside your tree
270
+ }
271
+ ```
272
+
273
+ ### Typed options
274
+
275
+ Autocomplete for every option:
276
+
277
+ ```ts
278
+ import type { ChatWidgetOptions } from '@helpai/elements';
279
+
280
+ const opts: ChatWidgetOptions = {
281
+ publicKey: 'agent_pk_...',
282
+ aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
283
+ presentation: { mode: 'floating' },
284
+ theme: { accent: '#ff3e00' },
285
+ launcher: { variant: 'pill', label: 'Ask AI' },
286
+ // ...
287
+ };
288
+ ```
289
+
290
+ ### SSR
291
+
292
+ The widget is client-only. Never call `init()` on the server. In
293
+ Next.js app router, wrap your component with `'use client'` (shown
294
+ above). In Remix / SvelteKit, use the `onMount` / `useEffect`
295
+ equivalent to defer init until the browser.
296
+
297
+ ---
298
+
299
+ ## Path D — Web Component
300
+
301
+ Declarative tag. Works in any framework or plain HTML.
302
+
303
+ ### Load + use
304
+
305
+ ```html
306
+ <!-- Load the bundle once (anywhere in the page) -->
307
+ <script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" defer></script>
308
+
309
+ <!-- Then use the element anywhere, any number of times -->
310
+ <web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="..." mode="inline" theme="auto" style="display: block; height: 480px"></web-ai-chat>
311
+ ```
312
+
313
+ ### Attributes
314
+
315
+ Same parser fields as `<script data-*>`, but without HTML's outer
316
+ `data-` dataset prefix. Values are strings (booleans are `"true"` /
317
+ `"false"`). `data-api-base-url` keeps its name because `data` refers to
318
+ the data API.
319
+
320
+ Reactive attributes re-resolve the live widget. `iframe` and
321
+ `start-minimized` are mount-time-only.
322
+
323
+ ```js
324
+ document.querySelector('web-ai-chat').setAttribute('theme', 'dark');
325
+ // The widget flips to dark mode.
326
+ ```
327
+
328
+ The element also has the panel methods: `open()`, `close()`, `expand()`, `openForm(id)` and `ask(text, options)`,
329
+ which opens the chat with `text` in the composer for the visitor to send (`{ send: true }` sends it):
330
+
331
+ ```js
332
+ document.querySelector('web-ai-chat').ask('How do I book a session?');
333
+ document.querySelector('web-ai-chat').ask('How do I book a session?', { send: true });
334
+ ```
335
+
336
+ ### Default mode
337
+
338
+ A bare `<web-ai-chat>` tag initially resolves to `mode="floating"`;
339
+ the handshake's `config.presentation.mode` supplies the deployment mode
340
+ when the host did not set one.
341
+
342
+ ### Container sizing (`inline`, `page`, `standalone`)
343
+
344
+ These modes have the host element declare `width: 100%; height: 100%`
345
+ inside its shadow root. Width fills the parent automatically; **height
346
+ only resolves if the parent has a defined height**. An undefined parent
347
+ height collapses the widget to `0px` tall and it appears empty.
348
+
349
+ ```html
350
+ <!-- ✓ Viewport-filling page -->
351
+ <html style="height: 100%">
352
+ <body style="height: 100%; margin: 0">
353
+ <web-ai-chat mode="page" public-key="…" ai-agent-deployment-id="…"></web-ai-chat>
354
+ </body>
355
+ </html>
356
+
357
+ <!-- ✓ Sized container (any of px / vh / 100% chain) -->
358
+ <div style="height: 600px">
359
+ <web-ai-chat mode="page" public-key="…" ai-agent-deployment-id="…"></web-ai-chat>
360
+ </div>
361
+
362
+ <!-- ✓ Flex / grid child with explicit sizing -->
363
+ <body style="display: flex; flex-direction: column; height: 100vh; margin: 0">
364
+ <header>…</header>
365
+ <web-ai-chat mode="page" style="flex: 1 1 0; min-height: 0" …></web-ai-chat>
366
+ </body>
367
+
368
+ <!-- ✗ Body has no defined height → host's 100% resolves to 0 -->
369
+ <body>
370
+ <web-ai-chat mode="page" …></web-ai-chat>
371
+ </body>
372
+ ```
373
+
374
+ `floating` mode is unaffected — the host is `position: fixed` and
375
+ the panel's size is set by `panelSize` (`normal` / `expanded` /
376
+ `fullscreen`), independent of the parent. `modal` and `drawer` are
377
+ also self-sized: modal anchors to the viewport center; drawer docks
378
+ to the side. See `../features/modes.md` §
379
+ 4.4 for the full sizing spec.
380
+
381
+ ### Inside a framework
382
+
383
+ Most frameworks treat custom elements like any other HTML element:
384
+
385
+ **React 19+** — native support:
386
+
387
+ ```tsx
388
+ <web-ai-chat public-key='agent_pk_...' ai-agent-deployment-id='01900000-0000-7000-8000-000000000000' mode='inline' />
389
+ ```
390
+
391
+ **React < 19** — the element works but TypeScript doesn't know the
392
+ custom type. Declare it:
393
+
394
+ ```ts
395
+ declare module 'react' {
396
+ namespace JSX {
397
+ interface IntrinsicElements {
398
+ 'web-ai-chat': {
399
+ 'public-key'?: string;
400
+ mode?: 'floating' | 'inline' | 'standalone' | 'page' | 'modal' | 'drawer';
401
+ theme?: 'auto' | 'light' | 'dark';
402
+ // ... (full list in configuration.md)
403
+ };
404
+ }
405
+ }
406
+ }
407
+ ```
408
+
409
+ **Vue 3** — works out of the box.
410
+
411
+ **Svelte** — works with `<svelte:options>`-less components.
412
+
413
+ **Angular** — add `CUSTOM_ELEMENTS_SCHEMA` to your module.
414
+
415
+ Framework-by-framework details: [Framework integrations](./frameworks.md).
416
+
417
+ ---
418
+
419
+ ## Version pinning
420
+
421
+ The bundle URL `https://cdn.help.ai/sdk/js/elements/latest/elements.js` always serves the
422
+ latest stable version. For reproducibility, pin to a specific
423
+ version:
424
+
425
+ ```html
426
+ <script src="https://cdn.help.ai/v1/elements.js" defer></script>
427
+ <!-- or -->
428
+ <script src="https://cdn.help.ai/v1.2.0/elements.js" defer></script>
429
+ ```
430
+
431
+ Pinning means you control upgrade timing. The floating URL means you
432
+ get new features + bug fixes automatically. Both are supported; most
433
+ customers use the floating URL for marketing sites and pinned
434
+ versions for mission-critical apps.
435
+
436
+ ---
437
+
438
+ ## Path E — Standalone ESM bundle (no build, no NPM install)
439
+
440
+ For pages without a build step (plain HTML, MDX, sandbox demos, code
441
+ playgrounds, AI-generated UIs), load the pre-bundled ESM file shipped
442
+ with every release. `elements.esm.js` is fully self-contained — every
443
+ dependency (preact, signals, streaming-markdown) is inlined. No bare
444
+ specifiers, no import maps, no `+esm` magic. Drop into any page with
445
+ `<script type="module">`.
446
+
447
+ ### Programmatic mount
448
+
449
+ ```html
450
+ <script type="module">
451
+ // Pin an exact version for production (see Caching & versioning below).
452
+ import { mount } from 'https://cdn.jsdelivr.net/npm/@helpai/elements@<version>/elements.esm.js';
453
+
454
+ mount({
455
+ publicKey: 'agent_pk_…',
456
+ aiAgentDeploymentId: '…',
457
+ presentation: { mode: 'inline' },
458
+ target: document.querySelector('#chat'),
459
+ });
460
+ </script>
461
+ ```
462
+
463
+ ### Web component (declarative tag)
464
+
465
+ The web-component bundle self-registers `<web-ai-chat>` on import:
466
+
467
+ ```html
468
+ <script type="module">
469
+ import 'https://cdn.jsdelivr.net/npm/@helpai/elements@<version>/elements-web-component.esm.js';
470
+ </script>
471
+
472
+ <web-ai-chat public-key="agent_pk_…" ai-agent-deployment-id="…" mode="inline"></web-ai-chat>
473
+ ```
474
+
475
+ ### File catalog (every `dist/<brand>/` ships these)
476
+
477
+ | File | Purpose | Use with |
478
+ | --------------------------------- | ------------------------------------------------------ | --------------------------------------- |
479
+ | `elements.js` | IIFE bundle, defines `window.HelpAI` | Plain `<script>` (no `type="module"`) |
480
+ | `elements.esm.js` | **Standalone ESM, fully bundled** | `<script type="module">` import |
481
+ | `elements-web-component.esm.js` | **Standalone ESM, registers `<web-ai-chat>`** | `<script type="module">` import (no JS) |
482
+ | `index.mjs` / `web-component.mjs` | tsdown ESM with bare imports (`import "preact"` etc.) | Bundlers (Vite, webpack, esbuild) |
483
+ | `react.mjs` / `react.d.ts` | React kit: `<HelpAI />`, `useHelpAI()`, widget bundled | React apps, `@helpai/elements/react` |
484
+ | `style.css` | Standalone stylesheet — host-element tokens + layout | `<link rel="stylesheet">` |
485
+ | `loader.js`, `loader.min.js` | GA-style queueing loader stub | Tag managers / analytics pipelines |
486
+ | `index.d.ts` | TypeScript declarations | NPM consumers |
487
+
488
+ ### CDN URL forms
489
+
490
+ | URL | Resolves to |
491
+ | --------------------------------------------------------------------------------- | -------------------------------------------------- |
492
+ | `https://cdn.help.ai/sdk/js/elements/latest/elements.js` | IIFE on the brand CDN |
493
+ | `https://cdn.help.ai/sdk/js/elements/latest/elements.esm.js` | Standalone ESM on the brand CDN |
494
+ | `https://cdn.jsdelivr.net/npm/@helpai/elements@<v>/elements.esm.js` | Standalone ESM on jsDelivr (immutable per version) |
495
+ | `https://cdn.jsdelivr.net/npm/@helpai/elements@<v>/elements-web-component.esm.js` | Web-component ESM on jsDelivr |
496
+
497
+ ### Version pinning
498
+
499
+ | Pattern | Behavior |
500
+ | ------------ | ------------------------------------------------- |
501
+ | `@1.4.0` | Exact version. Immutable, max-age cached forever. |
502
+ | `@1`, `@1.4` | Semver-range — latest matching `1.x.x` / `1.4.x`. |
503
+ | `@latest` | Always the most recent published version. |
504
+ | `@^1.4.0` | Caret range, just like in `package.json`. |
505
+
506
+ > **Production tip.** Pin a specific version for production traffic so
507
+ > a published patch can't surprise you. `@latest` is fine for
508
+ > playgrounds and internal tools.
509
+
510
+ > **Why not `+esm`?** jsDelivr's `/+esm` flag tries to auto-bundle on
511
+ > demand and trips on bare-specifier resolution for some packages —
512
+ > `Failed to resolve module specifier "preact"` is the common symptom.
513
+ > Loading the pre-bundled `elements.esm.js` avoids that entirely:
514
+ > built once at release time, immutable forever, every dep inlined.
515
+
516
+ ---
517
+
518
+ ## Caching, versioning & self-hosting
519
+
520
+ Two delivery models. Pick by how much you control the URL the customer pastes.
521
+
522
+ ### Option 1 — public CDN, pinned version (no infra; the default today)
523
+
524
+ Serve the published NPM package straight from **jsDelivr**, pinned to an exact
525
+ version — swap `<version>` for the release you want (the immutable pinning the live demos use):
526
+
527
+ ```html
528
+ <script type="module" async src="https://cdn.jsdelivr.net/npm/@helpai/elements@<version>/elements-web-component.esm.js"></script>
529
+ ```
530
+
531
+ - jsDelivr serves a pinned version as `Cache-Control: public, max-age=31536000,
532
+ immutable` — cached for a year, global edge, free.
533
+ - **Always pin an exact version** for production. `@latest` / ranges (`@0.56`)
534
+ are mutable (jsDelivr revalidates ~daily) **and** auto-update — a published
535
+ patch can change behavior under you.
536
+ - Trade-off: the customer must **bump the version in their snippet to update**,
537
+ and every release re-downloads the whole ~107 KB (one self-contained file).
538
+ - Speed it up on pages you control: `<link rel="preconnect"
539
+ href="https://cdn.jsdelivr.net" crossorigin>` (and optionally `rel="modulepreload"`).
540
+
541
+ ### Option 2 — your own CDN behind a stable URL (recommended; simplest for customers)
542
+
543
+ Point a CDN (`cdn.help.ai`, a Cloudflare/CloudFront origin, …) at the built
544
+ `dist/<brand>/` assets and give customers **one URL that never changes**:
545
+
546
+ ```html
547
+ <!-- The customer pastes this ONCE and never touches it again. -->
548
+ <script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" data-public-key="agent_pk_..." data-ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" defer></script>
549
+ ```
550
+
551
+ You ship updates **behind** that stable URL — the customer's page is frozen. The
552
+ caching rule is the whole trick:
553
+
554
+ | Asset | `Cache-Control` | Why |
555
+ | ------------------------------------- | -------------------------------------- | -------------------------------------------- |
556
+ | The stable entry (`elements.js`) | `public, max-age=300, must-revalidate` | Short TTL so a deploy propagates in minutes. |
557
+ | Content-hashed chunks (`*.<hash>.js`) | `public, max-age=31536000, immutable` | Hash in the name → safe to cache forever. |
558
+
559
+ Plus Brotli/Gzip, HTTP/2+3, and (optionally) `integrity="sha384-…"` SRI hashes.
560
+
561
+ ### Automatic chunking (zero customer changes)
562
+
563
+ The value of Option 2 is the **stable URL** above — the customer's snippet never
564
+ changes and you own caching + region. Chunking is a further, _optional_ step on
565
+ top (usually **not** worth it — see the caveat below). With **content-hashed
566
+ chunk filenames served from a version-less path**, an unchanged chunk keeps the
567
+ **same URL** across releases, so the browser reuses it — only the chunks that
568
+ actually changed re-download. To get there:
569
+
570
+ 1. Build ESM with code-splitting on (esbuild/tsdown `splitting: true`, `format:
571
+ "esm"`) and `[hash]` filenames. The thin entry imports the chunks.
572
+ 2. Add lazy `import()` boundaries at the **optional surfaces** — messenger tabs
573
+ (Home/Help/News), forms/HITL, voice. esbuild emits each as its own chunk and
574
+ hoists shared code (e.g. Preact) into a common chunk automatically. Core chat
575
+ loads first → smaller first paint; each feature caches independently.
576
+ 3. The customer keeps the single stable `<script>` — all splitting, hashing, and
577
+ cache invalidation happen behind it. No snippet change, ever.
578
+
579
+ > **Why this needs your own CDN — not jsDelivr.** On jsDelivr the version lives in
580
+ > the path (`/@0.56.2/…`). When you ship `0.56.3` **every** chunk URL changes —
581
+ > even a byte-identical vendor chunk gets `/@0.56.3/vendor.<hash>.js`, a guaranteed
582
+ > cache miss. So splitting on a version-pinned CDN trades one request for many of
583
+ > the same total bytes — _worse_. Cross-release chunk caching requires
584
+ > **content-addressed URLs where the version is not in the path**, i.e. a CDN you
585
+ > control. (Splitting still helps **first-paint** on either, via lazy features.)
586
+
587
+ > **Is it worth it? (usually no.)** We built and measured this for the `@helpai/elements`
588
+ > bundle and reverted it: only ~10 % of an ~107 KB gz bundle is lazy-able (the rest is
589
+ > core chat that loads on first paint regardless), and the lazy `import()` needed to
590
+ > create a chunk is **inlined** into the single-file builds — so it _adds_ ~1 KB there
591
+ > for no benefit. The cost lands on the builds that don't chunk. For a single
592
+ > self-contained file that's `async` + immutable-cached, the per-release re-download is
593
+ > a non-issue anyway. Reach for chunking only if your optional surfaces
594
+ > (tabs/forms/voice) grow large enough that lazy-loading them meaningfully shrinks first
595
+ > paint — and **measure with an esbuild metafile first**, don't split blind.
596
+
597
+ > **Data residency.** Self-hosting (Option 2) also lets you keep asset delivery in
598
+ > a chosen region; the runtime still calls the agent API at the configured
599
+ > `baseUrl`.
600
+
601
+ ---
602
+
603
+ ## What ships in the bundle
604
+
605
+ - **Preact 11 + @preact/signals** — reactive rendering primitives,
606
+ ~6 KB combined gzipped.
607
+ - **streaming-markdown** — incremental markdown renderer, ~3 KB.
608
+ - **Widget code** — chat transcript, composer, messenger-module tabs
609
+ (Home / Help / News), forms + HITL, config, themes — the bulk.
610
+ - **i18n** — built-in locale strings (EN + bundled translations).
611
+ - **CSS** — inlined into the bundle (no separate request).
612
+
613
+ Total **~107 KB gzipped** for the IIFE / standalone ESM (see the live
614
+ budgets in `.size-limit.json`, enforced in CI).
615
+ No external runtime dependencies. No React, no jQuery, no analytics
616
+ beacons. The bulk is widget code, **not** vendor — relevant when you
617
+ weigh chunking (see [Caching, versioning & self-hosting](#caching-versioning--self-hosting)).
618
+
619
+ ---
620
+
621
+ ## Next
622
+
623
+ - [Configuration](./configuration.md) — every option explained with examples.
624
+ - [Theming & customization](./theming.md) — matching your brand.
625
+ - [Framework integrations](./frameworks.md) — framework-specific recipes.
626
+ - [Troubleshooting](./troubleshooting.md) — "my widget isn't showing up".