@pmeweb/plugin-chat 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/INSTALL.md CHANGED
@@ -33,11 +33,17 @@ package's, approved once.
33
33
 
34
34
  ```jsonc
35
35
  "services": [{ "binding": "CHAT", "service": "pmeweb-chat", "entrypoint": "ChatApi" }],
36
- "ratelimits": [{ "name": "CHAT_LIMIT", "namespace_id": "<unused on the account>", "simple": { "limit": 20, "period": 60 } }]
36
+ "ratelimits": [
37
+ { "name": "CHAT_LIMIT", "namespace_id": "<unused on the account>", "simple": { "limit": 20, "period": 60 } }
38
+ ]
37
39
  ```
38
40
 
39
- The limit counts conversation starts, socket (re)connections and photo uploads per IP, not messages
40
- (pmeweb-chat caps turns per conversation and the day's spend).
41
+ It counts per IP, not messages: starts, socket (re)connections, photo uploads and the contact
42
+ form's posts. pmeweb-chat applies the GLOBAL limits (portal admin, « Assistant virtuel »):
43
+ messages per conversation, conversations per visitor per Quebec day and starts per minute (it
44
+ receives the IP from `handleChat` in `x-pmw-visitor-ip`), and the daily cap per site.
45
+ **Updating from 1.1.0**: remove the `CHAT_START_LIMIT` entry and its `Env` line; nothing else
46
+ changes on the site. From 1.0.0, nothing to add.
41
47
  4. [ASK] `npx wrangler secret put TURNSTILE_SECRET` (skip when the contact form already set it;
42
48
  `npx wrangler secret list` to check).
43
49
  5. **Worker** (`worker/index.ts`), before `pluginArtifacts` and the assets:
@@ -113,9 +119,11 @@ package's, approved once.
113
119
  10. **Not in `pmeweb.json` `forms`**: the chat's Turnstile widget renders only once the panel is
114
120
  open, so the portal's form-health browser check would find nothing on a page load.
115
121
  11. `npm run build && node ../../a11y-tools/check-plugins.mjs .` until PASS; `npm run check`.
116
- 12. **Portal** (« Modules › Assistant virtuel »): PmeWeb sets `alertEmails` and `dailyCap`
122
+ 12. **Portal** (« Modules › Assistant virtuel »): PmeWeb sets `alertEmails`
117
123
  (`alertEmails` is needed before the switch goes on: pmeweb-chat refuses a site without it), `alertPhone` if texts are on, and
118
- « Consignes » with the client; the client may set `botName` and `ownerName`.
124
+ « Consignes » with the client; the client may set `ownerName`, and changes the face, the icon's
125
+ colour and the name (`botName`) in « Contenu › Modules › Assistant virtuel › Apparence »
126
+ (applied by « Publier »).
119
127
  13. Deploy (still off: nothing on the site, health `disabled`). Once the client or the operator
120
128
  switches it on and publishes, from a real host: the bubble shows, a question gets an answer, the signed health
121
129
  route answers `chat.ok`.
package/README.md CHANGED
@@ -7,7 +7,7 @@ email (and text), and lets the owner take over live.
7
7
  Three pieces:
8
8
 
9
9
  - **This package**, on the site: the bubble and the opener (plain HTML, `Chat.astro`), an inline
10
- trigger under 2 KB, the panel loaded on the first touch (`dist/widget.js`), the site Worker's
10
+ trigger under 2.3 KB (`inlineBudget` 2304), the panel loaded on the first touch (`dist/widget.js`), the site Worker's
11
11
  `/api/chat/start`, `/api/chat/ws` and `/api/chat/photo` (`handleChat`), `health`, `cspHosts`, and the build-time
12
12
  context file (`@pmeweb/plugin-chat/build`).
13
13
  - **`pmeweb-chat`** (private): one Durable Object per conversation, the model (Gemma 4 on Workers
@@ -26,33 +26,79 @@ Install it with the `install-plugin` skill (`INSTALL.md`).
26
26
  sur vos toitures? », anything else « Quels sont vos délais? »), and « (Démonstration) Ici,
27
27
  l’assistant répond à partir du contenu de votre site. ». The composer is disabled under « Aperçu :
28
28
  la conversation est désactivée. ». No `/api/chat/*` call, no Turnstile, no socket, no lead; the
29
- routes still answer 403 `disabled` there, so nobody spends the client's cap from « Contenu ». The
30
- header shows « Assistant virtuel »: the portal's `botName` is only known to pmeweb-chat.
29
+ routes still answer 403 `disabled` there, so nobody spends the client's cap from « Contenu ». No
30
+ opener is scheduled there.
31
+ - **The preview's fragment** (off the real hosts only, read on load and on `hashchange`):
32
+ `#pmw-chat=panel` opens the demo's panel, `#pmw-chat=bubble` closes it,
33
+ `pmw-chat-botName=<url-encoded>` names the assistant (absent: « Assistant virtuel » alone). The
34
+ portal's « Apparence » screen drives it without a rebuild.
35
+ - **« Apparence »** (« Contenu › Modules › Assistant virtuel › Apparence »): the client picks the
36
+ face (`avatar`: photos, illustration or a « Sans visage » icon), the icon's colour (`avatarBg`,
37
+ default the site's accent) and the name (`botName`, which ships with « Publier »). `logo` stays
38
+ PmeWeb's, set at install.
31
39
  - **Nothing loads with the page.** The bubble is HTML and CSS; the panel, Turnstile and the socket
32
40
  arrive when the visitor presses, hovers, focuses or types on it.
33
- - **Openers.** One line per page, from the site's dictionary (`chat.openers`, `chat.defaultOpener`).
41
+ - **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.
34
42
  Shown once per visit (20 s, 30 % of the page, or exit intent with a mouse), never while the panel
35
- is open. The same line greets the visitor in the panel.
43
+ is open, never while the footer is in view (it waits for the next of those triggers). On a phone
44
+ (under 640 px) it fades out by itself 8 s after it appears. The same line greets the visitor in
45
+ the panel. The « once per visit » is a `pmw-chat-opener` flag in `sessionStorage` (a UI flag, no
46
+ personal information; storage blocked: once per page).
47
+ - **On a phone** (under 640 px, where the panel goes full screen) the bubble is a 56 px round
48
+ button with the face alone; « Assistant virtuel » stays in the markup, visually hidden, and the
49
+ button's `aria-label` still starts with it. From 640 px it is the pill with the text.
50
+ - **Out of the footer's way** (real hosts only). While the page's last `footer` element is in view
51
+ (IntersectionObserver), the bubble and the opener fade out (opacity and an 8 px slide, 0.2 s,
52
+ then `visibility: hidden`) and the root is `inert`; they come back when it leaves. A site whose
53
+ footer is not a `footer`, or that wants another zone kept clear, marks it `data-pmw-chat-avoid`
54
+ (the first one wins over the footer). Never while the panel is open or while focus is in the
55
+ bubble or the opener (a focusout re-checks). Reduced motion: hidden and shown at once.
36
56
  - **Never passing for a person.** « Assistant virtuel » is always in text; the green dot appears
37
57
  only when the owner has taken over, with the owner's name.
38
58
  - **The socket.** A drop (1006, a forced deploy of pmeweb-chat) reconnects with backoff 1 to 30 s
39
59
  and catches up from the last message seen; a message typed meanwhile is queued and sent on open,
40
60
  and after 3 sends without an echo shows « Non envoyé, Réessayer ». A 44xx close never reconnects.
41
- - **Fallback.** Daily cap reached, the model down, or the start refused: a three-field contact
42
- form that posts to `/api/chat/start` with `fallbackLead`.
61
+ - **Fallback.** Daily cap reached, 15 visitor messages in the conversation, the visitor's 10
62
+ conversations of the day used up (pmeweb-chat answers the start 409 `cap`), the model down, or
63
+ the start refused: a three-field contact form that posts to `/api/chat/start` with `fallbackLead`
64
+ (never counted against the visitor's 10, and on the looser `CHAT_LIMIT`).
65
+ - **Limits.** GLOBAL since 1.2.0, the same for every site, edited in the portal admin
66
+ (« Assistant virtuel ») and applied by pmeweb-chat: messages per conversation, conversations per
67
+ visitor per day, conversation starts per minute, the daily cap per site. On the site, only
68
+ `CHAT_LIMIT` (20 a minute per IP: starts, socket reconnections, photos, the contact form). The site Worker hands the visitor's IP
69
+ to pmeweb-chat in `x-pmw-visitor-ip` (1.1.0); pmeweb-chat keeps only a daily hash of it to count
70
+ conversations per day. Health reports `limit`.
43
71
  - **Photos** (`"photos": true`). Up to 3 per conversation, shrunk in the browser to a 1600 px JPEG
44
72
  (the canvas drops the EXIF, GPS included), posted to `/api/chat/photo` (rate-limited like a
45
73
  message, 5 MB cap), re-encoded by pmeweb-chat and kept in a private bucket. The visitor sees the
46
74
  local copy; the owner sees them through the portal. The bot does not read them.
47
75
  - **The goal** `chat-lead` fires once per conversation (`window.pmw?.event`).
48
76
 
77
+ ## Above a sticky bar
78
+
79
+ The bubble, the opener and the desktop panel all stand `--pmw-chat-bottom` (16px) above the bottom
80
+ edge, plus the safe area. A site with its own sticky bar at the bottom lifts them while the bar is
81
+ visible: it sets the variable to the bar's height plus 12px, on `<html>`, `<body>` or `#pmw-chat`,
82
+ from whatever already says the bar is up (a class, an attribute). The package eases the change
83
+ (0.3 s, none under reduced motion). The phone panel is full screen and covers the bar on purpose.
84
+
85
+ ```css
86
+ /* The bar is 61px tall (the safe area is already added by the package). */
87
+ @media (max-width: 899px) {
88
+ body:has(#sticky-cta:not(.translate-y-full)) { --pmw-chat-bottom: 73px; }
89
+ }
90
+ ```
91
+
92
+ Nothing site-specific lives in the package: the selector is the site's.
93
+
49
94
  ## Markup contract
50
95
 
51
96
  A site may write its own markup carrying these and render the inline trigger itself:
52
97
 
53
98
  | Selector | Role |
54
99
  |---|---|
55
- | `#pmw-chat` | the root, `hidden` until the trigger finds a real host; `data-open` while the panel is open, `data-unread` when a reply arrived with the panel closed |
100
+ | `#pmw-chat` | the root, `hidden` until the trigger runs; `data-open` while the panel is open, `data-unread` when a reply arrived with the panel closed, `inert` while it steps out of the footer's way |
101
+ | `[data-pmw-chat-avoid]` | anywhere on the page: the zone the bubble fades away from (default: the page's last `footer`) |
56
102
  | `[data-pmw-chat-opener]` | on the root: this page's opener text |
57
103
  | `[data-pmw-lazy]` | on the root: the URL of `dist/widget.js` (`?url&no-inline`) |
58
104
  | `[data-pmw-chat-open]` | the bubble and the opener's text: a click opens the panel |
@@ -70,10 +116,13 @@ the package defaults). For a `sans-visage` icon the circle is `config.avatarBg`
70
116
  (#14181f without one), whichever contrasts more; a circle under 3:1 against the page `background`
71
117
  or the `surface` gets a 1.5 px ring in `ink`. No text says so.
72
118
 
73
- The panel: the header, the visitor's bubbles and the send button in `primary`, their text white or
74
- `ink` (#14181f when the ink is too light), at 4.5:1 or the default navy stays; the panel and the
75
- bubble in `surface` with `ink` text (at 4.5:1 or the default white panel stays), the bot's bubbles
76
- in `background`, and muted text, links, the focus ring and errors re-derived to keep their minimum.
119
+ The panel: the visitor's bubbles and the send button in `primary`, their text white or `ink`
120
+ (#14181f when the ink is too light), at 4.5:1 or the default navy stays; the panel, its header,
121
+ the bot's bubbles and the launcher in `surface` with `ink` text (at 4.5:1 or the default white
122
+ panel stays); the thread behind the bubbles a 4 % tint of `accent` (else `primary`) over the
123
+ surface; a person who took over in an 8 % tint of the accent; and muted text, links, the live green,
124
+ the focus ring and errors re-derived to keep their minimum on both the surface and the thread.
125
+ Where a person takes over, the thread shows « {name} a pris le relais » (an `li role="separator"`).
77
126
  The fields stay white with their own text colour. The values land in one
78
127
  `<style>:where(:root){...}</style>` beside the root: zero specificity, so a site's own
79
128
  `--pmw-chat-*` value still wins.
@@ -84,9 +133,11 @@ Never declared by the package: each use carries its default, so a site sets any
84
133
  `--pmw-chat-accent` (#0a1b3d) and `--pmw-chat-accent-fg` (#ffffff), `--pmw-chat-avatar-bg`,
85
134
  `--pmw-chat-avatar-fg` and `--pmw-chat-avatar-ring` (the sans-visage circle), `--pmw-chat-fg` (#1f2328),
86
135
  `--pmw-chat-muted` (#4b5563), `--pmw-chat-bubble-bg` and `--pmw-chat-panel-bg` (#ffffff),
87
- `--pmw-chat-other-bg` (#f1f3f6), `--pmw-chat-border` (#c9ced6), `--pmw-chat-field-border`
136
+ `--pmw-chat-other-bg` (#ffffff, the bot's bubbles), `--pmw-chat-thread-bg` (#f7f8fb),
137
+ `--pmw-chat-owner-bg` (#eef2fa) and `--pmw-chat-owner-border` (#d5ddef), `--pmw-chat-line` (#e6e9f0)
138
+ and `--pmw-chat-rule` (#cfd5e1) (decoration), `--pmw-chat-border` (#c9ced6), `--pmw-chat-field-border`
88
139
  (#7d8590), `--pmw-chat-field-fg` (#1f2328, the text in the white fields), `--pmw-chat-focus`, `--pmw-chat-link`, `--pmw-chat-error` (#b42318), `--pmw-chat-live`
89
- (#3fb950), `--pmw-chat-font`, `--pmw-chat-right` and `--pmw-chat-bottom` (16px). Check every pair a
140
+ (#1a7f37, the « Vous répond en direct » text and the dots), `--pmw-chat-font`, `--pmw-chat-right` and `--pmw-chat-bottom` (16px). Check every pair a
90
141
  site changes with `a11y-tools/contrast.mjs`.
91
142
 
92
143
  ## What goes wrong in silence
@@ -98,7 +149,7 @@ site changes with `a11y-tools/contrast.mjs`.
98
149
  - `enabled` not exactly `true` (`"true"` in quotes, a typo): the chat is off, nothing on the site.
99
150
  - `@pmeweb/consent` older than 1.1.0: the privacy section prints while the chat is off
100
151
  (check-plugins refuses it).
101
- - Missing `alertEmails` or `dailyCap` in the portal: pmeweb-chat answers `not_configured`; health
152
+ - Missing `alertEmails` in the portal: pmeweb-chat answers `not_configured`; health
102
153
  says `settings: false`.
103
154
  - The context file absent from `dist/` or not served signed: the bot falls back to the contact form
104
155
  on every message.
@@ -108,7 +159,7 @@ site changes with `a11y-tools/contrast.mjs`.
108
159
  `config.avatar`, one of `src/faces.js` (the manifest's pattern is copied from it):
109
160
 
110
161
  - `photos-1` to `photos-5`, `illustration-1` to `illustration-5`: faces generated for PmeWeb (not
111
- real people), 96 x 96 WebP (about 1.5 KB) for the 48 px bubble at 2x; the panel header (40 px)
162
+ real people), 96 x 96 WebP (about 1.5 KB) for the 48 px bubble at 2x; the panel header (44 px)
112
163
  reuses the same file.
113
164
  - `sans-visage-1` to `sans-visage-5`: a speech-bubble glyph drawn inline in `currentColor` (no
114
165
  request). 3 to 5 are Lucide icons (ISC, `LICENSE-LUCIDE`).
@@ -1 +1 @@
1
- (()=>{var T=["pointerdown","pointerenter","focusin","keydown"];function L(s,n,{events:i=T}={}){let c=!1,t=()=>{if(!c){c=!0;for(let r of i)s.removeEventListener(r,t);n()}};for(let r of i)s.addEventListener(r,t,{passive:!0});return{run:t}}var g="pmw-chat-opener",x=2e4,b=.3;function d({doc:s=document,win:n=window,importLazy:i=t=>import(t),onInteraction:c=L}={}){let t=s.getElementById("pmw-chat");if(!t)return null;t.hidden=!1;let r=t.querySelector(".pmw-chat__teaser"),p=null,l=()=>p??(p=i(t.dataset.pmwLazy).then(e=>e.mountChat(t),e=>{throw p=null,e}));c(t,()=>l().catch(()=>{}));let m=n.sessionStorage,f=!1;try{f=!!m?.getItem(g)}catch{}let h=[],E=(e,o,a)=>{e.addEventListener(o,a,{passive:!0}),h.push([e,o,a])},v,y=()=>{f=!0;try{m?.setItem(g,"1")}catch{}n.clearTimeout(v);for(let[e,o,a]of h)e.removeEventListener(o,a)},u=()=>{f||!r||t.hasAttribute("data-open")||(y(),r.hidden=!1)};return t.addEventListener("click",e=>{let o=e.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");o&&(r&&(r.hidden=!0),y(),o.hasAttribute("data-pmw-chat-open")&&l().then(a=>a.open(),()=>{}))}),!f&&r&&(v=n.setTimeout(u,x),E(n,"scroll",()=>{let e=s.documentElement.scrollHeight-n.innerHeight;e>0&&n.scrollY/e>=b&&u()}),n.matchMedia?.("(pointer: fine)").matches&&E(s,"mouseout",e=>{!e.relatedTarget&&e.clientY<=0&&u()})),{load:l,show:u}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>d()):d();})();
1
+ (()=>{function S({doc:r=document,hostname:n=globalThis.location?.hostname??""}={}){let a=r.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(e=>typeof e=="string"&&e.toLowerCase()===c)}var A=["pointerdown","pointerenter","focusin","keydown"];function T(r,n,{events:a=A}={}){let s=!1,c=()=>{if(!s){s=!0;for(let e of a)r.removeEventListener(e,c);n()}};for(let e of a)r.addEventListener(e,c,{passive:!0});return{run:c}}var w="pmw-chat-opener",O=2e4,I=.3;function E({doc:r=document,win:n=window,hostname:a=n.location.hostname,importLazy:s=e=>import(e),onInteraction:c=T}={}){let e=r.getElementById("pmw-chat");if(!e)return null;e.hidden=!1;let l=e.querySelector(".pmw-chat__teaser"),u=null,d=()=>u??(u=s(e.dataset.pmwLazy).then(t=>t.mountChat(e),t=>{throw u=null,t}));c(e,()=>d().catch(()=>{}));let m=S({doc:r,hostname:a});if(!m){let t=()=>{let o=new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat");o==="panel"&&!e.hasAttribute("data-open")?d().then(i=>i.open(),()=>{}):o==="bubble"&&u&&u.then(i=>i.close(),()=>{})};t(),n.addEventListener("hashchange",t)}let h=!1,v=(t=r.activeElement)=>{let o=h&&!e.hasAttribute("data-open")&&!e.contains(t);e.toggleAttribute("inert",o)},y=r.querySelector("[data-pmw-chat-avoid]")??[...r.querySelectorAll("footer")].pop();m&&y&&(new n.IntersectionObserver(t=>{h=t.at(-1).isIntersecting,v()}).observe(y),e.addEventListener("focusout",t=>v(t.relatedTarget)));let g=n.sessionStorage,f=!1;try{f=!!g?.getItem(w)}catch{}let b=(t,o,i)=>t.addEventListener(o,x=>f||i(x),{passive:!0}),L=()=>{f=!0;try{g?.setItem(w,"1")}catch{}},p=()=>{f||h||!l||e.hasAttribute("data-open")||(L(),l.hidden=!1)};return e.addEventListener("click",t=>{let o=t.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");o&&(l&&(l.hidden=!0),L(),o.hasAttribute("data-pmw-chat-open")&&d().then(i=>i.open(),()=>{}))}),!f&&l&&m&&(n.setTimeout(p,O),b(n,"scroll",()=>{let t=r.documentElement.scrollHeight-n.innerHeight;t>0&&n.scrollY/t>=I&&p()}),n.matchMedia?.("(pointer: fine)").matches&&b(r,"mouseout",t=>{!t.relatedTarget&&t.clientY<=0&&p()})),{load:d,show:p}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>E()):E();})();
package/dist/widget.js CHANGED
@@ -1,2 +1,2 @@
1
- var He=`.pmw-chat__panel{position:fixed;z-index:81;inset:0;display:flex;flex-direction:column;background:var(--pmw-chat-panel-bg, #ffffff);color:var(--pmw-chat-fg, #1f2328);font-family:var(--pmw-chat-font, inherit);font-size:16px;line-height:1.45}.pmw-chat__panel[hidden],.pmw-chat__panel [hidden]{display:none!important}@media(min-width:640px){.pmw-chat__panel{inset:auto;right:calc(env(safe-area-inset-right,0px) + var(--pmw-chat-right, 16px));bottom:calc(env(safe-area-inset-bottom,0px) + var(--pmw-chat-bottom, 16px));width:380px;height:min(600px,calc(100dvh - 32px));border:1px solid var(--pmw-chat-border, #c9ced6);border-radius:12px;overflow:hidden;box-shadow:0 12px 36px #00000038}}.pmw-chat__panel button{font:inherit;cursor:pointer}.pmw-chat__panel :focus-visible{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__head :focus-visible{outline-color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__head{display:flex;align-items:center;gap:10px;padding:10px 8px 10px 12px;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__head .pmw-chat__avatar{width:40px;height:40px;box-shadow:none;background:var(--pmw-chat-avatar-bg, var(--pmw-chat-accent-fg, #ffffff));color:var(--pmw-chat-avatar-fg, var(--pmw-chat-accent, #0a1b3d))}.pmw-chat__head .pmw-chat__avatar img{width:40px;height:40px}.pmw-chat__who{flex:1;min-width:0}.pmw-chat__name{margin:0;font-weight:600;font-size:16px}.pmw-chat__role{margin:0;font-size:14px}.pmw-chat__dot{flex:none;width:10px;height:10px;border-radius:50%;background:var(--pmw-chat-live, #3fb950);box-shadow:0 0 0 2px var(--pmw-chat-accent, #0a1b3d)}.pmw-chat__close{flex:none;width:44px;height:44px;padding:0;border:0;border-radius:50%;display:flex;align-items:center;justify-content:center;background:none;color:inherit}.pmw-chat__close svg{width:18px;height:18px}.pmw-chat__body{flex:1;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding:10px 12px}.pmw-chat__notice{margin:0 0 8px;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__notice a{color:var(--pmw-chat-link, #0a1b3d);text-decoration:underline;text-underline-offset:2px}.pmw-chat__log{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:8px}.pmw-chat__msg{max-width:85%;padding:8px 12px;border-radius:12px;white-space:pre-wrap;overflow-wrap:anywhere}.pmw-chat__msg--bot,.pmw-chat__msg--owner{align-self:flex-start;background:var(--pmw-chat-other-bg, #f1f3f6);color:var(--pmw-chat-fg, #1f2328);border-bottom-left-radius:4px}.pmw-chat__msg--visitor{align-self:flex-end;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff);border-bottom-right-radius:4px}.pmw-chat__msg.is-pending{opacity:.75}.pmw-chat__msg.is-failed{outline:2px solid var(--pmw-chat-error, #b42318)}.pmw-chat__retry{display:block;margin-top:4px;font-size:14px}.pmw-chat__retry button{padding:0;border:0;background:none;color:inherit;text-decoration:underline}.pmw-chat__typing{align-self:flex-start;display:flex;gap:4px;padding:12px;border-radius:12px;background:var(--pmw-chat-other-bg, #f1f3f6)}.pmw-chat__typing span{width:6px;height:6px;border-radius:50%;background:var(--pmw-chat-muted, #4b5563);animation:pmw-chat-dot 1.2s infinite ease-in-out}.pmw-chat__typing span:nth-child(2){animation-delay:.15s}.pmw-chat__typing span:nth-child(3){animation-delay:.3s}@keyframes pmw-chat-dot{0%,80%,to{opacity:.3}40%{opacity:1}}@media(prefers-reduced-motion:reduce){.pmw-chat__typing span{animation:none;opacity:.7}}.pmw-chat__state{margin:8px 0 0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__state:empty{display:none}.pmw-chat__ended{margin-top:12px}.pmw-chat__ended p{margin:0 0 8px}.pmw-chat__composer{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;padding:10px 12px;border-top:1px solid var(--pmw-chat-border, #c9ced6)}.pmw-chat__composer textarea{flex:1;min-width:0;resize:none}.pmw-chat__composer textarea,.pmw-chat__field input,.pmw-chat__field textarea{font:inherit;font-size:16px;padding:8px 10px;border-radius:8px;border:1px solid var(--pmw-chat-field-border, #7d8590);background:#fff;color:var(--pmw-chat-field-fg, #1f2328)}.pmw-chat__send{min-height:44px;padding:8px 16px;border:0;border-radius:8px;font-weight:600;background:var(--pmw-chat-accent, #0a1b3d);color:var(--pmw-chat-accent-fg, #ffffff)}.pmw-chat__send:disabled,.pmw-chat__composer textarea:disabled{cursor:not-allowed;opacity:.6}.pmw-chat__restart{min-height:44px;padding:8px 16px;border-radius:8px;border:1px solid var(--pmw-chat-link, #0a1b3d);background:none;color:var(--pmw-chat-link, #0a1b3d);font-weight:600}.pmw-chat__composer .pmw-turnstile,.pmw-chat__fallback .pmw-turnstile{flex-basis:100%}.pmw-chat__attach{flex:none;position:relative;width:44px;height:44px;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;border:1px solid var(--pmw-chat-field-border, #7d8590);color:var(--pmw-chat-field-fg, #1f2328);background:#fff}.pmw-chat__attach svg{width:22px;height:22px}.pmw-chat__attach:focus-within{outline:3px solid var(--pmw-chat-focus, #0a1b3d);outline-offset:2px}.pmw-chat__hint{flex-basis:100%;margin:0;font-size:14px;color:var(--pmw-chat-muted, #4b5563)}.pmw-chat__composer .pmw-chat__photo-error{flex-basis:100%}.pmw-chat__photo{display:block;max-width:100%;max-height:220px;border-radius:8px}.pmw-chat__fallback{display:flex;flex-direction:column;gap:10px;margin-top:12px}.pmw-chat__fb-title{margin:0;font-weight:600}.pmw-chat__fb-intro{margin:0;font-size:15px}.pmw-chat__field{display:flex;flex-direction:column;gap:4px}.pmw-chat__field label{font-size:14px;font-weight:600}.pmw-chat__error{margin:0;font-size:14px;color:var(--pmw-chat-error, #b42318)}.pmw-chat__error:empty{display:none}.pmw-chat__sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}.pmw-chat[data-unread] .pmw-chat__bubble{position:relative}.pmw-chat[data-unread] .pmw-chat__bubble:after{content:"";position:absolute;top:2px;left:38px;width:12px;height:12px;border-radius:50%;background:var(--pmw-chat-error, #b42318);box-shadow:0 0 0 2px #fff}
2
- `;var De=new Set;function Ue(o,{doc:c=document,attrs:r={}}={}){if(De.has(o)||(De.add(o),c.querySelector(`script[src="${o}"]`)))return;let t=c.createElement("script");t.src=o,t.async=!0;for(let[d,h]of Object.entries(r))t.setAttribute(d,h);c.head.append(t)}function Be({doc:o=document,hostname:c=globalThis.location?.hostname??""}={}){let r=o.getElementById("pmw-hosts");if(!r)return!1;let t;try{t=JSON.parse(r.textContent??"")}catch{return!1}if(!Array.isArray(t))return!1;let d=String(c).toLowerCase();return t.some(h=>typeof h=="string"&&h.toLowerCase()===d)}var it="https://challenges.cloudflare.com/turnstile/v0/api.js";var ct=15e3,$e=200;function fe(o=document){Ue(it,{doc:o})}var lt=o=>o.querySelector('input[name="cf-turnstile-response"]')?.value??"",pt=o=>{let c=o.querySelector(".cf-turnstile");return!!c&&c.offsetHeight>0};async function je(o,{win:c=window,timeoutMs:r=ct}={}){typeof o.pmwTurnstileLoad=="function"?o.pmwTurnstileLoad():fe(c.document);let t=0;for(;lt(o)==="";){if(!pt(o)){if(t>=r)return!1;t+=$e}await new Promise(d=>c.setTimeout(d,$e))}return!0}function Fe(o,{win:c=window}={}){let r=o.querySelector(".cf-turnstile");if(!r||r.offsetHeight>0)return!1;try{return c.turnstile?.reset(r),!0}catch{return!1}}var Je={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var dt={fr:{assistant:"Assistant virtuel",openLabel:"Assistant virtuel, ouvrir le clavardage",dismissTeaser:"Masquer ce message",close:"Fermer le clavardage",panelTitle:"Clavardage",automated:"R\xE9ponses automatiques, tir\xE9es du contenu de ce site.",privacy:"Confidentialit\xE9",you:"Vous",messageLabel:"Votre message",send:"Envoyer",live:"Vous r\xE9pond en direct",connecting:"Connexion\u2026",reconnecting:"Connexion perdue, reconnexion\u2026",notSent:"Non envoy\xE9.",retry:"R\xE9essayer",ended:"Cette conversation est termin\xE9e.",newConversation:"Nouvelle conversation",reload:"Le clavardage a \xE9t\xE9 mis \xE0 jour. Rechargez la page pour continuer.",unavailable:"Le clavardage n\u2019est pas disponible pour le moment.",turnstile:"La v\xE9rification anti-robot n\u2019a pas abouti. R\xE9essayez.",tooMany:"Trop de messages en peu de temps. R\xE9essayez dans une minute.",fallbackTitle:"Laissez-nous vos coordonn\xE9es",fallbackCap:"L\u2019assistant ne peut plus r\xE9pondre aujourd\u2019hui. Laissez vos coordonn\xE9es et on vous recontactera.",fallbackDown:"L\u2019assistant ne peut pas r\xE9pondre pour le moment. Laissez vos coordonn\xE9es et on vous recontactera.",name:"Votre nom",contact:"T\xE9l\xE9phone ou courriel",need:"Votre demande",fallbackSend:"Envoyer la demande",fallbackSent:"Merci, votre demande a \xE9t\xE9 transmise.",fallbackMissing:"Remplissez les trois champs.",said:"{name}\xA0: {text}",attachPhoto:"Joindre une photo",photoHint:"\xC9vitez les photos de personnes ou de pi\xE8ces d\u2019identit\xE9.",photoAlt:"Votre photo",photoSent:"Photo envoy\xE9e",photoLimit:"Trois photos au plus par conversation.",photoTooLarge:"Cette photo est trop lourde.",photoUnreadable:"Ce fichier n\u2019est pas une photo lisible.",photoFailed:"La photo n\u2019a pas pu \xEAtre envoy\xE9e. R\xE9essayez.",demoAsk:"Pouvez-vous m\u2019en dire plus sur {topic}?",demoQuestion:"Quels sont vos d\xE9lais?",demoAnswer:"(D\xE9monstration) Ici, l\u2019assistant r\xE9pond \xE0 partir du contenu de votre site.",demoHint:"Aper\xE7u\xA0: la conversation est d\xE9sactiv\xE9e."},en:{assistant:"Virtual assistant",openLabel:"Virtual assistant, open the chat",dismissTeaser:"Hide this message",close:"Close the chat",panelTitle:"Chat",automated:"Automatic answers, drawn from this site\u2019s content.",privacy:"Privacy",you:"You",messageLabel:"Your message",send:"Send",live:"Replying live",connecting:"Connecting\u2026",reconnecting:"Connection lost, reconnecting\u2026",notSent:"Not sent.",retry:"Retry",ended:"This conversation has ended.",newConversation:"New conversation",reload:"The chat was updated. Reload the page to continue.",unavailable:"The chat is not available right now.",turnstile:"The anti-robot check did not go through. Try again.",tooMany:"Too many messages in a short time. Try again in a minute.",fallbackTitle:"Leave us your details",fallbackCap:"The assistant cannot answer any more today. Leave your details and we will get back to you.",fallbackDown:"The assistant cannot answer right now. Leave your details and we will get back to you.",name:"Your name",contact:"Phone or email",need:"Your request",fallbackSend:"Send the request",fallbackSent:"Thank you, your request was sent.",fallbackMissing:"Fill in all three fields.",said:"{name}: {text}",attachPhoto:"Attach a photo",photoHint:"Avoid photos of people or of identity documents.",photoAlt:"Your photo",photoSent:"Photo sent",photoLimit:"Three photos at most per conversation.",photoTooLarge:"This photo is too large.",photoUnreadable:"This file is not a readable photo.",photoFailed:"The photo could not be sent. Try again.",demoAsk:"Can you tell me more about {topic}?",demoQuestion:"What are your lead times?",demoAnswer:"(Demo) Here, the assistant answers from your site\u2019s content.",demoHint:"Preview: the conversation is turned off."}},Q=o=>String(o??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function me(o,c={}){let r=dt[Q(o)],t={...r};for(let[d,h]of Object.entries(c??{}))typeof h=="string"&&d in r&&(t[d]=h);return t}var W=(o,c)=>o.replace(/\{(\w+)\}/g,(r,t)=>t in c?String(c[t]):r);var we="pmw-chat",Ke=[1e3,2e3,4e3,8e3,16e3,3e4],Xe=3,ut="chat-lead",ht=3,ft=1600,mt=.85,wt=3e3,gt=8e3,_t='<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 8h3l2-3h6l2 3h3v11H4z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><circle cx="12" cy="13" r="3.5" fill="none" stroke="currentColor" stroke-width="2"/></svg>';async function bt(o,c){let r=await o.createImageBitmap(c,{imageOrientation:"from-image"}),t=Math.min(1,ft/Math.max(r.width,r.height)),d=Math.max(1,Math.round(r.width*t)),h=Math.max(1,Math.round(r.height*t)),f=o.document.createElement("canvas");return f.width=d,f.height=h,f.getContext("2d").drawImage(r,0,0,d,h),r.close?.(),new Promise((q,Z)=>f.toBlob(I=>I?q(I):Z(new Error("encode")),"image/jpeg",mt))}var xt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},vt={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function yt(o,c){let r=Q(c),t=me(r),d=String(o??"").trim().match(vt[r]);if(!d)return t.demoQuestion;let h=d[1].replace(/[\p{L}’']+/gu,f=>xt[r][f.toLowerCase()]??f);return W(t.demoAsk,{topic:h})}var St='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function s(o,c,r={},t){let d=o.createElement(c);for(let[h,f]of Object.entries(r))f!=null&&f!==!1&&d.setAttribute(h,f===!0?"":f);return t!==void 0&&(d.textContent=t),d}var kt=o=>({load:()=>fe(o.document),async token(c){return await je(c,{win:o}),c.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset:c=>Fe(c,{win:o})});function Ht(o,c={}){if(o.pmwChat)return o.pmwChat;let r=c.win??window,t=r.document,d=c.WebSocket??r.WebSocket,h=c.fetch??((...e)=>r.fetch(...e)),f=c.turnstile??kt(r),q=c.uuid??(()=>r.crypto.randomUUID()),Z=c.shrink??(e=>bt(r,e)),I=c.preview??(e=>new Promise((n,a)=>{let l=new r.FileReader;l.onload=()=>n(String(l.result)),l.onerror=()=>a(l.error),l.readAsDataURL(e)})),v={};try{v=JSON.parse(o.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let z=Q(v.lang),i=me(z,v.strings),ge=o.dataset.pmwChatOpener??"",Ye=o.querySelector(".pmw-chat__bubble"),w=c.demo??(v.demo===!0||!Be({doc:t,hostname:r.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(s(t,"style",{id:"pmw-chat-style"},He));let b=s(t,"div",{class:"pmw-chat__panel",role:"dialog","aria-modal":"true","aria-labelledby":"pmw-chat-name",hidden:!0}),_e=s(t,"div",{class:"pmw-chat__head"}),ee=o.querySelector(".pmw-chat__avatar")?.cloneNode(!0)??s(t,"span",{class:"pmw-chat__avatar","aria-hidden":"true"});for(let e of ee.querySelectorAll("[id]")){let n=e.id;e.id=`${n}-head`;for(let a of ee.querySelectorAll(`[mask="url(#${n})"]`))a.setAttribute("mask",`url(#${n}-head)`)}let be=s(t,"div",{class:"pmw-chat__who"}),xe=s(t,"p",{class:"pmw-chat__name",id:"pmw-chat-name"}),H=s(t,"p",{class:"pmw-chat__role"}),ve=s(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),D=s(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});D.innerHTML=St,be.append(xe,H),_e.append(ee,be,ve,D);let te=s(t,"p",{class:"pmw-chat__notice"},i.automated);v.privacyHref&&(te.append(" "),te.append(s(t,"a",{href:v.privacyHref},i.privacy)));let x=s(t,"ol",{class:"pmw-chat__log"}),Ge=s(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},ge),L=s(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});L.append(s(t,"span"),s(t,"span"),s(t,"span")),x.append(Ge);let ne=s(t,"p",{class:"pmw-chat__state",role:"status"}),ye=s(t,"p",{class:"pmw-chat__sr","aria-live":"polite"}),Se=e=>s(t,"div",{class:"cf-turnstile pmw-turnstile","data-sitekey":v.siteKey??"","data-action":"chat","data-theme":"light","data-language":z,"data-appearance":"interaction-only","data-size":"flexible"}),m=s(t,"form",{class:"pmw-chat__composer",novalidate:!0}),y=s(t,"textarea",{id:"pmw-chat-input",name:"message",rows:"2",maxlength:String(2e3),autocomplete:"off",disabled:w,"aria-describedby":w?"pmw-chat-demo-hint":null}),Qe=s(t,"button",{type:"submit",class:"pmw-chat__send",disabled:w},i.send);m.append(s(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),y,Qe),w?m.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint)):m.append(Se(m));let P=null,C=s(t,"p",{class:"pmw-chat__error pmw-chat__photo-error",role:"alert"});if(v.photos===!0){let e=s(t,"label",{class:"pmw-chat__attach"});P=s(t,"input",{type:"file",class:"pmw-chat__sr",accept:"image/*",multiple:!0,"aria-describedby":"pmw-chat-photo-hint",disabled:w}),e.innerHTML=_t,e.prepend(P),e.append(s(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),m.insertBefore(e,y),m.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),C)}let U=s(t,"div",{class:"pmw-chat__ended",hidden:!0}),ke=s(t,"p",{},i.ended),B=s(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);U.append(ke,B);let g=s(t,"form",{class:"pmw-chat__fallback",novalidate:!0,hidden:!0,"aria-labelledby":"pmw-chat-fb-title"}),Te=s(t,"p",{class:"pmw-chat__fb-intro"}),ae=(e,n,a)=>{let l=`pmw-chat-fb-${e}`,p=s(t,"div",{class:"pmw-chat__field"});return p.append(s(t,"label",{for:l},i[e]),s(t,n,{id:l,name:e,required:!0,...a})),p},$=s(t,"p",{class:"pmw-chat__error",role:"alert"});g.append(s(t,"p",{class:"pmw-chat__fb-title",id:"pmw-chat-fb-title"},i.fallbackTitle),Te,ae("name","input",{autocomplete:"name",maxlength:"100"}),ae("contact","input",{autocomplete:"on",maxlength:"200"}),ae("need","textarea",{rows:"3",maxlength:"2000"}),$,s(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),w||g.append(Se(g));let Le=s(t,"div",{class:"pmw-chat__body"});Le.append(te,x,ne,U,g),b.append(_e,Le,m,ye),o.append(b);let u=null;if(!w)try{u=JSON.parse(r.sessionStorage.getItem(we)??"null")}catch{u=null}let Ce=()=>{try{r.sessionStorage.setItem(we,JSON.stringify(u))}catch{}},_=null,oe=0,re=null,R=null,j=0,F="bot",J=null,E=!1,Ee=0,se=null,ie=new Set,k=new Map,V=new Set,ce=0,le=()=>u?.botName||null;function K(){let e=F==="human";xe.textContent=e?J||i.assistant:le()||i.assistant,H.textContent=e?i.live:le()?i.assistant:"",H.hidden=H.textContent==="",ve.hidden=!e,b.dataset.mode=F}K();let T=e=>{ne.textContent=e??"",ne.hidden=!e};T("");let Ae=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||J||i.assistant:le()||i.assistant;function A(e,n){let a=s(t,"li",{class:`pmw-chat__msg pmw-chat__msg--${n}`,"data-id":e}),l=s(t,"span",{class:"pmw-chat__sr"}),p=s(t,"span",{class:"pmw-chat__text"});return a.append(l,p),x.insertBefore(a,L.parentNode===x?L:null),a}let Oe=e=>[...x.querySelectorAll("li[data-id]")].find(n=>n.dataset.id===e),O=(e,n,a)=>{e.firstChild.textContent=W(i.said,{name:Ae(n),text:""}),e.lastChild.textContent=a===""&&n.photos?.length&&!e.querySelector("img")?i.photoSent:a};function pe(){L.hidden=!0,r.clearTimeout(se)}function Me(e,{announce:n}){if(ie.has(e.seq))return;ie.add(e.seq),j=Math.max(j,e.seq);let a=Oe(e.id);a||(a=A(e.id,e.role)),a.classList.remove("is-streaming","is-pending","is-failed"),a.querySelector(".pmw-chat__retry")?.remove(),O(a,e,e.text),e.role==="visitor"?(k.delete(e.id),e.photos?.length&&V.add(e.id)):(pe(),n&&(ye.textContent=W(i.said,{name:Ae(e),text:e.text})),b.hidden&&o.setAttribute("data-unread","")),M()}function We(e,n){pe();let a=Oe(e);a||(a=A(e,"bot"),a.classList.add("is-streaming"),O(a,{role:"bot"},"")),a.lastChild.textContent+=n,M()}let M=()=>{x.scrollTop=x.scrollHeight};w&&(O(A("demo-question","visitor"),{role:"visitor"},yt(ge,z)),O(A("demo-answer","bot"),{role:"bot"},i.demoAnswer));function X(e){if(e.failed=!0,e.li.classList.remove("is-pending"),e.li.classList.add("is-failed"),e.li.querySelector(".pmw-chat__retry"))return;let n=s(t,"span",{class:"pmw-chat__retry"},`${i.notSent} `),a=s(t,"button",{type:"button"},i.retry);a.addEventListener("click",()=>{e.failed=!1,e.tries=0,n.remove(),e.li.classList.replace("is-failed","is-pending"),u?Y()?de(e):N():ue()}),n.append(a),e.li.append(n)}let Y=()=>!!_&&_.readyState===1;function de(e){if(!(e.failed||!Y())){if(e.tries>=Xe)return X(e);e.tries++,_.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function N(){if(!u||E||_)return;r.clearTimeout(re);let e=r.location.protocol==="https:"?"wss:":"ws:",n=new URLSearchParams({c:u.c,k:u.k}),a=new d(`${e}//${r.location.host}/api/chat/ws?${n}`);_=a,a.onopen=()=>{oe=0,T(""),a.send(JSON.stringify({type:"hello",since:j}));for(let l of k.values())de(l)},a.onmessage=l=>{let p;try{p=JSON.parse(l.data)}catch{return}et(p)},a.onclose=l=>{_===a&&(_=null);for(let S of k.values())S.tries>=Xe&&X(S);if(l.code>=4e3&&l.code<5e3)return Ze(l.code);if(E)return;T(i.reconnecting);let p=Ke[Math.min(oe,Ke.length-1)];oe++,re=r.setTimeout(N,p)}}function Ze(e){if(e===Je.rpc)return G(i.reload,!1);G(i.ended,!0)}function G(e,n){E=!0,r.clearTimeout(re),T(""),ke.textContent=e,B.hidden=!n,U.hidden=!1,m.hidden=!0;for(let a of k.values())X(a)}function Ne(e){F=e.mode==="human"?"human":"bot",J=typeof e.ownerName=="string"?e.ownerName:null,K(),e.closed&&G(i.ended,!0)}function et(e){switch(e?.type){case"history":for(let n of e.messages??[])Me(n,{announce:!1});return Ne(e);case"message":return Me(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return We(e.id,e.text);case"typing":return e.role==="visitor"?void 0:(L.hidden=!1,x.append(L),r.clearTimeout(se),se=r.setTimeout(pe,gt),M());case"status":return Ne(e);case"fallback":return Ie(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Re();case"error":(e.code==="closed"||e.code==="deleted")&&G(i.ended,!0)}}let Pe=!1;function Re(){if(!(Pe||u?.lead)){Pe=!0,u&&(u.lead=!0,Ce());try{r.pmw?.event(ut)}catch{}}}async function qe(e,n){let a="";try{a=await f.token(e)}catch{a=""}let l,p=null;try{l=await h("/api/chat/start",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:a,lang:z,page:r.location.pathname,...n})}),p=await l.json()}catch{l=null}try{f.reset(e)}catch{}return l?.ok&&p?{ok:!0,data:p}:{ok:!1,error:l?.status===403?i.turnstile:l?.status===429?i.tooMany:null}}function ue(){return R||u||(T(i.connecting),R=qe(m,{}).then(e=>{if(R=null,e.ok&&e.data.conversationId&&e.data.visitorKey){u={c:e.data.conversationId,k:e.data.visitorKey,botName:typeof e.data.botName=="string"?e.data.botName:null},Ce(),K(),N();return}for(let n of k.values())X(n);if(e.error)return T(e.error);T(""),Ie(i.unavailable)})),R}function Ie(e){Te.textContent=e,g.hidden=!1,m.hidden=!0,g.querySelector("input")?.focus()}g.addEventListener("submit",async e=>{e.preventDefault();let n=p=>g.elements[p].value.trim(),a={name:n("name"),contact:n("contact"),need:n("need")};if(!a.name||!a.contact||!a.need){$.textContent=i.fallbackMissing;return}$.textContent="";let l=await qe(g,{fallbackLead:a});if(!l.ok){$.textContent=l.error??i.unavailable;return}g.replaceChildren(s(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Re()});let tt={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function nt(e){let n,a;try{n=await Z(e),a=await I(n)}catch{C.textContent=i.photoUnreadable;return}let l=q(),p=A(l,"visitor");p.classList.add("is-pending"),p.insertBefore(s(t,"img",{class:"pmw-chat__photo",src:a,alt:i.photoAlt}),p.lastChild),O(p,{role:"visitor"},""),M(),u||await ue();let S=null;if(u){let rt=new URLSearchParams({c:u.c,k:u.k,id:l});try{S=await h(`/api/chat/photo?${rt}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:n})}catch{S=null}}if(S?.ok){p.classList.remove("is-pending"),V.add(l),_||N();return}p.remove(),C.textContent=i[tt[S?.status]]??i.photoFailed}P?.addEventListener("change",async()=>{let e=[...P.files??[]];if(P.value="",C.textContent="",E||w||e.length===0)return;let n=ht-V.size-ce;e.length>n&&(C.textContent=i.photoLimit);let a=e.slice(0,Math.max(0,n));ce+=a.length;for(let l of a)try{await nt(l)}finally{ce--}});function ze(){let e=y.value.trim();if(!e||E||w)return;y.value="";let n={id:q(),text:e.slice(0,2e3),tries:0,failed:!1};n.li=A(n.id,"visitor"),n.li.classList.add("is-pending"),O(n.li,{role:"visitor"},n.text),k.set(n.id,n),M(),u?Y()?de(n):N():ue()}m.addEventListener("submit",e=>{e.preventDefault(),ze()}),y.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),ze())}),y.addEventListener("input",()=>{let e=Date.now();Y()&&e-Ee>wt&&(Ee=e,_.send(JSON.stringify({type:"typing"})))}),B.addEventListener("click",()=>{u=null;try{r.sessionStorage.removeItem(we)}catch{}try{_?.close(1e3)}catch{}_=null,E=!1,j=0,ie.clear(),k.clear(),V.clear(),C.textContent="",F="bot",J=null,K();for(let e of[...x.querySelectorAll("li[data-id]")])e.remove();U.hidden=!0,m.hidden=!1,y.focus()});let at=()=>[...b.querySelectorAll('a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])')].filter(e=>!e.disabled&&!e.closest("[hidden]"));b.addEventListener("keydown",e=>{if(e.key==="Escape"){e.preventDefault(),he();return}if(e.key!=="Tab")return;let n=at();if(n.length===0)return;let a=n[0],l=n[n.length-1];e.shiftKey&&t.activeElement===a?(e.preventDefault(),l.focus()):!e.shiftKey&&t.activeElement===l&&(e.preventDefault(),a.focus())}),D.addEventListener("click",()=>he());function ot(){w||f.load(),b.hidden=!1,o.setAttribute("data-open",""),o.removeAttribute("data-unread"),u&&N(),(w?D:m.hidden?g.hidden?B:g.querySelector("input"):y)?.focus(),M()}function he(){b.hidden=!0,o.removeAttribute("data-open"),Ye?.focus()}return o.pmwChat={open:ot,close:he,panel:b},o.pmwChat}export{Ke as BACKOFF_MS,ut as GOAL,ht as MAX_PHOTOS,Xe as MAX_TRIES,ft as PHOTO_EDGE,mt as PHOTO_QUALITY,we as SESSION_KEY,yt as demoQuestion,Ht as mountChat};
1
+ var 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};
package/manifest.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "category": "visitors",
7
7
  "icon": "message-circle",
8
8
  "load": "interaction",
9
- "inlineBudget": 2048,
9
+ "inlineBudget": 2304,
10
10
  "preview": "test",
11
11
  "activeWhen": ["enabled"],
12
12
  "config": {
@@ -33,8 +33,17 @@
33
33
  },
34
34
  "markup": { "pages": "all", "required": ["#pmw-chat", "[data-pmw-chat-opener]"] },
35
35
  "build": ["plugins/chat/context.json"],
36
- "portal": { "settings": [
37
- { "key": "botName", "kind": "text", "editor": "client",
36
+ "portal": {
37
+ "screens": [
38
+ { "key": "appearance",
39
+ "title": { "fr": "Apparence de l’assistant", "en": "Assistant’s appearance" },
40
+ "summary": { "fr": "Le visage, la couleur et le nom que vos visiteurs voient dans la bulle de discussion.", "en": "The face, colour and name your visitors see in the chat bubble." },
41
+ "crumb": { "fr": "Apparence", "en": "Appearance" },
42
+ "views": [ { "key": "bubble", "label": { "fr": "Bulle", "en": "Bubble" } },
43
+ { "key": "panel", "label": { "fr": "Panneau ouvert", "en": "Open panel" } } ] }
44
+ ],
45
+ "settings": [
46
+ { "key": "botName", "kind": "text", "editor": "client", "screen": "appearance", "applies": "publish",
38
47
  "label": { "fr": "Nom de l’assistant", "en": "Assistant’s name" },
39
48
  "help": { "fr": "Affiché avec la mention « Assistant virtuel », pour que personne ne le prenne pour un employé.", "en": "Shown with “Virtual assistant”, so nobody takes it for an employee." } },
40
49
  { "key": "ownerName", "kind": "text", "editor": "client",
@@ -48,15 +57,21 @@
48
57
  "help": { "fr": "Texto court à chaque demande, si les textos sont activés.", "en": "A short text for each request, when texts are on." } },
49
58
  { "key": "consignes", "kind": "longtext",
50
59
  "label": { "fr": "Consignes", "en": "Instructions" },
51
- "help": { "fr": "Heures, territoire, sujets à éviter; le contenu du site y est déjà.", "en": "Hours, service area, topics to avoid; the site’s content is already known." } },
52
- { "key": "dailyCap", "kind": "money", "required": true,
53
- "label": { "fr": "Plafond quotidien", "en": "Daily cap" },
54
- "help": { "fr": "Au-delà, le site affiche le formulaire de contact jusqu’à minuit.", "en": "Past it, the site shows the contact form until midnight." } }
60
+ "help": { "fr": "Heures, territoire, sujets à éviter; le contenu du site y est déjà.", "en": "Hours, service area, topics to avoid; the site’s content is already known." } }
55
61
  ],
56
62
  "config": [
57
63
  { "key": "enabled", "kind": "boolean", "editor": "client",
58
64
  "label": { "fr": "Afficher l’assistant sur votre site", "en": "Show the assistant on your site" },
59
- "help": { "fr": "Désactivé\u00a0: l’assistant disparaît du site; vos conversations et réglages sont gardés.", "en": "Off: the assistant leaves your site; your conversations and settings are kept." } }
65
+ "help": { "fr": "Désactivé\u00a0: l’assistant disparaît du site; vos conversations et réglages sont gardés.", "en": "Off: the assistant leaves your site; your conversations and settings are kept." } },
66
+ { "key": "avatar", "kind": "choice", "editor": "client", "screen": "appearance",
67
+ "label": { "fr": "Visage", "en": "Face" },
68
+ "groups": [
69
+ { "key": "photos", "label": { "fr": "Photos réalistes", "en": "Realistic photos" }, "help": { "fr": "Des visages créés pour PmeWeb, pas des personnes réelles.", "en": "Faces made for PmeWeb, not real people." }, "count": 5, "image": "avatars/photos-{n}.webp", "optionLabel": { "fr": "Visage {n}", "en": "Face {n}" } },
70
+ { "key": "illustration", "label": { "fr": "Illustration", "en": "Illustration" }, "help": { "fr": "Des visages créés pour PmeWeb, pas des personnes réelles.", "en": "Faces made for PmeWeb, not real people." }, "count": 5, "image": "avatars/illustration-{n}.webp", "optionLabel": { "fr": "Visage {n}", "en": "Face {n}" } },
71
+ { "key": "sans-visage", "label": { "fr": "Sans visage", "en": "No face" }, "help": { "fr": "Un dessin de bulle, sur une couleur de votre site.", "en": "A bubble drawing, on one of your site’s colours." }, "count": 5, "image": "avatars/sans-visage-{n}.svg", "optionLabel": { "fr": "Icône {n}", "en": "Icon {n}" }, "tint": "avatarBg" } ] },
72
+ { "key": "avatarBg", "kind": "color", "editor": "client", "screen": "appearance",
73
+ "label": { "fr": "Couleur de fond", "en": "Background colour" },
74
+ "default": { "design": "accent" }, "showWhen": { "key": "avatar", "group": "sans-visage" } }
60
75
  ] },
61
76
  "rpc": { "service": "pmeweb-chat", "entrypoint": "ChatApi", "version": 1, "accepts": "pmeweb-chat/plugin-rpc.json" },
62
77
  "health": true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmeweb/plugin-chat",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "PmeWeb chat plugin: the bubble, the lazy chat panel, the site Worker's /api/chat routes and the build-time context file. The conversations, the model and the alerts live in the pmeweb-chat service.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/Chat.astro CHANGED
@@ -46,6 +46,9 @@ const show = enabled || process.env.PMEWEB_PORTAL_PREVIEW === '1';
46
46
  const lang = langOf(rawLang);
47
47
  const s = stringsFor(lang, strings);
48
48
  if (typeof opener !== 'string' || opener.trim() === '') throw new Error('chat : pas d’accroche pour cette page');
49
+ // At most OPENER_MAX characters (operator, 2026-11-13): two lines on a phone, never more.
50
+ const OPENER_MAX = 50;
51
+ if ([...opener.trim()].length > OPENER_MAX) throw new Error(`chat : accroche de ${[...opener.trim()].length} caractères, ${OPENER_MAX} au plus : « ${opener.trim()} »`);
49
52
  if (typeof siteKey !== 'string' || siteKey === '') throw new Error('chat : la clé de site Turnstile manque');
50
53
 
51
54
  const avatar = String(config.avatar ?? '');
package/src/chat.css CHANGED
@@ -12,6 +12,19 @@
12
12
  display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
13
13
  font-family: var(--pmw-chat-font, inherit); font-size: 16px; line-height: 1.4;
14
14
  color: var(--pmw-chat-fg, #1f2328);
15
+ /* `bottom` eases when a site lifts the launcher above its sticky bar (--pmw-chat-bottom). */
16
+ transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s, bottom 0.3s ease;
17
+ /* The root's own box (the gap, a faded opener's kept space) never catches a tap meant for the page. */
18
+ pointer-events: none;
19
+ }
20
+ .pmw-chat > * { pointer-events: auto; }
21
+ /* Out of the footer's way: the trigger makes the root `inert` while the footer (or
22
+ [data-pmw-chat-avoid]) is in view, never while the panel is open or focus is inside. It fades
23
+ and slides 8 px down, then turns `visibility: hidden` once the fade is over; it comes back at
24
+ once. Opacity and transform only: nothing moves on the page. */
25
+ .pmw-chat[inert]:not([data-open]):not(:focus-within) {
26
+ opacity: 0; transform: translateY(8px); visibility: hidden;
27
+ transition: opacity 0.2s ease, transform 0.2s ease, visibility 0s 0.2s, bottom 0.3s ease;
15
28
  }
16
29
  .pmw-chat button { font: inherit; cursor: pointer; }
17
30
  .pmw-chat button:focus-visible {
@@ -40,7 +53,8 @@
40
53
  .pmw-chat__avatar svg { display: block; width: 100%; height: 100%; }
41
54
  .pmw-chat__label { font-size: 15px; font-weight: 600; white-space: nowrap; }
42
55
 
43
- /* The opener: once per visit, after 20 s, at 30 % of the page, or on exit intent (trigger.js). */
56
+ /* The opener: once per visit, after 20 s, at 30 % of the page, or on exit intent (trigger.js), never
57
+ while the footer is in view. */
44
58
  .pmw-chat__teaser[hidden] { display: none; }
45
59
  .pmw-chat__teaser {
46
60
  position: relative; max-width: min(300px, calc(100vw - 32px));
@@ -57,9 +71,30 @@
57
71
  }
58
72
  .pmw-chat__teaser-close svg { width: 16px; height: 16px; }
59
73
  @keyframes pmw-chat-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
74
+ @keyframes pmw-chat-out { to { opacity: 0; visibility: hidden; } }
75
+
76
+ /* A phone (under 640 px, where the panel goes full screen): the bubble is a 56 px round button,
77
+ the face alone. « Assistant virtuel » stays in the DOM, visually hidden; the button's
78
+ aria-label still names it. The opener fades out by itself 8 s after it appears (its space is
79
+ kept, above the bubble, so nothing moves). */
80
+ @media (max-width: 639.98px) {
81
+ .pmw-chat__bubble {
82
+ position: relative; box-sizing: border-box; width: 56px; height: 56px;
83
+ padding: 0; justify-content: center;
84
+ }
85
+ .pmw-chat__label {
86
+ position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
87
+ clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0;
88
+ }
89
+ .pmw-chat__teaser { animation: pmw-chat-in 0.22s ease both, pmw-chat-out 0.2s ease 8s forwards; }
90
+ }
60
91
  @media (prefers-reduced-motion: reduce) {
92
+ .pmw-chat, .pmw-chat[inert]:not([data-open]):not(:focus-within) { transition: none; }
61
93
  .pmw-chat__teaser { animation: none; }
62
94
  }
95
+ @media (prefers-reduced-motion: reduce) and (max-width: 639.98px) {
96
+ .pmw-chat__teaser { animation: pmw-chat-out 0s 8s forwards; }
97
+ }
63
98
  /* While the panel is open the bubble and the opener step aside. */
64
99
  .pmw-chat[data-open] .pmw-chat__bubble, .pmw-chat[data-open] .pmw-chat__teaser { display: none; }
65
100
  @media print { .pmw-chat { display: none; } }
package/src/colors.js CHANGED
@@ -59,41 +59,65 @@ export function avatarTheme(avatarBg, colors = {}) {
59
59
  }
60
60
 
61
61
  const ERRORS = ['#b42318', '#ffb4ab']
62
+ /** The « Vous répond en direct » line and the takeover dot: a dark green on light, a light one on dark. */
63
+ const LIVES = ['#1a7f37', '#3fb950']
64
+ const DEFAULT_FG = '#1f2328'
62
65
 
63
66
  /** `fg` laid over `bg` at `alpha`, as an opaque #rrggbb. */
64
- function mix(fg, bg, alpha) {
67
+ export function mix(fg, bg, alpha) {
65
68
  const ch = (h, i) => parseInt(h.slice(i, i + 2), 16)
66
69
  return `#${[1, 3, 5].map((i) => Math.round(ch(fg, i) * alpha + ch(bg, i) * (1 - alpha)).toString(16).padStart(2, '0')).join('')}`
67
70
  }
68
71
 
69
72
  /**
70
- * The panel (and the bubble beside it) in the site's colours, two groups each all-or-nothing:
71
- * - primary: the header, the visitor's bubbles and the send button, text white or ink at 4.5:1;
72
- * - surface: the panel and the bubble in `surface`, text in `ink` at 4.5:1, the bot's bubbles in
73
- * `background`, and the colours that sit on them (muted, link, focus, error) re-derived so each
74
- * keeps its minimum. A group whose pair fails sets nothing: the package defaults stay.
73
+ * The panel (and the bubble beside it) in the site's colours, three groups each all-or-nothing:
74
+ * - primary: the visitor's bubbles and the send button, text white or ink at 4.5:1;
75
+ * - surface: the panel, its header and the bot's bubbles in `surface`, text in `ink` at 4.5:1; the
76
+ * thread behind the bubbles a light tint of the accent (else primary, else ink) over surface (the
77
+ * surface itself when the tint would cost a pair); the lines a stronger tint (decoration); and the colours that sit on the
78
+ * surface AND the thread (muted, link, live, error, focus) re-derived so each keeps its minimum;
79
+ * - owner: the bubbles of a person who took over, a tint of the accent (else primary) over the
80
+ * panel, text at 4.5:1; failing that, with a themed surface, the panel's own colour.
81
+ * A group whose pair fails sets nothing: the package defaults stay.
75
82
  */
76
83
  export function panelTheme(colors = {}) {
77
84
  const v = {}
78
- const { primary, ink, surface, background } = colors
85
+ const { primary, accent, ink, surface, background } = colors
79
86
  if (primary) {
80
87
  const fg = textOn(primary, ink, 4.5)
81
88
  if (fg) Object.assign(v, { '--pmw-chat-accent': primary, '--pmw-chat-accent-fg': fg })
82
89
  }
83
- if (surface && ink && contrast(ink, surface) >= 4.5) {
90
+ const themed = !!(surface && ink && contrast(ink, surface) >= 4.5)
91
+ const hue = accent ?? primary
92
+ if (themed) {
93
+ const tint = mix(hue ?? ink, surface, 0.04)
94
+ const thread = contrast(ink, tint) >= 4.5 ? tint : surface
95
+ const both = (c, min = 4.5) => contrast(c, surface) >= min && contrast(c, thread) >= min
84
96
  const muted = mix(ink, surface, 0.75)
85
97
  const page = background ?? WHITE
86
98
  Object.assign(v, {
87
99
  '--pmw-chat-panel-bg': surface,
88
100
  '--pmw-chat-bubble-bg': surface,
89
101
  '--pmw-chat-fg': ink,
90
- '--pmw-chat-other-bg': background && contrast(ink, background) >= 4.5 ? background : surface,
91
- '--pmw-chat-muted': contrast(muted, surface) >= 4.5 ? muted : ink,
92
- '--pmw-chat-link': primary && contrast(primary, surface) >= 4.5 ? primary : ink,
93
- '--pmw-chat-focus': contrast(ink, page) >= 3 ? ink : INK_FALLBACK,
94
- '--pmw-chat-error': ERRORS.find((e) => contrast(e, surface) >= 4.5) ?? ink,
102
+ '--pmw-chat-other-bg': surface,
103
+ '--pmw-chat-thread-bg': thread,
104
+ '--pmw-chat-line': mix(hue ?? ink, surface, 0.1),
105
+ '--pmw-chat-rule': mix(hue ?? ink, surface, 0.2),
106
+ '--pmw-chat-muted': both(muted) ? muted : ink,
107
+ '--pmw-chat-link': primary && both(primary) ? primary : ink,
108
+ '--pmw-chat-live': LIVES.find((c) => both(c)) ?? ink,
109
+ '--pmw-chat-focus': contrast(ink, page) >= 3 && both(ink, 3) ? ink : INK_FALLBACK,
110
+ '--pmw-chat-error': ERRORS.find((e) => both(e)) ?? ink,
95
111
  })
96
112
  }
113
+ const base = themed ? surface : WHITE
114
+ const text = themed ? ink : DEFAULT_FG
115
+ const owner = hue ? mix(hue, base, 0.08) : null
116
+ if (owner && contrast(text, owner) >= 4.5) {
117
+ Object.assign(v, { '--pmw-chat-owner-bg': owner, '--pmw-chat-owner-border': mix(hue, base, 0.22) })
118
+ } else if (themed) {
119
+ Object.assign(v, { '--pmw-chat-owner-bg': surface, '--pmw-chat-owner-border': v['--pmw-chat-rule'] })
120
+ }
97
121
  return v
98
122
  }
99
123