@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 +19 -9
- package/dist/widget.js +2 -2
- package/package.json +1 -1
- package/src/widget.css +18 -6
- package/src/widget.js +148 -47
- package/src/worker.js +7 -1
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`)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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,
|
|
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
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
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
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
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
|
|
146
|
+
const draw = (b) => {
|
|
125
147
|
const ts = win.turnstile
|
|
126
|
-
if (!
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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
|
|
147
|
-
if (
|
|
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
|
|
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)
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
const ask = (on) =>
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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')
|