@gamaro.io/widget 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +55 -0
- package/README.md +662 -0
- package/dist/config.d.ts +6 -0
- package/dist/iframe-loader.d.ts +78 -0
- package/dist/index.d.ts +64 -0
- package/dist/loader.d.ts +146 -0
- package/dist/logger.d.ts +26 -0
- package/dist/postmessage.d.ts +52 -0
- package/dist/types.d.ts +185 -0
- package/dist/widget.es.js +3529 -0
- package/dist/widget.umd.js +39 -0
- package/package.json +87 -0
package/README.md
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
# @gamaro.io/widget
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@gamaro.io/widget)
|
|
4
|
+
[](#bundle-size)
|
|
5
|
+
[](#typescript)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
Versiq Widget SDK — embed a conversational conversion agent that answers
|
|
9
|
+
visitors using **your** site's data (catalogue, inventory, CRM, knowledge base),
|
|
10
|
+
not the public-web average a generic LLM assistant returns.
|
|
11
|
+
|
|
12
|
+
## Table of Contents
|
|
13
|
+
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Quick Start](#quick-start)
|
|
16
|
+
- [Why this widget converts](#why-this-widget-converts)
|
|
17
|
+
- [Configuration](#configuration)
|
|
18
|
+
- [API Reference](#api-reference)
|
|
19
|
+
- [Events](#events)
|
|
20
|
+
- [TypeScript](#typescript)
|
|
21
|
+
- [Vertical Resolution](#vertical-resolution)
|
|
22
|
+
- [Display Modes](#display-modes)
|
|
23
|
+
- [React Integration](#react-integration)
|
|
24
|
+
- [Data Attributes (Script Tag)](#data-attributes-script-tag)
|
|
25
|
+
- [Security](#security)
|
|
26
|
+
- [E2E Test Selectors (data-testid)](#e2e-test-selectors-data-testid)
|
|
27
|
+
- [Performance](#performance)
|
|
28
|
+
- [Browser Support](#browser-support)
|
|
29
|
+
- [Changelog](#changelog)
|
|
30
|
+
- [Support](#support)
|
|
31
|
+
- [License](#license)
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @gamaro.io/widget
|
|
37
|
+
# or
|
|
38
|
+
pnpm add @gamaro.io/widget
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
### Script Tag (CDN)
|
|
44
|
+
|
|
45
|
+
Hot-link the latest published bundle from any public npm CDN. A single line is
|
|
46
|
+
enough — the publishable key binds the widget to your Agent, everything else is
|
|
47
|
+
resolved server-side.
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script
|
|
51
|
+
src="https://unpkg.com/@gamaro.io/widget@latest/dist/widget.umd.js"
|
|
52
|
+
data-api-key="pk_your_key"
|
|
53
|
+
></script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Equivalent via jsDelivr:
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<script
|
|
60
|
+
src="https://cdn.jsdelivr.net/npm/@gamaro.io/widget@latest/dist/widget.umd.js"
|
|
61
|
+
data-api-key="pk_your_key"
|
|
62
|
+
></script>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`@latest` always serves the most recent release, so a bug fix or improvement
|
|
66
|
+
reaches your visitors without you editing the snippet. If you need to pin a
|
|
67
|
+
specific version for change-control reasons, replace `@latest` with `@<VERSION>`
|
|
68
|
+
(see [npm](https://www.npmjs.com/package/@gamaro.io/widget) for the list) — at
|
|
69
|
+
the cost of having to bump it manually on every release.
|
|
70
|
+
|
|
71
|
+
### Programmatic API
|
|
72
|
+
|
|
73
|
+
The minimal integration is one line — every visual and behavioural aspect
|
|
74
|
+
(theme, position, language, open-state, branding) is configured in the **Versiq
|
|
75
|
+
admin portal** for your Agent and resolved server-side from the API key.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { createWidget } from "@gamaro.io/widget";
|
|
79
|
+
|
|
80
|
+
const widget = createWidget({ apiKey: "pk_live_your_key" });
|
|
81
|
+
|
|
82
|
+
// Control the widget at runtime
|
|
83
|
+
widget.open();
|
|
84
|
+
widget.close();
|
|
85
|
+
widget.reset();
|
|
86
|
+
|
|
87
|
+
// Cleanup (e.g., on SPA route change)
|
|
88
|
+
widget.destroy();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Why this widget converts
|
|
92
|
+
|
|
93
|
+
Two reasons most platforms fail to convert mobile visitors, and two reasons this
|
|
94
|
+
widget does:
|
|
95
|
+
|
|
96
|
+
1. **It speaks with your data, not the public-web average.** The agent answers
|
|
97
|
+
from your catalogue, your stock, your CRM, your knowledge base — what your
|
|
98
|
+
competitors and generic AI assistants don't have.
|
|
99
|
+
2. **It is driven by your thumb, not your keyboard.** The LLM dynamically picks
|
|
100
|
+
the right component at every turn — quick replies, sliders, product cards —
|
|
101
|
+
instead of forcing a form. Visitors qualify their need in a few taps on
|
|
102
|
+
mobile, where 3+ field forms lose 50%+ of users (HubSpot).
|
|
103
|
+
|
|
104
|
+
The widget rendering layer (`QuickReplies`, `PropertyCard`, `ActionButtons`) is
|
|
105
|
+
the runtime that materialises this. Schemas live in the Versiq backend
|
|
106
|
+
repository and are out of scope for the SDK consumer.
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
Widget configuration is split into three scopes — only the first one is your
|
|
111
|
+
responsibility as an integrator.
|
|
112
|
+
|
|
113
|
+
### 1. Host-side (passed to `createWidget` or as `data-*` attributes)
|
|
114
|
+
|
|
115
|
+
These can only live on the integrator's page because they describe the host
|
|
116
|
+
context (DOM, identity, environment).
|
|
117
|
+
|
|
118
|
+
| Option | Type | Default | Description |
|
|
119
|
+
| ----------- | ----------------------- | -------------- | ------------------------------------------------------------------------------------------------------- |
|
|
120
|
+
| `apiKey` | `string` | **required** | Publishable API key (`pk_live_*`, `pk_test_*`). Binds the widget to one Agent. |
|
|
121
|
+
| `container` | `HTMLElement \| string` | - | DOM container for inline mode. Required only when the admin has set `position: "inline"` on the portal. |
|
|
122
|
+
| `baseUrl` | `string` | Production URL | Override the widget iframe origin. Only useful for sandbox / self-hosted setups. |
|
|
123
|
+
| `debug` | `boolean` | `false` | Enable debug logging in the browser console. |
|
|
124
|
+
| `email` | `string` | - | Pre-identified visitor email — must be paired with `userHash`. |
|
|
125
|
+
| `userId` | `string` | - | Host-side stable user identifier — must be paired with `userHash`. |
|
|
126
|
+
| `userHash` | `string` | - | HMAC-SHA256 of `email` (or `userId`), signed with the Agent identity secret. See _Identity_. |
|
|
127
|
+
|
|
128
|
+
### 2. Server-resolved (configured in the Versiq admin portal)
|
|
129
|
+
|
|
130
|
+
These are **not** passed by the integration — they are stored in the Agent's
|
|
131
|
+
`widget_config` JSONB row and fetched at bootstrap from the API key. To change
|
|
132
|
+
any of them, edit the Agent in the portal.
|
|
133
|
+
|
|
134
|
+
| Field | Configured in admin | Notes |
|
|
135
|
+
| ----------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
136
|
+
| `theme` | Apparence → palette | Full palette (`primaryColor`, `backgroundColor`, `textColor`, `borderRadius`, `fontFamily`, `colorScheme`). |
|
|
137
|
+
| `position` | Apparence → position | `"bottom-right"`, `"bottom-left"`, or `"inline"`. |
|
|
138
|
+
| `language` | Apparence → langue | ISO 639-1 (e.g. `fr`, `en`). Falls back to browser language when unset. |
|
|
139
|
+
| `showProfile` | Apparence → panneau profil | Toggles the profile panel in the widget header. |
|
|
140
|
+
| `open` | Apparence → état initial | Default open state on page load. Host can still call `widget.open()` programmatically. |
|
|
141
|
+
| `brand.title` | Branding → titre | Custom header title (falls back to a vertical-specific default). |
|
|
142
|
+
| `brand.avatarUrl` | Branding → avatar | Custom avatar URL (must originate from the portal upload — arbitrary URLs are rejected). |
|
|
143
|
+
| `vertical` | Agent creation | `real-estate`, `b2b-qualification`, … Bound to the API key. |
|
|
144
|
+
|
|
145
|
+
### 3. Client overrides (advanced — rarely needed in production)
|
|
146
|
+
|
|
147
|
+
For tooling, A/B previews or staging environments, every server-resolved field
|
|
148
|
+
above can also be passed as a `WidgetConfig` argument or `data-*` attribute. A
|
|
149
|
+
host-side value, when present, **takes precedence over the admin value**. This
|
|
150
|
+
is intentional — but in production you should configure things in the portal so
|
|
151
|
+
all your sites stay in sync.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Override only for a staging preview — production should leave this out
|
|
155
|
+
createWidget({
|
|
156
|
+
apiKey: "pk_test_...",
|
|
157
|
+
theme: { primaryColor: "#3B82F6" },
|
|
158
|
+
position: "bottom-left",
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### `ThemeConfig` shape (admin-defined, occasionally overridden)
|
|
163
|
+
|
|
164
|
+
| Option | Type | Description |
|
|
165
|
+
| ----------------- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
166
|
+
| `primaryColor` | `string` | Primary brand color (hex, e.g., `#3B82F6`) |
|
|
167
|
+
| `backgroundColor` | `string` | Background color for the widget container |
|
|
168
|
+
| `textColor` | `string` | Text color |
|
|
169
|
+
| `borderRadius` | `number` | Border radius in pixels |
|
|
170
|
+
| `fontFamily` | `string` | Font family |
|
|
171
|
+
| `colorScheme` | `"light" \| "dark" \| "auto"` | Widget color scheme. `"auto"` follows the visitor's `prefers-color-scheme`. Defaults to light. |
|
|
172
|
+
|
|
173
|
+
### Motion
|
|
174
|
+
|
|
175
|
+
The widget animates state changes (open/close resize, message and quick-reply
|
|
176
|
+
entrances, hover feedback) with a single, coherent motion language: a harmonic
|
|
177
|
+
duration scale and Material-style easing curves, all transitions kept under
|
|
178
|
+
400ms. These **motion tokens are internal** — they are not part of the
|
|
179
|
+
`ThemeConfig` contract and cannot be overridden by integrators (kept internal
|
|
180
|
+
until a concrete need arises). The widget fully honours
|
|
181
|
+
`prefers-reduced-motion: reduce`: when the visitor opts out, every transition
|
|
182
|
+
and animation is neutralised. Design rationale and the full token table live in
|
|
183
|
+
[ADR-018](../../docs/adr/ADR-018-widget-motion-design.md).
|
|
184
|
+
|
|
185
|
+
## API Reference
|
|
186
|
+
|
|
187
|
+
### createWidget(config)
|
|
188
|
+
|
|
189
|
+
Creates a new widget instance. The minimal call is `createWidget({ apiKey })` —
|
|
190
|
+
every behaviour comes from the admin portal. The example below illustrates the
|
|
191
|
+
inline mode where the integrator must additionally supply a `container`
|
|
192
|
+
(host-context only).
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const widget = createWidget({
|
|
196
|
+
apiKey: "pk_live_your_key",
|
|
197
|
+
// Required only when the admin set position: "inline" on the portal
|
|
198
|
+
container: document.getElementById("widget-container"),
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### VersiqWidget Methods
|
|
203
|
+
|
|
204
|
+
| Method | Description |
|
|
205
|
+
| ----------------------------------------------------- | -------------------------------------------------------- |
|
|
206
|
+
| `open()` | Open the widget |
|
|
207
|
+
| `close()` | Close the widget |
|
|
208
|
+
| `reset()` | Reset the conversation |
|
|
209
|
+
| `setTheme(theme: ThemeConfig)` | Update theme dynamically |
|
|
210
|
+
| `setColorScheme(scheme: "light" \| "dark" \| "auto")` | Sugar over `setTheme({ colorScheme })` for dark-mode UIs |
|
|
211
|
+
| `identify(params)` | Set host-attested identity (HMAC) |
|
|
212
|
+
| `destroy()` | Remove widget and cleanup |
|
|
213
|
+
| `on(event, handler)` | Subscribe to events |
|
|
214
|
+
| `off(event, handler)` | Unsubscribe from events |
|
|
215
|
+
|
|
216
|
+
### Window API (Script Tag)
|
|
217
|
+
|
|
218
|
+
When using the script tag, the API is available on `window.Versiq`:
|
|
219
|
+
|
|
220
|
+
```javascript
|
|
221
|
+
window.Versiq.open();
|
|
222
|
+
window.Versiq.close();
|
|
223
|
+
window.Versiq.setTheme({ primaryColor: "#10B981" });
|
|
224
|
+
|
|
225
|
+
// Wire your own dark-mode toggle to the widget:
|
|
226
|
+
window.Versiq.setColorScheme("dark"); // or "light", or "auto"
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Events
|
|
230
|
+
|
|
231
|
+
Subscribe to widget events to react to user interactions.
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
widget.on("ready", () => {
|
|
235
|
+
console.log("Widget is ready");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
widget.on("profile-update", () => {
|
|
239
|
+
console.log("Profile was updated"); // signal only, no data
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
widget.on("qualified", () => {
|
|
243
|
+
console.log("Lead qualified"); // signal only, no data
|
|
244
|
+
// Full profile + score: subscribe to the lead.qualified backend webhook
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
widget.on("message", (data) => {
|
|
248
|
+
console.log("New message:", data.message);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
widget.on("error", (data) => {
|
|
252
|
+
console.error("Widget error:", data.code, data.message);
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Event Types
|
|
257
|
+
|
|
258
|
+
| Event | Payload | Description |
|
|
259
|
+
| --------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
260
|
+
| `ready` | - | Widget is loaded and ready |
|
|
261
|
+
| `open` | - | Widget was opened |
|
|
262
|
+
| `close` | - | Widget was closed |
|
|
263
|
+
| `message` | `{ message: WidgetMessage }` | New chat message |
|
|
264
|
+
| `profile-update` | - | Profile was updated (signal only — subscribe to webhooks for the data) |
|
|
265
|
+
| `qualified` | - | Lead qualified (signal only — subscribe to the `lead.qualified` webhook for the data) |
|
|
266
|
+
| `funnel-stage-change` | `{ stage: string, previousStage: string \| null }` | Conversion funnel stage transition |
|
|
267
|
+
| `cta-shown` | `{ ctaType: string, objectiveId?: string }` | A call-to-action was displayed |
|
|
268
|
+
| `cta-clicked` | `{ ctaType: string, objectiveId?: string }` | A call-to-action was clicked |
|
|
269
|
+
| `lead-captured` | `{ hasEmail: boolean, hasPhone: boolean }` | Lead contact details captured |
|
|
270
|
+
| `error` | `{ code: string, message: string }` | An error occurred |
|
|
271
|
+
| `quota-warning` | `{ remaining: number, limit: number }` | Lead quota running low |
|
|
272
|
+
| `quota-exceeded` | - | Lead quota exceeded |
|
|
273
|
+
| `identity-verified` | `{ email: string, userId?: string }` | Host identity verified (HMAC) |
|
|
274
|
+
| `widget-unavailable` | `{ reason: "timeout" \| "error", attempts: number }` | SDK exhausted all load retries — the widget degrades to a static state |
|
|
275
|
+
|
|
276
|
+
## TypeScript
|
|
277
|
+
|
|
278
|
+
The package includes full TypeScript support with exported types:
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import type {
|
|
282
|
+
WidgetConfig,
|
|
283
|
+
ThemeConfig,
|
|
284
|
+
VersiqWidget,
|
|
285
|
+
WidgetEventType,
|
|
286
|
+
WidgetProfile,
|
|
287
|
+
WidgetMessage,
|
|
288
|
+
B2BProfile, // B2B-specific (backward compat)
|
|
289
|
+
} from "@gamaro.io/widget";
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Profile Types
|
|
293
|
+
|
|
294
|
+
- **`WidgetProfile`** (`Record<string, unknown>`) — Generic profile used in
|
|
295
|
+
events. Shape depends on the vertical (`BuyerProfile` for real-estate,
|
|
296
|
+
`B2BProfile` for b2b-qualification).
|
|
297
|
+
- **`B2BProfile`** — Typed B2B qualification profile (sector, companySize,
|
|
298
|
+
etc.). Kept for backward compatibility.
|
|
299
|
+
|
|
300
|
+
## Vertical Resolution
|
|
301
|
+
|
|
302
|
+
The vertical (`real-estate`, `b2b-qualification`, …) is **resolved server-side
|
|
303
|
+
from the API key** — you do not pass it from the integration. Each `pk_*` key is
|
|
304
|
+
bound to exactly one Agent, and each Agent is bound to one vertical. Switching
|
|
305
|
+
vertical = creating a new Agent + new key.
|
|
306
|
+
|
|
307
|
+
### Real-Estate flow
|
|
308
|
+
|
|
309
|
+
When the resolved vertical is `real-estate`, the widget runs in qualification
|
|
310
|
+
mode: Versiq qualifies the buyer through conversation and emits the profile — no
|
|
311
|
+
property data is needed from the integrator (data-dependent tools like
|
|
312
|
+
`searchProperties`, `getCityStats`, `estimateProperty` are automatically
|
|
313
|
+
excluded).
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
// Assumes the real-estate Agent is configured for `inline` mode in
|
|
317
|
+
// the admin portal. Only the host-context fields are passed here.
|
|
318
|
+
const widget = createWidget({
|
|
319
|
+
apiKey: "pk_live_your_real_estate_key",
|
|
320
|
+
container: "#chat",
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// UX signals only — trigger a CTA or an analytics event, no lead data here
|
|
324
|
+
widget.on("profile-update", () => {
|
|
325
|
+
showProgressIndicator();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
widget.on("qualified", () => {
|
|
329
|
+
showQualifiedBadge();
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The profile/score data itself never flows through `postMessage` (#1993) — it is
|
|
334
|
+
only delivered server-side via a POST to your configured `webhookUrl` on
|
|
335
|
+
`lead.qualified`, configured in the admin portal:
|
|
336
|
+
|
|
337
|
+
```json
|
|
338
|
+
{
|
|
339
|
+
"event": "lead.qualified",
|
|
340
|
+
"data": {
|
|
341
|
+
"sessionId": "…",
|
|
342
|
+
"profile": {
|
|
343
|
+
"userType": "prospect",
|
|
344
|
+
"location": "…",
|
|
345
|
+
"budget": 450000,
|
|
346
|
+
"propertyType": "…"
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Display Modes
|
|
353
|
+
|
|
354
|
+
Display mode (`bottom-right`, `bottom-left`, `inline`) is set in the admin
|
|
355
|
+
portal. From the integrator's side, the only difference is whether you also need
|
|
356
|
+
to supply a `container`.
|
|
357
|
+
|
|
358
|
+
### Floating (admin chose `bottom-right` or `bottom-left`)
|
|
359
|
+
|
|
360
|
+
Widget appears as a floating button in the corner of the page. No `container`
|
|
361
|
+
needed.
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
createWidget({ apiKey: "pk_live_your_key" });
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Inline (admin chose `inline`)
|
|
368
|
+
|
|
369
|
+
Widget is mounted directly into a DOM container you provide.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
createWidget({
|
|
373
|
+
apiKey: "pk_live_your_key",
|
|
374
|
+
container: document.getElementById("chat-container"),
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
## React Integration
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
"use client";
|
|
382
|
+
|
|
383
|
+
import { useEffect, useRef } from "react";
|
|
384
|
+
import { createWidget, type VersiqWidget } from "@gamaro.io/widget";
|
|
385
|
+
|
|
386
|
+
export function ContactWidget() {
|
|
387
|
+
const widgetRef = useRef<VersiqWidget | null>(null);
|
|
388
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
389
|
+
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (!containerRef.current || widgetRef.current) return;
|
|
392
|
+
|
|
393
|
+
// Assumes the Agent is configured for `inline` mode in the admin
|
|
394
|
+
// portal — only the host-context fields (apiKey + container) are passed
|
|
395
|
+
// here. Theme, position, language, etc. come from the server config.
|
|
396
|
+
const widget = createWidget({
|
|
397
|
+
apiKey: "pk_live_your_key",
|
|
398
|
+
container: containerRef.current,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
widgetRef.current = widget;
|
|
402
|
+
|
|
403
|
+
return () => {
|
|
404
|
+
widgetRef.current?.destroy();
|
|
405
|
+
widgetRef.current = null;
|
|
406
|
+
};
|
|
407
|
+
}, []);
|
|
408
|
+
|
|
409
|
+
return <div ref={containerRef} className="h-[600px]" />;
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## Data Attributes (Script Tag)
|
|
414
|
+
|
|
415
|
+
All `WidgetConfig` options can be passed as kebab-cased `data-*` attributes on
|
|
416
|
+
the `<script>` tag. The same scope split as the JavaScript API applies — in
|
|
417
|
+
practice only `data-api-key` is needed.
|
|
418
|
+
|
|
419
|
+
### Host-side (essential)
|
|
420
|
+
|
|
421
|
+
| Attribute | Maps to | Example |
|
|
422
|
+
| ---------------- | ----------- | --------------------------------------- |
|
|
423
|
+
| `data-api-key` | `apiKey` | `data-api-key="pk_live_abc123"` |
|
|
424
|
+
| `data-container` | `container` | `data-container="#chat"` |
|
|
425
|
+
| `data-base-url` | `baseUrl` | `data-base-url="https://gamaro.io/app"` |
|
|
426
|
+
| `data-debug` | `debug` | `data-debug="true"` |
|
|
427
|
+
| `data-email` | `email` | `data-email="user@example.com"` |
|
|
428
|
+
| `data-user-id` | `userId` | `data-user-id="usr_123"` |
|
|
429
|
+
| `data-user-hash` | `userHash` | `data-user-hash="<hmac>"` |
|
|
430
|
+
|
|
431
|
+
`data-container` is a CSS selector and is only needed when the admin set
|
|
432
|
+
`position: "inline"` on the portal.
|
|
433
|
+
|
|
434
|
+
> **No theme / position / language overrides via `data-*`.** Unlike the
|
|
435
|
+
> programmatic API, server-resolved fields (`theme`, `position`, `language`,
|
|
436
|
+
> `showProfile`, `open`) **cannot** be overridden from the script tag — the
|
|
437
|
+
> legacy `data-theme` / `data-position` / `data-language` / `data-show-profile`
|
|
438
|
+
> / `data-open` attributes are silently ignored. Configure these in the admin
|
|
439
|
+
> portal, or use `createWidget({ theme, position, … })` for the tooling/preview
|
|
440
|
+
> override path.
|
|
441
|
+
|
|
442
|
+
## Security
|
|
443
|
+
|
|
444
|
+
The widget runs inside an `<iframe>` in your visitors' browsers. Three host-side
|
|
445
|
+
concerns are worth understanding before going to production: identity
|
|
446
|
+
attestation, the host `Permissions-Policy`, and your `Content-Security-Policy`.
|
|
447
|
+
|
|
448
|
+
### Identity verification (HMAC)
|
|
449
|
+
|
|
450
|
+
By default a visitor is anonymous. If your page already knows who the visitor is
|
|
451
|
+
(a logged-in user), you can attest that identity to Versiq so leads are attached
|
|
452
|
+
to the right account — without trusting a client-supplied email blindly.
|
|
453
|
+
|
|
454
|
+
Pass `email` (or `userId`) **together with** a `userHash`: an HMAC-SHA256 of the
|
|
455
|
+
`email` (or `userId`), computed **server-side** with your Agent's identity
|
|
456
|
+
secret (never expose that secret in the browser). The widget forwards the triple
|
|
457
|
+
and the backend recomputes the HMAC to verify it; on success it emits the
|
|
458
|
+
`identity-verified` event.
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// Server-side (your backend) — pseudo-code
|
|
462
|
+
const userHash = hmacSHA256(user.email, AGENT_IDENTITY_SECRET);
|
|
463
|
+
|
|
464
|
+
// Client-side — pass the precomputed hash, never the secret
|
|
465
|
+
createWidget({
|
|
466
|
+
apiKey: "pk_live_your_key",
|
|
467
|
+
email: user.email,
|
|
468
|
+
userHash, // injected from your server-rendered page
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
A mismatched or missing `userHash` leaves the visitor anonymous — the widget
|
|
473
|
+
keeps working, it just does not attest the identity.
|
|
474
|
+
|
|
475
|
+
### Host page Permissions-Policy
|
|
476
|
+
|
|
477
|
+
Some widget features rely on browser-level permissions that **must be granted by
|
|
478
|
+
the host page** — the iframe itself can't unlock a capability the parent
|
|
479
|
+
document forbids.
|
|
480
|
+
|
|
481
|
+
| Feature | Browser permission | Required `Permissions-Policy` on the host |
|
|
482
|
+
| ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
483
|
+
| `Autour de moi` quick-reply (real-estate) — proposes nearby cities/districts based on the visitor's current coords | `geolocation` (`navigator.geolocation.getCurrentPosition`) | `geolocation=(self)` at minimum (or include the widget origin explicitly) |
|
|
484
|
+
| Copy property listing / share link buttons | `clipboard-write` | `clipboard-write=(self)` (most hosts already inherit the default) |
|
|
485
|
+
|
|
486
|
+
If your site already ships a `Permissions-Policy` header (recommended for
|
|
487
|
+
defense in depth — see
|
|
488
|
+
[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy)),
|
|
489
|
+
add `geolocation=(self)` to the list. The widget iframe is created with
|
|
490
|
+
`allow="clipboard-write; geolocation"` so the parent's permission propagates
|
|
491
|
+
automatically once it's not denied.
|
|
492
|
+
|
|
493
|
+
Minimal header example:
|
|
494
|
+
|
|
495
|
+
```http
|
|
496
|
+
Permissions-Policy: camera=(), microphone=(), geolocation=(self)
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**What happens if you forget**: the widget keeps working — except the
|
|
500
|
+
geolocation-backed quick-replies log
|
|
501
|
+
`Geolocation has been disabled in this document by permissions policy.` to the
|
|
502
|
+
visitor's console and the chip silently does nothing on click. The underlying
|
|
503
|
+
conversation still works; the visitor just types the city manually.
|
|
504
|
+
|
|
505
|
+
### Content-Security-Policy
|
|
506
|
+
|
|
507
|
+
Permissions-Policy controls **what the iframe is allowed to do**;
|
|
508
|
+
Content-Security-Policy controls **where scripts / iframes / styles can come
|
|
509
|
+
from**. Both are independent and both are needed in production. The CSP
|
|
510
|
+
directives required for the Versiq widget are documented in the Versiq backend
|
|
511
|
+
repo at `docs/B2B_INTEGRATION.md` § "Content-Security-Policy pour intégrateurs".
|
|
512
|
+
|
|
513
|
+
## E2E Test Selectors (data-testid)
|
|
514
|
+
|
|
515
|
+
For a smoke test that the widget mounted and works on your page, a small core of
|
|
516
|
+
`data-testid` selectors is exposed. These are guaranteed not to change without a
|
|
517
|
+
major version bump.
|
|
518
|
+
|
|
519
|
+
| Selector | Element | Additional attributes |
|
|
520
|
+
| ----------------------- | ----------------------------------------------------------------------------- | ---------------------------------- |
|
|
521
|
+
| `widget-root` | Chat container (open) | — |
|
|
522
|
+
| `widget-input` | Message input field | — |
|
|
523
|
+
| `widget-send` | Send button | — |
|
|
524
|
+
| `widget-message` | Message bubble | `data-role="user" \| "assistant"` |
|
|
525
|
+
| `widget-loading-status` | Running-tool status label beside the typing dots (e.g. "Recherche de biens…") | — (absent until a tool runs) |
|
|
526
|
+
| `widget-suggestion` | Quick reply chip | `data-index="<N>"` (position) |
|
|
527
|
+
| `widget-cta-button` | Call-to-action button | `data-objective="<objectiveType>"` |
|
|
528
|
+
| `widget-avatar` | Header avatar | — |
|
|
529
|
+
|
|
530
|
+
> The widget also exposes a large set of vertical-specific selectors
|
|
531
|
+
> (real-estate map, areas-overview, exploration mode, criteria popup, range
|
|
532
|
+
> slider, contact form). They are internal to the Versiq UI and out of scope for
|
|
533
|
+
> SDK consumers — the full contract lives in the Versiq backend repository under
|
|
534
|
+
> `apps/app/src/app/widget/embed/components/`.
|
|
535
|
+
|
|
536
|
+
### Example (Playwright)
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
await page.getByTestId("widget-input").fill("Looking for a 2-bedroom in Lyon");
|
|
540
|
+
await page.getByTestId("widget-send").click();
|
|
541
|
+
await expect(
|
|
542
|
+
page.getByTestId("widget-message").filter({ hasText: "Lyon" }),
|
|
543
|
+
).toBeVisible();
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
## Performance
|
|
547
|
+
|
|
548
|
+
### Bundle Size
|
|
549
|
+
|
|
550
|
+
| Format | Size (minified) | Size (gzipped) | Limit |
|
|
551
|
+
| ------ | --------------- | -------------- | ----- |
|
|
552
|
+
| UMD | ~84 KB | ~24 KB | 50 KB |
|
|
553
|
+
| ESM | ~112 KB | ~26 KB | - |
|
|
554
|
+
|
|
555
|
+
CI enforces the 50 KB gzipped limit via `size-limit`.
|
|
556
|
+
|
|
557
|
+
### Largest Contentful Paint (LCP)
|
|
558
|
+
|
|
559
|
+
**Target**: < 1.5s
|
|
560
|
+
|
|
561
|
+
The widget loads as an iframe, so LCP depends on:
|
|
562
|
+
|
|
563
|
+
1. SDK download (~16 KB gzipped) - typically < 100ms
|
|
564
|
+
2. Iframe creation - instant
|
|
565
|
+
3. Embed page load - varies by network
|
|
566
|
+
|
|
567
|
+
#### Measured baselines
|
|
568
|
+
|
|
569
|
+
| Environment | Device | LCP | Notes |
|
|
570
|
+
| ----------------------- | ------------ | ------------ | ----------------------------------------------------------------------------------------------------- |
|
|
571
|
+
| Localhost (no throttle) | Desktop | 0.14s | Reference for development. Not representative of real visitor conditions — production target is 1.5s. |
|
|
572
|
+
| Marketing demo, Fast 4G | Desktop | _to measure_ | Lighthouse against `https://<marketing>/fr/demo/real-estate`, throttle "Fast 4G", incognito. |
|
|
573
|
+
| Marketing demo, Slow 3G | Mobile (mid) | _to measure_ | Same URL, Lighthouse mobile preset, throttle "Slow 3G". |
|
|
574
|
+
| Marketing demo, Fast 4G | Mobile (mid) | _to measure_ | Same URL, Lighthouse mobile preset, throttle "Fast 4G". |
|
|
575
|
+
|
|
576
|
+
> The three "to measure" rows are reserved for the next manual measurement pass
|
|
577
|
+
> against the marketing demo in prod. They are kept visible here so a visitor of
|
|
578
|
+
> this README sees what is covered and what is not, rather than a single
|
|
579
|
+
> localhost number that paints an over-optimistic picture.
|
|
580
|
+
|
|
581
|
+
#### How to measure LCP yourself (Lighthouse)
|
|
582
|
+
|
|
583
|
+
Reproducible manual procedure on any deployment that embeds the widget (staging,
|
|
584
|
+
prod demo, or your own integration). The marketing demo is the canonical
|
|
585
|
+
reference.
|
|
586
|
+
|
|
587
|
+
1. Open Chrome in **incognito** (no extensions, no cache, no service worker from
|
|
588
|
+
a previous visit interfering with the measurement).
|
|
589
|
+
2. Go to `https://<marketing-host>/fr/demo/real-estate` (or your own page
|
|
590
|
+
embedding `@gamaro.io/widget`).
|
|
591
|
+
3. Open **DevTools → Lighthouse**.
|
|
592
|
+
4. Select **Performance** only, choose the **Mode = Navigation**.
|
|
593
|
+
5. Pick the device preset (Mobile or Desktop) and the throttling profile
|
|
594
|
+
(`Slow 4G`, `Fast 4G`, or `Slow 3G` — match the row you want to fill in the
|
|
595
|
+
table above).
|
|
596
|
+
6. Click **Analyze page load**.
|
|
597
|
+
7. Read the **Largest Contentful Paint** metric from the report (top-left block,
|
|
598
|
+
color-coded green/orange/red against the 2.5s "good" threshold).
|
|
599
|
+
|
|
600
|
+
Run each scenario **3 times** and report the median to absorb cold-cache /
|
|
601
|
+
network jitter. A single run is not a measurement, it's an anecdote.
|
|
602
|
+
|
|
603
|
+
#### Continuous measurement (production sites)
|
|
604
|
+
|
|
605
|
+
For integrators who want to keep an eye on the metric over time without running
|
|
606
|
+
Lighthouse manually:
|
|
607
|
+
|
|
608
|
+
- Install the
|
|
609
|
+
[**Web Vitals Chrome extension**](https://chrome.google.com/webstore/detail/web-vitals/ahfhijdlegdabablpippeagghigmibma)
|
|
610
|
+
for a live LCP overlay while browsing.
|
|
611
|
+
- Wire [**web-vitals**](https://github.com/GoogleChrome/web-vitals) into your
|
|
612
|
+
analytics pipeline (Google Analytics 4, Sentry, etc.) to track field LCP per
|
|
613
|
+
real visitor — Lighthouse alone only measures synthetic loads.
|
|
614
|
+
|
|
615
|
+
The Versiq widget itself does not emit web-vitals events (out of scope — the SDK
|
|
616
|
+
should not impose an analytics dependency on the host). Integrators own the
|
|
617
|
+
measurement pipeline for their own page.
|
|
618
|
+
|
|
619
|
+
### Integration Time
|
|
620
|
+
|
|
621
|
+
**Target**: < 15 minutes from docs to working widget
|
|
622
|
+
|
|
623
|
+
The Quick Start section provides copy-paste integration in under 5 minutes.
|
|
624
|
+
|
|
625
|
+
## Browser Support
|
|
626
|
+
|
|
627
|
+
The SDK ships modern ESM and UMD bundles and targets evergreen browsers:
|
|
628
|
+
|
|
629
|
+
| Browser | Supported |
|
|
630
|
+
| ------------------------ | --------------------- |
|
|
631
|
+
| Chrome / Edge (Chromium) | last 2 major versions |
|
|
632
|
+
| Firefox | last 2 major versions |
|
|
633
|
+
| Safari (macOS / iOS) | last 2 major versions |
|
|
634
|
+
| Internet Explorer | not supported |
|
|
635
|
+
|
|
636
|
+
The widget relies on `<iframe>`, `postMessage`, `WeakSet`, and `fetch` — all
|
|
637
|
+
available in every browser above. For the Node.js toolchain (bundlers, SSR
|
|
638
|
+
hosts), `engines.node` is `>=18`.
|
|
639
|
+
|
|
640
|
+
## Changelog
|
|
641
|
+
|
|
642
|
+
Release notes are maintained automatically by release-please — see
|
|
643
|
+
[CHANGELOG.md](./CHANGELOG.md). Versions are bumped lockstep with
|
|
644
|
+
`@gamaro.io/core-types`.
|
|
645
|
+
|
|
646
|
+
## Support
|
|
647
|
+
|
|
648
|
+
- **Integration help & bug reports**:
|
|
649
|
+
[open an issue](https://github.com/sdolard/Toize/issues).
|
|
650
|
+
- **Commercial licensing & contracts**: admin@dolard.eu
|
|
651
|
+
|
|
652
|
+
## License
|
|
653
|
+
|
|
654
|
+
Commercial Source-available — see [LICENSE](./LICENSE).
|
|
655
|
+
|
|
656
|
+
This is **not** an open-source release: the source is published so integrators
|
|
657
|
+
can audit what runs in their visitors' browsers, but fork / redistribution /
|
|
658
|
+
competing-SDK use are prohibited without a written agreement. Production use
|
|
659
|
+
against the Versiq backend is governed by the Versiq Commercial Terms of
|
|
660
|
+
Service.
|
|
661
|
+
|
|
662
|
+
For commercial licensing inquiries: admin@dolard.eu
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ApplicationConfigResponse } from "./types";
|
|
2
|
+
export declare class ConfigFetchError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(message: string, code: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function fetchApplicationConfig(apiKey: string, baseUrl: string, externalSignal?: AbortSignal): Promise<ApplicationConfigResponse>;
|