@helpai/elements 1.7.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.
@@ -1,23 +1,1567 @@
1
+ import { z } from "zod";
1
2
  /**
2
- * Custom element `<web-ai-chat>`.
3
+ * Pluggable client-side key-value storage.
3
4
  *
4
- * Attributes are a curated flat projection of {@link ChatWidgetOptions}:
5
- * `mode` maps to `presentation.mode`, `response-mode` to
6
- * `behavior.responseMode`, and so on. Reactive attributes trigger a
7
- * re-resolve when they change at runtime — see {@link REACTIVE_ATTRS}.
5
+ * Drop in a custom adapter to:
6
+ * - Add logging / analytics around reads + writes
7
+ * - Encrypt or sign values
8
+ * - Use a different backing store (sessionStorage, IndexedDB shim, in-memory)
9
+ * - Add quota / migration handling
8
10
  *
9
- * Mode resolution: the tag itself doesn't pin a default mode. When no
10
- * `mode=…` attribute is set, the resolver default (`"floating"`) seeds
11
- * the very first frame — which the App's handshake gate keeps blank
12
- * — then the server's pushed `config.presentation.mode` wins via the
13
- * shared runtime's handshake merge. Drop a bare `<web-ai-chat
14
- * public-key="…" ai-agent-deployment-id="…">` on the page and the
15
- * deployment's dashboard config drives the layout.
11
+ * The default implementation is {@link LocalStorageAdapter} — a thin wrapper
12
+ * over `localStorage` with try/catch around every call so storage being
13
+ * disabled (private mode pre-iOS 11, floating webviews, quota exceeded)
14
+ * silently degrades to no-op rather than throwing.
16
15
  *
17
- * Everything between `<App>` and the shadow host — the ErrorBoundary, the
18
- * handshake-config merge, the usage tracker, host-attribute application — lives
19
- * in the shared {@link createWidgetRuntime} that `mount()` (`src/index.ts`) also
20
- * uses, so the two boot paths can't drift. This file owns only what's specific
21
- * to the custom element: attribute parsing and the connect/disconnect lifecycle.
16
+ * Values are stored as opaque strings — the persistence layer above
17
+ * serialises typed values to JSON before calling `set`.
22
18
  */
