@pmeweb/plugin-chat 1.2.1 → 1.3.1
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/INSTALL.md +91 -19
- package/README.md +55 -3
- package/dist/trigger.iife.js +1 -1
- package/dist/widget.js +2 -2
- package/manifest.json +3 -1
- package/package.json +1 -1
- package/src/build.d.ts +22 -7
- package/src/build.js +118 -35
- package/src/strings.js +4 -2
- package/src/trigger.js +19 -9
- package/src/widget.css +27 -9
- package/src/widget.js +196 -40
- package/src/worker.js +1 -1
package/INSTALL.md
CHANGED
|
@@ -90,34 +90,67 @@ package's, approved once.
|
|
|
90
90
|
`design.colors`. `opener` is `t.chat.openers[path]`, else
|
|
91
91
|
the default filled with the page's title; `logo` only for `avatar: "logo"` (an `?url` import of
|
|
92
92
|
the site's logo). The panel links to `privacyHref`.
|
|
93
|
-
8. **The context file
|
|
93
|
+
8. **The context file** (since 1.3.0: what the assistant knows). Three sources, nothing else:
|
|
94
|
+
|
|
95
|
+
- **`src/chat/context.<lang>.md`**, one per language of the site (manifest `context`), WRITTEN
|
|
96
|
+
BY CLAUDE CODE AT INSTALL for the assistant, from the site's pages and dictionary (« The
|
|
97
|
+
context file » below). Installing the chat is writing this file.
|
|
98
|
+
- **The pricing source** (`public/pricing.md`, or what the site's `pricing.md` route renders):
|
|
99
|
+
prices live there only, never in the `.md`.
|
|
100
|
+
- **The collections listed in `pmeweb.json` `plugins.chat.contextCollections`**
|
|
101
|
+
(`"portfolio"`, `"realisations,services"`: names separated by commas, no space): one line per
|
|
102
|
+
published entry, its title and a summary (its `summary`, else `resume`/`description`/`about`...,
|
|
103
|
+
else the start of its body, cut at 200 characters). Never `blog` or `authors` (the package and
|
|
104
|
+
the gate refuse them). The portal publishes these entries without Claude Code, so they stay
|
|
105
|
+
current by themselves. List a collection only when it answers a visitor's question (« avez-vous
|
|
106
|
+
fait un site pour un couvreur? »).
|
|
107
|
+
|
|
108
|
+
The route, `src/pages/plugins/chat/context.json.ts`:
|
|
94
109
|
|
|
95
110
|
```ts
|
|
96
|
-
import {
|
|
97
|
-
import {
|
|
98
|
-
import
|
|
99
|
-
import
|
|
100
|
-
import
|
|
111
|
+
import { getCollection } from 'astro:content';
|
|
112
|
+
import { chatContext, contextCollections } from '@pmeweb/plugin-chat/build';
|
|
113
|
+
import site from '../../../../pmeweb.json';
|
|
114
|
+
import pricing from '../../../../public/pricing.md?raw';
|
|
115
|
+
import contextFr from '../../../chat/context.fr.md?raw';
|
|
116
|
+
import contextEn from '../../../chat/context.en.md?raw';
|
|
101
117
|
export const prerender = true;
|
|
102
|
-
export const GET = async () =>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
export const GET = async () => {
|
|
119
|
+
const entries = [];
|
|
120
|
+
for (const name of contextCollections(site.plugins?.chat?.contextCollections)) {
|
|
121
|
+
for (const e of getPublished(await getCollection(name))) { // the site's own filter
|
|
122
|
+
entries.push({ collection: name, label: 'Réalisations', lang: e.data.lang, title: e.data.titre, data: e.data, body: e.body });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return new Response(chatContext({
|
|
126
|
+
langs: { fr: { business: 'Nom', context: contextFr, pricing }, en: { business: 'Name', context: contextEn, pricing } },
|
|
127
|
+
collections: entries,
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
109
130
|
```
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
132
|
+
`title` is the collection's `titleField`; `summary` may be composed by the site when no single
|
|
133
|
+
field says it (pmeweb: the portfolio's industry, then its « about »). A French-only site passes
|
|
134
|
+
`fr` alone. Never read `dist/` from a route. The build fails on a missing or empty `.md`, a `.md`
|
|
135
|
+
over 16 000 characters, a blog collection, or a file over 200 KB.
|
|
136
|
+
|
|
137
|
+
**The gate.** `a11y-tools/check-plugins.mjs` (pre-push and the builder's « Publier » preflight)
|
|
138
|
+
FAILS when a language's file is missing, over 16 000 characters, contains a price, names a blog
|
|
139
|
+
collection in `contextCollections`, or when its first line's fingerprint
|
|
140
|
+
(`<!-- chat-context sources:<16 hex> -->`) is not the hash of the site's current sources: its
|
|
141
|
+
dictionaries `src/i18n/*.ts`, its pricing source (`public/pricing.md`,
|
|
142
|
+
`src/{lib,data}/{prices,pricing}.*`) and its page files under `src/pages` (the blog routes,
|
|
143
|
+
`plugins/` and `og-card/` excluded). After writing or reviewing the files, from the site:
|
|
144
|
+
`node ../../a11y-tools/chat-context.mjs --stamp`. Renewing the stamp is a deliberate act after a
|
|
145
|
+
review, never a reflex to make the gate pass: read what changed, put in the file what is new
|
|
146
|
+
information for a visitor (a section, a service, a process, a policy), then stamp.
|
|
147
|
+
|
|
115
148
|
9. **Privacy**: nothing to paste. `<PluginPrivacy>` renders the package's section (and the voip.ms
|
|
116
149
|
and photos paragraphs when those keys are set), only while `enabled` is true: it needs
|
|
117
150
|
`@pmeweb/consent` 1.1.0 or later (manifest `activeWhen`; 1.0.0 would print it switched off, and
|
|
118
151
|
check-plugins refuses that).
|
|
119
|
-
10. **Not in `pmeweb.json` `forms`**: the chat's Turnstile widget renders only
|
|
120
|
-
|
|
152
|
+
10. **Not in `pmeweb.json` `forms`**: the chat's Turnstile widget renders only after the visitor
|
|
153
|
+
interacts with the page or the opener appears (20 s), so the portal's form-health browser check would find nothing on a page load.
|
|
121
154
|
11. `npm run build && node ../../a11y-tools/check-plugins.mjs .` until PASS; `npm run check`.
|
|
122
155
|
12. **Portal** (« Modules › Assistant virtuel »): PmeWeb sets `alertEmails`
|
|
123
156
|
(`alertEmails` is needed before the switch goes on: pmeweb-chat refuses a site without it), `alertPhone` if texts are on, and
|
|
@@ -127,3 +160,42 @@ package's, approved once.
|
|
|
127
160
|
13. Deploy (still off: nothing on the site, health `disabled`). Once the client or the operator
|
|
128
161
|
switches it on and publishes, from a real host: the bubble shows, a question gets an answer, the signed health
|
|
129
162
|
route answers `chat.ok`.
|
|
163
|
+
|
|
164
|
+
## The context file
|
|
165
|
+
|
|
166
|
+
Written for the assistant, not for a visitor; it is never published. Read every page of the site
|
|
167
|
+
(`src/pages`, the components they render, the dictionary of the language) and the legal pages,
|
|
168
|
+
then write, in the site's language and wording, answer-shaped plain facts under `## ` headings
|
|
169
|
+
(each heading becomes a section the model reads):
|
|
170
|
+
|
|
171
|
+
- the business: what it does, for whom, where, its languages; the legal name and city when a page
|
|
172
|
+
already says them;
|
|
173
|
+
- the offer: each service or product and what it includes, in the site's own words;
|
|
174
|
+
- the process and the timelines;
|
|
175
|
+
- the frequent questions the site answers (its FAQ blocks, its terms: payment, cancellation,
|
|
176
|
+
ownership), each as a fact;
|
|
177
|
+
- what the business does not do, only where the site says so (never deduced from silence);
|
|
178
|
+
- how to reach it: form address, email, phone, hours, response time.
|
|
179
|
+
|
|
180
|
+
Rules: nothing that is not already public on the site (workspace rule: never newly publish client
|
|
181
|
+
information); **no price** (the gate refuses `$` with a digit; write « voir Tarifs »); no blog
|
|
182
|
+
post; no marketing adjectives, no em-dash, OQLF typography in French (`docs/AVOIDING_AI_TELLS.md`);
|
|
183
|
+
16 000 characters at most per language (pmeweb.ca: about 10 000). Then stamp it.
|
|
184
|
+
|
|
185
|
+
**Every later site change that adds or changes information** (a section, a service, a process, a
|
|
186
|
+
policy) updates the file in the same change; the gate says when the sources moved. Blog posts
|
|
187
|
+
never go in.
|
|
188
|
+
|
|
189
|
+
## Updating from 1.2.1
|
|
190
|
+
|
|
191
|
+
1.3.0 changes what the context is built from (1.2.1 sent llms.txt, the whole dictionary and every
|
|
192
|
+
entry's full text: about 26 000 tokens per reply on pmeweb.ca, mostly interface labels).
|
|
193
|
+
`context.json` keeps format 1; pmeweb-chat needs no change.
|
|
194
|
+
|
|
195
|
+
1. Write `src/chat/context.<lang>.md` for each language (« The context file »), then
|
|
196
|
+
`node ../../a11y-tools/chat-context.mjs --stamp`.
|
|
197
|
+
2. Rewrite `src/pages/plugins/chat/context.json.ts` as in step 8: `chatContext` no longer accepts
|
|
198
|
+
`llms` or `dictionary` (the build throws naming this section). Drop the blog posts.
|
|
199
|
+
3. If a collection helps answer visitors, list it: `"contextCollections": "portfolio"` in
|
|
200
|
+
`pmeweb.json` `plugins.chat`.
|
|
201
|
+
4. `npm run build && node ../../a11y-tools/check-plugins.mjs .` until PASS.
|
package/README.md
CHANGED
|
@@ -17,6 +17,22 @@ Three pieces:
|
|
|
17
17
|
|
|
18
18
|
Install it with the `install-plugin` skill (`INSTALL.md`).
|
|
19
19
|
|
|
20
|
+
## What the assistant knows (1.3.0)
|
|
21
|
+
|
|
22
|
+
`plugins/chat/context.json` (format 1, unchanged) is built from three sources only:
|
|
23
|
+
`src/chat/context.<lang>.md`, a file Claude Code writes for the assistant from the site (the
|
|
24
|
+
business, the offer and what it includes, the process, the frequent questions, what the business
|
|
25
|
+
does not do, how to reach it; no price, nothing not already public), the site's pricing source,
|
|
26
|
+
and the collections listed in `pmeweb.json` `plugins.chat.contextCollections` as one line per
|
|
27
|
+
entry (title and a 200-character summary). Never the blog, the dictionary or llms.txt. On
|
|
28
|
+
pmeweb.ca that is about 4 500 input tokens per reply instead of 22 300 (measured 2026-11-13, 38 replies each).
|
|
29
|
+
|
|
30
|
+
The `.md` carries a fingerprint of the site's sources on its first line;
|
|
31
|
+
`a11y-tools/check-plugins.mjs` fails a push (and the builder's preflight) when the site changed
|
|
32
|
+
since the last review, until someone rereads the change, updates the file and runs
|
|
33
|
+
`node ../../a11y-tools/chat-context.mjs --stamp`. Details: `INSTALL.md`, step 8 and « The context
|
|
34
|
+
file ».
|
|
35
|
+
|
|
20
36
|
## Behaviour worth knowing
|
|
21
37
|
|
|
22
38
|
- **A demo in the preview** (manifest `preview: "test"`). Off the site's real hosts
|
|
@@ -36,8 +52,39 @@ Install it with the `install-plugin` skill (`INSTALL.md`).
|
|
|
36
52
|
face (`avatar`: photos, illustration or a « Sans visage » icon), the icon's colour (`avatarBg`,
|
|
37
53
|
default the site's accent) and the name (`botName`, which ships with « Publier »). `logo` stays
|
|
38
54
|
PmeWeb's, set at install.
|
|
39
|
-
- **Nothing loads with the page.** The bubble is HTML and CSS
|
|
40
|
-
|
|
55
|
+
- **Nothing loads with the page.** The bubble is HTML and CSS (the inline trigger, under the
|
|
56
|
+
manifest's `inlineBudget`). The panel's code (`widget.js`) arrives on the visitor's first sign of
|
|
57
|
+
life anywhere on the page (a scroll, a pointer moving, a press, a key or a focus) or when the
|
|
58
|
+
opener appears (20 s), whichever comes first. The socket opens only on the first message.
|
|
59
|
+
- **Turnstile starts before the panel opens, never in a test** (operator, 2026-11-13). Loaded when
|
|
60
|
+
the panel opened, the challenge had no time to observe the visitor and often showed its
|
|
61
|
+
« Vérifiez que vous êtes humain » box. It now starts once `widget.js` is in AND the page has
|
|
62
|
+
painted (`load`, first contentful paint, LCP, whichever is latest) plus `WARM_MS` (3 s), or at
|
|
63
|
+
once on a press or a focus in the chat, or when the panel opens, whichever comes first. A loaded
|
|
64
|
+
`api.js` racing the first paint costs up to 30 PageSpeed points (`@pmeweb/turnstile`'s README);
|
|
65
|
+
Lighthouse and PageSpeed never scroll, move a pointer or type, so they fetch neither `widget.js`
|
|
66
|
+
nor `api.js` before the opener's 20 s. Measured on a demo page with the chat on (1.3.1,
|
|
67
|
+
Lighthouse 12.6.1 mobile, 3 runs): no request to `challenges.cloudflare.com` or `widget.js`,
|
|
68
|
+
performance 100. In Chromium at 390 px: a visitor scrolling at 1 s gets `widget.js` at 1.0 s,
|
|
69
|
+
`api.js` at 4.0 s; one who never touches the page, `widget.js` with the opener at 20.0 s,
|
|
70
|
+
`api.js` at 23.0 s; in both the token is minted before the panel opens.
|
|
71
|
+
- **The Cloudflare box shows only when Cloudflare asks.** The widgets (`interaction-only`) render
|
|
72
|
+
hidden in the closed panel and mint their token there. Each sits in its own row, collapsed until
|
|
73
|
+
Turnstile's `before-interactive` or `error` callback opens it; its success callback collapses it
|
|
74
|
+
again (« Succès ! » goes away, 0.25 s, at once under reduced motion), and a later expiry that
|
|
75
|
+
needs a click opens it again. In the composer the row sits above the field, so the field never
|
|
76
|
+
moves. An unused token is refreshed by Turnstile before its 300 s expiry; after each post the
|
|
77
|
+
widget is reset (`turnstile.reset(widget)`, never while its row is asking). A widget added after
|
|
78
|
+
another form already loaded `api.js` is rendered by the chat itself (implicit rendering scans the
|
|
79
|
+
page once).
|
|
80
|
+
- **The first message reads like a reply.** The typing dots show at once while the conversation
|
|
81
|
+
starts (token, `/api/chat/start`, socket); « Un instant… » joins them only past 8 s. Nothing is
|
|
82
|
+
started before the visitor writes: pmeweb-chat counts every start toward the visitor's
|
|
83
|
+
conversations of the day, so only the token is prepared ahead.
|
|
84
|
+
- **The thread stays at the bottom** while the visitor is there (within 40 px): streamed text, the
|
|
85
|
+
final message, the dots, a photo once decoded, the takeover and hand-back lines, the end block, the
|
|
86
|
+
composer growing. One smooth scroll per frame (instant under reduced motion). A visitor who
|
|
87
|
+
scrolled up to read stays put; sending a message always scrolls down.
|
|
41
88
|
- **Openers.** One line per page, from the site's dictionary (`chat.openers`, `chat.defaultOpener`). At most 50 characters (two lines on a phone): a longer one fails the build, naming it.
|
|
42
89
|
Shown once per visit (20 s, 30 % of the page, or exit intent with a mouse), never while the panel
|
|
43
90
|
is open, never while the footer is in view (it waits for the next of those triggers). On a phone
|
|
@@ -54,7 +101,9 @@ Install it with the `install-plugin` skill (`INSTALL.md`).
|
|
|
54
101
|
(the first one wins over the footer). Never while the panel is open or while focus is in the
|
|
55
102
|
bubble or the opener (a focusout re-checks). Reduced motion: hidden and shown at once.
|
|
56
103
|
- **Never passing for a person.** « Assistant virtuel » is always in text; the green dot appears
|
|
57
|
-
only when the owner has taken over, with the owner's name.
|
|
104
|
+
only when the owner has taken over, with the owner's name. Where the owner takes over, the thread
|
|
105
|
+
shows « {nom} a pris le relais »; where they hand back (« Rendre à l’assistant »), « L’assistant
|
|
106
|
+
virtuel a repris la conversation » (the dot muted), live and on replay, once per hand-back.
|
|
58
107
|
- **The socket.** A drop (1006, a forced deploy of pmeweb-chat) reconnects with backoff 1 to 30 s
|
|
59
108
|
and catches up from the last message seen; a message typed meanwhile is queued and sent on open,
|
|
60
109
|
and after 3 sends without an echo shows « Non envoyé, Réessayer ». A 44xx close never reconnects.
|
|
@@ -153,6 +202,9 @@ site changes with `a11y-tools/contrast.mjs`.
|
|
|
153
202
|
says `settings: false`.
|
|
154
203
|
- The context file absent from `dist/` or not served signed: the bot falls back to the contact form
|
|
155
204
|
on every message.
|
|
205
|
+
- A fact the site states but `src/chat/context.<lang>.md` does not: the bot says it does not know.
|
|
206
|
+
The fingerprint catches changes to the dictionaries, the pricing source and `src/pages`, not to a
|
|
207
|
+
component that hardcodes text (a site's copy belongs in its dictionary).
|
|
156
208
|
|
|
157
209
|
## Avatars
|
|
158
210
|
|
package/dist/trigger.iife.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{function
|
|
1
|
+
(()=>{function x({doc:o=document,hostname:n=globalThis.location?.hostname??""}={}){let a=o.getElementById("pmw-hosts");if(!a)return!1;let s;try{s=JSON.parse(a.textContent??"")}catch{return!1}if(!Array.isArray(s))return!1;let c=String(n).toLowerCase();return s.some(t=>typeof t=="string"&&t.toLowerCase()===c)}var v=["pointerdown","pointerenter","focusin","keydown"];function A(o,n,{events:a=v}={}){let s=!1,c=()=>{if(!s){s=!0;for(let t of a)o.removeEventListener(t,c);n()}};for(let t of a)o.addEventListener(t,c,{passive:!0});return{run:c}}var I="pmw-chat-opener",C=2e4,N=.3,P=[...v,"pointermove","scroll"];function y({doc:o=document,win:n=window,hostname:a=n.location.hostname,importLazy:s=t=>import(t),onInteraction:c=A}={}){let t=o.getElementById("pmw-chat");if(!t)return null;t.hidden=!1;let i=t.querySelector(".pmw-chat__teaser"),f=null,p=()=>f??(f=s(t.dataset.pmwLazy).then(e=>e.mountChat(t),e=>{throw f=null,e})),g=()=>p().catch(()=>{}),L=()=>p().then(e=>e.open(),()=>{}),d=()=>t.hasAttribute("data-open");c(o,g,{events:P});let m=x({doc:o,hostname:a});if(!m){let e=()=>{let r=new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat");r==="panel"&&!d()?L():r==="bubble"&&f&&f.then(E=>E.close?.(),()=>{})};e(),n.addEventListener("hashchange",e)}let h=!1,b=(e=o.activeElement)=>{let r=h&&!d()&&!t.contains(e);t.toggleAttribute("inert",r)},T=o.querySelector("[data-pmw-chat-avoid]")??[...o.querySelectorAll("footer")].pop();m&&T&&(new n.IntersectionObserver(e=>{h=e.at(-1).isIntersecting,b()}).observe(T),t.addEventListener("focusout",e=>b(e.relatedTarget)));let S=n.sessionStorage,l=!1;try{l=!!S?.getItem(I)}catch{}let O=(e,r,E)=>e.addEventListener(r,_=>l||E(_),{passive:!0}),w=()=>{l=!0;try{S?.setItem(I,"1")}catch{}},u=()=>{l||h||!i||d()||(w(),i.hidden=!1,g())};return t.addEventListener("click",e=>{let r=e.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");r&&(i&&(i.hidden=!0),w(),r.hasAttribute("data-pmw-chat-open")&&L())}),!l&&i&&m&&(n.setTimeout(u,C),O(n,"scroll",()=>{let e=o.documentElement.scrollHeight-n.innerHeight;e>0&&n.scrollY/e>=N&&u()}),n.matchMedia?.("(pointer: fine)").matches&&O(o,"mouseout",e=>{!e.relatedTarget&&e.clientY<=0&&u()})),{load:p,show:u}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>y()):y();})();
|
package/dist/widget.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`;var je=new Set;function Ge(s,{doc:l=document,attrs:r={}}={}){if(je.has(s)||(je.add(s),l.querySelector(`script[src="${s}"]`)))return;let t=l.createElement("script");t.src=s,t.async=!0;for(let[d,m]of Object.entries(r))t.setAttribute(d,m);l.head.append(t)}function Ve({doc:s=document,hostname:l=globalThis.location?.hostname??""}={}){let r=s.getElementById("pmw-hosts");if(!r)return!1;let t;try{t=JSON.parse(r.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let d=String(l).toLowerCase();return t.some(m=>typeof m=="string"&&m.toLowerCase()===d)}var mt="https://challenges.cloudflare.com/turnstile/v0/api.js";var ut=15e3,Ke=200;function fe(s=document){Ge(mt,{doc:s})}var ft=s=>s.querySelector('input[name="cf-turnstile-response"]')?.value??"",wt=s=>{let l=s.querySelector(".cf-turnstile");return!!l&&l.offsetHeight>0};async function Xe(s,{win:l=window,timeoutMs:r=ut}={}){typeof s.pmwTurnstileLoad=="function"?s.pmwTurnstileLoad():fe(l.document);let t=0;for(;ft(s)==="";){if(!wt(s)){if(t>=r)return!1;t+=Ke}await new Promise(d=>l.setTimeout(d,Ke))}return!0}function Ye(s,{win:l=window}={}){let r=s.querySelector(".cf-turnstile");if(!r||r.offsetHeight>0)return!1;try{return l.turnstile?.reset(r),!0}catch{return!1}}var Qe={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var _t={fr:{assistant:"Assistant virtuel",openLabel:"Assistant virtuel, ouvrir le clavardage",dismissTeaser:"Masquer ce message",close:"Fermer le clavardage",panelTitle:"Clavardage",automated:"R\xE9ponses automatiques, tir\xE9es du contenu de ce site.",privacy:"Confidentialit\xE9",you:"Vous",messageLabel:"Votre message",send:"Envoyer",live:"Vous r\xE9pond en direct",relay:"{name} a pris le relais",relayGeneric:"Une personne de l\u2019\xE9quipe a pris le relais",connecting:"Connexion\u2026",reconnecting:"Connexion perdue, reconnexion\u2026",notSent:"Non envoy\xE9.",retry:"R\xE9essayer",ended:"Cette conversation est termin\xE9e.",newConversation:"Nouvelle conversation",reload:"Rechargez la page pour continuer.",unavailable:"Un probl\xE8me est survenu. R\xE9essayez dans un moment.",turnstile:"L\u2019envoi n\u2019a pas abouti. R\xE9essayez.",tooMany:"Un instant, puis r\xE9essayez.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"Laissez vos coordonn\xE9es et nous vous recontacterons.",fallbackDown:"Laissez vos coordonn\xE9es et nous vous recontacterons.",name:"Votre nom",contact:"T\xE9l\xE9phone ou courriel",need:"Votre demande",fallbackSend:"Envoyer la demande",fallbackSent:"Merci, votre demande a \xE9t\xE9 transmise.",fallbackMissing:"Remplissez les trois champs.",said:"{name}\xA0: {text}",attachPhoto:"Joindre une photo",photoHint:"\xC9vitez les photos de personnes ou de pi\xE8ces d\u2019identit\xE9.",photoAlt:"Votre photo",photoSent:"Photo envoy\xE9e",photoLimit:"Trois photos au plus par conversation.",photoTooLarge:"Cette photo est trop lourde.",photoUnreadable:"Ce fichier n\u2019est pas une photo lisible.",photoFailed:"La photo n\u2019a pas pu \xEAtre envoy\xE9e. R\xE9essayez.",demoAsk:"Pouvez-vous m\u2019en dire plus sur {topic}?",demoQuestion:"Quels sont vos d\xE9lais?",demoAnswer:"(D\xE9monstration) Ici, l\u2019assistant r\xE9pond \xE0 partir du contenu de votre site.",demoHint:"Aper\xE7u\xA0: la conversation est d\xE9sactiv\xE9e."},en:{assistant:"Virtual assistant",openLabel:"Virtual assistant, open the chat",dismissTeaser:"Hide this message",close:"Close the chat",panelTitle:"Chat",automated:"Automatic answers, drawn from this site\u2019s content.",privacy:"Privacy",you:"You",messageLabel:"Your message",send:"Send",live:"Replying live",relay:"{name} took over",relayGeneric:"A team member took over",connecting:"Connecting\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"Reload the page to continue.",unavailable:"Something went wrong. Try again in a moment.",turnstile:"Sending did not go through. Try again.",tooMany:"One moment, then try again.",fallbackTitle:"Leave us your details",fallbackCap:"Leave your details and we will get back to you.",fallbackDown:"Leave your details and we will get back to you.",name:"Your name",contact:"Phone or email",need:"Your request",fallbackSend:"Send the request",fallbackSent:"Thank you, your request was sent.",fallbackMissing:"Fill in all three fields.",said:"{name}: {text}",attachPhoto:"Attach a photo",photoHint:"Avoid photos of people or of identity documents.",photoAlt:"Your photo",photoSent:"Photo sent",photoLimit:"Three photos at most per conversation.",photoTooLarge:"This photo is too large.",photoUnreadable:"This file is not a readable photo.",photoFailed:"The photo could not be sent. Try again.",demoAsk:"Can you tell me more about {topic}?",demoQuestion:"What are your lead times?",demoAnswer:"(Demo) Here, the assistant answers from your site\u2019s content.",demoHint:"Preview: the conversation is turned off."}},Z=s=>String(s??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function we(s,l={}){let r=_t[Z(s)],t={...r};for(let[d,m]of Object.entries(l??{}))typeof m=="string"&&d in r&&(t[d]=m);return t}var H=(s,l)=>s.replace(/\{(\w+)\}/g,(r,t)=>t in l?String(l[t]):r);var _e="pmw-chat",Ze=[1e3,2e3,4e3,8e3,16e3,3e4],et=3,gt="chat-lead",xt=3,bt=1600,vt=.85,yt=3e3,kt=8e3,St='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></svg>';async function Tt(s,l){let r=await s.createImageBitmap(l,{imageOrientation:"from-image"}),t=Math.min(1,bt/Math.max(r.width,r.height)),d=Math.max(1,Math.round(r.width*t)),m=Math.max(1,Math.round(r.height*t)),u=s.document.createElement("canvas");return u.width=d,u.height=m,u.getContext("2d").drawImage(r,0,0,d,m),r.close?.(),new Promise((U,ee)=>u.toBlob(D=>D?U(D):ee(new Error("encode")),"image/jpeg",vt))}var Lt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},Ct={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function Et(s,l){let r=Z(l),t=we(r),d=String(s??"").trim().match(Ct[r]);if(!d)return t.demoQuestion;let m=d[1].replace(/[\p{L}’']+/gu,u=>Lt[r][u.toLowerCase()]??u);return H(t.demoAsk,{topic:m})}var Ot='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function o(s,l,r={},t){let d=s.createElement(l);for(let[m,u]of Object.entries(r))u!=null&&u!==!1&&d.setAttribute(m,u===!0?"":u);return t!==void 0&&(d.textContent=t),d}var At=s=>({load:()=>fe(s.document),async token(l){return await Xe(l,{win:s}),l.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset:l=>Ye(l,{win:s})});function Jt(s,l={}){if(s.pmwChat)return s.pmwChat;let r=l.win??window,t=r.document,d=l.WebSocket??r.WebSocket,m=l.fetch??((...e)=>r.fetch(...e)),u=l.turnstile??At(r),U=l.uuid??(()=>r.crypto.randomUUID()),ee=l.shrink??(e=>Tt(r,e)),D=l.preview??(e=>new Promise((n,a)=>{let c=new r.FileReader;c.onload=()=>n(String(c.result)),c.onerror=()=>a(c.error),c.readAsDataURL(e)})),S={};try{S=JSON.parse(s.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let B=Z(S.lang),i=we(B,S.strings),ge=s.dataset.pmwChatOpener??"",tt=s.querySelector(".pmw-chat__bubble"),f=l.demo??(S.demo===!0||!Ve({doc:t,hostname:r.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(o(t,"style",{id:"pmw-chat-style"},Je));let k=o(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),xe=o(t,"div",{class:"pmw-chat__head"}),te=s.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??o(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of te.querySelectorAll("[id]")){let n=e.id;e.id=`${n}-head`;for(let a of te.querySelectorAll(`[mask="url(#${n})"]`))a.setAttribute("mask",`url(#${n}-head)`)}let be=o(t,"div",{class:"pmw-chat__who"}),ve=o(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),$=o(t,"p",{class:"pmw-chat__role"}),ye=o(t,"span",{class:"pmw-chat__face"}),ke=o(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),F=o(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});F.innerHTML=Ot;let J=o(t,"p",{class:"pmw-chat__notice"},i.automated);S.privacyHref&&(J.append(" "),J.append(o(t,"a",{href:S.privacyHref},i.privacy))),ye.append(te,ke),be.append(ve,$,J),xe.append(ye,be,F);let w=o(t,"ol",{class:"pmw-chat__log"}),at=o(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},ge),y=o(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});y.append(o(t,"span"),o(t,"span"),o(t,"span")),w.append(at);let ae=o(t,"p",{class:"pmw-chat__state",role:"status"}),Se=o(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Te=e=>o(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":S.siteKey??"","data-action":"chat","data-theme":"light","data-language":B,"data-appearance":"interaction-only","data-size":"flexible"}),_=o(t,"form",{class:"pmw-chat__composer",novalidate:!0}),g=o(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"1",maxlength:String(2e3),placeholder:i.messageLabel,autocomplete:"off",disabled:f,"aria-describedby":f?"pmw-chat-demo-hint":null}),nt=o(t,"button",{type:"submit",class:"pmw-chat__send",disabled:f},i.send),ne=o(t,"div",{class:"pmw-chat__box"});ne.append(o(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),g,nt),_.append(ne),f?_.append(o(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint)):_.append(Te(_));let q=null,O=o(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(S.photos===!0){let e=o(t,"label",{class:"pmw-chat__attach"});q=o(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:f}),e.innerHTML=St,e.prepend(q),e.append(o(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),ne.insertBefore(e,g),_.append(o(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),O)}let j=o(t,"div",{class:"pmw-chat__ended",hidden:!0}),Le=o(t,"p",{},i.ended),G=o(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);j.append(Le,G);let x=o(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),Ce=o(t,"p",{class:"pmw-chat__fb-intro"}),oe=(e,n,a)=>{let c=`pmw-chat-fb-${e}`,p=o(t,"div",{class:"pmw-chat__field"});return p.append(o(t,"label",{for:c},i[e]),o(t,n,{id:c,name:e,required:!0,...a})),p},V=o(t,"p",{class:"pmw-chat__error",role:"alert"});x.append(o(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),Ce,oe("name","input",{autocomplete:"name",maxlength:"100"}),oe("contact","input",{autocomplete:"on",maxlength:"200"}),oe("need","textarea",{rows:"3",maxlength:"2000"}),V,o(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),f||x.append(Te(x));let Ee=o(t,"div",{class:"pmw-chat__body"});Ee.append(w,ae,j,x),k.append(xe,Ee,_,Se),s.append(k);let h=null;if(!f)try{h=JSON.parse(r.sessionStorage.getItem(_e)??"null")}catch{h=null}let Oe=()=>{try{r.sessionStorage.setItem(_e,JSON.stringify(h))}catch{}},b=null,re=0,se=null,R=null,K=0,I="bot",T=null,A=!1,Ae=0,ie=null,ce=new Set,L=new Map,X=new Set,le=0,Ne=()=>new URLSearchParams(r.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Me=f?Ne():null,pe=()=>f?Me:h?.botName||null;function z(){let e=I==="human";ve.textContent=e?T||i.assistant:pe()||i.assistant,$.textContent=e?i.live:pe()?i.assistant:"",$.hidden=$.textContent==="",J.hidden=e,ke.hidden=!e,k.dataset.mode=I}z();let C=e=>{ae.textContent=e??"",ae.hidden=!e};C("");let Pe=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||T||i.assistant:pe()||i.assistant;function N(e,n){let a=o(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${n}`,"data-id":e}),c=o(t,"span",{class:"pmw-chat__sr"}),p=o(t,"span",{class:"pmw-chat__text"});return a.append(c,p),w.insertBefore(a,y.parentNode===w?y:null),a}function qe(e,n){let a=e?e.previousElementSibling:y.parentNode===w?y.previousElementSibling:w.lastElementChild;for(;a;a=a.previousElementSibling){if(a.classList.contains("pmw-chat__relay")||a.classList.contains("pmw-chat__msg--owner"))return;if(a.classList.contains("pmw-chat__msg--bot"))break}let c=n?H(i.relay,{name:n}):i.relayGeneric,p=o(t,"li",{class:"pmw-chat__relay",role:"separator","aria-label":c}),v=o(t,"span",{class:"pmw-chat__relay-label","aria-hidden":"true"});v.append(o(t,"span",{class:"pmw-chat__relay-dot"}),c),p.append(v),w.insertBefore(p,e??(y.parentNode===w?y:null))}let Re=e=>[...w.querySelectorAll("li[data-id]")].find(n=>n.dataset.id===e),E=(e,n,a)=>{e.firstChild.textContent=H(i.said,{name:Pe(n),text:""}),e.lastChild.textContent=a===""&&n.photos?.length&&!e.querySelector("img")?i.photoSent:a};function de(){y.hidden=!0,r.clearTimeout(ie)}function Ie(e,{announce:n}){if(ce.has(e.seq))return;ce.add(e.seq),K=Math.max(K,e.seq);let a=Re(e.id);a||(a=N(e.id,e.role),e.role==="owner"&&qe(a,e.by||T)),a.classList.remove("is-streaming","is-pending","is-failed"),a.querySelector(".pmw-chat__retry")?.remove(),E(a,e,e.text),e.role==="visitor"?(L.delete(e.id),e.photos?.length&&X.add(e.id)):(de(),n&&(Se.textContent=H(i.said,{name:Pe(e),text:e.text})),k.hidden&&s.setAttribute("data-unread","")),M()}function ot(e,n){de();let a=Re(e);a||(a=N(e,"bot"),a.classList.add("is-streaming"),E(a,{role:"bot"},"")),a.lastChild.textContent+=n,M()}let M=()=>{w.scrollTop=w.scrollHeight};if(f){E(N("demo-question","visitor"),{role:"visitor"},Et(ge,B));let e=N("demo-answer","bot");E(e,{role:"bot"},i.demoAnswer),r.addEventListener("hashchange",()=>{Me=Ne(),z(),E(e,{role:"bot"},i.demoAnswer)})}function Y(e){if(e.failed=!0,e.li.classList.remove("is-pending"),e.li.classList.add("is-failed"),e.li.querySelector(".pmw-chat__retry"))return;let n=o(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),a=o(t,"button",{type:"button"},i.retry);a.addEventListener("click",()=>{e.failed=!1,e.tries=0,n.remove(),e.li.classList.replace("is-failed","is-pending"),h?Q()?he(e):P():me()}),n.append(a),e.li.append(n)}let Q=()=>!!b&&b.readyState===1;function he(e){if(!(e.failed||!Q())){if(e.tries>=et)return Y(e);e.tries++,b.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function P(){if(!h||A||b)return;r.clearTimeout(se);let e=r.location.protocol==="https:"?"wss:":"ws:",n=new URLSearchParams({c:h.c,k:h.k}),a=new d(`${e}//${r.location.host}/api/chat/ws?${n}`);b=a,a.onopen=()=>{re=0,C(""),a.send(JSON.stringify({type:"hello",since:K}));for(let c of L.values())he(c)},a.onmessage=c=>{let p;try{p=JSON.parse(c.data)}catch{return}st(p)},a.onclose=c=>{b===a&&(b=null);for(let v of L.values())v.tries>=et&&Y(v);if(c.code>=4e3&&c.code<5e3)return rt(c.code);if(A)return;C(i.reconnecting);let p=Ze[Math.min(re,Ze.length-1)];re++,se=r.setTimeout(P,p)}}function rt(e){if(e===Qe.rpc)return W(i.reload,!1);W(i.ended,!0)}function W(e,n){A=!0,r.clearTimeout(se),C(""),Le.textContent=e,G.hidden=!n,j.hidden=!1,_.hidden=!0;for(let a of L.values())Y(a)}function ze(e){I=e.mode==="human"?"human":"bot",T=typeof e.ownerName=="string"?e.ownerName:null,z(),I==="human"&&qe(null,T),e.closed&&W(i.ended,!0)}function st(e){switch(e?.type){case"history":typeof e.ownerName=="string"&&(T=e.ownerName);for(let n of e.messages??[])Ie(n,{announce:!1});return ze(e);case"message":return Ie(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return ot(e.id,e.text);case"typing":return e.role==="visitor"?void 0:(y.hidden=!1,w.append(y),r.clearTimeout(ie),ie=r.setTimeout(de,kt),M());case"status":return ze(e);case"fallback":return Be(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Ue();case"error":(e.code==="closed"||e.code==="deleted")&&W(i.ended,!0)}}let He=!1;function Ue(){if(!(He||h?.lead)){He=!0,h&&(h.lead=!0,Oe());try{r.pmw?.event(gt)}catch{}}}async function De(e,n){let a="";try{a=await u.token(e)}catch{a=""}let c,p=null;try{c=await m("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:a,lang:B,page:r.location.pathname,...n})}),p=await c.json()}catch{c=null}try{u.reset(e)}catch{}return c?.ok&&p?{ok:!0,data:p}:{ok:!1,error:c?.status===403?i.turnstile:c?.status===429?i.tooMany:null,cap:c?.status===409&&p?.error==="cap"}}function me(){return R||h||(C(i.connecting),R=De(_,{}).then(e=>{if(R=null,e.ok&&e.data.conversationId&&e.data.visitorKey){h={c:e.data.conversationId,k:e.data.visitorKey,botName:typeof e.data.botName=="string"?e.data.botName:null},Oe(),z(),P();return}for(let n of L.values())Y(n);if(e.error)return C(e.error);C(""),Be(e.cap?i.fallbackCap:i.fallbackDown)})),R}function Be(e){Ce.textContent=e,x.hidden=!1,_.hidden=!0,x.querySelector("input")?.focus()}x.addEventListener("submit",async e=>{e.preventDefault();let n=p=>x.elements[p].value.trim(),a={name:n("name"),contact:n("contact"),need:n("need")};if(!a.name||!a.contact||!a.need){V.textContent=i.fallbackMissing;return}V.textContent="";let c=await De(x,{fallbackLead:a});if(!c.ok){V.textContent=c.error??i.unavailable;return}x.replaceChildren(o(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Ue()});let it={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function ct(e){let n,a;try{n=await ee(e),a=await D(n)}catch{O.textContent=i.photoUnreadable;return}let c=U(),p=N(c,"visitor");p.classList.add("is-pending"),p.insertBefore(o(t,"img",{class:"pmw-chat__photo",src:a,alt:i.photoAlt}),p.lastChild),E(p,{role:"visitor"},""),M(),h||await me();let v=null;if(h){let dt=new URLSearchParams({c:h.c,k:h.k,id:c});try{v=await m(`/api/chat/photo?${dt}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:n})}catch{v=null}}if(v?.ok){p.classList.remove("is-pending"),X.add(c),b||P();return}p.remove(),O.textContent=i[it[v?.status]]??i.photoFailed}q?.addEventListener("change",async()=>{let e=[...q.files??[]];if(q.value="",O.textContent="",A||f||e.length===0)return;let n=xt-X.size-le;e.length>n&&(O.textContent=i.photoLimit);let a=e.slice(0,Math.max(0,n));le+=a.length;for(let c of a)try{await ct(c)}finally{le--}});function $e(){let e=g.value.trim();if(!e||A||f)return;g.value="",Fe();let n={id:U(),text:e.slice(0,2e3),tries:0,failed:!1};n.li=N(n.id,"visitor"),n.li.classList.add("is-pending"),E(n.li,{role:"visitor"},n.text),L.set(n.id,n),M(),h?Q()?he(n):P():me()}_.addEventListener("submit",e=>{e.preventDefault(),$e()}),g.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),$e())});let Fe=()=>{g.style.height="auto",g.style.height=g.value?`${Math.min(g.scrollHeight,132)}px`:""};g.addEventListener("input",()=>{Fe();let e=Date.now();Q()&&e-Ae>yt&&(Ae=e,b.send(JSON.stringify({type:"typing"})))}),G.addEventListener("click",()=>{h=null;try{r.sessionStorage.removeItem(_e)}catch{}try{b?.close(1e3)}catch{}b=null,A=!1,K=0,ce.clear(),L.clear(),X.clear(),O.textContent="",I="bot",T=null,z();for(let e of[...w.querySelectorAll("li[data-id], .pmw-chat__relay")])e.remove();j.hidden=!0,_.hidden=!1,g.focus()});let lt=()=>[...k.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));k.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),ue();return}if(e.key!=="Tab")return;let n=lt();if(n.length===0)return;let a=n[0],c=n[n.length-1];e.shiftKey&&t.activeElement===a?(e.preventDefault(),c.focus()):!e.shiftKey&&t.activeElement===c&&(e.preventDefault(),a.focus())}),F.addEventListener("click",()=>ue());function pt(){f||u.load(),k.hidden=!1,s.setAttribute("data-open",""),s.removeAttribute("data-unread"),h&&P(),(f?F:_.hidden?x.hidden?G:x.querySelector("input"):g)?.focus(),M()}function ue(){k.hidden=!0,s.removeAttribute("data-open"),tt?.focus()}return s.pmwChat={open:pt,close:ue,panel:k},s.pmwChat}export{Ze as BACKOFF_MS,gt as GOAL,xt as MAX_PHOTOS,et as MAX_TRIES,bt as PHOTO_EDGE,vt as PHOTO_QUALITY,_e as SESSION_KEY,Et as demoQuestion,Jt as mountChat};
|
|
1
|
+
var ot=`.pmw-chat__panel{position:fixed;z-index:81;inset:0;display:flex;flex-direction:column;background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-fg, #1f2328);font-family:var(--pmw-chat-font, inherit);font-size:16px;line-height:1.45}.pmw-chat__panel[hidden],.pmw-chat__panel [hidden]{display:none!important}@media(min-width:640px){.pmw-chat__panel{inset:auto;right:calc(env(safe-area-inset-right,0px) + var(--pmw-chat-right, 16px));bottom:calc(env(safe-area-inset-bottom,0px) + var(--pmw-chat-bottom, 16px));width:380px;height:min(600px,calc(100dvh - 16px - env(safe-area-inset-bottom,0px) - var(--pmw-chat-bottom, 16px)));border:1px solid var(--pmw-chat-line, #e6e9f0);border-radius:20px;overflow:hidden;box-shadow:0 18px 48px #1018282e}}.pmw-chat__panel :where(button){font:inherit;cursor:pointer}.pmw-chat__panel :focus-visible{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__head{display:flex;align-items:center;gap:12px;padding:14px 10px 14px 16px;border-bottom:1px solid var(--pmw-chat-line, #e6e9f0)}@media(max-width:639.98px){.pmw-chat__head{padding:12px 8px 12px 16px}}.pmw-chat__face{position:relative;flex:none;display:flex}.pmw-chat__head .pmw-chat__avatar{width:44px;height:44px;box-shadow:none}.pmw-chat__head .pmw-chat__avatar--icon{box-shadow:0 0 0 1.5px var(--pmw-chat-avatar-ring, transparent)}.pmw-chat__head .pmw-chat__avatar img{width:44px;height:44px}.pmw-chat__head .pmw-chat__avatar--logo img{width:32px;height:32px}.pmw-chat__head .pmw-chat__avatar svg{width:100%;height:100%}.pmw-chat__dot{position:absolute;right:-1px;bottom:-1px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-live, #1a7f37);box-shadow:0 0 0 2px var(--pmw-chat-panel-bg, #ffffff)}.pmw-chat__who{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.pmw-chat__name{margin:0;font-weight:600;font-size:16px}.pmw-chat__role,.pmw-chat__notice{margin:0;font-size:13px;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__panel[data-mode=human] .pmw-chat__role{color:var(--pmw-chat-live, #1a7f37);font-weight:500}.pmw-chat__notice a{color:var(--pmw-chat-link, #0a1b3d);text-decoration:underline;text-underline-offset:2px}.pmw-chat__close{flex:none;width:44px;height:44px;padding:0;border:0;border-radius:50%;display:flex;align-items:center;justify-content:center;background:transparent;color:var(--pmw-chat-fg, #1f2328)}.pmw-chat__close:hover{background:var(--pmw-chat-thread-bg, #f7f8fb)}.pmw-chat__close svg{width:18px;height:18px}.pmw-chat__body{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding:18px 16px;background:var(--pmw-chat-thread-bg, #f7f8fb)}.pmw-chat__log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.pmw-chat__log>li{margin-top:4px}.pmw-chat__log>li:first-child,.pmw-chat__msg--bot+.pmw-chat__msg--bot,.pmw-chat__msg--bot+.pmw-chat__typing,.pmw-chat__msg--owner+.pmw-chat__msg--owner,.pmw-chat__msg--owner+.pmw-chat__typing,.pmw-chat__msg--visitor+.pmw-chat__msg--visitor{margin-top:0}.pmw-chat__msg{max-width:82%;padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere}.pmw-chat__msg--bot,.pmw-chat__msg--owner{align-self:flex-start;border-bottom-left-radius:6px;color:var(--pmw-chat-fg, #1f2328)}.pmw-chat__msg--bot{background:var(--pmw-chat-other-bg, #ffffff);border:1px solid var(--pmw-chat-line, #e6e9f0)}.pmw-chat__msg--owner{background:var(--pmw-chat-owner-bg, #eef2fa);border:1px solid var(--pmw-chat-owner-border, #d5ddef)}.pmw-chat__msg--visitor{align-self:flex-end;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff);border-bottom-right-radius:6px}.pmw-chat__msg--bot+.pmw-chat__msg--bot,.pmw-chat__msg--owner+.pmw-chat__msg--owner{border-top-left-radius:6px}.pmw-chat__msg--visitor+.pmw-chat__msg--visitor{border-top-right-radius:6px}.pmw-chat__msg:has(>.pmw-chat__photo){padding:4px}.pmw-chat__msg:has(>.pmw-chat__photo) .pmw-chat__text:empty{display:none}.pmw-chat__msg.is-pending{opacity:.75}.pmw-chat__msg.is-failed{outline:2px solid var(--pmw-chat-error, #b42318);outline-offset:2px}.pmw-chat__retry{display:block;margin-top:4px;font-size:14px}.pmw-chat__retry button{padding:0;border:0;background:none;color:inherit;text-decoration:underline}.pmw-chat__typing{align-self:flex-start;display:flex;gap:5px;padding:14px 16px;border-radius:18px 18px 18px 6px;background:var(--pmw-chat-other-bg, #ffffff);border:1px solid var(--pmw-chat-line, #e6e9f0)}.pmw-chat__typing span{width:7px;height:7px;border-radius:50%;background:var(--pmw-chat-muted, #4b5563);animation:pmw-chat-dot 1.2s infinite ease-in-out}.pmw-chat__typing span:nth-child(2){animation-delay:.15s}.pmw-chat__typing span:nth-child(3){animation-delay:.3s}@keyframes pmw-chat-dot{0%,80%,to{opacity:.3}40%{opacity:1}}@media(prefers-reduced-motion:reduce){.pmw-chat__typing span{animation:none;opacity:.7}}.pmw-chat__log>.pmw-chat__relay,.pmw-chat__log>.pmw-chat__handback{align-self:stretch;margin:18px 0 8px;display:flex;align-items:center;gap:10px;font-size:13px;font-weight:500;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__relay:before,.pmw-chat__relay:after,.pmw-chat__handback:before,.pmw-chat__handback:after{content:"";flex:1;min-width:16px;height:1px;background:var(--pmw-chat-rule, #cfd5e1)}.pmw-chat__relay-label{display:flex;align-items:center;gap:6px;text-align:center}.pmw-chat__relay-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--pmw-chat-live, #1a7f37)}.pmw-chat__relay-dot--back{background:var(--pmw-chat-muted, #4b5563)}.pmw-chat__log>.pmw-chat__relay+li,.pmw-chat__log>.pmw-chat__handback+li{margin-top:0}.pmw-chat__state{margin:8px 0 0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__state:empty{display:none}.pmw-chat__ended{margin-top:22px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center}.pmw-chat__ended:before{content:"";align-self:stretch;height:1px;background:var(--pmw-chat-line, #e6e9f0)}.pmw-chat__ended p{margin:0;font-size:15px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__restart{min-height:44px;padding:0 20px;border-radius:22px;border:1px solid var(--pmw-chat-link, #0a1b3d);background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-link, #0a1b3d);font-weight:600}.pmw-chat__composer{--pmw-chat-gap: 8px;display:flex;flex-direction:column;gap:var(--pmw-chat-gap);padding:14px 16px 16px;border-top:1px solid var(--pmw-chat-line, #e6e9f0);background:var(--pmw-chat-panel-bg, #ffffff)}@media(max-width:639.98px){.pmw-chat__composer{padding:12px 16px calc(14px + env(safe-area-inset-bottom,0px))}}.pmw-chat__box{display:flex;align-items:flex-end;gap:6px;padding:4px;border-radius:24px;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__box textarea{flex:1;min-width:0;box-sizing:border-box;min-height:44px;max-height:132px;resize:none;margin:0;padding:10px 2px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:16px;line-height:1.5}.pmw-chat__box textarea::placeholder{color:#6b7280;opacity:1}@supports selector(:has(*)){.pmw-chat__box textarea:focus-visible{outline:none}.pmw-chat__box:has(textarea:focus-visible){outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}}.pmw-chat__send{flex:none;min-height:44px;padding:0 18px;border:0;border-radius:22px;font-weight:600;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__send:disabled,.pmw-chat__box textarea:disabled{cursor:not-allowed;opacity:.6}.pmw-chat__check{display:grid;grid-template-rows:0fr;margin-bottom:calc(-1 * var(--pmw-chat-gap, 0px));transition:grid-template-rows .25s ease,margin-bottom .25s ease}.pmw-chat__check[data-ask]{grid-template-rows:1fr;margin-bottom:4px}.pmw-chat__check-in{min-height:0;overflow:hidden}@media(prefers-reduced-motion:reduce){.pmw-chat__check{transition:none}}.pmw-chat__attach{flex:none;position:relative;width:44px;height:44px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:inherit}.pmw-chat__attach:hover{background:#eef0f4}.pmw-chat__attach svg{width:22px;height:22px}.pmw-chat__attach:focus-within{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__hint{margin:0 8px;font-size:13px;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__composer .pmw-chat__photo-error{margin:0 8px}.pmw-chat__photo{display:block;max-width:100%;max-height:220px;border-radius:14px}.pmw-chat__fallback{--pmw-chat-gap: 14px;display:flex;flex-direction:column;gap:var(--pmw-chat-gap);margin-top:16px}.pmw-chat__fb-title{margin:0;font-size:20px;font-weight:600;line-height:1.3}.pmw-chat__fb-intro{margin:-8px 0 0;font-size:15px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__field{display:flex;flex-direction:column;gap:4px}.pmw-chat__field label{font-size:14px;font-weight:600}.pmw-chat__field input,.pmw-chat__field textarea{font:inherit;font-size:16px;padding:10px 14px;border-radius:12px;resize:none;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__fallback .pmw-chat__send{align-self:stretch;min-height:48px;border-radius:24px}.pmw-chat__error{margin:0;font-size:14px;color:var(--pmw-chat-error, #b42318)}.pmw-chat__error:empty{display:none}.pmw-chat__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.pmw-chat[data-unread] .pmw-chat__bubble{position:relative}.pmw-chat[data-unread] .pmw-chat__bubble:after{content:"";position:absolute;top:2px;left:38px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-error, #b42318);box-shadow:0 0 0 2px #fff}
|
|
2
|
+
`;var At="400px 0px";var rt=new Set,ce=null;function st(r,{doc:l=document,attrs:n={}}={}){if(rt.has(r)||(rt.add(r),l.querySelector(`script[src="${r}"]`)))return;let t=l.createElement("script");t.src=r,t.async=!0;for(let[m,p]of Object.entries(n))t.setAttribute(m,p);l.head.append(t)}function it({doc:r=document,hostname:l=globalThis.location?.hostname??""}={}){let n=r.getElementById("pmw-hosts");if(!n)return!1;let t;try{t=JSON.parse(n.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let m=String(l).toLowerCase();return t.some(p=>typeof p=="string"&&p.toLowerCase()===m)}function Ot(r){return ce||(ce=new Promise(l=>{let n=null,t=null,m=0,p=r.PerformanceObserver?.supportedEntryTypes??[],d=typeof r.PerformanceObserver=="function"&&p.includes("paint"),f=()=>{n===null||d&&t===null||l(Math.max(n,t??0,m))};if(d)try{new r.PerformanceObserver(v=>{for(let T of v.getEntries())T.name==="first-contentful-paint"&&(t=T.startTime);f()}).observe({type:"paint",buffered:!0}),p.includes("largest-contentful-paint")&&new r.PerformanceObserver(v=>{for(let T of v.getEntries())m=Math.max(m,T.startTime)}).observe({type:"largest-contentful-paint",buffered:!0})}catch{t=0}let E=()=>{n=r.performance.now(),f()};r.document.readyState==="complete"?E():r.addEventListener("load",E,{once:!0})}),ce)}function Le(r,l,{win:n=window,rootMargin:t=At,settleMs:m=1500}={}){let p=null,d=!1,f=()=>{d||(d=!0,p?.disconnect(),r.removeEventListener("focusin",f),r.removeEventListener("pointerdown",f),l())};r.addEventListener("focusin",f),r.addEventListener("pointerdown",f);let E=Ot(n),v=async()=>{let T=await E;n.setTimeout(f,Math.max(0,T+m-n.performance.now()))};return typeof n.IntersectionObserver=="function"?(p=new n.IntersectionObserver(T=>{T.some(L=>L.isIntersecting)&&(p.disconnect(),v())},{rootMargin:t}),p.observe(r)):v(),{run:f}}var Mt="https://challenges.cloudflare.com/turnstile/v0/api.js";var Nt=15e3,ct=200;function Ce(r=document){st(Mt,{doc:r})}var Pt=r=>r.querySelector('input[name="cf-turnstile-response"]')?.value??"",Rt=r=>{let l=r.querySelector(".cf-turnstile");return!!l&&l.offsetHeight>0};async function lt(r,{win:l=window,timeoutMs:n=Nt}={}){typeof r.pmwTurnstileLoad=="function"?r.pmwTurnstileLoad():Ce(l.document);let t=0;for(;Pt(r)==="";){if(!Rt(r)){if(t>=n)return!1;t+=ct}await new Promise(m=>l.setTimeout(m,ct))}return!0}var pt={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var qt={fr:{assistant:"Assistant virtuel",openLabel:"Assistant virtuel, ouvrir le clavardage",dismissTeaser:"Masquer ce message",close:"Fermer le clavardage",panelTitle:"Clavardage",automated:"R\xE9ponses automatiques, tir\xE9es du contenu de ce site.",privacy:"Confidentialit\xE9",you:"Vous",messageLabel:"Votre message",send:"Envoyer",live:"Vous r\xE9pond en direct",relay:"{name} a pris le relais",relayGeneric:"Une personne de l\u2019\xE9quipe a pris le relais",handback:"L\u2019assistant virtuel a repris la conversation",oneMoment:"Un instant\u2026",reconnecting:"Connexion perdue, reconnexion\u2026",notSent:"Non envoy\xE9.",retry:"R\xE9essayer",ended:"Cette conversation est termin\xE9e.",newConversation:"Nouvelle conversation",reload:"Rechargez la page pour continuer.",unavailable:"Un probl\xE8me est survenu. R\xE9essayez dans un moment.",turnstile:"L\u2019envoi n\u2019a pas abouti. R\xE9essayez.",tooMany:"Un instant, puis r\xE9essayez.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"Laissez vos coordonn\xE9es et nous vous recontacterons.",fallbackDown:"Laissez vos coordonn\xE9es et nous vous recontacterons.",name:"Votre nom",contact:"T\xE9l\xE9phone ou courriel",need:"Votre demande",fallbackSend:"Envoyer la demande",fallbackSent:"Merci, votre demande a \xE9t\xE9 transmise.",fallbackMissing:"Remplissez les trois champs.",said:"{name}\xA0: {text}",attachPhoto:"Joindre une photo",photoHint:"\xC9vitez les photos de personnes ou de pi\xE8ces d\u2019identit\xE9.",photoAlt:"Votre photo",photoSent:"Photo envoy\xE9e",photoLimit:"Trois photos au plus par conversation.",photoTooLarge:"Cette photo est trop lourde.",photoUnreadable:"Ce fichier n\u2019est pas une photo lisible.",photoFailed:"La photo n\u2019a pas pu \xEAtre envoy\xE9e. R\xE9essayez.",demoAsk:"Pouvez-vous m\u2019en dire plus sur {topic}?",demoQuestion:"Quels sont vos d\xE9lais?",demoAnswer:"(D\xE9monstration) Ici, l\u2019assistant r\xE9pond \xE0 partir du contenu de votre site.",demoHint:"Aper\xE7u\xA0: la conversation est d\xE9sactiv\xE9e."},en:{assistant:"Virtual assistant",openLabel:"Virtual assistant, open the chat",dismissTeaser:"Hide this message",close:"Close the chat",panelTitle:"Chat",automated:"Automatic answers, drawn from this site\u2019s content.",privacy:"Privacy",you:"You",messageLabel:"Your message",send:"Send",live:"Replying live",relay:"{name} took over",relayGeneric:"A team member took over",handback:"The virtual assistant is back in the conversation",oneMoment:"One moment\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"Reload the page to continue.",unavailable:"Something went wrong. Try again in a moment.",turnstile:"Sending did not go through. Try again.",tooMany:"One moment, then try again.",fallbackTitle:"Leave us your details",fallbackCap:"Leave your details and we will get back to you.",fallbackDown:"Leave your details and we will get back to you.",name:"Your name",contact:"Phone or email",need:"Your request",fallbackSend:"Send the request",fallbackSent:"Thank you, your request was sent.",fallbackMissing:"Fill in all three fields.",said:"{name}: {text}",attachPhoto:"Attach a photo",photoHint:"Avoid photos of people or of identity documents.",photoAlt:"Your photo",photoSent:"Photo sent",photoLimit:"Three photos at most per conversation.",photoTooLarge:"This photo is too large.",photoUnreadable:"This file is not a readable photo.",photoFailed:"The photo could not be sent. Try again.",demoAsk:"Can you tell me more about {topic}?",demoQuestion:"What are your lead times?",demoAnswer:"(Demo) Here, the assistant answers from your site\u2019s content.",demoHint:"Preview: the conversation is turned off."}},le=r=>String(r??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function Ee(r,l={}){let n=qt[le(r)],t={...n};for(let[m,p]of Object.entries(l??{}))typeof p=="string"&&m in n&&(t[m]=p);return t}var V=(r,l)=>r.replace(/\{(\w+)\}/g,(n,t)=>t in l?String(l[t]):n);var Ae="pmw-chat",ht=[1e3,2e3,4e3,8e3,16e3,3e4],mt=3,It="chat-lead",zt=3,Ht=1600,$t=.85,Dt=3e3,Ut=8e3,Bt=40,Ft=3e3,ut=8e3,Jt='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></svg>';async function jt(r,l){let n=await r.createImageBitmap(l,{imageOrientation:"from-image"}),t=Math.min(1,Ht/Math.max(n.width,n.height)),m=Math.max(1,Math.round(n.width*t)),p=Math.max(1,Math.round(n.height*t)),d=r.document.createElement("canvas");return d.width=m,d.height=p,d.getContext("2d").drawImage(n,0,0,m,p),n.close?.(),new Promise((f,E)=>d.toBlob(v=>v?f(v):E(new Error("encode")),"image/jpeg",$t))}var Gt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},Kt={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function Vt(r,l){let n=le(l),t=Ee(n),m=String(r??"").trim().match(Kt[n]);if(!m)return t.demoQuestion;let p=m[1].replace(/[\p{L}’']+/gu,d=>Gt[n][d.toLowerCase()]??d);return V(t.demoAsk,{topic:p})}var Xt='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function s(r,l,n={},t){let m=r.createElement(l);for(let[p,d]of Object.entries(n))d!=null&&d!==!1&&m.setAttribute(p,d===!0?"":d);return t!==void 0&&(m.textContent=t),m}var Yt=(r,l)=>{let n=!1,t=()=>{let p=r.turnstile;if(!p?.render)return!1;for(let d of l)if(!d.childElementCount)try{p.render(d)}catch{}return!0},m=()=>{if(Ce(r.document),n||t())return;n=!0;let p=0,d=()=>{!t()&&++p<600&&r.setTimeout(d,100)};r.setTimeout(d,100)};for(let p of l){let d=p.closest("form");d&&(d.pmwTurnstileLoad=m)}return{load:m,async token(p){return await lt(p,{win:r}),p.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset(p){let d=p.querySelector(".cf-turnstile");d&&!d.closest("[data-ask]")&&r.turnstile?.reset(d)}}};function la(r,l={}){if(r.pmwChat)return r.pmwChat;let n=l.win??window,t=n.document,m=l.WebSocket??n.WebSocket,p=l.fetch??((...e)=>n.fetch(...e)),d=[],f=l.turnstile??null,E=l.uuid??(()=>n.crypto.randomUUID()),v=l.shrink??(e=>jt(n,e)),T=l.preview??(e=>new Promise((a,o)=>{let c=new n.FileReader;c.onload=()=>a(String(c.result)),c.onerror=()=>o(c.error),c.readAsDataURL(e)})),L={};try{L=JSON.parse(r.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let X=le(L.lang),i=Ee(X,L.strings),Oe=r.dataset.pmwChatOpener??"",ft=r.querySelector(".pmw-chat__bubble"),w=l.demo??(L.demo===!0||!it({doc:t,hostname:n.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(s(t,"style",{id:"pmw-chat-style"},ot));let A=s(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),Me=s(t,"div",{class:"pmw-chat__head"}),pe=r.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??s(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of pe.querySelectorAll("[id]")){let a=e.id;e.id=`${a}-head`;for(let o of pe.querySelectorAll(`[mask="url(#${a})"]`))o.setAttribute("mask",`url(#${a}-head)`)}let Ne=s(t,"div",{class:"pmw-chat__who"}),Pe=s(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),Y=s(t,"p",{class:"pmw-chat__role"}),Re=s(t,"span",{class:"pmw-chat__face"}),qe=s(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),W=s(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});W.innerHTML=Xt;let Q=s(t,"p",{class:"pmw-chat__notice"},i.automated);L.privacyHref&&(Q.append(" "),Q.append(s(t,"a",{href:L.privacyHref},i.privacy))),Re.append(pe,qe),Ne.append(Pe,Y,Q),Me.append(Re,Ne,W);let _=s(t,"ol",{class:"pmw-chat__log"}),wt=s(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},Oe),C=s(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});C.append(s(t,"span"),s(t,"span"),s(t,"span")),_.append(wt);let de=s(t,"p",{class:"pmw-chat__state",role:"status"}),Ie=s(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),ze=e=>{let a=s(t,"div",{class:"pmw-chat__check"}),o=s(t,"div",{class:"pmw-chat__check-in"}),c=`pmwChatTurnstile${e}`,h=D=>()=>{a.toggleAttribute("data-ask",D),k()};n[`${c}Ask`]=h(!0),n[`${c}Error`]=h(!0),n[`${c}Done`]=h(!1);let x=s(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":L.siteKey??"","data-action":"chat","data-theme":"light","data-language":X,"data-appearance":"interaction-only","data-size":"flexible","data-callback":`${c}Done`,"data-before-interactive-callback":`${c}Ask`,"data-error-callback":`${c}Error`});return d.push(x),o.append(x),a.append(o),a},g=s(t,"form",{class:"pmw-chat__composer",novalidate:!0}),b=s(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"1",maxlength:String(2e3),placeholder:i.messageLabel,autocomplete:"off",disabled:w,"aria-describedby":w?"pmw-chat-demo-hint":null}),_t=s(t,"button",{type:"submit",class:"pmw-chat__send",disabled:w},i.send),he=s(t,"div",{class:"pmw-chat__box"});he.append(s(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),b,_t),w||g.append(ze("c")),g.append(he),w&&g.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint));let U=null,I=s(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(L.photos===!0){let e=s(t,"label",{class:"pmw-chat__attach"});U=s(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:w}),e.innerHTML=Jt,e.prepend(U),e.append(s(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),he.insertBefore(e,b),g.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),I)}let Z=s(t,"div",{class:"pmw-chat__ended",hidden:!0}),He=s(t,"p",{},i.ended),ee=s(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);Z.append(He,ee);let y=s(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),$e=s(t,"p",{class:"pmw-chat__fb-intro"}),me=(e,a,o)=>{let c=`pmw-chat-fb-${e}`,h=s(t,"div",{class:"pmw-chat__field"});return h.append(s(t,"label",{for:c},i[e]),s(t,a,{id:c,name:e,required:!0,...o})),h},B=s(t,"p",{class:"pmw-chat__error",role:"alert"});y.append(s(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),$e,me("name","input",{autocomplete:"name",maxlength:"100"}),me("contact","input",{autocomplete:"on",maxlength:"200"}),me("need","textarea",{rows:"3",maxlength:"2000"}),B,s(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),w||y.insertBefore(ze("f"),B);let O=s(t,"div",{class:"pmw-chat__body"});O.append(_,de,Z,y),A.append(Me,O,g,Ie),r.append(A),f??(f=Yt(n,d));let F=!0,De=0,ue=0,gt=()=>O.scrollHeight-O.scrollTop-O.clientHeight;O.addEventListener("scroll",()=>{let e=O.scrollTop;gt()<=Bt?F=!0:e<De&&(F=!1),De=e},{passive:!0});let bt=e=>n.requestAnimationFrame?n.requestAnimationFrame(e):n.setTimeout(e,16);function k(e=!1,a=!1){e&&(F=!0),!(!F||ue)&&(ue=bt(()=>{if(ue=0,!F)return;let o=a||!!n.matchMedia?.("(prefers-reduced-motion: reduce)").matches;O.scrollTo({top:O.scrollHeight,behavior:o?"instant":"smooth"})}))}if(typeof n.ResizeObserver=="function"){let e=new n.ResizeObserver(()=>k());e.observe(_),e.observe(g)}let u=null;if(!w)try{u=JSON.parse(n.sessionStorage.getItem(Ae)??"null")}catch{u=null}let Ue=()=>{try{n.sessionStorage.setItem(Ae,JSON.stringify(u))}catch{}},S=null,fe=0,we=null,J=null,te=0,j="bot",P=null,z=!1,Be=0,_e=null,ge=new Set,R=new Map,ae=new Set,be=0,Fe=()=>new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Je=w?Fe():null,xe=()=>w?Je:u?.botName||null;function G(){let e=j==="human";Pe.textContent=e?P||i.assistant:xe()||i.assistant,Y.textContent=e?i.live:xe()?i.assistant:"",Y.hidden=Y.textContent==="",Q.hidden=e,qe.hidden=!e,A.dataset.mode=j}G();let M=e=>{de.textContent=e??"",de.hidden=!e};M("");let je=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||P||i.assistant:xe()||i.assistant;function H(e,a){let o=s(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${a}`,"data-id":e}),c=s(t,"span",{class:"pmw-chat__sr"}),h=s(t,"span",{class:"pmw-chat__text"});return o.append(c,h),_.insertBefore(o,C.parentNode===_?C:null),o}let Ge=()=>C.parentNode===_?C.previousElementSibling:_.lastElementChild,N=(e,a)=>e.classList.contains(a);function Ke(e,a,o,c){let h=s(t,"li",{class:e,role:"separator","aria-label":o}),x=s(t,"span",{class:"pmw-chat__relay-label","aria-hidden":"true"});x.append(s(t,"span",{class:a}),o),h.append(x),_.insertBefore(h,c??(C.parentNode===_?C:null)),k()}function Ve(e,a){for(let o=e?e.previousElementSibling:Ge();o;o=o.previousElementSibling){if(N(o,"pmw-chat__relay")||N(o,"pmw-chat__msg--owner"))return;if(N(o,"pmw-chat__msg--bot")||N(o,"pmw-chat__handback"))break}Ke("pmw-chat__relay","pmw-chat__relay-dot",a?V(i.relay,{name:a}):i.relayGeneric,e)}function ve(e){for(let a=e?e.previousElementSibling:Ge();;a=a.previousElementSibling){if(!a||N(a,"pmw-chat__handback")||N(a,"pmw-chat__msg--bot"))return;if(N(a,"pmw-chat__relay")||N(a,"pmw-chat__msg--owner"))break}Ke("pmw-chat__handback","pmw-chat__relay-dot pmw-chat__relay-dot--back",i.handback,e)}let Xe=e=>[..._.querySelectorAll("li[data-id]")].find(a=>a.dataset.id===e),q=(e,a,o)=>{e.firstChild.textContent=V(i.said,{name:je(a),text:""}),e.lastChild.textContent=o===""&&a.photos?.length&&!e.querySelector("img")?i.photoSent:o};function K(){C.hidden=!0,n.clearTimeout(_e)}function ye(e){C.hidden=!1,_.append(C),n.clearTimeout(_e),e&&(_e=n.setTimeout(K,e)),k()}function Ye(e,{announce:a}){if(ge.has(e.seq))return;ge.add(e.seq),te=Math.max(te,e.seq);let o=Xe(e.id);o||(o=H(e.id,e.role),e.role==="owner"?Ve(o,e.by||P):e.role==="bot"&&ve(o)),o.classList.remove("is-streaming","is-pending","is-failed"),o.querySelector(".pmw-chat__retry")?.remove(),q(o,e,e.text),e.role==="visitor"?(R.delete(e.id),e.photos?.length&&ae.add(e.id)):(K(),a&&(Ie.textContent=V(i.said,{name:je(e),text:e.text})),A.hidden&&r.setAttribute("data-unread","")),k()}function xt(e,a){K();let o=Xe(e);o||(o=H(e,"bot"),ve(o),o.classList.add("is-streaming"),q(o,{role:"bot"},"")),o.lastChild.textContent+=a,k()}if(w){q(H("demo-question","visitor"),{role:"visitor"},Vt(Oe,X));let e=H("demo-answer","bot");q(e,{role:"bot"},i.demoAnswer),n.addEventListener("hashchange",()=>{Je=Fe(),G(),q(e,{role:"bot"},i.demoAnswer)})}function ne(e){if(e.failed=!0,e.li.classList.remove("is-pending"),e.li.classList.add("is-failed"),e.li.querySelector(".pmw-chat__retry"))return;let a=s(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),o=s(t,"button",{type:"button"},i.retry);o.addEventListener("click",()=>{e.failed=!1,e.tries=0,a.remove(),e.li.classList.replace("is-failed","is-pending"),u?oe()?ke(e):$():Se()}),a.append(o),e.li.append(a)}let oe=()=>!!S&&S.readyState===1;function ke(e){if(!(e.failed||!oe())){if(e.tries>=mt)return ne(e);e.tries++,S.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function $(){if(!u||z||S)return;n.clearTimeout(we);let e=n.location.protocol==="https:"?"wss:":"ws:",a=new URLSearchParams({c:u.c,k:u.k}),o=new m(`${e}//${n.location.host}/api/chat/ws?${a}`);S=o,o.onopen=()=>{fe=0,se&&(ie(),ye(ut)),M(""),o.send(JSON.stringify({type:"hello",since:te}));for(let c of R.values())ke(c)},o.onmessage=c=>{let h;try{h=JSON.parse(c.data)}catch{return}yt(h)},o.onclose=c=>{S===o&&(S=null);for(let x of R.values())x.tries>=mt&&ne(x);if(c.code>=4e3&&c.code<5e3)return vt(c.code);if(z)return;ie(),M(i.reconnecting);let h=ht[Math.min(fe,ht.length-1)];fe++,we=n.setTimeout($,h)}}function vt(e){if(e===pt.rpc)return re(i.reload,!1);re(i.ended,!0)}function re(e,a){z=!0,n.clearTimeout(we),M(""),He.textContent=e,ee.hidden=!a,Z.hidden=!1,g.hidden=!0;for(let o of R.values())ne(o);k()}function We(e){j=e.mode==="human"?"human":"bot",P=typeof e.ownerName=="string"?e.ownerName:null,G(),j==="human"?Ve(null,P):ve(null),e.closed&&re(i.ended,!0)}function yt(e){switch(e?.type){case"history":typeof e.ownerName=="string"&&(P=e.ownerName);for(let a of e.messages??[])Ye(a,{announce:!1});return We(e);case"message":return Ye(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return xt(e.id,e.text);case"typing":return e.role==="visitor"?void 0:ye(ut);case"status":return We(e);case"fallback":return tt(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Ze();case"error":(e.code==="closed"||e.code==="deleted")&&re(i.ended,!0)}}let Qe=!1;function Ze(){if(!(Qe||u?.lead)){Qe=!0,u&&(u.lead=!0,Ue());try{n.pmw?.event(It)}catch{}}}async function et(e,a){let o="";try{o=await f.token(e)}catch{o=""}let c,h=null;try{c=await p("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:o,lang:X,page:n.location.pathname,...a})}),h=await c.json()}catch{c=null}try{f.reset(e)}catch{}return c?.ok&&h?{ok:!0,data:h}:{ok:!1,error:c?.status===403?i.turnstile:c?.status===429?i.tooMany:null,cap:c?.status===409&&h?.error==="cap"}}let se=null,ie=()=>{n.clearTimeout(se),se=null};function Se(){return J||u||(M(""),ye(),se=n.setTimeout(()=>M(i.oneMoment),Ut),J=et(g,{}).then(e=>{if(J=null,e.ok&&e.data.conversationId&&e.data.visitorKey){u={c:e.data.conversationId,k:e.data.visitorKey,botName:typeof e.data.botName=="string"?e.data.botName:null},Ue(),G(),$();return}ie(),K();for(let a of R.values())ne(a);if(e.error)return M(e.error);M(""),tt(e.cap?i.fallbackCap:i.fallbackDown)})),J}function tt(e){$e.textContent=e,y.hidden=!1,g.hidden=!0,y.querySelector("input")?.focus()}y.addEventListener("submit",async e=>{e.preventDefault();let a=h=>y.elements[h].value.trim(),o={name:a("name"),contact:a("contact"),need:a("need")};if(!o.name||!o.contact||!o.need){B.textContent=i.fallbackMissing;return}B.textContent="";let c=await et(y,{fallbackLead:o});if(!c.ok){B.textContent=c.error??i.unavailable;return}y.replaceChildren(s(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Ze()});let kt={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function St(e){let a,o;try{a=await v(e),o=await T(a)}catch{I.textContent=i.photoUnreadable;return}let c=E(),h=H(c,"visitor");h.classList.add("is-pending");let x=s(t,"img",{class:"pmw-chat__photo",src:o,alt:i.photoAlt});x.addEventListener("load",()=>k(),{once:!0}),h.insertBefore(x,h.lastChild),q(h,{role:"visitor"},""),k(!0),u||await Se();let D=null;if(u){let Ct=new URLSearchParams({c:u.c,k:u.k,id:c});try{D=await p(`/api/chat/photo?${Ct}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:a})}catch{D=null}}if(D?.ok){h.classList.remove("is-pending"),ae.add(c),S||$();return}h.remove(),I.textContent=i[kt[D?.status]]??i.photoFailed}U?.addEventListener("change",async()=>{let e=[...U.files??[]];if(U.value="",I.textContent="",z||w||e.length===0)return;let a=zt-ae.size-be;e.length>a&&(I.textContent=i.photoLimit);let o=e.slice(0,Math.max(0,a));be+=o.length;for(let c of o)try{await St(c)}finally{be--}});function at(){let e=b.value.trim();if(!e||z||w)return;b.value="",nt();let a={id:E(),text:e.slice(0,2e3),tries:0,failed:!1};a.li=H(a.id,"visitor"),a.li.classList.add("is-pending"),q(a.li,{role:"visitor"},a.text),R.set(a.id,a),k(!0),u?oe()?ke(a):$():Se()}g.addEventListener("submit",e=>{e.preventDefault(),at()}),b.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),at())});let nt=()=>{b.style.height="auto",b.style.height=b.value?`${Math.min(b.scrollHeight,132)}px`:""};b.addEventListener("input",()=>{nt();let e=Date.now();oe()&&e-Be>Ft&&(Be=e,S.send(JSON.stringify({type:"typing"})))}),ee.addEventListener("click",()=>{u=null;try{n.sessionStorage.removeItem(Ae)}catch{}try{S?.close(1e3)}catch{}S=null,z=!1,te=0,ge.clear(),R.clear(),ae.clear(),I.textContent="",j="bot",P=null,G();for(let e of[..._.querySelectorAll("li[data-id], .pmw-chat__relay, .pmw-chat__handback")])e.remove();K(),ie(),Z.hidden=!0,g.hidden=!1,b.focus()});let Tt=()=>[...A.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));A.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),Te();return}if(e.key!=="Tab")return;let a=Tt();if(a.length===0)return;let o=a[0],c=a[a.length-1];e.shiftKey&&t.activeElement===o?(e.preventDefault(),c.focus()):!e.shiftKey&&t.activeElement===c&&(e.preventDefault(),o.focus())}),W.addEventListener("click",()=>Te());function Lt(){w||f.load(),A.hidden=!1,r.setAttribute("data-open",""),r.removeAttribute("data-unread"),u&&$(),(w?W:g.hidden?y.hidden?ee:y.querySelector("input"):b)?.focus(),k(!0,!0)}function Te(){A.hidden=!0,r.removeAttribute("data-open"),ft?.focus()}return w||Le(r,()=>f.load(),{win:n,settleMs:Dt}),r.pmwChat={open:Lt,close:Te,panel:A},r.pmwChat}export{ht as BACKOFF_MS,It as GOAL,zt as MAX_PHOTOS,mt as MAX_TRIES,Ht as PHOTO_EDGE,$t as PHOTO_QUALITY,Bt as PIN_SLACK,Ae as SESSION_KEY,Ut as SLOW_START_MS,Dt as WARM_MS,Vt as demoQuestion,la as mountChat};
|
package/manifest.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"sms": { "pattern": "^voipms$", "optional": true },
|
|
16
16
|
"photos": { "type": "boolean", "optional": true },
|
|
17
17
|
"avatar": { "pattern": "^(?:photos-[1-5]|illustration-[1-5]|sans-visage-[1-5]|logo)$" },
|
|
18
|
-
"avatarBg": { "pattern": "^#[0-9a-f]{6}$", "optional": true }
|
|
18
|
+
"avatarBg": { "pattern": "^#[0-9a-f]{6}$", "optional": true },
|
|
19
|
+
"contextCollections": { "pattern": "^[a-z][a-z0-9-]*(?:,[a-z][a-z0-9-]*)*$", "optional": true }
|
|
19
20
|
},
|
|
20
21
|
"bindings": [
|
|
21
22
|
{ "kind": "service", "binding": "CHAT", "service": "pmeweb-chat", "entrypoint": "ChatApi" },
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
},
|
|
34
35
|
"markup": { "pages": "all", "required": ["#pmw-chat", "[data-pmw-chat-opener]"] },
|
|
35
36
|
"build": ["plugins/chat/context.json"],
|
|
37
|
+
"context": { "file": "src/chat/context.{lang}.md", "maxChars": 16000 },
|
|
36
38
|
"portal": {
|
|
37
39
|
"screens": [
|
|
38
40
|
{ "key": "appearance",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/plugin-chat",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "PmeWeb chat plugin: the bubble, the lazy chat panel, the site Worker's /api/chat routes and the build-time context file. The conversations, the model and the alerts live in the pmeweb-chat service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/build.d.ts
CHANGED
|
@@ -1,30 +1,45 @@
|
|
|
1
1
|
export declare const CONTEXT_FORMAT: 1
|
|
2
2
|
export declare const MAX_CONTEXT_BYTES: number
|
|
3
|
+
/** The curated file's budget per language, stamp line excluded. */
|
|
4
|
+
export declare const MAX_CONTEXT_CHARS: number
|
|
5
|
+
export declare const SUMMARY_CHARS: number
|
|
6
|
+
export declare const SUMMARY_FIELDS: readonly string[]
|
|
7
|
+
/** blog and authors: never in the context. */
|
|
8
|
+
export declare const NEVER_COLLECTIONS: readonly string[]
|
|
9
|
+
|
|
10
|
+
type Localized = string | Partial<Record<'fr' | 'en', string>>
|
|
3
11
|
|
|
4
12
|
export interface ChatContextLang {
|
|
5
13
|
/** The business's name in this language (the bot says it; pmeweb-chat falls back to the host). */
|
|
6
14
|
business?: string
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
/** The pricing text (the source
|
|
15
|
+
/** The text of src/chat/context.<lang>.md (the stamp line is removed here). Required. */
|
|
16
|
+
context: string
|
|
17
|
+
/** The pricing text (the site's pricing source, public/pricing.md on most sites). */
|
|
10
18
|
pricing?: string
|
|
11
|
-
/** src/i18n/<lang>.ts: flattened to `key: value` lines, strings only. */
|
|
12
|
-
dictionary?: object
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface ChatContextEntry {
|
|
22
|
+
/** A collection listed in plugins.chat.contextCollections; never blog or authors. */
|
|
16
23
|
collection: string
|
|
24
|
+
/** The section's title (default: the collection's name). */
|
|
25
|
+
label?: Localized
|
|
17
26
|
/** Absent: the entry belongs to every language. */
|
|
18
27
|
lang?: 'fr' | 'en'
|
|
19
28
|
title: string
|
|
29
|
+
/** The entry's one line; absent: its summary-like field (SUMMARY_FIELDS), else its body. Cut at 200 characters. */
|
|
30
|
+
summary?: Localized
|
|
20
31
|
data?: Record<string, unknown>
|
|
21
32
|
body?: string
|
|
22
33
|
}
|
|
23
34
|
|
|
24
|
-
/** The JSON text of plugins/chat/context.json. Deterministic; throws
|
|
35
|
+
/** The JSON text of plugins/chat/context.json. Deterministic; throws on a missing or oversized curated file. */
|
|
25
36
|
export declare function chatContext(sources: {
|
|
26
37
|
langs: Partial<Record<'fr' | 'en', ChatContextLang>>
|
|
27
38
|
collections?: ChatContextEntry[]
|
|
28
39
|
}): string
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
/** `plugins.chat.contextCollections` as a list of collection names; throws on blog or authors. */
|
|
42
|
+
export declare function contextCollections(value: unknown): string[]
|
|
43
|
+
export declare function entrySummary(entry: ChatContextEntry, lang: 'fr' | 'en'): string
|
|
44
|
+
export declare function stripStamp(md: string): string
|
|
45
|
+
export declare function contextSections(md: string, fallbackTitle: string): { title: string; text: string }[]
|