@pmeweb/plugin-chat 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +2 -2
- package/README.md +36 -3
- package/dist/trigger.iife.js +1 -1
- package/dist/widget.js +2 -2
- package/package.json +1 -1
- package/src/strings.js +4 -2
- package/src/trigger.js +19 -9
- package/src/widget.css +27 -9
- package/src/widget.js +196 -40
- package/src/worker.js +1 -1
package/INSTALL.md
CHANGED
|
@@ -149,8 +149,8 @@ package's, approved once.
|
|
|
149
149
|
and photos paragraphs when those keys are set), only while `enabled` is true: it needs
|
|
150
150
|
`@pmeweb/consent` 1.1.0 or later (manifest `activeWhen`; 1.0.0 would print it switched off, and
|
|
151
151
|
check-plugins refuses that).
|
|
152
|
-
10. **Not in `pmeweb.json` `forms`**: the chat's Turnstile widget renders only
|
|
153
|
-
|
|
152
|
+
10. **Not in `pmeweb.json` `forms`**: the chat's Turnstile widget renders only after the visitor
|
|
153
|
+
interacts with the page or the opener appears (20 s), so the portal's form-health browser check would find nothing on a page load.
|
|
154
154
|
11. `npm run build && node ../../a11y-tools/check-plugins.mjs .` until PASS; `npm run check`.
|
|
155
155
|
12. **Portal** (« Modules › Assistant virtuel »): PmeWeb sets `alertEmails`
|
|
156
156
|
(`alertEmails` is needed before the switch goes on: pmeweb-chat refuses a site without it), `alertPhone` if texts are on, and
|
package/README.md
CHANGED
|
@@ -52,8 +52,39 @@ file ».
|
|
|
52
52
|
face (`avatar`: photos, illustration or a « Sans visage » icon), the icon's colour (`avatarBg`,
|
|
53
53
|
default the site's accent) and the name (`botName`, which ships with « Publier »). `logo` stays
|
|
54
54
|
PmeWeb's, set at install.
|
|
55
|
-
- **Nothing loads with the page.** The bubble is HTML and CSS
|
|
56
|
-
|
|
55
|
+
- **Nothing loads with the page.** The bubble is HTML and CSS (the inline trigger, under the
|
|
56
|
+
manifest's `inlineBudget`). The panel's code (`widget.js`) arrives on the visitor's first sign of
|
|
57
|
+
life anywhere on the page (a scroll, a pointer moving, a press, a key or a focus) or when the
|
|
58
|
+
opener appears (20 s), whichever comes first. The socket opens only on the first message.
|
|
59
|
+
- **Turnstile starts before the panel opens, never in a test** (operator, 2026-11-13). Loaded when
|
|
60
|
+
the panel opened, the challenge had no time to observe the visitor and often showed its
|
|
61
|
+
« Vérifiez que vous êtes humain » box. It now starts once `widget.js` is in AND the page has
|
|
62
|
+
painted (`load`, first contentful paint, LCP, whichever is latest) plus `WARM_MS` (3 s), or at
|
|
63
|
+
once on a press or a focus in the chat, or when the panel opens, whichever comes first. A loaded
|
|
64
|
+
`api.js` racing the first paint costs up to 30 PageSpeed points (`@pmeweb/turnstile`'s README);
|
|
65
|
+
Lighthouse and PageSpeed never scroll, move a pointer or type, so they fetch neither `widget.js`
|
|
66
|
+
nor `api.js` before the opener's 20 s. Measured on a demo page with the chat on (1.3.1,
|
|
67
|
+
Lighthouse 12.6.1 mobile, 3 runs): no request to `challenges.cloudflare.com` or `widget.js`,
|
|
68
|
+
performance 100. In Chromium at 390 px: a visitor scrolling at 1 s gets `widget.js` at 1.0 s,
|
|
69
|
+
`api.js` at 4.0 s; one who never touches the page, `widget.js` with the opener at 20.0 s,
|
|
70
|
+
`api.js` at 23.0 s; in both the token is minted before the panel opens.
|
|
71
|
+
- **The Cloudflare box shows only when Cloudflare asks.** The widgets (`interaction-only`) render
|
|
72
|
+
hidden in the closed panel and mint their token there. Each sits in its own row, collapsed until
|
|
73
|
+
Turnstile's `before-interactive` or `error` callback opens it; its success callback collapses it
|
|
74
|
+
again (« Succès ! » goes away, 0.25 s, at once under reduced motion), and a later expiry that
|
|
75
|
+
needs a click opens it again. In the composer the row sits above the field, so the field never
|
|
76
|
+
moves. An unused token is refreshed by Turnstile before its 300 s expiry; after each post the
|
|
77
|
+
widget is reset (`turnstile.reset(widget)`, never while its row is asking). A widget added after
|
|
78
|
+
another form already loaded `api.js` is rendered by the chat itself (implicit rendering scans the
|
|
79
|
+
page once).
|
|
80
|
+
- **The first message reads like a reply.** The typing dots show at once while the conversation
|
|
81
|
+
starts (token, `/api/chat/start`, socket); « Un instant… » joins them only past 8 s. Nothing is
|
|
82
|
+
started before the visitor writes: pmeweb-chat counts every start toward the visitor's
|
|
83
|
+
conversations of the day, so only the token is prepared ahead.
|
|
84
|
+
- **The thread stays at the bottom** while the visitor is there (within 40 px): streamed text, the
|
|
85
|
+
final message, the dots, a photo once decoded, the takeover and hand-back lines, the end block, the
|
|
86
|
+
composer growing. One smooth scroll per frame (instant under reduced motion). A visitor who
|
|
87
|
+
scrolled up to read stays put; sending a message always scrolls down.
|
|
57
88
|
- **Openers.** One line per page, from the site's dictionary (`chat.openers`, `chat.defaultOpener`). At most 50 characters (two lines on a phone): a longer one fails the build, naming it.
|
|
58
89
|
Shown once per visit (20 s, 30 % of the page, or exit intent with a mouse), never while the panel
|
|
59
90
|
is open, never while the footer is in view (it waits for the next of those triggers). On a phone
|
|
@@ -70,7 +101,9 @@ file ».
|
|
|
70
101
|
(the first one wins over the footer). Never while the panel is open or while focus is in the
|
|
71
102
|
bubble or the opener (a focusout re-checks). Reduced motion: hidden and shown at once.
|
|
72
103
|
- **Never passing for a person.** « Assistant virtuel » is always in text; the green dot appears
|
|
73
|
-
only when the owner has taken over, with the owner's name.
|
|
104
|
+
only when the owner has taken over, with the owner's name. Where the owner takes over, the thread
|
|
105
|
+
shows « {nom} a pris le relais »; where they hand back (« Rendre à l’assistant »), « L’assistant
|
|
106
|
+
virtuel a repris la conversation » (the dot muted), live and on replay, once per hand-back.
|
|
74
107
|
- **The socket.** A drop (1006, a forced deploy of pmeweb-chat) reconnects with backoff 1 to 30 s
|
|
75
108
|
and catches up from the last message seen; a message typed meanwhile is queued and sent on open,
|
|
76
109
|
and after 3 sends without an echo shows « Non envoyé, Réessayer ». A 44xx close never reconnects.
|
package/dist/trigger.iife.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{function
|
|
1
|
+
(()=>{function x({doc:o=document,hostname:n=globalThis.location?.hostname??""}={}){let a=o.getElementById("pmw-hosts");if(!a)return!1;let s;try{s=JSON.parse(a.textContent??"")}catch{return!1}if(!Array.isArray(s))return!1;let c=String(n).toLowerCase();return s.some(t=>typeof t=="string"&&t.toLowerCase()===c)}var v=["pointerdown","pointerenter","focusin","keydown"];function A(o,n,{events:a=v}={}){let s=!1,c=()=>{if(!s){s=!0;for(let t of a)o.removeEventListener(t,c);n()}};for(let t of a)o.addEventListener(t,c,{passive:!0});return{run:c}}var I="pmw-chat-opener",C=2e4,N=.3,P=[...v,"pointermove","scroll"];function y({doc:o=document,win:n=window,hostname:a=n.location.hostname,importLazy:s=t=>import(t),onInteraction:c=A}={}){let t=o.getElementById("pmw-chat");if(!t)return null;t.hidden=!1;let i=t.querySelector(".pmw-chat__teaser"),f=null,p=()=>f??(f=s(t.dataset.pmwLazy).then(e=>e.mountChat(t),e=>{throw f=null,e})),g=()=>p().catch(()=>{}),L=()=>p().then(e=>e.open(),()=>{}),d=()=>t.hasAttribute("data-open");c(o,g,{events:P});let m=x({doc:o,hostname:a});if(!m){let e=()=>{let r=new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat");r==="panel"&&!d()?L():r==="bubble"&&f&&f.then(E=>E.close?.(),()=>{})};e(),n.addEventListener("hashchange",e)}let h=!1,b=(e=o.activeElement)=>{let r=h&&!d()&&!t.contains(e);t.toggleAttribute("inert",r)},T=o.querySelector("[data-pmw-chat-avoid]")??[...o.querySelectorAll("footer")].pop();m&&T&&(new n.IntersectionObserver(e=>{h=e.at(-1).isIntersecting,b()}).observe(T),t.addEventListener("focusout",e=>b(e.relatedTarget)));let S=n.sessionStorage,l=!1;try{l=!!S?.getItem(I)}catch{}let O=(e,r,E)=>e.addEventListener(r,_=>l||E(_),{passive:!0}),w=()=>{l=!0;try{S?.setItem(I,"1")}catch{}},u=()=>{l||h||!i||d()||(w(),i.hidden=!1,g())};return t.addEventListener("click",e=>{let r=e.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");r&&(i&&(i.hidden=!0),w(),r.hasAttribute("data-pmw-chat-open")&&L())}),!l&&i&&m&&(n.setTimeout(u,C),O(n,"scroll",()=>{let e=o.documentElement.scrollHeight-n.innerHeight;e>0&&n.scrollY/e>=N&&u()}),n.matchMedia?.("(pointer: fine)").matches&&O(o,"mouseout",e=>{!e.relatedTarget&&e.clientY<=0&&u()})),{load:p,show:u}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>y()):y();})();
|
package/dist/widget.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`;var je=new Set;function Ge(s,{doc:l=document,attrs:r={}}={}){if(je.has(s)||(je.add(s),l.querySelector(`script[src="${s}"]`)))return;let t=l.createElement("script");t.src=s,t.async=!0;for(let[d,m]of Object.entries(r))t.setAttribute(d,m);l.head.append(t)}function Ve({doc:s=document,hostname:l=globalThis.location?.hostname??""}={}){let r=s.getElementById("pmw-hosts");if(!r)return!1;let t;try{t=JSON.parse(r.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let d=String(l).toLowerCase();return t.some(m=>typeof m=="string"&&m.toLowerCase()===d)}var mt="https://challenges.cloudflare.com/turnstile/v0/api.js";var ut=15e3,Ke=200;function fe(s=document){Ge(mt,{doc:s})}var ft=s=>s.querySelector('input[name="cf-turnstile-response"]')?.value??"",wt=s=>{let l=s.querySelector(".cf-turnstile");return!!l&&l.offsetHeight>0};async function Xe(s,{win:l=window,timeoutMs:r=ut}={}){typeof s.pmwTurnstileLoad=="function"?s.pmwTurnstileLoad():fe(l.document);let t=0;for(;ft(s)==="";){if(!wt(s)){if(t>=r)return!1;t+=Ke}await new Promise(d=>l.setTimeout(d,Ke))}return!0}function Ye(s,{win:l=window}={}){let r=s.querySelector(".cf-turnstile");if(!r||r.offsetHeight>0)return!1;try{return l.turnstile?.reset(r),!0}catch{return!1}}var Qe={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var _t={fr:{assistant:"Assistant virtuel",openLabel:"Assistant virtuel, ouvrir le clavardage",dismissTeaser:"Masquer ce message",close:"Fermer le clavardage",panelTitle:"Clavardage",automated:"R\xE9ponses automatiques, tir\xE9es du contenu de ce site.",privacy:"Confidentialit\xE9",you:"Vous",messageLabel:"Votre message",send:"Envoyer",live:"Vous r\xE9pond en direct",relay:"{name} a pris le relais",relayGeneric:"Une personne de l\u2019\xE9quipe a pris le relais",connecting:"Connexion\u2026",reconnecting:"Connexion perdue, reconnexion\u2026",notSent:"Non envoy\xE9.",retry:"R\xE9essayer",ended:"Cette conversation est termin\xE9e.",newConversation:"Nouvelle conversation",reload:"Rechargez la page pour continuer.",unavailable:"Un probl\xE8me est survenu. R\xE9essayez dans un moment.",turnstile:"L\u2019envoi n\u2019a pas abouti. R\xE9essayez.",tooMany:"Un instant, puis r\xE9essayez.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"Laissez vos coordonn\xE9es et nous vous recontacterons.",fallbackDown:"Laissez vos coordonn\xE9es et nous vous recontacterons.",name:"Votre nom",contact:"T\xE9l\xE9phone ou courriel",need:"Votre demande",fallbackSend:"Envoyer la demande",fallbackSent:"Merci, votre demande a \xE9t\xE9 transmise.",fallbackMissing:"Remplissez les trois champs.",said:"{name}\xA0: {text}",attachPhoto:"Joindre une photo",photoHint:"\xC9vitez les photos de personnes ou de pi\xE8ces d\u2019identit\xE9.",photoAlt:"Votre photo",photoSent:"Photo envoy\xE9e",photoLimit:"Trois photos au plus par conversation.",photoTooLarge:"Cette photo est trop lourde.",photoUnreadable:"Ce fichier n\u2019est pas une photo lisible.",photoFailed:"La photo n\u2019a pas pu \xEAtre envoy\xE9e. R\xE9essayez.",demoAsk:"Pouvez-vous m\u2019en dire plus sur {topic}?",demoQuestion:"Quels sont vos d\xE9lais?",demoAnswer:"(D\xE9monstration) Ici, l\u2019assistant r\xE9pond \xE0 partir du contenu de votre site.",demoHint:"Aper\xE7u\xA0: la conversation est d\xE9sactiv\xE9e."},en:{assistant:"Virtual assistant",openLabel:"Virtual assistant, open the chat",dismissTeaser:"Hide this message",close:"Close the chat",panelTitle:"Chat",automated:"Automatic answers, drawn from this site\u2019s content.",privacy:"Privacy",you:"You",messageLabel:"Your message",send:"Send",live:"Replying live",relay:"{name} took over",relayGeneric:"A team member took over",connecting:"Connecting\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"Reload the page to continue.",unavailable:"Something went wrong. Try again in a moment.",turnstile:"Sending did not go through. Try again.",tooMany:"One moment, then try again.",fallbackTitle:"Leave us your details",fallbackCap:"Leave your details and we will get back to you.",fallbackDown:"Leave your details and we will get back to you.",name:"Your name",contact:"Phone or email",need:"Your request",fallbackSend:"Send the request",fallbackSent:"Thank you, your request was sent.",fallbackMissing:"Fill in all three fields.",said:"{name}: {text}",attachPhoto:"Attach a photo",photoHint:"Avoid photos of people or of identity documents.",photoAlt:"Your photo",photoSent:"Photo sent",photoLimit:"Three photos at most per conversation.",photoTooLarge:"This photo is too large.",photoUnreadable:"This file is not a readable photo.",photoFailed:"The photo could not be sent. Try again.",demoAsk:"Can you tell me more about {topic}?",demoQuestion:"What are your lead times?",demoAnswer:"(Demo) Here, the assistant answers from your site\u2019s content.",demoHint:"Preview: the conversation is turned off."}},Z=s=>String(s??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function we(s,l={}){let r=_t[Z(s)],t={...r};for(let[d,m]of Object.entries(l??{}))typeof m=="string"&&d in r&&(t[d]=m);return t}var H=(s,l)=>s.replace(/\{(\w+)\}/g,(r,t)=>t in l?String(l[t]):r);var _e="pmw-chat",Ze=[1e3,2e3,4e3,8e3,16e3,3e4],et=3,gt="chat-lead",xt=3,bt=1600,vt=.85,yt=3e3,kt=8e3,St='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></svg>';async function Tt(s,l){let r=await s.createImageBitmap(l,{imageOrientation:"from-image"}),t=Math.min(1,bt/Math.max(r.width,r.height)),d=Math.max(1,Math.round(r.width*t)),m=Math.max(1,Math.round(r.height*t)),u=s.document.createElement("canvas");return u.width=d,u.height=m,u.getContext("2d").drawImage(r,0,0,d,m),r.close?.(),new Promise((U,ee)=>u.toBlob(D=>D?U(D):ee(new Error("encode")),"image/jpeg",vt))}var Lt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},Ct={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function Et(s,l){let r=Z(l),t=we(r),d=String(s??"").trim().match(Ct[r]);if(!d)return t.demoQuestion;let m=d[1].replace(/[\p{L}’']+/gu,u=>Lt[r][u.toLowerCase()]??u);return H(t.demoAsk,{topic:m})}var Ot='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function o(s,l,r={},t){let d=s.createElement(l);for(let[m,u]of Object.entries(r))u!=null&&u!==!1&&d.setAttribute(m,u===!0?"":u);return t!==void 0&&(d.textContent=t),d}var At=s=>({load:()=>fe(s.document),async token(l){return await Xe(l,{win:s}),l.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset:l=>Ye(l,{win:s})});function Jt(s,l={}){if(s.pmwChat)return s.pmwChat;let r=l.win??window,t=r.document,d=l.WebSocket??r.WebSocket,m=l.fetch??((...e)=>r.fetch(...e)),u=l.turnstile??At(r),U=l.uuid??(()=>r.crypto.randomUUID()),ee=l.shrink??(e=>Tt(r,e)),D=l.preview??(e=>new Promise((n,a)=>{let c=new r.FileReader;c.onload=()=>n(String(c.result)),c.onerror=()=>a(c.error),c.readAsDataURL(e)})),S={};try{S=JSON.parse(s.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let B=Z(S.lang),i=we(B,S.strings),ge=s.dataset.pmwChatOpener??"",tt=s.querySelector(".pmw-chat__bubble"),f=l.demo??(S.demo===!0||!Ve({doc:t,hostname:r.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(o(t,"style",{id:"pmw-chat-style"},Je));let k=o(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),xe=o(t,"div",{class:"pmw-chat__head"}),te=s.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??o(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of te.querySelectorAll("[id]")){let n=e.id;e.id=`${n}-head`;for(let a of te.querySelectorAll(`[mask="url(#${n})"]`))a.setAttribute("mask",`url(#${n}-head)`)}let be=o(t,"div",{class:"pmw-chat__who"}),ve=o(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),$=o(t,"p",{class:"pmw-chat__role"}),ye=o(t,"span",{class:"pmw-chat__face"}),ke=o(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),F=o(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});F.innerHTML=Ot;let J=o(t,"p",{class:"pmw-chat__notice"},i.automated);S.privacyHref&&(J.append(" "),J.append(o(t,"a",{href:S.privacyHref},i.privacy))),ye.append(te,ke),be.append(ve,$,J),xe.append(ye,be,F);let w=o(t,"ol",{class:"pmw-chat__log"}),at=o(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},ge),y=o(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});y.append(o(t,"span"),o(t,"span"),o(t,"span")),w.append(at);let ae=o(t,"p",{class:"pmw-chat__state",role:"status"}),Se=o(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Te=e=>o(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":S.siteKey??"","data-action":"chat","data-theme":"light","data-language":B,"data-appearance":"interaction-only","data-size":"flexible"}),_=o(t,"form",{class:"pmw-chat__composer",novalidate:!0}),g=o(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"1",maxlength:String(2e3),placeholder:i.messageLabel,autocomplete:"off",disabled:f,"aria-describedby":f?"pmw-chat-demo-hint":null}),nt=o(t,"button",{type:"submit",class:"pmw-chat__send",disabled:f},i.send),ne=o(t,"div",{class:"pmw-chat__box"});ne.append(o(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),g,nt),_.append(ne),f?_.append(o(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint)):_.append(Te(_));let q=null,O=o(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(S.photos===!0){let e=o(t,"label",{class:"pmw-chat__attach"});q=o(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:f}),e.innerHTML=St,e.prepend(q),e.append(o(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),ne.insertBefore(e,g),_.append(o(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),O)}let j=o(t,"div",{class:"pmw-chat__ended",hidden:!0}),Le=o(t,"p",{},i.ended),G=o(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);j.append(Le,G);let x=o(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),Ce=o(t,"p",{class:"pmw-chat__fb-intro"}),oe=(e,n,a)=>{let c=`pmw-chat-fb-${e}`,p=o(t,"div",{class:"pmw-chat__field"});return p.append(o(t,"label",{for:c},i[e]),o(t,n,{id:c,name:e,required:!0,...a})),p},V=o(t,"p",{class:"pmw-chat__error",role:"alert"});x.append(o(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),Ce,oe("name","input",{autocomplete:"name",maxlength:"100"}),oe("contact","input",{autocomplete:"on",maxlength:"200"}),oe("need","textarea",{rows:"3",maxlength:"2000"}),V,o(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),f||x.append(Te(x));let Ee=o(t,"div",{class:"pmw-chat__body"});Ee.append(w,ae,j,x),k.append(xe,Ee,_,Se),s.append(k);let h=null;if(!f)try{h=JSON.parse(r.sessionStorage.getItem(_e)??"null")}catch{h=null}let Oe=()=>{try{r.sessionStorage.setItem(_e,JSON.stringify(h))}catch{}},b=null,re=0,se=null,R=null,K=0,I="bot",T=null,A=!1,Ae=0,ie=null,ce=new Set,L=new Map,X=new Set,le=0,Ne=()=>new URLSearchParams(r.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Me=f?Ne():null,pe=()=>f?Me:h?.botName||null;function z(){let e=I==="human";ve.textContent=e?T||i.assistant:pe()||i.assistant,$.textContent=e?i.live:pe()?i.assistant:"",$.hidden=$.textContent==="",J.hidden=e,ke.hidden=!e,k.dataset.mode=I}z();let C=e=>{ae.textContent=e??"",ae.hidden=!e};C("");let Pe=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||T||i.assistant:pe()||i.assistant;function N(e,n){let a=o(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${n}`,"data-id":e}),c=o(t,"span",{class:"pmw-chat__sr"}),p=o(t,"span",{class:"pmw-chat__text"});return a.append(c,p),w.insertBefore(a,y.parentNode===w?y:null),a}function qe(e,n){let a=e?e.previousElementSibling:y.parentNode===w?y.previousElementSibling:w.lastElementChild;for(;a;a=a.previousElementSibling){if(a.classList.contains("pmw-chat__relay")||a.classList.contains("pmw-chat__msg--owner"))return;if(a.classList.contains("pmw-chat__msg--bot"))break}let c=n?H(i.relay,{name:n}):i.relayGeneric,p=o(t,"li",{class:"pmw-chat__relay",role:"separator","aria-label":c}),v=o(t,"span",{class:"pmw-chat__relay-label","aria-hidden":"true"});v.append(o(t,"span",{class:"pmw-chat__relay-dot"}),c),p.append(v),w.insertBefore(p,e??(y.parentNode===w?y:null))}let Re=e=>[...w.querySelectorAll("li[data-id]")].find(n=>n.dataset.id===e),E=(e,n,a)=>{e.firstChild.textContent=H(i.said,{name:Pe(n),text:""}),e.lastChild.textContent=a===""&&n.photos?.length&&!e.querySelector("img")?i.photoSent:a};function de(){y.hidden=!0,r.clearTimeout(ie)}function Ie(e,{announce:n}){if(ce.has(e.seq))return;ce.add(e.seq),K=Math.max(K,e.seq);let a=Re(e.id);a||(a=N(e.id,e.role),e.role==="owner"&&qe(a,e.by||T)),a.classList.remove("is-streaming","is-pending","is-failed"),a.querySelector(".pmw-chat__retry")?.remove(),E(a,e,e.text),e.role==="visitor"?(L.delete(e.id),e.photos?.length&&X.add(e.id)):(de(),n&&(Se.textContent=H(i.said,{name:Pe(e),text:e.text})),k.hidden&&s.setAttribute("data-unread","")),M()}function ot(e,n){de();let a=Re(e);a||(a=N(e,"bot"),a.classList.add("is-streaming"),E(a,{role:"bot"},"")),a.lastChild.textContent+=n,M()}let M=()=>{w.scrollTop=w.scrollHeight};if(f){E(N("demo-question","visitor"),{role:"visitor"},Et(ge,B));let e=N("demo-answer","bot");E(e,{role:"bot"},i.demoAnswer),r.addEventListener("hashchange",()=>{Me=Ne(),z(),E(e,{role:"bot"},i.demoAnswer)})}function Y(e){if(e.failed=!0,e.li.classList.remove("is-pending"),e.li.classList.add("is-failed"),e.li.querySelector(".pmw-chat__retry"))return;let n=o(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),a=o(t,"button",{type:"button"},i.retry);a.addEventListener("click",()=>{e.failed=!1,e.tries=0,n.remove(),e.li.classList.replace("is-failed","is-pending"),h?Q()?he(e):P():me()}),n.append(a),e.li.append(n)}let Q=()=>!!b&&b.readyState===1;function he(e){if(!(e.failed||!Q())){if(e.tries>=et)return Y(e);e.tries++,b.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function P(){if(!h||A||b)return;r.clearTimeout(se);let e=r.location.protocol==="https:"?"wss:":"ws:",n=new URLSearchParams({c:h.c,k:h.k}),a=new d(`${e}//${r.location.host}/api/chat/ws?${n}`);b=a,a.onopen=()=>{re=0,C(""),a.send(JSON.stringify({type:"hello",since:K}));for(let c of L.values())he(c)},a.onmessage=c=>{let p;try{p=JSON.parse(c.data)}catch{return}st(p)},a.onclose=c=>{b===a&&(b=null);for(let v of L.values())v.tries>=et&&Y(v);if(c.code>=4e3&&c.code<5e3)return rt(c.code);if(A)return;C(i.reconnecting);let p=Ze[Math.min(re,Ze.length-1)];re++,se=r.setTimeout(P,p)}}function rt(e){if(e===Qe.rpc)return W(i.reload,!1);W(i.ended,!0)}function W(e,n){A=!0,r.clearTimeout(se),C(""),Le.textContent=e,G.hidden=!n,j.hidden=!1,_.hidden=!0;for(let a of L.values())Y(a)}function ze(e){I=e.mode==="human"?"human":"bot",T=typeof e.ownerName=="string"?e.ownerName:null,z(),I==="human"&&qe(null,T),e.closed&&W(i.ended,!0)}function st(e){switch(e?.type){case"history":typeof e.ownerName=="string"&&(T=e.ownerName);for(let n of e.messages??[])Ie(n,{announce:!1});return ze(e);case"message":return Ie(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return ot(e.id,e.text);case"typing":return e.role==="visitor"?void 0:(y.hidden=!1,w.append(y),r.clearTimeout(ie),ie=r.setTimeout(de,kt),M());case"status":return ze(e);case"fallback":return Be(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Ue();case"error":(e.code==="closed"||e.code==="deleted")&&W(i.ended,!0)}}let He=!1;function Ue(){if(!(He||h?.lead)){He=!0,h&&(h.lead=!0,Oe());try{r.pmw?.event(gt)}catch{}}}async function De(e,n){let a="";try{a=await u.token(e)}catch{a=""}let c,p=null;try{c=await m("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:a,lang:B,page:r.location.pathname,...n})}),p=await c.json()}catch{c=null}try{u.reset(e)}catch{}return c?.ok&&p?{ok:!0,data:p}:{ok:!1,error:c?.status===403?i.turnstile:c?.status===429?i.tooMany:null,cap:c?.status===409&&p?.error==="cap"}}function me(){return R||h||(C(i.connecting),R=De(_,{}).then(e=>{if(R=null,e.ok&&e.data.conversationId&&e.data.visitorKey){h={c:e.data.conversationId,k:e.data.visitorKey,botName:typeof e.data.botName=="string"?e.data.botName:null},Oe(),z(),P();return}for(let n of L.values())Y(n);if(e.error)return C(e.error);C(""),Be(e.cap?i.fallbackCap:i.fallbackDown)})),R}function Be(e){Ce.textContent=e,x.hidden=!1,_.hidden=!0,x.querySelector("input")?.focus()}x.addEventListener("submit",async e=>{e.preventDefault();let n=p=>x.elements[p].value.trim(),a={name:n("name"),contact:n("contact"),need:n("need")};if(!a.name||!a.contact||!a.need){V.textContent=i.fallbackMissing;return}V.textContent="";let c=await De(x,{fallbackLead:a});if(!c.ok){V.textContent=c.error??i.unavailable;return}x.replaceChildren(o(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Ue()});let it={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function ct(e){let n,a;try{n=await ee(e),a=await D(n)}catch{O.textContent=i.photoUnreadable;return}let c=U(),p=N(c,"visitor");p.classList.add("is-pending"),p.insertBefore(o(t,"img",{class:"pmw-chat__photo",src:a,alt:i.photoAlt}),p.lastChild),E(p,{role:"visitor"},""),M(),h||await me();let v=null;if(h){let dt=new URLSearchParams({c:h.c,k:h.k,id:c});try{v=await m(`/api/chat/photo?${dt}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:n})}catch{v=null}}if(v?.ok){p.classList.remove("is-pending"),X.add(c),b||P();return}p.remove(),O.textContent=i[it[v?.status]]??i.photoFailed}q?.addEventListener("change",async()=>{let e=[...q.files??[]];if(q.value="",O.textContent="",A||f||e.length===0)return;let n=xt-X.size-le;e.length>n&&(O.textContent=i.photoLimit);let a=e.slice(0,Math.max(0,n));le+=a.length;for(let c of a)try{await ct(c)}finally{le--}});function $e(){let e=g.value.trim();if(!e||A||f)return;g.value="",Fe();let n={id:U(),text:e.slice(0,2e3),tries:0,failed:!1};n.li=N(n.id,"visitor"),n.li.classList.add("is-pending"),E(n.li,{role:"visitor"},n.text),L.set(n.id,n),M(),h?Q()?he(n):P():me()}_.addEventListener("submit",e=>{e.preventDefault(),$e()}),g.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),$e())});let Fe=()=>{g.style.height="auto",g.style.height=g.value?`${Math.min(g.scrollHeight,132)}px`:""};g.addEventListener("input",()=>{Fe();let e=Date.now();Q()&&e-Ae>yt&&(Ae=e,b.send(JSON.stringify({type:"typing"})))}),G.addEventListener("click",()=>{h=null;try{r.sessionStorage.removeItem(_e)}catch{}try{b?.close(1e3)}catch{}b=null,A=!1,K=0,ce.clear(),L.clear(),X.clear(),O.textContent="",I="bot",T=null,z();for(let e of[...w.querySelectorAll("li[data-id], .pmw-chat__relay")])e.remove();j.hidden=!0,_.hidden=!1,g.focus()});let lt=()=>[...k.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));k.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),ue();return}if(e.key!=="Tab")return;let n=lt();if(n.length===0)return;let a=n[0],c=n[n.length-1];e.shiftKey&&t.activeElement===a?(e.preventDefault(),c.focus()):!e.shiftKey&&t.activeElement===c&&(e.preventDefault(),a.focus())}),F.addEventListener("click",()=>ue());function pt(){f||u.load(),k.hidden=!1,s.setAttribute("data-open",""),s.removeAttribute("data-unread"),h&&P(),(f?F:_.hidden?x.hidden?G:x.querySelector("input"):g)?.focus(),M()}function ue(){k.hidden=!0,s.removeAttribute("data-open"),tt?.focus()}return s.pmwChat={open:pt,close:ue,panel:k},s.pmwChat}export{Ze as BACKOFF_MS,gt as GOAL,xt as MAX_PHOTOS,et as MAX_TRIES,bt as PHOTO_EDGE,vt as PHOTO_QUALITY,_e as SESSION_KEY,Et as demoQuestion,Jt as mountChat};
|
|
1
|
+
var ot=`.pmw-chat__panel{position:fixed;z-index:81;inset:0;display:flex;flex-direction:column;background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-fg, #1f2328);font-family:var(--pmw-chat-font, inherit);font-size:16px;line-height:1.45}.pmw-chat__panel[hidden],.pmw-chat__panel [hidden]{display:none!important}@media(min-width:640px){.pmw-chat__panel{inset:auto;right:calc(env(safe-area-inset-right,0px) + var(--pmw-chat-right, 16px));bottom:calc(env(safe-area-inset-bottom,0px) + var(--pmw-chat-bottom, 16px));width:380px;height:min(600px,calc(100dvh - 16px - env(safe-area-inset-bottom,0px) - var(--pmw-chat-bottom, 16px)));border:1px solid var(--pmw-chat-line, #e6e9f0);border-radius:20px;overflow:hidden;box-shadow:0 18px 48px #1018282e}}.pmw-chat__panel :where(button){font:inherit;cursor:pointer}.pmw-chat__panel :focus-visible{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__head{display:flex;align-items:center;gap:12px;padding:14px 10px 14px 16px;border-bottom:1px solid var(--pmw-chat-line, #e6e9f0)}@media(max-width:639.98px){.pmw-chat__head{padding:12px 8px 12px 16px}}.pmw-chat__face{position:relative;flex:none;display:flex}.pmw-chat__head .pmw-chat__avatar{width:44px;height:44px;box-shadow:none}.pmw-chat__head .pmw-chat__avatar--icon{box-shadow:0 0 0 1.5px var(--pmw-chat-avatar-ring, transparent)}.pmw-chat__head .pmw-chat__avatar img{width:44px;height:44px}.pmw-chat__head .pmw-chat__avatar--logo img{width:32px;height:32px}.pmw-chat__head .pmw-chat__avatar svg{width:100%;height:100%}.pmw-chat__dot{position:absolute;right:-1px;bottom:-1px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-live, #1a7f37);box-shadow:0 0 0 2px var(--pmw-chat-panel-bg, #ffffff)}.pmw-chat__who{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.pmw-chat__name{margin:0;font-weight:600;font-size:16px}.pmw-chat__role,.pmw-chat__notice{margin:0;font-size:13px;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__panel[data-mode=human] .pmw-chat__role{color:var(--pmw-chat-live, #1a7f37);font-weight:500}.pmw-chat__notice a{color:var(--pmw-chat-link, #0a1b3d);text-decoration:underline;text-underline-offset:2px}.pmw-chat__close{flex:none;width:44px;height:44px;padding:0;border:0;border-radius:50%;display:flex;align-items:center;justify-content:center;background:transparent;color:var(--pmw-chat-fg, #1f2328)}.pmw-chat__close:hover{background:var(--pmw-chat-thread-bg, #f7f8fb)}.pmw-chat__close svg{width:18px;height:18px}.pmw-chat__body{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding:18px 16px;background:var(--pmw-chat-thread-bg, #f7f8fb)}.pmw-chat__log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.pmw-chat__log>li{margin-top:4px}.pmw-chat__log>li:first-child,.pmw-chat__msg--bot+.pmw-chat__msg--bot,.pmw-chat__msg--bot+.pmw-chat__typing,.pmw-chat__msg--owner+.pmw-chat__msg--owner,.pmw-chat__msg--owner+.pmw-chat__typing,.pmw-chat__msg--visitor+.pmw-chat__msg--visitor{margin-top:0}.pmw-chat__msg{max-width:82%;padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere}.pmw-chat__msg--bot,.pmw-chat__msg--owner{align-self:flex-start;border-bottom-left-radius:6px;color:var(--pmw-chat-fg, #1f2328)}.pmw-chat__msg--bot{background:var(--pmw-chat-other-bg, #ffffff);border:1px solid var(--pmw-chat-line, #e6e9f0)}.pmw-chat__msg--owner{background:var(--pmw-chat-owner-bg, #eef2fa);border:1px solid var(--pmw-chat-owner-border, #d5ddef)}.pmw-chat__msg--visitor{align-self:flex-end;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff);border-bottom-right-radius:6px}.pmw-chat__msg--bot+.pmw-chat__msg--bot,.pmw-chat__msg--owner+.pmw-chat__msg--owner{border-top-left-radius:6px}.pmw-chat__msg--visitor+.pmw-chat__msg--visitor{border-top-right-radius:6px}.pmw-chat__msg:has(>.pmw-chat__photo){padding:4px}.pmw-chat__msg:has(>.pmw-chat__photo) .pmw-chat__text:empty{display:none}.pmw-chat__msg.is-pending{opacity:.75}.pmw-chat__msg.is-failed{outline:2px solid var(--pmw-chat-error, #b42318);outline-offset:2px}.pmw-chat__retry{display:block;margin-top:4px;font-size:14px}.pmw-chat__retry button{padding:0;border:0;background:none;color:inherit;text-decoration:underline}.pmw-chat__typing{align-self:flex-start;display:flex;gap:5px;padding:14px 16px;border-radius:18px 18px 18px 6px;background:var(--pmw-chat-other-bg, #ffffff);border:1px solid var(--pmw-chat-line, #e6e9f0)}.pmw-chat__typing span{width:7px;height:7px;border-radius:50%;background:var(--pmw-chat-muted, #4b5563);animation:pmw-chat-dot 1.2s infinite ease-in-out}.pmw-chat__typing span:nth-child(2){animation-delay:.15s}.pmw-chat__typing span:nth-child(3){animation-delay:.3s}@keyframes pmw-chat-dot{0%,80%,to{opacity:.3}40%{opacity:1}}@media(prefers-reduced-motion:reduce){.pmw-chat__typing span{animation:none;opacity:.7}}.pmw-chat__log>.pmw-chat__relay,.pmw-chat__log>.pmw-chat__handback{align-self:stretch;margin:18px 0 8px;display:flex;align-items:center;gap:10px;font-size:13px;font-weight:500;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__relay:before,.pmw-chat__relay:after,.pmw-chat__handback:before,.pmw-chat__handback:after{content:"";flex:1;min-width:16px;height:1px;background:var(--pmw-chat-rule, #cfd5e1)}.pmw-chat__relay-label{display:flex;align-items:center;gap:6px;text-align:center}.pmw-chat__relay-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--pmw-chat-live, #1a7f37)}.pmw-chat__relay-dot--back{background:var(--pmw-chat-muted, #4b5563)}.pmw-chat__log>.pmw-chat__relay+li,.pmw-chat__log>.pmw-chat__handback+li{margin-top:0}.pmw-chat__state{margin:8px 0 0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__state:empty{display:none}.pmw-chat__ended{margin-top:22px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center}.pmw-chat__ended:before{content:"";align-self:stretch;height:1px;background:var(--pmw-chat-line, #e6e9f0)}.pmw-chat__ended p{margin:0;font-size:15px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__restart{min-height:44px;padding:0 20px;border-radius:22px;border:1px solid var(--pmw-chat-link, #0a1b3d);background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-link, #0a1b3d);font-weight:600}.pmw-chat__composer{--pmw-chat-gap: 8px;display:flex;flex-direction:column;gap:var(--pmw-chat-gap);padding:14px 16px 16px;border-top:1px solid var(--pmw-chat-line, #e6e9f0);background:var(--pmw-chat-panel-bg, #ffffff)}@media(max-width:639.98px){.pmw-chat__composer{padding:12px 16px calc(14px + env(safe-area-inset-bottom,0px))}}.pmw-chat__box{display:flex;align-items:flex-end;gap:6px;padding:4px;border-radius:24px;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__box textarea{flex:1;min-width:0;box-sizing:border-box;min-height:44px;max-height:132px;resize:none;margin:0;padding:10px 2px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:16px;line-height:1.5}.pmw-chat__box textarea::placeholder{color:#6b7280;opacity:1}@supports selector(:has(*)){.pmw-chat__box textarea:focus-visible{outline:none}.pmw-chat__box:has(textarea:focus-visible){outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}}.pmw-chat__send{flex:none;min-height:44px;padding:0 18px;border:0;border-radius:22px;font-weight:600;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__send:disabled,.pmw-chat__box textarea:disabled{cursor:not-allowed;opacity:.6}.pmw-chat__check{display:grid;grid-template-rows:0fr;margin-bottom:calc(-1 * var(--pmw-chat-gap, 0px));transition:grid-template-rows .25s ease,margin-bottom .25s ease}.pmw-chat__check[data-ask]{grid-template-rows:1fr;margin-bottom:4px}.pmw-chat__check-in{min-height:0;overflow:hidden}@media(prefers-reduced-motion:reduce){.pmw-chat__check{transition:none}}.pmw-chat__attach{flex:none;position:relative;width:44px;height:44px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:inherit}.pmw-chat__attach:hover{background:#eef0f4}.pmw-chat__attach svg{width:22px;height:22px}.pmw-chat__attach:focus-within{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__hint{margin:0 8px;font-size:13px;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__composer .pmw-chat__photo-error{margin:0 8px}.pmw-chat__photo{display:block;max-width:100%;max-height:220px;border-radius:14px}.pmw-chat__fallback{--pmw-chat-gap: 14px;display:flex;flex-direction:column;gap:var(--pmw-chat-gap);margin-top:16px}.pmw-chat__fb-title{margin:0;font-size:20px;font-weight:600;line-height:1.3}.pmw-chat__fb-intro{margin:-8px 0 0;font-size:15px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__field{display:flex;flex-direction:column;gap:4px}.pmw-chat__field label{font-size:14px;font-weight:600}.pmw-chat__field input,.pmw-chat__field textarea{font:inherit;font-size:16px;padding:10px 14px;border-radius:12px;resize:none;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__fallback .pmw-chat__send{align-self:stretch;min-height:48px;border-radius:24px}.pmw-chat__error{margin:0;font-size:14px;color:var(--pmw-chat-error, #b42318)}.pmw-chat__error:empty{display:none}.pmw-chat__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.pmw-chat[data-unread] .pmw-chat__bubble{position:relative}.pmw-chat[data-unread] .pmw-chat__bubble:after{content:"";position:absolute;top:2px;left:38px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-error, #b42318);box-shadow:0 0 0 2px #fff}
|
|
2
|
+
`;var At="400px 0px";var rt=new Set,ce=null;function st(r,{doc:l=document,attrs:n={}}={}){if(rt.has(r)||(rt.add(r),l.querySelector(`script[src="${r}"]`)))return;let t=l.createElement("script");t.src=r,t.async=!0;for(let[m,p]of Object.entries(n))t.setAttribute(m,p);l.head.append(t)}function it({doc:r=document,hostname:l=globalThis.location?.hostname??""}={}){let n=r.getElementById("pmw-hosts");if(!n)return!1;let t;try{t=JSON.parse(n.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let m=String(l).toLowerCase();return t.some(p=>typeof p=="string"&&p.toLowerCase()===m)}function Ot(r){return ce||(ce=new Promise(l=>{let n=null,t=null,m=0,p=r.PerformanceObserver?.supportedEntryTypes??[],d=typeof r.PerformanceObserver=="function"&&p.includes("paint"),f=()=>{n===null||d&&t===null||l(Math.max(n,t??0,m))};if(d)try{new r.PerformanceObserver(v=>{for(let T of v.getEntries())T.name==="first-contentful-paint"&&(t=T.startTime);f()}).observe({type:"paint",buffered:!0}),p.includes("largest-contentful-paint")&&new r.PerformanceObserver(v=>{for(let T of v.getEntries())m=Math.max(m,T.startTime)}).observe({type:"largest-contentful-paint",buffered:!0})}catch{t=0}let E=()=>{n=r.performance.now(),f()};r.document.readyState==="complete"?E():r.addEventListener("load",E,{once:!0})}),ce)}function Le(r,l,{win:n=window,rootMargin:t=At,settleMs:m=1500}={}){let p=null,d=!1,f=()=>{d||(d=!0,p?.disconnect(),r.removeEventListener("focusin",f),r.removeEventListener("pointerdown",f),l())};r.addEventListener("focusin",f),r.addEventListener("pointerdown",f);let E=Ot(n),v=async()=>{let T=await E;n.setTimeout(f,Math.max(0,T+m-n.performance.now()))};return typeof n.IntersectionObserver=="function"?(p=new n.IntersectionObserver(T=>{T.some(L=>L.isIntersecting)&&(p.disconnect(),v())},{rootMargin:t}),p.observe(r)):v(),{run:f}}var Mt="https://challenges.cloudflare.com/turnstile/v0/api.js";var Nt=15e3,ct=200;function Ce(r=document){st(Mt,{doc:r})}var Pt=r=>r.querySelector('input[name="cf-turnstile-response"]')?.value??"",Rt=r=>{let l=r.querySelector(".cf-turnstile");return!!l&&l.offsetHeight>0};async function lt(r,{win:l=window,timeoutMs:n=Nt}={}){typeof r.pmwTurnstileLoad=="function"?r.pmwTurnstileLoad():Ce(l.document);let t=0;for(;Pt(r)==="";){if(!Rt(r)){if(t>=n)return!1;t+=ct}await new Promise(m=>l.setTimeout(m,ct))}return!0}var pt={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var qt={fr:{assistant:"Assistant virtuel",openLabel:"Assistant virtuel, ouvrir le clavardage",dismissTeaser:"Masquer ce message",close:"Fermer le clavardage",panelTitle:"Clavardage",automated:"R\xE9ponses automatiques, tir\xE9es du contenu de ce site.",privacy:"Confidentialit\xE9",you:"Vous",messageLabel:"Votre message",send:"Envoyer",live:"Vous r\xE9pond en direct",relay:"{name} a pris le relais",relayGeneric:"Une personne de l\u2019\xE9quipe a pris le relais",handback:"L\u2019assistant virtuel a repris la conversation",oneMoment:"Un instant\u2026",reconnecting:"Connexion perdue, reconnexion\u2026",notSent:"Non envoy\xE9.",retry:"R\xE9essayer",ended:"Cette conversation est termin\xE9e.",newConversation:"Nouvelle conversation",reload:"Rechargez la page pour continuer.",unavailable:"Un probl\xE8me est survenu. R\xE9essayez dans un moment.",turnstile:"L\u2019envoi n\u2019a pas abouti. R\xE9essayez.",tooMany:"Un instant, puis r\xE9essayez.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"Laissez vos coordonn\xE9es et nous vous recontacterons.",fallbackDown:"Laissez vos coordonn\xE9es et nous vous recontacterons.",name:"Votre nom",contact:"T\xE9l\xE9phone ou courriel",need:"Votre demande",fallbackSend:"Envoyer la demande",fallbackSent:"Merci, votre demande a \xE9t\xE9 transmise.",fallbackMissing:"Remplissez les trois champs.",said:"{name}\xA0: {text}",attachPhoto:"Joindre une photo",photoHint:"\xC9vitez les photos de personnes ou de pi\xE8ces d\u2019identit\xE9.",photoAlt:"Votre photo",photoSent:"Photo envoy\xE9e",photoLimit:"Trois photos au plus par conversation.",photoTooLarge:"Cette photo est trop lourde.",photoUnreadable:"Ce fichier n\u2019est pas une photo lisible.",photoFailed:"La photo n\u2019a pas pu \xEAtre envoy\xE9e. R\xE9essayez.",demoAsk:"Pouvez-vous m\u2019en dire plus sur {topic}?",demoQuestion:"Quels sont vos d\xE9lais?",demoAnswer:"(D\xE9monstration) Ici, l\u2019assistant r\xE9pond \xE0 partir du contenu de votre site.",demoHint:"Aper\xE7u\xA0: la conversation est d\xE9sactiv\xE9e."},en:{assistant:"Virtual assistant",openLabel:"Virtual assistant, open the chat",dismissTeaser:"Hide this message",close:"Close the chat",panelTitle:"Chat",automated:"Automatic answers, drawn from this site\u2019s content.",privacy:"Privacy",you:"You",messageLabel:"Your message",send:"Send",live:"Replying live",relay:"{name} took over",relayGeneric:"A team member took over",handback:"The virtual assistant is back in the conversation",oneMoment:"One moment\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"Reload the page to continue.",unavailable:"Something went wrong. Try again in a moment.",turnstile:"Sending did not go through. Try again.",tooMany:"One moment, then try again.",fallbackTitle:"Leave us your details",fallbackCap:"Leave your details and we will get back to you.",fallbackDown:"Leave your details and we will get back to you.",name:"Your name",contact:"Phone or email",need:"Your request",fallbackSend:"Send the request",fallbackSent:"Thank you, your request was sent.",fallbackMissing:"Fill in all three fields.",said:"{name}: {text}",attachPhoto:"Attach a photo",photoHint:"Avoid photos of people or of identity documents.",photoAlt:"Your photo",photoSent:"Photo sent",photoLimit:"Three photos at most per conversation.",photoTooLarge:"This photo is too large.",photoUnreadable:"This file is not a readable photo.",photoFailed:"The photo could not be sent. Try again.",demoAsk:"Can you tell me more about {topic}?",demoQuestion:"What are your lead times?",demoAnswer:"(Demo) Here, the assistant answers from your site\u2019s content.",demoHint:"Preview: the conversation is turned off."}},le=r=>String(r??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function Ee(r,l={}){let n=qt[le(r)],t={...n};for(let[m,p]of Object.entries(l??{}))typeof p=="string"&&m in n&&(t[m]=p);return t}var V=(r,l)=>r.replace(/\{(\w+)\}/g,(n,t)=>t in l?String(l[t]):n);var Ae="pmw-chat",ht=[1e3,2e3,4e3,8e3,16e3,3e4],mt=3,It="chat-lead",zt=3,Ht=1600,$t=.85,Dt=3e3,Ut=8e3,Bt=40,Ft=3e3,ut=8e3,Jt='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></svg>';async function jt(r,l){let n=await r.createImageBitmap(l,{imageOrientation:"from-image"}),t=Math.min(1,Ht/Math.max(n.width,n.height)),m=Math.max(1,Math.round(n.width*t)),p=Math.max(1,Math.round(n.height*t)),d=r.document.createElement("canvas");return d.width=m,d.height=p,d.getContext("2d").drawImage(n,0,0,m,p),n.close?.(),new Promise((f,E)=>d.toBlob(v=>v?f(v):E(new Error("encode")),"image/jpeg",$t))}var Gt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},Kt={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function Vt(r,l){let n=le(l),t=Ee(n),m=String(r??"").trim().match(Kt[n]);if(!m)return t.demoQuestion;let p=m[1].replace(/[\p{L}’']+/gu,d=>Gt[n][d.toLowerCase()]??d);return V(t.demoAsk,{topic:p})}var Xt='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function s(r,l,n={},t){let m=r.createElement(l);for(let[p,d]of Object.entries(n))d!=null&&d!==!1&&m.setAttribute(p,d===!0?"":d);return t!==void 0&&(m.textContent=t),m}var Yt=(r,l)=>{let n=!1,t=()=>{let p=r.turnstile;if(!p?.render)return!1;for(let d of l)if(!d.childElementCount)try{p.render(d)}catch{}return!0},m=()=>{if(Ce(r.document),n||t())return;n=!0;let p=0,d=()=>{!t()&&++p<600&&r.setTimeout(d,100)};r.setTimeout(d,100)};for(let p of l){let d=p.closest("form");d&&(d.pmwTurnstileLoad=m)}return{load:m,async token(p){return await lt(p,{win:r}),p.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset(p){let d=p.querySelector(".cf-turnstile");d&&!d.closest("[data-ask]")&&r.turnstile?.reset(d)}}};function la(r,l={}){if(r.pmwChat)return r.pmwChat;let n=l.win??window,t=n.document,m=l.WebSocket??n.WebSocket,p=l.fetch??((...e)=>n.fetch(...e)),d=[],f=l.turnstile??null,E=l.uuid??(()=>n.crypto.randomUUID()),v=l.shrink??(e=>jt(n,e)),T=l.preview??(e=>new Promise((a,o)=>{let c=new n.FileReader;c.onload=()=>a(String(c.result)),c.onerror=()=>o(c.error),c.readAsDataURL(e)})),L={};try{L=JSON.parse(r.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let X=le(L.lang),i=Ee(X,L.strings),Oe=r.dataset.pmwChatOpener??"",ft=r.querySelector(".pmw-chat__bubble"),w=l.demo??(L.demo===!0||!it({doc:t,hostname:n.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(s(t,"style",{id:"pmw-chat-style"},ot));let A=s(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),Me=s(t,"div",{class:"pmw-chat__head"}),pe=r.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??s(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of pe.querySelectorAll("[id]")){let a=e.id;e.id=`${a}-head`;for(let o of pe.querySelectorAll(`[mask="url(#${a})"]`))o.setAttribute("mask",`url(#${a}-head)`)}let Ne=s(t,"div",{class:"pmw-chat__who"}),Pe=s(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),Y=s(t,"p",{class:"pmw-chat__role"}),Re=s(t,"span",{class:"pmw-chat__face"}),qe=s(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),W=s(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});W.innerHTML=Xt;let Q=s(t,"p",{class:"pmw-chat__notice"},i.automated);L.privacyHref&&(Q.append(" "),Q.append(s(t,"a",{href:L.privacyHref},i.privacy))),Re.append(pe,qe),Ne.append(Pe,Y,Q),Me.append(Re,Ne,W);let _=s(t,"ol",{class:"pmw-chat__log"}),wt=s(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},Oe),C=s(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});C.append(s(t,"span"),s(t,"span"),s(t,"span")),_.append(wt);let de=s(t,"p",{class:"pmw-chat__state",role:"status"}),Ie=s(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),ze=e=>{let a=s(t,"div",{class:"pmw-chat__check"}),o=s(t,"div",{class:"pmw-chat__check-in"}),c=`pmwChatTurnstile${e}`,h=D=>()=>{a.toggleAttribute("data-ask",D),k()};n[`${c}Ask`]=h(!0),n[`${c}Error`]=h(!0),n[`${c}Done`]=h(!1);let x=s(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":L.siteKey??"","data-action":"chat","data-theme":"light","data-language":X,"data-appearance":"interaction-only","data-size":"flexible","data-callback":`${c}Done`,"data-before-interactive-callback":`${c}Ask`,"data-error-callback":`${c}Error`});return d.push(x),o.append(x),a.append(o),a},g=s(t,"form",{class:"pmw-chat__composer",novalidate:!0}),b=s(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"1",maxlength:String(2e3),placeholder:i.messageLabel,autocomplete:"off",disabled:w,"aria-describedby":w?"pmw-chat-demo-hint":null}),_t=s(t,"button",{type:"submit",class:"pmw-chat__send",disabled:w},i.send),he=s(t,"div",{class:"pmw-chat__box"});he.append(s(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),b,_t),w||g.append(ze("c")),g.append(he),w&&g.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint));let U=null,I=s(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(L.photos===!0){let e=s(t,"label",{class:"pmw-chat__attach"});U=s(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:w}),e.innerHTML=Jt,e.prepend(U),e.append(s(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),he.insertBefore(e,b),g.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),I)}let Z=s(t,"div",{class:"pmw-chat__ended",hidden:!0}),He=s(t,"p",{},i.ended),ee=s(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);Z.append(He,ee);let y=s(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),$e=s(t,"p",{class:"pmw-chat__fb-intro"}),me=(e,a,o)=>{let c=`pmw-chat-fb-${e}`,h=s(t,"div",{class:"pmw-chat__field"});return h.append(s(t,"label",{for:c},i[e]),s(t,a,{id:c,name:e,required:!0,...o})),h},B=s(t,"p",{class:"pmw-chat__error",role:"alert"});y.append(s(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),$e,me("name","input",{autocomplete:"name",maxlength:"100"}),me("contact","input",{autocomplete:"on",maxlength:"200"}),me("need","textarea",{rows:"3",maxlength:"2000"}),B,s(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),w||y.insertBefore(ze("f"),B);let O=s(t,"div",{class:"pmw-chat__body"});O.append(_,de,Z,y),A.append(Me,O,g,Ie),r.append(A),f??(f=Yt(n,d));let F=!0,De=0,ue=0,gt=()=>O.scrollHeight-O.scrollTop-O.clientHeight;O.addEventListener("scroll",()=>{let e=O.scrollTop;gt()<=Bt?F=!0:e<De&&(F=!1),De=e},{passive:!0});let bt=e=>n.requestAnimationFrame?n.requestAnimationFrame(e):n.setTimeout(e,16);function k(e=!1,a=!1){e&&(F=!0),!(!F||ue)&&(ue=bt(()=>{if(ue=0,!F)return;let o=a||!!n.matchMedia?.("(prefers-reduced-motion: reduce)").matches;O.scrollTo({top:O.scrollHeight,behavior:o?"instant":"smooth"})}))}if(typeof n.ResizeObserver=="function"){let e=new n.ResizeObserver(()=>k());e.observe(_),e.observe(g)}let u=null;if(!w)try{u=JSON.parse(n.sessionStorage.getItem(Ae)??"null")}catch{u=null}let Ue=()=>{try{n.sessionStorage.setItem(Ae,JSON.stringify(u))}catch{}},S=null,fe=0,we=null,J=null,te=0,j="bot",P=null,z=!1,Be=0,_e=null,ge=new Set,R=new Map,ae=new Set,be=0,Fe=()=>new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Je=w?Fe():null,xe=()=>w?Je:u?.botName||null;function G(){let e=j==="human";Pe.textContent=e?P||i.assistant:xe()||i.assistant,Y.textContent=e?i.live:xe()?i.assistant:"",Y.hidden=Y.textContent==="",Q.hidden=e,qe.hidden=!e,A.dataset.mode=j}G();let M=e=>{de.textContent=e??"",de.hidden=!e};M("");let je=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||P||i.assistant:xe()||i.assistant;function H(e,a){let o=s(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${a}`,"data-id":e}),c=s(t,"span",{class:"pmw-chat__sr"}),h=s(t,"span",{class:"pmw-chat__text"});return o.append(c,h),_.insertBefore(o,C.parentNode===_?C:null),o}let Ge=()=>C.parentNode===_?C.previousElementSibling:_.lastElementChild,N=(e,a)=>e.classList.contains(a);function Ke(e,a,o,c){let h=s(t,"li",{class:e,role:"separator","aria-label":o}),x=s(t,"span",{class:"pmw-chat__relay-label","aria-hidden":"true"});x.append(s(t,"span",{class:a}),o),h.append(x),_.insertBefore(h,c??(C.parentNode===_?C:null)),k()}function Ve(e,a){for(let o=e?e.previousElementSibling:Ge();o;o=o.previousElementSibling){if(N(o,"pmw-chat__relay")||N(o,"pmw-chat__msg--owner"))return;if(N(o,"pmw-chat__msg--bot")||N(o,"pmw-chat__handback"))break}Ke("pmw-chat__relay","pmw-chat__relay-dot",a?V(i.relay,{name:a}):i.relayGeneric,e)}function ve(e){for(let a=e?e.previousElementSibling:Ge();;a=a.previousElementSibling){if(!a||N(a,"pmw-chat__handback")||N(a,"pmw-chat__msg--bot"))return;if(N(a,"pmw-chat__relay")||N(a,"pmw-chat__msg--owner"))break}Ke("pmw-chat__handback","pmw-chat__relay-dot pmw-chat__relay-dot--back",i.handback,e)}let Xe=e=>[..._.querySelectorAll("li[data-id]")].find(a=>a.dataset.id===e),q=(e,a,o)=>{e.firstChild.textContent=V(i.said,{name:je(a),text:""}),e.lastChild.textContent=o===""&&a.photos?.length&&!e.querySelector("img")?i.photoSent:o};function K(){C.hidden=!0,n.clearTimeout(_e)}function ye(e){C.hidden=!1,_.append(C),n.clearTimeout(_e),e&&(_e=n.setTimeout(K,e)),k()}function Ye(e,{announce:a}){if(ge.has(e.seq))return;ge.add(e.seq),te=Math.max(te,e.seq);let o=Xe(e.id);o||(o=H(e.id,e.role),e.role==="owner"?Ve(o,e.by||P):e.role==="bot"&&ve(o)),o.classList.remove("is-streaming","is-pending","is-failed"),o.querySelector(".pmw-chat__retry")?.remove(),q(o,e,e.text),e.role==="visitor"?(R.delete(e.id),e.photos?.length&&ae.add(e.id)):(K(),a&&(Ie.textContent=V(i.said,{name:je(e),text:e.text})),A.hidden&&r.setAttribute("data-unread","")),k()}function xt(e,a){K();let o=Xe(e);o||(o=H(e,"bot"),ve(o),o.classList.add("is-streaming"),q(o,{role:"bot"},"")),o.lastChild.textContent+=a,k()}if(w){q(H("demo-question","visitor"),{role:"visitor"},Vt(Oe,X));let e=H("demo-answer","bot");q(e,{role:"bot"},i.demoAnswer),n.addEventListener("hashchange",()=>{Je=Fe(),G(),q(e,{role:"bot"},i.demoAnswer)})}function ne(e){if(e.failed=!0,e.li.classList.remove("is-pending"),e.li.classList.add("is-failed"),e.li.querySelector(".pmw-chat__retry"))return;let a=s(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),o=s(t,"button",{type:"button"},i.retry);o.addEventListener("click",()=>{e.failed=!1,e.tries=0,a.remove(),e.li.classList.replace("is-failed","is-pending"),u?oe()?ke(e):$():Se()}),a.append(o),e.li.append(a)}let oe=()=>!!S&&S.readyState===1;function ke(e){if(!(e.failed||!oe())){if(e.tries>=mt)return ne(e);e.tries++,S.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function $(){if(!u||z||S)return;n.clearTimeout(we);let e=n.location.protocol==="https:"?"wss:":"ws:",a=new URLSearchParams({c:u.c,k:u.k}),o=new m(`${e}//${n.location.host}/api/chat/ws?${a}`);S=o,o.onopen=()=>{fe=0,se&&(ie(),ye(ut)),M(""),o.send(JSON.stringify({type:"hello",since:te}));for(let c of R.values())ke(c)},o.onmessage=c=>{let h;try{h=JSON.parse(c.data)}catch{return}yt(h)},o.onclose=c=>{S===o&&(S=null);for(let x of R.values())x.tries>=mt&&ne(x);if(c.code>=4e3&&c.code<5e3)return vt(c.code);if(z)return;ie(),M(i.reconnecting);let h=ht[Math.min(fe,ht.length-1)];fe++,we=n.setTimeout($,h)}}function vt(e){if(e===pt.rpc)return re(i.reload,!1);re(i.ended,!0)}function re(e,a){z=!0,n.clearTimeout(we),M(""),He.textContent=e,ee.hidden=!a,Z.hidden=!1,g.hidden=!0;for(let o of R.values())ne(o);k()}function We(e){j=e.mode==="human"?"human":"bot",P=typeof e.ownerName=="string"?e.ownerName:null,G(),j==="human"?Ve(null,P):ve(null),e.closed&&re(i.ended,!0)}function yt(e){switch(e?.type){case"history":typeof e.ownerName=="string"&&(P=e.ownerName);for(let a of e.messages??[])Ye(a,{announce:!1});return We(e);case"message":return Ye(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return xt(e.id,e.text);case"typing":return e.role==="visitor"?void 0:ye(ut);case"status":return We(e);case"fallback":return tt(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Ze();case"error":(e.code==="closed"||e.code==="deleted")&&re(i.ended,!0)}}let Qe=!1;function Ze(){if(!(Qe||u?.lead)){Qe=!0,u&&(u.lead=!0,Ue());try{n.pmw?.event(It)}catch{}}}async function et(e,a){let o="";try{o=await f.token(e)}catch{o=""}let c,h=null;try{c=await p("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:o,lang:X,page:n.location.pathname,...a})}),h=await c.json()}catch{c=null}try{f.reset(e)}catch{}return c?.ok&&h?{ok:!0,data:h}:{ok:!1,error:c?.status===403?i.turnstile:c?.status===429?i.tooMany:null,cap:c?.status===409&&h?.error==="cap"}}let se=null,ie=()=>{n.clearTimeout(se),se=null};function Se(){return J||u||(M(""),ye(),se=n.setTimeout(()=>M(i.oneMoment),Ut),J=et(g,{}).then(e=>{if(J=null,e.ok&&e.data.conversationId&&e.data.visitorKey){u={c:e.data.conversationId,k:e.data.visitorKey,botName:typeof e.data.botName=="string"?e.data.botName:null},Ue(),G(),$();return}ie(),K();for(let a of R.values())ne(a);if(e.error)return M(e.error);M(""),tt(e.cap?i.fallbackCap:i.fallbackDown)})),J}function tt(e){$e.textContent=e,y.hidden=!1,g.hidden=!0,y.querySelector("input")?.focus()}y.addEventListener("submit",async e=>{e.preventDefault();let a=h=>y.elements[h].value.trim(),o={name:a("name"),contact:a("contact"),need:a("need")};if(!o.name||!o.contact||!o.need){B.textContent=i.fallbackMissing;return}B.textContent="";let c=await et(y,{fallbackLead:o});if(!c.ok){B.textContent=c.error??i.unavailable;return}y.replaceChildren(s(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Ze()});let kt={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function St(e){let a,o;try{a=await v(e),o=await T(a)}catch{I.textContent=i.photoUnreadable;return}let c=E(),h=H(c,"visitor");h.classList.add("is-pending");let x=s(t,"img",{class:"pmw-chat__photo",src:o,alt:i.photoAlt});x.addEventListener("load",()=>k(),{once:!0}),h.insertBefore(x,h.lastChild),q(h,{role:"visitor"},""),k(!0),u||await Se();let D=null;if(u){let Ct=new URLSearchParams({c:u.c,k:u.k,id:c});try{D=await p(`/api/chat/photo?${Ct}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:a})}catch{D=null}}if(D?.ok){h.classList.remove("is-pending"),ae.add(c),S||$();return}h.remove(),I.textContent=i[kt[D?.status]]??i.photoFailed}U?.addEventListener("change",async()=>{let e=[...U.files??[]];if(U.value="",I.textContent="",z||w||e.length===0)return;let a=zt-ae.size-be;e.length>a&&(I.textContent=i.photoLimit);let o=e.slice(0,Math.max(0,a));be+=o.length;for(let c of o)try{await St(c)}finally{be--}});function at(){let e=b.value.trim();if(!e||z||w)return;b.value="",nt();let a={id:E(),text:e.slice(0,2e3),tries:0,failed:!1};a.li=H(a.id,"visitor"),a.li.classList.add("is-pending"),q(a.li,{role:"visitor"},a.text),R.set(a.id,a),k(!0),u?oe()?ke(a):$():Se()}g.addEventListener("submit",e=>{e.preventDefault(),at()}),b.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),at())});let nt=()=>{b.style.height="auto",b.style.height=b.value?`${Math.min(b.scrollHeight,132)}px`:""};b.addEventListener("input",()=>{nt();let e=Date.now();oe()&&e-Be>Ft&&(Be=e,S.send(JSON.stringify({type:"typing"})))}),ee.addEventListener("click",()=>{u=null;try{n.sessionStorage.removeItem(Ae)}catch{}try{S?.close(1e3)}catch{}S=null,z=!1,te=0,ge.clear(),R.clear(),ae.clear(),I.textContent="",j="bot",P=null,G();for(let e of[..._.querySelectorAll("li[data-id], .pmw-chat__relay, .pmw-chat__handback")])e.remove();K(),ie(),Z.hidden=!0,g.hidden=!1,b.focus()});let Tt=()=>[...A.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));A.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),Te();return}if(e.key!=="Tab")return;let a=Tt();if(a.length===0)return;let o=a[0],c=a[a.length-1];e.shiftKey&&t.activeElement===o?(e.preventDefault(),c.focus()):!e.shiftKey&&t.activeElement===c&&(e.preventDefault(),o.focus())}),W.addEventListener("click",()=>Te());function Lt(){w||f.load(),A.hidden=!1,r.setAttribute("data-open",""),r.removeAttribute("data-unread"),u&&$(),(w?W:g.hidden?y.hidden?ee:y.querySelector("input"):b)?.focus(),k(!0,!0)}function Te(){A.hidden=!0,r.removeAttribute("data-open"),ft?.focus()}return w||Le(r,()=>f.load(),{win:n,settleMs:Dt}),r.pmwChat={open:Lt,close:Te,panel:A},r.pmwChat}export{ht as BACKOFF_MS,It as GOAL,zt as MAX_PHOTOS,mt as MAX_TRIES,Ht as PHOTO_EDGE,$t as PHOTO_QUALITY,Bt as PIN_SLACK,Ae as SESSION_KEY,Ut as SLOW_START_MS,Dt as WARM_MS,Vt as demoQuestion,la as mountChat};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/plugin-chat",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "PmeWeb chat plugin: the bubble, the lazy chat panel, the site Worker's /api/chat routes and the build-time context file. The conversations, the model and the alerts live in the pmeweb-chat service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/strings.js
CHANGED
|
@@ -23,7 +23,8 @@ export const STRINGS = {
|
|
|
23
23
|
live: 'Vous répond en direct',
|
|
24
24
|
relay: '{name} a pris le relais',
|
|
25
25
|
relayGeneric: 'Une personne de l’équipe a pris le relais',
|
|
26
|
-
|
|
26
|
+
handback: 'L’assistant virtuel a repris la conversation',
|
|
27
|
+
oneMoment: 'Un instant…',
|
|
27
28
|
reconnecting: 'Connexion perdue, reconnexion…',
|
|
28
29
|
notSent: 'Non envoyé.',
|
|
29
30
|
retry: 'Réessayer',
|
|
@@ -70,7 +71,8 @@ export const STRINGS = {
|
|
|
70
71
|
live: 'Replying live',
|
|
71
72
|
relay: '{name} took over',
|
|
72
73
|
relayGeneric: 'A team member took over',
|
|
73
|
-
|
|
74
|
+
handback: 'The virtual assistant is back in the conversation',
|
|
75
|
+
oneMoment: 'One moment…',
|
|
74
76
|
reconnecting: 'Connection lost, reconnecting…',
|
|
75
77
|
notSent: 'Not sent.',
|
|
76
78
|
retry: 'Retry',
|
package/src/trigger.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// The inline part of the chat (dist/trigger.iife.js, under the manifest's inlineBudget). At page
|
|
2
2
|
// open it fetches nothing: it shows the bubble on every host and schedules the opener. The panel
|
|
3
|
-
// (dist/widget.js) is imported on the first
|
|
4
|
-
// onInteraction
|
|
3
|
+
// (dist/widget.js) is imported on the visitor's first sign of life ANYWHERE on the page
|
|
4
|
+
// (@pmeweb/loader onInteraction on the document, PAGE_EVENTS: a press, a focus, a key, a pointer
|
|
5
|
+
// moving, a scroll) and opened on a click. A test run (Lighthouse, PageSpeed) never does any of
|
|
6
|
+
// these, so it never fetches the widget before the opener's 20 s (the opener appearing imports it
|
|
7
|
+
// too: the visitor may open it next). Importing it that early is what lets the widget start
|
|
8
|
+
// Turnstile well before the panel opens (widget.js, « TURNSTILE »). Off the site's real hosts the
|
|
5
9
|
// panel runs its demo (manifest preview: test); the widget decides that, not this file.
|
|
6
10
|
//
|
|
7
11
|
// THE DEMO FOLLOWS THE URL FRAGMENT, off the real hosts only (phase 4 spec 2.4), so the portal's
|
|
@@ -21,11 +25,13 @@
|
|
|
21
25
|
// `inert` (out of the tab order and the accessibility tree): chat.css fades it out, then turns it
|
|
22
26
|
// `visibility: hidden`. Never while the panel is open or while focus is inside the root: a focusout
|
|
23
27
|
// re-checks, so closing the panel near the footer hides the bubble once focus moves on.
|
|
24
|
-
import { isCanonical, onInteraction as interact } from '@pmeweb/loader'
|
|
28
|
+
import { INTERACTION_EVENTS, isCanonical, onInteraction as interact } from '@pmeweb/loader'
|
|
25
29
|
|
|
26
30
|
export const OPENER_KEY = 'pmw-chat-opener'
|
|
27
31
|
export const OPENER_DELAY_MS = 20000
|
|
28
32
|
export const OPENER_SCROLL = 0.3
|
|
33
|
+
/** The page-wide signs of a person: the loader's own, plus a pointer moving and a scroll. */
|
|
34
|
+
export const PAGE_EVENTS = [...INTERACTION_EVENTS, 'pointermove', 'scroll']
|
|
29
35
|
|
|
30
36
|
export function startChat({
|
|
31
37
|
doc = document, win = window, hostname = win.location.hostname, importLazy = (u) => import(/* @vite-ignore */ u), onInteraction = interact,
|
|
@@ -36,13 +42,16 @@ export function startChat({
|
|
|
36
42
|
const teaser = root.querySelector('.pmw-chat__teaser')
|
|
37
43
|
let widget = null
|
|
38
44
|
const load = () => (widget ??= importLazy(root.dataset.pmwLazy).then((m) => m.mountChat(root), (e) => { widget = null; throw e }))
|
|
39
|
-
|
|
45
|
+
const warm = () => load().catch(() => {})
|
|
46
|
+
const open = () => load().then((w) => w.open(), () => {})
|
|
47
|
+
const isOpen = () => root.hasAttribute('data-open')
|
|
48
|
+
onInteraction(doc, warm, { events: PAGE_EVENTS })
|
|
40
49
|
const canonical = isCanonical({ doc, hostname })
|
|
41
50
|
if (!canonical) {
|
|
42
51
|
const view = () => {
|
|
43
52
|
const v = new URLSearchParams(win.location.hash.slice(1)).get('pmw-chat')
|
|
44
|
-
if (v === 'panel' && !
|
|
45
|
-
else if (v === 'bubble' && widget) widget.then((w) => w.close(), () => {})
|
|
53
|
+
if (v === 'panel' && !isOpen()) open()
|
|
54
|
+
else if (v === 'bubble' && widget) widget.then((w) => w.close?.(), () => {})
|
|
46
55
|
}
|
|
47
56
|
view()
|
|
48
57
|
win.addEventListener('hashchange', view)
|
|
@@ -50,7 +59,7 @@ export function startChat({
|
|
|
50
59
|
|
|
51
60
|
let away = false
|
|
52
61
|
const sync = (f = doc.activeElement) => {
|
|
53
|
-
const h = away && !
|
|
62
|
+
const h = away && !isOpen() && !root.contains(f)
|
|
54
63
|
root.toggleAttribute('inert', h)
|
|
55
64
|
}
|
|
56
65
|
const zone = doc.querySelector('[data-pmw-chat-avoid]') ?? [...doc.querySelectorAll('footer')].pop()
|
|
@@ -70,16 +79,17 @@ export function startChat({
|
|
|
70
79
|
try { store?.setItem(OPENER_KEY, '1') } catch { /* nothing to remember with */ }
|
|
71
80
|
}
|
|
72
81
|
const show = () => {
|
|
73
|
-
if (done || away || !teaser ||
|
|
82
|
+
if (done || away || !teaser || isOpen()) return
|
|
74
83
|
finish()
|
|
75
84
|
teaser.hidden = false
|
|
85
|
+
warm()
|
|
76
86
|
}
|
|
77
87
|
root.addEventListener('click', (e) => {
|
|
78
88
|
const t = e.target.closest?.('[data-pmw-chat-open],[data-pmw-chat-dismiss]')
|
|
79
89
|
if (!t) return
|
|
80
90
|
if (teaser) teaser.hidden = true
|
|
81
91
|
finish()
|
|
82
|
-
if (t.hasAttribute('data-pmw-chat-open'))
|
|
92
|
+
if (t.hasAttribute('data-pmw-chat-open')) open()
|
|
83
93
|
})
|
|
84
94
|
if (!done && teaser && canonical) {
|
|
85
95
|
win.setTimeout(show, OPENER_DELAY_MS)
|
package/src/widget.css
CHANGED
|
@@ -97,15 +97,17 @@
|
|
|
97
97
|
@media (prefers-reduced-motion: reduce) {
|
|
98
98
|
.pmw-chat__typing span { animation: none; opacity: 0.7; }
|
|
99
99
|
}
|
|
100
|
-
/* Where a person takes over: a line, the green dot and « {name} a pris le relais », centred.
|
|
101
|
-
|
|
100
|
+
/* Where a person takes over: a line, the green dot and « {name} a pris le relais », centred. Where
|
|
101
|
+
they hand back: the same line, the dot in the muted colour (nobody is live any more). */
|
|
102
|
+
.pmw-chat__log > .pmw-chat__relay, .pmw-chat__log > .pmw-chat__handback {
|
|
102
103
|
align-self: stretch; margin: 18px 0 8px; display: flex; align-items: center; gap: 10px;
|
|
103
104
|
font-size: 13px; font-weight: 500; line-height: 1.4; color: var(--pmw-chat-muted, #4b5563);
|
|
104
105
|
}
|
|
105
|
-
.pmw-chat__relay::before, .pmw-chat__relay::after { content: ''; flex: 1; min-width: 16px; height: 1px; background: var(--pmw-chat-rule, #cfd5e1); }
|
|
106
|
+
.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); }
|
|
106
107
|
.pmw-chat__relay-label { display: flex; align-items: center; gap: 6px; text-align: center; }
|
|
107
108
|
.pmw-chat__relay-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--pmw-chat-live, #1a7f37); }
|
|
108
|
-
.pmw-
|
|
109
|
+
.pmw-chat__relay-dot--back { background: var(--pmw-chat-muted, #4b5563); }
|
|
110
|
+
.pmw-chat__log > .pmw-chat__relay + li, .pmw-chat__log > .pmw-chat__handback + li { margin-top: 0; }
|
|
109
111
|
.pmw-chat__state { margin: 8px 0 0; font-size: 14px; color: var(--pmw-chat-muted, #4b5563); }
|
|
110
112
|
.pmw-chat__state:empty { display: none; }
|
|
111
113
|
/* The end: a thin line, the sentence centred, an outlined pill. */
|
|
@@ -117,13 +119,16 @@
|
|
|
117
119
|
background: var(--pmw-chat-panel-bg, #ffffff); color: var(--pmw-chat-link, #0a1b3d); font-weight: 600;
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
/* The composer: one rounded white field holding the photo button, the text and the send pill.
|
|
122
|
+
/* The composer: one rounded white field holding the photo button, the text and the send pill.
|
|
123
|
+
16 px on each side, so the field's focus ring (3 px, 2 px out) keeps 11 px off the panel's edge;
|
|
124
|
+
on a phone the bottom adds the home indicator's safe area. */
|
|
121
125
|
.pmw-chat__composer {
|
|
122
|
-
|
|
126
|
+
--pmw-chat-gap: 8px;
|
|
127
|
+
display: flex; flex-direction: column; gap: var(--pmw-chat-gap); padding: 14px 16px 16px;
|
|
123
128
|
border-top: 1px solid var(--pmw-chat-line, #e6e9f0); background: var(--pmw-chat-panel-bg, #ffffff);
|
|
124
129
|
}
|
|
125
130
|
@media (max-width: 639.98px) {
|
|
126
|
-
.pmw-chat__composer { padding:
|
|
131
|
+
.pmw-chat__composer { padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px)); }
|
|
127
132
|
}
|
|
128
133
|
.pmw-chat__box {
|
|
129
134
|
display: flex; align-items: flex-end; gap: 6px; padding: 4px; border-radius: 24px;
|
|
@@ -145,7 +150,20 @@
|
|
|
145
150
|
background: var(--pmw-chat-accent, #0a1b3d); color: var(--pmw-chat-accent-fg, #ffffff);
|
|
146
151
|
}
|
|
147
152
|
.pmw-chat__send:disabled, .pmw-chat__box textarea:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
148
|
-
|
|
153
|
+
/* Turnstile's row (widget.js « TURNSTILE »): collapsed, with no height and no gap, until Cloudflare
|
|
154
|
+
asks the visitor to act (its before-interactive or error callback sets data-ask); collapsed again
|
|
155
|
+
on success. Full width, aligned with the field (the widget is data-size flexible, 300 px minimum:
|
|
156
|
+
the composer's inside is 358 px on a 390 px phone, 346 px in the 380 px desktop panel). In the
|
|
157
|
+
composer it sits ABOVE the field, which therefore never moves when it opens or closes. */
|
|
158
|
+
.pmw-chat__check {
|
|
159
|
+
display: grid; grid-template-rows: 0fr; margin-bottom: calc(-1 * var(--pmw-chat-gap, 0px));
|
|
160
|
+
transition: grid-template-rows 0.25s ease, margin-bottom 0.25s ease;
|
|
161
|
+
}
|
|
162
|
+
.pmw-chat__check[data-ask] { grid-template-rows: 1fr; margin-bottom: 4px; }
|
|
163
|
+
.pmw-chat__check-in { min-height: 0; overflow: hidden; }
|
|
164
|
+
@media (prefers-reduced-motion: reduce) {
|
|
165
|
+
.pmw-chat__check { transition: none; }
|
|
166
|
+
}
|
|
149
167
|
/* The photo picker: a real file input, visually hidden inside its label, so Tab reaches it and the
|
|
150
168
|
label shows its focus ring. 44 x 44 like every other control. */
|
|
151
169
|
.pmw-chat__attach {
|
|
@@ -160,7 +178,7 @@
|
|
|
160
178
|
.pmw-chat__photo { display: block; max-width: 100%; max-height: 220px; border-radius: 14px; }
|
|
161
179
|
|
|
162
180
|
/* The contact form, when the chat cannot answer: a 20 px title, rounded fields, a full-width pill. */
|
|
163
|
-
.pmw-chat__fallback { display: flex; flex-direction: column; gap:
|
|
181
|
+
.pmw-chat__fallback { --pmw-chat-gap: 14px; display: flex; flex-direction: column; gap: var(--pmw-chat-gap); margin-top: 16px; }
|
|
164
182
|
.pmw-chat__fb-title { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; }
|
|
165
183
|
.pmw-chat__fb-intro { margin: -8px 0 0; font-size: 15px; color: var(--pmw-chat-muted, #4b5563); }
|
|
166
184
|
.pmw-chat__field { display: flex; flex-direction: column; gap: 4px; }
|
package/src/widget.js
CHANGED
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
// a visitor's question drawn from it (demoQuestion) and a canned answer, with the composer disabled
|
|
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
|
-
import { isCanonical } from '@pmeweb/loader'
|
|
41
|
-
import { addScript as loadTurnstile,
|
|
40
|
+
import { isCanonical, onApproach } from '@pmeweb/loader'
|
|
41
|
+
import { addScript as loadTurnstile, tokenReady } from '@pmeweb/turnstile'
|
|
42
42
|
import { CLOSE, MAX_TEXT } from './protocol.js'
|
|
43
43
|
import { fill, langOf, stringsFor } from './strings.js'
|
|
44
44
|
|
|
@@ -49,6 +49,12 @@ export const GOAL = 'chat-lead'
|
|
|
49
49
|
export const MAX_PHOTOS = 3
|
|
50
50
|
export const PHOTO_EDGE = 1600
|
|
51
51
|
export const PHOTO_QUALITY = 0.85
|
|
52
|
+
/** How long after the page has painted (and the widget was imported) Turnstile starts, unasked. */
|
|
53
|
+
export const WARM_MS = 3000
|
|
54
|
+
/** A first start slower than this shows « Un instant… » under the typing dots. */
|
|
55
|
+
export const SLOW_START_MS = 8000
|
|
56
|
+
/** How close to the bottom of the thread (px) still counts as « at the bottom ». */
|
|
57
|
+
export const PIN_SLACK = 40
|
|
52
58
|
const TYPING_SEND_MS = 3000
|
|
53
59
|
const TYPING_SHOW_MS = 8000
|
|
54
60
|
|
|
@@ -90,14 +96,58 @@ function el(doc, tag, attrs = {}, text) {
|
|
|
90
96
|
return e
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
// TURNSTILE (operator, 2026-11-13). Loaded only when the panel opened, the challenge had no time to
|
|
100
|
+
// observe the visitor and often showed its « Vérifiez que vous êtes humain » box. So it now starts
|
|
101
|
+
// early, but never in a test run: the trigger imports this file on the visitor's first sign of life
|
|
102
|
+
// on the page (a scroll, a pointer moving, a press, a key), and mountChat then starts api.js through
|
|
103
|
+
// @pmeweb/loader onApproach on the chat root (fixed on screen): once the page has painted (`load`,
|
|
104
|
+
// first contentful paint, LCP, and this file's own import, whichever is latest) plus WARM_MS, or at
|
|
105
|
+
// once on a press or a focus in the chat, or when the panel opens. Lighthouse never interacts, so it
|
|
106
|
+
// never imports this file and never fetches api.js; a visitor who reads for a few seconds before
|
|
107
|
+
// opening the panel usually finds the token minted.
|
|
108
|
+
//
|
|
109
|
+
// THE WIDGETS RENDER HIDDEN, in the closed panel (a display:none iframe still runs: measured with
|
|
110
|
+
// Cloudflare's test keys, a token in ~1.9 s). Each sits in a row that is COLLAPSED until Cloudflare
|
|
111
|
+
// itself asks for the visitor: its `before-interactive` and `error` callbacks open the row, its
|
|
112
|
+
// success callback collapses it again (so « Succès ! » goes away), and a later expiry that needs a
|
|
113
|
+
// click opens it again. The rule « never hide the box with a guessed rule » holds: the row follows
|
|
114
|
+
// Cloudflare's callbacks, never the iframe's size. Implicit rendering only scans the page once, when
|
|
115
|
+
// api.js runs, so a widget added after a contact form already loaded api.js is rendered here.
|
|
116
|
+
//
|
|
117
|
+
// THE TOKEN: Turnstile refreshes an unused one before its 300 s expiry (refresh-expired: auto); after
|
|
118
|
+
// each post the widget is reset (a token is single-use), only while its row is not asking: a reset
|
|
119
|
+
// would wipe the box the visitor is solving. `reset(el)` names our widget, never every widget on the
|
|
120
|
+
// page (@pmeweb/turnstile rule 4). Its own resetToken is not used: it keys on the box's height, and
|
|
121
|
+
// a solved box keeps its height inside our collapsed row.
|
|
122
|
+
const defaultTurnstile = (win, widgets) => {
|
|
123
|
+
let polling = false
|
|
124
|
+
const render = () => {
|
|
125
|
+
const ts = win.turnstile
|
|
126
|
+
if (!ts?.render) return false
|
|
127
|
+
for (const w of widgets) if (!w.childElementCount) try { ts.render(w) } catch { /* left to tokenReady's timeout */ }
|
|
128
|
+
return true
|
|
129
|
+
}
|
|
130
|
+
const load = () => {
|
|
131
|
+
loadTurnstile(win.document)
|
|
132
|
+
if (polling || render()) return
|
|
133
|
+
polling = true
|
|
134
|
+
let n = 0
|
|
135
|
+
const tick = () => { if (!render() && ++n < 600) win.setTimeout(tick, 100) }
|
|
136
|
+
win.setTimeout(tick, 100)
|
|
137
|
+
}
|
|
138
|
+
for (const w of widgets) { const f = w.closest('form'); if (f) f.pmwTurnstileLoad = load }
|
|
139
|
+
return {
|
|
140
|
+
load,
|
|
141
|
+
async token(form) {
|
|
142
|
+
await tokenReady(form, { win })
|
|
143
|
+
return form.querySelector('input[name="cf-turnstile-response"]')?.value ?? ''
|
|
144
|
+
},
|
|
145
|
+
reset(form) {
|
|
146
|
+
const w = form.querySelector('.cf-turnstile')
|
|
147
|
+
if (w && !w.closest('[data-ask]')) win.turnstile?.reset(w)
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
101
151
|
|
|
102
152
|
export function mountChat(root, deps = {}) {
|
|
103
153
|
if (root.pmwChat) return root.pmwChat
|
|
@@ -105,7 +155,8 @@ export function mountChat(root, deps = {}) {
|
|
|
105
155
|
const doc = win.document
|
|
106
156
|
const WS = deps.WebSocket ?? win.WebSocket
|
|
107
157
|
const fetchImpl = deps.fetch ?? ((...a) => win.fetch(...a))
|
|
108
|
-
const
|
|
158
|
+
const widgets = []
|
|
159
|
+
let turnstile = deps.turnstile ?? null
|
|
109
160
|
const uuid = deps.uuid ?? (() => win.crypto.randomUUID())
|
|
110
161
|
const shrink = deps.shrink ?? ((file) => shrinkPhoto(win, file))
|
|
111
162
|
// A data: URL, not a blob: one: site CSPs allow `img-src data:` (boilerplate worker), not blob:.
|
|
@@ -164,19 +215,37 @@ export function mountChat(root, deps = {}) {
|
|
|
164
215
|
const state = el(doc, 'p', { class: 'pmw-chat__state', role: 'status' })
|
|
165
216
|
const announcer = el(doc, 'p', { class: 'pmw-chat__sr', 'aria-live': 'polite' })
|
|
166
217
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
218
|
+
// A widget in its row (see « TURNSTILE » above). Implicit rendering reads callbacks as global
|
|
219
|
+
// function names, one set per widget (`c` the composer, `f` the contact form).
|
|
220
|
+
const widget = (key) => {
|
|
221
|
+
const row = el(doc, 'div', { class: 'pmw-chat__check' })
|
|
222
|
+
const inner = el(doc, 'div', { class: 'pmw-chat__check-in' })
|
|
223
|
+
const cb = `pmwChatTurnstile${key}`
|
|
224
|
+
const ask = (on) => () => { row.toggleAttribute('data-ask', on); follow() }
|
|
225
|
+
win[`${cb}Ask`] = ask(true)
|
|
226
|
+
win[`${cb}Error`] = ask(true)
|
|
227
|
+
win[`${cb}Done`] = ask(false)
|
|
228
|
+
const w = el(doc, 'div', {
|
|
229
|
+
class: 'cf-turnstile pmw-turnstile', 'data-sitekey': cfg.siteKey ?? '', 'data-action': 'chat', 'data-theme': 'light',
|
|
230
|
+
'data-language': lang, 'data-appearance': 'interaction-only', 'data-size': 'flexible',
|
|
231
|
+
'data-callback': `${cb}Done`, 'data-before-interactive-callback': `${cb}Ask`, 'data-error-callback': `${cb}Error`,
|
|
232
|
+
})
|
|
233
|
+
widgets.push(w)
|
|
234
|
+
inner.append(w)
|
|
235
|
+
row.append(inner)
|
|
236
|
+
return row
|
|
237
|
+
}
|
|
171
238
|
const composer = el(doc, 'form', { class: 'pmw-chat__composer', novalidate: true })
|
|
172
239
|
const input = el(doc, 'textarea', { id: 'pmw-chat-input', name: 'message', rows: '1', maxlength: String(MAX_TEXT), placeholder: s.messageLabel, autocomplete: 'off', disabled: demo, 'aria-describedby': demo ? 'pmw-chat-demo-hint' : null })
|
|
173
240
|
const send = el(doc, 'button', { type: 'submit', class: 'pmw-chat__send', disabled: demo }, s.send)
|
|
174
241
|
// One rounded field holds the photo button, the text and the send button; hints and errors go under it.
|
|
175
242
|
const box = el(doc, 'div', { class: 'pmw-chat__box' })
|
|
176
243
|
box.append(el(doc, 'label', { class: 'pmw-chat__sr', for: 'pmw-chat-input' }, s.messageLabel), input, send)
|
|
244
|
+
// The check row sits ABOVE the field: the composer is pinned to the panel's bottom, so the row
|
|
245
|
+
// opening or collapsing never moves the field the visitor is typing in.
|
|
246
|
+
if (!demo) composer.append(widget('c'))
|
|
177
247
|
composer.append(box)
|
|
178
248
|
if (demo) composer.append(el(doc, 'p', { class: 'pmw-chat__hint', id: 'pmw-chat-demo-hint' }, s.demoHint))
|
|
179
|
-
else composer.append(widget(composer))
|
|
180
249
|
let picker = null
|
|
181
250
|
const photoError = el(doc, 'p', { class: 'pmw-chat__error pmw-chat__photo-error', role: 'alert' })
|
|
182
251
|
if (cfg.photos === true) {
|
|
@@ -210,11 +279,49 @@ export function mountChat(root, deps = {}) {
|
|
|
210
279
|
field('need', 'textarea', { rows: '3', maxlength: '2000' }),
|
|
211
280
|
fbError, el(doc, 'button', { type: 'submit', class: 'pmw-chat__send' }, s.fallbackSend),
|
|
212
281
|
)
|
|
213
|
-
if (!demo) fallback.
|
|
282
|
+
if (!demo) fallback.insertBefore(widget('f'), fbError)
|
|
214
283
|
const body = el(doc, 'div', { class: 'pmw-chat__body' })
|
|
215
284
|
body.append(log, state, ended, fallback)
|
|
216
285
|
panel.append(head, body, composer, announcer)
|
|
217
286
|
root.append(panel)
|
|
287
|
+
turnstile ??= defaultTurnstile(win, widgets)
|
|
288
|
+
|
|
289
|
+
// ---- following the thread ---------------------------------------------------------------
|
|
290
|
+
// PINNED (operator, 2026-11-13): while the visitor is at the bottom (within PIN_SLACK), every
|
|
291
|
+
// change that adds height (a streamed fragment, the stored message replacing it, the typing dots,
|
|
292
|
+
// a photo once decoded, the takeover line, the end block, the composer growing or its check row
|
|
293
|
+
// opening) keeps the thread at the bottom. A visitor who scrolled UP to read stays where they are;
|
|
294
|
+
// reaching the bottom again pins it again. Sending a message is explicit: it always pins. The
|
|
295
|
+
// scroll is smooth, one per animation frame however many fragments land in it (a stream would
|
|
296
|
+
// otherwise stack dozens), and instant under prefers-reduced-motion and when the panel opens.
|
|
297
|
+
// Our own scrolls only ever move down, so a scroll that moves UP away from the bottom is the
|
|
298
|
+
// visitor's.
|
|
299
|
+
let pinned = true
|
|
300
|
+
let lastTop = 0
|
|
301
|
+
let followAt = 0
|
|
302
|
+
const gap = () => body.scrollHeight - body.scrollTop - body.clientHeight
|
|
303
|
+
body.addEventListener('scroll', () => {
|
|
304
|
+
const t = body.scrollTop
|
|
305
|
+
if (gap() <= PIN_SLACK) pinned = true
|
|
306
|
+
else if (t < lastTop) pinned = false
|
|
307
|
+
lastTop = t
|
|
308
|
+
}, { passive: true })
|
|
309
|
+
const nextFrame = (f) => (win.requestAnimationFrame ? win.requestAnimationFrame(f) : win.setTimeout(f, 16))
|
|
310
|
+
function follow(force = false, instant = false) {
|
|
311
|
+
if (force) pinned = true
|
|
312
|
+
if (!pinned || followAt) return
|
|
313
|
+
followAt = nextFrame(() => {
|
|
314
|
+
followAt = 0
|
|
315
|
+
if (!pinned) return
|
|
316
|
+
const still = instant || !!win.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
317
|
+
body.scrollTo({ top: body.scrollHeight, behavior: still ? 'instant' : 'smooth' })
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
if (typeof win.ResizeObserver === 'function') {
|
|
321
|
+
const ro = new win.ResizeObserver(() => follow())
|
|
322
|
+
ro.observe(log)
|
|
323
|
+
ro.observe(composer)
|
|
324
|
+
}
|
|
218
325
|
|
|
219
326
|
// ---- state ------------------------------------------------------------------------------
|
|
220
327
|
let session = null
|
|
@@ -269,18 +376,37 @@ export function mountChat(root, deps = {}) {
|
|
|
269
376
|
// scan back stops at a line already there or at an owner message, so one takeover gets one line
|
|
270
377
|
// however often the frames repeat it; the bot answering again and a person taking over again
|
|
271
378
|
// gets another.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
379
|
+
//
|
|
380
|
+
// THE HAND-BACK (operator, 2026-11-13) is its mirror: where the owner gives the conversation back
|
|
381
|
+
// (« Rendre à l’assistant »), one line « L’assistant virtuel a repris la conversation », always
|
|
382
|
+
// the « assistant virtuel » wording (never the bot's name alone: it must not pass for a person).
|
|
383
|
+
// It goes before the first bot message that follows a person (live, and on replay), or at the end
|
|
384
|
+
// when the mode turns back to bot before the bot speaks. Its scan back stops at a hand-back line
|
|
385
|
+
// already there or at a bot message (nothing to mark), and inserts only past an owner message or a
|
|
386
|
+
// takeover line: a conversation the bot never left gets none. A takeover after it gets its own line.
|
|
387
|
+
const lastLine = () => (typing.parentNode === log ? typing.previousElementSibling : log.lastElementChild)
|
|
388
|
+
const isA = (x, c) => x.classList.contains(c)
|
|
389
|
+
function separator(cls, dotCls, text, at) {
|
|
390
|
+
const sep = el(doc, 'li', { class: cls, role: 'separator', 'aria-label': text })
|
|
280
391
|
const label = el(doc, 'span', { class: 'pmw-chat__relay-label', 'aria-hidden': 'true' })
|
|
281
|
-
label.append(el(doc, 'span', { class:
|
|
392
|
+
label.append(el(doc, 'span', { class: dotCls }), text)
|
|
282
393
|
sep.append(label)
|
|
283
394
|
log.insertBefore(sep, at ?? (typing.parentNode === log ? typing : null))
|
|
395
|
+
follow()
|
|
396
|
+
}
|
|
397
|
+
function relay(at, by) {
|
|
398
|
+
for (let x = at ? at.previousElementSibling : lastLine(); x; x = x.previousElementSibling) {
|
|
399
|
+
if (isA(x, 'pmw-chat__relay') || isA(x, 'pmw-chat__msg--owner')) return
|
|
400
|
+
if (isA(x, 'pmw-chat__msg--bot') || isA(x, 'pmw-chat__handback')) break
|
|
401
|
+
}
|
|
402
|
+
separator('pmw-chat__relay', 'pmw-chat__relay-dot', by ? fill(s.relay, { name: by }) : s.relayGeneric, at)
|
|
403
|
+
}
|
|
404
|
+
function handback(at) {
|
|
405
|
+
for (let x = at ? at.previousElementSibling : lastLine(); ; x = x.previousElementSibling) {
|
|
406
|
+
if (!x || isA(x, 'pmw-chat__handback') || isA(x, 'pmw-chat__msg--bot')) return
|
|
407
|
+
if (isA(x, 'pmw-chat__relay') || isA(x, 'pmw-chat__msg--owner')) break
|
|
408
|
+
}
|
|
409
|
+
separator('pmw-chat__handback', 'pmw-chat__relay-dot pmw-chat__relay-dot--back', s.handback, at)
|
|
284
410
|
}
|
|
285
411
|
const find = (id) => [...log.querySelectorAll('li[data-id]')].find((li) => li.dataset.id === id)
|
|
286
412
|
const setText = (li, m, text) => {
|
|
@@ -292,6 +418,14 @@ export function mountChat(root, deps = {}) {
|
|
|
292
418
|
typing.hidden = true
|
|
293
419
|
win.clearTimeout(typingTimer)
|
|
294
420
|
}
|
|
421
|
+
/** The dots, until a reply hides them; `ms` also hides them after that long without one. */
|
|
422
|
+
function showTyping(ms) {
|
|
423
|
+
typing.hidden = false
|
|
424
|
+
log.append(typing)
|
|
425
|
+
win.clearTimeout(typingTimer)
|
|
426
|
+
if (ms) typingTimer = win.setTimeout(hideTyping, ms)
|
|
427
|
+
follow()
|
|
428
|
+
}
|
|
295
429
|
function render(m, { announce }) {
|
|
296
430
|
if (seen.has(m.seq)) return
|
|
297
431
|
seen.add(m.seq)
|
|
@@ -300,6 +434,7 @@ export function mountChat(root, deps = {}) {
|
|
|
300
434
|
if (!li) {
|
|
301
435
|
li = line(m.id, m.role)
|
|
302
436
|
if (m.role === 'owner') relay(li, m.by || ownerName)
|
|
437
|
+
else if (m.role === 'bot') handback(li)
|
|
303
438
|
}
|
|
304
439
|
li.classList.remove('is-streaming', 'is-pending', 'is-failed')
|
|
305
440
|
li.querySelector('.pmw-chat__retry')?.remove()
|
|
@@ -313,20 +448,20 @@ export function mountChat(root, deps = {}) {
|
|
|
313
448
|
if (announce) announcer.textContent = fill(s.said, { name: speaker(m), text: m.text })
|
|
314
449
|
if (panel.hidden) root.setAttribute('data-unread', '')
|
|
315
450
|
}
|
|
316
|
-
|
|
451
|
+
follow()
|
|
317
452
|
}
|
|
318
453
|
function delta(id, text) {
|
|
319
454
|
hideTyping()
|
|
320
455
|
let li = find(id)
|
|
321
456
|
if (!li) {
|
|
322
457
|
li = line(id, 'bot')
|
|
458
|
+
handback(li)
|
|
323
459
|
li.classList.add('is-streaming')
|
|
324
460
|
setText(li, { role: 'bot' }, '')
|
|
325
461
|
}
|
|
326
462
|
li.lastChild.textContent += text
|
|
327
|
-
|
|
463
|
+
follow()
|
|
328
464
|
}
|
|
329
|
-
const scroll = () => { log.scrollTop = log.scrollHeight }
|
|
330
465
|
if (demo) {
|
|
331
466
|
setText(line('demo-question', 'visitor'), { role: 'visitor' }, demoQuestion(opener, lang))
|
|
332
467
|
const answer = line('demo-answer', 'bot')
|
|
@@ -375,6 +510,8 @@ export function mountChat(root, deps = {}) {
|
|
|
375
510
|
ws = sock
|
|
376
511
|
sock.onopen = () => {
|
|
377
512
|
attempt = 0
|
|
513
|
+
// A first start: the dots stay until the reply, for TYPING_SHOW_MS at most, like the service's.
|
|
514
|
+
if (slowTimer) { notSlow(); showTyping(TYPING_SHOW_MS) }
|
|
378
515
|
showState('')
|
|
379
516
|
sock.send(JSON.stringify({ type: 'hello', since: lastSeq }))
|
|
380
517
|
for (const entry of pending.values()) transmit(entry)
|
|
@@ -389,6 +526,7 @@ export function mountChat(root, deps = {}) {
|
|
|
389
526
|
for (const entry of pending.values()) if (entry.tries >= MAX_TRIES) failed(entry)
|
|
390
527
|
if (e.code >= 4000 && e.code < 5000) return refused(e.code)
|
|
391
528
|
if (over) return
|
|
529
|
+
notSlow()
|
|
392
530
|
showState(s.reconnecting)
|
|
393
531
|
const wait = BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]
|
|
394
532
|
attempt++
|
|
@@ -408,12 +546,14 @@ export function mountChat(root, deps = {}) {
|
|
|
408
546
|
ended.hidden = false
|
|
409
547
|
composer.hidden = true
|
|
410
548
|
for (const entry of pending.values()) failed(entry)
|
|
549
|
+
follow()
|
|
411
550
|
}
|
|
412
551
|
function status(m) {
|
|
413
552
|
mode = m.mode === 'human' ? 'human' : 'bot'
|
|
414
553
|
ownerName = typeof m.ownerName === 'string' ? m.ownerName : null
|
|
415
554
|
header()
|
|
416
555
|
if (mode === 'human') relay(null, ownerName)
|
|
556
|
+
else handback(null)
|
|
417
557
|
if (m.closed) end(s.ended, true)
|
|
418
558
|
}
|
|
419
559
|
function frame(m) {
|
|
@@ -428,11 +568,7 @@ export function mountChat(root, deps = {}) {
|
|
|
428
568
|
return delta(m.id, m.text)
|
|
429
569
|
case 'typing':
|
|
430
570
|
if (m.role === 'visitor') return
|
|
431
|
-
|
|
432
|
-
log.append(typing)
|
|
433
|
-
win.clearTimeout(typingTimer)
|
|
434
|
-
typingTimer = win.setTimeout(hideTyping, TYPING_SHOW_MS)
|
|
435
|
-
return scroll()
|
|
571
|
+
return showTyping(TYPING_SHOW_MS)
|
|
436
572
|
case 'status':
|
|
437
573
|
return status(m)
|
|
438
574
|
case 'fallback':
|
|
@@ -473,9 +609,19 @@ export function mountChat(root, deps = {}) {
|
|
|
473
609
|
const error = res?.status === 403 ? s.turnstile : res?.status === 429 ? s.tooMany : null
|
|
474
610
|
return { ok: false, error, cap: res?.status === 409 && data?.error === 'cap' }
|
|
475
611
|
}
|
|
612
|
+
// THE FIRST START (operator, 2026-11-13) reads like any reply: the typing dots at once, no
|
|
613
|
+
// « Connexion… ». Only a start slower than SLOW_START_MS adds « Un instant… »; the socket's open
|
|
614
|
+
// clears it, a drop says « Connexion perdue, reconnexion… » as before. Nothing is started before
|
|
615
|
+
// the visitor writes: pmeweb-chat counts every /start toward the visitor's conversations of the day
|
|
616
|
+
// (src/ipLimit.ts there), so a panel opened and never used must not spend one. Only the token is
|
|
617
|
+
// prepared ahead (« TURNSTILE » above).
|
|
618
|
+
let slowTimer = null
|
|
619
|
+
const notSlow = () => { win.clearTimeout(slowTimer); slowTimer = null }
|
|
476
620
|
function begin() {
|
|
477
621
|
if (starting || session) return starting
|
|
478
|
-
showState(
|
|
622
|
+
showState('')
|
|
623
|
+
showTyping()
|
|
624
|
+
slowTimer = win.setTimeout(() => showState(s.oneMoment), SLOW_START_MS)
|
|
479
625
|
starting = post(composer, {}).then((r) => {
|
|
480
626
|
starting = null
|
|
481
627
|
if (r.ok && r.data.conversationId && r.data.visitorKey) {
|
|
@@ -485,6 +631,8 @@ export function mountChat(root, deps = {}) {
|
|
|
485
631
|
connect()
|
|
486
632
|
return
|
|
487
633
|
}
|
|
634
|
+
notSlow()
|
|
635
|
+
hideTyping()
|
|
488
636
|
for (const entry of pending.values()) failed(entry)
|
|
489
637
|
if (r.error) return showState(r.error)
|
|
490
638
|
showState('')
|
|
@@ -532,9 +680,12 @@ export function mountChat(root, deps = {}) {
|
|
|
532
680
|
const id = uuid()
|
|
533
681
|
const li = line(id, 'visitor')
|
|
534
682
|
li.classList.add('is-pending')
|
|
535
|
-
|
|
683
|
+
const img = el(doc, 'img', { class: 'pmw-chat__photo', src, alt: s.photoAlt })
|
|
684
|
+
// Its height is known only once decoded: re-pin then (follow() is a no-op if they scrolled up).
|
|
685
|
+
img.addEventListener('load', () => follow(), { once: true })
|
|
686
|
+
li.insertBefore(img, li.lastChild)
|
|
536
687
|
setText(li, { role: 'visitor' }, '')
|
|
537
|
-
|
|
688
|
+
follow(true)
|
|
538
689
|
if (!session) await begin()
|
|
539
690
|
let res = null
|
|
540
691
|
if (session) {
|
|
@@ -580,7 +731,7 @@ export function mountChat(root, deps = {}) {
|
|
|
580
731
|
entry.li.classList.add('is-pending')
|
|
581
732
|
setText(entry.li, { role: 'visitor' }, entry.text)
|
|
582
733
|
pending.set(entry.id, entry)
|
|
583
|
-
|
|
734
|
+
follow(true)
|
|
584
735
|
if (!session) begin()
|
|
585
736
|
else if (isOpen()) transmit(entry)
|
|
586
737
|
else connect()
|
|
@@ -616,7 +767,9 @@ export function mountChat(root, deps = {}) {
|
|
|
616
767
|
mode = 'bot'
|
|
617
768
|
ownerName = null
|
|
618
769
|
header()
|
|
619
|
-
for (const li of [...log.querySelectorAll('li[data-id], .pmw-chat__relay')]) li.remove()
|
|
770
|
+
for (const li of [...log.querySelectorAll('li[data-id], .pmw-chat__relay, .pmw-chat__handback')]) li.remove()
|
|
771
|
+
hideTyping()
|
|
772
|
+
notSlow()
|
|
620
773
|
ended.hidden = true
|
|
621
774
|
composer.hidden = false
|
|
622
775
|
input.focus()
|
|
@@ -644,7 +797,7 @@ export function mountChat(root, deps = {}) {
|
|
|
644
797
|
root.removeAttribute('data-unread')
|
|
645
798
|
if (session) connect()
|
|
646
799
|
;(demo ? closeBtn : composer.hidden ? (fallback.hidden ? restart : fallback.querySelector('input')) : input)?.focus()
|
|
647
|
-
|
|
800
|
+
follow(true, true)
|
|
648
801
|
}
|
|
649
802
|
function close() {
|
|
650
803
|
panel.hidden = true
|
|
@@ -652,6 +805,9 @@ export function mountChat(root, deps = {}) {
|
|
|
652
805
|
bubble?.focus()
|
|
653
806
|
}
|
|
654
807
|
|
|
808
|
+
// The early start (« TURNSTILE » above). Off the real hosts there is nothing to start.
|
|
809
|
+
if (!demo) onApproach(root, () => turnstile.load(), { win, settleMs: WARM_MS })
|
|
810
|
+
|
|
655
811
|
root.pmwChat = { open: show, close, panel }
|
|
656
812
|
return root.pmwChat
|
|
657
813
|
}
|
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.1'
|
|
36
36
|
export { RPC_VERSION }
|
|
37
37
|
|
|
38
38
|
const SERVICE = 'https://pmeweb-chat.internal'
|