19
+ interface ClientStorage {
20
+ /** Returns the stored string for `key`, or `null` when absent / unreadable. */
21
+ get(key: string): string | null;
22
+ /** Stores `value` under `key`. Best-effort — silent on quota / disabled storage. */
23
+ set(key: string, value: string): void;
24
+ /** Removes `key`. No-op if absent. */
25
+ remove(key: string): void;
26
+ }
27
+ /**
28
+ * UI strings — built-in defaults, type contracts, and runtime resolution.
29
+ *
30
+ * The widget bundles five complete locales (en/fr/ar/es/he — English is also the
31
+ * fallback for any missing key; see {@link BUILTIN_STRINGS}). Further locales, or
32
+ * per-key overrides, are supplied by the page owner (`init({ i18n: { strings } })`)
33
+ * or the SaaS backend in `config.i18n.strings` — both pushed *partially*: only the keys
34
+ * they want to override.
35
+ *
36
+ * To keep render hot-paths cheap, the merged shape is **pre-computed once**
37
+ * (at config-resolve time and after each server-settings merge) into a fully
38
+ * populated {@link Strings} map. UI components then read `strings.send` as a
39
+ * normal property access — no per-render lookup, no per-render allocation.
40
+ */
41
+ type StringKey = 'launcherOpen' | 'launcherLabel' | 'panelTitle' | 'composerPlaceholder' | 'send' | 'stop' | 'queueMessage' | 'removeQueuedMessage' | 'compactingContext' | 'attach' | 'micStart' | 'micStop' | 'micUnsupported' | 'expand' | 'collapse' | 'fullscreen' | 'exitFullscreen' | 'popOut' | 'resizeHandle' | 'scrollToBottom' | 'close' | 'moreActions' | 'soundOn' | 'soundOff' | 'language' | 'theme' | 'themeAuto' | 'themeLight' | 'themeDark' | 'textSize' | 'textSizeSmall' | 'textSizeNormal' | 'textSizeLarge' | 'history' | 'historyTitle' | 'historyEmpty' | 'historyLoading' | 'historyBack' | 'historyContinue' | 'loadMoreConversations' | 'conversationLoading' | 'loadPreviousMessages' | 'conversationClosed' | 'startNewConversation' | 'talkToOperator' | 'connectingOperator' | 'noOperatorAvailable' | 'chattingWithOperator' | 'cancelOperatorRequest' | 'dateToday' | 'dateYesterday' | 'dateLastWeek' | 'dateOlder' | 'newConversation' | 'dropZone' | 'attachmentTooLarge' | 'attachmentTooMany' | 'attachmentMimeRejected' | 'errorRetry' | 'errorGeneric' | 'errorRateLimited' | 'loading' | 'visualPreparing' | 'visualError' | 'visualPartial' | 'thinking' | 'thoughts' | 'toolResult' | 'sources' | 'feedbackUp' | 'feedbackDown' | 'feedbackThanks' | 'copy' | 'copied' | 'collapseSidebar' | 'expandSidebar' | 'formSubmit' | 'formSkip' | 'formSubmitted' | 'formSkipped' | 'formFillOut' | 'formRequired' | 'formInvalidEmail' | 'formInvalidTel' | 'formInvalidUrl' | 'formInvalidNumber' | 'formTooShort' | 'formTooLong' | 'formNumberTooSmall' | 'formNumberTooLarge' | 'formPatternMismatch' | 'formChooseAtLeast' | 'formChooseAtMost' | 'formOther' | 'formOtherPlaceholder' | 'inputRequired' | 'inputBadge' | 'inputAnswered' | 'inputSkipped' | 'inputSubmit' | 'inputSubmitHint' | 'inputSkip' | 'showOptions' | 'hideOptions' | 'qtypeText' | 'qtypeEmail' | 'qtypePhone' | 'qtypeNumber' | 'qtypeDate' | 'qtypeChooseOne' | 'qtypeChooseAny' | 'qtypeBoolean' | 'confirmYes' | 'confirmNo' | 'approve' | 'reject' | 'approvalPrompt' | 'approvalBody' | 'approvalYourResponse' | 'approvalEditHint' | 'approvalSubmitting' | 'edit' | 'toolParameters' | 'statusAwaiting' | 'statusResponded' | 'statusCompleted' | 'statusDenied' | 'statusError' | 'statusRunning' | 'statusSuperseded' | 'tabHome' | 'tabConversations' | 'tabHelp' | 'tabNews' | 'modulesEmpty' | 'moduleBack' | 'contentLoading' | 'contentHelpful' | 'homeGreeting' | 'homeGreetingNamed' | 'homeGreetingLead' | 'homeSearchPlaceholder' | 'homeAskPlaceholder' | 'homeWhatsNew' | 'homeContentTitle' | 'homeStatus' | 'helpTitle' | 'helpSearchPlaceholder' | 'helpEmpty' | 'helpLoading' | 'helpSearchEmpty' | 'helpShowMore' | 'newsTitle' | 'newsEmpty' | 'newsLoading' | 'newsBack' | 'newsPublishedAt' | 'newsShowMore' | 'newsUnseen';
42
+ /** A partial map for one locale — what overrides look like on the wire. */
43
+ type LocaleStrings = Partial<Record<StringKey, string>>;
44
+ /**
45
+ * Override shape accepted by both `init({ i18n: { strings } })` and the server's
46
+ * `config.i18n.strings`. Two forms supported:
47
+ *
48
+ * 1. **Flat** — `{ send: "Click", stop: "Halt" }`. Applies to the active
49
+ * locale. Convenient for single-language deployments.
50
+ * 2. **Nested** — `{ en: { send: "Click" }, fr: { send: "Cliquer" } }`.
51
+ * Lets a multilingual backend push every translation in one payload —
52
+ * the widget picks the entry matching the user's locale and falls back
53
+ * to the `en` entry (then to built-in English) for any missing keys.
54
+ *
55
+ * Both forms may be **partial** at every level — anything you don't supply
56
+ * inherits the built-in default. Detection is by value type: if any
57
+ * top-level value is an object, the whole override is treated as nested.
58
+ */
59
+ type StringsOverride = LocaleStrings | Partial<Record<string, LocaleStrings>>;
60
+ /**
61
+ * Primitive type aliases shared across every config section. Kept in
62
+ * one file so a domain-specific change (adding a new `Mode` value,
63
+ * say) doesn't churn many files.
64
+ */
65
+ /** Screen corner the floating launcher anchors to (ignored in `inline` / `standalone`). */
66
+ type Position = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
67
+ /**
68
+ * Runtime rendering mode — what the widget *looks like* on the page.
69
+ *
70
+ * - `floating` — floating launcher FAB + popover panel.
71
+ * - `inline` — full chat in normal flow, sized to its container.
72
+ * - `standalone` — fills the viewport (panel only, no sidebar).
73
+ * - `page` — full viewport with sidebar (logo + nav blocks + conversation list) + chat.
74
+ * - `modal` — centered modal with dimmed backdrop. No launcher FAB —
75
+ * the page brings its own trigger and calls
76
+ * `instance.open()`. Sized via `size.width` / `size.height`.
77
+ * Defaults: `startMinimized: true` (so it doesn't auto-open),
78
+ * Escape + backdrop-click close.
79
+ * - `drawer` — side-docked flyout. The page stays interactive
80
+ * alongside (no backdrop). Width comes from `size.width`
81
+ * (default 440px). Side comes from `position`. Defaults
82
+ * to a flyout with `size.inset: "3vh"` vertical /
83
+ * `"12px"` horizontal — restore the legacy full-edge
84
+ * slab with `size: { width: "100vw", height: "100vh", inset: "0" }`.
85
+ *
86
+ * Page mode reads `site` and `blocks` from the handshake response.
87
+ */
88
+ type Mode = 'floating' | 'inline' | 'standalone' | 'page' | 'modal' | 'drawer';
89
+ /** End-user text-size preference — scales the whole type ramp. */
90
+ type TextSize = 'small' | 'normal' | 'large';
91
+ /** Theme preference. `auto` follows `prefers-color-scheme`; `host` follows the page's own light/dark. */
92
+ type ThemePreference = 'auto' | 'light' | 'dark' | 'host';
93
+ /**
94
+ * How the assistant's reply is delivered.
95
+ *
96
+ * - `'streaming'` (default) — per-token deltas arrive as the model generates;
97
+ * user sees a typewriter effect in real time.
98
+ * - `'buffered'` — server buffers the complete reply, then sends it as one
99
+ * chunk. The UI shows a "thinking…" indicator until the reply appears
100
+ * whole.
101
+ */
102
+ type ResponseMode = 'streaming' | 'buffered';
103
+ /**
104
+ * Identifiers for every header action the widget can render.
105
+ *
106
+ * `history` + `close` render as dedicated header buttons; the rest appear in
107
+ * the ⋯ overflow menu. The enabled set is a flat {@link ActionList}; order is
108
+ * irrelevant (the menu has a fixed layout). Unlisted actions are hidden.
109
+ *
110
+ * - `clear` — Start a new conversation (compose icon).
111
+ * - `expand` — Toggle `normal` ↔ `expanded` (1.5× panel).
112
+ * - `fullscreen` — Toggle `normal` ↔ `fullscreen` (viewport fill).
113
+ * - `popOut` — Open the active conversation in a dedicated tab.
114
+ * - `sound` — Toggle notification sounds + haptics on/off (switch-style).
115
+ * - `language` — Switch the UI locale at runtime.
116
+ * - `theme` — Cycle `auto` / `light` / `dark`.
117
+ * - `textSize` — Cycle `normal` / `large` / `small`.
118
+ * - `close` — Close the floating panel.
119
+ */
120
+ type ActionName = 'clear' | 'expand' | 'fullscreen' | 'popOut' | 'sound' | 'history' | 'language' | 'theme' | 'textSize' | 'close';
121
+ /**
122
+ * The header's enabled actions. Unlisted actions are hidden; order is
123
+ * irrelevant. Per-mode defaults (`DEFAULT_ACTIONS_BY_MODE`) pick a sensible
124
+ * set for each mode; a deployment can replace the whole list.
125
+ */
126
+ type ActionList = ActionName[];
127
+ /**
128
+ * Composer chrome and capability flags. `ChatWidgetOptions.composer` owns the
129
+ * send-button skin + attachment limits; `ChatWidgetOptions.features` owns voice,
130
+ * file-upload, HITL, and operator gates.
131
+ */
132
+ /**
133
+ * How voice input is captured and transcribed.
134
+ *
135
+ * - `'local'` (default when supported) — Web Speech API, runs in the browser.
136
+ * Free, no audio ever leaves the device; quality varies by browser.
137
+ * - `'server'` — record audio with `MediaRecorder`, upload to the transcribe
138
+ * endpoint, receive text back. Consistent quality; uses your upload quota.
139
+ * - `'disabled'` — hide the mic button entirely.
140
+ */
141
+ type VoiceMode = 'local' | 'server' | 'disabled';
142
+ /** Capability flags. Any field left `undefined` uses its default. */
143
+ interface FeatureFlags {
144
+ /** Voice input behaviour (see {@link VoiceMode}). Default: `'local'`. */
145
+ voice?: VoiceMode;
146
+ /**
147
+ * Turn OFF the human-in-the-loop tool UI — the inline ask-questions form and the
148
+ * approve/reject controls for gated tools. Unset/`false` = shown (default); `true`
149
+ * suppresses the UI (the wire still carries the signals; they just won't show).
150
+ */
151
+ disableHumanInLoop?: boolean;
152
+ /**
153
+ * Turn OFF file uploads — hide the paperclip button + reject drag/drop/paste.
154
+ * Unset/`false` = enabled (default); `true` disables them.
155
+ */
156
+ disableFileUpload?: boolean;
157
+ /**
158
+ * Opt IN operator control — exposes the "Talk to a person" affordance + connected-to-operator
159
+ * states. Positive opt-in (unlike the `disable*` flags) for a new capability; default off.
160
+ */
161
+ enableOperatorRequest?: boolean;
162
+ }
163
+ /**
164
+ * Client-side attachment limits. Server enforcement is still required —
165
+ * these prevent wasted upload bandwidth and give the user fast feedback.
166
+ */
167
+ interface AttachmentLimits {
168
+ maxBytes?: number;
169
+ maxCount?: number;
170
+ /** Comma-separated MIME allowlist, e.g. `'image/*,application/pdf'`. */
171
+ accept?: string;
172
+ }
173
+ /** Outline shape of the send button. */
174
+ type SendButtonShape = 'circle' | 'square' | 'pill';
175
+ /** Icon shown inside the send button. */
176
+ type SendButtonIcon = 'arrowUp' | 'paperPlane' | 'send' | 'chevron';
177
+ /** Fill/stroke treatment. */
178
+ type SendButtonVariant = 'filled' | 'outline' | 'ghost';
179
+ interface SendButtonOptions {
180
+ /** Outline shape. Default: `"circle"`. */
181
+ shape?: SendButtonShape;
182
+ /** Icon inside the button. Default: `"arrowUp"`. */
183
+ icon?: SendButtonIcon;
184
+ /** Fill treatment. Default: `"filled"`. */
185
+ variant?: SendButtonVariant;
186
+ }
187
+ interface ComposerOptions {
188
+ sendButton?: SendButtonOptions;
189
+ /** Client-side attachment limits. Server still validates at upload time. */
190
+ attachments?: AttachmentLimits;
191
+ }
192
+ /**
193
+ * Audible + tactile feedback knobs. Both channels default to enabled (browser
194
+ * autoplay policy still gates audio playback).
195
+ */
196
+ /** Lifecycle events the feedback bus can react to. */
197
+ type FeedbackEvent = 'messageReceived' | 'messageSent' | 'error' | 'voiceStart' | 'voiceStop';
198
+ /**
199
+ * Per-event sound configuration.
200
+ *
201
+ * - `true` — use the built-in synthesised tone.
202
+ * - `false` — disable this event's sound.
203
+ * - `string` — URL to a custom audio file (MP3, WAV, OGG). Decoded once + cached.
204
+ */
205
+ type SoundEventValue = boolean | string;
206
+ interface SoundOptions {
207
+ /** Master on/off. Default: `true` (browser autoplay policy still applies). */
208
+ enabled?: boolean;
209
+ /** Global volume 0..1. Default: `0.3` (subtle). */
210
+ volume?: number;
211
+ /** Per-event overrides. Missing keys use the built-in default (see feedback/defaults). */
212
+ events?: Partial<Record<FeedbackEvent, SoundEventValue>>;
213
+ }
214
+ /**
215
+ * Per-event haptic pattern. Android `navigator.vibrate()` shape.
216
+ *
217
+ * - `true` — use the default pattern for this event.
218
+ * - `false` — disable haptics for this event.
219
+ * - `number` — single vibration of that many ms.
220
+ * - `number[]` — alternating `[vibrate, pause, vibrate, …]`.
221
+ *
222
+ * iOS Safari has no custom-pattern support; supported events fire a single
223
+ * system haptic via the `<input type=checkbox switch>` trick.
224
+ */
225
+ type HapticEventValue = boolean | number | number[];
226
+ interface HapticsOptions {
227
+ /** Default: `true` on touch devices, silently ignored elsewhere. */
228
+ enabled?: boolean;
229
+ events?: Partial<Record<FeedbackEvent, HapticEventValue>>;
230
+ }
231
+ /**
232
+ * Section: audible + tactile feedback. Both channels default to enabled
233
+ * (browser autoplay policy still gates audio playback).
234
+ */
235
+ interface FeedbackOptions {
236
+ sound?: SoundOptions;
237
+ haptics?: HapticsOptions;
238
+ }
239
+ /**
240
+ * Event-driven forms engine — framework-free runtime types (mirror of
241
+ * `src/schema/widget/forms.ts`). The IIFE path has no Zod, so these plain
242
+ * interfaces are what the resolver, controller, and form UI consume.
243
+ *
244
+ * A deployment declares an array of {@link FormDef}s; each is bound to trigger
245
+ * event(s) and shown as a gate that replaces the composer (locking the chat
246
+ * until submit/skip). Its answers are recorded via one shared submit endpoint.
247
+ * Generic across verticals — there is NO industry logic; a clinic intake, an
248
+ * insurance claim form, a CSAT survey, and a sales signal capture are all just
249
+ * different config.
250
+ *
251
+ * The same {@link FormField} shape powers the AI-driven ask-questions tool.
252
+ */
253
+ /** A renderable field type. `multiselect` collects an array of values. */
254
+ type FieldType = 'text' | 'email' | 'tel' | 'url' | 'number' | 'date' | 'time' | 'textarea' | 'select' | 'radio' | 'checkbox' | 'multiselect';
255
+ /** One choice for `select` / `radio` / `multiselect` fields. */
256
+ interface FieldOption {
257
+ value: string;
258
+ /** Display text; falls back to `value`. */
259
+ label?: string;
260
+ description?: string;
261
+ }
262
+ /** Validation constraints applied client-side (the backend still validates). */
263
+ interface FieldValidation {
264
+ /** Regex source string (anchored as written) — text-like fields. */
265
+ pattern?: string;
266
+ minLength?: number;
267
+ maxLength?: number;
268
+ /** Numeric range — `number` fields. */
269
+ min?: number;
270
+ max?: number;
271
+ /** Selection-count bounds — `multiselect` fields. */
272
+ minSelections?: number;
273
+ maxSelections?: number;
274
+ }
275
+ /** One field in a form. */
276
+ interface FormField {
277
+ /** Stable key the value is recorded under (e.g. `email`). */
278
+ name: string;
279
+ label: string;
280
+ type: FieldType;
281
+ placeholder?: string;
282
+ required?: boolean;
283
+ /** Pre-filled value (string for scalars; comma-joined for `multiselect`). */
284
+ defaultValue?: string;
285
+ /** Choices for `select` / `radio` / `multiselect`. */
286
+ options?: FieldOption[];
287
+ validation?: FieldValidation;
288
+ /** `select`/`radio`/`multiselect` — allow a free-text "Other" answer. */
289
+ allowOther?: boolean;
290
+ /** Small helper line under the field. */
291
+ validationHint?: string;
292
+ }
293
+ /**
294
+ * Trigger that surfaces a form. Terse string with an optional `:param` so it's
295
+ * serializable + server-pushable. `after-messages:3`, `idle:30`,
296
+ * `page-area:pricing`.
297
+ */
298
+ type FormTrigger = 'pre-chat' | `after-messages:${number}` | 'conversation-closed' | 'panel-close' | `idle:${number}` | `page-area:${string}` | 'manual';
299
+ /**
300
+ * Extra eligibility predicate, evaluated when a trigger fires. Small and
301
+ * declarative on purpose — richer routing belongs to the agent / backend.
302
+ */
303
+ interface FormCondition {
304
+ /** Only show if `userContext` lacks ALL of these keys (progressive profiling). */
305
+ missingContext?: string[];
306
+ /** Only on these page areas (`pageContext.area`). */
307
+ pageArea?: string | string[];
308
+ minMessages?: number;
309
+ maxMessages?: number;
310
+ }
311
+ /**
312
+ * How often a form may show: `once` per device (persisted), once per
313
+ * `conversation` (re-eligible when a new conversation starts), or `always`.
314
+ */
315
+ type FormFrequency = 'once' | 'conversation' | 'always';
316
+ /** One form as authored in config (raw input). */
317
+ interface FormDef {
318
+ /** Stable id — persistence dedupe + `manual` `openForm(id)`. */
319
+ id: string;
320
+ on: FormTrigger | FormTrigger[];
321
+ when?: FormCondition;
322
+ fields: FormField[];
323
+ title?: string;
324
+ description?: string;
325
+ submitLabel?: string;
326
+ skippable?: boolean;
327
+ /** Default `once`. */
328
+ frequency?: FormFrequency;
329
+ /**
330
+ * Mirror the submitted answers into `userContext` so they ride every
331
+ * subsequent request (the agent sees them in-conversation). Default `true`.
332
+ * Set `false` for PII (DOB, policy #, symptoms) — those then go ONLY to the
333
+ * submit endpoint, never onto the always-on context envelope.
334
+ */
335
+ mirrorToContext?: boolean;
336
+ /**
337
+ * Skipped marker offers a "Fill out" button that reopens the form (skip is
338
+ * a deferral, not a refusal). Default `true`. Set `false` when a skip must
339
+ * be final — e.g. a consent ask that should not be re-surfaced.
340
+ */
341
+ reopenable?: boolean;
342
+ /**
343
+ * Submitted marker expands in place to a read-only list of the visitor's
344
+ * answers. Default `true`. Set `false` to keep the marker plain — e.g.
345
+ * sensitive answers that shouldn't linger on screen (pairs with
346
+ * `mirrorToContext: false`; the backend should also omit `values` from the
347
+ * `bootstrap` `formSubmissions` records for such forms).
348
+ */
349
+ reviewable?: boolean;
350
+ /** BCP-47 locale the top-level strings (title / fields copy) are authored in. */
351
+ defaultLocale?: string;
352
+ /**
353
+ * Per-locale string overrides. The data module trims this to the requested
354
+ * `?locale=` (≤1 entry); the widget overlays it onto the default copy at
355
+ * resolve time (`overlayFormDef`). Absent strings fall back to the default.
356
+ */
357
+ translations?: FormTranslation[];
358
+ }
359
+ /**
360
+ * Per-locale overrides of a form's HUMAN-FACING strings only. Structure (field
361
+ * `name` / `type` / `validation`, option `value`) is shared across locales and
362
+ * stays canonical, so submissions + validation are locale-independent — a
363
+ * translation matches the canonical form by the stable `name` (field) / `value`
364
+ * (option) keys. Absent strings fall back to the top-level (defaultLocale) copy.
365
+ *
366
+ * The data module trims `FormDef.translations` to the requested `?locale=` (≤1
367
+ * entry) on read; the widget overlays that entry onto the default fields. Mirror
368
+ * of `module-data-api` `FormTranslation`.
369
+ */
370
+ interface FieldOptionTranslation {
371
+ /** The canonical option `value` this localizes. */
372
+ value: string;
373
+ label?: string;
374
+ description?: string;
375
+ }
376
+ interface FormFieldTranslation {
377
+ /** The canonical field `name` this localizes. */
378
+ name: string;
379
+ label?: string;
380
+ placeholder?: string;
381
+ validationHint?: string;
382
+ options?: FieldOptionTranslation[];
383
+ }
384
+ interface FormTranslation {
385
+ locale: string;
386
+ title?: string;
387
+ description?: string;
388
+ submitLabel?: string;
389
+ fields?: FormFieldTranslation[];
390
+ }
391
+ /** The `forms` block is an ordered list (max 20); order = priority. */
392
+ type FormsOptions = FormDef[];
393
+ /**
394
+ * Launcher (floating-mode FAB) + the pre-engagement callout bubble.
395
+ * Inputs are option-bag shaped; the resolved variant lives in `widget.ts`.
396
+ */
397
+ /**
398
+ * Launcher visual variant.
399
+ *
400
+ * - `circle` — icon only, round bubble (default).
401
+ * - `pill` — icon + label, pill-shaped.
402
+ * - `bar` — rectangular button, stretches with content.
403
+ * - `minimal` — transparent outline, text-only.
404
+ */
405
+ type LauncherVariant = 'circle' | 'pill' | 'bar' | 'minimal';
406
+ /** Launcher size preset. Resolves to concrete CSS lengths in panel.css. */
407
+ type LauncherSize = 'sm' | 'md' | 'lg';
408
+ /**
409
+ * Visual variant of the launcher callout bubble.
410
+ *
411
+ * - `pill` — short, rounded label with a tiny arrow toward the FAB.
412
+ * Best for one-liners like "Click here →".
413
+ * - `bubble` — speech-bubble shape with a tail. Friendly persona feel.
414
+ * - `callout` — larger pill with a louder arrow. Most attention-grabbing.
415
+ */
416
+ type CalloutShape = 'pill' | 'bubble' | 'callout';
417
+ /**
418
+ * Side of the launcher the callout anchors to. `auto` picks the side
419
+ * facing into the page based on the FAB's corner. Picks pointing AT the
420
+ * FAB's own viewport edge auto-flip to the opposite side.
421
+ */
422
+ type CalloutPosition = 'auto' | 'horizontal' | 'vertical';
423
+ /**
424
+ * Pre-engagement message bubble next to the FAB. Use it for a greeting
425
+ * ("👋 Hi, need help?"), an attention-grabbing nudge ("Click here →"),
426
+ * or an onboarding tip — same component, configured for the use case.
427
+ *
428
+ * `persistent: true` (default) → re-show on every visit regardless of
429
+ * dismissal.
430
+ * `persistent: false` → clicking X stores this exact text and keeps it
431
+ * hidden across visits.
432
+ */
433
+ interface LauncherCalloutOptions {
434
+ /** Text content. Empty string hides the callout. */
435
+ text: string;
436
+ /** Visual shape. Default: `"pill"`. */
437
+ shape?: CalloutShape;
438
+ /** Side of the launcher to anchor on. Default: `"auto"`. */
439
+ position?: CalloutPosition;
440
+ /** Delay before the bubble appears (ms). Default: `1200`. */
441
+ delayMs?: number;
442
+ /** Auto-dismiss after this many ms. Default: never (`0`). */
443
+ autoDismissMs?: number;
444
+ /** Animate with a subtle horizontal nudge. Default: `true`. */
445
+ animated?: boolean;
446
+ /** Re-show on every visit regardless of prior dismissal. Default: `true`. */
447
+ persistent?: boolean;
448
+ }
449
+ /** Launcher appearance + behaviour. Only applies in `floating` mode. */
450
+ interface LauncherOptions {
451
+ variant?: LauncherVariant;
452
+ size?: LauncherSize;
453
+ label?: string;
454
+ icon?: string;
455
+ hidden?: boolean;
456
+ /** Pre-engagement bubble next to the FAB. See {@link LauncherCalloutOptions}. */
457
+ callout?: LauncherCalloutOptions;
458
+ }
459
+ /**
460
+ * Messenger "modules" runtime config types — framework-free mirror of
461
+ * `src/schema/widget/modules.ts`. The IIFE runtime path has no Zod, so
462
+ * these plain interfaces are what the resolver + UI consume.
463
+ *
464
+ * `modules` is an ordered list of tabs; each picks a UI `layout`, the content
465
+ * collections it shows (`contentCollectionIds` — the filter on `GET /pai/content`),
466
+ * and a translatable `label`.
467
+ */
468
+ /** The UI layouts a tab can render with. `chat` is the conversation; the rest render content. */
469
+ type ModuleLayout = 'chat' | 'home' | 'help' | 'news';
470
+ /** A team/agent avatar shown in the Home hero. */
471
+ interface ModuleAvatar {
472
+ name: string;
473
+ /** Image URL; initials of `name` are shown when omitted. */
474
+ avatar?: string;
475
+ role?: string;
476
+ }
477
+ /** The Home status card. */
478
+ interface ModuleStatus {
479
+ /**
480
+ * Status line. An i18n key or a literal (resolved via `localizeText`).
481
+ * Omit to use the built-in generic `homeStatus` string — so a deployment can
482
+ * show a localized status without authoring copy or a custom key per locale.
483
+ */
484
+ text?: string;
485
+ /**
486
+ * Status severity → status-icon colour. Free text; `operational` (default),
487
+ * `degraded`, and `down` are styled, any other value uses the operational style.
488
+ */
489
+ level?: string;
490
+ /** Opens in an in-widget iframe when tapped. */
491
+ url?: string;
492
+ }
493
+ /**
494
+ * `home` layout config — flat feature flags (no block authoring). The Home tab
495
+ * is assembled from these in a fixed order: greeting → search → recent conversation
496
+ * → status → a content list (the pinned items of the tab's `contentCollectionIds`,
497
+ * titled `contentBlockTitle`).
498
+ */
499
+ interface HomeConfig {
500
+ brandName?: string;
501
+ hideGreeting?: boolean;
502
+ greetingText?: string;
503
+ userAvatars?: ModuleAvatar[];
504
+ hideSearchBar?: boolean;
505
+ hideRecentConversations?: boolean;
506
+ status?: ModuleStatus;
507
+ contentBlockTitle?: string;
508
+ }
509
+ /** One tab as authored in config (raw input). */
510
+ interface ModuleOptions extends HomeConfig {
511
+ label: string;
512
+ layout: ModuleLayout;
513
+ /** Content collections this tab shows, in order (max 20). */
514
+ contentCollectionIds?: string[];
515
+ id?: string;
516
+ }
517
+ /** The `modules` block is an ordered list of tabs (max 4). */
518
+ type ModulesOptions = ModuleOptions[];
519
+ /** CSS-custom-property overrides applied as inline styles on the host. */
520
+ interface ThemeOverrides {
521
+ /** Light/dark beside the overrides — `host` keeps a deployment's accent while the page sets the mode. */
522
+ mode?: ThemePreference;
523
+ accent?: string;
524
+ accentText?: string;
525
+ radius?: string;
526
+ fontFamily?: string;
527
+ /** Readable content-column cap (→ `--conversation-max`). CSS length; default `768px`. */
528
+ contentMaxWidth?: string;
529
+ }
530
+ /**
531
+ * Widget sizing overrides. Each field accepts any valid CSS length
532
+ * (`'640px'`, `'80vh'`, `'calc(100vh - 64px)'`).
533
+ *
534
+ * Three display sizes are supported (in order from compact to largest):
535
+ * 1. **normal** — default. Uses `width`/`height` (or CSS defaults).
536
+ * 2. **expanded** — triggered by the expand header button. Uses
537
+ * `expanded.width`/`expanded.height`.
538
+ * 3. **fullscreen** — triggered by the fullscreen header button. Always
539
+ * fills the viewport regardless of other dimensions.
540
+ */
541
+ interface SizeOptions {
542
+ /** Normal-mode width. Default: mode-specific (see `src/styles/panel.css`). */
543
+ width?: string;
544
+ /** Normal-mode height. Default: mode-specific. */
545
+ height?: string;
546
+ /** Dimensions when the user clicks the expand button. */
547
+ expanded?: {
548
+ width?: string;
549
+ height?: string;
550
+ };
551
+ /**
552
+ * Edge inset for `drawer` mode — turns the side dock into a flyout
553
+ * with the given gap from each viewport edge (`16px`, `2vh`,
554
+ * `min(24px, 4vw)`, …). Combine with a partial `height` (`80vh`,
555
+ * `min(720px, calc(100vh - 32px))`) for the classic floating
556
+ * side-panel look. Ignored by every other mode.
557
+ */
558
+ inset?: string;
559
+ /** Enable + configure user-drag resize of the panel in normal mode. */
560
+ resize?: ResizeOptions;
561
+ /**
562
+ * Which size to open with on the **first** render.
563
+ *
564
+ * - `"auto"` (**default**, also when omitted) — responsive by viewport: phones
565
+ * (`window.innerWidth < 640`) open `fullscreen` edge-to-edge, tablets
566
+ * (`640 … autoSizeBreakpoint`) open `normal`, desktop/laptop
567
+ * (`≥ autoSizeBreakpoint`) open `expanded`. Right for the device with no config;
568
+ * the backend can tune the desktop breakpoint without baking it into the frontend.
569
+ * - `"normal"` — force the compact panel everywhere.
570
+ * - `"expanded"` — force-start at the configured `expanded.width/height`.
571
+ * - `"fullscreen"` — force-start at viewport fill.
572
+ *
573
+ * Only applies to the very first mount. The user's last size is
574
+ * preserved across close → reopen; a page reload re-evaluates.
575
+ * Standalone / page mode always overrides to `fullscreen`.
576
+ *
577
+ * Default: `"auto"`.
578
+ */
579
+ initialSize?: 'auto' | 'normal' | 'expanded' | 'fullscreen';
580
+ /**
581
+ * Viewport-width threshold (px) above which `initialSize: "auto"` picks
582
+ * `"expanded"`. Default: `900`.
583
+ */
584
+ autoSizeBreakpoint?: number;
585
+ }
586
+ /**
587
+ * Drag-resize configuration. When `enabled`, a corner grip appears on the
588
+ * panel; the user drags it to resize. The resolved dimensions persist via
589
+ * `userPrefs.widgetSize` (cached client-side + synced to the backend).
590
+ */
591
+ interface ResizeOptions {
592
+ /** Show the resize grip. Default: `false`. */
593
+ enabled?: boolean;
594
+ minWidth?: string;
595
+ maxWidth?: string;
596
+ minHeight?: string;
597
+ maxHeight?: string;
598
+ }
599
+ /**
600
+ * Section: how the widget physically presents itself on the page.
601
+ */
602
+ interface PresentationOptions {
603
+ mode?: Mode;
604
+ position?: Position;
605
+ /**
606
+ * Wrap the widget in a same-origin iframe for strict JS + CSS isolation
607
+ * beyond what shadow DOM provides. Composes with every mode.
608
+ */
609
+ iframe?: boolean;
610
+ size?: SizeOptions;
611
+ }
612
+ /**
613
+ * Section: conversation runtime behaviour — auto-open, streaming mode, and the
614
+ * transcript surface (what renders, how it scrolls, reaction controls).
615
+ */
616
+ interface BehaviorOptions {
617
+ /** When `true`, don't auto-open the panel on first visit. */
618
+ startMinimized?: boolean;
619
+ /** Streaming vs. buffered reply delivery. */
620
+ responseMode?: ResponseMode;
621
+ /** Show the model's reasoning ("thoughts") as a collapsible block. Default `false`. */
622
+ showReasoning?: boolean;
623
+ /** Show automatic tool/function calls (details + result) in the transcript. Default `false`. */
624
+ showToolCalls?: boolean;
625
+ /** Show citation sources (web links + document references) in the transcript. Default `false`. */
626
+ showSources?: boolean;
627
+ /** Turn OFF the 👍/👎 feedback controls on rateable (assistant/operator) replies. Unset/`false` = shown (default). */
628
+ disableMessageFeedback?: boolean;
629
+ /** Auto-send a suggestion chip's text on tap. Unset/`false` = fill the composer to edit (default). */
630
+ autoSendSuggestions?: boolean;
631
+ /** URL opened by the `popOut` header action. */
632
+ popOutUrl?: string;
633
+ }
634
+ /**
635
+ * Section: panel header actions — the enabled set (`history`/`close` buttons +
636
+ * ⋯ menu). Unlisted actions are hidden.
637
+ */
638
+ interface HeaderOptions {
639
+ actions?: ActionList;
640
+ }
641
+ /**
642
+ * Section: locale resolution + per-instance string overrides.
643
+ *
644
+ * Order of precedence at startup:
645
+ * 1. End-user's persisted picker choice (if matches `availableLocales`).
646
+ * 2. `i18n.locale` (page-owner explicit override).
647
+ * 3. Server-pushed `i18n.defaultLocale`.
648
+ * 4. `navigator.language` (when default is `"auto"`).
649
+ * 5. `"en"` ultimate fallback.
650
+ */
651
+ interface I18nOptions {
652
+ locale?: string;
653
+ defaultLocale?: string;
654
+ availableLocales?: string[];
655
+ strings?: StringsOverride;
656
+ }
657
+ /**
658
+ * "Powered by …" attribution shown below the composer. Any combination of
659
+ * logo + text + href is valid; all three absent hides the element.
660
+ */
661
+ interface PoweredByOption {
662
+ /** Visible text (e.g. `"Powered by Help AI"`). */
663
+ text?: string;
664
+ /** If set, the attribution is an `<a>` pointing here (`noopener noreferrer`). */
665
+ href?: string;
666
+ /** Optional small logo URL rendered before the text. */
667
+ logoUrl?: string;
668
+ }
669
+ /**
670
+ * Section: footer area below the composer — brand attribution and
671
+ * the optional ChatGPT-style "AI can make mistakes" disclaimer.
672
+ */
673
+ interface FooterOptions {
674
+ /** Optional "Powered by …" attribution rendered below the disclaimer. */
675
+ poweredBy?: PoweredByOption;
676
+ /**
677
+ * Small muted text rendered below the composer. Same pattern as ChatGPT's
678
+ * "ChatGPT can make mistakes." When absent / empty, the line is hidden.
679
+ */
680
+ composerDisclaimer?: string;
681
+ }
682
+ /**
683
+ * Host-asserted context about the current end-user, forwarded to the backend
684
+ * and the agent purely as **informational context** — never as an auth
685
+ * credential. The widget runs on third-party sites (e.g. `example.com`); some
686
+ * have their own login, others are guest-only. We don't register or
687
+ * authenticate these users (browser access always identifies a deployment;
688
+ * a public key is optional for intentional guest mode).
689
+ *
690
+ * `id` / `email` / `name` are recognised fields used for the Home greeting and
691
+ * transcripts; any extra scalar fields become custom context the agent can use
692
+ * (plan, role, signup date, …). Untrusted input — sanitised client-side via
693
+ * `sanitizeUserContext` before it goes on the wire.
694
+ */
695
+ interface UserContext {
696
+ id?: string;
697
+ email?: string;
698
+ name?: string;
699
+ /** Subscription / pricing tier (e.g. `"pro"`). */
700
+ plan?: string;
701
+ /** The user's role on the host site (e.g. `"admin"`, `"member"`). */
702
+ role?: string;
703
+ /** Phone number. */
704
+ phone?: string;
705
+ /** Arbitrary extra scalar context forwarded to the agent. */
706
+ [key: string]: string | number | boolean | undefined;
707
+ }
708
+ /**
709
+ * Suggested values for {@link PageContext.area} — the host-defined surface the
710
+ * visitor is currently on. **Free-form**: any string is accepted; these are
711
+ * the common ones (they also drive editor autocomplete). The agent uses it to
712
+ * adapt routing / tone — a pricing-page visitor differs from a docs reader,
713
+ * a help-desk ticket, or a checkout.
714
+ */
715
+ type PageArea = 'home' | 'docs' | 'help-desk' | 'support' | 'pricing' | 'checkout' | 'orders' | 'account' | 'blog' | (string & {});
716
+ /**
717
+ * Host-asserted context about the *page / visit* — where on the site the
718
+ * visitor is and which surface they're using. Informational context for the
719
+ * agent, separate from {@link UserContext} (who the visitor is). `url`
720
+ * auto-fills from the host page's `location.href` when omitted. Untrusted —
721
+ * sanitised client-side before sending.
722
+ */
723
+ interface PageContext {
724
+ /** Host-defined surface the visitor is on (see {@link PageArea}). */
725
+ area?: PageArea;
726
+ /** Full page URL. Auto-filled from `location.href` when omitted. */
727
+ url?: string;
728
+ /** Page path, e.g. `/pricing/enterprise`. */
729
+ path?: string;
730
+ /** Human-readable page title. */
731
+ title?: string;
732
+ /** Arbitrary extra scalar context about this page / visit. */
733
+ [key: string]: string | number | boolean | undefined;
734
+ }
735
+ /**
736
+ * Override the HTTP endpoints the widget calls. `upload` / `transcribe`
737
+ * paths are appended to the agent base (`${baseUrl}/api/agent`);
738
+ * `submitForm` is appended to the data base (`dataApiBaseUrl`). Absolute URLs
739
+ * also work (useful when the upload endpoint lives on a different origin
740
+ * like a CDN presign service).
741
+ */
742
+ interface Endpoints {
743
+ /** File upload endpoint. Default: `'/pai/upload-file'`. */
744
+ upload?: string;
745
+ /** Voice transcription endpoint. Default: `'/pai/transcribe-audio'`. */
746
+ transcribe?: string;
747
+ /**
748
+ * Form submission endpoint. Default: `'/submit-form'`. When any form
749
+ * (a pre-chat signal capture, a CSAT survey, etc.) is completed, the widget POSTs
750
+ * the captured values here (fire-and-forget), keyed by the same `visitorId` +
751
+ * `conversationId` the conversation uses; the payload carries `formId` + `trigger`
752
+ * so the backend can route. Optional: a backend that doesn't implement it just
753
+ * 404s and the submission is ignored.
754
+ */
755
+ submitForm?: string;
756
+ }
757
+ /**
758
+ * Built-in anonymous usage tracking — utm.gif-style pixel hits that give
759
+ * the widget OWNER (the site embedding it) insights about how visitors
760
+ * use the widget: events, pages, devices, locales, funnels.
761
+ *
762
+ * Privacy contract (enforced in `src/core/tracking.ts`):
763
+ * - never sends message text or form values —
764
+ * event payloads pass through a static per-event allowlist;
765
+ * - the only visitor identifier is the widget's existing anonymous
766
+ * `visitorId`; deployment and anonymous conversation ids provide context;
767
+ * - page metadata is origin + pathname, with query/hash removed;
768
+ * - geo + browser are derived server-side (IP + User-Agent header),
769
+ * never collected client-side;
770
+ * - `navigator.globalPrivacyControl` / Do-Not-Track force it off
771
+ * regardless of config.
772
+ */
773
+ /** Section: anonymous owner-facing usage tracking. */
774
+ interface TrackingOptions {
775
+ /**
776
+ * Master switch. Default `true` — the widget always fires; the data
777
+ * module gates ingestion per deployment server-side. Set `false`
778
+ * (deployment config or init) to silence the client entirely.
779
+ */
780
+ enabled?: boolean;
781
+ /**
782
+ * Pixel collector URL. Default: `${dataApiBaseUrl}/pai/elements-px.gif`
783
+ * (e.g. `https://help.ai/api/data/pai/elements-px.gif`).
784
+ */
785
+ endpoint?: string;
786
+ /**
787
+ * Per-widget-runtime sampling 0..1. `0.25` samples roughly a quarter of
788
+ * mounted runtimes; one decision is reused for that runtime's funnel.
789
+ * Default `1`.
790
+ */
791
+ sampleRate?: number;
792
+ /**
793
+ * Anti-forgery token, echoed as `tk=` on every hit. **Server-issued,
794
+ * never set by hand** — the backend mints it in the
795
+ * `/handshake` response (HMAC over `pk`/`vid`/expiry with a
796
+ * server-side secret; see `docs/api/tracking-collector.md`) and the
797
+ * collector verifies it statelessly. Opaque to the client.
798
+ */
799
+ token?: string;
800
+ }
801
+ /**
802
+ * Raw options passed to {@link mount}, {@link init}, the Web Component, or
803
+ * the `<script data-*>` auto-boot path. The public `mount()` / `init()`
804
+ * boundary requires `aiAgentDeploymentId`; parsers keep it optional while
805
+ * assembling an attribute snapshot so a missing value can fail at that boundary.
806
+ *
807
+ * Two layers:
808
+ *
809
+ * - **Embed (L2)** — top-level fields owned by the customer's
810
+ * developer (`publicKey`, `aiAgentDeploymentId`, `userContext`, …).
811
+ * Issued by the SaaS portal, pasted into their site code.
812
+ * - **Widget settings (L3)** — nested sections owned by the portal
813
+ * admin: `presentation`, `behavior`, `theme`, `launcher`, `header`,
814
+ * `composer`, `feedback`, `i18n`, `footer`, `features`, and the other
815
+ * schema-backed sections below.
816
+ *
817
+ * Runtime-only fields (`target`, `storage`, `onMessage` callbacks) live
818
+ * alongside at the top level — they're function / DOM types that can't be
819
+ * serialised on the wire.
820
+ */
821
+ interface ChatWidgetOptions {
822
+ /**
823
+ * Stable identifier for this widget instance. Use when mounting more
824
+ * than one widget on the same page (different deployments, different
825
+ * targets). Isolates client-storage namespaces (visitorId, active conversationId,
826
+ * cached userPrefs). Default: `"default"`.
827
+ */
828
+ widgetId?: string;
829
+ /** Deployment public key (`agent_pk_...`). Sent as `x-public-key` when present. */
830
+ publicKey?: string;
831
+ /** Deployment ID (UUID). Required by every public mount and sent as `x-ai-agent-deployment-id`. */
832
+ aiAgentDeploymentId?: string;
833
+ /**
834
+ * MAIN site origin (no trailing slash, e.g. `https://help.ai`). The API
835
+ * modules derive from it — the production edge mounts the agent API at
836
+ * `${baseUrl}/api/agent` and the data API at `${baseUrl}/api/data`.
837
+ * Defaults to the brand's origin.
838
+ */
839
+ baseUrl?: string;
840
+ /**
841
+ * Agent-API base (no trailing slash) — set it when the
842
+ * `module-agent-api` service is reached DIRECTLY (a standalone
843
+ * local module, e.g. `http://localhost:3069`) instead of through the
844
+ * edge. Routes attach as-is (`${agentApiBaseUrl}/pai/handshake`).
845
+ * Precedence: this option > derived from the main origin
846
+ * (`${baseUrl}/api/agent`). Not server-pushable — the handshake already
847
+ * travelled over it.
848
+ */
849
+ agentApiBaseUrl?: string;
850
+ /**
851
+ * Data-API base (no trailing slash) — the `module-data-api`
852
+ * service serving content, form definitions, and form submissions under
853
+ * `/pai/*` (`content`, `bootstrap`, `submit-form`).
854
+ * Set it for a standalone local module (e.g. `http://localhost:3071`).
855
+ * Precedence: this option > server-pushed `config.dataApiBaseUrl` > derived
856
+ * from the main origin (`${baseUrl}/api/data`, e.g.
857
+ * `https://help.ai/api/data`).
858
+ */
859
+ dataApiBaseUrl?: string;
860
+ /** Host-asserted, informational end-user context (untrusted; not auth). */
861
+ userContext?: UserContext;
862
+ /** Host-asserted, informational page/visit context (untrusted; `area`, url, …). */
863
+ pageContext?: PageContext;
864
+ /** HTTP endpoint overrides. */
865
+ endpoints?: Endpoints;
866
+ /**
867
+ * Console log verbosity. `false` / `"off"` silences everything,
868
+ * `true` → `"debug"`. Levels `error` / `warn` / `info` / `debug` /
869
+ * `trace` gate output additively. Runtime-toggleable via
870
+ * `chat.setDebug(level)` or `?<brand>-debug=trace` in the page URL.
871
+ */
872
+ debug?: boolean | 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
873
+ presentation?: PresentationOptions;
874
+ behavior?: BehaviorOptions;
875
+ /** Theme preference (`'auto' | 'light' | 'dark'`) or overrides object. */
876
+ theme?: ThemePreference | ThemeOverrides;
877
+ launcher?: LauncherOptions;
878
+ header?: HeaderOptions;
879
+ composer?: ComposerOptions;
880
+ feedback?: FeedbackOptions;
881
+ i18n?: I18nOptions;
882
+ footer?: FooterOptions;
883
+ /** Capability flags (file upload, voice mode, HITL, operator request). Server-pushable. */
884
+ features?: FeatureFlags;
885
+ /** Event-driven forms (signal capture, CSAT, intake, consent…). Server-pushable. */
886
+ forms?: FormsOptions;
887
+ /** Messenger modules (Chat / Home / Messages / Help / News tabs). Server-pushable. */
888
+ modules?: ModulesOptions;
889
+ /** Anonymous owner-facing usage tracking (pixel hits). Server-pushable. */
890
+ tracking?: TrackingOptions;
891
+ /**
892
+ * Custom client-side storage adapter. Implements the {@link ClientStorage}
893
+ * interface (`get` / `set` / `remove` / `clear`).
894
+ */
895
+ storage?: ClientStorage;
896
+ onMessage?: (msg: {
897
+ role: string;
898
+ text: string;
899
+ }) => void;
900
+ onOpen?: () => void;
901
+ onClose?: () => void;
902
+ onError?: (err: unknown) => void;
903
+ }
904
+ /** `true` for any string other than the literal `"false"`. Useful for boolean attrs. */
905
+ declare function boolAttr(v: string): boolean;
906
+ /**
907
+ * Attributes that don't fit a generic table because their parser
908
+ * needs custom logic (multi-attr aggregation, empty-string accepted,
909
+ * etc.). Listed here so they're folded into `REACTIVE_ATTRS` without
910
+ * a free-floating string literal further down.
911
+ */
912
+ declare const SPECIAL_REACTIVE_ATTRS: readonly ['launcher-label', 'callout-text', 'callout-delay', 'actions', 'user-context', 'page-context'];
913
+ /**
914
+ * Every flat-attribute table in this file. Single source of truth —
915
+ * both the runtime parser and `REACTIVE_ATTRS` derive from this list.
916
+ */
917
+ declare const ATTR_TABLES: readonly [readonly [["public-key", "publicKey"], ["ai-agent-deployment-id", "aiAgentDeploymentId"], ["base-url", "baseUrl"], ["agent-api-base-url", "agentApiBaseUrl"], ["data-api-base-url", "dataApiBaseUrl"], ["theme", "theme", (v: string) => string]], readonly [["mode", "mode", (v: string) => string], ["position", "position", (v: string) => string], ["iframe", "iframe", typeof boolAttr]], readonly [["width", "width"], ["height", "height"]], readonly [["response-mode", "responseMode", (v: string) => string], ["start-minimized", "startMinimized", typeof boolAttr], ["show-reasoning", "showReasoning", typeof boolAttr], ["show-tool-calls", "showToolCalls", typeof boolAttr], ["show-sources", "showSources", typeof boolAttr], ["disable-message-feedback", "disableMessageFeedback", typeof boolAttr], ["auto-send-suggestions", "autoSendSuggestions", typeof boolAttr], ["pop-out-url", "popOutUrl"]], readonly [["locale", "locale"]], readonly [["launcher-variant", "variant", (v: string) => string], ["launcher-size", "size", (v: string) => string], ["launcher-icon", "icon"], ["launcher-hidden", "hidden", typeof boolAttr]], readonly [["features-disable-file-upload", "disableFileUpload", typeof boolAttr], ["features-voice", "voice", (v: string) => string]], readonly [["endpoint-upload", "upload"], ["endpoint-transcribe", "transcribe"]], readonly [["user-id", "id"], ["user-email", "email"], ["user-name", "name"]], readonly [["page-area", "area"], ["page-url", "url"], ["page-path", "path"], ["page-title", "title"]]];
918
+ /** Every attribute `<web-ai-chat>` reads, as written in HTML — the names its JSX typings offer. */
919
+ type WebAiChatAttributeName = (typeof ATTR_TABLES)[number][number][0] | (typeof SPECIAL_REACTIVE_ATTRS)[number];
920
+ declare const handshakeResponseSchema: z.ZodObject<{
921
+ deployment: z.ZodObject<{
922
+ id: z.ZodString;
923
+ name: z.ZodString;
924
+ slug: z.ZodOptional<z.ZodString>;
925
+ publicKey: z.ZodOptional<z.ZodString>;
926
+ }, z.core.$loose>;
927
+ agent: z.ZodObject<{
928
+ id: z.ZodString;
929
+ name: z.ZodString;
930
+ title: z.ZodOptional<z.ZodString>;
931
+ status: z.ZodOptional<z.ZodEnum<{
932
+ away: "away";
933
+ offline: "offline";
934
+ online: "online";
935
+ }>>;
936
+ }, z.core.$loose>;
937
+ config: z.ZodOptional<z.ZodObject<{
938
+ presentation: z.ZodOptional<z.ZodOptional<z.ZodObject<{
939
+ mode: z.ZodDefault<z.ZodEnum<{
940
+ drawer: "drawer";
941
+ floating: "floating";
942
+ inline: "inline";
943
+ modal: "modal";
944
+ page: "page";
945
+ standalone: "standalone";
946
+ }>>;
947
+ position: z.ZodDefault<z.ZodEnum<{
948
+ "bottom-left": "bottom-left";
949
+ "bottom-right": "bottom-right";
950
+ "top-left": "top-left";
951
+ "top-right": "top-right";
952
+ }>>;
953
+ iframe: z.ZodDefault<z.ZodBoolean>;
954
+ size: z.ZodOptional<z.ZodObject<{
955
+ width: z.ZodOptional<z.ZodString>;
956
+ height: z.ZodOptional<z.ZodString>;
957
+ expanded: z.ZodOptional<z.ZodObject<{
958
+ width: z.ZodOptional<z.ZodString>;
959
+ height: z.ZodOptional<z.ZodString>;
960
+ }, z.core.$loose>>;
961
+ resize: z.ZodOptional<z.ZodObject<{
962
+ enabled: z.ZodDefault<z.ZodBoolean>;
963
+ minWidth: z.ZodOptional<z.ZodString>;
964
+ maxWidth: z.ZodOptional<z.ZodString>;
965
+ minHeight: z.ZodOptional<z.ZodString>;
966
+ maxHeight: z.ZodOptional<z.ZodString>;
967
+ }, z.core.$loose>>;
968
+ inset: z.ZodOptional<z.ZodString>;
969
+ initialSize: z.ZodDefault<z.ZodEnum<{
970
+ auto: "auto";
971
+ expanded: "expanded";
972
+ fullscreen: "fullscreen";
973
+ normal: "normal";
974
+ }>>;
975
+ autoSizeBreakpoint: z.ZodDefault<z.ZodNumber>;
976
+ }, z.core.$loose>>;
977
+ }, z.core.$loose>>>;
978
+ behavior: z.ZodOptional<z.ZodOptional<z.ZodObject<{
979
+ startMinimized: z.ZodDefault<z.ZodBoolean>;
980
+ responseMode: z.ZodDefault<z.ZodEnum<{
981
+ buffered: "buffered";
982
+ streaming: "streaming";
983
+ }>>;
984
+ showReasoning: z.ZodDefault<z.ZodBoolean>;
985
+ showToolCalls: z.ZodDefault<z.ZodBoolean>;
986
+ showSources: z.ZodDefault<z.ZodBoolean>;
987
+ disableMessageFeedback: z.ZodDefault<z.ZodBoolean>;
988
+ autoSendSuggestions: z.ZodDefault<z.ZodBoolean>;
989
+ popOutUrl: z.ZodOptional<z.ZodString>;
990
+ }, z.core.$loose>>>;
991
+ theme: z.ZodOptional<z.ZodDefault<z.ZodUnion<readonly [z.ZodEnum<{
992
+ auto: "auto";
993
+ dark: "dark";
994
+ host: "host";
995
+ light: "light";
996
+ }>, z.ZodObject<{
997
+ mode: z.ZodOptional<z.ZodEnum<{
998
+ auto: "auto";
999
+ dark: "dark";
1000
+ host: "host";
1001
+ light: "light";
1002
+ }>>;
1003
+ accent: z.ZodOptional<z.ZodString>;
1004
+ accentText: z.ZodOptional<z.ZodString>;
1005
+ radius: z.ZodOptional<z.ZodString>;
1006
+ fontFamily: z.ZodOptional<z.ZodString>;
1007
+ contentMaxWidth: z.ZodOptional<z.ZodString>;
1008
+ }, z.core.$loose>]>>>;
1009
+ launcher: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1010
+ variant: z.ZodDefault<z.ZodEnum<{
1011
+ bar: "bar";
1012
+ circle: "circle";
1013
+ minimal: "minimal";
1014
+ pill: "pill";
1015
+ }>>;
1016
+ size: z.ZodDefault<z.ZodEnum<{
1017
+ lg: "lg";
1018
+ md: "md";
1019
+ sm: "sm";
1020
+ }>>;
1021
+ label: z.ZodOptional<z.ZodString>;
1022
+ icon: z.ZodOptional<z.ZodString>;
1023
+ hidden: z.ZodDefault<z.ZodBoolean>;
1024
+ callout: z.ZodOptional<z.ZodObject<{
1025
+ text: z.ZodDefault<z.ZodString>;
1026
+ shape: z.ZodDefault<z.ZodEnum<{
1027
+ bubble: "bubble";
1028
+ callout: "callout";
1029
+ pill: "pill";
1030
+ }>>;
1031
+ position: z.ZodDefault<z.ZodEnum<{
1032
+ auto: "auto";
1033
+ horizontal: "horizontal";
1034
+ vertical: "vertical";
1035
+ }>>;
1036
+ delayMs: z.ZodDefault<z.ZodNumber>;
1037
+ autoDismissMs: z.ZodDefault<z.ZodNumber>;
1038
+ animated: z.ZodDefault<z.ZodBoolean>;
1039
+ persistent: z.ZodDefault<z.ZodBoolean>;
1040
+ }, z.core.$loose>>;
1041
+ }, z.core.$loose>>>;
1042
+ header: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1043
+ actions: z.ZodOptional<z.ZodArray<z.ZodEnum<{
1044
+ clear: "clear";
1045
+ close: "close";
1046
+ expand: "expand";
1047
+ fullscreen: "fullscreen";
1048
+ history: "history";
1049
+ language: "language";
1050
+ popOut: "popOut";
1051
+ sound: "sound";
1052
+ textSize: "textSize";
1053
+ theme: "theme";
1054
+ }>>>;
1055
+ }, z.core.$loose>>>;
1056
+ composer: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1057
+ sendButton: z.ZodOptional<z.ZodObject<{
1058
+ shape: z.ZodDefault<z.ZodEnum<{
1059
+ circle: "circle";
1060
+ pill: "pill";
1061
+ square: "square";
1062
+ }>>;
1063
+ icon: z.ZodDefault<z.ZodEnum<{
1064
+ arrowUp: "arrowUp";
1065
+ chevron: "chevron";
1066
+ paperPlane: "paperPlane";
1067
+ send: "send";
1068
+ }>>;
1069
+ variant: z.ZodDefault<z.ZodEnum<{
1070
+ filled: "filled";
1071
+ ghost: "ghost";
1072
+ outline: "outline";
1073
+ }>>;
1074
+ }, z.core.$loose>>;
1075
+ attachments: z.ZodOptional<z.ZodObject<{
1076
+ maxBytes: z.ZodDefault<z.ZodNumber>;
1077
+ maxCount: z.ZodDefault<z.ZodNumber>;
1078
+ accept: z.ZodDefault<z.ZodString>;
1079
+ }, z.core.$loose>>;
1080
+ }, z.core.$loose>>>;
1081
+ feedback: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1082
+ sound: z.ZodOptional<z.ZodObject<{
1083
+ enabled: z.ZodDefault<z.ZodBoolean>;
1084
+ volume: z.ZodDefault<z.ZodNumber>;
1085
+ events: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
1086
+ error: "error";
1087
+ messageReceived: "messageReceived";
1088
+ messageSent: "messageSent";
1089
+ voiceStart: "voiceStart";
1090
+ voiceStop: "voiceStop";
1091
+ }> & z.core.$partial, z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>>;
1092
+ }, z.core.$loose>>;
1093
+ haptics: z.ZodOptional<z.ZodObject<{
1094
+ enabled: z.ZodDefault<z.ZodBoolean>;
1095
+ events: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
1096
+ error: "error";
1097
+ messageReceived: "messageReceived";
1098
+ messageSent: "messageSent";
1099
+ voiceStart: "voiceStart";
1100
+ voiceStop: "voiceStop";
1101
+ }> & z.core.$partial, z.ZodUnion<readonly [z.ZodBoolean, z.ZodNumber, z.ZodArray<z.ZodNumber>]>>>;
1102
+ }, z.core.$loose>>;
1103
+ }, z.core.$loose>>>;
1104
+ i18n: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1105
+ locale: z.ZodOptional<z.ZodString>;
1106
+ defaultLocale: z.ZodDefault<z.ZodUnion<readonly [z.ZodLiteral<"auto">, z.ZodString]>>;
1107
+ availableLocales: z.ZodOptional<z.ZodArray<z.ZodString>>;
1108
+ strings: z.ZodOptional<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodString>, z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>>]>>;
1109
+ }, z.core.$loose>>>;
1110
+ footer: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1111
+ poweredBy: z.ZodOptional<z.ZodObject<{
1112
+ text: z.ZodOptional<z.ZodString>;
1113
+ href: z.ZodOptional<z.ZodURL>;
1114
+ logoUrl: z.ZodOptional<z.ZodURL>;
1115
+ }, z.core.$loose>>;
1116
+ composerDisclaimer: z.ZodOptional<z.ZodString>;
1117
+ }, z.core.$loose>>>;
1118
+ features: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1119
+ disableFileUpload: z.ZodDefault<z.ZodBoolean>;
1120
+ voice: z.ZodDefault<z.ZodEnum<{
1121
+ disabled: "disabled";
1122
+ local: "local";
1123
+ server: "server";
1124
+ }>>;
1125
+ disableHumanInLoop: z.ZodDefault<z.ZodBoolean>;
1126
+ enableOperatorRequest: z.ZodDefault<z.ZodBoolean>;
1127
+ }, z.core.$loose>>>;
1128
+ modules: z.ZodOptional<z.ZodOptional<z.ZodArray<z.ZodObject<{
1129
+ label: z.ZodString;
1130
+ layout: z.ZodEnum<{
1131
+ chat: "chat";
1132
+ help: "help";
1133
+ home: "home";
1134
+ news: "news";
1135
+ }>;
1136
+ contentCollectionIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
1137
+ id: z.ZodOptional<z.ZodString>;
1138
+ brandName: z.ZodOptional<z.ZodString>;
1139
+ hideGreeting: z.ZodOptional<z.ZodBoolean>;
1140
+ greetingText: z.ZodOptional<z.ZodString>;
1141
+ userAvatars: z.ZodOptional<z.ZodArray<z.ZodObject<{
1142
+ name: z.ZodString;
1143
+ avatar: z.ZodOptional<z.ZodString>;
1144
+ role: z.ZodOptional<z.ZodString>;
1145
+ }, z.core.$loose>>>;
1146
+ hideSearchBar: z.ZodOptional<z.ZodBoolean>;
1147
+ hideRecentConversations: z.ZodOptional<z.ZodBoolean>;
1148
+ status: z.ZodOptional<z.ZodObject<{
1149
+ text: z.ZodOptional<z.ZodString>;
1150
+ level: z.ZodOptional<z.ZodString>;
1151
+ url: z.ZodOptional<z.ZodString>;
1152
+ }, z.core.$strip>>;
1153
+ contentBlockTitle: z.ZodOptional<z.ZodString>;
1154
+ }, z.core.$loose>>>>;
1155
+ tracking: z.ZodOptional<z.ZodOptional<z.ZodObject<{
1156
+ enabled: z.ZodOptional<z.ZodBoolean>;
1157
+ endpoint: z.ZodOptional<z.ZodURL>;
1158
+ sampleRate: z.ZodOptional<z.ZodNumber>;
1159
+ token: z.ZodOptional<z.ZodString>;
1160
+ }, z.core.$loose>>>;
1161
+ dataApiBaseUrl: z.ZodOptional<z.ZodString>;
1162
+ storageVersion: z.ZodOptional<z.ZodString>;
1163
+ }, z.core.$loose>>;
1164
+ site: z.ZodOptional<z.ZodObject<{
1165
+ title: z.ZodOptional<z.ZodString>;
1166
+ logo: z.ZodOptional<z.ZodObject<{
1167
+ url: z.ZodString;
1168
+ alt: z.ZodOptional<z.ZodString>;
1169
+ }, z.core.$strict>>;
1170
+ logoDark: z.ZodOptional<z.ZodObject<{
1171
+ url: z.ZodString;
1172
+ alt: z.ZodOptional<z.ZodString>;
1173
+ }, z.core.$strict>>;
1174
+ }, z.core.$loose>>;
1175
+ blocks: z.ZodOptional<z.ZodObject<{
1176
+ navigation: z.ZodOptional<z.ZodArray<z.ZodObject<{
1177
+ id: z.ZodOptional<z.ZodString>;
1178
+ label: z.ZodString;
1179
+ href: z.ZodOptional<z.ZodString>;
1180
+ icon: z.ZodOptional<z.ZodString>;
1181
+ description: z.ZodOptional<z.ZodString>;
1182
+ }, z.core.$loose>>>;
1183
+ linkCards: z.ZodOptional<z.ZodArray<z.ZodObject<{
1184
+ id: z.ZodOptional<z.ZodString>;
1185
+ label: z.ZodString;
1186
+ href: z.ZodOptional<z.ZodString>;
1187
+ icon: z.ZodOptional<z.ZodString>;
1188
+ description: z.ZodOptional<z.ZodString>;
1189
+ }, z.core.$loose>>>;
1190
+ }, z.core.$loose>>;
1191
+ welcome: z.ZodOptional<z.ZodObject<{
1192
+ messages: z.ZodOptional<z.ZodArray<z.ZodObject<{
1193
+ id: z.ZodString;
1194
+ role: z.ZodEnum<{
1195
+ assistant: "assistant";
1196
+ system: "system";
1197
+ }>;
1198
+ text: z.ZodString;
1199
+ }, z.core.$loose>>>;
1200
+ suggestions: z.ZodOptional<z.ZodArray<z.ZodObject<{
1201
+ id: z.ZodString;
1202
+ label: z.ZodString;
1203
+ text: z.ZodOptional<z.ZodString>;
1204
+ }, z.core.$loose>>>;
1205
+ }, z.core.$loose>>;
1206
+ visitorId: z.ZodString;
1207
+ conversationId: z.ZodString;
1208
+ userPrefs: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
1209
+ rebind: z.ZodOptional<z.ZodObject<{
1210
+ conversationId: z.ZodString;
1211
+ }, z.core.$strict>>;
1212
+ }, z.core.$loose>;
1213
+ type HandshakeResponse = z.infer<typeof handshakeResponseSchema>;
1214
+ /** A 👍/👎 reaction on a rateable (assistant/operator) message; `null` clears a prior choice. */
1215
+ type FeedbackValue = 'up' | 'down' | null;
1216
+ /**
1217
+ * Payload shape per event. Keyed by event name; use `void` for events that
1218
+ * carry no data.
1219
+ *
1220
+ * **Every user-visible action fires here.** Subscribe via
1221
+ * `chat.on("eventName", fn)` to add analytics, side effects, or audit
1222
+ * trails without forking the widget. The same names also stream to the
1223
+ * structured logger at `info` level when `debug` is enabled.
1224
+ */
1225
+ interface EventMap {
1226
+ /** Panel opened (floating / modal / drawer). */
1227
+ open: void;
1228
+ /** Panel closed. */
1229
+ close: void;
1230
+ /** Expanded mode toggled (1.5× size). Payload is the new state. */
1231
+ expand: boolean;
1232
+ /** Fullscreen mode toggled. Payload is the new state. */
1233
+ fullscreen: boolean;
1234
+ /** End-user dragged the resize grip — payload is the resolved size. */
1235
+ resize: {
1236
+ width: number;
1237
+ height: number;
1238
+ };
1239
+ /** A message was exchanged. Fires for user, assistant, and operator messages. */
1240
+ message: {
1241
+ role: string;
1242
+ text: string;
1243
+ };
1244
+ /** User submitted a message (fires before the request is sent). */
1245
+ send: {
1246
+ text: string;
1247
+ attachmentCount: number;
1248
+ };
1249
+ /** User cancelled an in-flight stream. */
1250
+ stop: void;
1251
+ /** Clear / new-conversation requested. */
1252
+ clear: void;
1253
+ /** User picked a chat from the history pane / sidebar. */
1254
+ selectConversation: {
1255
+ conversationId: string;
1256
+ };
1257
+ /** User clicked a suggested-message chip. */
1258
+ suggestion: {
1259
+ text: string;
1260
+ };
1261
+ /** History pane toggled. Payload is the resulting view. */
1262
+ toggleHistory: {
1263
+ view: 'chat' | 'history';
1264
+ };
1265
+ /** A form (intake, CSAT, …) was completed or skipped. */
1266
+ formSubmit: {
1267
+ formId: string;
1268
+ values: Record<string, string>;
1269
+ skipped: boolean;
1270
+ };
1271
+ /** User answered an ask-questions tool request (re-POST follows). */
1272
+ toolResult: {
1273
+ toolCallId: string;
1274
+ };
1275
+ /** User approved / rejected a gated tool call (re-POST follows). */
1276
+ toolDecision: {
1277
+ toolCallId: string;
1278
+ approved: boolean;
1279
+ };
1280
+ /**
1281
+ * User gave feedback on a rateable reply (AI or operator). `value` is `"up"` / `"down"`, or
1282
+ * `null` when they cleared a prior choice. `messageId` is the backend message
1283
+ * id. Also POSTed best-effort to `/pai/message-feedback`; a host listener can
1284
+ * mirror it to analytics.
1285
+ */
1286
+ messageFeedback: {
1287
+ messageId: string;
1288
+ value: FeedbackValue;
1289
+ };
1290
+ /** A content list (news / help / home block) rendered. */
1291
+ contentView: {
1292
+ section: string;
1293
+ count: number;
1294
+ };
1295
+ /** An article / content item was opened — with the collection it belongs to. */
1296
+ contentOpen: {
1297
+ contentId: string;
1298
+ contentCollectionId: string;
1299
+ };
1300
+ /** The help search ran. Length + hit count only — never the query text. */
1301
+ contentSearch: {
1302
+ queryLength: number;
1303
+ hitCount: number;
1304
+ };
1305
+ /** An opened article was read (dwell threshold passed). Fires once per open. */
1306
+ contentRead: {
1307
+ contentId: string;
1308
+ };
1309
+ /** A link inside an opened article was followed. */
1310
+ contentLinkClick: {
1311
+ contentId: string;
1312
+ };
1313
+ /** Files attached (drag-drop, file picker, paste). */
1314
+ attach: {
1315
+ count: number;
1316
+ totalBytes: number;
1317
+ };
1318
+ /** An attachment was removed before send. */
1319
+ removeAttachment: {
1320
+ id: string;
1321
+ };
1322
+ /** Voice recording started. */
1323
+ voiceStart: void;
1324
+ /** Voice recording stopped and was sent for transcription. */
1325
+ voiceStop: {
1326
+ durationMs: number;
1327
+ };
1328
+ /** Voice recording cancelled (e.g. user released outside the button). */
1329
+ voiceCancel: void;
1330
+ /** End-user picked a different UI locale. Payload is the new BCP-47 tag. */
1331
+ localeChange: string;
1332
+ /** End-user picked a different theme mode (`auto` / `light` / `dark`). */
1333
+ themeChange: ThemePreference;
1334
+ /** End-user picked a different text size (`small` / `normal` / `large`). */
1335
+ textSizeChange: TextSize;
1336
+ /** End-user toggled the global sound mute. */
1337
+ soundToggle: {
1338
+ muted: boolean;
1339
+ };
1340
+ /** Page-mode sidebar collapsed / expanded. */
1341
+ sidebarToggle: {
1342
+ collapsed: boolean;
1343
+ };
1344
+ /** Pre-engagement launcher callout dismissed. */
1345
+ calloutDismiss: void;
1346
+ /** An unhandled error occurred somewhere in the transport or UI. */
1347
+ error: unknown;
1348
+ /** Connect bootstrap succeeded. Payload is the typed server response. */
1349
+ handshake: HandshakeResponse;
1350
+ /** Server replaced the requested conversationId and the widget adopted it. */
1351
+ conversationRebound: {
1352
+ previous: string;
1353
+ current: string;
1354
+ };
1355
+ /** The widget's state changed — open, ready, unread or the conversation. Payload is the whole new state. */
1356
+ state: WidgetState;
1357
+ /** How many conversations have replies the visitor hasn't read changed. Payload is the new count. */
1358
+ unread: number;
1359
+ }
1360
+ /** What a page can read about the widget at any moment (`chat.state`), and the `state` event's payload. */
1361
+ interface WidgetState {
1362
+ /** The panel is showing. Always `true` for `inline`, `page` and `standalone`. */
1363
+ open: boolean;
1364
+ /** The widget has connected (its first handshake settled) and takes commands right away. */
1365
+ ready: boolean;
1366
+ /**
1367
+ * Conversations with replies the visitor hasn't read. Known once the widget has been opened in this page load
1368
+ * (a closed widget makes no requests beyond the handshake), and kept live from then on.
1369
+ */
1370
+ unread: number;
1371
+ /** The visitor's current conversation, once there is one. */
1372
+ conversationId?: string;
1373
+ }
1374
+ type EventName = keyof EventMap;
1375
+ /**
1376
+ * Host-element command bus.
1377
+ *
1378
+ * The programmatic API (`chat.open()`, `.close()`, `.ask()`, …) and the
1379
+ * `<web-ai-chat>` element's methods dispatch `CustomEvent`s on the shadow
1380
+ * host element. The App listens for those same events and routes them to
1381
+ * its state handlers. That keeps the two modules decoupled — `index.ts`
1382
+ * doesn't hold a ref to App state, App doesn't know about the public
1383
+ * instance handle.
1384
+ *
1385
+ * This module is the single source of truth for the event names + the
1386
+ * tiny helpers on both sides of the bridge.
1387
+ */
1388
+ /** Where `open()` lands: a tab, and in Help or News optionally one article or post (its content id). */
1389
+ interface OpenTarget {
1390
+ to: 'home' | 'chats' | 'help' | 'news';
1391
+ id?: string;
1392
+ }
1393
+ /** How `ask(text, options)` lands: in the composer for the visitor to send (default), or sent right away. */
1394
+ interface AskOptions {
1395
+ /** Send the text as the visitor's message instead of filling the composer with it. Default `false`. */
1396
+ send?: boolean;
1397
+ }
1398
+ /**
1399
+ * Comprehensive logger for the widget runtime.
1400
+ *
1401
+ * Designed for "I have a customer reporting a weird bug on their
1402
+ * site, I want every relevant detail without rebuilding" debugging:
1403
+ *
1404
+ * • Five levels (off / error / warn / info / debug / trace) that
1405
+ * gate output. Default `warn` — errors + warnings always go
1406
+ * through, info / debug / trace stay silent until opted in.
1407
+ * • Scoped child loggers — `logger.scope("transport")` returns a
1408
+ * logger that tags every line `[helpai:transport] …`. Every
1409
+ * subsystem has its own scope so the console transcript reads
1410
+ * like a flight recorder.
1411
+ * • Structured payloads — every call accepts an object as the
1412
+ * second arg; the browser console renders it inline, expandable.
1413
+ * • Remote toggle — `chat.setDebug("trace")`, `HelpAI.setDebug(…)`,
1414
+ * `?helpai-debug=trace`, or the `options.debug` mount param all
1415
+ * converge on the same internal level. Persisted to
1416
+ * `localStorage["helpai.debug"]` so a refresh keeps visibility.
1417
+ * • Co-hosted safety — every signal (query, global key, log tag) is
1418
+ * brand-prefixed so two widgets on the same page don't fight over
1419
+ * flags or pollute each other's transcripts.
1420
+ */
1421
+ type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
1422
+ /** One structured line, as forwarded to an external {@link setLogSink} sink. */
1423
+ interface LogEntry {
1424
+ level: LogLevel;
1425
+ /** Subsystem tag, e.g. `"transport"` or `"core:hooks"`. */
1426
+ scope: string;
1427
+ message: string;
1428
+ payload?: unknown;
1429
+ }
1430
+ /**
1431
+ * Handle returned by {@link mount} / {@link init}. All methods are safe to call
1432
+ * after `destroy()` — they become no-ops rather than throwing.
1433
+ */
1434
+ interface ChatWidgetInstance {
1435
+ /** Stable identifier — matches `options.widgetId ?? "default"`. */
1436
+ readonly widgetId: string;
1437
+ /**
1438
+ * Open the floating panel (no-op in `inline` / `standalone` mode). With a target it also shows that tab at its
1439
+ * root, or one Help article / News post in it by content id — for a site's own "Help" or "What's new" button. A
1440
+ * tab the widget doesn't have just opens the panel.
1441
+ *
1442
+ * @example
1443
+ * ```ts
1444
+ * chat.open({ to: 'news' });
1445
+ * chat.open({ to: 'help', id: '019a0b6c-7e2a-7000-9d3e-4a5b6c7d8e01' });
1446
+ * ```
1447
+ */
1448
+ open: (target?: OpenTarget) => void;
1449
+ /** Close the floating panel (no-op in `inline` / `standalone` mode). */
1450
+ close: () => void;
1451
+ /** Unmount the widget and release all subscriptions. */
1452
+ destroy: () => void;
1453
+ /** Toggle fullscreen mode. In `standalone` mode this is a no-op. */
1454
+ expand: () => void;
1455
+ /**
1456
+ * Surface a configured form by id (a form with a `manual` trigger). Lets the
1457
+ * host open, say, an appointment or feedback form from its own button. No-op
1458
+ * if no form with that id + a `manual` trigger exists.
1459
+ */
1460
+ openForm: (id: string) => void;
1461
+ /**
1462
+ * Open the chat with `text` in the composer, ready for the visitor to edit and send — for a site's own "Ask about
1463
+ * pricing" button. With `{ send: true }` it goes out as the visitor's message instead. Either way it waits until
1464
+ * the chat can take it (still connecting, a pre-chat form to answer; a send also waits for a reply still arriving).
1465
+ *
1466
+ * @example
1467
+ * ```ts
1468
+ * chat.ask('How do I book a session?'); // open the chat with the question in the composer
1469
+ * chat.ask('How do I book a session?', { send: true }); // open the chat and send it
1470
+ * ```
1471
+ */
1472
+ ask: (text: string, options?: AskOptions) => void;
1473
+ /**
1474
+ * The widget's state right now: `open`, `ready`, `unread` (conversations with unread replies) and the current
1475
+ * `conversationId`. The `state` event delivers each change; `unread` fires when the count changes.
1476
+ *
1477
+ * @example
1478
+ * ```ts
1479
+ * helpLink.dataset.unread = String(chat.state.unread);
1480
+ * chat.on('unread', (count) => (helpLink.dataset.unread = String(count)));
1481
+ * ```
1482
+ */
1483
+ readonly state: WidgetState;
1484
+ /** Resolves once the widget has connected; commands sent after it act at once. Resolves once, never rejects. */
1485
+ readonly ready: Promise<void>;
1486
+ /**
1487
+ * Start over as a new anonymous visitor — call it when your user logs out. Forgets the stored visitor, their current
1488
+ * conversation, preferences and the user context set with `setUserContext`, then reconnects as a first visit.
1489
+ * Listeners added with `on()` stay.
1490
+ */
1491
+ reset: () => void;
1492
+ /**
1493
+ * Subscribe to a lifecycle event. Returns an `off` function. See
1494
+ * {@link EventMap} for the full set + payload shapes — currently
1495
+ * `open | close | message | error | expand | fullscreen |
1496
+ * handshake | localeChange`.
1497
+ */
1498
+ on: <E extends EventName>(name: E, fn: (payload: EventMap[E]) => void) => () => void;
1499
+ /**
1500
+ * Merge new options into the running instance without a full remount.
1501
+ * Useful for theme toggles or other live config tweaks.
1502
+ */
1503
+ update: (patch: Partial<ChatWidgetOptions>) => void;
1504
+ /**
1505
+ * Set / merge the host-asserted end-user context (`id`, `email`, `name`, plus
1506
+ * any custom scalar fields). Informational only — not auth. Merges into the
1507
+ * current context, refreshes the Home greeting, and re-handshakes so the
1508
+ * agent gets it. Call this after the visitor logs in. Untrusted input is
1509
+ * sanitised; bad fields are dropped with a `warn`.
1510
+ *
1511
+ * @example
1512
+ * ```ts
1513
+ * getInstance().setUserContext({ id: "u_1", name: "Mohamed", email: "mo@acme.com", plan: "pro" });
1514
+ * ```
1515
+ */
1516
+ setUserContext: (userContext: UserContext) => void;
1517
+ /** Clear the end-user context (e.g. on logout) and re-handshake anonymously. */
1518
+ clearUserContext: () => void;
1519
+ /**
1520
+ * Set / merge the host-asserted page/visit context (`area`, `url`, `path`,
1521
+ * `title`, plus custom scalar fields). Use it to tell the agent which surface
1522
+ * the visitor is on (e.g. `area: "pricing"`) and update it on SPA navigation.
1523
+ * Merges into the current context and re-handshakes.
1524
+ *
1525
+ * @example
1526
+ * ```ts
1527
+ * getInstance().setPageContext({ area: "pricing", path: "/pricing/enterprise" });
1528
+ * ```
1529
+ */
1530
+ setPageContext: (pageContext: PageContext) => void;
1531
+ /** Clear the page/visit context and re-handshake. */
1532
+ clearPageContext: () => void;
1533
+ /**
1534
+ * Set the runtime log level. Accepts `"off"` / `"error"` / `"warn"` /
1535
+ * `"info"` / `"debug"` / `"trace"`, or `true` (→ `"debug"`) /
1536
+ * `false` (→ `"off"`), or a number 0..5. Persists to localStorage so
1537
+ * a page refresh keeps the same verbosity — useful for asking a
1538
+ * customer to reproduce a bug and capture the console transcript.
1539
+ *
1540
+ * Affects every widget on the page (logger is module-global; setting
1541
+ * it from one instance applies everywhere). Equivalent to calling
1542
+ * `HelpAI.setDebug(level)` on the brand global.
1543
+ */
1544
+ setDebug: (level: boolean | number | string) => void;
1545
+ /** Current log level. Read-only. */
1546
+ readonly debugLevel: () => LogLevel;
1547
+ /**
1548
+ * Forward every log line (that passes the active level) to an external sink —
1549
+ * pipe errors/warnings/debug into Sentry, Datadog, your own logger, etc. Pass
1550
+ * `null` to clear. One global sink per bundle (logger is module-global); use
1551
+ * `setDebug("trace")` to capture everything, not just warnings + errors.
1552
+ */
1553
+ onLog: (sink: ((entry: LogEntry) => void) | null) => void;
1554
+ }
1555
+ /** The attributes a page sets on `<web-ai-chat>`, as HTML strings; `ai-agent-deployment-id` is required. */
1556
+ export type WebAiChatAttributes = Partial<Record<WebAiChatAttributeName, string>> & {
1557
+ 'ai-agent-deployment-id': string;
1558
+ };
1559
+ /** A page's `<web-ai-chat>`: the same commands, state and events as the `mount()` handle. */
1560
+ export type WebAiChatElement = HTMLElement & Pick<ChatWidgetInstance, 'open' | 'close' | 'expand' | 'openForm' | 'ask' | 'on' | 'state' | 'ready' | 'reset'>;
1561
+ declare global {
1562
+ interface HTMLElementTagNameMap {
1563
+ 'web-ai-chat': WebAiChatElement;
1564
+ }
1565
+ }
23
1566
  export declare function registerWebComponent(): void;
1567
+ export {};