@pmeweb/plugin-chat 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -68,15 +68,25 @@ file ».
68
68
  performance 100. In Chromium at 390 px: a visitor scrolling at 1 s gets `widget.js` at 1.0 s,
69
69
  `api.js` at 4.0 s; one who never touches the page, `widget.js` with the opener at 20.0 s,
70
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).
71
+ - **The Cloudflare box shows only when Cloudflare asks.** The widgets (`interaction-only`) are
72
+ rendered by the chat itself, explicitly (options and callbacks as an object), exactly once per
73
+ box (a `data-pmw-rendered` mark is the guard). They carry no `cf-turnstile` class, so `api.js`'s
74
+ implicit pass, which renders a contact form's `<Turnstile>`, never reaches them, and the chat
75
+ never renders, resets or alters a widget it does not own. The composer's box mints its token in
76
+ the closed panel, which stays laid out (transparent, `inert`, no pointer events) so the box has
77
+ a real size; the contact form's box is rendered once that form is shown. Each sits in its own
78
+ row, collapsed (the box transparent and out of the flow, full width) until Turnstile's
79
+ `before-interactive` callback opens it; its success callback collapses it again (« Succès ! »
80
+ goes away, 0.25 s, at once under reduced motion). In the composer the row sits above the field,
81
+ so the field never moves. An unused token is refreshed by Turnstile before its 300 s expiry; a
82
+ used one is replaced just before the next post (`turnstile.reset(id)`, never while its row is
83
+ asking).
84
+ - **A failed challenge is retried once, silently.** A retryable error code (300*, 600*, 110600,
85
+ 110620) resets the widget once without showing anything. A second failure keeps the row
86
+ collapsed; the next post shows « L’envoi n’a pas abouti. Réessayez. » without posting, and the
87
+ one after starts a fresh challenge. Measured with Cloudflare's always-block test key (which
88
+ answers 600010, the code a desktop Chrome in DevTools device emulation gets): one reset, one
89
+ second 600010, no box shown.
80
90
  - **The first message reads like a reply.** The typing dots show at once while the conversation
81
91
  starts (token, `/api/chat/start`, socket); « Un instant… » joins them only past 8 s. Nothing is
82
92
  started before the visitor writes: pmeweb-chat counts every start toward the visitor's
