@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.
@@ -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