@pmeweb/plugin-chat 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INSTALL.md CHANGED
@@ -33,11 +33,18 @@ package's, approved once.
33
33
 
34
34
  ```jsonc
35
35
  "services": [{ "binding": "CHAT", "service": "pmeweb-chat", "entrypoint": "ChatApi" }],
36
- "ratelimits": [{ "name": "CHAT_LIMIT", "namespace_id": "<unused on the account>", "simple": { "limit": 20, "period": 60 } }]
36
+ "ratelimits": [
37
+ { "name": "CHAT_LIMIT", "namespace_id": "<unused on the account>", "simple": { "limit": 20, "period": 60 } },
38
+ { "name": "CHAT_START_LIMIT", "namespace_id": "<another unused on the account>", "simple": { "limit": 3, "period": 60 } }
39
+ ]
37
40
  ```
38
41
 
39
- The limit counts conversation starts, socket (re)connections and photo uploads per IP, not messages
40
- (pmeweb-chat caps turns per conversation and the day's spend).
42
+ Both count per IP, not messages. `CHAT_START_LIMIT` counts conversation starts (3 a minute);
43
+ `CHAT_LIMIT` counts socket (re)connections, photo uploads and the contact form's posts. pmeweb-chat
44
+ caps the rest: 15 visitor messages per conversation, 10 conversations per visitor IP per Quebec
45
+ day (it receives the IP from `handleChat` in `x-pmw-visitor-ip`), and the site's daily spend.
46
+ **Updating from 1.0.0**: add the `CHAT_START_LIMIT` entry and `CHAT_START_LIMIT: RateLimit` to
47
+ `Env` (check-plugins refuses the update without it); nothing else changes on the site.
41
48
  4. [ASK] `npx wrangler secret put TURNSTILE_SECRET` (skip when the contact form already set it;
42
49
  `npx wrangler secret list` to check).
43
50
  5. **Worker** (`worker/index.ts`), before `pluginArtifacts` and the assets:
@@ -58,7 +65,7 @@ package's, approved once.
58
65
  CSP: `mergeCsp(policy, chatCsp())` (Turnstile's script and frame; the socket is same-origin).
59
66
  With `"photos": true` the panel's thumbnail is a `data:` URL: `img-src` keeps `data:` (the
60
67
  boilerplate's does).
61
- `Env` gains `CHAT: Fetcher` and `CHAT_LIMIT: RateLimit`.
68
+ `Env` gains `CHAT: Fetcher`, `CHAT_LIMIT: RateLimit` and `CHAT_START_LIMIT: RateLimit`.
62
69
  6. **Openers**, written by Claude Code at install, in every dictionary (`src/i18n/<lang>.ts`),
63
70
  strings only, OQLF typography, `docs/AVOIDING_AI_TELLS.md`:
64
71
 
@@ -115,7 +122,9 @@ package's, approved once.
115
122
  11. `npm run build && node ../../a11y-tools/check-plugins.mjs .` until PASS; `npm run check`.
116
123
  12. **Portal** (« Modules › Assistant virtuel »): PmeWeb sets `alertEmails` and `dailyCap`
117
124
  (`alertEmails` is needed before the switch goes on: pmeweb-chat refuses a site without it), `alertPhone` if texts are on, and
118
- « Consignes » with the client; the client may set `botName` and `ownerName`.
125
+ « Consignes » with the client; the client may set `ownerName`, and changes the face, the icon's
126
+ colour and the name (`botName`) in « Contenu › Modules › Assistant virtuel › Apparence »
127
+ (applied by « Publier »).
119
128
  13. Deploy (still off: nothing on the site, health `disabled`). Once the client or the operator
120
129
  switches it on and publishes, from a real host: the bubble shows, a question gets an answer, the signed health
121
130
  route answers `chat.ok`.
package/README.md CHANGED
@@ -26,8 +26,16 @@ Install it with the `install-plugin` skill (`INSTALL.md`).
26
26
  sur vos toitures? », anything else « Quels sont vos délais? »), and « (Démonstration) Ici,
27
27
  l’assistant répond à partir du contenu de votre site. ». The composer is disabled under « Aperçu :
28
28
  la conversation est désactivée. ». No `/api/chat/*` call, no Turnstile, no socket, no lead; the
29
- routes still answer 403 `disabled` there, so nobody spends the client's cap from « Contenu ». The
30
- header shows « Assistant virtuel »: the portal's `botName` is only known to pmeweb-chat.
29
+ routes still answer 403 `disabled` there, so nobody spends the client's cap from « Contenu ». No
30
+ opener is scheduled there.
31
+ - **The preview's fragment** (off the real hosts only, read on load and on `hashchange`):
32
+ `#pmw-chat=panel` opens the demo's panel, `#pmw-chat=bubble` closes it,
33
+ `pmw-chat-botName=<url-encoded>` names the assistant (absent: « Assistant virtuel » alone). The
34
+ portal's « Apparence » screen drives it without a rebuild.
35
+ - **« Apparence »** (« Contenu › Modules › Assistant virtuel › Apparence »): the client picks the
36
+ face (`avatar`: photos, illustration or a « Sans visage » icon), the icon's colour (`avatarBg`,
37
+ default the site's accent) and the name (`botName`, which ships with « Publier »). `logo` stays
38
+ PmeWeb's, set at install.
31
39
  - **Nothing loads with the page.** The bubble is HTML and CSS; the panel, Turnstile and the socket
32
40
  arrive when the visitor presses, hovers, focuses or types on it.
33
41
  - **Openers.** One line per page, from the site's dictionary (`chat.openers`, `chat.defaultOpener`).
@@ -38,8 +46,14 @@ Install it with the `install-plugin` skill (`INSTALL.md`).
38
46
  - **The socket.** A drop (1006, a forced deploy of pmeweb-chat) reconnects with backoff 1 to 30 s
39
47
  and catches up from the last message seen; a message typed meanwhile is queued and sent on open,
40
48
  and after 3 sends without an echo shows « Non envoyé, Réessayer ». A 44xx close never reconnects.
41
- - **Fallback.** Daily cap reached, the model down, or the start refused: a three-field contact
42
- form that posts to `/api/chat/start` with `fallbackLead`.
49
+ - **Fallback.** Daily cap reached, 15 visitor messages in the conversation, the visitor's 10
50
+ conversations of the day used up (pmeweb-chat answers the start 409 `cap`), the model down, or
51
+ the start refused: a three-field contact form that posts to `/api/chat/start` with `fallbackLead`
52
+ (never counted against the visitor's 10, and on the looser `CHAT_LIMIT`).
53
+ - **Limits.** Per IP: `CHAT_START_LIMIT` (3 conversation starts a minute) and `CHAT_LIMIT` (20 a
54
+ minute: socket reconnections, photos, the contact form). The site Worker hands the visitor's IP
55
+ to pmeweb-chat in `x-pmw-visitor-ip` (1.1.0); pmeweb-chat keeps only a daily hash of it to count
56
+ conversations per day. Health reports `limit` and `startLimit`.
43
57
  - **Photos** (`"photos": true`). Up to 3 per conversation, shrunk in the browser to a 1600 px JPEG
44
58
  (the canvas drops the EXIF, GPS included), posted to `/api/chat/photo` (rate-limited like a
45
59
  message, 5 MB cap), re-encoded by pmeweb-chat and kept in a private bucket. The visitor sees the
@@ -1 +1 @@
1
- (()=>{var T=["pointerdown","pointerenter","focusin","keydown"];function L(s,n,{events:i=T}={}){let c=!1,t=()=>{if(!c){c=!0;for(let r of i)s.removeEventListener(r,t);n()}};for(let r of i)s.addEventListener(r,t,{passive:!0});return{run:t}}var g="pmw-chat-opener",x=2e4,b=.3;function d({doc:s=document,win:n=window,importLazy:i=t=>import(t),onInteraction:c=L}={}){let t=s.getElementById("pmw-chat");if(!t)return null;t.hidden=!1;let r=t.querySelector(".pmw-chat__teaser"),p=null,l=()=>p??(p=i(t.dataset.pmwLazy).then(e=>e.mountChat(t),e=>{throw p=null,e}));c(t,()=>l().catch(()=>{}));let m=n.sessionStorage,f=!1;try{f=!!m?.getItem(g)}catch{}let h=[],E=(e,o,a)=>{e.addEventListener(o,a,{passive:!0}),h.push([e,o,a])},v,y=()=>{f=!0;try{m?.setItem(g,"1")}catch{}n.clearTimeout(v);for(let[e,o,a]of h)e.removeEventListener(o,a)},u=()=>{f||!r||t.hasAttribute("data-open")||(y(),r.hidden=!1)};return t.addEventListener("click",e=>{let o=e.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");o&&(r&&(r.hidden=!0),y(),o.hasAttribute("data-pmw-chat-open")&&l().then(a=>a.open(),()=>{}))}),!f&&r&&(v=n.setTimeout(u,x),E(n,"scroll",()=>{let e=s.documentElement.scrollHeight-n.innerHeight;e>0&&n.scrollY/e>=b&&u()}),n.matchMedia?.("(pointer: fine)").matches&&E(s,"mouseout",e=>{!e.relatedTarget&&e.clientY<=0&&u()})),{load:l,show:u}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>d()):d();})();
1
+ (()=>{function b({doc:s=document,hostname:n=globalThis.location?.hostname??""}={}){let c=s.getElementById("pmw-hosts");if(!c)return!1;let a;try{a=JSON.parse(c.textContent??"")}catch{return!1}if(!Array.isArray(a))return!1;let i=String(n).toLowerCase();return a.some(t=>typeof t=="string"&&t.toLowerCase()===i)}var S=["pointerdown","pointerenter","focusin","keydown"];function T(s,n,{events:c=S}={}){let a=!1,i=()=>{if(!a){a=!0;for(let t of c)s.removeEventListener(t,i);n()}};for(let t of c)s.addEventListener(t,i,{passive:!0});return{run:i}}var x="pmw-chat-opener",O=2e4,w=.3;function m({doc:s=document,win:n=window,hostname:c=n.location.hostname,importLazy:a=t=>import(t),onInteraction:i=T}={}){let t=s.getElementById("pmw-chat");if(!t)return null;t.hidden=!1;let f=t.querySelector(".pmw-chat__teaser"),l=null,u=()=>l??(l=a(t.dataset.pmwLazy).then(e=>e.mountChat(t),e=>{throw l=null,e}));i(t,()=>u().catch(()=>{}));let h=b({doc:s,hostname:c});if(!h){let e=()=>{let o=new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat");o==="panel"&&!t.hasAttribute("data-open")?u().then(r=>r.open(),()=>{}):o==="bubble"&&l&&l.then(r=>r.close(),()=>{})};e(),n.addEventListener("hashchange",e)}let E=n.sessionStorage,d=!1;try{d=!!E?.getItem(x)}catch{}let v=[],y=(e,o,r)=>{e.addEventListener(o,r,{passive:!0}),v.push([e,o,r])},L,g=()=>{d=!0;try{E?.setItem(x,"1")}catch{}n.clearTimeout(L);for(let[e,o,r]of v)e.removeEventListener(o,r)},p=()=>{d||!f||t.hasAttribute("data-open")||(g(),f.hidden=!1)};return t.addEventListener("click",e=>{let o=e.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");o&&(f&&(f.hidden=!0),g(),o.hasAttribute("data-pmw-chat-open")&&u().then(r=>r.open(),()=>{}))}),!d&&f&&h&&(L=n.setTimeout(p,O),y(n,"scroll",()=>{let e=s.documentElement.scrollHeight-n.innerHeight;e>0&&n.scrollY/e>=w&&p()}),n.matchMedia?.("(pointer: fine)").matches&&y(s,"mouseout",e=>{!e.relatedTarget&&e.clientY<=0&&p()})),{load:u,show:p}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>m()):m();})();
package/dist/widget.js CHANGED
@@ -1,2 +1,2 @@
1
- var He=`.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 - 32px));border:1px solid var(--pmw-chat-border, #c9ced6);border-radius:12px;overflow:hidden;box-shadow:0 12px 36px #00000038}}.pmw-chat__panel button{font:inherit;cursor:pointer}.pmw-chat__panel :focus-visible{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__head :focus-visible{outline-color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__head{display:flex;align-items:center;gap:10px;padding:10px 8px 10px 12px;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__head .pmw-chat__avatar{width:40px;height:40px;box-shadow:none;background:var(--pmw-chat-avatar-bg, var(--pmw-chat-accent-fg, #ffffff));color:var(--pmw-chat-avatar-fg, var(--pmw-chat-accent, #0a1b3d))}.pmw-chat__head .pmw-chat__avatar img{width:40px;height:40px}.pmw-chat__who{flex:1;min-width:0}.pmw-chat__name{margin:0;font-weight:600;font-size:16px}.pmw-chat__role{margin:0;font-size:14px}.pmw-chat__dot{flex:none;width:10px;height:10px;border-radius:50%;background:var(--pmw-chat-live, #3fb950);box-shadow:0 0 0 2px var(--pmw-chat-accent, #0a1b3d)}.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:none;color:inherit}.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:10px 12px}.pmw-chat__notice{margin:0 0 8px;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__notice a{color:var(--pmw-chat-link, #0a1b3d);text-decoration:underline;text-underline-offset:2px}.pmw-chat__log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.pmw-chat__msg{max-width:85%;padding:8px 12px;border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}.pmw-chat__msg--bot,.pmw-chat__msg--owner{align-self:flex-start;background:var(--pmw-chat-other-bg, #f1f3f6);color:var(--pmw-chat-fg, #1f2328);border-bottom-left-radius:4px}.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:4px}.pmw-chat__msg.is-pending{opacity:.75}.pmw-chat__msg.is-failed{outline:2px solid var(--pmw-chat-error, #b42318)}.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:4px;padding:12px;border-radius:12px;background:var(--pmw-chat-other-bg, #f1f3f6)}.pmw-chat__typing span{width:6px;height:6px;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__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:12px}.pmw-chat__ended p{margin:0 0 8px}.pmw-chat__composer{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;padding:10px 12px;border-top:1px solid var(--pmw-chat-border, #c9ced6)}.pmw-chat__composer textarea{flex:1;min-width:0;resize:none}.pmw-chat__composer textarea,.pmw-chat__field input,.pmw-chat__field textarea{font:inherit;font-size:16px;padding:8px 10px;border-radius:8px;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__send{min-height:44px;padding:8px 16px;border:0;border-radius:8px;font-weight:600;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__send:disabled,.pmw-chat__composer textarea:disabled{cursor:not-allowed;opacity:.6}.pmw-chat__restart{min-height:44px;padding:8px 16px;border-radius:8px;border:1px solid var(--pmw-chat-link, #0a1b3d);background:none;color:var(--pmw-chat-link, #0a1b3d);font-weight:600}.pmw-chat__composer .pmw-turnstile,.pmw-chat__fallback .pmw-turnstile{flex-basis:100%}.pmw-chat__attach{flex:none;position:relative;width:44px;height:44px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;border:1px solid var(--pmw-chat-field-border, #7d8590);color:var(--pmw-chat-field-fg, #1f2328);background:#fff}.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{flex-basis:100%;margin:0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__composer .pmw-chat__photo-error{flex-basis:100%}.pmw-chat__photo{display:block;max-width:100%;max-height:220px;border-radius:8px}.pmw-chat__fallback{display:flex;flex-direction:column;gap:10px;margin-top:12px}.pmw-chat__fb-title{margin:0;font-weight:600}.pmw-chat__fb-intro{margin:0;font-size:15px}.pmw-chat__field{display:flex;flex-direction:column;gap:4px}.pmw-chat__field label{font-size:14px;font-weight:600}.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 De=new Set;function Ue(o,{doc:c=document,attrs:r={}}={}){if(De.has(o)||(De.add(o),c.querySelector(`script[src="${o}"]`)))return;let t=c.createElement("script");t.src=o,t.async=!0;for(let[d,h]of Object.entries(r))t.setAttribute(d,h);c.head.append(t)}function Be({doc:o=document,hostname:c=globalThis.location?.hostname??""}={}){let r=o.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(c).toLowerCase();return t.some(h=>typeof h=="string"&&h.toLowerCase()===d)}var it="https://challenges.cloudflare.com/turnstile/v0/api.js";var ct=15e3,$e=200;function fe(o=document){Ue(it,{doc:o})}var lt=o=>o.querySelector('input[name="cf-turnstile-response"]')?.value??"",pt=o=>{let c=o.querySelector(".cf-turnstile");return!!c&&c.offsetHeight>0};async function je(o,{win:c=window,timeoutMs:r=ct}={}){typeof o.pmwTurnstileLoad=="function"?o.pmwTurnstileLoad():fe(c.document);let t=0;for(;lt(o)==="";){if(!pt(o)){if(t>=r)return!1;t+=$e}await new Promise(d=>c.setTimeout(d,$e))}return!0}function Fe(o,{win:c=window}={}){let r=o.querySelector(".cf-turnstile");if(!r||r.offsetHeight>0)return!1;try{return c.turnstile?.reset(r),!0}catch{return!1}}var Je={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var dt={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",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:"Le clavardage a \xE9t\xE9 mis \xE0 jour. Rechargez la page pour continuer.",unavailable:"Le clavardage n\u2019est pas disponible pour le moment.",turnstile:"La v\xE9rification anti-robot n\u2019a pas abouti. R\xE9essayez.",tooMany:"Trop de messages en peu de temps. R\xE9essayez dans une minute.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"L\u2019assistant ne peut plus r\xE9pondre aujourd\u2019hui. Laissez vos coordonn\xE9es et on vous recontactera.",fallbackDown:"L\u2019assistant ne peut pas r\xE9pondre pour le moment. Laissez vos coordonn\xE9es et on vous recontactera.",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",connecting:"Connecting\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"The chat was updated. Reload the page to continue.",unavailable:"The chat is not available right now.",turnstile:"The anti-robot check did not go through. Try again.",tooMany:"Too many messages in a short time. Try again in a minute.",fallbackTitle:"Leave us your details",fallbackCap:"The assistant cannot answer any more today. Leave your details and we will get back to you.",fallbackDown:"The assistant cannot answer right now. 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."}},Q=o=>String(o??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function me(o,c={}){let r=dt[Q(o)],t={...r};for(let[d,h]of Object.entries(c??{}))typeof h=="string"&&d in r&&(t[d]=h);return t}var W=(o,c)=>o.replace(/\{(\w+)\}/g,(r,t)=>t in c?String(c[t]):r);var we="pmw-chat",Ke=[1e3,2e3,4e3,8e3,16e3,3e4],Xe=3,ut="chat-lead",ht=3,ft=1600,mt=.85,wt=3e3,gt=8e3,_t='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 8h3l2-3h6l2 3h3v11H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><circle cx="12" cy="13" r="3.5" fill="none" stroke="currentColor" stroke-width="2"/></svg>';async function bt(o,c){let r=await o.createImageBitmap(c,{imageOrientation:"from-image"}),t=Math.min(1,ft/Math.max(r.width,r.height)),d=Math.max(1,Math.round(r.width*t)),h=Math.max(1,Math.round(r.height*t)),f=o.document.createElement("canvas");return f.width=d,f.height=h,f.getContext("2d").drawImage(r,0,0,d,h),r.close?.(),new Promise((q,Z)=>f.toBlob(I=>I?q(I):Z(new Error("encode")),"image/jpeg",mt))}var xt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},vt={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function yt(o,c){let r=Q(c),t=me(r),d=String(o??"").trim().match(vt[r]);if(!d)return t.demoQuestion;let h=d[1].replace(/[\p{L}’']+/gu,f=>xt[r][f.toLowerCase()]??f);return W(t.demoAsk,{topic:h})}var St='<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(o,c,r={},t){let d=o.createElement(c);for(let[h,f]of Object.entries(r))f!=null&&f!==!1&&d.setAttribute(h,f===!0?"":f);return t!==void 0&&(d.textContent=t),d}var kt=o=>({load:()=>fe(o.document),async token(c){return await je(c,{win:o}),c.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset:c=>Fe(c,{win:o})});function Ht(o,c={}){if(o.pmwChat)return o.pmwChat;let r=c.win??window,t=r.document,d=c.WebSocket??r.WebSocket,h=c.fetch??((...e)=>r.fetch(...e)),f=c.turnstile??kt(r),q=c.uuid??(()=>r.crypto.randomUUID()),Z=c.shrink??(e=>bt(r,e)),I=c.preview??(e=>new Promise((n,a)=>{let l=new r.FileReader;l.onload=()=>n(String(l.result)),l.onerror=()=>a(l.error),l.readAsDataURL(e)})),v={};try{v=JSON.parse(o.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let z=Q(v.lang),i=me(z,v.strings),ge=o.dataset.pmwChatOpener??"",Ye=o.querySelector(".pmw-chat__bubble"),w=c.demo??(v.demo===!0||!Be({doc:t,hostname:r.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(s(t,"style",{id:"pmw-chat-style"},He));let b=s(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),_e=s(t,"div",{class:"pmw-chat__head"}),ee=o.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??s(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of ee.querySelectorAll("[id]")){let n=e.id;e.id=`${n}-head`;for(let a of ee.querySelectorAll(`[mask="url(#${n})"]`))a.setAttribute("mask",`url(#${n}-head)`)}let be=s(t,"div",{class:"pmw-chat__who"}),xe=s(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),H=s(t,"p",{class:"pmw-chat__role"}),ve=s(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),D=s(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});D.innerHTML=St,be.append(xe,H),_e.append(ee,be,ve,D);let te=s(t,"p",{class:"pmw-chat__notice"},i.automated);v.privacyHref&&(te.append(" "),te.append(s(t,"a",{href:v.privacyHref},i.privacy)));let x=s(t,"ol",{class:"pmw-chat__log"}),Ge=s(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},ge),L=s(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});L.append(s(t,"span"),s(t,"span"),s(t,"span")),x.append(Ge);let ne=s(t,"p",{class:"pmw-chat__state",role:"status"}),ye=s(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Se=e=>s(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":v.siteKey??"","data-action":"chat","data-theme":"light","data-language":z,"data-appearance":"interaction-only","data-size":"flexible"}),m=s(t,"form",{class:"pmw-chat__composer",novalidate:!0}),y=s(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"2",maxlength:String(2e3),autocomplete:"off",disabled:w,"aria-describedby":w?"pmw-chat-demo-hint":null}),Qe=s(t,"button",{type:"submit",class:"pmw-chat__send",disabled:w},i.send);m.append(s(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),y,Qe),w?m.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint)):m.append(Se(m));let P=null,C=s(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(v.photos===!0){let e=s(t,"label",{class:"pmw-chat__attach"});P=s(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:w}),e.innerHTML=_t,e.prepend(P),e.append(s(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),m.insertBefore(e,y),m.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),C)}let U=s(t,"div",{class:"pmw-chat__ended",hidden:!0}),ke=s(t,"p",{},i.ended),B=s(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);U.append(ke,B);let g=s(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),Te=s(t,"p",{class:"pmw-chat__fb-intro"}),ae=(e,n,a)=>{let l=`pmw-chat-fb-${e}`,p=s(t,"div",{class:"pmw-chat__field"});return p.append(s(t,"label",{for:l},i[e]),s(t,n,{id:l,name:e,required:!0,...a})),p},$=s(t,"p",{class:"pmw-chat__error",role:"alert"});g.append(s(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),Te,ae("name","input",{autocomplete:"name",maxlength:"100"}),ae("contact","input",{autocomplete:"on",maxlength:"200"}),ae("need","textarea",{rows:"3",maxlength:"2000"}),$,s(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),w||g.append(Se(g));let Le=s(t,"div",{class:"pmw-chat__body"});Le.append(te,x,ne,U,g),b.append(_e,Le,m,ye),o.append(b);let u=null;if(!w)try{u=JSON.parse(r.sessionStorage.getItem(we)??"null")}catch{u=null}let Ce=()=>{try{r.sessionStorage.setItem(we,JSON.stringify(u))}catch{}},_=null,oe=0,re=null,R=null,j=0,F="bot",J=null,E=!1,Ee=0,se=null,ie=new Set,k=new Map,V=new Set,ce=0,le=()=>u?.botName||null;function K(){let e=F==="human";xe.textContent=e?J||i.assistant:le()||i.assistant,H.textContent=e?i.live:le()?i.assistant:"",H.hidden=H.textContent==="",ve.hidden=!e,b.dataset.mode=F}K();let T=e=>{ne.textContent=e??"",ne.hidden=!e};T("");let Ae=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||J||i.assistant:le()||i.assistant;function A(e,n){let a=s(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${n}`,"data-id":e}),l=s(t,"span",{class:"pmw-chat__sr"}),p=s(t,"span",{class:"pmw-chat__text"});return a.append(l,p),x.insertBefore(a,L.parentNode===x?L:null),a}let Oe=e=>[...x.querySelectorAll("li[data-id]")].find(n=>n.dataset.id===e),O=(e,n,a)=>{e.firstChild.textContent=W(i.said,{name:Ae(n),text:""}),e.lastChild.textContent=a===""&&n.photos?.length&&!e.querySelector("img")?i.photoSent:a};function pe(){L.hidden=!0,r.clearTimeout(se)}function Me(e,{announce:n}){if(ie.has(e.seq))return;ie.add(e.seq),j=Math.max(j,e.seq);let a=Oe(e.id);a||(a=A(e.id,e.role)),a.classList.remove("is-streaming","is-pending","is-failed"),a.querySelector(".pmw-chat__retry")?.remove(),O(a,e,e.text),e.role==="visitor"?(k.delete(e.id),e.photos?.length&&V.add(e.id)):(pe(),n&&(ye.textContent=W(i.said,{name:Ae(e),text:e.text})),b.hidden&&o.setAttribute("data-unread","")),M()}function We(e,n){pe();let a=Oe(e);a||(a=A(e,"bot"),a.classList.add("is-streaming"),O(a,{role:"bot"},"")),a.lastChild.textContent+=n,M()}let M=()=>{x.scrollTop=x.scrollHeight};w&&(O(A("demo-question","visitor"),{role:"visitor"},yt(ge,z)),O(A("demo-answer","bot"),{role:"bot"},i.demoAnswer));function X(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=s(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),a=s(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"),u?Y()?de(e):N():ue()}),n.append(a),e.li.append(n)}let Y=()=>!!_&&_.readyState===1;function de(e){if(!(e.failed||!Y())){if(e.tries>=Xe)return X(e);e.tries++,_.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function N(){if(!u||E||_)return;r.clearTimeout(re);let e=r.location.protocol==="https:"?"wss:":"ws:",n=new URLSearchParams({c:u.c,k:u.k}),a=new d(`${e}//${r.location.host}/api/chat/ws?${n}`);_=a,a.onopen=()=>{oe=0,T(""),a.send(JSON.stringify({type:"hello",since:j}));for(let l of k.values())de(l)},a.onmessage=l=>{let p;try{p=JSON.parse(l.data)}catch{return}et(p)},a.onclose=l=>{_===a&&(_=null);for(let S of k.values())S.tries>=Xe&&X(S);if(l.code>=4e3&&l.code<5e3)return Ze(l.code);if(E)return;T(i.reconnecting);let p=Ke[Math.min(oe,Ke.length-1)];oe++,re=r.setTimeout(N,p)}}function Ze(e){if(e===Je.rpc)return G(i.reload,!1);G(i.ended,!0)}function G(e,n){E=!0,r.clearTimeout(re),T(""),ke.textContent=e,B.hidden=!n,U.hidden=!1,m.hidden=!0;for(let a of k.values())X(a)}function Ne(e){F=e.mode==="human"?"human":"bot",J=typeof e.ownerName=="string"?e.ownerName:null,K(),e.closed&&G(i.ended,!0)}function et(e){switch(e?.type){case"history":for(let n of e.messages??[])Me(n,{announce:!1});return Ne(e);case"message":return Me(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return We(e.id,e.text);case"typing":return e.role==="visitor"?void 0:(L.hidden=!1,x.append(L),r.clearTimeout(se),se=r.setTimeout(pe,gt),M());case"status":return Ne(e);case"fallback":return Ie(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Re();case"error":(e.code==="closed"||e.code==="deleted")&&G(i.ended,!0)}}let Pe=!1;function Re(){if(!(Pe||u?.lead)){Pe=!0,u&&(u.lead=!0,Ce());try{r.pmw?.event(ut)}catch{}}}async function qe(e,n){let a="";try{a=await f.token(e)}catch{a=""}let l,p=null;try{l=await h("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:a,lang:z,page:r.location.pathname,...n})}),p=await l.json()}catch{l=null}try{f.reset(e)}catch{}return l?.ok&&p?{ok:!0,data:p}:{ok:!1,error:l?.status===403?i.turnstile:l?.status===429?i.tooMany:null}}function ue(){return R||u||(T(i.connecting),R=qe(m,{}).then(e=>{if(R=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},Ce(),K(),N();return}for(let n of k.values())X(n);if(e.error)return T(e.error);T(""),Ie(i.unavailable)})),R}function Ie(e){Te.textContent=e,g.hidden=!1,m.hidden=!0,g.querySelector("input")?.focus()}g.addEventListener("submit",async e=>{e.preventDefault();let n=p=>g.elements[p].value.trim(),a={name:n("name"),contact:n("contact"),need:n("need")};if(!a.name||!a.contact||!a.need){$.textContent=i.fallbackMissing;return}$.textContent="";let l=await qe(g,{fallbackLead:a});if(!l.ok){$.textContent=l.error??i.unavailable;return}g.replaceChildren(s(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Re()});let tt={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function nt(e){let n,a;try{n=await Z(e),a=await I(n)}catch{C.textContent=i.photoUnreadable;return}let l=q(),p=A(l,"visitor");p.classList.add("is-pending"),p.insertBefore(s(t,"img",{class:"pmw-chat__photo",src:a,alt:i.photoAlt}),p.lastChild),O(p,{role:"visitor"},""),M(),u||await ue();let S=null;if(u){let rt=new URLSearchParams({c:u.c,k:u.k,id:l});try{S=await h(`/api/chat/photo?${rt}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:n})}catch{S=null}}if(S?.ok){p.classList.remove("is-pending"),V.add(l),_||N();return}p.remove(),C.textContent=i[tt[S?.status]]??i.photoFailed}P?.addEventListener("change",async()=>{let e=[...P.files??[]];if(P.value="",C.textContent="",E||w||e.length===0)return;let n=ht-V.size-ce;e.length>n&&(C.textContent=i.photoLimit);let a=e.slice(0,Math.max(0,n));ce+=a.length;for(let l of a)try{await nt(l)}finally{ce--}});function ze(){let e=y.value.trim();if(!e||E||w)return;y.value="";let n={id:q(),text:e.slice(0,2e3),tries:0,failed:!1};n.li=A(n.id,"visitor"),n.li.classList.add("is-pending"),O(n.li,{role:"visitor"},n.text),k.set(n.id,n),M(),u?Y()?de(n):N():ue()}m.addEventListener("submit",e=>{e.preventDefault(),ze()}),y.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),ze())}),y.addEventListener("input",()=>{let e=Date.now();Y()&&e-Ee>wt&&(Ee=e,_.send(JSON.stringify({type:"typing"})))}),B.addEventListener("click",()=>{u=null;try{r.sessionStorage.removeItem(we)}catch{}try{_?.close(1e3)}catch{}_=null,E=!1,j=0,ie.clear(),k.clear(),V.clear(),C.textContent="",F="bot",J=null,K();for(let e of[...x.querySelectorAll("li[data-id]")])e.remove();U.hidden=!0,m.hidden=!1,y.focus()});let at=()=>[...b.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));b.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),he();return}if(e.key!=="Tab")return;let n=at();if(n.length===0)return;let a=n[0],l=n[n.length-1];e.shiftKey&&t.activeElement===a?(e.preventDefault(),l.focus()):!e.shiftKey&&t.activeElement===l&&(e.preventDefault(),a.focus())}),D.addEventListener("click",()=>he());function ot(){w||f.load(),b.hidden=!1,o.setAttribute("data-open",""),o.removeAttribute("data-unread"),u&&N(),(w?D:m.hidden?g.hidden?B:g.querySelector("input"):y)?.focus(),M()}function he(){b.hidden=!0,o.removeAttribute("data-open"),Ye?.focus()}return o.pmwChat={open:ot,close:he,panel:b},o.pmwChat}export{Ke as BACKOFF_MS,ut as GOAL,ht as MAX_PHOTOS,Xe as MAX_TRIES,ft as PHOTO_EDGE,mt as PHOTO_QUALITY,we as SESSION_KEY,yt as demoQuestion,Ht as mountChat};
1
+ var Ue=`.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 - 32px));border:1px solid var(--pmw-chat-border, #c9ced6);border-radius:12px;overflow:hidden;box-shadow:0 12px 36px #00000038}}.pmw-chat__panel button{font:inherit;cursor:pointer}.pmw-chat__panel :focus-visible{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__head :focus-visible{outline-color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__head{display:flex;align-items:center;gap:10px;padding:10px 8px 10px 12px;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__head .pmw-chat__avatar{width:40px;height:40px;box-shadow:none;background:var(--pmw-chat-avatar-bg, var(--pmw-chat-accent-fg, #ffffff));color:var(--pmw-chat-avatar-fg, var(--pmw-chat-accent, #0a1b3d))}.pmw-chat__head .pmw-chat__avatar img{width:40px;height:40px}.pmw-chat__who{flex:1;min-width:0}.pmw-chat__name{margin:0;font-weight:600;font-size:16px}.pmw-chat__role{margin:0;font-size:14px}.pmw-chat__dot{flex:none;width:10px;height:10px;border-radius:50%;background:var(--pmw-chat-live, #3fb950);box-shadow:0 0 0 2px var(--pmw-chat-accent, #0a1b3d)}.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:none;color:inherit}.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:10px 12px}.pmw-chat__notice{margin:0 0 8px;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__notice a{color:var(--pmw-chat-link, #0a1b3d);text-decoration:underline;text-underline-offset:2px}.pmw-chat__log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.pmw-chat__msg{max-width:85%;padding:8px 12px;border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}.pmw-chat__msg--bot,.pmw-chat__msg--owner{align-self:flex-start;background:var(--pmw-chat-other-bg, #f1f3f6);color:var(--pmw-chat-fg, #1f2328);border-bottom-left-radius:4px}.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:4px}.pmw-chat__msg.is-pending{opacity:.75}.pmw-chat__msg.is-failed{outline:2px solid var(--pmw-chat-error, #b42318)}.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:4px;padding:12px;border-radius:12px;background:var(--pmw-chat-other-bg, #f1f3f6)}.pmw-chat__typing span{width:6px;height:6px;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__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:12px}.pmw-chat__ended p{margin:0 0 8px}.pmw-chat__composer{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;padding:10px 12px;border-top:1px solid var(--pmw-chat-border, #c9ced6)}.pmw-chat__composer textarea{flex:1;min-width:0;resize:none}.pmw-chat__composer textarea,.pmw-chat__field input,.pmw-chat__field textarea{font:inherit;font-size:16px;padding:8px 10px;border-radius:8px;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__send{min-height:44px;padding:8px 16px;border:0;border-radius:8px;font-weight:600;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__send:disabled,.pmw-chat__composer textarea:disabled{cursor:not-allowed;opacity:.6}.pmw-chat__restart{min-height:44px;padding:8px 16px;border-radius:8px;border:1px solid var(--pmw-chat-link, #0a1b3d);background:none;color:var(--pmw-chat-link, #0a1b3d);font-weight:600}.pmw-chat__composer .pmw-turnstile,.pmw-chat__fallback .pmw-turnstile{flex-basis:100%}.pmw-chat__attach{flex:none;position:relative;width:44px;height:44px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;border:1px solid var(--pmw-chat-field-border, #7d8590);color:var(--pmw-chat-field-fg, #1f2328);background:#fff}.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{flex-basis:100%;margin:0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__composer .pmw-chat__photo-error{flex-basis:100%}.pmw-chat__photo{display:block;max-width:100%;max-height:220px;border-radius:8px}.pmw-chat__fallback{display:flex;flex-direction:column;gap:10px;margin-top:12px}.pmw-chat__fb-title{margin:0;font-weight:600}.pmw-chat__fb-intro{margin:0;font-size:15px}.pmw-chat__field{display:flex;flex-direction:column;gap:4px}.pmw-chat__field label{font-size:14px;font-weight:600}.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 Be=new Set;function $e(r,{doc:c=document,attrs:o={}}={}){if(Be.has(r)||(Be.add(r),c.querySelector(`script[src="${r}"]`)))return;let t=c.createElement("script");t.src=r,t.async=!0;for(let[d,h]of Object.entries(o))t.setAttribute(d,h);c.head.append(t)}function je({doc:r=document,hostname:c=globalThis.location?.hostname??""}={}){let o=r.getElementById("pmw-hosts");if(!o)return!1;let t;try{t=JSON.parse(o.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let d=String(c).toLowerCase();return t.some(h=>typeof h=="string"&&h.toLowerCase()===d)}var lt="https://challenges.cloudflare.com/turnstile/v0/api.js";var pt=15e3,Fe=200;function fe(r=document){$e(lt,{doc:r})}var dt=r=>r.querySelector('input[name="cf-turnstile-response"]')?.value??"",ut=r=>{let c=r.querySelector(".cf-turnstile");return!!c&&c.offsetHeight>0};async function Je(r,{win:c=window,timeoutMs:o=pt}={}){typeof r.pmwTurnstileLoad=="function"?r.pmwTurnstileLoad():fe(c.document);let t=0;for(;dt(r)==="";){if(!ut(r)){if(t>=o)return!1;t+=Fe}await new Promise(d=>c.setTimeout(d,Fe))}return!0}function Ve(r,{win:c=window}={}){let o=r.querySelector(".cf-turnstile");if(!o||o.offsetHeight>0)return!1;try{return c.turnstile?.reset(o),!0}catch{return!1}}var Ke={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var ht={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",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:"Le clavardage a \xE9t\xE9 mis \xE0 jour. Rechargez la page pour continuer.",unavailable:"Le clavardage n\u2019est pas disponible pour le moment.",turnstile:"La v\xE9rification anti-robot n\u2019a pas abouti. R\xE9essayez.",tooMany:"Trop de messages en peu de temps. R\xE9essayez dans une minute.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"L\u2019assistant ne peut plus r\xE9pondre aujourd\u2019hui. Laissez vos coordonn\xE9es et on vous recontactera.",fallbackDown:"L\u2019assistant ne peut pas r\xE9pondre pour le moment. Laissez vos coordonn\xE9es et on vous recontactera.",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",connecting:"Connecting\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"The chat was updated. Reload the page to continue.",unavailable:"The chat is not available right now.",turnstile:"The anti-robot check did not go through. Try again.",tooMany:"Too many messages in a short time. Try again in a minute.",fallbackTitle:"Leave us your details",fallbackCap:"The assistant cannot answer any more today. Leave your details and we will get back to you.",fallbackDown:"The assistant cannot answer right now. 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."}},Q=r=>String(r??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function me(r,c={}){let o=ht[Q(r)],t={...o};for(let[d,h]of Object.entries(c??{}))typeof h=="string"&&d in o&&(t[d]=h);return t}var W=(r,c)=>r.replace(/\{(\w+)\}/g,(o,t)=>t in c?String(c[t]):o);var we="pmw-chat",Ye=[1e3,2e3,4e3,8e3,16e3,3e4],Ge=3,ft="chat-lead",mt=3,wt=1600,gt=.85,_t=3e3,bt=8e3,xt='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 8h3l2-3h6l2 3h3v11H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><circle cx="12" cy="13" r="3.5" fill="none" stroke="currentColor" stroke-width="2"/></svg>';async function vt(r,c){let o=await r.createImageBitmap(c,{imageOrientation:"from-image"}),t=Math.min(1,wt/Math.max(o.width,o.height)),d=Math.max(1,Math.round(o.width*t)),h=Math.max(1,Math.round(o.height*t)),f=r.document.createElement("canvas");return f.width=d,f.height=h,f.getContext("2d").drawImage(o,0,0,d,h),o.close?.(),new Promise((I,Z)=>f.toBlob(z=>z?I(z):Z(new Error("encode")),"image/jpeg",gt))}var yt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},St={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function kt(r,c){let o=Q(c),t=me(o),d=String(r??"").trim().match(St[o]);if(!d)return t.demoQuestion;let h=d[1].replace(/[\p{L}’']+/gu,f=>yt[o][f.toLowerCase()]??f);return W(t.demoAsk,{topic:h})}var Tt='<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,c,o={},t){let d=r.createElement(c);for(let[h,f]of Object.entries(o))f!=null&&f!==!1&&d.setAttribute(h,f===!0?"":f);return t!==void 0&&(d.textContent=t),d}var Lt=r=>({load:()=>fe(r.document),async token(c){return await Je(c,{win:r}),c.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset:c=>Ve(c,{win:r})});function Ut(r,c={}){if(r.pmwChat)return r.pmwChat;let o=c.win??window,t=o.document,d=c.WebSocket??o.WebSocket,h=c.fetch??((...e)=>o.fetch(...e)),f=c.turnstile??Lt(o),I=c.uuid??(()=>o.crypto.randomUUID()),Z=c.shrink??(e=>vt(o,e)),z=c.preview??(e=>new Promise((n,a)=>{let l=new o.FileReader;l.onload=()=>n(String(l.result)),l.onerror=()=>a(l.error),l.readAsDataURL(e)})),v={};try{v=JSON.parse(r.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let H=Q(v.lang),i=me(H,v.strings),ge=r.dataset.pmwChatOpener??"",Qe=r.querySelector(".pmw-chat__bubble"),m=c.demo??(v.demo===!0||!je({doc:t,hostname:o.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(s(t,"style",{id:"pmw-chat-style"},Ue));let b=s(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),_e=s(t,"div",{class:"pmw-chat__head"}),ee=r.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??s(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of ee.querySelectorAll("[id]")){let n=e.id;e.id=`${n}-head`;for(let a of ee.querySelectorAll(`[mask="url(#${n})"]`))a.setAttribute("mask",`url(#${n}-head)`)}let be=s(t,"div",{class:"pmw-chat__who"}),xe=s(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),D=s(t,"p",{class:"pmw-chat__role"}),ve=s(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),U=s(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});U.innerHTML=Tt,be.append(xe,D),_e.append(ee,be,ve,U);let te=s(t,"p",{class:"pmw-chat__notice"},i.automated);v.privacyHref&&(te.append(" "),te.append(s(t,"a",{href:v.privacyHref},i.privacy)));let x=s(t,"ol",{class:"pmw-chat__log"}),We=s(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},ge),C=s(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});C.append(s(t,"span"),s(t,"span"),s(t,"span")),x.append(We);let ne=s(t,"p",{class:"pmw-chat__state",role:"status"}),ye=s(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Se=e=>s(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":v.siteKey??"","data-action":"chat","data-theme":"light","data-language":H,"data-appearance":"interaction-only","data-size":"flexible"}),w=s(t,"form",{class:"pmw-chat__composer",novalidate:!0}),y=s(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"2",maxlength:String(2e3),autocomplete:"off",disabled:m,"aria-describedby":m?"pmw-chat-demo-hint":null}),Ze=s(t,"button",{type:"submit",class:"pmw-chat__send",disabled:m},i.send);w.append(s(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),y,Ze),m?w.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint)):w.append(Se(w));let P=null,E=s(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(v.photos===!0){let e=s(t,"label",{class:"pmw-chat__attach"});P=s(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:m}),e.innerHTML=xt,e.prepend(P),e.append(s(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),w.insertBefore(e,y),w.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),E)}let B=s(t,"div",{class:"pmw-chat__ended",hidden:!0}),ke=s(t,"p",{},i.ended),$=s(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);B.append(ke,$);let g=s(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),Te=s(t,"p",{class:"pmw-chat__fb-intro"}),ae=(e,n,a)=>{let l=`pmw-chat-fb-${e}`,p=s(t,"div",{class:"pmw-chat__field"});return p.append(s(t,"label",{for:l},i[e]),s(t,n,{id:l,name:e,required:!0,...a})),p},j=s(t,"p",{class:"pmw-chat__error",role:"alert"});g.append(s(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),Te,ae("name","input",{autocomplete:"name",maxlength:"100"}),ae("contact","input",{autocomplete:"on",maxlength:"200"}),ae("need","textarea",{rows:"3",maxlength:"2000"}),j,s(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),m||g.append(Se(g));let Le=s(t,"div",{class:"pmw-chat__body"});Le.append(te,x,ne,B,g),b.append(_e,Le,w,ye),r.append(b);let u=null;if(!m)try{u=JSON.parse(o.sessionStorage.getItem(we)??"null")}catch{u=null}let Ce=()=>{try{o.sessionStorage.setItem(we,JSON.stringify(u))}catch{}},_=null,oe=0,re=null,R=null,F=0,J="bot",V=null,A=!1,Ee=0,se=null,ie=new Set,k=new Map,K=new Set,ce=0,Ae=()=>new URLSearchParams(o.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Oe=m?Ae():null,le=()=>m?Oe:u?.botName||null;function q(){let e=J==="human";xe.textContent=e?V||i.assistant:le()||i.assistant,D.textContent=e?i.live:le()?i.assistant:"",D.hidden=D.textContent==="",ve.hidden=!e,b.dataset.mode=J}q();let T=e=>{ne.textContent=e??"",ne.hidden=!e};T("");let Ne=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||V||i.assistant:le()||i.assistant;function O(e,n){let a=s(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${n}`,"data-id":e}),l=s(t,"span",{class:"pmw-chat__sr"}),p=s(t,"span",{class:"pmw-chat__text"});return a.append(l,p),x.insertBefore(a,C.parentNode===x?C:null),a}let Me=e=>[...x.querySelectorAll("li[data-id]")].find(n=>n.dataset.id===e),L=(e,n,a)=>{e.firstChild.textContent=W(i.said,{name:Ne(n),text:""}),e.lastChild.textContent=a===""&&n.photos?.length&&!e.querySelector("img")?i.photoSent:a};function pe(){C.hidden=!0,o.clearTimeout(se)}function Pe(e,{announce:n}){if(ie.has(e.seq))return;ie.add(e.seq),F=Math.max(F,e.seq);let a=Me(e.id);a||(a=O(e.id,e.role)),a.classList.remove("is-streaming","is-pending","is-failed"),a.querySelector(".pmw-chat__retry")?.remove(),L(a,e,e.text),e.role==="visitor"?(k.delete(e.id),e.photos?.length&&K.add(e.id)):(pe(),n&&(ye.textContent=W(i.said,{name:Ne(e),text:e.text})),b.hidden&&r.setAttribute("data-unread","")),N()}function et(e,n){pe();let a=Me(e);a||(a=O(e,"bot"),a.classList.add("is-streaming"),L(a,{role:"bot"},"")),a.lastChild.textContent+=n,N()}let N=()=>{x.scrollTop=x.scrollHeight};if(m){L(O("demo-question","visitor"),{role:"visitor"},kt(ge,H));let e=O("demo-answer","bot");L(e,{role:"bot"},i.demoAnswer),o.addEventListener("hashchange",()=>{Oe=Ae(),q(),L(e,{role:"bot"},i.demoAnswer)})}function X(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=s(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),a=s(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"),u?Y()?de(e):M():ue()}),n.append(a),e.li.append(n)}let Y=()=>!!_&&_.readyState===1;function de(e){if(!(e.failed||!Y())){if(e.tries>=Ge)return X(e);e.tries++,_.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function M(){if(!u||A||_)return;o.clearTimeout(re);let e=o.location.protocol==="https:"?"wss:":"ws:",n=new URLSearchParams({c:u.c,k:u.k}),a=new d(`${e}//${o.location.host}/api/chat/ws?${n}`);_=a,a.onopen=()=>{oe=0,T(""),a.send(JSON.stringify({type:"hello",since:F}));for(let l of k.values())de(l)},a.onmessage=l=>{let p;try{p=JSON.parse(l.data)}catch{return}nt(p)},a.onclose=l=>{_===a&&(_=null);for(let S of k.values())S.tries>=Ge&&X(S);if(l.code>=4e3&&l.code<5e3)return tt(l.code);if(A)return;T(i.reconnecting);let p=Ye[Math.min(oe,Ye.length-1)];oe++,re=o.setTimeout(M,p)}}function tt(e){if(e===Ke.rpc)return G(i.reload,!1);G(i.ended,!0)}function G(e,n){A=!0,o.clearTimeout(re),T(""),ke.textContent=e,$.hidden=!n,B.hidden=!1,w.hidden=!0;for(let a of k.values())X(a)}function Re(e){J=e.mode==="human"?"human":"bot",V=typeof e.ownerName=="string"?e.ownerName:null,q(),e.closed&&G(i.ended,!0)}function nt(e){switch(e?.type){case"history":for(let n of e.messages??[])Pe(n,{announce:!1});return Re(e);case"message":return Pe(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return et(e.id,e.text);case"typing":return e.role==="visitor"?void 0:(C.hidden=!1,x.append(C),o.clearTimeout(se),se=o.setTimeout(pe,bt),N());case"status":return Re(e);case"fallback":return He(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Ie();case"error":(e.code==="closed"||e.code==="deleted")&&G(i.ended,!0)}}let qe=!1;function Ie(){if(!(qe||u?.lead)){qe=!0,u&&(u.lead=!0,Ce());try{o.pmw?.event(ft)}catch{}}}async function ze(e,n){let a="";try{a=await f.token(e)}catch{a=""}let l,p=null;try{l=await h("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:a,lang:H,page:o.location.pathname,...n})}),p=await l.json()}catch{l=null}try{f.reset(e)}catch{}return l?.ok&&p?{ok:!0,data:p}:{ok:!1,error:l?.status===403?i.turnstile:l?.status===429?i.tooMany:null,cap:l?.status===409&&p?.error==="cap"}}function ue(){return R||u||(T(i.connecting),R=ze(w,{}).then(e=>{if(R=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},Ce(),q(),M();return}for(let n of k.values())X(n);if(e.error)return T(e.error);T(""),He(e.cap?i.fallbackCap:i.unavailable)})),R}function He(e){Te.textContent=e,g.hidden=!1,w.hidden=!0,g.querySelector("input")?.focus()}g.addEventListener("submit",async e=>{e.preventDefault();let n=p=>g.elements[p].value.trim(),a={name:n("name"),contact:n("contact"),need:n("need")};if(!a.name||!a.contact||!a.need){j.textContent=i.fallbackMissing;return}j.textContent="";let l=await ze(g,{fallbackLead:a});if(!l.ok){j.textContent=l.error??i.unavailable;return}g.replaceChildren(s(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Ie()});let at={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function ot(e){let n,a;try{n=await Z(e),a=await z(n)}catch{E.textContent=i.photoUnreadable;return}let l=I(),p=O(l,"visitor");p.classList.add("is-pending"),p.insertBefore(s(t,"img",{class:"pmw-chat__photo",src:a,alt:i.photoAlt}),p.lastChild),L(p,{role:"visitor"},""),N(),u||await ue();let S=null;if(u){let it=new URLSearchParams({c:u.c,k:u.k,id:l});try{S=await h(`/api/chat/photo?${it}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:n})}catch{S=null}}if(S?.ok){p.classList.remove("is-pending"),K.add(l),_||M();return}p.remove(),E.textContent=i[at[S?.status]]??i.photoFailed}P?.addEventListener("change",async()=>{let e=[...P.files??[]];if(P.value="",E.textContent="",A||m||e.length===0)return;let n=mt-K.size-ce;e.length>n&&(E.textContent=i.photoLimit);let a=e.slice(0,Math.max(0,n));ce+=a.length;for(let l of a)try{await ot(l)}finally{ce--}});function De(){let e=y.value.trim();if(!e||A||m)return;y.value="";let n={id:I(),text:e.slice(0,2e3),tries:0,failed:!1};n.li=O(n.id,"visitor"),n.li.classList.add("is-pending"),L(n.li,{role:"visitor"},n.text),k.set(n.id,n),N(),u?Y()?de(n):M():ue()}w.addEventListener("submit",e=>{e.preventDefault(),De()}),y.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),De())}),y.addEventListener("input",()=>{let e=Date.now();Y()&&e-Ee>_t&&(Ee=e,_.send(JSON.stringify({type:"typing"})))}),$.addEventListener("click",()=>{u=null;try{o.sessionStorage.removeItem(we)}catch{}try{_?.close(1e3)}catch{}_=null,A=!1,F=0,ie.clear(),k.clear(),K.clear(),E.textContent="",J="bot",V=null,q();for(let e of[...x.querySelectorAll("li[data-id]")])e.remove();B.hidden=!0,w.hidden=!1,y.focus()});let rt=()=>[...b.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));b.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),he();return}if(e.key!=="Tab")return;let n=rt();if(n.length===0)return;let a=n[0],l=n[n.length-1];e.shiftKey&&t.activeElement===a?(e.preventDefault(),l.focus()):!e.shiftKey&&t.activeElement===l&&(e.preventDefault(),a.focus())}),U.addEventListener("click",()=>he());function st(){m||f.load(),b.hidden=!1,r.setAttribute("data-open",""),r.removeAttribute("data-unread"),u&&M(),(m?U:w.hidden?g.hidden?$:g.querySelector("input"):y)?.focus(),N()}function he(){b.hidden=!0,r.removeAttribute("data-open"),Qe?.focus()}return r.pmwChat={open:st,close:he,panel:b},r.pmwChat}export{Ye as BACKOFF_MS,ft as GOAL,mt as MAX_PHOTOS,Ge as MAX_TRIES,wt as PHOTO_EDGE,gt as PHOTO_QUALITY,we as SESSION_KEY,kt as demoQuestion,Ut as mountChat};
package/manifest.json CHANGED
@@ -19,7 +19,8 @@
19
19
  },
20
20
  "bindings": [
21
21
  { "kind": "service", "binding": "CHAT", "service": "pmeweb-chat", "entrypoint": "ChatApi" },
22
- { "kind": "ratelimit", "binding": "CHAT_LIMIT" }
22
+ { "kind": "ratelimit", "binding": "CHAT_LIMIT" },
23
+ { "kind": "ratelimit", "binding": "CHAT_START_LIMIT" }
23
24
  ],
24
25
  "secrets": ["TURNSTILE_SECRET"],
25
26
  "goals": ["chat-lead"],
@@ -33,8 +34,17 @@
33
34
  },
34
35
  "markup": { "pages": "all", "required": ["#pmw-chat", "[data-pmw-chat-opener]"] },
35
36
  "build": ["plugins/chat/context.json"],
36
- "portal": { "settings": [
37
- { "key": "botName", "kind": "text", "editor": "client",
37
+ "portal": {
38
+ "screens": [
39
+ { "key": "appearance",
40
+ "title": { "fr": "Apparence de l’assistant", "en": "Assistant’s appearance" },
41
+ "summary": { "fr": "Le visage, la couleur et le nom que vos visiteurs voient dans la bulle de discussion.", "en": "The face, colour and name your visitors see in the chat bubble." },
42
+ "crumb": { "fr": "Apparence", "en": "Appearance" },
43
+ "views": [ { "key": "bubble", "label": { "fr": "Bulle", "en": "Bubble" } },
44
+ { "key": "panel", "label": { "fr": "Panneau ouvert", "en": "Open panel" } } ] }
45
+ ],
46
+ "settings": [
47
+ { "key": "botName", "kind": "text", "editor": "client", "screen": "appearance", "applies": "publish",
38
48
  "label": { "fr": "Nom de l’assistant", "en": "Assistant’s name" },
39
49
  "help": { "fr": "Affiché avec la mention « Assistant virtuel », pour que personne ne le prenne pour un employé.", "en": "Shown with “Virtual assistant”, so nobody takes it for an employee." } },
40
50
  { "key": "ownerName", "kind": "text", "editor": "client",
@@ -56,7 +66,16 @@
56
66
  "config": [
57
67
  { "key": "enabled", "kind": "boolean", "editor": "client",
58
68
  "label": { "fr": "Afficher l’assistant sur votre site", "en": "Show the assistant on your site" },
59
- "help": { "fr": "Désactivé\u00a0: l’assistant disparaît du site; vos conversations et réglages sont gardés.", "en": "Off: the assistant leaves your site; your conversations and settings are kept." } }
69
+ "help": { "fr": "Désactivé\u00a0: l’assistant disparaît du site; vos conversations et réglages sont gardés.", "en": "Off: the assistant leaves your site; your conversations and settings are kept." } },
70
+ { "key": "avatar", "kind": "choice", "editor": "client", "screen": "appearance",
71
+ "label": { "fr": "Visage", "en": "Face" },
72
+ "groups": [
73
+ { "key": "photos", "label": { "fr": "Photos réalistes", "en": "Realistic photos" }, "help": { "fr": "Des visages créés pour PmeWeb, pas des personnes réelles.", "en": "Faces made for PmeWeb, not real people." }, "count": 5, "image": "avatars/photos-{n}.webp", "optionLabel": { "fr": "Visage {n}", "en": "Face {n}" } },
74
+ { "key": "illustration", "label": { "fr": "Illustration", "en": "Illustration" }, "help": { "fr": "Des visages créés pour PmeWeb, pas des personnes réelles.", "en": "Faces made for PmeWeb, not real people." }, "count": 5, "image": "avatars/illustration-{n}.webp", "optionLabel": { "fr": "Visage {n}", "en": "Face {n}" } },
75
+ { "key": "sans-visage", "label": { "fr": "Sans visage", "en": "No face" }, "help": { "fr": "Un dessin de bulle, sur une couleur de votre site.", "en": "A bubble drawing, on one of your site’s colours." }, "count": 5, "image": "avatars/sans-visage-{n}.svg", "optionLabel": { "fr": "Icône {n}", "en": "Icon {n}" }, "tint": "avatarBg" } ] },
76
+ { "key": "avatarBg", "kind": "color", "editor": "client", "screen": "appearance",
77
+ "label": { "fr": "Couleur de fond", "en": "Background colour" },
78
+ "default": { "design": "accent" }, "showWhen": { "key": "avatar", "group": "sans-visage" } }
60
79
  ] },
61
80
  "rpc": { "service": "pmeweb-chat", "entrypoint": "ChatApi", "version": 1, "accepts": "pmeweb-chat/plugin-rpc.json" },
62
81
  "health": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmeweb/plugin-chat",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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/trigger.js CHANGED
@@ -4,17 +4,23 @@
4
4
  // onInteraction: press, hover, focus, key) and opened on a click. Off the site's real hosts the
5
5
  // panel runs its demo (manifest preview: test); the widget decides that, not this file.
6
6
  //
7
+ // THE DEMO FOLLOWS THE URL FRAGMENT, off the real hosts only (phase 4 spec 2.4), so the portal's
8
+ // preview switches views without a rebuild: `#pmw-chat=panel` imports the widget at once and opens
9
+ // the panel, `#pmw-chat=bubble` closes it; read on load and on `hashchange`. The name
10
+ // (`pmw-chat-botName`) is the widget's to read. No opener is scheduled there: it would cover the
11
+ // preview the portal is showing.
12
+ //
7
13
  // The opener shows once per visit (sessionStorage), on the first of: 20 s on the page, 30 % of the
8
14
  // page scrolled, or exit intent (a fine pointer leaving through the top of the window). Never while
9
15
  // the panel is open, never again once dismissed or once the panel has been opened.
10
- import { onInteraction as interact } from '@pmeweb/loader'
16
+ import { isCanonical, onInteraction as interact } from '@pmeweb/loader'
11
17
 
12
18
  export const OPENER_KEY = 'pmw-chat-opener'
13
19
  export const OPENER_DELAY_MS = 20000
14
20
  export const OPENER_SCROLL = 0.3
15
21
 
16
22
  export function startChat({
17
- doc = document, win = window, importLazy = (u) => import(/* @vite-ignore */ u), onInteraction = interact,
23
+ doc = document, win = window, hostname = win.location.hostname, importLazy = (u) => import(/* @vite-ignore */ u), onInteraction = interact,
18
24
  } = {}) {
19
25
  const root = doc.getElementById('pmw-chat')
20
26
  if (!root) return null
@@ -23,6 +29,16 @@ export function startChat({
23
29
  let widget = null
24
30
  const load = () => (widget ??= importLazy(root.dataset.pmwLazy).then((m) => m.mountChat(root), (e) => { widget = null; throw e }))
25
31
  onInteraction(root, () => load().catch(() => {}))
32
+ const canonical = isCanonical({ doc, hostname })
33
+ if (!canonical) {
34
+ const view = () => {
35
+ const v = new URLSearchParams(win.location.hash.slice(1)).get('pmw-chat')
36
+ if (v === 'panel' && !root.hasAttribute('data-open')) load().then((w) => w.open(), () => {})
37
+ else if (v === 'bubble' && widget) widget.then((w) => w.close(), () => {})
38
+ }
39
+ view()
40
+ win.addEventListener('hashchange', view)
41
+ }
26
42
 
27
43
  const store = win.sessionStorage
28
44
  let done = false
@@ -48,7 +64,7 @@ export function startChat({
48
64
  finish()
49
65
  if (t.hasAttribute('data-pmw-chat-open')) load().then((w) => w.open(), () => {})
50
66
  })
51
- if (!done && teaser) {
67
+ if (!done && teaser && canonical) {
52
68
  timer = win.setTimeout(show, OPENER_DELAY_MS)
53
69
  on(win, 'scroll', () => {
54
70
  const room = doc.documentElement.scrollHeight - win.innerHeight
package/src/widget.js CHANGED
@@ -227,7 +227,10 @@ export function mountChat(root, deps = {}) {
227
227
  const photosSent = new Set()
228
228
  let photosBusy = 0
229
229
 
230
- const botName = () => session?.botName || null
230
+ // The demo's name is the portal preview's, from the fragment (phase 4 spec 2.4); never on a real host.
231
+ const fragmentName = () => new URLSearchParams(win.location.hash.slice(1)).get('pmw-chat-botName')?.trim() || null
232
+ let demoName = demo ? fragmentName() : null
233
+ const botName = () => (demo ? demoName : session?.botName || null)
231
234
  function header() {
232
235
  const human = mode === 'human'
233
236
  name.textContent = human ? (ownerName || s.assistant) : (botName() || s.assistant)
@@ -294,7 +297,13 @@ export function mountChat(root, deps = {}) {
294
297
  const scroll = () => { log.scrollTop = log.scrollHeight }
295
298
  if (demo) {
296
299
  setText(line('demo-question', 'visitor'), { role: 'visitor' }, demoQuestion(opener, lang))
297
- setText(line('demo-answer', 'bot'), { role: 'bot' }, s.demoAnswer)
300
+ const answer = line('demo-answer', 'bot')
301
+ setText(answer, { role: 'bot' }, s.demoAnswer)
302
+ win.addEventListener('hashchange', () => {
303
+ demoName = fragmentName()
304
+ header()
305
+ setText(answer, { role: 'bot' }, s.demoAnswer)
306
+ })
298
307
  }
299
308
 
300
309
  function failed(entry) {
@@ -428,7 +437,7 @@ export function mountChat(root, deps = {}) {
428
437
  try { turnstile.reset(form) } catch { /* a token is single-use; the next post asks again */ }
429
438
  if (res?.ok && data) return { ok: true, data }
430
439
  const error = res?.status === 403 ? s.turnstile : res?.status === 429 ? s.tooMany : null
431
- return { ok: false, error }
440
+ return { ok: false, error, cap: res?.status === 409 && data?.error === 'cap' }
432
441
  }
433
442
  function begin() {
434
443
  if (starting || session) return starting
@@ -445,7 +454,8 @@ export function mountChat(root, deps = {}) {
445
454
  for (const entry of pending.values()) failed(entry)
446
455
  if (r.error) return showState(r.error)
447
456
  showState('')
448
- showFallback(s.unavailable)
457
+ // 409 cap: the visitor's conversations for the day are used up (pmeweb-chat counts them per IP).
458
+ showFallback(r.cap ? s.fallbackCap : s.unavailable)
449
459
  })
450
460
  return starting
451
461
  }
package/src/worker.d.ts CHANGED
@@ -25,6 +25,8 @@ export interface ChatEnv {
25
25
  TURNSTILE_SECRET?: string
26
26
  /** A Workers Rate Limiting binding (`ratelimits` in wrangler.jsonc), keyed `chat:<ip>`. */
27
27
  CHAT_LIMIT?: { limit(o: { key: string }): Promise<{ success: boolean }> }
28
+ /** The conversation-start limit (3 a minute), keyed `chat-start:<ip>`; absent, starts use CHAT_LIMIT. */
29
+ CHAT_START_LIMIT?: { limit(o: { key: string }): Promise<{ success: boolean }> }
28
30
  }
29
31
 
30
32
  /**
@@ -56,5 +58,5 @@ export declare const RPC_VERSION: number
56
58
  export declare function health(env: Partial<ChatEnv>, opts?: { host?: string; config?: ChatConfig | Record<string, unknown> }): Promise<{
57
59
  version: string
58
60
  rpc: number
59
- checks: { secret: boolean; binding: boolean; limit: boolean; rpc: boolean; settings: boolean } | { disabled: true }
61
+ checks: { secret: boolean; binding: boolean; limit: boolean; startLimit: boolean; rpc: boolean; settings: boolean } | { disabled: true }
60
62
  }>
package/src/worker.js CHANGED
@@ -9,8 +9,13 @@
9
9
  // workers.dev address. Without `hosts`, every request is off (fail closed).
10
10
  // - The host sent to pmeweb-chat is the one this request ARRIVED on, never a header or a body
11
11
  // field: it is how pmeweb-chat finds the client's settings.
12
- // - start: Turnstile (@pmeweb/turnstile, action `chat`, solved on this host), then CHAT_LIMIT keyed
13
- // on the visitor's IP, then pmeweb-chat. The IP goes to siteverify and the limiter only.
12
+ // - start: Turnstile (@pmeweb/turnstile, action `chat`, solved on this host), then CHAT_START_LIMIT
13
+ // (3 a minute) keyed on the visitor's IP, then pmeweb-chat. The contact form's start
14
+ // (`fallbackLead`) is on the looser CHAT_LIMIT instead, so a visitor refused a conversation can
15
+ // still leave a lead; a site that has not bound CHAT_START_LIMIT yet falls back to CHAT_LIMIT.
16
+ // The IP goes to siteverify, the limiters and, in the `x-pmw-visitor-ip` header only, to
17
+ // pmeweb-chat, which counts conversations per visitor per day (10) and keeps only a daily hash
18
+ // of it. pmeweb-chat trusts that header because only service bindings reach it.
14
19
  // Only named fields go on; `sms` is sent only as `voipms` (pmeweb-chat refuses any other value),
15
20
  // `photos` only as true. pmeweb-chat's status and JSON pass through (426 rpc_version, 503
16
21
  // not_configured, ...): the widget reads them.
@@ -28,7 +33,7 @@ import { verifyTurnstile } from '@pmeweb/turnstile/worker'
28
33
  import { RPC_HEADER, RPC_VERSION } from './protocol.js'
29
34
 
30
35
  /** This package's version, reported by the health route (a test holds it equal to package.json). */
31
- export const VERSION = '1.0.0'
36
+ export const VERSION = '1.1.0'
32
37
  export { RPC_VERSION }
33
38
 
34
39
  const SERVICE = 'https://pmeweb-chat.internal'
@@ -52,10 +57,14 @@ function json(body, status = 200, headers = {}) {
52
57
 
53
58
  const langOf = (v) => (v === 'en' || v === 'fr' ? v : null)
54
59
 
55
- async function allowed(request, env) {
56
- if (!env.CHAT_LIMIT) return true
60
+ const VISITOR_IP_HEADER = 'x-pmw-visitor-ip'
61
+
62
+ /** CHAT_LIMIT by default (sockets, photos, the contact form); `start` uses CHAT_START_LIMIT when bound. */
63
+ async function allowed(request, env, { start = false } = {}) {
64
+ const [limiter, prefix] = start && env.CHAT_START_LIMIT ? [env.CHAT_START_LIMIT, 'chat-start'] : [env.CHAT_LIMIT, 'chat']
65
+ if (!limiter) return true
57
66
  try {
58
- return (await env.CHAT_LIMIT.limit({ key: `chat:${request.headers.get('cf-connecting-ip') ?? 'unknown'}` })).success
67
+ return (await limiter.limit({ key: `${prefix}:${request.headers.get('cf-connecting-ip') ?? 'unknown'}` })).success
59
68
  } catch (e) {
60
69
  // The limiter is approximate by design; a failure of it lets a Turnstile-verified visitor
61
70
  // through, and pmeweb-chat still caps turns and the day's spend.
@@ -102,7 +111,7 @@ async function start(request, url, env, { config, lang }) {
102
111
  console.warn(`[chat] turnstile refused: ${verdict.reason}`)
103
112
  return json({ ok: false, error: 'turnstile' }, 403)
104
113
  }
105
- if (!(await allowed(request, env))) return json({ ok: false, error: 'too_many' }, 429)
114
+ if (!(await allowed(request, env, { start: !lead }))) return json({ ok: false, error: 'too_many' }, 429)
106
115
 
107
116
  const page = typeof b.page === 'string' && b.page.startsWith('/') && !b.page.startsWith('//') && b.page.length <= 512 ? b.page : null
108
117
  const body = { host: url.hostname.toLowerCase(), lang: langOf(b.lang) ?? langOf(lang) ?? 'fr', page }
@@ -110,11 +119,14 @@ async function start(request, url, env, { config, lang }) {
110
119
  if (config?.photos === true) body.photos = true
111
120
  if (lead) body.fallbackLead = lead
112
121
 
122
+ const headers = { 'content-type': 'application/json', [RPC_HEADER]: String(RPC_VERSION) }
123
+ const ip = request.headers.get('cf-connecting-ip')
124
+ if (ip) headers[VISITOR_IP_HEADER] = ip
113
125
  let res
114
126
  try {
115
127
  res = await env.CHAT.fetch(`${SERVICE}/start`, {
116
128
  method: 'POST',
117
- headers: { 'content-type': 'application/json', [RPC_HEADER]: String(RPC_VERSION) },
129
+ headers,
118
130
  body: JSON.stringify(body),
119
131
  })
120
132
  } catch {
@@ -135,7 +147,7 @@ async function socket(request, url, env) {
135
147
  const q = new URLSearchParams()
136
148
  for (const k of ['c', 'k']) q.set(k, url.searchParams.get(k) ?? '')
137
149
  const headers = new Headers(request.headers)
138
- for (const h of ['cookie', 'authorization']) headers.delete(h)
150
+ for (const h of ['cookie', 'authorization', VISITOR_IP_HEADER]) headers.delete(h)
139
151
  headers.set(RPC_HEADER, String(RPC_VERSION))
140
152
  try {
141
153
  return await env.CHAT.fetch(new Request(`${SERVICE}/ws?${q}`, { method: 'GET', headers }))
@@ -253,6 +265,7 @@ export async function health(env, { host, config } = {}) {
253
265
  secret: !!env.TURNSTILE_SECRET,
254
266
  binding: !!env.CHAT,
255
267
  limit: !!env.CHAT_LIMIT,
268
+ startLimit: !!env.CHAT_START_LIMIT,
256
269
  rpc,
257
270
  settings: rpc && !!host && answer.configured === true,
258
271
  },