@paramms/chat-widget 1.0.40 → 1.0.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,7 +8,9 @@ Real-time embeddable chat widget for the Relay platform. Drop into any website w
8
8
  npm install @paramms/chat-widget
9
9
  ```
10
10
 
11
- ## Quick start — CDN (no npm, no build)
11
+ ## Quick start — CDN, ESM `mount()` (no npm, no build, manual container)
12
+
13
+ This is the ESM path — you control the mount point yourself. For a floating bubble on any site with zero JavaScript (or a config object), see [Script tag](#script-tag-any-website--plain-html-wordpress-shopify-phprailsdjangolaravel-or-literally-anything) below instead — it's usually simpler unless you specifically need to place the widget inline in your own container (`data-relay-*`/`Relay('boot', ...)` don't support choosing a container element; this ESM form does, via `el`).
12
14
 
13
15
  ```html
14
16
  <div id="chat"></div>
@@ -111,9 +113,137 @@ Prefer a floating bubble that opens the same app in a panel? `<ChatAppLauncher
111
113
 
112
114
  > **Multi-tenancy note:** conversations are created and keyed under the tenant the server resolves *from* `profileId` — guests can't spoof it. For **reading your own inbox**, `ChatApp`/`ChatAppLauncher` (and `useRelayChatList`/`mountChatList`/`GET /conversations/mine`) also accept a **`tenantId`** directly: it lists every conversation the user has with that business across all of its chatrooms, and is equally safe because the list is always self-scoped to the caller's own identity. `scope="tenant"` / `tenantId` never crosses into another business's data.
113
115
 
116
+ ## Script tag (any website — plain HTML, WordPress, Shopify, PHP/Rails/Django/Laravel, or literally anything)
117
+
118
+ This is the path for every site that isn't React: one `<script>` tag, no build step, no framework. It works two ways — pick based on how much JavaScript you're willing to write:
119
+
120
+ - **Zero-JavaScript** — `data-relay-*` attributes right on the `<script>` tag. This is the one that matters most for a plain-HTML site, a WordPress block, or a no-code builder, because it's the *only* option when there's no JavaScript on the page at all — a server template just fills in attribute values.
121
+ - **Full config** — a JS object (`window.relaySettings = {...}` or `Relay('boot', {...})`, same shape either way). Needed the moment you want something an HTML attribute can't hold: a nested object, an array, a function, or changing the widget's identity *after* the page has loaded (e.g. once a visitor logs in).
122
+
123
+ Both configure the exact same widget with the exact same field names as the React components above — `profileId`, `userName`/`userEmail`/`userAvatar`, `contextTitle`/`contextSubtitle`/`contextStatus` (or `listingTitle`/`listingMeta`/`listingPrice`/`listingStatus` for a marketplace-style card), `accent`, `launcher`, `position`, `launcherMessage`. If you already know the React props, you already know these.
124
+
125
+ ### Zero-JavaScript — `data-relay-*` attributes
126
+
127
+ ```html
128
+ <script
129
+ async
130
+ src="https://relay.paramms.com/embed.js"
131
+ data-relay-app="YOUR_PROFILE_ID"
132
+ data-relay-user="user_123"
133
+ data-relay-user-name="Jane Doe"
134
+ data-relay-user-email="jane@example.com"
135
+ data-relay-accent="#4F63F5"
136
+ data-relay-position="bottom-right"
137
+ ></script>
138
+ ```
139
+
140
+ Drop that before `</body>` and you have a working floating bubble — no other JavaScript needed anywhere on the page. Every attribute is optional except `data-relay-app`.
141
+
142
+ | Attribute | Matches field | Notes |
143
+ |---|---|---|
144
+ | `data-relay-app` | `profileId` | **Required.** `data-relay-profile-id` works identically — same thing, matches the React prop name |
145
+ | `data-relay-user` | `userId` | Unauthenticated stable id. Omit for an anonymous guest |
146
+ | `data-relay-token` | `token` | Signed ES256 JWT — the production identity tier |
147
+ | `data-relay-user-name` | `userName` | Shown to agents |
148
+ | `data-relay-user-email` | `userEmail` | Shown to agents; also powers the offline email fallback |
149
+ | `data-relay-user-avatar` | `userAvatar` | Shown to agents |
150
+ | `data-relay-listing` | `listingId` | Sugar for `subjectId: "listing_<id>"` |
151
+ | `data-relay-context-title` / `-subtitle` / `-status` | `contextTitle` / `contextSubtitle` / `contextStatus` | Context card (general — an order, ticket, booking) |
152
+ | `data-relay-listing-title` / `-meta` / `-price` / `-status` | `listingTitle` / `listingMeta` / `listingPrice` / `listingStatus` | Marketplace card (a specific item — price + status badge). Use these OR the `context-*` set, both build the same card |
153
+ | `data-relay-accent` | `accent` | Brand colour hex |
154
+ | `data-relay-url` | `url` | Only needed for self-hosted Relay |
155
+ | `data-relay-position` | `position` | `bottom-right` \| `bottom-left` |
156
+ | `data-relay-launcher` | `launcher` | Set to `"false"` for an inline (non-floating) widget |
157
+ | `data-relay-launcher-message` / `data-relay-launcher-subtitle` | `launcherMessage` | Two flat attributes combine into the teaser card — see note below |
158
+ | `data-relay-target` | `el` | CSS selector for an existing element to mount into. Ignored in launcher mode |
159
+ | `data-relay-height` | `height` | Inline container height. Ignored in launcher mode |
160
+ | `data-relay-inbox` | `inbox` | `"true"` adds a back-chevron → full conversation list. Requires `data-relay-launcher="false"` — see the note below the table |
161
+ | `data-relay-inbox-scope` | `inboxScope` | `tenant` (default) \| `profile` |
162
+
163
+ **What's NOT available as an attribute:** `user`/`subject` as nested objects, `quickReplies`, `i18n`, and `refreshToken` — an HTML attribute can only hold a string, so these need the JS-object form below. (`launcherMessage`'s two attributes are a workaround for exactly this: the React prop takes one `{ title, subtitle }` object, which an attribute can't express, so it's split into two flat attributes that get recombined.)
164
+
165
+ **`inbox` requires `data-relay-launcher="false"`** — same limit React has (a launcher+inbox combination needs a different component there, `ChatAppLauncher`); with the default floating launcher, `data-relay-inbox` is silently ignored.
166
+
167
+ ### Full config — JS object
168
+
169
+ Same field names, all in one place, same object whether it's set on page load or reacts to something happening later:
170
+
171
+ ```html
172
+ <script async src="https://relay.paramms.com/embed.js"></script>
173
+ <script>
174
+ window.relaySettings = {
175
+ profileId: 'YOUR_PROFILE_ID', // required — your chatroom id
176
+ url: undefined, // optional — only for self-hosted Relay
177
+ apiUrl: undefined, // optional — only if REST lives on a different origin than the socket
178
+
179
+ token: undefined, // optional — signed ES256 JWT, production identity (wins over userId)
180
+ userId: 'user_123', // optional — unauthenticated stable id; omit both → anonymous guest
181
+ refreshToken: () => fetchNewToken(), // optional — called when a signed token expires
182
+
183
+ userName: 'Jane Doe', // optional — shown to agents, not identity
184
+ userEmail: 'jane@example.com', // optional — also powers the offline email fallback
185
+ userAvatar: 'https://…/jane.png', // optional
186
+ // or, if it's easier to build one object: user: { name, email, avatar }
187
+
188
+ subjectId: undefined, // optional — pins a dedicated thread; usually just use listingId below
189
+ listingId: '4821', // optional — sugar for subjectId: "listing_4821"
190
+ contextTitle: 'Order #4821', // optional — context card title (general use)
191
+ contextSubtitle: 'Placed Mar 3 · $129.00', // optional — context card subtitle
192
+ contextStatus: 'Shipped', // optional — status badge
193
+ // or, for a marketplace-style card: listingTitle / listingMeta / listingPrice / listingStatus
194
+ // or build the card yourself: subject: { title, subtitle, tags, status }
195
+
196
+ accent: '#4F63F5', // optional — brand colour
197
+ launcher: true, // optional — floating bubble vs inline; default true
198
+ position: 'bottom-right', // optional — 'bottom-right' | 'bottom-left'
199
+ launcherMessage: { title: 'Questions? Chat with us', subtitle: 'Start a conversation' }, // optional — a bare string also works (title only)
200
+
201
+ quickReplies: ['Track my order', 'Return an item'], // optional — reply chips above the input
202
+ i18n: { send: 'Enviar' }, // optional — UI string overrides
203
+ translateLang: undefined, // optional — auto-translate incoming messages (ISO code)
204
+
205
+ // Inline placement (ignored in launcher mode — a floating bubble is a
206
+ // fixed popup mount() owns, not something placed at a point in the page):
207
+ el: '#chat', // optional — CSS selector or element; omit → an auto-created host
208
+ height: '600px', // optional — inline container height
209
+ inbox: false, // optional — back-chevron → full conversation list (inline only, see below)
210
+ inboxScope: 'tenant', // optional — 'tenant' (default, all your chatrooms) | 'profile' (this one only)
211
+ }
212
+ </script>
213
+ ```
214
+
215
+ `Relay('boot', {...})` (below) takes this exact same object — use whichever form fits how the page is built.
216
+
217
+ **`inbox` matches React exactly, including its one limit:** it only works with `launcher: false`. A floating launcher bubble is a fixed-size popup the widget owns; React itself needs a *different* component (`ChatAppLauncher`) for a launcher-with-inbox experience, and the script-tag path draws the same line. With `inbox: true` and `launcher: false`, the widget's header shows a back-chevron; tapping it swaps to the full conversation list (reusing the same list engine the dashboard's own inbox runs on); tapping a row opens that conversation; a ✕ in the list view returns to this widget's original thread.
218
+
219
+ ### Commands — the same object, applied later
220
+
221
+ For identity that arrives after the page loads (a visitor logs in), or SPA-style navigation between pages without a full reload, call `Relay(...)` directly instead of (or in addition to) setting `window.relaySettings`. Same field names as above — `Relay('boot'/'identify'/'update', {...})` all take a `RelaySettings` object, just at a different moment:
222
+
223
+ ```html
224
+ <script async src="https://relay.paramms.com/embed.js"></script>
225
+ <script>
226
+ Relay('boot', { profileId: 'YOUR_PROFILE_ID' }) // mount immediately, anonymous
227
+
228
+ // later, once the visitor logs in:
229
+ Relay('identify', { userId: currentUser.id, userName: currentUser.name }) // merges their guest history in
230
+
231
+ // on navigation to a different listing/order page:
232
+ Relay('update', { listingId: newListing.id, listingTitle: newListing.title })
233
+
234
+ // on logout:
235
+ Relay('shutdown') // removes the widget + local session
236
+ </script>
237
+ ```
238
+
239
+ `Relay(...)` is queue-safe — calls made before `embed.js` finishes loading are never lost. `identify`/`update` merge onto whatever's currently running (a fresh `boot` replaces it entirely).
240
+
241
+ **Network requirement:** if the host page sets a Content-Security-Policy, it must allow the relay connection — `connect-src https://api.paramms.com wss://api.paramms.com;` (both are separate origins to the browser; include both), or your self-hosted equivalent.
242
+
114
243
  ### Old section below (kept for reference)