package/dist/widget.js CHANGED
@@ -1,2 +1,2 @@
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};
1
+ var it=`.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]{display:flex!important;opacity:0;pointer-events:none}.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{position:relative;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}.pmw-chat__check:not([data-ask]) .pmw-chat__check-in{position:absolute;top:0;left:0;right:0;overflow:visible;opacity:0;pointer-events:none}@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}.pmw-chat__fb-done{margin:12px 0 0;padding:12px 14px;border-radius:12px;background:#e9f6ee;color:#14532d;border:1px solid #b7dfc4;font-weight:500}
2
+ `;var Mt="400px 0px";var st=new Set,pe=null;function ct(r,{doc:p=document,attrs:o={}}={}){if(st.has(r)||(st.add(r),p.querySelector(`script[src="${r}"]`)))return;let t=p.createElement("script");t.src=r,t.async=!0;for(let[f,m]of Object.entries(o))t.setAttribute(f,m);p.head.append(t)}function lt({doc:r=document,hostname:p=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 f=String(p).toLowerCase();return t.some(m=>typeof m=="string"&&m.toLowerCase()===f)}function Nt(r){return pe||(pe=new Promise(p=>{let o=null,t=null,f=0,m=r.PerformanceObserver?.supportedEntryTypes??[],w=typeof r.PerformanceObserver=="function"&&m.includes("paint"),l=()=>{o===null||w&&t===null||p(Math.max(o,t??0,f))};if(w)try{new r.PerformanceObserver(x=>{for(let v of x.getEntries())v.name==="first-contentful-paint"&&(t=v.startTime);l()}).observe({type:"paint",buffered:!0}),m.includes("largest-contentful-paint")&&new r.PerformanceObserver(x=>{for(let v of x.getEntries())f=Math.max(f,v.startTime)}).observe({type:"largest-contentful-paint",buffered:!0})}catch{t=0}let u=()=>{o=r.performance.now(),l()};r.document.readyState==="complete"?u():r.addEventListener("load",u,{once:!0})}),pe)}function Me(r,p,{win:o=window,rootMargin:t=Mt,settleMs:f=1500}={}){let m=null,w=!1,l=()=>{w||(w=!0,m?.disconnect(),r.removeEventListener("focusin",l),r.removeEventListener("pointerdown",l),p())};r.addEventListener("focusin",l),r.addEventListener("pointerdown",l);let u=Nt(o),x=async()=>{let v=await u;o.setTimeout(l,Math.max(0,v+f-o.performance.now()))};return typeof o.IntersectionObserver=="function"?(m=new o.IntersectionObserver(v=>{v.some(k=>k.isIntersecting)&&(m.disconnect(),x())},{rootMargin:t}),m.observe(r)):x(),{run:l}}var Pt="https://challenges.cloudflare.com/turnstile/v0/api.js";var pt=15e3;function dt(r=document){ct(Pt,{doc:r})}var ht={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var Rt={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."}},de=r=>String(r??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function Ne(r,p={}){let o=Rt[de(r)],t={...o};for(let[f,m]of Object.entries(p??{}))typeof m=="string"&&f in o&&(t[f]=m);return t}var Y=(r,p)=>r.replace(/\{(\w+)\}/g,(o,t)=>t in p?String(p[t]):o);var he="pmw-chat",ut=[1e3,2e3,4e3,8e3,16e3,3e4],ft=3,qt="chat-lead",It=3,zt=1600,Ht=.85,Dt=3e3,Ut=7200*1e3,Bt=8e3,Ft=40,$t=3e3,wt=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,p){let o=await r.createImageBitmap(p,{imageOrientation:"from-image"}),t=Math.min(1,zt/Math.max(o.width,o.height)),f=Math.max(1,Math.round(o.width*t)),m=Math.max(1,Math.round(o.height*t)),w=r.document.createElement("canvas");return w.width=f,w.height=m,w.getContext("2d").drawImage(o,0,0,f,m),o.close?.(),new Promise((l,u)=>w.toBlob(x=>x?l(x):u(new Error("encode")),"image/jpeg",Ht))}var Kt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},Gt={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function Vt(r,p){let o=de(p),t=Ne(o),f=String(r??"").trim().match(Gt[o]);if(!f)return t.demoQuestion;let m=f[1].replace(/[\p{L}’']+/gu,w=>Kt[o][w.toLowerCase()]??w);return Y(t.demoAsk,{topic:m})}var Yt='<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 i(r,p,o={},t){let f=r.createElement(p);for(let[m,w]of Object.entries(o))w!=null&&w!==!1&&f.setAttribute(m,w===!0?"":w);return t!==void 0&&(f.textContent=t),f}var Xt=/^(?:300\d{3}|600\d{3}|110600|110620)$/,Wt=r=>Xt.test(String(r??"")),_t=200,Qt=(r,p)=>{let o=!1,t=l=>{let u=r.turnstile;if(!(l.el.hasAttribute("data-pmw-rendered")||!l.shown())){l.el.setAttribute("data-pmw-rendered","");try{l.id=u.render(l.el,l.params)}catch{}}},f=()=>{if(!r.turnstile?.render)return!1;for(let l of p)t(l);return!0},m=()=>{if(dt(r.document),o||f())return;o=!0;let l=0,u=()=>{!f()&&++l<600&&r.setTimeout(u,100)};r.setTimeout(u,100)},w=l=>p.find(u=>u.form===l);for(let l of p)l.params["error-callback"]=u=>(l.ask(!1),Wt(u)&&!l.retried&&l.id!==void 0?(l.retried=!0,r.setTimeout(()=>{try{r.turnstile?.reset(l.id)}catch{l.failed=!0}},0)):l.failed=!0,!0);return{load:m,async token(l){m();let u=w(l),x=0;for(;;){if(u?.failed)return null;let v=l.querySelector('input[name="cf-turnstile-response"]')?.value??"";if(v)return v;if(!u?.asking()){if(x>=pt)return"";x+=_t}await new Promise(k=>r.setTimeout(k,_t))}},reset(l){let u=w(l);!u||u.id===void 0||u.asking()||(u.failed=!1,u.retried=!1,r.turnstile?.reset(u.id))}}};function da(r,p={}){if(r.pmwChat)return r.pmwChat;let o=p.win??window,t=o.document,f=p.WebSocket??o.WebSocket,m=p.fetch??((...e)=>o.fetch(...e)),w=[],l=p.turnstile??null,u=p.uuid??(()=>o.crypto.randomUUID()),x=p.shrink??(e=>jt(o,e)),v=p.preview??(e=>new Promise((a,n)=>{let c=new o.FileReader;c.onload=()=>a(String(c.result)),c.onerror=()=>n(c.error),c.readAsDataURL(e)})),k={};try{k=JSON.parse(r.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let X=de(k.lang),s=Ne(X,k.strings),Pe=r.dataset.pmwChatOpener??"",Re=r.querySelector(".pmw-chat__bubble"),g=p.demo??(k.demo===!0||!lt({doc:t,hostname:o.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(i(t,"style",{id:"pmw-chat-style"},it));let S=i(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0});S.inert=!0;let qe=i(t,"div",{class:"pmw-chat__head"}),me=r.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??i(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of me.querySelectorAll("[id]")){let a=e.id;e.id=`${a}-head`;for(let n of me.querySelectorAll(`[mask="url(#${a})"]`))n.setAttribute("mask",`url(#${a}-head)`)}let Ie=i(t,"div",{class:"pmw-chat__who"}),ze=i(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),W=i(t,"p",{class:"pmw-chat__role"}),He=i(t,"span",{class:"pmw-chat__face"}),De=i(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),Q=i(t,"button",{type:"button",class:"pmw-chat__close","aria-label":s.close});Q.innerHTML=Yt;let Z=i(t,"p",{class:"pmw-chat__notice"},s.automated);k.privacyHref&&(Z.append(" "),Z.append(i(t,"a",{href:k.privacyHref},s.privacy))),He.append(me,De),Ie.append(ze,W,Z),qe.append(He,Ie,Q);let T=i(t,"ol",{class:"pmw-chat__log"}),gt=i(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},Pe),A=i(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});A.append(i(t,"span"),i(t,"span"),i(t,"span")),T.append(gt);let ue=i(t,"p",{class:"pmw-chat__state",role:"status"}),Ue=i(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Be=(e,a)=>{let n=i(t,"div",{class:"pmw-chat__check"}),c=i(t,"div",{class:"pmw-chat__check-in"});c.inert=!0;let d=le=>{n.toggleAttribute("data-ask",le),c.inert=!le,E()},_=i(t,"div",{class:"pmw-chat__turnstile"}),O={el:_,form:e,shown:a,ask:d,asking:()=>n.hasAttribute("data-ask"),params:{sitekey:k.siteKey??"",action:"chat",theme:"light",language:X,appearance:"interaction-only",size:"flexible","refresh-expired":"auto",callback:()=>{O.retried=!1,O.failed=!1,d(!1)},"before-interactive-callback":()=>d(!0)}};return w.push(O),c.append(_),n.append(c),n},y=i(t,"form",{class:"pmw-chat__composer",novalidate:!0}),L=i(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"1",maxlength:String(2e3),placeholder:s.messageLabel,autocomplete:"off",disabled:g,"aria-describedby":g?"pmw-chat-demo-hint":null}),bt=i(t,"button",{type:"submit",class:"pmw-chat__send",disabled:g},s.send),fe=i(t,"div",{class:"pmw-chat__box"});fe.append(i(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},s.messageLabel),L,bt),g||y.append(Be(y,()=>!0)),y.append(fe),g&&y.append(i(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},s.demoHint));let F=null,z=i(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(k.photos===!0){let e=i(t,"label",{class:"pmw-chat__attach"});F=i(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:g}),e.innerHTML=Jt,e.prepend(F),e.append(i(t,"span",{class:"pmw-chat__sr"},s.attachPhoto)),fe.insertBefore(e,L),y.append(i(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},s.photoHint),z)}let ee=i(t,"div",{class:"pmw-chat__ended",hidden:!0}),Fe=i(t,"p",{},s.ended),te=i(t,"button",{type:"button",class:"pmw-chat__restart"},s.newConversation);ee.append(Fe,te);let b=i(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),$e=i(t,"p",{class:"pmw-chat__fb-intro"}),we=(e,a,n)=>{let c=`pmw-chat-fb-${e}`,d=i(t,"div",{class:"pmw-chat__field"});return d.append(i(t,"label",{for:c},s[e]),i(t,a,{id:c,name:e,required:!0,...n})),d},$=i(t,"p",{class:"pmw-chat__error",role:"alert"});b.append(i(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},s.fallbackTitle),$e,we("name","input",{autocomplete:"name",maxlength:"100"}),we("contact","input",{autocomplete:"on",maxlength:"200"}),we("need","textarea",{rows:"3",maxlength:"2000"}),$,i(t,"button",{type:"submit",class:"pmw-chat__send"},s.fallbackSend)),g||b.insertBefore(Be(b,()=>!b.hidden),$);let M=i(t,"div",{class:"pmw-chat__body"});M.append(T,ue,ee,b),S.append(qe,M,y,Ue),r.append(S),l??(l=Qt(o,w));let J=!0,Je=0,_e=0,xt=()=>M.scrollHeight-M.scrollTop-M.clientHeight;M.addEventListener("scroll",()=>{let e=M.scrollTop;xt()<=Ft?J=!0:e<Je&&(J=!1),Je=e},{passive:!0});let vt=e=>o.requestAnimationFrame?o.requestAnimationFrame(e):o.setTimeout(e,16);function E(e=!1,a=!1){e&&(J=!0),!(!J||_e)&&(_e=vt(()=>{if(_e=0,!J)return;let n=a||!!o.matchMedia?.("(prefers-reduced-motion: reduce)").matches;M.scrollTo({top:M.scrollHeight,behavior:n?"instant":"smooth"})}))}if(typeof o.ResizeObserver=="function"){let e=new o.ResizeObserver(()=>E());e.observe(T),e.observe(y)}let h=null;if(!g)try{h=JSON.parse(o.sessionStorage.getItem(he)??"null")}catch{h=null}if(h&&typeof h.at=="number"&&!(Date.now()-h.at<Ut)){h=null;try{o.sessionStorage.removeItem(he)}catch{}}let ge=()=>{try{o.sessionStorage.setItem(he,JSON.stringify(h))}catch{}},yt=()=>{h&&(h.at=Date.now(),ge())},C=null,be=0,xe=null,j=null,ae=0,K="bot",R=null,H=!1,je=0,ve=null,ye=new Set,q=new Map,ne=new Set,ke=0,Ke=()=>new URLSearchParams(o.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Ge=g?Ke():null,Se=()=>g?Ge:h?.botName||null;function G(){let e=K==="human";ze.textContent=e?R||s.assistant:Se()||s.assistant,W.textContent=e?s.live:Se()?s.assistant:"",W.hidden=W.textContent==="",Z.hidden=e,De.hidden=!e,S.dataset.mode=K}G();let N=e=>{ue.textContent=e??"",ue.hidden=!e};N("");let Ve=e=>e.role==="visitor"?s.you:e.role==="owner"?e.by||R||s.assistant:Se()||s.assistant;function D(e,a){let n=i(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${a}`,"data-id":e}),c=i(t,"span",{class:"pmw-chat__sr"}),d=i(t,"span",{class:"pmw-chat__text"});return n.append(c,d),T.insertBefore(n,A.parentNode===T?A:null),n}let Ye=()=>A.parentNode===T?A.previousElementSibling:T.lastElementChild,P=(e,a)=>e.classList.contains(a);function Xe(e,a,n,c){let d=i(t,"li",{class:e,role:"separator","aria-label":n}),_=i(t,"span",{class:"pmw-chat__relay-label","aria-hidden":"true"});_.append(i(t,"span",{class:a}),n),d.append(_),T.insertBefore(d,c??(A.parentNode===T?A:null)),E()}function We(e,a){for(let n=e?e.previousElementSibling:Ye();n;n=n.previousElementSibling){if(P(n,"pmw-chat__relay")||P(n,"pmw-chat__msg--owner"))return;if(P(n,"pmw-chat__msg--bot")||P(n,"pmw-chat__handback"))break}Xe("pmw-chat__relay","pmw-chat__relay-dot",a?Y(s.relay,{name:a}):s.relayGeneric,e)}function Te(e){for(let a=e?e.previousElementSibling:Ye();;a=a.previousElementSibling){if(!a||P(a,"pmw-chat__handback")||P(a,"pmw-chat__msg--bot"))return;if(P(a,"pmw-chat__relay")||P(a,"pmw-chat__msg--owner"))break}Xe("pmw-chat__handback","pmw-chat__relay-dot pmw-chat__relay-dot--back",s.handback,e)}let Qe=e=>[...T.querySelectorAll("li[data-id]")].find(a=>a.dataset.id===e),I=(e,a,n)=>{e.firstChild.textContent=Y(s.said,{name:Ve(a),text:""}),e.lastChild.textContent=n===""&&a.photos?.length&&!e.querySelector("img")?s.photoSent:n};function U(){A.hidden=!0,o.clearTimeout(ve)}function Le(e){A.hidden=!1,T.append(A),o.clearTimeout(ve),e&&(ve=o.setTimeout(U,e)),E()}function Ze(e,{announce:a}){if(ye.has(e.seq))return;ye.add(e.seq),ae=Math.max(ae,e.seq);let n=Qe(e.id);n||(n=D(e.id,e.role),e.role==="owner"?We(n,e.by||R):e.role==="bot"&&Te(n)),n.classList.remove("is-streaming","is-pending","is-failed"),n.querySelector(".pmw-chat__retry")?.remove(),I(n,e,e.text),e.role==="visitor"?(q.delete(e.id),e.photos?.length&&ne.add(e.id)):(U(),a&&(Ue.textContent=Y(s.said,{name:Ve(e),text:e.text})),S.hidden&&r.setAttribute("data-unread","")),E()}function kt(e,a){U();let n=Qe(e);n||(n=D(e,"bot"),Te(n),n.classList.add("is-streaming"),I(n,{role:"bot"},"")),n.lastChild.textContent+=a,E()}if(g){I(D("demo-question","visitor"),{role:"visitor"},Vt(Pe,X));let e=D("demo-answer","bot");I(e,{role:"bot"},s.demoAnswer),o.addEventListener("hashchange",()=>{Ge=Ke(),G(),I(e,{role:"bot"},s.demoAnswer)})}function oe(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=i(t,"span",{class:"pmw-chat__retry"},`${s.notSent} `),n=i(t,"button",{type:"button"},s.retry);n.addEventListener("click",()=>{e.failed=!1,e.tries=0,a.remove(),e.li.classList.replace("is-failed","is-pending"),h?re()?Ee(e):B():Ae()}),a.append(n),e.li.append(a)}let re=()=>!!C&&C.readyState===1;function Ee(e){if(!(e.failed||!re())){if(e.tries>=ft)return oe(e);e.tries++,C.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function B(){if(!h||H||C)return;o.clearTimeout(xe);let e=o.location.protocol==="https:"?"wss:":"ws:",a=new URLSearchParams({c:h.c,k:h.k}),n=new f(`${e}//${o.location.host}/api/chat/ws?${a}`);C=n,n.onopen=()=>{be=0,ce&&(V(),Le(wt)),N(""),n.send(JSON.stringify({type:"hello",since:ae}));for(let c of q.values())Ee(c)},n.onmessage=c=>{let d;try{d=JSON.parse(c.data)}catch{return}Tt(d)},n.onclose=c=>{C===n&&(C=null);for(let _ of q.values())_.tries>=ft&&oe(_);if(c.code>=4e3&&c.code<5e3)return St(c.code);if(H)return;V(),N(s.reconnecting);let d=ut[Math.min(be,ut.length-1)];be++,xe=o.setTimeout(B,d)}}function St(e){if(e===ht.rpc)return ie(s.reload,!1);ie(s.ended,!0)}function ie(e,a){H=!0,o.clearTimeout(xe),N(""),Fe.textContent=e,te.hidden=!a,ee.hidden=!1,y.hidden=!0;for(let n of q.values())oe(n);E()}function et(e){K=e.mode==="human"?"human":"bot",R=typeof e.ownerName=="string"?e.ownerName:null,G(),K==="human"?We(null,R):Te(null),e.closed&&ie(s.ended,!0)}function Tt(e){switch(e?.type){case"history":typeof e.ownerName=="string"&&(R=e.ownerName);for(let a of e.messages??[])Ze(a,{announce:!1});return et(e);case"message":return yt(),Ze(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return kt(e.id,e.text);case"typing":return e.role==="visitor"?void 0:Le(wt);case"status":return et(e);case"fallback":return nt(e.reason==="cap"?s.fallbackCap:s.fallbackDown);case"lead":return at();case"error":(e.code==="closed"||e.code==="deleted")&&ie(s.ended,!0)}}let tt=!1;function at(){if(!(tt||h?.lead)){tt=!0,h&&(h.lead=!0,ge());try{o.pmw?.event(qt)}catch{}}}let se=new Set;async function Ce(e,a){let n="";if(se.has(e)){se.delete(e);try{l.reset(e)}catch{}}try{n=await l.token(e)}catch{n=""}if(n===null)return se.add(e),{ok:!1,error:s.turnstile,cap:!1,turnstile:!0};let c,d=null;try{c=await m("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:n,lang:X,page:o.location.pathname,...a})}),d=await c.json()}catch{c=null}return se.add(e),c?.ok&&d?{ok:!0,data:d}:{ok:!1,error:c?.status===403?s.turnstile:c?.status===429?s.tooMany:null,cap:c?.status===409&&d?.error==="cap",turnstile:c?.status===403}}let ce=null,V=()=>{o.clearTimeout(ce),ce=null};function Ae(){return j||h||(N(""),Le(),ce=o.setTimeout(()=>N(s.oneMoment),Bt),j=Ce(y,{}).then(e=>{if(j=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,at:Date.now()},ge(),G(),B();return}V(),U();for(let a of q.values())oe(a);if(e.error)return N(e.error);N(""),nt(e.cap?s.fallbackCap:s.fallbackDown)})),j}function nt(e){U(),V(),N(""),$e.textContent=e,b.hidden=!1,y.hidden=!0,g||l.load(),b.querySelector("input")?.focus()}b.addEventListener("submit",async e=>{e.preventDefault();let a=O=>b.elements[O].value.trim(),n={name:a("name"),contact:a("contact"),need:a("need")};if(!n.name||!n.contact||!n.need){$.textContent=s.fallbackMissing;return}$.textContent="";let c=b.querySelector('button[type="submit"]');if(c.disabled)return;c.disabled=!0;let d={fallbackLead:n,...h?.c&&h?.k?{c:h.c,k:h.k}:{}},_=await Ce(b,d);if(!_.ok&&_.turnstile&&(_=await Ce(b,d)),c.disabled=!1,!_.ok){$.textContent=_.error??s.unavailable;return}b.replaceChildren(i(t,"p",{class:"pmw-chat__fb-done",role:"status"},s.fallbackSent)),at()});let Lt={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function Et(e){let a,n;try{a=await x(e),n=await v(a)}catch{z.textContent=s.photoUnreadable;return}let c=u(),d=D(c,"visitor");d.classList.add("is-pending");let _=i(t,"img",{class:"pmw-chat__photo",src:n,alt:s.photoAlt});_.addEventListener("load",()=>E(),{once:!0}),d.insertBefore(_,d.lastChild),I(d,{role:"visitor"},""),E(!0),h||await Ae();let O=null;if(h){let le=new URLSearchParams({c:h.c,k:h.k,id:c});try{O=await m(`/api/chat/photo?${le}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:a})}catch{O=null}}if(O?.ok){d.classList.remove("is-pending"),ne.add(c),C||B();return}d.remove(),z.textContent=s[Lt[O?.status]]??s.photoFailed}F?.addEventListener("change",async()=>{let e=[...F.files??[]];if(F.value="",z.textContent="",H||g||e.length===0)return;let a=It-ne.size-ke;e.length>a&&(z.textContent=s.photoLimit);let n=e.slice(0,Math.max(0,a));ke+=n.length;for(let c of n)try{await Et(c)}finally{ke--}});function ot(){let e=L.value.trim();if(!e||H||g)return;L.value="",rt();let a={id:u(),text:e.slice(0,2e3),tries:0,failed:!1};a.li=D(a.id,"visitor"),a.li.classList.add("is-pending"),I(a.li,{role:"visitor"},a.text),q.set(a.id,a),E(!0),h?re()?Ee(a):B():Ae()}y.addEventListener("submit",e=>{e.preventDefault(),ot()}),L.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),ot())});let rt=()=>{L.style.height="auto",L.style.height=L.value?`${Math.min(L.scrollHeight,132)}px`:""};L.addEventListener("input",()=>{rt();let e=Date.now();re()&&e-je>$t&&(je=e,C.send(JSON.stringify({type:"typing"})))}),te.addEventListener("click",()=>{h=null;try{o.sessionStorage.removeItem(he)}catch{}try{C?.close(1e3)}catch{}C=null,H=!1,ae=0,ye.clear(),q.clear(),ne.clear(),z.textContent="",K="bot",R=null,G();for(let e of[...T.querySelectorAll("li[data-id], .pmw-chat__relay, .pmw-chat__handback")])e.remove();U(),V(),ee.hidden=!0,y.hidden=!1,L.focus()});let Ct=()=>[...S.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));S.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),Oe();return}if(e.key!=="Tab")return;let a=Ct();if(a.length===0)return;let n=a[0],c=a[a.length-1];e.shiftKey&&t.activeElement===n?(e.preventDefault(),c.focus()):!e.shiftKey&&t.activeElement===c&&(e.preventDefault(),n.focus())}),Q.addEventListener("click",e=>Oe({keyboard:e.detail===0}));function At(){g||l.load(),S.hidden=!1,S.inert=!1,r.setAttribute("data-open",""),r.removeAttribute("data-unread"),h&&B(),(g?Q:y.hidden?b.hidden?te:b.querySelector("input"):L)?.focus(),E(!0,!0)}function Oe({keyboard:e=!0}={}){S.hidden=!0,S.inert=!0,r.removeAttribute("data-open"),e&&Re?.focus(),r.dispatchEvent(new o.FocusEvent("focusout",{relatedTarget:e?Re:null}))}return g||Me(r,()=>l.load(),{win:o,settleMs:Dt}),r.pmwChat={open:At,close:Oe,panel:S},r.pmwChat}export{ut as BACKOFF_MS,qt as GOAL,Ut as IDLE_MS,It as MAX_PHOTOS,ft as MAX_TRIES,zt as PHOTO_EDGE,Ht as PHOTO_QUALITY,Ft as PIN_SLACK,he as SESSION_KEY,Bt as SLOW_START_MS,Dt as WARM_MS,Vt as demoQuestion,da as mountChat,Wt as retryable};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmeweb/plugin-chat",
3
- "version": "1.3.1",
3
+ "version": "1.3.2",
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/widget.css CHANGED
@@ -14,7 +14,10 @@
14
14
  background: var(--pmw-chat-panel-bg, #ffffff); color: var(--pmw-chat-fg, #1f2328);
15
15
  font-family: var(--pmw-chat-font, inherit); font-size: 16px; line-height: 1.45;
16
16
  }
17
- .pmw-chat__panel[hidden], .pmw-chat__panel [hidden] { display: none !important; }
17
+ /* Closed, the panel stays laid out, transparent and inert (widget.js « TURNSTILE »: the challenge
18
+ is rendered into a box with a real size, never a display:none one). */
19
+ .pmw-chat__panel[hidden] { display: flex !important; opacity: 0; pointer-events: none; }
20
+ .pmw-chat__panel [hidden] { display: none !important; }
18
21
  @media (min-width: 640px) {
19
22
  .pmw-chat__panel {
20
23
  inset: auto;
@@ -151,16 +154,19 @@
151
154
  }
152
155
  .pmw-chat__send:disabled, .pmw-chat__box textarea:disabled { cursor: not-allowed; opacity: 0.6; }
153
156
  /* Turnstile's row (widget.js « TURNSTILE »): collapsed, with no height and no gap, until Cloudflare
154
- asks the visitor to act (its before-interactive or error callback sets data-ask); collapsed again
155
- on success. Full width, aligned with the field (the widget is data-size flexible, 300 px minimum:
156
- the composer's inside is 358 px on a 390 px phone, 346 px in the 380 px desktop panel). In the
157
- composer it sits ABOVE the field, which therefore never moves when it opens or closes. */
157
+ asks the visitor to act (its before-interactive callback sets data-ask); collapsed again on
158
+ success. Collapsed, its box keeps the row's full width and its natural height, out of the flow,
159
+ transparent and without pointer events (and inert), so the challenge never lays out in a 0 px box.
160
+ Full width, aligned with the field (the widget is size flexible, 300 px minimum: the composer's
161
+ inside is 358 px on a 390 px phone, 346 px in the 380 px desktop panel). In the composer it sits
162
+ ABOVE the field, which therefore never moves when it opens or closes. */
158
163
  .pmw-chat__check {
159
- display: grid; grid-template-rows: 0fr; margin-bottom: calc(-1 * var(--pmw-chat-gap, 0px));
164
+ position: relative; display: grid; grid-template-rows: 0fr; margin-bottom: calc(-1 * var(--pmw-chat-gap, 0px));
160
165
  transition: grid-template-rows 0.25s ease, margin-bottom 0.25s ease;
161
166
  }
162
167
  .pmw-chat__check[data-ask] { grid-template-rows: 1fr; margin-bottom: 4px; }
163
168
  .pmw-chat__check-in { min-height: 0; overflow: hidden; }
169
+ .pmw-chat__check:not([data-ask]) .pmw-chat__check-in { position: absolute; top: 0; left: 0; right: 0; overflow: visible; opacity: 0; pointer-events: none; }
164
170
  @media (prefers-reduced-motion: reduce) {
165
171
  .pmw-chat__check { transition: none; }
166
172
  }
@@ -197,3 +203,9 @@
197
203
  content: ''; position: absolute; top: 2px; left: 38px; width: 12px; height: 12px; border-radius: 50%;
198
204
  background: var(--pmw-chat-error, #b42318); box-shadow: 0 0 0 2px #ffffff;
199
205
  }
206
+ /* The contact form's confirmation: a box of its own, never plain text under the thread
207
+ (operator, 2026-11-13). #14532d on #e9f6ee is 8.19:1 (a11y-tools/contrast.mjs), whatever the site's palette. */
208
+ .pmw-chat__fb-done {
209
+ margin: 12px 0 0; padding: 12px 14px; border-radius: 12px;
210
+ background: #e9f6ee; color: #14532d; border: 1px solid #b7dfc4; font-weight: 500;
211
+ }
package/src/widget.js CHANGED
@@ -38,7 +38,7 @@
38
38
  // and a hint saying why. The client sees their face and colours in the « Contenu » preview.
39
39
  import css from './widget.css?inline'
40
40
  import { isCanonical, onApproach } from '@pmeweb/loader'
41
- import { addScript as loadTurnstile, tokenReady } from '@pmeweb/turnstile'
41
+ import { addScript as loadTurnstile, TOKEN_TIMEOUT_MS } from '@pmeweb/turnstile'
42
42
  import { CLOSE, MAX_TEXT } from './protocol.js'
43
43
  import { fill, langOf, stringsFor } from './strings.js'
44
44
 
@@ -51,6 +51,8 @@ export const PHOTO_EDGE = 1600
51
51
  export const PHOTO_QUALITY = 0.85
52
52
  /** How long after the page has painted (and the widget was imported) Turnstile starts, unasked. */
53
53
  export const WARM_MS = 3000
54
+ /** A conversation with no message for this long is over: the next one starts fresh (and pmeweb-chat closes it). */
55
+ export const IDLE_MS = 2 * 60 * 60 * 1000
54
56
  /** A first start slower than this shows « Un instant… » under the typing dots. */
55
57
  export const SLOW_START_MS = 8000
56
58
  /** How close to the bottom of the thread (px) still counts as « at the bottom ». */
@@ -106,25 +108,50 @@ function el(doc, tag, attrs = {}, text) {
106
108
  // never imports this file and never fetches api.js; a visitor who reads for a few seconds before
107
109
  // opening the panel usually finds the token minted.
108
110
  //
109
- // THE WIDGETS RENDER HIDDEN, in the closed panel (a display:none iframe still runs: measured with
110
- // Cloudflare's test keys, a token in ~1.9 s). Each sits in a row that is COLLAPSED until Cloudflare
111
- // itself asks for the visitor: its `before-interactive` and `error` callbacks open the row, its
112
- // success callback collapses it again (so « Succès ! » goes away), and a later expiry that needs a
113
- // click opens it again. The rule « never hide the box with a guessed rule » holds: the row follows
114
- // Cloudflare's callbacks, never the iframe's size. Implicit rendering only scans the page once, when
115
- // api.js runs, so a widget added after a contact form already loaded api.js is rendered here.
111
+ // EXPLICIT RENDERING, ONE RENDER PER BOX (1.3.2, after a 600010 on pmeweb.ca). api.js is the page's
112
+ // one copy, the same URL @pmeweb/turnstile adds for a contact form, and it renders IMPLICITLY every
113
+ // `.cf-turnstile` it finds: that is the form's widget, and the chat never renders, resets or alters
114
+ // a widget it does not own. Ours carry no `cf-turnstile` class, so implicit rendering cannot reach
115
+ // them; the chat renders each box itself, with its options and callbacks as an object (no global
116
+ // callback names), once: a `data-pmw-rendered` mark set before the call is the guard, never the
117
+ // DOM Cloudflare inserts. `turnstile.ready()` is not used: with api.js added async it throws
118
+ // (« Remove async/defer ... », measured 2026-11-13); `turnstile.render` existing is the signal, and
119
+ // api.js has finished its implicit pass before `window.turnstile` is visible (measured, Chromium
120
+ // and WebKit).
116
121
  //
117
- // THE TOKEN: Turnstile refreshes an unused one before its 300 s expiry (refresh-expired: auto); after
118
- // each post the widget is reset (a token is single-use), only while its row is not asking: a reset
119
- // would wipe the box the visitor is solving. `reset(el)` names our widget, never every widget on the
120
- // page (@pmeweb/turnstile rule 4). Its own resetToken is not used: it keys on the box's height, and
121
- // a solved box keeps its height inside our collapsed row.
122
- const defaultTurnstile = (win, widgets) => {
122
+ // A BOX WITH REAL DIMENSIONS. 1.3.1 rendered into a row collapsed to 0 px, in a panel and a form
123
+ // that were display:none (measured: the render saw a 0 px box). Now the closed panel stays laid out
124
+ // (transparent, inert, no pointer events), the collapsed row keeps its box at full width and natural
125
+ // height but transparent and out of the flow, and the contact form's box is rendered only once that
126
+ // form is shown. The row opens only when Cloudflare itself asks for the visitor
127
+ // (`before-interactive-callback`) and collapses again on success, so « Succès ! » goes away. The
128
+ // rule « never hide the box with a guessed rule » holds: the row follows Cloudflare's callbacks,
129
+ // never the iframe's size.
130
+ //
131
+ // ERRORS. A retryable code (300*, 600*, 110600, 110620: Cloudflare's docs say reset and retry) resets
132
+ // the widget ONCE, silently. A second failure keeps the row collapsed and marks the box failed: the
133
+ // next post does not wait for a token, it shows the neutral « L'envoi n'a pas abouti » line, and the
134
+ // post after that starts a fresh challenge. Any other code is a failure at once.
135
+ //
136
+ // THE TOKEN: Turnstile refreshes an unused one before its 300 s expiry (refresh-expired: auto). A
137
+ // used one is replaced just before the next post (« spent » below), never while the row is asking: a
138
+ // reset would wipe the box the visitor is solving. `reset(id)` names our widget, never every widget
139
+ // on the page (@pmeweb/turnstile rule 4).
140
+ const RETRYABLE = /^(?:300\d{3}|600\d{3}|110600|110620)$/
141
+ export const retryable = (code) => RETRYABLE.test(String(code ?? ''))
142
+ const POLL_MS = 200
143
+
144
+ const defaultTurnstile = (win, boxes) => {
123
145
  let polling = false
124
- const render = () => {
146
+ const draw = (b) => {
125
147
  const ts = win.turnstile
126
- if (!ts?.render) return false
127
- for (const w of widgets) if (!w.childElementCount) try { ts.render(w) } catch { /* left to tokenReady's timeout */ }
148
+ if (b.el.hasAttribute('data-pmw-rendered') || !b.shown()) return
149
+ b.el.setAttribute('data-pmw-rendered', '')
150
+ try { b.id = ts.render(b.el, b.params) } catch { /* the token call gives up after TOKEN_TIMEOUT_MS */ }
151
+ }
152
+ const render = () => {
153
+ if (!win.turnstile?.render) return false
154
+ for (const b of boxes) draw(b)
128
155
  return true
129
156
  }
130
157
  const load = () => {
@@ -135,16 +162,43 @@ const defaultTurnstile = (win, widgets) => {
135
162
  const tick = () => { if (!render() && ++n < 600) win.setTimeout(tick, 100) }
136
163
  win.setTimeout(tick, 100)
137
164
  }
138
- for (const w of widgets) { const f = w.closest('form'); if (f) f.pmwTurnstileLoad = load }
165
+ const of = (form) => boxes.find((b) => b.form === form)
166
+ for (const b of boxes) {
167
+ b.params['error-callback'] = (code) => {
168
+ b.ask(false)
169
+ if (retryable(code) && !b.retried && b.id !== undefined) {
170
+ b.retried = true
171
+ win.setTimeout(() => { try { win.turnstile?.reset(b.id) } catch { b.failed = true } }, 0)
172
+ } else {
173
+ b.failed = true
174
+ }
175
+ return true
176
+ }
177
+ }
139
178
  return {
140
179
  load,
180
+ /** The token, '' after TOKEN_TIMEOUT_MS with no challenge on screen, null when the box failed. */
141
181
  async token(form) {
142
- await tokenReady(form, { win })
143
- return form.querySelector('input[name="cf-turnstile-response"]')?.value ?? ''
182
+ load()
183
+ const b = of(form)
184
+ let waited = 0
185
+ for (;;) {
186
+ if (b?.failed) return null
187
+ const v = form.querySelector('input[name="cf-turnstile-response"]')?.value ?? ''
188
+ if (v) return v
189
+ if (!b?.asking()) {
190
+ if (waited >= TOKEN_TIMEOUT_MS) return ''
191
+ waited += POLL_MS
192
+ }
193
+ await new Promise((r) => win.setTimeout(r, POLL_MS))
194
+ }
144
195
  },
145
196
  reset(form) {
146
- const w = form.querySelector('.cf-turnstile')
147
- if (w && !w.closest('[data-ask]')) win.turnstile?.reset(w)
197
+ const b = of(form)
198
+ if (!b || b.id === undefined || b.asking()) return
199
+ b.failed = false
200
+ b.retried = false
201
+ win.turnstile?.reset(b.id)
148
202
  },
149
203
  }
150
204
  }
@@ -155,7 +209,7 @@ export function mountChat(root, deps = {}) {
155
209
  const doc = win.document
156
210
  const WS = deps.WebSocket ?? win.WebSocket
157
211
  const fetchImpl = deps.fetch ?? ((...a) => win.fetch(...a))
158
- const widgets = []
212
+ const boxes = []
159
213
  let turnstile = deps.turnstile ?? null
160
214
  const uuid = deps.uuid ?? (() => win.crypto.randomUUID())
161
215
  const shrink = deps.shrink ?? ((file) => shrinkPhoto(win, file))
@@ -179,6 +233,9 @@ export function mountChat(root, deps = {}) {
179
233
 
180
234
  // ---- the panel --------------------------------------------------------------------------
181
235
  const panel = el(doc, 'div', { class: 'pmw-chat__panel', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'pmw-chat-name', hidden: true })
236
+ // Closed, the panel stays laid out (transparent, no pointer events) so its Turnstile box has a real
237
+ // size (« TURNSTILE » above); inert keeps it out of the tab order and the accessibility tree.
238
+ panel.inert = true
182
239
  const head = el(doc, 'div', { class: 'pmw-chat__head' })
183
240
  const avatar = root.querySelector('.pmw-chat__avatar')?.cloneNode(true) ?? el(doc, 'span', { class: 'pmw-chat__avatar', 'aria-hidden': 'true' })
184
241
  // An icon may hold a <mask id>: the clone gets its own ids, since the bubble (and the original
@@ -215,22 +272,23 @@ export function mountChat(root, deps = {}) {
215
272
  const state = el(doc, 'p', { class: 'pmw-chat__state', role: 'status' })
216
273
  const announcer = el(doc, 'p', { class: 'pmw-chat__sr', 'aria-live': 'polite' })
217
274
 
218
- // A widget in its row (see « TURNSTILE » above). Implicit rendering reads callbacks as global
219
- // function names, one set per widget (`c` the composer, `f` the contact form).
220
- const widget = (key) => {
275
+ // A widget in its row (see « TURNSTILE » above): the box the chat renders explicitly, once.
276
+ const widget = (form, shown) => {
221
277
  const row = el(doc, 'div', { class: 'pmw-chat__check' })
222
278
  const inner = el(doc, 'div', { class: 'pmw-chat__check-in' })
223
- const cb = `pmwChatTurnstile${key}`
224
- const ask = (on) => () => { row.toggleAttribute('data-ask', on); follow() }
225
- win[`${cb}Ask`] = ask(true)
226
- win[`${cb}Error`] = ask(true)
227
- win[`${cb}Done`] = ask(false)
228
- const w = el(doc, 'div', {
229
- class: 'cf-turnstile pmw-turnstile', 'data-sitekey': cfg.siteKey ?? '', 'data-action': 'chat', 'data-theme': 'light',
230
- 'data-language': lang, 'data-appearance': 'interaction-only', 'data-size': 'flexible',
231
- 'data-callback': `${cb}Done`, 'data-before-interactive-callback': `${cb}Ask`, 'data-error-callback': `${cb}Error`,
232
- })
233
- widgets.push(w)
279
+ inner.inert = true
280
+ const ask = (on) => { row.toggleAttribute('data-ask', on); inner.inert = !on; follow() }
281
+ const w = el(doc, 'div', { class: 'pmw-chat__turnstile' })
282
+ const box = {
283
+ el: w, form, shown, ask, asking: () => row.hasAttribute('data-ask'),
284
+ params: {
285
+ sitekey: cfg.siteKey ?? '', action: 'chat', theme: 'light', language: lang,
286
+ appearance: 'interaction-only', size: 'flexible', 'refresh-expired': 'auto',
287
+ callback: () => { box.retried = false; box.failed = false; ask(false) },
288
+ 'before-interactive-callback': () => ask(true),
289
+ },
290
+ }
291
+ boxes.push(box)
234
292
  inner.append(w)
235
293
  row.append(inner)
236
294
  return row
@@ -243,7 +301,7 @@ export function mountChat(root, deps = {}) {
243
301
  box.append(el(doc, 'label', { class: 'pmw-chat__sr', for: 'pmw-chat-input' }, s.messageLabel), input, send)
244
302
  // The check row sits ABOVE the field: the composer is pinned to the panel's bottom, so the row
245
303
  // opening or collapsing never moves the field the visitor is typing in.
246
- if (!demo) composer.append(widget('c'))
304
+ if (!demo) composer.append(widget(composer, () => true))
247
305
  composer.append(box)
248
306
  if (demo) composer.append(el(doc, 'p', { class: 'pmw-chat__hint', id: 'pmw-chat-demo-hint' }, s.demoHint))
249
307
  let picker = null
@@ -279,12 +337,12 @@ export function mountChat(root, deps = {}) {
279
337
  field('need', 'textarea', { rows: '3', maxlength: '2000' }),
280
338
  fbError, el(doc, 'button', { type: 'submit', class: 'pmw-chat__send' }, s.fallbackSend),
281
339
  )
282
- if (!demo) fallback.insertBefore(widget('f'), fbError)
340
+ if (!demo) fallback.insertBefore(widget(fallback, () => !fallback.hidden), fbError)
283
341
  const body = el(doc, 'div', { class: 'pmw-chat__body' })
284
342
  body.append(log, state, ended, fallback)
285
343
  panel.append(head, body, composer, announcer)
286
344
  root.append(panel)
287
- turnstile ??= defaultTurnstile(win, widgets)
345
+ turnstile ??= defaultTurnstile(win, boxes)
288
346
 
289
347
  // ---- following the thread ---------------------------------------------------------------
290
348
  // PINNED (operator, 2026-11-13): while the visitor is at the bottom (within PIN_SLACK), every
@@ -326,7 +384,14 @@ export function mountChat(root, deps = {}) {
326
384
  // ---- state ------------------------------------------------------------------------------
327
385
  let session = null
328
386
  if (!demo) try { session = JSON.parse(win.sessionStorage.getItem(SESSION_KEY) ?? 'null') } catch { session = null }
387
+ // TWO HOURS WITHOUT A MESSAGE END IT (operator, 2026-11-13): a tab left open overnight starts a
388
+ // new conversation; pmeweb-chat closes the old one on its side too. `at` is the last message seen.
389
+ if (session && typeof session.at === 'number' && !(Date.now() - session.at < IDLE_MS)) {
390
+ session = null
391
+ try { win.sessionStorage.removeItem(SESSION_KEY) } catch { /* this tab only */ }
392
+ }
329
393
  const save = () => { try { win.sessionStorage.setItem(SESSION_KEY, JSON.stringify(session)) } catch { /* this tab only */ } }
394
+ const touch = () => { if (session) { session.at = Date.now(); save() } }
330
395
  let ws = null
331
396
  let attempt = 0
332
397
  let retryTimer = null
@@ -563,6 +628,7 @@ export function mountChat(root, deps = {}) {
563
628
  for (const x of m.messages ?? []) render(x, { announce: false })
564
629
  return status(m)
565
630
  case 'message':
631
+ touch()
566
632
  return render(m.msg, { announce: m.msg?.role !== 'visitor' })
567
633
  case 'delta':
568
634
  return delta(m.id, m.text)
@@ -589,9 +655,23 @@ export function mountChat(root, deps = {}) {
589
655
  }
590
656
 
591
657
  // ---- starting a conversation ------------------------------------------------------------
658
+ /** Forms whose Turnstile token was used: reset just before the next post, never right after. */
659
+ const spent = new Set()
592
660
  async function post(form, extra) {
593
661
  let token = ''
662
+ // A used token is replaced only when a NEW one is needed (a restart, the form, a retry): a reset
663
+ // right after a successful start made Cloudflare show its checkbox under a conversation that
664
+ // needs no check any more (messages go over the socket; operator, 2026-11-13).
665
+ if (spent.has(form)) {
666
+ spent.delete(form)
667
+ try { turnstile.reset(form) } catch { /* the token call below waits for a fresh one */ }
668
+ }
594
669
  try { token = await turnstile.token(form) } catch { token = '' }
670
+ // The box failed twice (« TURNSTILE » above): nothing to post; the next post starts a fresh one.
671
+ if (token === null) {
672
+ spent.add(form)
673
+ return { ok: false, error: s.turnstile, cap: false, turnstile: true }
674
+ }
595
675
  let res
596
676
  let data = null
597
677
  try {
@@ -604,10 +684,10 @@ export function mountChat(root, deps = {}) {
604
684
  } catch {
605
685
  res = null
606
686
  }
607
- try { turnstile.reset(form) } catch { /* a token is single-use; the next post asks again */ }
687
+ spent.add(form)
608
688
  if (res?.ok && data) return { ok: true, data }
609
689
  const error = res?.status === 403 ? s.turnstile : res?.status === 429 ? s.tooMany : null
610
- return { ok: false, error, cap: res?.status === 409 && data?.error === 'cap' }
690
+ return { ok: false, error, cap: res?.status === 409 && data?.error === 'cap', turnstile: res?.status === 403 }
611
691
  }
612
692
  // THE FIRST START (operator, 2026-11-13) reads like any reply: the typing dots at once, no
613
693
  // « Connexion… ». Only a start slower than SLOW_START_MS adds « Un instant… »; the socket's open
@@ -625,7 +705,7 @@ export function mountChat(root, deps = {}) {
625
705
  starting = post(composer, {}).then((r) => {
626
706
  starting = null
627
707
  if (r.ok && r.data.conversationId && r.data.visitorKey) {
628
- session = { c: r.data.conversationId, k: r.data.visitorKey, botName: typeof r.data.botName === 'string' ? r.data.botName : null }
708
+ session = { c: r.data.conversationId, k: r.data.visitorKey, botName: typeof r.data.botName === 'string' ? r.data.botName : null, at: Date.now() }
629
709
  save()
630
710
  header()
631
711
  connect()
@@ -642,9 +722,13 @@ export function mountChat(root, deps = {}) {
642
722
  return starting
643
723
  }
644
724
  function showFallback(text) {
725
+ // Nobody is writing any more: the dots and « Un instant… » go with the form (operator, 2026-11-13).
726
+ hideTyping(); notSlow(); showState('')
645
727
  fbIntro.textContent = text
646
728
  fallback.hidden = false
647
729
  composer.hidden = true
730
+ // Its Turnstile box is rendered now that it has a size (« TURNSTILE » above).
731
+ if (!demo) turnstile.load()
648
732
  fallback.querySelector('input')?.focus()
649
733
  }
650
734
  fallback.addEventListener('submit', async (e) => {
@@ -656,7 +740,17 @@ export function mountChat(root, deps = {}) {
656
740
  return
657
741
  }
658
742
  fbError.textContent = ''
659
- const r = await post(fallback, { fallbackLead: fields })
743
+ // One request per press: the button waits for the answer (a second press made a second lead).
744
+ const btn = fallback.querySelector('button[type="submit"]')
745
+ if (btn.disabled) return
746
+ btn.disabled = true
747
+ // The conversation the visitor had joins the request, so the link shows its thread (1.3.2).
748
+ const extra = { fallbackLead: fields, ...(session?.c && session?.k ? { c: session.c, k: session.k } : {}) }
749
+ let r = await post(fallback, extra)
750
+ // A refused check is retried ONCE with a fresh token before the visitor sees anything: the
751
+ // first token can be one Cloudflare already redeemed or let expire (operator, 2026-11-13).
752
+ if (!r.ok && r.turnstile) r = await post(fallback, extra)
753
+ btn.disabled = false
660
754
  if (!r.ok) {
661
755
  fbError.textContent = r.error ?? s.unavailable
662
756
  return
@@ -788,21 +882,28 @@ export function mountChat(root, deps = {}) {
788
882
  if (e.shiftKey && doc.activeElement === first) { e.preventDefault(); last.focus() }
789
883
  else if (!e.shiftKey && doc.activeElement === last) { e.preventDefault(); first.focus() }
790
884
  })
791
- closeBtn.addEventListener('click', () => close())
885
+ // A mouse or touch close (click.detail > 0) does not park focus on the bubble: a focused bubble
886
+ // never steps aside for the footer (operator, 2026-11-13). Keyboard closes (Enter, Escape) still do.
887
+ closeBtn.addEventListener('click', (e) => close({ keyboard: e.detail === 0 }))
792
888
 
793
889
  function show() {
794
890
  if (!demo) turnstile.load()
795
891
  panel.hidden = false
892
+ panel.inert = false
796
893
  root.setAttribute('data-open', '')
797
894
  root.removeAttribute('data-unread')
798
895
  if (session) connect()
799
896
  ;(demo ? closeBtn : composer.hidden ? (fallback.hidden ? restart : fallback.querySelector('input')) : input)?.focus()
800
897
  follow(true, true)
801
898
  }
802
- function close() {
899
+ function close({ keyboard = true } = {}) {
803
900
  panel.hidden = true
901
+ panel.inert = true
804
902
  root.removeAttribute('data-open')
805
- bubble?.focus()
903
+ if (keyboard) bubble?.focus()
904
+ // The trigger re-checks the footer on a focusout (trigger.js sync): tell it the panel is shut, so
905
+ // the bubble steps aside at once when the footer is already in view.
906
+ root.dispatchEvent(new win.FocusEvent('focusout', { relatedTarget: keyboard ? bubble : null }))
806
907
  }
807
908
 
808
909
  // The early start (« TURNSTILE » above). Off the real hosts there is nothing to start.
package/src/worker.js CHANGED
@@ -32,7 +32,7 @@ import { verifyTurnstile } from '@pmeweb/turnstile/worker'
32
32
  import { RPC_HEADER, RPC_VERSION } from './protocol.js'
33
33
 
34
34
  /** This package's version, reported by the health route (a test holds it equal to package.json). */
35
- export const VERSION = '1.3.1'
35
+ export const VERSION = '1.3.2'
36
36
  export { RPC_VERSION }
37
37
 
38
38
  const SERVICE = 'https://pmeweb-chat.internal'
@@ -117,6 +117,12 @@ async function start(request, url, env, { config, lang }) {
117
117
  if (config?.sms === 'voipms') body.sms = 'voipms'
118
118
  if (config?.photos === true) body.photos = true
119
119
  if (lead) body.fallbackLead = lead
120
+ // The conversation the visitor already had, so the form's request joins its thread (1.3.2); the
121
+ // service checks the key, as it does for the socket.
122
+ if (lead && typeof b.c === 'string' && /^[0-9a-f-]{36}$/i.test(b.c) && typeof b.k === 'string' && /^[A-Za-z0-9_-]{16,64}$/.test(b.k)) {
123
+ body.conversationId = b.c
124
+ body.visitorKey = b.k
125
+ }
120
126
 
121
127
  const headers = { 'content-type': 'application/json', [RPC_HEADER]: String(RPC_VERSION) }
122
128
  const ip = request.headers.get('cf-connecting-ip')