@helpai/elements 1.8.0 → 1.10.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.
- package/README.md +34 -12
- package/configurator.mjs +1 -1
- package/docs/README.md +183 -0
- package/docs/analytics.md +551 -0
- package/docs/api-reference.md +637 -0
- package/docs/browser-support.md +302 -0
- package/docs/configuration.md +1014 -0
- package/docs/events-and-hooks.md +483 -0
- package/docs/faq.md +435 -0
- package/docs/frameworks.md +643 -0
- package/docs/installation.md +626 -0
- package/docs/options.md +323 -0
- package/docs/quickstart.md +229 -0
- package/docs/recipes.md +725 -0
- package/docs/security-privacy.md +636 -0
- package/docs/theming.md +570 -0
- package/docs/troubleshooting.md +523 -0
- package/elements-web-component.esm.js +39 -39
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +39 -39
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +39 -39
- package/elements.esm.js.map +4 -4
- package/elements.js +39 -39
- package/elements.js.map +4 -4
- package/index.d.ts +107 -3
- package/index.mjs +13 -13
- package/jsx-react.d.ts +15 -0
- package/jsx-svelte.d.ts +14 -0
- package/jsx-vue.d.ts +14 -0
- package/llms-full.txt +7954 -0
- package/llms.txt +37 -0
- package/loader.js +3 -3
- package/loader.min.js +1 -1
- package/package.json +36 -1
- package/react.d.ts +3575 -0
- package/react.mjs +62 -0
- package/schema.d.ts +56 -0
- package/schema.json +24 -2
- package/schema.mjs +1 -1
- package/web-component.d.ts +1583 -17
- package/web-component.mjs +14 -14
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Copy-paste-ready solutions for common integration scenarios.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Custom launcher button](#custom-launcher-button)
|
|
10
|
+
- [Programmatic control](#programmatic-control)
|
|
11
|
+
- [Multiple widgets on one page](#multiple-widgets-on-one-page)
|
|
12
|
+
- [SPA route changes](#spa-route-changes)
|
|
13
|
+
- [Inline chat inside a sidebar](#inline-chat-inside-a-sidebar)
|
|
14
|
+
- [Auto-open on a specific page](#auto-open-on-a-specific-page)
|
|
15
|
+
- [Close on specific user actions](#close-on-specific-user-actions)
|
|
16
|
+
- [Logged-in user personalization](#logged-in-user-personalization)
|
|
17
|
+
- [Analytics forwarding](#analytics-forwarding)
|
|
18
|
+
- [Error reporting](#error-reporting)
|
|
19
|
+
- [Custom storage adapter](#custom-storage-adapter)
|
|
20
|
+
- [Feature flags at runtime](#feature-flags-at-runtime)
|
|
21
|
+
- [A/B test the launcher](#ab-test-the-launcher)
|
|
22
|
+
- [Force a specific language](#force-a-specific-language)
|
|
23
|
+
- [Gate the widget behind consent](#gate-the-widget-behind-consent)
|
|
24
|
+
- [Conditionally show based on URL](#conditionally-show-based-on-url)
|
|
25
|
+
- [Ask the assistant from your own buttons](#ask-the-assistant-from-your-own-buttons)
|
|
26
|
+
- [Deep links in emails and docs](#deep-links-in-emails-and-docs)
|
|
27
|
+
- [Follow your site's light/dark](#follow-your-sites-lightdark)
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Custom launcher button
|
|
32
|
+
|
|
33
|
+
Hide the default FAB, trigger via your own UI:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { init } from '@helpai/elements';
|
|
37
|
+
|
|
38
|
+
const chat = init({
|
|
39
|
+
publicKey: 'agent_pk_...',
|
|
40
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
41
|
+
launcher: { hidden: true },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
document.querySelector('#my-custom-chat-btn').addEventListener('click', () => {
|
|
45
|
+
chat.open();
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
No script at all — mark your own element and hide the default FAB in the dashboard or with `launcher-hidden`:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<a href="/help" data-helpai-open>Chat with us</a> <a href="/help" data-helpai-open="help">Help articles</a>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Badge your button with the widget's unread count (conversations with replies the visitor hasn't read):
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// React
|
|
59
|
+
function ChatButton() {
|
|
60
|
+
const chat = useRef<ChatWidgetInstance | null>(null);
|
|
61
|
+
const [unread, setUnread] = useState(0);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
const instance = init({
|
|
65
|
+
publicKey: import.meta.env.VITE_HELPAI_KEY,
|
|
66
|
+
aiAgentDeploymentId: import.meta.env.VITE_HELPAI_DEPLOYMENT_ID,
|
|
67
|
+
launcher: { hidden: true },
|
|
68
|
+
});
|
|
69
|
+
chat.current = instance;
|
|
70
|
+
setUnread(instance.state.unread);
|
|
71
|
+
const off = instance.on('unread', setUnread);
|
|
72
|
+
return () => {
|
|
73
|
+
off();
|
|
74
|
+
instance.destroy();
|
|
75
|
+
};
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<button onClick={() => chat.current?.open()} className='my-btn'>
|
|
80
|
+
Chat
|
|
81
|
+
{unread > 0 && <span className='badge'>{unread}</span>}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Programmatic control
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
93
|
+
|
|
94
|
+
// Open / close
|
|
95
|
+
chat.open();
|
|
96
|
+
chat.close();
|
|
97
|
+
|
|
98
|
+
// Toggle tiers
|
|
99
|
+
chat.expand(); // normal ↔ expanded
|
|
100
|
+
|
|
101
|
+
// Change theme at runtime
|
|
102
|
+
chat.update({ theme: 'dark' });
|
|
103
|
+
|
|
104
|
+
// Change launcher label dynamically
|
|
105
|
+
chat.update({ launcher: { variant: 'pill', label: 'New message!' } });
|
|
106
|
+
|
|
107
|
+
// Later — unmount
|
|
108
|
+
chat.destroy();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
> **`update()` is a shallow merge.** Nested objects are replaced, not
|
|
112
|
+
> merged. If you want to preserve other fields of a nested object,
|
|
113
|
+
> spread it yourself first.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Multiple widgets on one page
|
|
118
|
+
|
|
119
|
+
Two (or more) chat widgets with different configs:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
const support = init({
|
|
123
|
+
widgetId: 'support',
|
|
124
|
+
publicKey: SUPPORT_KEY,
|
|
125
|
+
aiAgentDeploymentId: SUPPORT_DEPLOYMENT_ID,
|
|
126
|
+
presentation: { position: 'bottom-right' },
|
|
127
|
+
launcher: { label: 'Support', variant: 'pill' },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const sales = init({
|
|
131
|
+
widgetId: 'sales',
|
|
132
|
+
publicKey: SALES_KEY,
|
|
133
|
+
aiAgentDeploymentId: SALES_DEPLOYMENT_ID,
|
|
134
|
+
presentation: { position: 'bottom-left' },
|
|
135
|
+
launcher: { label: 'Sales', variant: 'pill', icon: 'spark' },
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Each has:
|
|
140
|
+
|
|
141
|
+
- Its own chat thread + history.
|
|
142
|
+
- Its own persisted state (`localStorage` namespaced by `widgetId`).
|
|
143
|
+
- Its own event stream.
|
|
144
|
+
|
|
145
|
+
Control individual widgets:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
support.open(); // opens support only
|
|
149
|
+
sales.close(); // closes sales only
|
|
150
|
+
|
|
151
|
+
// Look up by widgetId later
|
|
152
|
+
import { getInstance, getAllInstances } from '@helpai/elements';
|
|
153
|
+
const supportAgain = getInstance('support');
|
|
154
|
+
const allWidgets = getAllInstances(); // both
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## SPA route changes
|
|
160
|
+
|
|
161
|
+
Using `init()` is safe to call on every route change — it's
|
|
162
|
+
idempotent. If an instance with the same `widgetId` exists, it merges
|
|
163
|
+
options in rather than re-mounting.
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
// Next.js App Router
|
|
167
|
+
'use client';
|
|
168
|
+
|
|
169
|
+
import { init } from '@helpai/elements';
|
|
170
|
+
import { usePathname } from 'next/navigation';
|
|
171
|
+
import { useEffect } from 'react';
|
|
172
|
+
|
|
173
|
+
export function Chat() {
|
|
174
|
+
const pathname = usePathname();
|
|
175
|
+
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
init({
|
|
178
|
+
publicKey: process.env.NEXT_PUBLIC_HELPAI_KEY!,
|
|
179
|
+
aiAgentDeploymentId: process.env.NEXT_PUBLIC_HELPAI_DEPLOYMENT_ID!,
|
|
180
|
+
// Per-page customization
|
|
181
|
+
launcher: {
|
|
182
|
+
hidden: pathname.startsWith('/checkout'), // hide on checkout
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
}, [pathname]);
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
To fully tear down on navigation:
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const chat = init({ publicKey, aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
196
|
+
return () => chat.destroy();
|
|
197
|
+
}, []);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Inline chat inside a sidebar
|
|
203
|
+
|
|
204
|
+
```html
|
|
205
|
+
<div style="display: flex; height: 100vh">
|
|
206
|
+
<aside style="width: 320px; border-right: 1px solid #eee">
|
|
207
|
+
<h2>Help</h2>
|
|
208
|
+
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" mode="inline" theme="auto"></web-ai-chat>
|
|
209
|
+
</aside>
|
|
210
|
+
|
|
211
|
+
<main style="flex: 1">
|
|
212
|
+
<!-- your app content -->
|
|
213
|
+
</main>
|
|
214
|
+
</div>
|
|
215
|
+
|
|
216
|
+
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" defer></script>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The widget fills its parent container — 320px wide, full viewport
|
|
220
|
+
height. Parent needs to have a defined size.
|
|
221
|
+
|
|
222
|
+
### In React / Vue / etc.
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
// React
|
|
226
|
+
<aside className='chat-sidebar'>
|
|
227
|
+
<web-ai-chat public-key={KEY} ai-agent-deployment-id={DEPLOYMENT_ID} mode='inline' />
|
|
228
|
+
</aside>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
```vue
|
|
232
|
+
<!-- Vue -->
|
|
233
|
+
<aside class="chat-sidebar">
|
|
234
|
+
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="01900000-0000-7000-8000-000000000000" mode="inline" />
|
|
235
|
+
</aside>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Parent CSS:
|
|
239
|
+
|
|
240
|
+
```css
|
|
241
|
+
.chat-sidebar {
|
|
242
|
+
width: 320px;
|
|
243
|
+
height: 100vh;
|
|
244
|
+
display: flex;
|
|
245
|
+
flex-direction: column;
|
|
246
|
+
}
|
|
247
|
+
.chat-sidebar web-ai-chat {
|
|
248
|
+
flex: 1 1 0;
|
|
249
|
+
min-height: 0; /* critical — lets the widget shrink below its content */
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Auto-open on a specific page
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
const chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', behavior: { startMinimized: true } });
|
|
259
|
+
|
|
260
|
+
if (window.location.pathname === '/help') {
|
|
261
|
+
// Delay a beat so the widget has time to mount
|
|
262
|
+
setTimeout(() => chat.open(), 500);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Or forcibly start open on every page:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', behavior: { startMinimized: false } });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
> User choice wins over `behavior.startMinimized`. If the user closes the
|
|
273
|
+
> widget and reloads, it stays closed regardless.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Close on specific user actions
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
const chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
281
|
+
|
|
282
|
+
// Close when user submits your signup form
|
|
283
|
+
document.querySelector('#signup-form').addEventListener('submit', () => {
|
|
284
|
+
chat.close();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Close on ESC anywhere (the widget's internal ESC already does this
|
|
288
|
+
// inside the panel; this adds page-wide)
|
|
289
|
+
document.addEventListener('keydown', (e) => {
|
|
290
|
+
if (e.key === 'Escape') chat.close();
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Logged-in user personalization
|
|
297
|
+
|
|
298
|
+
Pass host-asserted context for personalized responses. This is
|
|
299
|
+
informational for the AI — never an auth credential:
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
// After the user signs in:
|
|
303
|
+
import { init } from '@helpai/elements';
|
|
304
|
+
|
|
305
|
+
init({
|
|
306
|
+
publicKey: 'agent_pk_...',
|
|
307
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
308
|
+
userContext: {
|
|
309
|
+
id: currentUser.id,
|
|
310
|
+
email: currentUser.email,
|
|
311
|
+
name: currentUser.name,
|
|
312
|
+
plan: currentUser.plan, // arbitrary custom scalar field
|
|
313
|
+
},
|
|
314
|
+
pageContext: {
|
|
315
|
+
area: 'account',
|
|
316
|
+
path: location.pathname,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
In a SPA the visitor is usually anonymous on first load and logs in
|
|
322
|
+
later. Rather than remounting, update context on the live instance — it
|
|
323
|
+
re-handshakes with the **same** visitor/conversation, so the conversation is
|
|
324
|
+
continuous:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
const chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
328
|
+
|
|
329
|
+
// after login:
|
|
330
|
+
chat.setUserContext({ id: currentUser.id, name: currentUser.name, plan: 'pro' });
|
|
331
|
+
|
|
332
|
+
// on SPA navigation:
|
|
333
|
+
chat.setPageContext({ area: 'checkout', path: '/checkout' });
|
|
334
|
+
|
|
335
|
+
// on logout, on a computer someone else may use next:
|
|
336
|
+
chat.reset(); // a new anonymous visitor: no history, no user context
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`reset()` forgets the stored visitor, their current conversation, preferences and user context, then reconnects as a
|
|
340
|
+
first visit; your `on()` listeners stay. Use `clearUserContext()` instead when the same person keeps the
|
|
341
|
+
conversation.
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Analytics forwarding
|
|
346
|
+
|
|
347
|
+
### Google Analytics 4
|
|
348
|
+
|
|
349
|
+
```html
|
|
350
|
+
<script>
|
|
351
|
+
window.HelpAIHooks = window.HelpAIHooks || [];
|
|
352
|
+
window.HelpAIHooks.push([
|
|
353
|
+
'*',
|
|
354
|
+
'*',
|
|
355
|
+
(inst, event, data) => {
|
|
356
|
+
gtag('event', `chat_${event}`, {
|
|
357
|
+
widget_id: inst.widgetId,
|
|
358
|
+
payload: typeof data === 'string' ? data : JSON.stringify(data).slice(0, 100),
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
</script>
|
|
363
|
+
<script src="https://cdn.help.ai/sdk/js/elements/latest/elements.js" defer></script>
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Segment
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { hook } from '@helpai/elements';
|
|
370
|
+
|
|
371
|
+
hook('*', '*', (inst, event, data) => {
|
|
372
|
+
window.analytics.track(`Chat ${event}`, {
|
|
373
|
+
widgetId: inst.widgetId,
|
|
374
|
+
...(typeof data === 'object' && data !== null ? data : {}),
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Mixpanel
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
import { hook } from '@helpai/elements';
|
|
383
|
+
|
|
384
|
+
hook('*', 'open', (inst) => mixpanel.track('Chat Opened', { widget: inst.widgetId }));
|
|
385
|
+
hook('*', 'message', (inst, _e, m) => {
|
|
386
|
+
if (m.role === 'user') mixpanel.track('Chat Message', { widget: inst.widgetId, chars: m.text.length });
|
|
387
|
+
});
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### PostHog
|
|
391
|
+
|
|
392
|
+
```ts
|
|
393
|
+
import { hook } from '@helpai/elements';
|
|
394
|
+
|
|
395
|
+
hook('*', '*', (inst, event, data) => {
|
|
396
|
+
posthog.capture(`chat:${event}`, { widgetId: inst.widgetId, data });
|
|
397
|
+
});
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Amplitude
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import * as amplitude from '@amplitude/analytics-browser';
|
|
404
|
+
import { hook } from '@helpai/elements';
|
|
405
|
+
|
|
406
|
+
hook('*', '*', (inst, event, data) => {
|
|
407
|
+
amplitude.track(`chat:${event}`, { widgetId: inst.widgetId, data });
|
|
408
|
+
});
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Error reporting
|
|
414
|
+
|
|
415
|
+
### Sentry
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { hook } from '@helpai/elements';
|
|
419
|
+
import * as Sentry from '@sentry/browser';
|
|
420
|
+
|
|
421
|
+
hook('*', 'error', (inst, _event, err) => {
|
|
422
|
+
Sentry.captureException(err, {
|
|
423
|
+
tags: { widget: inst.widgetId },
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Datadog RUM
|
|
429
|
+
|
|
430
|
+
```ts
|
|
431
|
+
import { datadogRum } from '@datadog/browser-rum';
|
|
432
|
+
import { hook } from '@helpai/elements';
|
|
433
|
+
|
|
434
|
+
hook('*', 'error', (inst, _event, err) => {
|
|
435
|
+
datadogRum.addError(err, { widget: inst.widgetId });
|
|
436
|
+
});
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
### Rollbar
|
|
440
|
+
|
|
441
|
+
```ts
|
|
442
|
+
import { hook } from '@helpai/elements';
|
|
443
|
+
import Rollbar from 'rollbar';
|
|
444
|
+
|
|
445
|
+
const rollbar = new Rollbar({ accessToken: '...', captureUncaught: true });
|
|
446
|
+
|
|
447
|
+
hook('*', 'error', (inst, _event, err) => {
|
|
448
|
+
rollbar.error(err instanceof Error ? err : new Error(String(err)), {
|
|
449
|
+
widget: inst.widgetId,
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Custom storage adapter
|
|
457
|
+
|
|
458
|
+
Default storage is `localStorage`. Custom adapters must be synchronous and
|
|
459
|
+
implement `get`, `set`, and `remove`.
|
|
460
|
+
|
|
461
|
+
### Conversation storage (cleared on tab close)
|
|
462
|
+
|
|
463
|
+
```ts
|
|
464
|
+
import { init, type ChatWidgetOptions } from '@helpai/elements';
|
|
465
|
+
|
|
466
|
+
type StorageAdapter = NonNullable<ChatWidgetOptions['storage']>;
|
|
467
|
+
|
|
468
|
+
const conversationAdapter: StorageAdapter = {
|
|
469
|
+
get: (key) => sessionStorage.getItem(key),
|
|
470
|
+
set: (key, value) => sessionStorage.setItem(key, value),
|
|
471
|
+
remove: (key) => sessionStorage.removeItem(key),
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', storage: conversationAdapter });
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Encrypted wrapper
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
import { init, type ChatWidgetOptions } from '@helpai/elements';
|
|
481
|
+
|
|
482
|
+
type StorageAdapter = NonNullable<ChatWidgetOptions['storage']>;
|
|
483
|
+
|
|
484
|
+
const encrypted: StorageAdapter = {
|
|
485
|
+
get: (key) => {
|
|
486
|
+
const raw = localStorage.getItem(key);
|
|
487
|
+
return raw ? decrypt(raw) : null;
|
|
488
|
+
},
|
|
489
|
+
set: (key, value) => localStorage.setItem(key, encrypt(value)),
|
|
490
|
+
remove: (key) => localStorage.removeItem(key),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', storage: encrypted });
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### In-memory (no persistence)
|
|
497
|
+
|
|
498
|
+
```ts
|
|
499
|
+
const mem = new Map<string, string>();
|
|
500
|
+
const memoryStorage: StorageAdapter = {
|
|
501
|
+
get: (k) => mem.get(k) ?? null,
|
|
502
|
+
set: (k, v) => {
|
|
503
|
+
mem.set(k, v);
|
|
504
|
+
},
|
|
505
|
+
remove: (k) => {
|
|
506
|
+
mem.delete(k);
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000', storage: memoryStorage });
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
## Feature flags at runtime
|
|
516
|
+
|
|
517
|
+
Disable features per-page or per-user:
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
const chat = init({
|
|
521
|
+
publicKey: 'agent_pk_...',
|
|
522
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
523
|
+
features: {
|
|
524
|
+
disableFileUpload: true, // hide paperclip + disable drop/paste
|
|
525
|
+
voice: 'disabled', // hide mic button
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Later — enable for paying users
|
|
530
|
+
if (user.isPaying) {
|
|
531
|
+
chat.update({
|
|
532
|
+
features: {
|
|
533
|
+
disableFileUpload: false,
|
|
534
|
+
voice: 'local',
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
---
|
|
541
|
+
|
|
542
|
+
## A/B test the launcher
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
const variant = Math.random() < 0.5 ? 'A' : 'B';
|
|
546
|
+
|
|
547
|
+
const launcherByVariant = {
|
|
548
|
+
A: { variant: 'circle', icon: 'chat' },
|
|
549
|
+
B: { variant: 'pill', label: 'Ask us', icon: 'spark' },
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const chat = init({
|
|
553
|
+
publicKey: 'agent_pk_...',
|
|
554
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
555
|
+
launcher: launcherByVariant[variant],
|
|
556
|
+
onOpen: () => analytics.track('chat_open', { variant }),
|
|
557
|
+
onMessage: () => analytics.track('chat_message', { variant }),
|
|
558
|
+
});
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Force a specific language
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
init({
|
|
567
|
+
publicKey: 'agent_pk_...',
|
|
568
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
569
|
+
i18n: {
|
|
570
|
+
locale: 'fr-CA', // force
|
|
571
|
+
availableLocales: [], // hide the picker
|
|
572
|
+
},
|
|
573
|
+
});
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
Let users switch but start in French:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
init({
|
|
580
|
+
publicKey: 'agent_pk_...',
|
|
581
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
582
|
+
i18n: {
|
|
583
|
+
defaultLocale: 'fr-CA',
|
|
584
|
+
availableLocales: ['en', 'fr-CA', 'es'],
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## Gate the widget behind consent
|
|
592
|
+
|
|
593
|
+
GDPR / CCPA pattern — only mount after the user accepts cookies:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
import { init, destroy } from '@helpai/elements';
|
|
597
|
+
|
|
598
|
+
let chat: ChatWidgetInstance | null = null;
|
|
599
|
+
|
|
600
|
+
window.addEventListener('cookies-accepted', () => {
|
|
601
|
+
chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
window.addEventListener('cookies-rejected', () => {
|
|
605
|
+
if (chat) {
|
|
606
|
+
chat.destroy();
|
|
607
|
+
// Clear any data the widget wrote
|
|
608
|
+
for (const key of Object.keys(localStorage)) {
|
|
609
|
+
if (key.startsWith('helpai.')) localStorage.removeItem(key);
|
|
610
|
+
}
|
|
611
|
+
chat = null;
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
With **in-memory storage** as a compromise when consent is declined:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
const mem = new Map<string, string>();
|
|
620
|
+
init({
|
|
621
|
+
publicKey: 'agent_pk_...',
|
|
622
|
+
aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000',
|
|
623
|
+
storage: {
|
|
624
|
+
get: (k) => mem.get(k) ?? null,
|
|
625
|
+
set: (k, v) => {
|
|
626
|
+
mem.set(k, v);
|
|
627
|
+
},
|
|
628
|
+
remove: (k) => {
|
|
629
|
+
mem.delete(k);
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
});
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
Widget works but state evaporates on reload.
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Conditionally show based on URL
|
|
640
|
+
|
|
641
|
+
```ts
|
|
642
|
+
import { init } from '@helpai/elements';
|
|
643
|
+
|
|
644
|
+
const allowedPaths = ['/docs', '/help', '/pricing'];
|
|
645
|
+
|
|
646
|
+
if (allowedPaths.some((p) => location.pathname.startsWith(p))) {
|
|
647
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
SPA version — tear down when leaving allowed paths:
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
import { init, destroy, getInstance } from '@helpai/elements';
|
|
655
|
+
|
|
656
|
+
function syncChat(pathname: string) {
|
|
657
|
+
const allowed = ['/docs', '/help', '/pricing'].some((p) => pathname.startsWith(p));
|
|
658
|
+
if (allowed && !getInstance()) {
|
|
659
|
+
init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
660
|
+
} else if (!allowed && getInstance()) {
|
|
661
|
+
destroy();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Hook into your router
|
|
666
|
+
syncChat(location.pathname); // initial
|
|
667
|
+
window.addEventListener('popstate', () => syncChat(location.pathname));
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
---
|
|
671
|
+
|
|
672
|
+
## Ask the assistant from your own buttons
|
|
673
|
+
|
|
674
|
+
`ask(text)` opens the chat with the text in the composer for the visitor to send (`{ send: true }` sends it right
|
|
675
|
+
away), so a pricing page, a product card or a help article can offer "Ask about this" in one line:
|
|
676
|
+
|
|
677
|
+
```ts
|
|
678
|
+
const chat = init({ publicKey: 'agent_pk_...', aiAgentDeploymentId: '01900000-0000-7000-8000-000000000000' });
|
|
679
|
+
document.querySelector('#ask-pricing')?.addEventListener('click', () => chat.ask('How much does the Pro plan cost?'));
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
No script needed: mark the element with `data-helpai-ask`. An empty value asks the element's own text; add
|
|
683
|
+
`data-helpai-send` to send it instead of filling the composer.
|
|
684
|
+
|
|
685
|
+
```html
|
|
686
|
+
<button data-helpai-ask="Which plan fits a team of 10?">Help me choose</button> <button data-helpai-ask>Do you ship to Canada?</button>
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
The same call works everywhere the widget does: `HelpAI('ask', text, options)` with the loader snippet (queued until
|
|
690
|
+
the bundle arrives) and `document.querySelector("web-ai-chat").ask(text, options)` for the element. The text waits
|
|
691
|
+
until the chat can take it (still connecting, a pre-chat form to answer; a send also waits for a reply still
|
|
692
|
+
arriving), so a button never needs to check the widget's state. Call it from something the visitor did.
|
|
693
|
+
|
|
694
|
+
## Deep links in emails and docs
|
|
695
|
+
|
|
696
|
+
Link straight into the widget from a support email, a docs page or a QR code — the page opens with the widget where
|
|
697
|
+
you pointed it:
|
|
698
|
+
|
|
699
|
+
```text
|
|
700
|
+
https://acme.com/account?helpai=help/019a0b6c-7e2a-7000-9d3e-4a5b6c7d8e01 → Help, at that article
|
|
701
|
+
https://acme.com/pricing#helpai-ask=Which+plan+fits+a+team+of+10%3F → the chat, with that question ready to send
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
The widget acts once and removes its parameters from the address. A linked question is never sent for the visitor.
|
|
705
|
+
|
|
706
|
+
---
|
|
707
|
+
|
|
708
|
+
## Follow your site's light/dark
|
|
709
|
+
|
|
710
|
+
If your site has its own light/dark toggle, let the widget follow it instead of the OS:
|
|
711
|
+
|
|
712
|
+
```html
|
|
713
|
+
<web-ai-chat public-key="agent_pk_..." ai-agent-deployment-id="..." theme="host"></web-ai-chat>
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
The widget reads your page's `dark` / `light` class, `data-theme` or `color-scheme` and flips with it, keeping the
|
|
717
|
+
brand accent set in the dashboard.
|
|
718
|
+
|
|
719
|
+
---
|
|
720
|
+
|
|
721
|
+
## Next
|
|
722
|
+
|
|
723
|
+
- [Events & hooks](./events-and-hooks.md) — full event API
|
|
724
|
+
- [Configuration](./configuration.md) — every option
|
|
725
|
+
- [Troubleshooting](./troubleshooting.md) — when things go wrong
|