115
244
 
116
245
  ```tsx
246
+
117
247
  'use client'
118
248
  import { ChatWidget } from '@paramms/chat-widget/react'
119
249
 
@@ -214,16 +344,7 @@ arrives automatically over the socket. Pass `launcherMessage` only to override
214
344
  the dashboard value for one embed (it also renders instantly, before the
215
345
  socket connects).
216
346
 
217
- Script-tag embeds use data attributes:
218
-
219
- ```html
220
- <script
221
- src="https://relay.paramms.com/embed.js"
222
- data-relay-app="YOUR_PROFILE_ID"
223
- data-relay-launcher-message="Questions? Chat with us"
224
- data-relay-launcher-subtitle="Start a conversation"
225
- ></script>
226
- ```
347
+ Script-tag embeds use `data-relay-launcher-message` / `data-relay-launcher-subtitle` (or the `launcherMessage: {title,subtitle}` object in JS-object form) — see the [Script tag](#script-tag-any-website--plain-html-wordpress-shopify-phprailsdjangolaravel-or-literally-anything) section above for the full reference.
227
348
 
228
349
  ## Internationalisation
229
350
 
@@ -234,12 +355,18 @@ Script-tag embeds use data attributes:
234
355
  i18n={{
235
356
  placeholder: 'Écrivez un message…',
236
357
  send: 'Envoyer',
237
- offline: 'Nous sommes hors ligne pour l\'instant',
358
+ offline: 'Nous sommes absents pour le moment', // the away notice
238
359
  poweredBy: '', // empty string hides the footer
239
360
  }}
240
361
  />
241
362
  ```
