@pmeweb/plugin-chat 1.3.0 → 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +2 -2
- package/README.md +46 -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 +40 -10
- package/src/widget.js +304 -47
- package/src/worker.js +7 -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,49 @@ 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`) are
|
|
72
|
+
rendered by the chat itself, explicitly (options and callbacks as an object), exactly once per
|
|
73
|
+
box (a `data-pmw-rendered` mark is the guard). They carry no `cf-turnstile` class, so `api.js`'s
|
|
74
|
+
implicit pass, which renders a contact form's `<Turnstile>`, never reaches them, and the chat
|
|
75
|
+
never renders, resets or alters a widget it does not own. The composer's box mints its token in
|
|
76
|
+
the closed panel, which stays laid out (transparent, `inert`, no pointer events) so the box has
|
|
77
|
+
a real size; the contact form's box is rendered once that form is shown. Each sits in its own
|
|
78
|
+
row, collapsed (the box transparent and out of the flow, full width) until Turnstile's
|
|
79
|
+
`before-interactive` callback opens it; its success callback collapses it again (« Succès ! »
|
|
80
|
+
goes away, 0.25 s, at once under reduced motion). In the composer the row sits above the field,
|
|
81
|
+
so the field never moves. An unused token is refreshed by Turnstile before its 300 s expiry; a
|
|
82
|
+
used one is replaced just before the next post (`turnstile.reset(id)`, never while its row is
|
|
83
|
+
asking).
|
|
84
|
+
- **A failed challenge is retried once, silently.** A retryable error code (300*, 600*, 110600,
|
|
85
|
+
110620) resets the widget once without showing anything. A second failure keeps the row
|
|
86
|
+
collapsed; the next post shows « L’envoi n’a pas abouti. Réessayez. » without posting, and the
|
|
87
|
+
one after starts a fresh challenge. Measured with Cloudflare's always-block test key (which
|
|
88
|
+
answers 600010, the code a desktop Chrome in DevTools device emulation gets): one reset, one
|
|
89
|
+
second 600010, no box shown.
|
|
90
|
+
- **The first message reads like a reply.** The typing dots show at once while the conversation
|
|
91
|
+
starts (token, `/api/chat/start`, socket); « Un instant… » joins them only past 8 s. Nothing is
|
|
92
|
+
started before the visitor writes: pmeweb-chat counts every start toward the visitor's
|
|
93
|
+
conversations of the day, so only the token is prepared ahead.
|
|
94
|
+
- **The thread stays at the bottom** while the visitor is there (within 40 px): streamed text, the
|
|
95
|
+
final message, the dots, a photo once decoded, the takeover and hand-back lines, the end block, the
|
|
96
|
+
composer growing. One smooth scroll per frame (instant under reduced motion). A visitor who
|
|
97
|
+
scrolled up to read stays put; sending a message always scrolls down.
|
|
57
98
|
- **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
99
|
Shown once per visit (20 s, 30 % of the page, or exit intent with a mouse), never while the panel
|
|
59
100
|
is open, never while the footer is in view (it waits for the next of those triggers). On a phone
|
|
@@ -70,7 +111,9 @@ file ».
|
|
|
70
111
|
(the first one wins over the footer). Never while the panel is open or while focus is in the
|
|
71
112
|
bubble or the opener (a focusout re-checks). Reduced motion: hidden and shown at once.
|
|
72
113
|
- **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.
|
|
114
|
+
only when the owner has taken over, with the owner's name. Where the owner takes over, the thread
|
|
115
|
+
shows « {nom} a pris le relais »; where they hand back (« Rendre à l’assistant »), « L’assistant
|
|
116
|
+
virtuel a repris la conversation » (the dot muted), live and on replay, once per hand-back.
|
|
74
117
|
- **The socket.** A drop (1006, a forced deploy of pmeweb-chat) reconnects with backoff 1 to 30 s
|
|
75
118
|
and catches up from the last message seen; a message typed meanwhile is queued and sent on open,
|
|
76
119
|
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 Je=`.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{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{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__log>.pmw-chat__relay+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{display:flex;flex-direction:column;gap:6px;padding:12px 12px 10px;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:10px 12px 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__composer .pmw-turnstile,.pmw-chat__fallback .pmw-turnstile{align-self:stretch}.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{display:flex;flex-direction:column;gap:14px;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 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 it=`.pmw-chat__panel{position:fixed;z-index:81;inset:0;display:flex;flex-direction:column;background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-fg, #1f2328);font-family:var(--pmw-chat-font, inherit);font-size:16px;line-height:1.45}.pmw-chat__panel[hidden]{display:flex!important;opacity:0;pointer-events:none}.pmw-chat__panel [hidden]{display:none!important}@media(min-width:640px){.pmw-chat__panel{inset:auto;right:calc(env(safe-area-inset-right,0px) + var(--pmw-chat-right, 16px));bottom:calc(env(safe-area-inset-bottom,0px) + var(--pmw-chat-bottom, 16px));width:380px;height:min(600px,calc(100dvh - 16px - env(safe-area-inset-bottom,0px) - var(--pmw-chat-bottom, 16px)));border:1px solid var(--pmw-chat-line, #e6e9f0);border-radius:20px;overflow:hidden;box-shadow:0 18px 48px #1018282e}}.pmw-chat__panel :where(button){font:inherit;cursor:pointer}.pmw-chat__panel :focus-visible{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__head{display:flex;align-items:center;gap:12px;padding:14px 10px 14px 16px;border-bottom:1px solid var(--pmw-chat-line, #e6e9f0)}@media(max-width:639.98px){.pmw-chat__head{padding:12px 8px 12px 16px}}.pmw-chat__face{position:relative;flex:none;display:flex}.pmw-chat__head .pmw-chat__avatar{width:44px;height:44px;box-shadow:none}.pmw-chat__head .pmw-chat__avatar--icon{box-shadow:0 0 0 1.5px var(--pmw-chat-avatar-ring, transparent)}.pmw-chat__head .pmw-chat__avatar img{width:44px;height:44px}.pmw-chat__head .pmw-chat__avatar--logo img{width:32px;height:32px}.pmw-chat__head .pmw-chat__avatar svg{width:100%;height:100%}.pmw-chat__dot{position:absolute;right:-1px;bottom:-1px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-live, #1a7f37);box-shadow:0 0 0 2px var(--pmw-chat-panel-bg, #ffffff)}.pmw-chat__who{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}.pmw-chat__name{margin:0;font-weight:600;font-size:16px}.pmw-chat__role,.pmw-chat__notice{margin:0;font-size:13px;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__panel[data-mode=human] .pmw-chat__role{color:var(--pmw-chat-live, #1a7f37);font-weight:500}.pmw-chat__notice a{color:var(--pmw-chat-link, #0a1b3d);text-decoration:underline;text-underline-offset:2px}.pmw-chat__close{flex:none;width:44px;height:44px;padding:0;border:0;border-radius:50%;display:flex;align-items:center;justify-content:center;background:transparent;color:var(--pmw-chat-fg, #1f2328)}.pmw-chat__close:hover{background:var(--pmw-chat-thread-bg, #f7f8fb)}.pmw-chat__close svg{width:18px;height:18px}.pmw-chat__body{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding:18px 16px;background:var(--pmw-chat-thread-bg, #f7f8fb)}.pmw-chat__log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}.pmw-chat__log>li{margin-top:4px}.pmw-chat__log>li:first-child,.pmw-chat__msg--bot+.pmw-chat__msg--bot,.pmw-chat__msg--bot+.pmw-chat__typing,.pmw-chat__msg--owner+.pmw-chat__msg--owner,.pmw-chat__msg--owner+.pmw-chat__typing,.pmw-chat__msg--visitor+.pmw-chat__msg--visitor{margin-top:0}.pmw-chat__msg{max-width:82%;padding:10px 14px;border-radius:18px;white-space:pre-wrap;overflow-wrap:anywhere}.pmw-chat__msg--bot,.pmw-chat__msg--owner{align-self:flex-start;border-bottom-left-radius:6px;color:var(--pmw-chat-fg, #1f2328)}.pmw-chat__msg--bot{background:var(--pmw-chat-other-bg, #ffffff);border:1px solid var(--pmw-chat-line, #e6e9f0)}.pmw-chat__msg--owner{background:var(--pmw-chat-owner-bg, #eef2fa);border:1px solid var(--pmw-chat-owner-border, #d5ddef)}.pmw-chat__msg--visitor{align-self:flex-end;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff);border-bottom-right-radius:6px}.pmw-chat__msg--bot+.pmw-chat__msg--bot,.pmw-chat__msg--owner+.pmw-chat__msg--owner{border-top-left-radius:6px}.pmw-chat__msg--visitor+.pmw-chat__msg--visitor{border-top-right-radius:6px}.pmw-chat__msg:has(>.pmw-chat__photo){padding:4px}.pmw-chat__msg:has(>.pmw-chat__photo) .pmw-chat__text:empty{display:none}.pmw-chat__msg.is-pending{opacity:.75}.pmw-chat__msg.is-failed{outline:2px solid var(--pmw-chat-error, #b42318);outline-offset:2px}.pmw-chat__retry{display:block;margin-top:4px;font-size:14px}.pmw-chat__retry button{padding:0;border:0;background:none;color:inherit;text-decoration:underline}.pmw-chat__typing{align-self:flex-start;display:flex;gap:5px;padding:14px 16px;border-radius:18px 18px 18px 6px;background:var(--pmw-chat-other-bg, #ffffff);border:1px solid var(--pmw-chat-line, #e6e9f0)}.pmw-chat__typing span{width:7px;height:7px;border-radius:50%;background:var(--pmw-chat-muted, #4b5563);animation:pmw-chat-dot 1.2s infinite ease-in-out}.pmw-chat__typing span:nth-child(2){animation-delay:.15s}.pmw-chat__typing span:nth-child(3){animation-delay:.3s}@keyframes pmw-chat-dot{0%,80%,to{opacity:.3}40%{opacity:1}}@media(prefers-reduced-motion:reduce){.pmw-chat__typing span{animation:none;opacity:.7}}.pmw-chat__log>.pmw-chat__relay,.pmw-chat__log>.pmw-chat__handback{align-self:stretch;margin:18px 0 8px;display:flex;align-items:center;gap:10px;font-size:13px;font-weight:500;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__relay:before,.pmw-chat__relay:after,.pmw-chat__handback:before,.pmw-chat__handback:after{content:"";flex:1;min-width:16px;height:1px;background:var(--pmw-chat-rule, #cfd5e1)}.pmw-chat__relay-label{display:flex;align-items:center;gap:6px;text-align:center}.pmw-chat__relay-dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--pmw-chat-live, #1a7f37)}.pmw-chat__relay-dot--back{background:var(--pmw-chat-muted, #4b5563)}.pmw-chat__log>.pmw-chat__relay+li,.pmw-chat__log>.pmw-chat__handback+li{margin-top:0}.pmw-chat__state{margin:8px 0 0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__state:empty{display:none}.pmw-chat__ended{margin-top:22px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center}.pmw-chat__ended:before{content:"";align-self:stretch;height:1px;background:var(--pmw-chat-line, #e6e9f0)}.pmw-chat__ended p{margin:0;font-size:15px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__restart{min-height:44px;padding:0 20px;border-radius:22px;border:1px solid var(--pmw-chat-link, #0a1b3d);background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-link, #0a1b3d);font-weight:600}.pmw-chat__composer{--pmw-chat-gap: 8px;display:flex;flex-direction:column;gap:var(--pmw-chat-gap);padding:14px 16px 16px;border-top:1px solid var(--pmw-chat-line, #e6e9f0);background:var(--pmw-chat-panel-bg, #ffffff)}@media(max-width:639.98px){.pmw-chat__composer{padding:12px 16px calc(14px + env(safe-area-inset-bottom,0px))}}.pmw-chat__box{display:flex;align-items:flex-end;gap:6px;padding:4px;border-radius:24px;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__box textarea{flex:1;min-width:0;box-sizing:border-box;min-height:44px;max-height:132px;resize:none;margin:0;padding:10px 2px;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;font-size:16px;line-height:1.5}.pmw-chat__box textarea::placeholder{color:#6b7280;opacity:1}@supports selector(:has(*)){.pmw-chat__box textarea:focus-visible{outline:none}.pmw-chat__box:has(textarea:focus-visible){outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}}.pmw-chat__send{flex:none;min-height:44px;padding:0 18px;border:0;border-radius:22px;font-weight:600;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__send:disabled,.pmw-chat__box textarea:disabled{cursor:not-allowed;opacity:.6}.pmw-chat__check{position:relative;display:grid;grid-template-rows:0fr;margin-bottom:calc(-1 * var(--pmw-chat-gap, 0px));transition:grid-template-rows .25s ease,margin-bottom .25s ease}.pmw-chat__check[data-ask]{grid-template-rows:1fr;margin-bottom:4px}.pmw-chat__check-in{min-height:0;overflow:hidden}.pmw-chat__check:not([data-ask]) .pmw-chat__check-in{position:absolute;top:0;left:0;right:0;overflow:visible;opacity:0;pointer-events:none}@media(prefers-reduced-motion:reduce){.pmw-chat__check{transition:none}}.pmw-chat__attach{flex:none;position:relative;width:44px;height:44px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;color:inherit}.pmw-chat__attach:hover{background:#eef0f4}.pmw-chat__attach svg{width:22px;height:22px}.pmw-chat__attach:focus-within{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__hint{margin:0 8px;font-size:13px;line-height:1.4;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__composer .pmw-chat__photo-error{margin:0 8px}.pmw-chat__photo{display:block;max-width:100%;max-height:220px;border-radius:14px}.pmw-chat__fallback{--pmw-chat-gap: 14px;display:flex;flex-direction:column;gap:var(--pmw-chat-gap);margin-top:16px}.pmw-chat__fb-title{margin:0;font-size:20px;font-weight:600;line-height:1.3}.pmw-chat__fb-intro{margin:-8px 0 0;font-size:15px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__field{display:flex;flex-direction:column;gap:4px}.pmw-chat__field label{font-size:14px;font-weight:600}.pmw-chat__field input,.pmw-chat__field textarea{font:inherit;font-size:16px;padding:10px 14px;border-radius:12px;resize:none;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__fallback .pmw-chat__send{align-self:stretch;min-height:48px;border-radius:24px}.pmw-chat__error{margin:0;font-size:14px;color:var(--pmw-chat-error, #b42318)}.pmw-chat__error:empty{display:none}.pmw-chat__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.pmw-chat[data-unread] .pmw-chat__bubble{position:relative}.pmw-chat[data-unread] .pmw-chat__bubble:after{content:"";position:absolute;top:2px;left:38px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-error, #b42318);box-shadow:0 0 0 2px #fff}.pmw-chat__fb-done{margin:12px 0 0;padding:12px 14px;border-radius:12px;background:#e9f6ee;color:#14532d;border:1px solid #b7dfc4;font-weight:500}
|
|
2
|
+
`;var Mt="400px 0px";var st=new Set,pe=null;function ct(r,{doc:p=document,attrs:o={}}={}){if(st.has(r)||(st.add(r),p.querySelector(`script[src="${r}"]`)))return;let t=p.createElement("script");t.src=r,t.async=!0;for(let[f,m]of Object.entries(o))t.setAttribute(f,m);p.head.append(t)}function lt({doc:r=document,hostname:p=globalThis.location?.hostname??""}={}){let o=r.getElementById("pmw-hosts");if(!o)return!1;let t;try{t=JSON.parse(o.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let f=String(p).toLowerCase();return t.some(m=>typeof m=="string"&&m.toLowerCase()===f)}function Nt(r){return pe||(pe=new Promise(p=>{let o=null,t=null,f=0,m=r.PerformanceObserver?.supportedEntryTypes??[],w=typeof r.PerformanceObserver=="function"&&m.includes("paint"),l=()=>{o===null||w&&t===null||p(Math.max(o,t??0,f))};if(w)try{new r.PerformanceObserver(x=>{for(let v of x.getEntries())v.name==="first-contentful-paint"&&(t=v.startTime);l()}).observe({type:"paint",buffered:!0}),m.includes("largest-contentful-paint")&&new r.PerformanceObserver(x=>{for(let v of x.getEntries())f=Math.max(f,v.startTime)}).observe({type:"largest-contentful-paint",buffered:!0})}catch{t=0}let u=()=>{o=r.performance.now(),l()};r.document.readyState==="complete"?u():r.addEventListener("load",u,{once:!0})}),pe)}function Me(r,p,{win:o=window,rootMargin:t=Mt,settleMs:f=1500}={}){let m=null,w=!1,l=()=>{w||(w=!0,m?.disconnect(),r.removeEventListener("focusin",l),r.removeEventListener("pointerdown",l),p())};r.addEventListener("focusin",l),r.addEventListener("pointerdown",l);let u=Nt(o),x=async()=>{let v=await u;o.setTimeout(l,Math.max(0,v+f-o.performance.now()))};return typeof o.IntersectionObserver=="function"?(m=new o.IntersectionObserver(v=>{v.some(k=>k.isIntersecting)&&(m.disconnect(),x())},{rootMargin:t}),m.observe(r)):x(),{run:l}}var Pt="https://challenges.cloudflare.com/turnstile/v0/api.js";var pt=15e3;function dt(r=document){ct(Pt,{doc:r})}var ht={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var Rt={fr:{assistant:"Assistant virtuel",openLabel:"Assistant virtuel, ouvrir le clavardage",dismissTeaser:"Masquer ce message",close:"Fermer le clavardage",panelTitle:"Clavardage",automated:"R\xE9ponses automatiques, tir\xE9es du contenu de ce site.",privacy:"Confidentialit\xE9",you:"Vous",messageLabel:"Votre message",send:"Envoyer",live:"Vous r\xE9pond en direct",relay:"{name} a pris le relais",relayGeneric:"Une personne de l\u2019\xE9quipe a pris le relais",handback:"L\u2019assistant virtuel a repris la conversation",oneMoment:"Un instant\u2026",reconnecting:"Connexion perdue, reconnexion\u2026",notSent:"Non envoy\xE9.",retry:"R\xE9essayer",ended:"Cette conversation est termin\xE9e.",newConversation:"Nouvelle conversation",reload:"Rechargez la page pour continuer.",unavailable:"Un probl\xE8me est survenu. R\xE9essayez dans un moment.",turnstile:"L\u2019envoi n\u2019a pas abouti. R\xE9essayez.",tooMany:"Un instant, puis r\xE9essayez.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"Laissez vos coordonn\xE9es et nous vous recontacterons.",fallbackDown:"Laissez vos coordonn\xE9es et nous vous recontacterons.",name:"Votre nom",contact:"T\xE9l\xE9phone ou courriel",need:"Votre demande",fallbackSend:"Envoyer la demande",fallbackSent:"Merci, votre demande a \xE9t\xE9 transmise.",fallbackMissing:"Remplissez les trois champs.",said:"{name}\xA0: {text}",attachPhoto:"Joindre une photo",photoHint:"\xC9vitez les photos de personnes ou de pi\xE8ces d\u2019identit\xE9.",photoAlt:"Votre photo",photoSent:"Photo envoy\xE9e",photoLimit:"Trois photos au plus par conversation.",photoTooLarge:"Cette photo est trop lourde.",photoUnreadable:"Ce fichier n\u2019est pas une photo lisible.",photoFailed:"La photo n\u2019a pas pu \xEAtre envoy\xE9e. R\xE9essayez.",demoAsk:"Pouvez-vous m\u2019en dire plus sur {topic}?",demoQuestion:"Quels sont vos d\xE9lais?",demoAnswer:"(D\xE9monstration) Ici, l\u2019assistant r\xE9pond \xE0 partir du contenu de votre site.",demoHint:"Aper\xE7u\xA0: la conversation est d\xE9sactiv\xE9e."},en:{assistant:"Virtual assistant",openLabel:"Virtual assistant, open the chat",dismissTeaser:"Hide this message",close:"Close the chat",panelTitle:"Chat",automated:"Automatic answers, drawn from this site\u2019s content.",privacy:"Privacy",you:"You",messageLabel:"Your message",send:"Send",live:"Replying live",relay:"{name} took over",relayGeneric:"A team member took over",handback:"The virtual assistant is back in the conversation",oneMoment:"One moment\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"Reload the page to continue.",unavailable:"Something went wrong. Try again in a moment.",turnstile:"Sending did not go through. Try again.",tooMany:"One moment, then try again.",fallbackTitle:"Leave us your details",fallbackCap:"Leave your details and we will get back to you.",fallbackDown:"Leave your details and we will get back to you.",name:"Your name",contact:"Phone or email",need:"Your request",fallbackSend:"Send the request",fallbackSent:"Thank you, your request was sent.",fallbackMissing:"Fill in all three fields.",said:"{name}: {text}",attachPhoto:"Attach a photo",photoHint:"Avoid photos of people or of identity documents.",photoAlt:"Your photo",photoSent:"Photo sent",photoLimit:"Three photos at most per conversation.",photoTooLarge:"This photo is too large.",photoUnreadable:"This file is not a readable photo.",photoFailed:"The photo could not be sent. Try again.",demoAsk:"Can you tell me more about {topic}?",demoQuestion:"What are your lead times?",demoAnswer:"(Demo) Here, the assistant answers from your site\u2019s content.",demoHint:"Preview: the conversation is turned off."}},de=r=>String(r??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function Ne(r,p={}){let o=Rt[de(r)],t={...o};for(let[f,m]of Object.entries(p??{}))typeof m=="string"&&f in o&&(t[f]=m);return t}var Y=(r,p)=>r.replace(/\{(\w+)\}/g,(o,t)=>t in p?String(p[t]):o);var he="pmw-chat",ut=[1e3,2e3,4e3,8e3,16e3,3e4],ft=3,qt="chat-lead",It=3,zt=1600,Ht=.85,Dt=3e3,Ut=7200*1e3,Bt=8e3,Ft=40,$t=3e3,wt=8e3,Jt='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="3"/><circle cx="9" cy="10" r="1.6"/><path d="m21 16-5-5-8 8"/></svg>';async function jt(r,p){let o=await r.createImageBitmap(p,{imageOrientation:"from-image"}),t=Math.min(1,zt/Math.max(o.width,o.height)),f=Math.max(1,Math.round(o.width*t)),m=Math.max(1,Math.round(o.height*t)),w=r.document.createElement("canvas");return w.width=f,w.height=m,w.getContext("2d").drawImage(o,0,0,f,m),o.close?.(),new Promise((l,u)=>w.toBlob(x=>x?l(x):u(new Error("encode")),"image/jpeg",Ht))}var Kt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},Gt={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function Vt(r,p){let o=de(p),t=Ne(o),f=String(r??"").trim().match(Gt[o]);if(!f)return t.demoQuestion;let m=f[1].replace(/[\p{L}’']+/gu,w=>Kt[o][w.toLowerCase()]??w);return Y(t.demoAsk,{topic:m})}var Yt='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function i(r,p,o={},t){let f=r.createElement(p);for(let[m,w]of Object.entries(o))w!=null&&w!==!1&&f.setAttribute(m,w===!0?"":w);return t!==void 0&&(f.textContent=t),f}var Xt=/^(?:300\d{3}|600\d{3}|110600|110620)$/,Wt=r=>Xt.test(String(r??"")),_t=200,Qt=(r,p)=>{let o=!1,t=l=>{let u=r.turnstile;if(!(l.el.hasAttribute("data-pmw-rendered")||!l.shown())){l.el.setAttribute("data-pmw-rendered","");try{l.id=u.render(l.el,l.params)}catch{}}},f=()=>{if(!r.turnstile?.render)return!1;for(let l of p)t(l);return!0},m=()=>{if(dt(r.document),o||f())return;o=!0;let l=0,u=()=>{!f()&&++l<600&&r.setTimeout(u,100)};r.setTimeout(u,100)},w=l=>p.find(u=>u.form===l);for(let l of p)l.params["error-callback"]=u=>(l.ask(!1),Wt(u)&&!l.retried&&l.id!==void 0?(l.retried=!0,r.setTimeout(()=>{try{r.turnstile?.reset(l.id)}catch{l.failed=!0}},0)):l.failed=!0,!0);return{load:m,async token(l){m();let u=w(l),x=0;for(;;){if(u?.failed)return null;let v=l.querySelector('input[name="cf-turnstile-response"]')?.value??"";if(v)return v;if(!u?.asking()){if(x>=pt)return"";x+=_t}await new Promise(k=>r.setTimeout(k,_t))}},reset(l){let u=w(l);!u||u.id===void 0||u.asking()||(u.failed=!1,u.retried=!1,r.turnstile?.reset(u.id))}}};function da(r,p={}){if(r.pmwChat)return r.pmwChat;let o=p.win??window,t=o.document,f=p.WebSocket??o.WebSocket,m=p.fetch??((...e)=>o.fetch(...e)),w=[],l=p.turnstile??null,u=p.uuid??(()=>o.crypto.randomUUID()),x=p.shrink??(e=>jt(o,e)),v=p.preview??(e=>new Promise((a,n)=>{let c=new o.FileReader;c.onload=()=>a(String(c.result)),c.onerror=()=>n(c.error),c.readAsDataURL(e)})),k={};try{k=JSON.parse(r.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let X=de(k.lang),s=Ne(X,k.strings),Pe=r.dataset.pmwChatOpener??"",Re=r.querySelector(".pmw-chat__bubble"),g=p.demo??(k.demo===!0||!lt({doc:t,hostname:o.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(i(t,"style",{id:"pmw-chat-style"},it));let S=i(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0});S.inert=!0;let qe=i(t,"div",{class:"pmw-chat__head"}),me=r.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??i(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of me.querySelectorAll("[id]")){let a=e.id;e.id=`${a}-head`;for(let n of me.querySelectorAll(`[mask="url(#${a})"]`))n.setAttribute("mask",`url(#${a}-head)`)}let Ie=i(t,"div",{class:"pmw-chat__who"}),ze=i(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),W=i(t,"p",{class:"pmw-chat__role"}),He=i(t,"span",{class:"pmw-chat__face"}),De=i(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),Q=i(t,"button",{type:"button",class:"pmw-chat__close","aria-label":s.close});Q.innerHTML=Yt;let Z=i(t,"p",{class:"pmw-chat__notice"},s.automated);k.privacyHref&&(Z.append(" "),Z.append(i(t,"a",{href:k.privacyHref},s.privacy))),He.append(me,De),Ie.append(ze,W,Z),qe.append(He,Ie,Q);let T=i(t,"ol",{class:"pmw-chat__log"}),gt=i(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},Pe),A=i(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});A.append(i(t,"span"),i(t,"span"),i(t,"span")),T.append(gt);let ue=i(t,"p",{class:"pmw-chat__state",role:"status"}),Ue=i(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Be=(e,a)=>{let n=i(t,"div",{class:"pmw-chat__check"}),c=i(t,"div",{class:"pmw-chat__check-in"});c.inert=!0;let d=le=>{n.toggleAttribute("data-ask",le),c.inert=!le,E()},_=i(t,"div",{class:"pmw-chat__turnstile"}),O={el:_,form:e,shown:a,ask:d,asking:()=>n.hasAttribute("data-ask"),params:{sitekey:k.siteKey??"",action:"chat",theme:"light",language:X,appearance:"interaction-only",size:"flexible","refresh-expired":"auto",callback:()=>{O.retried=!1,O.failed=!1,d(!1)},"before-interactive-callback":()=>d(!0)}};return w.push(O),c.append(_),n.append(c),n},y=i(t,"form",{class:"pmw-chat__composer",novalidate:!0}),L=i(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"1",maxlength:String(2e3),placeholder:s.messageLabel,autocomplete:"off",disabled:g,"aria-describedby":g?"pmw-chat-demo-hint":null}),bt=i(t,"button",{type:"submit",class:"pmw-chat__send",disabled:g},s.send),fe=i(t,"div",{class:"pmw-chat__box"});fe.append(i(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},s.messageLabel),L,bt),g||y.append(Be(y,()=>!0)),y.append(fe),g&&y.append(i(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},s.demoHint));let F=null,z=i(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(k.photos===!0){let e=i(t,"label",{class:"pmw-chat__attach"});F=i(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:g}),e.innerHTML=Jt,e.prepend(F),e.append(i(t,"span",{class:"pmw-chat__sr"},s.attachPhoto)),fe.insertBefore(e,L),y.append(i(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},s.photoHint),z)}let ee=i(t,"div",{class:"pmw-chat__ended",hidden:!0}),Fe=i(t,"p",{},s.ended),te=i(t,"button",{type:"button",class:"pmw-chat__restart"},s.newConversation);ee.append(Fe,te);let b=i(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),$e=i(t,"p",{class:"pmw-chat__fb-intro"}),we=(e,a,n)=>{let c=`pmw-chat-fb-${e}`,d=i(t,"div",{class:"pmw-chat__field"});return d.append(i(t,"label",{for:c},s[e]),i(t,a,{id:c,name:e,required:!0,...n})),d},$=i(t,"p",{class:"pmw-chat__error",role:"alert"});b.append(i(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},s.fallbackTitle),$e,we("name","input",{autocomplete:"name",maxlength:"100"}),we("contact","input",{autocomplete:"on",maxlength:"200"}),we("need","textarea",{rows:"3",maxlength:"2000"}),$,i(t,"button",{type:"submit",class:"pmw-chat__send"},s.fallbackSend)),g||b.insertBefore(Be(b,()=>!b.hidden),$);let M=i(t,"div",{class:"pmw-chat__body"});M.append(T,ue,ee,b),S.append(qe,M,y,Ue),r.append(S),l??(l=Qt(o,w));let J=!0,Je=0,_e=0,xt=()=>M.scrollHeight-M.scrollTop-M.clientHeight;M.addEventListener("scroll",()=>{let e=M.scrollTop;xt()<=Ft?J=!0:e<Je&&(J=!1),Je=e},{passive:!0});let vt=e=>o.requestAnimationFrame?o.requestAnimationFrame(e):o.setTimeout(e,16);function E(e=!1,a=!1){e&&(J=!0),!(!J||_e)&&(_e=vt(()=>{if(_e=0,!J)return;let n=a||!!o.matchMedia?.("(prefers-reduced-motion: reduce)").matches;M.scrollTo({top:M.scrollHeight,behavior:n?"instant":"smooth"})}))}if(typeof o.ResizeObserver=="function"){let e=new o.ResizeObserver(()=>E());e.observe(T),e.observe(y)}let h=null;if(!g)try{h=JSON.parse(o.sessionStorage.getItem(he)??"null")}catch{h=null}if(h&&typeof h.at=="number"&&!(Date.now()-h.at<Ut)){h=null;try{o.sessionStorage.removeItem(he)}catch{}}let ge=()=>{try{o.sessionStorage.setItem(he,JSON.stringify(h))}catch{}},yt=()=>{h&&(h.at=Date.now(),ge())},C=null,be=0,xe=null,j=null,ae=0,K="bot",R=null,H=!1,je=0,ve=null,ye=new Set,q=new Map,ne=new Set,ke=0,Ke=()=>new URLSearchParams(o.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Ge=g?Ke():null,Se=()=>g?Ge:h?.botName||null;function G(){let e=K==="human";ze.textContent=e?R||s.assistant:Se()||s.assistant,W.textContent=e?s.live:Se()?s.assistant:"",W.hidden=W.textContent==="",Z.hidden=e,De.hidden=!e,S.dataset.mode=K}G();let N=e=>{ue.textContent=e??"",ue.hidden=!e};N("");let Ve=e=>e.role==="visitor"?s.you:e.role==="owner"?e.by||R||s.assistant:Se()||s.assistant;function D(e,a){let n=i(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${a}`,"data-id":e}),c=i(t,"span",{class:"pmw-chat__sr"}),d=i(t,"span",{class:"pmw-chat__text"});return n.append(c,d),T.insertBefore(n,A.parentNode===T?A:null),n}let Ye=()=>A.parentNode===T?A.previousElementSibling:T.lastElementChild,P=(e,a)=>e.classList.contains(a);function Xe(e,a,n,c){let d=i(t,"li",{class:e,role:"separator","aria-label":n}),_=i(t,"span",{class:"pmw-chat__relay-label","aria-hidden":"true"});_.append(i(t,"span",{class:a}),n),d.append(_),T.insertBefore(d,c??(A.parentNode===T?A:null)),E()}function We(e,a){for(let n=e?e.previousElementSibling:Ye();n;n=n.previousElementSibling){if(P(n,"pmw-chat__relay")||P(n,"pmw-chat__msg--owner"))return;if(P(n,"pmw-chat__msg--bot")||P(n,"pmw-chat__handback"))break}Xe("pmw-chat__relay","pmw-chat__relay-dot",a?Y(s.relay,{name:a}):s.relayGeneric,e)}function Te(e){for(let a=e?e.previousElementSibling:Ye();;a=a.previousElementSibling){if(!a||P(a,"pmw-chat__handback")||P(a,"pmw-chat__msg--bot"))return;if(P(a,"pmw-chat__relay")||P(a,"pmw-chat__msg--owner"))break}Xe("pmw-chat__handback","pmw-chat__relay-dot pmw-chat__relay-dot--back",s.handback,e)}let Qe=e=>[...T.querySelectorAll("li[data-id]")].find(a=>a.dataset.id===e),I=(e,a,n)=>{e.firstChild.textContent=Y(s.said,{name:Ve(a),text:""}),e.lastChild.textContent=n===""&&a.photos?.length&&!e.querySelector("img")?s.photoSent:n};function U(){A.hidden=!0,o.clearTimeout(ve)}function Le(e){A.hidden=!1,T.append(A),o.clearTimeout(ve),e&&(ve=o.setTimeout(U,e)),E()}function Ze(e,{announce:a}){if(ye.has(e.seq))return;ye.add(e.seq),ae=Math.max(ae,e.seq);let n=Qe(e.id);n||(n=D(e.id,e.role),e.role==="owner"?We(n,e.by||R):e.role==="bot"&&Te(n)),n.classList.remove("is-streaming","is-pending","is-failed"),n.querySelector(".pmw-chat__retry")?.remove(),I(n,e,e.text),e.role==="visitor"?(q.delete(e.id),e.photos?.length&&ne.add(e.id)):(U(),a&&(Ue.textContent=Y(s.said,{name:Ve(e),text:e.text})),S.hidden&&r.setAttribute("data-unread","")),E()}function kt(e,a){U();let n=Qe(e);n||(n=D(e,"bot"),Te(n),n.classList.add("is-streaming"),I(n,{role:"bot"},"")),n.lastChild.textContent+=a,E()}if(g){I(D("demo-question","visitor"),{role:"visitor"},Vt(Pe,X));let e=D("demo-answer","bot");I(e,{role:"bot"},s.demoAnswer),o.addEventListener("hashchange",()=>{Ge=Ke(),G(),I(e,{role:"bot"},s.demoAnswer)})}function oe(e){if(e.failed=!0,e.li.classList.remove("is-pending"),e.li.classList.add("is-failed"),e.li.querySelector(".pmw-chat__retry"))return;let a=i(t,"span",{class:"pmw-chat__retry"},`${s.notSent} `),n=i(t,"button",{type:"button"},s.retry);n.addEventListener("click",()=>{e.failed=!1,e.tries=0,a.remove(),e.li.classList.replace("is-failed","is-pending"),h?re()?Ee(e):B():Ae()}),a.append(n),e.li.append(a)}let re=()=>!!C&&C.readyState===1;function Ee(e){if(!(e.failed||!re())){if(e.tries>=ft)return oe(e);e.tries++,C.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function B(){if(!h||H||C)return;o.clearTimeout(xe);let e=o.location.protocol==="https:"?"wss:":"ws:",a=new URLSearchParams({c:h.c,k:h.k}),n=new f(`${e}//${o.location.host}/api/chat/ws?${a}`);C=n,n.onopen=()=>{be=0,ce&&(V(),Le(wt)),N(""),n.send(JSON.stringify({type:"hello",since:ae}));for(let c of q.values())Ee(c)},n.onmessage=c=>{let d;try{d=JSON.parse(c.data)}catch{return}Tt(d)},n.onclose=c=>{C===n&&(C=null);for(let _ of q.values())_.tries>=ft&&oe(_);if(c.code>=4e3&&c.code<5e3)return St(c.code);if(H)return;V(),N(s.reconnecting);let d=ut[Math.min(be,ut.length-1)];be++,xe=o.setTimeout(B,d)}}function St(e){if(e===ht.rpc)return ie(s.reload,!1);ie(s.ended,!0)}function ie(e,a){H=!0,o.clearTimeout(xe),N(""),Fe.textContent=e,te.hidden=!a,ee.hidden=!1,y.hidden=!0;for(let n of q.values())oe(n);E()}function et(e){K=e.mode==="human"?"human":"bot",R=typeof e.ownerName=="string"?e.ownerName:null,G(),K==="human"?We(null,R):Te(null),e.closed&&ie(s.ended,!0)}function Tt(e){switch(e?.type){case"history":typeof e.ownerName=="string"&&(R=e.ownerName);for(let a of e.messages??[])Ze(a,{announce:!1});return et(e);case"message":return yt(),Ze(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return kt(e.id,e.text);case"typing":return e.role==="visitor"?void 0:Le(wt);case"status":return et(e);case"fallback":return nt(e.reason==="cap"?s.fallbackCap:s.fallbackDown);case"lead":return at();case"error":(e.code==="closed"||e.code==="deleted")&&ie(s.ended,!0)}}let tt=!1;function at(){if(!(tt||h?.lead)){tt=!0,h&&(h.lead=!0,ge());try{o.pmw?.event(qt)}catch{}}}let se=new Set;async function Ce(e,a){let n="";if(se.has(e)){se.delete(e);try{l.reset(e)}catch{}}try{n=await l.token(e)}catch{n=""}if(n===null)return se.add(e),{ok:!1,error:s.turnstile,cap:!1,turnstile:!0};let c,d=null;try{c=await m("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:n,lang:X,page:o.location.pathname,...a})}),d=await c.json()}catch{c=null}return se.add(e),c?.ok&&d?{ok:!0,data:d}:{ok:!1,error:c?.status===403?s.turnstile:c?.status===429?s.tooMany:null,cap:c?.status===409&&d?.error==="cap",turnstile:c?.status===403}}let ce=null,V=()=>{o.clearTimeout(ce),ce=null};function Ae(){return j||h||(N(""),Le(),ce=o.setTimeout(()=>N(s.oneMoment),Bt),j=Ce(y,{}).then(e=>{if(j=null,e.ok&&e.data.conversationId&&e.data.visitorKey){h={c:e.data.conversationId,k:e.data.visitorKey,botName:typeof e.data.botName=="string"?e.data.botName:null,at:Date.now()},ge(),G(),B();return}V(),U();for(let a of q.values())oe(a);if(e.error)return N(e.error);N(""),nt(e.cap?s.fallbackCap:s.fallbackDown)})),j}function nt(e){U(),V(),N(""),$e.textContent=e,b.hidden=!1,y.hidden=!0,g||l.load(),b.querySelector("input")?.focus()}b.addEventListener("submit",async e=>{e.preventDefault();let a=O=>b.elements[O].value.trim(),n={name:a("name"),contact:a("contact"),need:a("need")};if(!n.name||!n.contact||!n.need){$.textContent=s.fallbackMissing;return}$.textContent="";let c=b.querySelector('button[type="submit"]');if(c.disabled)return;c.disabled=!0;let d={fallbackLead:n,...h?.c&&h?.k?{c:h.c,k:h.k}:{}},_=await Ce(b,d);if(!_.ok&&_.turnstile&&(_=await Ce(b,d)),c.disabled=!1,!_.ok){$.textContent=_.error??s.unavailable;return}b.replaceChildren(i(t,"p",{class:"pmw-chat__fb-done",role:"status"},s.fallbackSent)),at()});let Lt={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function Et(e){let a,n;try{a=await x(e),n=await v(a)}catch{z.textContent=s.photoUnreadable;return}let c=u(),d=D(c,"visitor");d.classList.add("is-pending");let _=i(t,"img",{class:"pmw-chat__photo",src:n,alt:s.photoAlt});_.addEventListener("load",()=>E(),{once:!0}),d.insertBefore(_,d.lastChild),I(d,{role:"visitor"},""),E(!0),h||await Ae();let O=null;if(h){let le=new URLSearchParams({c:h.c,k:h.k,id:c});try{O=await m(`/api/chat/photo?${le}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:a})}catch{O=null}}if(O?.ok){d.classList.remove("is-pending"),ne.add(c),C||B();return}d.remove(),z.textContent=s[Lt[O?.status]]??s.photoFailed}F?.addEventListener("change",async()=>{let e=[...F.files??[]];if(F.value="",z.textContent="",H||g||e.length===0)return;let a=It-ne.size-ke;e.length>a&&(z.textContent=s.photoLimit);let n=e.slice(0,Math.max(0,a));ke+=n.length;for(let c of n)try{await Et(c)}finally{ke--}});function ot(){let e=L.value.trim();if(!e||H||g)return;L.value="",rt();let a={id:u(),text:e.slice(0,2e3),tries:0,failed:!1};a.li=D(a.id,"visitor"),a.li.classList.add("is-pending"),I(a.li,{role:"visitor"},a.text),q.set(a.id,a),E(!0),h?re()?Ee(a):B():Ae()}y.addEventListener("submit",e=>{e.preventDefault(),ot()}),L.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),ot())});let rt=()=>{L.style.height="auto",L.style.height=L.value?`${Math.min(L.scrollHeight,132)}px`:""};L.addEventListener("input",()=>{rt();let e=Date.now();re()&&e-je>$t&&(je=e,C.send(JSON.stringify({type:"typing"})))}),te.addEventListener("click",()=>{h=null;try{o.sessionStorage.removeItem(he)}catch{}try{C?.close(1e3)}catch{}C=null,H=!1,ae=0,ye.clear(),q.clear(),ne.clear(),z.textContent="",K="bot",R=null,G();for(let e of[...T.querySelectorAll("li[data-id], .pmw-chat__relay, .pmw-chat__handback")])e.remove();U(),V(),ee.hidden=!0,y.hidden=!1,L.focus()});let Ct=()=>[...S.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));S.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),Oe();return}if(e.key!=="Tab")return;let a=Ct();if(a.length===0)return;let n=a[0],c=a[a.length-1];e.shiftKey&&t.activeElement===n?(e.preventDefault(),c.focus()):!e.shiftKey&&t.activeElement===c&&(e.preventDefault(),n.focus())}),Q.addEventListener("click",e=>Oe({keyboard:e.detail===0}));function At(){g||l.load(),S.hidden=!1,S.inert=!1,r.setAttribute("data-open",""),r.removeAttribute("data-unread"),h&&B(),(g?Q:y.hidden?b.hidden?te:b.querySelector("input"):L)?.focus(),E(!0,!0)}function Oe({keyboard:e=!0}={}){S.hidden=!0,S.inert=!0,r.removeAttribute("data-open"),e&&Re?.focus(),r.dispatchEvent(new o.FocusEvent("focusout",{relatedTarget:e?Re:null}))}return g||Me(r,()=>l.load(),{win:o,settleMs:Dt}),r.pmwChat={open:At,close:Oe,panel:S},r.pmwChat}export{ut as BACKOFF_MS,qt as GOAL,Ut as IDLE_MS,It as MAX_PHOTOS,ft as MAX_TRIES,zt as PHOTO_EDGE,Ht as PHOTO_QUALITY,Ft as PIN_SLACK,he as SESSION_KEY,Bt as SLOW_START_MS,Dt as WARM_MS,Vt as demoQuestion,da as mountChat,Wt as retryable};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/plugin-chat",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "PmeWeb chat plugin: the bubble, the lazy chat panel, the site Worker's /api/chat routes and the build-time context file. The conversations, the model and the alerts live in the pmeweb-chat service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/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
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
background: var(--pmw-chat-panel-bg, #ffffff); color: var(--pmw-chat-fg, #1f2328);
|
|
15
15
|
font-family: var(--pmw-chat-font, inherit); font-size: 16px; line-height: 1.45;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
/* Closed, the panel stays laid out, transparent and inert (widget.js « TURNSTILE »: the challenge
|
|
18
|
+
is rendered into a box with a real size, never a display:none one). */
|
|
19
|
+
.pmw-chat__panel[hidden] { display: flex !important; opacity: 0; pointer-events: none; }
|
|
20
|
+
.pmw-chat__panel [hidden] { display: none !important; }
|
|
18
21
|
@media (min-width: 640px) {
|
|
19
22
|
.pmw-chat__panel {
|
|
20
23
|
inset: auto;
|
|
@@ -97,15 +100,17 @@
|
|
|
97
100
|
@media (prefers-reduced-motion: reduce) {
|
|
98
101
|
.pmw-chat__typing span { animation: none; opacity: 0.7; }
|
|
99
102
|
}
|
|
100
|
-
/* Where a person takes over: a line, the green dot and « {name} a pris le relais », centred.
|
|
101
|
-
|
|
103
|
+
/* Where a person takes over: a line, the green dot and « {name} a pris le relais », centred. Where
|
|
104
|
+
they hand back: the same line, the dot in the muted colour (nobody is live any more). */
|
|
105
|
+
.pmw-chat__log > .pmw-chat__relay, .pmw-chat__log > .pmw-chat__handback {
|
|
102
106
|
align-self: stretch; margin: 18px 0 8px; display: flex; align-items: center; gap: 10px;
|
|
103
107
|
font-size: 13px; font-weight: 500; line-height: 1.4; color: var(--pmw-chat-muted, #4b5563);
|
|
104
108
|
}
|
|
105
|
-
.pmw-chat__relay::before, .pmw-chat__relay::after { content: ''; flex: 1; min-width: 16px; height: 1px; background: var(--pmw-chat-rule, #cfd5e1); }
|
|
109
|
+
.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
110
|
.pmw-chat__relay-label { display: flex; align-items: center; gap: 6px; text-align: center; }
|
|
107
111
|
.pmw-chat__relay-dot { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--pmw-chat-live, #1a7f37); }
|
|
108
|
-
.pmw-
|
|
112
|
+
.pmw-chat__relay-dot--back { background: var(--pmw-chat-muted, #4b5563); }
|
|
113
|
+
.pmw-chat__log > .pmw-chat__relay + li, .pmw-chat__log > .pmw-chat__handback + li { margin-top: 0; }
|
|
109
114
|
.pmw-chat__state { margin: 8px 0 0; font-size: 14px; color: var(--pmw-chat-muted, #4b5563); }
|
|
110
115
|
.pmw-chat__state:empty { display: none; }
|
|
111
116
|
/* The end: a thin line, the sentence centred, an outlined pill. */
|
|
@@ -117,13 +122,16 @@
|
|
|
117
122
|
background: var(--pmw-chat-panel-bg, #ffffff); color: var(--pmw-chat-link, #0a1b3d); font-weight: 600;
|
|
118
123
|
}
|
|
119
124
|
|
|
120
|
-
/* The composer: one rounded white field holding the photo button, the text and the send pill.
|
|
125
|
+
/* The composer: one rounded white field holding the photo button, the text and the send pill.
|
|
126
|
+
16 px on each side, so the field's focus ring (3 px, 2 px out) keeps 11 px off the panel's edge;
|
|
127
|
+
on a phone the bottom adds the home indicator's safe area. */
|
|
121
128
|
.pmw-chat__composer {
|
|
122
|
-
|
|
129
|
+
--pmw-chat-gap: 8px;
|
|
130
|
+
display: flex; flex-direction: column; gap: var(--pmw-chat-gap); padding: 14px 16px 16px;
|
|
123
131
|
border-top: 1px solid var(--pmw-chat-line, #e6e9f0); background: var(--pmw-chat-panel-bg, #ffffff);
|
|
124
132
|
}
|
|
125
133
|
@media (max-width: 639.98px) {
|
|
126
|
-
.pmw-chat__composer { padding:
|
|
134
|
+
.pmw-chat__composer { padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px)); }
|
|
127
135
|
}
|
|
128
136
|
.pmw-chat__box {
|
|
129
137
|
display: flex; align-items: flex-end; gap: 6px; padding: 4px; border-radius: 24px;
|
|
@@ -145,7 +153,23 @@
|
|
|
145
153
|
background: var(--pmw-chat-accent, #0a1b3d); color: var(--pmw-chat-accent-fg, #ffffff);
|
|
146
154
|
}
|
|
147
155
|
.pmw-chat__send:disabled, .pmw-chat__box textarea:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
148
|
-
|
|
156
|
+
/* Turnstile's row (widget.js « TURNSTILE »): collapsed, with no height and no gap, until Cloudflare
|
|
157
|
+
asks the visitor to act (its before-interactive callback sets data-ask); collapsed again on
|
|
158
|
+
success. Collapsed, its box keeps the row's full width and its natural height, out of the flow,
|
|
159
|
+
transparent and without pointer events (and inert), so the challenge never lays out in a 0 px box.
|
|
160
|
+
Full width, aligned with the field (the widget is size flexible, 300 px minimum: the composer's
|
|
161
|
+
inside is 358 px on a 390 px phone, 346 px in the 380 px desktop panel). In the composer it sits
|
|
162
|
+
ABOVE the field, which therefore never moves when it opens or closes. */
|
|
163
|
+
.pmw-chat__check {
|
|
164
|
+
position: relative; display: grid; grid-template-rows: 0fr; margin-bottom: calc(-1 * var(--pmw-chat-gap, 0px));
|
|
165
|
+
transition: grid-template-rows 0.25s ease, margin-bottom 0.25s ease;
|
|
166
|
+
}
|
|
167
|
+
.pmw-chat__check[data-ask] { grid-template-rows: 1fr; margin-bottom: 4px; }
|
|
168
|
+
.pmw-chat__check-in { min-height: 0; overflow: hidden; }
|
|
169
|
+
.pmw-chat__check:not([data-ask]) .pmw-chat__check-in { position: absolute; top: 0; left: 0; right: 0; overflow: visible; opacity: 0; pointer-events: none; }
|
|
170
|
+
@media (prefers-reduced-motion: reduce) {
|
|
171
|
+
.pmw-chat__check { transition: none; }
|
|
172
|
+
}
|
|
149
173
|
/* The photo picker: a real file input, visually hidden inside its label, so Tab reaches it and the
|
|
150
174
|
label shows its focus ring. 44 x 44 like every other control. */
|
|
151
175
|
.pmw-chat__attach {
|
|
@@ -160,7 +184,7 @@
|
|
|
160
184
|
.pmw-chat__photo { display: block; max-width: 100%; max-height: 220px; border-radius: 14px; }
|
|
161
185
|
|
|
162
186
|
/* 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:
|
|
187
|
+
.pmw-chat__fallback { --pmw-chat-gap: 14px; display: flex; flex-direction: column; gap: var(--pmw-chat-gap); margin-top: 16px; }
|
|
164
188
|
.pmw-chat__fb-title { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; }
|
|
165
189
|
.pmw-chat__fb-intro { margin: -8px 0 0; font-size: 15px; color: var(--pmw-chat-muted, #4b5563); }
|
|
166
190
|
.pmw-chat__field { display: flex; flex-direction: column; gap: 4px; }
|
|
@@ -179,3 +203,9 @@
|
|
|
179
203
|
content: ''; position: absolute; top: 2px; left: 38px; width: 12px; height: 12px; border-radius: 50%;
|
|
180
204
|
background: var(--pmw-chat-error, #b42318); box-shadow: 0 0 0 2px #ffffff;
|
|
181
205
|
}
|
|
206
|
+
/* The contact form's confirmation: a box of its own, never plain text under the thread
|
|
207
|
+
(operator, 2026-11-13). #14532d on #e9f6ee is 8.19:1 (a11y-tools/contrast.mjs), whatever the site's palette. */
|
|
208
|
+
.pmw-chat__fb-done {
|
|
209
|
+
margin: 12px 0 0; padding: 12px 14px; border-radius: 12px;
|
|
210
|
+
background: #e9f6ee; color: #14532d; border: 1px solid #b7dfc4; font-weight: 500;
|
|
211
|
+
}
|
package/src/widget.js
CHANGED
|
@@ -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, TOKEN_TIMEOUT_MS } from '@pmeweb/turnstile'
|
|
42
42
|
import { CLOSE, MAX_TEXT } from './protocol.js'
|
|
43
43
|
import { fill, langOf, stringsFor } from './strings.js'
|
|
44
44
|
|
|
@@ -49,6 +49,14 @@ 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 conversation with no message for this long is over: the next one starts fresh (and pmeweb-chat closes it). */
|
|
55
|
+
export const IDLE_MS = 2 * 60 * 60 * 1000
|
|
56
|
+
/** A first start slower than this shows « Un instant… » under the typing dots. */
|
|
57
|
+
export const SLOW_START_MS = 8000
|
|
58
|
+
/** How close to the bottom of the thread (px) still counts as « at the bottom ». */
|
|
59
|
+
export const PIN_SLACK = 40
|
|
52
60
|
const TYPING_SEND_MS = 3000
|
|
53
61
|
const TYPING_SHOW_MS = 8000
|
|
54
62
|
|
|
@@ -90,14 +98,110 @@ function el(doc, tag, attrs = {}, text) {
|
|
|
90
98
|
return e
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
// TURNSTILE (operator, 2026-11-13). Loaded only when the panel opened, the challenge had no time to
|
|
102
|
+
// observe the visitor and often showed its « Vérifiez que vous êtes humain » box. So it now starts
|
|
103
|
+
// early, but never in a test run: the trigger imports this file on the visitor's first sign of life
|
|
104
|
+
// on the page (a scroll, a pointer moving, a press, a key), and mountChat then starts api.js through
|
|
105
|
+
// @pmeweb/loader onApproach on the chat root (fixed on screen): once the page has painted (`load`,
|
|
106
|
+
// first contentful paint, LCP, and this file's own import, whichever is latest) plus WARM_MS, or at
|
|
107
|
+
// once on a press or a focus in the chat, or when the panel opens. Lighthouse never interacts, so it
|
|
108
|
+
// never imports this file and never fetches api.js; a visitor who reads for a few seconds before
|
|
109
|
+
// opening the panel usually finds the token minted.
|
|
110
|
+
//
|
|
111
|
+
// EXPLICIT RENDERING, ONE RENDER PER BOX (1.3.2, after a 600010 on pmeweb.ca). api.js is the page's
|
|
112
|
+
// one copy, the same URL @pmeweb/turnstile adds for a contact form, and it renders IMPLICITLY every
|
|
113
|
+
// `.cf-turnstile` it finds: that is the form's widget, and the chat never renders, resets or alters
|
|
114
|
+
// a widget it does not own. Ours carry no `cf-turnstile` class, so implicit rendering cannot reach
|
|
115
|
+
// them; the chat renders each box itself, with its options and callbacks as an object (no global
|
|
116
|
+
// callback names), once: a `data-pmw-rendered` mark set before the call is the guard, never the
|
|
117
|
+
// DOM Cloudflare inserts. `turnstile.ready()` is not used: with api.js added async it throws
|
|
118
|
+
// (« Remove async/defer ... », measured 2026-11-13); `turnstile.render` existing is the signal, and
|
|
119
|
+
// api.js has finished its implicit pass before `window.turnstile` is visible (measured, Chromium
|
|
120
|
+
// and WebKit).
|
|
121
|
+
//
|
|
122
|
+
// A BOX WITH REAL DIMENSIONS. 1.3.1 rendered into a row collapsed to 0 px, in a panel and a form
|
|
123
|
+
// that were display:none (measured: the render saw a 0 px box). Now the closed panel stays laid out
|
|
124
|
+
// (transparent, inert, no pointer events), the collapsed row keeps its box at full width and natural
|
|
125
|
+
// height but transparent and out of the flow, and the contact form's box is rendered only once that
|
|
126
|
+
// form is shown. The row opens only when Cloudflare itself asks for the visitor
|
|
127
|
+
// (`before-interactive-callback`) and collapses again on success, so « Succès ! » goes away. The
|
|
128
|
+
// rule « never hide the box with a guessed rule » holds: the row follows Cloudflare's callbacks,
|
|
129
|
+
// never the iframe's size.
|
|
130
|
+
//
|
|
131
|
+
// ERRORS. A retryable code (300*, 600*, 110600, 110620: Cloudflare's docs say reset and retry) resets
|
|
132
|
+
// the widget ONCE, silently. A second failure keeps the row collapsed and marks the box failed: the
|
|
133
|
+
// next post does not wait for a token, it shows the neutral « L'envoi n'a pas abouti » line, and the
|
|
134
|
+
// post after that starts a fresh challenge. Any other code is a failure at once.
|
|
135
|
+
//
|
|
136
|
+
// THE TOKEN: Turnstile refreshes an unused one before its 300 s expiry (refresh-expired: auto). A
|
|
137
|
+
// used one is replaced just before the next post (« spent » below), never while the row is asking: a
|
|
138
|
+
// reset would wipe the box the visitor is solving. `reset(id)` names our widget, never every widget
|
|
139
|
+
// on the page (@pmeweb/turnstile rule 4).
|
|
140
|
+
const RETRYABLE = /^(?:300\d{3}|600\d{3}|110600|110620)$/
|
|
141
|
+
export const retryable = (code) => RETRYABLE.test(String(code ?? ''))
|
|
142
|
+
const POLL_MS = 200
|
|
143
|
+
|
|
144
|
+
const defaultTurnstile = (win, boxes) => {
|
|
145
|
+
let polling = false
|
|
146
|
+
const draw = (b) => {
|
|
147
|
+
const ts = win.turnstile
|
|
148
|
+
if (b.el.hasAttribute('data-pmw-rendered') || !b.shown()) return
|
|
149
|
+
b.el.setAttribute('data-pmw-rendered', '')
|
|
150
|
+
try { b.id = ts.render(b.el, b.params) } catch { /* the token call gives up after TOKEN_TIMEOUT_MS */ }
|
|
151
|
+
}
|
|
152
|
+
const render = () => {
|
|
153
|
+
if (!win.turnstile?.render) return false
|
|
154
|
+
for (const b of boxes) draw(b)
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
const load = () => {
|
|
158
|
+
loadTurnstile(win.document)
|
|
159
|
+
if (polling || render()) return
|
|
160
|
+
polling = true
|
|
161
|
+
let n = 0
|
|
162
|
+
const tick = () => { if (!render() && ++n < 600) win.setTimeout(tick, 100) }
|
|
163
|
+
win.setTimeout(tick, 100)
|
|
164
|
+
}
|
|
165
|
+
const of = (form) => boxes.find((b) => b.form === form)
|
|
166
|
+
for (const b of boxes) {
|
|
167
|
+
b.params['error-callback'] = (code) => {
|
|
168
|
+
b.ask(false)
|
|
169
|
+
if (retryable(code) && !b.retried && b.id !== undefined) {
|
|
170
|
+
b.retried = true
|
|
171
|
+
win.setTimeout(() => { try { win.turnstile?.reset(b.id) } catch { b.failed = true } }, 0)
|
|
172
|
+
} else {
|
|
173
|
+
b.failed = true
|
|
174
|
+
}
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
load,
|
|
180
|
+
/** The token, '' after TOKEN_TIMEOUT_MS with no challenge on screen, null when the box failed. */
|
|
181
|
+
async token(form) {
|
|
182
|
+
load()
|
|
183
|
+
const b = of(form)
|
|
184
|
+
let waited = 0
|
|
185
|
+
for (;;) {
|
|
186
|
+
if (b?.failed) return null
|
|
187
|
+
const v = form.querySelector('input[name="cf-turnstile-response"]')?.value ?? ''
|
|
188
|
+
if (v) return v
|
|
189
|
+
if (!b?.asking()) {
|
|
190
|
+
if (waited >= TOKEN_TIMEOUT_MS) return ''
|
|
191
|
+
waited += POLL_MS
|
|
192
|
+
}
|
|
193
|
+
await new Promise((r) => win.setTimeout(r, POLL_MS))
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
reset(form) {
|
|
197
|
+
const b = of(form)
|
|
198
|
+
if (!b || b.id === undefined || b.asking()) return
|
|
199
|
+
b.failed = false
|
|
200
|
+
b.retried = false
|
|
201
|
+
win.turnstile?.reset(b.id)
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
}
|
|
101
205
|
|
|
102
206
|
export function mountChat(root, deps = {}) {
|
|
103
207
|
if (root.pmwChat) return root.pmwChat
|
|
@@ -105,7 +209,8 @@ export function mountChat(root, deps = {}) {
|
|
|
105
209
|
const doc = win.document
|
|
106
210
|
const WS = deps.WebSocket ?? win.WebSocket
|
|
107
211
|
const fetchImpl = deps.fetch ?? ((...a) => win.fetch(...a))
|
|
108
|
-
const
|
|
212
|
+
const boxes = []
|
|
213
|
+
let turnstile = deps.turnstile ?? null
|
|
109
214
|
const uuid = deps.uuid ?? (() => win.crypto.randomUUID())
|
|
110
215
|
const shrink = deps.shrink ?? ((file) => shrinkPhoto(win, file))
|
|
111
216
|
// A data: URL, not a blob: one: site CSPs allow `img-src data:` (boilerplate worker), not blob:.
|
|
@@ -128,6 +233,9 @@ export function mountChat(root, deps = {}) {
|
|
|
128
233
|
|
|
129
234
|
// ---- the panel --------------------------------------------------------------------------
|
|
130
235
|
const panel = el(doc, 'div', { class: 'pmw-chat__panel', role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'pmw-chat-name', hidden: true })
|
|
236
|
+
// Closed, the panel stays laid out (transparent, no pointer events) so its Turnstile box has a real
|
|
237
|
+
// size (« TURNSTILE » above); inert keeps it out of the tab order and the accessibility tree.
|
|
238
|
+
panel.inert = true
|
|
131
239
|
const head = el(doc, 'div', { class: 'pmw-chat__head' })
|
|
132
240
|
const avatar = root.querySelector('.pmw-chat__avatar')?.cloneNode(true) ?? el(doc, 'span', { class: 'pmw-chat__avatar', 'aria-hidden': 'true' })
|
|
133
241
|
// An icon may hold a <mask id>: the clone gets its own ids, since the bubble (and the original
|
|
@@ -164,19 +272,38 @@ export function mountChat(root, deps = {}) {
|
|
|
164
272
|
const state = el(doc, 'p', { class: 'pmw-chat__state', role: 'status' })
|
|
165
273
|
const announcer = el(doc, 'p', { class: 'pmw-chat__sr', 'aria-live': 'polite' })
|
|
166
274
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
275
|
+
// A widget in its row (see « TURNSTILE » above): the box the chat renders explicitly, once.
|
|
276
|
+
const widget = (form, shown) => {
|
|
277
|
+
const row = el(doc, 'div', { class: 'pmw-chat__check' })
|
|
278
|
+
const inner = el(doc, 'div', { class: 'pmw-chat__check-in' })
|
|
279
|
+
inner.inert = true
|
|
280
|
+
const ask = (on) => { row.toggleAttribute('data-ask', on); inner.inert = !on; follow() }
|
|
281
|
+
const w = el(doc, 'div', { class: 'pmw-chat__turnstile' })
|
|
282
|
+
const box = {
|
|
283
|
+
el: w, form, shown, ask, asking: () => row.hasAttribute('data-ask'),
|
|
284
|
+
params: {
|
|
285
|
+
sitekey: cfg.siteKey ?? '', action: 'chat', theme: 'light', language: lang,
|
|
286
|
+
appearance: 'interaction-only', size: 'flexible', 'refresh-expired': 'auto',
|
|
287
|
+
callback: () => { box.retried = false; box.failed = false; ask(false) },
|
|
288
|
+
'before-interactive-callback': () => ask(true),
|
|
289
|
+
},
|
|
290
|
+
}
|
|
291
|
+
boxes.push(box)
|
|
292
|
+
inner.append(w)
|
|
293
|
+
row.append(inner)
|
|
294
|
+
return row
|
|
295
|
+
}
|
|
171
296
|
const composer = el(doc, 'form', { class: 'pmw-chat__composer', novalidate: true })
|
|
172
297
|
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
298
|
const send = el(doc, 'button', { type: 'submit', class: 'pmw-chat__send', disabled: demo }, s.send)
|
|
174
299
|
// One rounded field holds the photo button, the text and the send button; hints and errors go under it.
|
|
175
300
|
const box = el(doc, 'div', { class: 'pmw-chat__box' })
|
|
176
301
|
box.append(el(doc, 'label', { class: 'pmw-chat__sr', for: 'pmw-chat-input' }, s.messageLabel), input, send)
|
|
302
|
+
// The check row sits ABOVE the field: the composer is pinned to the panel's bottom, so the row
|
|
303
|
+
// opening or collapsing never moves the field the visitor is typing in.
|
|
304
|
+
if (!demo) composer.append(widget(composer, () => true))
|
|
177
305
|
composer.append(box)
|
|
178
306
|
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
307
|
let picker = null
|
|
181
308
|
const photoError = el(doc, 'p', { class: 'pmw-chat__error pmw-chat__photo-error', role: 'alert' })
|
|
182
309
|
if (cfg.photos === true) {
|
|
@@ -210,16 +337,61 @@ export function mountChat(root, deps = {}) {
|
|
|
210
337
|
field('need', 'textarea', { rows: '3', maxlength: '2000' }),
|
|
211
338
|
fbError, el(doc, 'button', { type: 'submit', class: 'pmw-chat__send' }, s.fallbackSend),
|
|
212
339
|
)
|
|
213
|
-
if (!demo) fallback.
|
|
340
|
+
if (!demo) fallback.insertBefore(widget(fallback, () => !fallback.hidden), fbError)
|
|
214
341
|
const body = el(doc, 'div', { class: 'pmw-chat__body' })
|
|
215
342
|
body.append(log, state, ended, fallback)
|
|
216
343
|
panel.append(head, body, composer, announcer)
|
|
217
344
|
root.append(panel)
|
|
345
|
+
turnstile ??= defaultTurnstile(win, boxes)
|
|
346
|
+
|
|
347
|
+
// ---- following the thread ---------------------------------------------------------------
|
|
348
|
+
// PINNED (operator, 2026-11-13): while the visitor is at the bottom (within PIN_SLACK), every
|
|
349
|
+
// change that adds height (a streamed fragment, the stored message replacing it, the typing dots,
|
|
350
|
+
// a photo once decoded, the takeover line, the end block, the composer growing or its check row
|
|
351
|
+
// opening) keeps the thread at the bottom. A visitor who scrolled UP to read stays where they are;
|
|
352
|
+
// reaching the bottom again pins it again. Sending a message is explicit: it always pins. The
|
|
353
|
+
// scroll is smooth, one per animation frame however many fragments land in it (a stream would
|
|
354
|
+
// otherwise stack dozens), and instant under prefers-reduced-motion and when the panel opens.
|
|
355
|
+
// Our own scrolls only ever move down, so a scroll that moves UP away from the bottom is the
|
|
356
|
+
// visitor's.
|
|
357
|
+
let pinned = true
|
|
358
|
+
let lastTop = 0
|
|
359
|
+
let followAt = 0
|
|
360
|
+
const gap = () => body.scrollHeight - body.scrollTop - body.clientHeight
|
|
361
|
+
body.addEventListener('scroll', () => {
|
|
362
|
+
const t = body.scrollTop
|
|
363
|
+
if (gap() <= PIN_SLACK) pinned = true
|
|
364
|
+
else if (t < lastTop) pinned = false
|
|
365
|
+
lastTop = t
|
|
366
|
+
}, { passive: true })
|
|
367
|
+
const nextFrame = (f) => (win.requestAnimationFrame ? win.requestAnimationFrame(f) : win.setTimeout(f, 16))
|
|
368
|
+
function follow(force = false, instant = false) {
|
|
369
|
+
if (force) pinned = true
|
|
370
|
+
if (!pinned || followAt) return
|
|
371
|
+
followAt = nextFrame(() => {
|
|
372
|
+
followAt = 0
|
|
373
|
+
if (!pinned) return
|
|
374
|
+
const still = instant || !!win.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
375
|
+
body.scrollTo({ top: body.scrollHeight, behavior: still ? 'instant' : 'smooth' })
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
if (typeof win.ResizeObserver === 'function') {
|
|
379
|
+
const ro = new win.ResizeObserver(() => follow())
|
|
380
|
+
ro.observe(log)
|
|
381
|
+
ro.observe(composer)
|
|
382
|
+
}
|
|
218
383
|
|
|
219
384
|
// ---- state ------------------------------------------------------------------------------
|
|
220
385
|
let session = null
|
|
221
386
|
if (!demo) try { session = JSON.parse(win.sessionStorage.getItem(SESSION_KEY) ?? 'null') } catch { session = null }
|
|
387
|
+
// TWO HOURS WITHOUT A MESSAGE END IT (operator, 2026-11-13): a tab left open overnight starts a
|
|
388
|
+
// new conversation; pmeweb-chat closes the old one on its side too. `at` is the last message seen.
|
|
389
|
+
if (session && typeof session.at === 'number' && !(Date.now() - session.at < IDLE_MS)) {
|
|
390
|
+
session = null
|
|
391
|
+
try { win.sessionStorage.removeItem(SESSION_KEY) } catch { /* this tab only */ }
|
|
392
|
+
}
|
|
222
393
|
const save = () => { try { win.sessionStorage.setItem(SESSION_KEY, JSON.stringify(session)) } catch { /* this tab only */ } }
|
|
394
|
+
const touch = () => { if (session) { session.at = Date.now(); save() } }
|
|
223
395
|
let ws = null
|
|
224
396
|
let attempt = 0
|
|
225
397
|
let retryTimer = null
|
|
@@ -269,18 +441,37 @@ export function mountChat(root, deps = {}) {
|
|
|
269
441
|
// scan back stops at a line already there or at an owner message, so one takeover gets one line
|
|
270
442
|
// however often the frames repeat it; the bot answering again and a person taking over again
|
|
271
443
|
// gets another.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
444
|
+
//
|
|
445
|
+
// THE HAND-BACK (operator, 2026-11-13) is its mirror: where the owner gives the conversation back
|
|
446
|
+
// (« Rendre à l’assistant »), one line « L’assistant virtuel a repris la conversation », always
|
|
447
|
+
// the « assistant virtuel » wording (never the bot's name alone: it must not pass for a person).
|
|
448
|
+
// It goes before the first bot message that follows a person (live, and on replay), or at the end
|
|
449
|
+
// when the mode turns back to bot before the bot speaks. Its scan back stops at a hand-back line
|
|
450
|
+
// already there or at a bot message (nothing to mark), and inserts only past an owner message or a
|
|
451
|
+
// takeover line: a conversation the bot never left gets none. A takeover after it gets its own line.
|
|
452
|
+
const lastLine = () => (typing.parentNode === log ? typing.previousElementSibling : log.lastElementChild)
|
|
453
|
+
const isA = (x, c) => x.classList.contains(c)
|
|
454
|
+
function separator(cls, dotCls, text, at) {
|
|
455
|
+
const sep = el(doc, 'li', { class: cls, role: 'separator', 'aria-label': text })
|
|
280
456
|
const label = el(doc, 'span', { class: 'pmw-chat__relay-label', 'aria-hidden': 'true' })
|
|
281
|
-
label.append(el(doc, 'span', { class:
|
|
457
|
+
label.append(el(doc, 'span', { class: dotCls }), text)
|
|
282
458
|
sep.append(label)
|
|
283
459
|
log.insertBefore(sep, at ?? (typing.parentNode === log ? typing : null))
|
|
460
|
+
follow()
|
|
461
|
+
}
|
|
462
|
+
function relay(at, by) {
|
|
463
|
+
for (let x = at ? at.previousElementSibling : lastLine(); x; x = x.previousElementSibling) {
|
|
464
|
+
if (isA(x, 'pmw-chat__relay') || isA(x, 'pmw-chat__msg--owner')) return
|
|
465
|
+
if (isA(x, 'pmw-chat__msg--bot') || isA(x, 'pmw-chat__handback')) break
|
|
466
|
+
}
|
|
467
|
+
separator('pmw-chat__relay', 'pmw-chat__relay-dot', by ? fill(s.relay, { name: by }) : s.relayGeneric, at)
|
|
468
|
+
}
|
|
469
|
+
function handback(at) {
|
|
470
|
+
for (let x = at ? at.previousElementSibling : lastLine(); ; x = x.previousElementSibling) {
|
|
471
|
+
if (!x || isA(x, 'pmw-chat__handback') || isA(x, 'pmw-chat__msg--bot')) return
|
|
472
|
+
if (isA(x, 'pmw-chat__relay') || isA(x, 'pmw-chat__msg--owner')) break
|
|
473
|
+
}
|
|
474
|
+
separator('pmw-chat__handback', 'pmw-chat__relay-dot pmw-chat__relay-dot--back', s.handback, at)
|
|
284
475
|
}
|
|
285
476
|
const find = (id) => [...log.querySelectorAll('li[data-id]')].find((li) => li.dataset.id === id)
|
|
286
477
|
const setText = (li, m, text) => {
|
|
@@ -292,6 +483,14 @@ export function mountChat(root, deps = {}) {
|
|
|
292
483
|
typing.hidden = true
|
|
293
484
|
win.clearTimeout(typingTimer)
|
|
294
485
|
}
|
|
486
|
+
/** The dots, until a reply hides them; `ms` also hides them after that long without one. */
|
|
487
|
+
function showTyping(ms) {
|
|
488
|
+
typing.hidden = false
|
|
489
|
+
log.append(typing)
|
|
490
|
+
win.clearTimeout(typingTimer)
|
|
491
|
+
if (ms) typingTimer = win.setTimeout(hideTyping, ms)
|
|
492
|
+
follow()
|
|
493
|
+
}
|
|
295
494
|
function render(m, { announce }) {
|
|
296
495
|
if (seen.has(m.seq)) return
|
|
297
496
|
seen.add(m.seq)
|
|
@@ -300,6 +499,7 @@ export function mountChat(root, deps = {}) {
|
|
|
300
499
|
if (!li) {
|
|
301
500
|
li = line(m.id, m.role)
|
|
302
501
|
if (m.role === 'owner') relay(li, m.by || ownerName)
|
|
502
|
+
else if (m.role === 'bot') handback(li)
|
|
303
503
|
}
|
|
304
504
|
li.classList.remove('is-streaming', 'is-pending', 'is-failed')
|
|
305
505
|
li.querySelector('.pmw-chat__retry')?.remove()
|
|
@@ -313,20 +513,20 @@ export function mountChat(root, deps = {}) {
|
|
|
313
513
|
if (announce) announcer.textContent = fill(s.said, { name: speaker(m), text: m.text })
|
|
314
514
|
if (panel.hidden) root.setAttribute('data-unread', '')
|
|
315
515
|
}
|
|
316
|
-
|
|
516
|
+
follow()
|
|
317
517
|
}
|
|
318
518
|
function delta(id, text) {
|
|
319
519
|
hideTyping()
|
|
320
520
|
let li = find(id)
|
|
321
521
|
if (!li) {
|
|
322
522
|
li = line(id, 'bot')
|
|
523
|
+
handback(li)
|
|
323
524
|
li.classList.add('is-streaming')
|
|
324
525
|
setText(li, { role: 'bot' }, '')
|
|
325
526
|
}
|
|
326
527
|
li.lastChild.textContent += text
|
|
327
|
-
|
|
528
|
+
follow()
|
|
328
529
|
}
|
|
329
|
-
const scroll = () => { log.scrollTop = log.scrollHeight }
|
|
330
530
|
if (demo) {
|
|
331
531
|
setText(line('demo-question', 'visitor'), { role: 'visitor' }, demoQuestion(opener, lang))
|
|
332
532
|
const answer = line('demo-answer', 'bot')
|
|
@@ -375,6 +575,8 @@ export function mountChat(root, deps = {}) {
|
|
|
375
575
|
ws = sock
|
|
376
576
|
sock.onopen = () => {
|
|
377
577
|
attempt = 0
|
|
578
|
+
// A first start: the dots stay until the reply, for TYPING_SHOW_MS at most, like the service's.
|
|
579
|
+
if (slowTimer) { notSlow(); showTyping(TYPING_SHOW_MS) }
|
|
378
580
|
showState('')
|
|
379
581
|
sock.send(JSON.stringify({ type: 'hello', since: lastSeq }))
|
|
380
582
|
for (const entry of pending.values()) transmit(entry)
|
|
@@ -389,6 +591,7 @@ export function mountChat(root, deps = {}) {
|
|
|
389
591
|
for (const entry of pending.values()) if (entry.tries >= MAX_TRIES) failed(entry)
|
|
390
592
|
if (e.code >= 4000 && e.code < 5000) return refused(e.code)
|
|
391
593
|
if (over) return
|
|
594
|
+
notSlow()
|
|
392
595
|
showState(s.reconnecting)
|
|
393
596
|
const wait = BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]
|
|
394
597
|
attempt++
|
|
@@ -408,12 +611,14 @@ export function mountChat(root, deps = {}) {
|
|
|
408
611
|
ended.hidden = false
|
|
409
612
|
composer.hidden = true
|
|
410
613
|
for (const entry of pending.values()) failed(entry)
|
|
614
|
+
follow()
|
|
411
615
|
}
|
|
412
616
|
function status(m) {
|
|
413
617
|
mode = m.mode === 'human' ? 'human' : 'bot'
|
|
414
618
|
ownerName = typeof m.ownerName === 'string' ? m.ownerName : null
|
|
415
619
|
header()
|
|
416
620
|
if (mode === 'human') relay(null, ownerName)
|
|
621
|
+
else handback(null)
|
|
417
622
|
if (m.closed) end(s.ended, true)
|
|
418
623
|
}
|
|
419
624
|
function frame(m) {
|
|
@@ -423,16 +628,13 @@ export function mountChat(root, deps = {}) {
|
|
|
423
628
|
for (const x of m.messages ?? []) render(x, { announce: false })
|
|
424
629
|
return status(m)
|
|
425
630
|
case 'message':
|
|
631
|
+
touch()
|
|
426
632
|
return render(m.msg, { announce: m.msg?.role !== 'visitor' })
|
|
427
633
|
case 'delta':
|
|
428
634
|
return delta(m.id, m.text)
|
|
429
635
|
case 'typing':
|
|
430
636
|
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()
|
|
637
|
+
return showTyping(TYPING_SHOW_MS)
|
|
436
638
|
case 'status':
|
|
437
639
|
return status(m)
|
|
438
640
|
case 'fallback':
|
|
@@ -453,9 +655,23 @@ export function mountChat(root, deps = {}) {
|
|
|
453
655
|
}
|
|
454
656
|
|
|
455
657
|
// ---- starting a conversation ------------------------------------------------------------
|
|
658
|
+
/** Forms whose Turnstile token was used: reset just before the next post, never right after. */
|
|
659
|
+
const spent = new Set()
|
|
456
660
|
async function post(form, extra) {
|
|
457
661
|
let token = ''
|
|
662
|
+
// A used token is replaced only when a NEW one is needed (a restart, the form, a retry): a reset
|
|
663
|
+
// right after a successful start made Cloudflare show its checkbox under a conversation that
|
|
664
|
+
// needs no check any more (messages go over the socket; operator, 2026-11-13).
|
|
665
|
+
if (spent.has(form)) {
|
|
666
|
+
spent.delete(form)
|
|
667
|
+
try { turnstile.reset(form) } catch { /* the token call below waits for a fresh one */ }
|
|
668
|
+
}
|
|
458
669
|
try { token = await turnstile.token(form) } catch { token = '' }
|
|
670
|
+
// The box failed twice (« TURNSTILE » above): nothing to post; the next post starts a fresh one.
|
|
671
|
+
if (token === null) {
|
|
672
|
+
spent.add(form)
|
|
673
|
+
return { ok: false, error: s.turnstile, cap: false, turnstile: true }
|
|
674
|
+
}
|
|
459
675
|
let res
|
|
460
676
|
let data = null
|
|
461
677
|
try {
|
|
@@ -468,23 +684,35 @@ export function mountChat(root, deps = {}) {
|
|
|
468
684
|
} catch {
|
|
469
685
|
res = null
|
|
470
686
|
}
|
|
471
|
-
|
|
687
|
+
spent.add(form)
|
|
472
688
|
if (res?.ok && data) return { ok: true, data }
|
|
473
689
|
const error = res?.status === 403 ? s.turnstile : res?.status === 429 ? s.tooMany : null
|
|
474
|
-
return { ok: false, error, cap: res?.status === 409 && data?.error === 'cap' }
|
|
690
|
+
return { ok: false, error, cap: res?.status === 409 && data?.error === 'cap', turnstile: res?.status === 403 }
|
|
475
691
|
}
|
|
692
|
+
// THE FIRST START (operator, 2026-11-13) reads like any reply: the typing dots at once, no
|
|
693
|
+
// « Connexion… ». Only a start slower than SLOW_START_MS adds « Un instant… »; the socket's open
|
|
694
|
+
// clears it, a drop says « Connexion perdue, reconnexion… » as before. Nothing is started before
|
|
695
|
+
// the visitor writes: pmeweb-chat counts every /start toward the visitor's conversations of the day
|
|
696
|
+
// (src/ipLimit.ts there), so a panel opened and never used must not spend one. Only the token is
|
|
697
|
+
// prepared ahead (« TURNSTILE » above).
|
|
698
|
+
let slowTimer = null
|
|
699
|
+
const notSlow = () => { win.clearTimeout(slowTimer); slowTimer = null }
|
|
476
700
|
function begin() {
|
|
477
701
|
if (starting || session) return starting
|
|
478
|
-
showState(
|
|
702
|
+
showState('')
|
|
703
|
+
showTyping()
|
|
704
|
+
slowTimer = win.setTimeout(() => showState(s.oneMoment), SLOW_START_MS)
|
|
479
705
|
starting = post(composer, {}).then((r) => {
|
|
480
706
|
starting = null
|
|
481
707
|
if (r.ok && r.data.conversationId && r.data.visitorKey) {
|
|
482
|
-
session = { c: r.data.conversationId, k: r.data.visitorKey, botName: typeof r.data.botName === 'string' ? r.data.botName : null }
|
|
708
|
+
session = { c: r.data.conversationId, k: r.data.visitorKey, botName: typeof r.data.botName === 'string' ? r.data.botName : null, at: Date.now() }
|
|
483
709
|
save()
|
|
484
710
|
header()
|
|
485
711
|
connect()
|
|
486
712
|
return
|
|
487
713
|
}
|
|
714
|
+
notSlow()
|
|
715
|
+
hideTyping()
|
|
488
716
|
for (const entry of pending.values()) failed(entry)
|
|
489
717
|
if (r.error) return showState(r.error)
|
|
490
718
|
showState('')
|
|
@@ -494,9 +722,13 @@ export function mountChat(root, deps = {}) {
|
|
|
494
722
|
return starting
|
|
495
723
|
}
|
|
496
724
|
function showFallback(text) {
|
|
725
|
+
// Nobody is writing any more: the dots and « Un instant… » go with the form (operator, 2026-11-13).
|
|
726
|
+
hideTyping(); notSlow(); showState('')
|
|
497
727
|
fbIntro.textContent = text
|
|
498
728
|
fallback.hidden = false
|
|
499
729
|
composer.hidden = true
|
|
730
|
+
// Its Turnstile box is rendered now that it has a size (« TURNSTILE » above).
|
|
731
|
+
if (!demo) turnstile.load()
|
|
500
732
|
fallback.querySelector('input')?.focus()
|
|
501
733
|
}
|
|
502
734
|
fallback.addEventListener('submit', async (e) => {
|
|
@@ -508,7 +740,17 @@ export function mountChat(root, deps = {}) {
|
|
|
508
740
|
return
|
|
509
741
|
}
|
|
510
742
|
fbError.textContent = ''
|
|
511
|
-
|
|
743
|
+
// One request per press: the button waits for the answer (a second press made a second lead).
|
|
744
|
+
const btn = fallback.querySelector('button[type="submit"]')
|
|
745
|
+
if (btn.disabled) return
|
|
746
|
+
btn.disabled = true
|
|
747
|
+
// The conversation the visitor had joins the request, so the link shows its thread (1.3.2).
|
|
748
|
+
const extra = { fallbackLead: fields, ...(session?.c && session?.k ? { c: session.c, k: session.k } : {}) }
|
|
749
|
+
let r = await post(fallback, extra)
|
|
750
|
+
// A refused check is retried ONCE with a fresh token before the visitor sees anything: the
|
|
751
|
+
// first token can be one Cloudflare already redeemed or let expire (operator, 2026-11-13).
|
|
752
|
+
if (!r.ok && r.turnstile) r = await post(fallback, extra)
|
|
753
|
+
btn.disabled = false
|
|
512
754
|
if (!r.ok) {
|
|
513
755
|
fbError.textContent = r.error ?? s.unavailable
|
|
514
756
|
return
|
|
@@ -532,9 +774,12 @@ export function mountChat(root, deps = {}) {
|
|
|
532
774
|
const id = uuid()
|
|
533
775
|
const li = line(id, 'visitor')
|
|
534
776
|
li.classList.add('is-pending')
|
|
535
|
-
|
|
777
|
+
const img = el(doc, 'img', { class: 'pmw-chat__photo', src, alt: s.photoAlt })
|
|
778
|
+
// Its height is known only once decoded: re-pin then (follow() is a no-op if they scrolled up).
|
|
779
|
+
img.addEventListener('load', () => follow(), { once: true })
|
|
780
|
+
li.insertBefore(img, li.lastChild)
|
|
536
781
|
setText(li, { role: 'visitor' }, '')
|
|
537
|
-
|
|
782
|
+
follow(true)
|
|
538
783
|
if (!session) await begin()
|
|
539
784
|
let res = null
|
|
540
785
|
if (session) {
|
|
@@ -580,7 +825,7 @@ export function mountChat(root, deps = {}) {
|
|
|
580
825
|
entry.li.classList.add('is-pending')
|
|
581
826
|
setText(entry.li, { role: 'visitor' }, entry.text)
|
|
582
827
|
pending.set(entry.id, entry)
|
|
583
|
-
|
|
828
|
+
follow(true)
|
|
584
829
|
if (!session) begin()
|
|
585
830
|
else if (isOpen()) transmit(entry)
|
|
586
831
|
else connect()
|
|
@@ -616,7 +861,9 @@ export function mountChat(root, deps = {}) {
|
|
|
616
861
|
mode = 'bot'
|
|
617
862
|
ownerName = null
|
|
618
863
|
header()
|
|
619
|
-
for (const li of [...log.querySelectorAll('li[data-id], .pmw-chat__relay')]) li.remove()
|
|
864
|
+
for (const li of [...log.querySelectorAll('li[data-id], .pmw-chat__relay, .pmw-chat__handback')]) li.remove()
|
|
865
|
+
hideTyping()
|
|
866
|
+
notSlow()
|
|
620
867
|
ended.hidden = true
|
|
621
868
|
composer.hidden = false
|
|
622
869
|
input.focus()
|
|
@@ -635,23 +882,33 @@ export function mountChat(root, deps = {}) {
|
|
|
635
882
|
if (e.shiftKey && doc.activeElement === first) { e.preventDefault(); last.focus() }
|
|
636
883
|
else if (!e.shiftKey && doc.activeElement === last) { e.preventDefault(); first.focus() }
|
|
637
884
|
})
|
|
638
|
-
|
|
885
|
+
// A mouse or touch close (click.detail > 0) does not park focus on the bubble: a focused bubble
|
|
886
|
+
// never steps aside for the footer (operator, 2026-11-13). Keyboard closes (Enter, Escape) still do.
|
|
887
|
+
closeBtn.addEventListener('click', (e) => close({ keyboard: e.detail === 0 }))
|
|
639
888
|
|
|
640
889
|
function show() {
|
|
641
890
|
if (!demo) turnstile.load()
|
|
642
891
|
panel.hidden = false
|
|
892
|
+
panel.inert = false
|
|
643
893
|
root.setAttribute('data-open', '')
|
|
644
894
|
root.removeAttribute('data-unread')
|
|
645
895
|
if (session) connect()
|
|
646
896
|
;(demo ? closeBtn : composer.hidden ? (fallback.hidden ? restart : fallback.querySelector('input')) : input)?.focus()
|
|
647
|
-
|
|
897
|
+
follow(true, true)
|
|
648
898
|
}
|
|
649
|
-
function close() {
|
|
899
|
+
function close({ keyboard = true } = {}) {
|
|
650
900
|
panel.hidden = true
|
|
901
|
+
panel.inert = true
|
|
651
902
|
root.removeAttribute('data-open')
|
|
652
|
-
bubble?.focus()
|
|
903
|
+
if (keyboard) bubble?.focus()
|
|
904
|
+
// The trigger re-checks the footer on a focusout (trigger.js sync): tell it the panel is shut, so
|
|
905
|
+
// the bubble steps aside at once when the footer is already in view.
|
|
906
|
+
root.dispatchEvent(new win.FocusEvent('focusout', { relatedTarget: keyboard ? bubble : null }))
|
|
653
907
|
}
|
|
654
908
|
|
|
909
|
+
// The early start (« TURNSTILE » above). Off the real hosts there is nothing to start.
|
|
910
|
+
if (!demo) onApproach(root, () => turnstile.load(), { win, settleMs: WARM_MS })
|
|
911
|
+
|
|
655
912
|
root.pmwChat = { open: show, close, panel }
|
|
656
913
|
return root.pmwChat
|
|
657
914
|
}
|
package/src/worker.js
CHANGED
|
@@ -32,7 +32,7 @@ import { verifyTurnstile } from '@pmeweb/turnstile/worker'
|
|
|
32
32
|
import { RPC_HEADER, RPC_VERSION } from './protocol.js'
|
|
33
33
|
|
|
34
34
|
/** This package's version, reported by the health route (a test holds it equal to package.json). */
|
|
35
|
-
export const VERSION = '1.3.
|
|
35
|
+
export const VERSION = '1.3.2'
|
|
36
36
|
export { RPC_VERSION }
|
|
37
37
|
|
|
38
38
|
const SERVICE = 'https://pmeweb-chat.internal'
|
|
@@ -117,6 +117,12 @@ async function start(request, url, env, { config, lang }) {
|
|
|
117
117
|
if (config?.sms === 'voipms') body.sms = 'voipms'
|
|
118
118
|
if (config?.photos === true) body.photos = true
|
|
119
119
|
if (lead) body.fallbackLead = lead
|
|
120
|
+
// The conversation the visitor already had, so the form's request joins its thread (1.3.2); the
|
|
121
|
+
// service checks the key, as it does for the socket.
|
|
122
|
+
if (lead && typeof b.c === 'string' && /^[0-9a-f-]{36}$/i.test(b.c) && typeof b.k === 'string' && /^[A-Za-z0-9_-]{16,64}$/.test(b.k)) {
|
|
123
|
+
body.conversationId = b.c
|
|
124
|
+
body.visitorKey = b.k
|
|
125
|
+
}
|
|
120
126
|
|
|
121
127
|
const headers = { 'content-type': 'application/json', [RPC_HEADER]: String(RPC_VERSION) }
|
|
122
128
|
const ip = request.headers.get('cf-connecting-ip')
|