242
363
 
364
+ `offline` is the fallback text for the **away notice** shown above the composer
365
+ outside the chatroom's office hours. It never blocks anything: guests can always
366
+ send, the message is delivered like any other, and an agent replies when they're
367
+ back. The chatroom's own "offline message" (dashboard → *Widget message &
368
+ availability*) takes precedence over this string.
369
+
243
370
  RTL is detected automatically for Arabic, Hebrew, Persian and Urdu browsers.
244
371
 
245
372
  ## All options
package/dist/embed.d.ts CHANGED
@@ -1,9 +1,19 @@
1
- /** Everything an embedder can pass. All optional except `appId`. Names are
2
- * intentionally plain no knowledge of the widget internals required. */
1
+ import { type MountOptions, type UserInfo } from './index.js';
2
+ /** Everything an embedder can pass. All optional except `profileId`. Field
3
+ * names DELIBERATELY mirror the React props (`ChatWidgetProps` /
4
+ * `MarketplaceChatProps` in react.tsx) so the same mental model — and often
5
+ * the same field names — carries over whether you're using React or a plain
6
+ * script tag. Where a name changed over time the old one still works (see
7
+ * the `@deprecated` notes) — this is a published package embedded on live
8
+ * customer sites (WordPress plugin, Shopify theme block), so nothing here is
9
+ * ever removed, only added to. */
3
10
  export interface RelaySettings {
4
- /** The chatroom id (from your Relay dashboard). Required. `profileId` is an alias. */
5
- appId?: string;
11
+ /** The chatroom id (from your Relay dashboard). Required. Matches the React
12
+ * `profileId` prop name. `appId` is the original alias — still works. */
6
13
  profileId?: string;
14
+ /** @deprecated alias for `profileId` — kept working, `profileId` is now the
15
+ * documented name (matches React). */
16
+ appId?: string;
7
17
  /** Relay server URL. Defaults to the hosted relay; set for self-hosted. */
8
18
  url?: string;
9
19
  apiUrl?: string;
@@ -12,27 +22,117 @@ export interface RelaySettings {
12
22
  * both for an anonymous visitor. See EMBED.md. */
13
23
  token?: string;
14
24
  userId?: string;
15
- /** Display info shown to agents (not identity). */
25
+ /** Called when a signed `token` is rejected (expired): return a fresh token
26
+ * from your backend to renew the session without a reload. Matches the
27
+ * React `refreshToken` prop. Only usable from `window.relaySettings` /
28
+ * `Relay('boot', ...)` (a function can't be expressed as an HTML
29
+ * attribute) — not available via `data-relay-*`. */
30
+ refreshToken?: () => Promise<string | null>;
31
+ /** Display info shown to agents (not identity) — matches the React
32
+ * `userName` / `userEmail` / `userAvatar` props. */
33
+ userName?: string;
34
+ userEmail?: string;
35
+ userAvatar?: string;
36
+ /** @deprecated flat aliases for `userName` / `userEmail` / `userAvatar` —
37
+ * kept working (the shipped Shopify integration used these names nested
38
+ * under `user`, see `user` below, which is the fix for that; these bare
39
+ * top-level fields predate that and still work standalone). */
16
40
  name?: string;
17
41
  email?: string;
18
42
  avatar?: string;
19
- /** Subject the chat is about (e.g. a marketplace listing). `listingId` is sugar
20
- * for `subjectId: "listing_<id>"`. */
43
+ /** Same info as `userName`/`userEmail`/`userAvatar`, as one nested object
44
+ * matches `MountOptions.user` / React's internal shape exactly, and is
45
+ * what a server-rendered snippet (e.g. Shopify Liquid, WordPress PHP) will
46
+ * most naturally emit: `user: { name: "...", email: "..." }`. Takes
47
+ * precedence over the flat fields if both are somehow given. */
48
+ user?: UserInfo;
49
+ /** Subject the chat is about (e.g. a marketplace listing). `listingId` is
50
+ * sugar for `subjectId: "listing_<id>"`. */
21
51
  subjectId?: string;
22
52
  listingId?: string;
53
+ /** Context-card fields — matches React's `ChatWidget` `contextTitle` /
54
+ * `contextSubtitle` / `contextStatus` props (a general "here's what this
55
+ * conversation is about" card: an order, ticket, booking, etc). */
56
+ contextTitle?: string;
57
+ contextSubtitle?: string;
58
+ contextStatus?: string;
59
+ /** Marketplace-card fields — matches React's `MarketplaceChat`
60
+ * `listingTitle` / `listingMeta` / `listingPrice` / `listingStatus` props
61
+ * (a specific-item card: price + status badge, e.g. "2019 Camry — $12,500
62
+ * — Available"). Use these OR `contextTitle`/etc — both build the same
63
+ * card, pick whichever vocabulary matches your use case. */
64
+ listingTitle?: string;
65
+ listingMeta?: string;
66
+ listingPrice?: number;
67
+ listingStatus?: string;
68
+ /** @deprecated original flat names for the context/marketplace card —
69
+ * kept working. `contextTitle`/`listingTitle` are now the documented
70
+ * names (matching the two React components). */
23
71
  subjectTitle?: string;
24
72
  subjectMeta?: string;
25
73
  subjectPrice?: number;
26
74
  subjectStatus?: string;
75
+ /** The context/marketplace card as one nested object, if you'd rather build
76
+ * it yourself than use the flat fields above — matches
77
+ * `MountOptions.subject` exactly. Takes precedence over every flat field
78
+ * above if given. */
79
+ subject?: {
80
+ title?: string;
81
+ subtitle?: string;
82
+ tags?: string[];
83
+ status?: string;
84
+ ownerLabel?: string;
85
+ };
86
+ /** Pre-set reply chips shown above the input — matches the React
87
+ * `quickReplies` prop. `window.relaySettings` / `Relay('boot', ...)` only
88
+ * (an array can't be expressed as a single `data-relay-*` attribute). */
89
+ quickReplies?: string[];
90
+ /** i18n string overrides — matches the React `i18n` prop. Same restriction
91
+ * as `quickReplies`: object, so JS-object form only. */
92
+ i18n?: MountOptions['i18n'];
27
93
  /** Appearance. `launcher` defaults to true (a floating bubble). */
28
94
  accent?: string;
29
95
  launcher?: boolean;
30
96
  position?: 'bottom-right' | 'bottom-left';
31
- /** Launcher teaser ("optional message" above the bubble). Title, or
32
- * title + subtitle. Omit to use the chatroom's manifest value. */
33
- launcherMessage?: string;
97
+ /** Launcher teaser ("optional message" above the bubble). Matches the React
98
+ * `launcherMessage` prop exactly: a bare string (title only), or
99
+ * `{ title, subtitle }`. `launcherSubtitle` below is a SEPARATE flat
100
+ * field kept only so `data-relay-launcher-message` /
101
+ * `data-relay-launcher-subtitle` (two HTML attributes — an attribute
102
+ * can't hold a nested object) can still combine into the same shape; in
103
+ * JS-object form just pass the object directly, same as React. Omit to
104
+ * use the chatroom's manifest value. */
105
+ launcherMessage?: string | {
106
+ title: string;
107
+ subtitle?: string;
108
+ };
109
+ /** @deprecated HTML-attribute-only companion to a string `launcherMessage`
110
+ * — see the note above. Prefer `launcherMessage: { title, subtitle }` in
111
+ * JS-object form. */
34
112
  launcherSubtitle?: string;
35
113
  translateLang?: string;
114
+ /** Mount INLINE into an existing element instead of the auto-created,
115
+ * body-appended host that the default floating launcher uses. A CSS
116
+ * selector string (works from `data-relay-target` too) or an element
117
+ * reference (JS-object form only). Ignored when `launcher` is true — same
118
+ * restriction as `height`/`inbox` below: a floating launcher panel is a
119
+ * fixed-size popup `mount()` owns, not something you place in the page. */
120
+ el?: string | HTMLElement;
121
+ /** Inline container height — matches the React `height` prop. Only applies
122
+ * when `launcher` is false/omitted. */
123
+ height?: string;
124
+ /** Adds a back-chevron to the widget that swaps it for the full
125
+ * conversation list — matches the React `ChatWidget`/`MarketplaceChat`
126
+ * `inbox` prop, INCLUDING its one limitation: not supported in launcher
127
+ * mode. (React itself requires a different component, `ChatAppLauncher`,
128
+ * for a launcher+inbox combination — same scope boundary here.) Tapping a
129
+ * row opens that conversation; its own back-chevron returns to the list; a
130
+ * ✕ in the list view returns to this widget's original single-thread
131
+ * view. Requires `launcher: false`. */
132
+ inbox?: boolean;
133
+ /** Inbox scope when `inbox` is set: `'tenant'` (default) lists the user's
134
+ * threads across ALL your chatrooms; `'profile'` limits it to this one. */
135
+ inboxScope?: 'tenant' | 'profile';
36
136
  }
37
137
  export type RelayCommand = 'boot' | 'update' | 'identify' | 'shutdown';
38
138
  /** The public command dispatcher exposed as `window.Relay`. */