@pmeweb/plugin-chat 0.0.0-stage → 1.1.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 +130 -0
- package/LICENSE-LUCIDE +19 -0
- package/README.md +131 -2
- package/avatars/illustration-1.webp +0 -0
- package/avatars/illustration-2.webp +0 -0
- package/avatars/illustration-3.webp +0 -0
- package/avatars/illustration-4.webp +0 -0
- package/avatars/illustration-5.webp +0 -0
- package/avatars/photos-1.webp +0 -0
- package/avatars/photos-2.webp +0 -0
- package/avatars/photos-3.webp +0 -0
- package/avatars/photos-4.webp +0 -0
- package/avatars/photos-5.webp +0 -0
- package/avatars/sans-visage-1.svg +4 -0
- package/avatars/sans-visage-2.svg +3 -0
- package/avatars/sans-visage-3.svg +4 -0
- package/avatars/sans-visage-4.svg +4 -0
- package/avatars/sans-visage-5.svg +4 -0
- package/dist/trigger.iife.js +1 -0
- package/dist/widget.js +2 -0
- package/manifest.json +82 -0
- package/package.json +48 -4
- package/privacy/en.json +20 -0
- package/privacy/fr.json +20 -0
- package/src/Chat.astro +110 -0
- package/src/build.d.ts +30 -0
- package/src/build.js +87 -0
- package/src/chat.css +65 -0
- package/src/colors.js +104 -0
- package/src/csp.d.ts +2 -0
- package/src/csp.js +5 -0
- package/src/faces.js +24 -0
- package/src/protocol.js +53 -0
- package/src/protocol.ts +113 -0
- package/src/strings.js +114 -0
- package/src/trigger-entry.js +3 -0
- package/src/trigger.js +78 -0
- package/src/widget.css +108 -0
- package/src/widget.js +616 -0
- package/src/worker.d.ts +62 -0
- package/src/worker.js +273 -0
package/INSTALL.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Installing @pmeweb/plugin-chat
|
|
2
|
+
|
|
3
|
+
Through the `install-plugin` skill only. Ask the operator: which face (`avatar`: `photos-<n>`,
|
|
4
|
+
`illustration-<n>`, `sans-visage-<n>` or `logo`, the list in `src/faces.js`), whether texts are on (`sms: "voipms"`, pilot on pmeweb.ca only until a dedicated SMS number
|
|
5
|
+
exists), and the portal settings in step 12. Never a legal specific: the privacy text is the
|
|
6
|
+
package's, approved once.
|
|
7
|
+
|
|
8
|
+
1. **Before**: the site has `src/data/hosts.ts` and `<PluginHosts>` in `Base.astro`, a Managed
|
|
9
|
+
Turnstile key in `src/data/turnstile.ts` whose hostnames include the production domain,
|
|
10
|
+
`pluginArtifacts(request, env)` in the worker before the assets (boilerplate), `Disallow:
|
|
11
|
+
/plugins/` in `robots.txt`, and `<PluginPrivacy>` (`@pmeweb/consent`) on the privacy page of
|
|
12
|
+
every language.
|
|
13
|
+
2. **Dependencies**, exact versions, through the shared set (`node scripts/bump.mjs`):
|
|
14
|
+
`@pmeweb/plugin-chat`, `@pmeweb/plugin-kit` (1.1.0 or later). `pmeweb.json`:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
"plugins": { "chat": { "enabled": false, "provider": "workers-ai", "avatar": "photos-3" } }
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`enabled` is the client's switch (« Afficher l’assistant sur votre site », the first field of the
|
|
21
|
+
module screen, shipped by « Publier »). Installed OFF: anything but `true` puts nothing on the
|
|
22
|
+
live site (no markup, no script, no CSS), the routes answer 403 `disabled`, no privacy section,
|
|
23
|
+
health says `disabled`. A « Contenu » preview still shows the demo. Never a URL flag.
|
|
24
|
+
|
|
25
|
+
plus `"sms": "voipms"` and/or `"photos": true` when chosen, `"avatarBg": "#rrggbb"` (lowercase) for
|
|
26
|
+
a `sans-visage` icon whose circle should not be the site's accent, and the goal:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{ "key": "chat-lead", "label": { "fr": "Demandes laissées au clavardage", "en": "Requests left in the chat" } }
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
3. **`wrangler.jsonc`**:
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
"services": [{ "binding": "CHAT", "service": "pmeweb-chat", "entrypoint": "ChatApi" }],
|
|
36
|
+
"ratelimits": [
|
|
37
|
+
{ "name": "CHAT_LIMIT", "namespace_id": "<unused on the account>", "simple": { "limit": 20, "period": 60 } },
|
|
38
|
+
{ "name": "CHAT_START_LIMIT", "namespace_id": "<another unused on the account>", "simple": { "limit": 3, "period": 60 } }
|
|
39
|
+
]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Both count per IP, not messages. `CHAT_START_LIMIT` counts conversation starts (3 a minute);
|
|
43
|
+
`CHAT_LIMIT` counts socket (re)connections, photo uploads and the contact form's posts. pmeweb-chat
|
|
44
|
+
caps the rest: 15 visitor messages per conversation, 10 conversations per visitor IP per Quebec
|
|
45
|
+
day (it receives the IP from `handleChat` in `x-pmw-visitor-ip`), and the site's daily spend.
|
|
46
|
+
**Updating from 1.0.0**: add the `CHAT_START_LIMIT` entry and `CHAT_START_LIMIT: RateLimit` to
|
|
47
|
+
`Env` (check-plugins refuses the update without it); nothing else changes on the site.
|
|
48
|
+
4. [ASK] `npx wrangler secret put TURNSTILE_SECRET` (skip when the contact form already set it;
|
|
49
|
+
`npx wrangler secret list` to check).
|
|
50
|
+
5. **Worker** (`worker/index.ts`), before `pluginArtifacts` and the assets:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { handleChat, health as chatHealth } from '@pmeweb/plugin-chat';
|
|
54
|
+
import { cspHosts as chatCsp } from '@pmeweb/plugin-chat/csp';
|
|
55
|
+
// ...
|
|
56
|
+
if (url.pathname.startsWith('/api/chat/') && plugins.chat && env.CHAT) {
|
|
57
|
+
// NOT wrapped in withSecurityHeaders: /api/chat/ws answers a 101 whose socket a rebuilt
|
|
58
|
+
// Response would drop. The JSON answers carry nosniff and no-store already.
|
|
59
|
+
return handleChat(request, env, ctx, { hosts: CANONICAL_HOSTS, config: plugins.chat, lang: 'fr' });
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Health: `...(plugins.chat ? { chat: (e) => chatHealth(e, { host: CANONICAL_HOSTS[0], config: plugins.chat }) } : {})`
|
|
64
|
+
in the object passed to `pluginHealth` (pmeweb-chat finds the client's settings by host).
|
|
65
|
+
CSP: `mergeCsp(policy, chatCsp())` (Turnstile's script and frame; the socket is same-origin).
|
|
66
|
+
With `"photos": true` the panel's thumbnail is a `data:` URL: `img-src` keeps `data:` (the
|
|
67
|
+
boilerplate's does).
|
|
68
|
+
`Env` gains `CHAT: Fetcher`, `CHAT_LIMIT: RateLimit` and `CHAT_START_LIMIT: RateLimit`.
|
|
69
|
+
6. **Openers**, written by Claude Code at install, in every dictionary (`src/i18n/<lang>.ts`),
|
|
70
|
+
strings only, OQLF typography, `docs/AVOIDING_AI_TELLS.md`:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
chat: {
|
|
74
|
+
openers: { '/': 'Une question sur nos toitures?', '/services/': '...' }, // one per built page
|
|
75
|
+
defaultOpener: 'Une question sur « {titre} »?', // collection pages
|
|
76
|
+
},
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A collection whose `route` is a page path gets `defaultOpener` with the entry's title; a `#`
|
|
80
|
+
route is covered by its page's opener. A page with neither fails the build (the component
|
|
81
|
+
throws « pas d’accroche ») and the gate (`markup.required`).
|
|
82
|
+
7. **`Base.astro`**, once, near the end of `<body>`:
|
|
83
|
+
|
|
84
|
+
```astro
|
|
85
|
+
{plugins.chat && <Chat config={plugins.chat} lang={lang} siteKey={TURNSTILE_SITE_KEY} opener={opener}
|
|
86
|
+
logo={logoUrl} site={site} privacyHref={lang === 'en' ? '/en/privacy/' : '/confidentialite/'} />}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`import Chat from '@pmeweb/plugin-chat/Chat.astro'`. `site` is `pmeweb.json` (Base.astro already
|
|
90
|
+
imports it for the consent card): the bubble and the panel take their colours from
|
|
91
|
+
`design.colors`. `opener` is `t.chat.openers[path]`, else
|
|
92
|
+
the default filled with the page's title; `logo` only for `avatar: "logo"` (an `?url` import of
|
|
93
|
+
the site's logo). The panel links to `privacyHref`.
|
|
94
|
+
8. **The context file**, `src/pages/plugins/chat/context.json.ts`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { chatContext } from '@pmeweb/plugin-chat/build';
|
|
98
|
+
import { fr } from '../../../i18n/fr';
|
|
99
|
+
import { en } from '../../../i18n/en';
|
|
100
|
+
import { llmsText } from '../../../lib/llms'; // what src/pages/llms.txt.ts renders
|
|
101
|
+
import { pricingText } from '../../../lib/pricing'; // the source of public/pricing.md
|
|
102
|
+
export const prerender = true;
|
|
103
|
+
export const GET = async () => new Response(chatContext({
|
|
104
|
+
langs: {
|
|
105
|
+
fr: { business: 'Nom', llms: await llmsText('fr'), pricing: pricingText('fr'), dictionary: fr },
|
|
106
|
+
en: { business: 'Name', llms: await llmsText('en'), pricing: pricingText('en'), dictionary: en },
|
|
107
|
+
},
|
|
108
|
+
collections: [/* published entries: { collection, lang?, title, data, body } */],
|
|
109
|
+
}));
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The `lib/` names differ per site: read its `src/pages/llms.txt.ts` and pricing module and import
|
|
113
|
+
what they export (extract a function if the page inlines it). Never read `dist/` from a route.
|
|
114
|
+
Collections through `getPublished` only. A French-only site passes `fr` alone. Above 200 KB the
|
|
115
|
+
build fails.
|
|
116
|
+
9. **Privacy**: nothing to paste. `<PluginPrivacy>` renders the package's section (and the voip.ms
|
|
117
|
+
and photos paragraphs when those keys are set), only while `enabled` is true: it needs
|
|
118
|
+
`@pmeweb/consent` 1.1.0 or later (manifest `activeWhen`; 1.0.0 would print it switched off, and
|
|
119
|
+
check-plugins refuses that).
|
|
120
|
+
10. **Not in `pmeweb.json` `forms`**: the chat's Turnstile widget renders only once the panel is
|
|
121
|
+
open, so the portal's form-health browser check would find nothing on a page load.
|
|
122
|
+
11. `npm run build && node ../../a11y-tools/check-plugins.mjs .` until PASS; `npm run check`.
|
|
123
|
+
12. **Portal** (« Modules › Assistant virtuel »): PmeWeb sets `alertEmails` and `dailyCap`
|
|
124
|
+
(`alertEmails` is needed before the switch goes on: pmeweb-chat refuses a site without it), `alertPhone` if texts are on, and
|
|
125
|
+
« Consignes » with the client; the client may set `ownerName`, and changes the face, the icon's
|
|
126
|
+
colour and the name (`botName`) in « Contenu › Modules › Assistant virtuel › Apparence »
|
|
127
|
+
(applied by « Publier »).
|
|
128
|
+
13. Deploy (still off: nothing on the site, health `disabled`). Once the client or the operator
|
|
129
|
+
switches it on and publishes, from a real host: the bubble shows, a question gets an answer, the signed health
|
|
130
|
+
route answers `chat.ok`.
|
package/LICENSE-LUCIDE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
avatars/sans-visage-3.svg, sans-visage-4.svg and sans-visage-5.svg are Lucide icons
|
|
2
|
+
(message-circle-heart, message-circle-more, message-circle-question-mark; https://lucide.dev),
|
|
3
|
+
scaled into a 48 grid. They are used under Lucide's licence:
|
|
4
|
+
|
|
5
|
+
ISC License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
8
|
+
|
|
9
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
10
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
11
|
+
copyright notice and this permission notice appear in all copies.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
14
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
15
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
16
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
17
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
18
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
19
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,132 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pmeweb/plugin-chat
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A virtual assistant for a PmeWeb site: a bubble that answers visitors from the site's own content,
|
|
4
|
+
takes a request (name, a way to reach them, the need) one question at a time, alerts the owner by
|
|
5
|
+
email (and text), and lets the owner take over live.
|
|
6
|
+
|
|
7
|
+
Three pieces:
|
|
8
|
+
|
|
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
|
|
11
|
+
`/api/chat/start`, `/api/chat/ws` and `/api/chat/photo` (`handleChat`), `health`, `cspHosts`, and the build-time
|
|
12
|
+
context file (`@pmeweb/plugin-chat/build`).
|
|
13
|
+
- **`pmeweb-chat`** (private): one Durable Object per conversation, the model (Gemma 4 on Workers
|
|
14
|
+
AI), the limits and the daily cap, the lead email and SMS. Reached over the `CHAT` service binding.
|
|
15
|
+
- **The portal**: the settings (`botName`, `ownerName`, the alert recipients, « Consignes », the
|
|
16
|
+
daily cap), « Conversations » and the owner's public link.
|
|
17
|
+
|
|
18
|
+
Install it with the `install-plugin` skill (`INSTALL.md`).
|
|
19
|
+
|
|
20
|
+
## Behaviour worth knowing
|
|
21
|
+
|
|
22
|
+
- **A demo in the preview** (manifest `preview: "test"`). Off the site's real hosts
|
|
23
|
+
(`apercu.pmeweb.ca`, `workers.dev`, `astro dev`, or a page without `#pmw-hosts`) the bubble shows
|
|
24
|
+
with its face and colours, and the panel plays a scripted exchange: the page's opener, a visitor's
|
|
25
|
+
question drawn from it (« Une question sur nos toitures? » becomes « Pouvez-vous m’en dire plus
|
|
26
|
+
sur vos toitures? », anything else « Quels sont vos délais? »), and « (Démonstration) Ici,
|
|
27
|
+
l’assistant répond à partir du contenu de votre site. ». The composer is disabled under « Aperçu :
|
|
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 ». 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.
|
|
39
|
+
- **Nothing loads with the page.** The bubble is HTML and CSS; the panel, Turnstile and the socket
|
|
40
|
+
arrive when the visitor presses, hovers, focuses or types on it.
|
|
41
|
+
- **Openers.** One line per page, from the site's dictionary (`chat.openers`, `chat.defaultOpener`).
|
|
42
|
+
Shown once per visit (20 s, 30 % of the page, or exit intent with a mouse), never while the panel
|
|
43
|
+
is open. The same line greets the visitor in the panel.
|
|
44
|
+
- **Never passing for a person.** « Assistant virtuel » is always in text; the green dot appears
|
|
45
|
+
only when the owner has taken over, with the owner's name.
|
|
46
|
+
- **The socket.** A drop (1006, a forced deploy of pmeweb-chat) reconnects with backoff 1 to 30 s
|
|
47
|
+
and catches up from the last message seen; a message typed meanwhile is queued and sent on open,
|
|
48
|
+
and after 3 sends without an echo shows « Non envoyé, Réessayer ». A 44xx close never reconnects.
|
|
49
|
+
- **Fallback.** Daily cap reached, 15 visitor messages in the conversation, the visitor's 10
|
|
50
|
+
conversations of the day used up (pmeweb-chat answers the start 409 `cap`), the model down, or
|
|
51
|
+
the start refused: a three-field contact form that posts to `/api/chat/start` with `fallbackLead`
|
|
52
|
+
(never counted against the visitor's 10, and on the looser `CHAT_LIMIT`).
|
|
53
|
+
- **Limits.** Per IP: `CHAT_START_LIMIT` (3 conversation starts a minute) and `CHAT_LIMIT` (20 a
|
|
54
|
+
minute: socket reconnections, photos, the contact form). The site Worker hands the visitor's IP
|
|
55
|
+
to pmeweb-chat in `x-pmw-visitor-ip` (1.1.0); pmeweb-chat keeps only a daily hash of it to count
|
|
56
|
+
conversations per day. Health reports `limit` and `startLimit`.
|
|
57
|
+
- **Photos** (`"photos": true`). Up to 3 per conversation, shrunk in the browser to a 1600 px JPEG
|
|
58
|
+
(the canvas drops the EXIF, GPS included), posted to `/api/chat/photo` (rate-limited like a
|
|
59
|
+
message, 5 MB cap), re-encoded by pmeweb-chat and kept in a private bucket. The visitor sees the
|
|
60
|
+
local copy; the owner sees them through the portal. The bot does not read them.
|
|
61
|
+
- **The goal** `chat-lead` fires once per conversation (`window.pmw?.event`).
|
|
62
|
+
|
|
63
|
+
## Markup contract
|
|
64
|
+
|
|
65
|
+
A site may write its own markup carrying these and render the inline trigger itself:
|
|
66
|
+
|
|
67
|
+
| Selector | Role |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `#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 |
|
|
70
|
+
| `[data-pmw-chat-opener]` | on the root: this page's opener text |
|
|
71
|
+
| `[data-pmw-lazy]` | on the root: the URL of `dist/widget.js` (`?url&no-inline`) |
|
|
72
|
+
| `[data-pmw-chat-open]` | the bubble and the opener's text: a click opens the panel |
|
|
73
|
+
| `[data-pmw-chat-dismiss]` | hides the opener |
|
|
74
|
+
| `.pmw-chat__teaser` | the opener, `hidden` until shown |
|
|
75
|
+
| `.pmw-chat__bubble`, `.pmw-chat__avatar` | the bubble (focus returns here) and its avatar (cloned into the panel header) |
|
|
76
|
+
| `script[data-pmw-chat-config]` | JSON: `lang`, `siteKey`, `avatar`, `kind`, `privacyHref`, `strings` |
|
|
77
|
+
| `script[data-pmw-plugin="chat"]` | the inline trigger (the gate measures it) |
|
|
78
|
+
|
|
79
|
+
## Colours from the site
|
|
80
|
+
|
|
81
|
+
`Chat.astro` takes the site's `pmeweb.json` as `site` and reads `design.colors` at build (absent:
|
|
82
|
+
the package defaults). For a `sans-visage` icon the circle is `config.avatarBg` (lowercase
|
|
83
|
+
`#rrggbb`), else `design.colors.accent`, else #0a1b3d; the glyph is white or the site's `ink`
|
|
84
|
+
(#14181f without one), whichever contrasts more; a circle under 3:1 against the page `background`
|
|
85
|
+
or the `surface` gets a 1.5 px ring in `ink`. No text says so.
|
|
86
|
+
|
|
87
|
+
The panel: the header, the visitor's bubbles and the send button in `primary`, their text white or
|
|
88
|
+
`ink` (#14181f when the ink is too light), at 4.5:1 or the default navy stays; the panel and the
|
|
89
|
+
bubble in `surface` with `ink` text (at 4.5:1 or the default white panel stays), the bot's bubbles
|
|
90
|
+
in `background`, and muted text, links, the focus ring and errors re-derived to keep their minimum.
|
|
91
|
+
The fields stay white with their own text colour. The values land in one
|
|
92
|
+
`<style>:where(:root){...}</style>` beside the root: zero specificity, so a site's own
|
|
93
|
+
`--pmw-chat-*` value still wins.
|
|
94
|
+
|
|
95
|
+
## CSS variables
|
|
96
|
+
|
|
97
|
+
Never declared by the package: each use carries its default, so a site sets any of them anywhere.
|
|
98
|
+
`--pmw-chat-accent` (#0a1b3d) and `--pmw-chat-accent-fg` (#ffffff), `--pmw-chat-avatar-bg`,
|
|
99
|
+
`--pmw-chat-avatar-fg` and `--pmw-chat-avatar-ring` (the sans-visage circle), `--pmw-chat-fg` (#1f2328),
|
|
100
|
+
`--pmw-chat-muted` (#4b5563), `--pmw-chat-bubble-bg` and `--pmw-chat-panel-bg` (#ffffff),
|
|
101
|
+
`--pmw-chat-other-bg` (#f1f3f6), `--pmw-chat-border` (#c9ced6), `--pmw-chat-field-border`
|
|
102
|
+
(#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`
|
|
103
|
+
(#3fb950), `--pmw-chat-font`, `--pmw-chat-right` and `--pmw-chat-bottom` (16px). Check every pair a
|
|
104
|
+
site changes with `a11y-tools/contrast.mjs`.
|
|
105
|
+
|
|
106
|
+
## What goes wrong in silence
|
|
107
|
+
|
|
108
|
+
- No `TURNSTILE_SECRET`: every start answers 503 `not_configured` (the panel shows the contact
|
|
109
|
+
form). The health route says `secret: false`.
|
|
110
|
+
- `health` without `{ host }`: `settings` is always false. Without `{ config }` it cannot know the
|
|
111
|
+
switch and reports a disabled chat as broken (`settings: false` or worse) instead of `disabled`.
|
|
112
|
+
- `enabled` not exactly `true` (`"true"` in quotes, a typo): the chat is off, nothing on the site.
|
|
113
|
+
- `@pmeweb/consent` older than 1.1.0: the privacy section prints while the chat is off
|
|
114
|
+
(check-plugins refuses it).
|
|
115
|
+
- Missing `alertEmails` or `dailyCap` in the portal: pmeweb-chat answers `not_configured`; health
|
|
116
|
+
says `settings: false`.
|
|
117
|
+
- The context file absent from `dist/` or not served signed: the bot falls back to the contact form
|
|
118
|
+
on every message.
|
|
119
|
+
|
|
120
|
+
## Avatars
|
|
121
|
+
|
|
122
|
+
`config.avatar`, one of `src/faces.js` (the manifest's pattern is copied from it):
|
|
123
|
+
|
|
124
|
+
- `photos-1` to `photos-5`, `illustration-1` to `illustration-5`: faces generated for PmeWeb (not
|
|
125
|
+
real people), 96 x 96 WebP (about 1.5 KB) for the 48 px bubble at 2x; the panel header (40 px)
|
|
126
|
+
reuses the same file.
|
|
127
|
+
- `sans-visage-1` to `sans-visage-5`: a speech-bubble glyph drawn inline in `currentColor` (no
|
|
128
|
+
request). 3 to 5 are Lucide icons (ISC, `LICENSE-LUCIDE`).
|
|
129
|
+
- `logo`: the site's own, through the component's `logo` prop.
|
|
130
|
+
|
|
131
|
+
Adding faces (`3d`, faces 6 to 10) is not a breaking change: the files in `avatars/`, the counts in
|
|
132
|
+
`src/faces.js`, the new pattern in `manifest.json`. A value a site uses never goes away.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
|
2
|
+
<mask id="pmw-chat-m"><rect width="48" height="48" fill="#fff"/><path d="M19 21.5q5 4.2 10 0" fill="none" stroke="#000" stroke-width="2.4" stroke-linecap="round"/><circle cx="30.5" cy="16" r="1.9" fill="#000"/></mask>
|
|
3
|
+
<path d="M13 15.5a4.5 4.5 0 0 1 4.5-4.5h13a4.5 4.5 0 0 1 4.5 4.5v10a4.5 4.5 0 0 1-4.5 4.5H21l-5.5 5v-5.4A4.5 4.5 0 0 1 13 25.5z" fill="currentColor" mask="url(#pmw-chat-m)"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
|
2
|
+
<g fill="none" stroke="currentColor" stroke-width="2.4" stroke-linejoin="round" stroke-linecap="round"><path d="M10 14a4 4 0 0 1 4-4h12a4 4 0 0 1 4 4v7a4 4 0 0 1-4 4h-8l-5 4v-4.2A4 4 0 0 1 10 21z"/><path d="M33.5 17a4 4 0 0 1 4.5 4v6a4 4 0 0 1-3 3.8V35l-5-4h-7"/></g>
|
|
3
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
|
2
|
+
<!-- Lucide « message-circle-heart » (lucide.dev, @lucide/svelte 1.31.0), ISC licence. Scaled into the 48 grid beside icons 1 and 2. -->
|
|
3
|
+
<g transform="translate(10 10) scale(1.1667)" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path><path d="M7.828 13.07A3 3 0 0 1 12 8.764a3 3 0 0 1 5.004 2.224 3 3 0 0 1-.832 2.083l-3.447 3.62a1 1 0 0 1-1.45-.001z"></path></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
|
2
|
+
<!-- Lucide « message-circle-more » (lucide.dev, @lucide/svelte 1.31.0), ISC licence. Scaled into the 48 grid beside icons 1 and 2. -->
|
|
3
|
+
<g transform="translate(10 10) scale(1.1667)" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path><path d="M8 12h.01"></path><path d="M12 12h.01"></path><path d="M16 12h.01"></path></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
|
2
|
+
<!-- Lucide « message-circle-question-mark » (lucide.dev, @lucide/svelte 1.31.0), ISC licence. Scaled into the 48 grid beside icons 1 and 2. -->
|
|
3
|
+
<g transform="translate(10 10) scale(1.1667)" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"></path><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><path d="M12 17h.01"></path></g>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{function b({doc:s=document,hostname:n=globalThis.location?.hostname??""}={}){let c=s.getElementById("pmw-hosts");if(!c)return!1;let a;try{a=JSON.parse(c.textContent??"")}catch{return!1}if(!Array.isArray(a))return!1;let i=String(n).toLowerCase();return a.some(t=>typeof t=="string"&&t.toLowerCase()===i)}var S=["pointerdown","pointerenter","focusin","keydown"];function T(s,n,{events:c=S}={}){let a=!1,i=()=>{if(!a){a=!0;for(let t of c)s.removeEventListener(t,i);n()}};for(let t of c)s.addEventListener(t,i,{passive:!0});return{run:i}}var x="pmw-chat-opener",O=2e4,w=.3;function m({doc:s=document,win:n=window,hostname:c=n.location.hostname,importLazy:a=t=>import(t),onInteraction:i=T}={}){let t=s.getElementById("pmw-chat");if(!t)return null;t.hidden=!1;let f=t.querySelector(".pmw-chat__teaser"),l=null,u=()=>l??(l=a(t.dataset.pmwLazy).then(e=>e.mountChat(t),e=>{throw l=null,e}));i(t,()=>u().catch(()=>{}));let h=b({doc:s,hostname:c});if(!h){let e=()=>{let o=new URLSearchParams(n.location.hash.slice(1)).get("pmw-chat");o==="panel"&&!t.hasAttribute("data-open")?u().then(r=>r.open(),()=>{}):o==="bubble"&&l&&l.then(r=>r.close(),()=>{})};e(),n.addEventListener("hashchange",e)}let E=n.sessionStorage,d=!1;try{d=!!E?.getItem(x)}catch{}let v=[],y=(e,o,r)=>{e.addEventListener(o,r,{passive:!0}),v.push([e,o,r])},L,g=()=>{d=!0;try{E?.setItem(x,"1")}catch{}n.clearTimeout(L);for(let[e,o,r]of v)e.removeEventListener(o,r)},p=()=>{d||!f||t.hasAttribute("data-open")||(g(),f.hidden=!1)};return t.addEventListener("click",e=>{let o=e.target.closest?.("[data-pmw-chat-open],[data-pmw-chat-dismiss]");o&&(f&&(f.hidden=!0),g(),o.hasAttribute("data-pmw-chat-open")&&u().then(r=>r.open(),()=>{}))}),!d&&f&&h&&(L=n.setTimeout(p,O),y(n,"scroll",()=>{let e=s.documentElement.scrollHeight-n.innerHeight;e>0&&n.scrollY/e>=w&&p()}),n.matchMedia?.("(pointer: fine)").matches&&y(s,"mouseout",e=>{!e.relatedTarget&&e.clientY<=0&&p()})),{load:u,show:p}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>m()):m();})();
|
package/dist/widget.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var Ue=`.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 Be=new Set;function $e(r,{doc:c=document,attrs:o={}}={}){if(Be.has(r)||(Be.add(r),c.querySelector(`script[src="${r}"]`)))return;let t=c.createElement("script");t.src=r,t.async=!0;for(let[d,h]of Object.entries(o))t.setAttribute(d,h);c.head.append(t)}function je({doc:r=document,hostname:c=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 d=String(c).toLowerCase();return t.some(h=>typeof h=="string"&&h.toLowerCase()===d)}var lt="https://challenges.cloudflare.com/turnstile/v0/api.js";var pt=15e3,Fe=200;function fe(r=document){$e(lt,{doc:r})}var dt=r=>r.querySelector('input[name="cf-turnstile-response"]')?.value??"",ut=r=>{let c=r.querySelector(".cf-turnstile");return!!c&&c.offsetHeight>0};async function Je(r,{win:c=window,timeoutMs:o=pt}={}){typeof r.pmwTurnstileLoad=="function"?r.pmwTurnstileLoad():fe(c.document);let t=0;for(;dt(r)==="";){if(!ut(r)){if(t>=o)return!1;t+=Fe}await new Promise(d=>c.setTimeout(d,Fe))}return!0}function Ve(r,{win:c=window}={}){let o=r.querySelector(".cf-turnstile");if(!o||o.offsetHeight>0)return!1;try{return c.turnstile?.reset(o),!0}catch{return!1}}var Ke={unauthorized:4401,expired:4410,rpc:4426,deleted:4404};var ht={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=r=>String(r??"").slice(0,2).toLowerCase()==="en"?"en":"fr";function me(r,c={}){let o=ht[Q(r)],t={...o};for(let[d,h]of Object.entries(c??{}))typeof h=="string"&&d in o&&(t[d]=h);return t}var W=(r,c)=>r.replace(/\{(\w+)\}/g,(o,t)=>t in c?String(c[t]):o);var we="pmw-chat",Ye=[1e3,2e3,4e3,8e3,16e3,3e4],Ge=3,ft="chat-lead",mt=3,wt=1600,gt=.85,_t=3e3,bt=8e3,xt='<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 vt(r,c){let o=await r.createImageBitmap(c,{imageOrientation:"from-image"}),t=Math.min(1,wt/Math.max(o.width,o.height)),d=Math.max(1,Math.round(o.width*t)),h=Math.max(1,Math.round(o.height*t)),f=r.document.createElement("canvas");return f.width=d,f.height=h,f.getContext("2d").drawImage(o,0,0,d,h),o.close?.(),new Promise((I,Z)=>f.toBlob(z=>z?I(z):Z(new Error("encode")),"image/jpeg",gt))}var yt={fr:{nos:"vos",notre:"votre",nous:"vous"},en:{our:"your",us:"you",we:"you"}},St={fr:/^une question sur (.+?)\s*\?$/i,en:/^(?:a |any )?questions? (?:about|on) (.+?)\s*\?$/i};function kt(r,c){let o=Q(c),t=me(o),d=String(r??"").trim().match(St[o]);if(!d)return t.demoQuestion;let h=d[1].replace(/[\p{L}’']+/gu,f=>yt[o][f.toLowerCase()]??f);return W(t.demoAsk,{topic:h})}var Tt='<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" fill="none"/></svg>';function s(r,c,o={},t){let d=r.createElement(c);for(let[h,f]of Object.entries(o))f!=null&&f!==!1&&d.setAttribute(h,f===!0?"":f);return t!==void 0&&(d.textContent=t),d}var Lt=r=>({load:()=>fe(r.document),async token(c){return await Je(c,{win:r}),c.querySelector('input[name="cf-turnstile-response"]')?.value??""},reset:c=>Ve(c,{win:r})});function Ut(r,c={}){if(r.pmwChat)return r.pmwChat;let o=c.win??window,t=o.document,d=c.WebSocket??o.WebSocket,h=c.fetch??((...e)=>o.fetch(...e)),f=c.turnstile??Lt(o),I=c.uuid??(()=>o.crypto.randomUUID()),Z=c.shrink??(e=>vt(o,e)),z=c.preview??(e=>new Promise((n,a)=>{let l=new o.FileReader;l.onload=()=>n(String(l.result)),l.onerror=()=>a(l.error),l.readAsDataURL(e)})),v={};try{v=JSON.parse(r.querySelector("[data-pmw-chat-config]")?.textContent??"{}")}catch{}let H=Q(v.lang),i=me(H,v.strings),ge=r.dataset.pmwChatOpener??"",Qe=r.querySelector(".pmw-chat__bubble"),m=c.demo??(v.demo===!0||!je({doc:t,hostname:o.location.hostname}));t.getElementById("pmw-chat-style")||t.head.append(s(t,"style",{id:"pmw-chat-style"},Ue));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=r.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"}),D=s(t,"p",{class:"pmw-chat__role"}),ve=s(t,"span",{class:"pmw-chat__dot","aria-hidden":"true",hidden:!0}),U=s(t,"button",{type:"button",class:"pmw-chat__close","aria-label":i.close});U.innerHTML=Tt,be.append(xe,D),_e.append(ee,be,ve,U);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"}),We=s(t,"li",{class:"pmw-chat__msg pmw-chat__msg--bot pmw-chat__msg--greeting"},ge),C=s(t,"li",{class:"pmw-chat__typing","aria-hidden":"true",hidden:!0});C.append(s(t,"span"),s(t,"span"),s(t,"span")),x.append(We);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":H,"data-appearance":"interaction-only","data-size":"flexible"}),w=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:m,"aria-describedby":m?"pmw-chat-demo-hint":null}),Ze=s(t,"button",{type:"submit",class:"pmw-chat__send",disabled:m},i.send);w.append(s(t,"label",{class:"pmw-chat__sr",for:"pmw-chat-input"},i.messageLabel),y,Ze),m?w.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-demo-hint"},i.demoHint)):w.append(Se(w));let P=null,E=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:m}),e.innerHTML=xt,e.prepend(P),e.append(s(t,"span",{class:"pmw-chat__sr"},i.attachPhoto)),w.insertBefore(e,y),w.append(s(t,"p",{class:"pmw-chat__hint",id:"pmw-chat-photo-hint"},i.photoHint),E)}let B=s(t,"div",{class:"pmw-chat__ended",hidden:!0}),ke=s(t,"p",{},i.ended),$=s(t,"button",{type:"button",class:"pmw-chat__restart"},i.newConversation);B.append(ke,$);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},j=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"}),j,s(t,"button",{type:"submit",class:"pmw-chat__send"},i.fallbackSend)),m||g.append(Se(g));let Le=s(t,"div",{class:"pmw-chat__body"});Le.append(te,x,ne,B,g),b.append(_e,Le,w,ye),r.append(b);let u=null;if(!m)try{u=JSON.parse(o.sessionStorage.getItem(we)??"null")}catch{u=null}let Ce=()=>{try{o.sessionStorage.setItem(we,JSON.stringify(u))}catch{}},_=null,oe=0,re=null,R=null,F=0,J="bot",V=null,A=!1,Ee=0,se=null,ie=new Set,k=new Map,K=new Set,ce=0,Ae=()=>new URLSearchParams(o.location.hash.slice(1)).get("pmw-chat-botName")?.trim()||null,Oe=m?Ae():null,le=()=>m?Oe:u?.botName||null;function q(){let e=J==="human";xe.textContent=e?V||i.assistant:le()||i.assistant,D.textContent=e?i.live:le()?i.assistant:"",D.hidden=D.textContent==="",ve.hidden=!e,b.dataset.mode=J}q();let T=e=>{ne.textContent=e??"",ne.hidden=!e};T("");let Ne=e=>e.role==="visitor"?i.you:e.role==="owner"?e.by||V||i.assistant:le()||i.assistant;function O(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,C.parentNode===x?C:null),a}let Me=e=>[...x.querySelectorAll("li[data-id]")].find(n=>n.dataset.id===e),L=(e,n,a)=>{e.firstChild.textContent=W(i.said,{name:Ne(n),text:""}),e.lastChild.textContent=a===""&&n.photos?.length&&!e.querySelector("img")?i.photoSent:a};function pe(){C.hidden=!0,o.clearTimeout(se)}function Pe(e,{announce:n}){if(ie.has(e.seq))return;ie.add(e.seq),F=Math.max(F,e.seq);let a=Me(e.id);a||(a=O(e.id,e.role)),a.classList.remove("is-streaming","is-pending","is-failed"),a.querySelector(".pmw-chat__retry")?.remove(),L(a,e,e.text),e.role==="visitor"?(k.delete(e.id),e.photos?.length&&K.add(e.id)):(pe(),n&&(ye.textContent=W(i.said,{name:Ne(e),text:e.text})),b.hidden&&r.setAttribute("data-unread","")),N()}function et(e,n){pe();let a=Me(e);a||(a=O(e,"bot"),a.classList.add("is-streaming"),L(a,{role:"bot"},"")),a.lastChild.textContent+=n,N()}let N=()=>{x.scrollTop=x.scrollHeight};if(m){L(O("demo-question","visitor"),{role:"visitor"},kt(ge,H));let e=O("demo-answer","bot");L(e,{role:"bot"},i.demoAnswer),o.addEventListener("hashchange",()=>{Oe=Ae(),q(),L(e,{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):M():ue()}),n.append(a),e.li.append(n)}let Y=()=>!!_&&_.readyState===1;function de(e){if(!(e.failed||!Y())){if(e.tries>=Ge)return X(e);e.tries++,_.send(JSON.stringify({type:"message",id:e.id,text:e.text}))}}function M(){if(!u||A||_)return;o.clearTimeout(re);let e=o.location.protocol==="https:"?"wss:":"ws:",n=new URLSearchParams({c:u.c,k:u.k}),a=new d(`${e}//${o.location.host}/api/chat/ws?${n}`);_=a,a.onopen=()=>{oe=0,T(""),a.send(JSON.stringify({type:"hello",since:F}));for(let l of k.values())de(l)},a.onmessage=l=>{let p;try{p=JSON.parse(l.data)}catch{return}nt(p)},a.onclose=l=>{_===a&&(_=null);for(let S of k.values())S.tries>=Ge&&X(S);if(l.code>=4e3&&l.code<5e3)return tt(l.code);if(A)return;T(i.reconnecting);let p=Ye[Math.min(oe,Ye.length-1)];oe++,re=o.setTimeout(M,p)}}function tt(e){if(e===Ke.rpc)return G(i.reload,!1);G(i.ended,!0)}function G(e,n){A=!0,o.clearTimeout(re),T(""),ke.textContent=e,$.hidden=!n,B.hidden=!1,w.hidden=!0;for(let a of k.values())X(a)}function Re(e){J=e.mode==="human"?"human":"bot",V=typeof e.ownerName=="string"?e.ownerName:null,q(),e.closed&&G(i.ended,!0)}function nt(e){switch(e?.type){case"history":for(let n of e.messages??[])Pe(n,{announce:!1});return Re(e);case"message":return Pe(e.msg,{announce:e.msg?.role!=="visitor"});case"delta":return et(e.id,e.text);case"typing":return e.role==="visitor"?void 0:(C.hidden=!1,x.append(C),o.clearTimeout(se),se=o.setTimeout(pe,bt),N());case"status":return Re(e);case"fallback":return He(e.reason==="cap"?i.fallbackCap:i.fallbackDown);case"lead":return Ie();case"error":(e.code==="closed"||e.code==="deleted")&&G(i.ended,!0)}}let qe=!1;function Ie(){if(!(qe||u?.lead)){qe=!0,u&&(u.lead=!0,Ce());try{o.pmw?.event(ft)}catch{}}}async function ze(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:H,page:o.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,cap:l?.status===409&&p?.error==="cap"}}function ue(){return R||u||(T(i.connecting),R=ze(w,{}).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(),q(),M();return}for(let n of k.values())X(n);if(e.error)return T(e.error);T(""),He(e.cap?i.fallbackCap:i.unavailable)})),R}function He(e){Te.textContent=e,g.hidden=!1,w.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){j.textContent=i.fallbackMissing;return}j.textContent="";let l=await ze(g,{fallbackLead:a});if(!l.ok){j.textContent=l.error??i.unavailable;return}g.replaceChildren(s(t,"p",{class:"pmw-chat__fb-done",role:"status"},i.fallbackSent)),Ie()});let at={413:"photoTooLarge",415:"photoUnreadable",409:"photoLimit",429:"tooMany"};async function ot(e){let n,a;try{n=await Z(e),a=await z(n)}catch{E.textContent=i.photoUnreadable;return}let l=I(),p=O(l,"visitor");p.classList.add("is-pending"),p.insertBefore(s(t,"img",{class:"pmw-chat__photo",src:a,alt:i.photoAlt}),p.lastChild),L(p,{role:"visitor"},""),N(),u||await ue();let S=null;if(u){let it=new URLSearchParams({c:u.c,k:u.k,id:l});try{S=await h(`/api/chat/photo?${it}`,{method:"POST",headers:{"content-type":"image/jpeg"},body:n})}catch{S=null}}if(S?.ok){p.classList.remove("is-pending"),K.add(l),_||M();return}p.remove(),E.textContent=i[at[S?.status]]??i.photoFailed}P?.addEventListener("change",async()=>{let e=[...P.files??[]];if(P.value="",E.textContent="",A||m||e.length===0)return;let n=mt-K.size-ce;e.length>n&&(E.textContent=i.photoLimit);let a=e.slice(0,Math.max(0,n));ce+=a.length;for(let l of a)try{await ot(l)}finally{ce--}});function De(){let e=y.value.trim();if(!e||A||m)return;y.value="";let n={id:I(),text:e.slice(0,2e3),tries:0,failed:!1};n.li=O(n.id,"visitor"),n.li.classList.add("is-pending"),L(n.li,{role:"visitor"},n.text),k.set(n.id,n),N(),u?Y()?de(n):M():ue()}w.addEventListener("submit",e=>{e.preventDefault(),De()}),y.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!e.isComposing&&(e.preventDefault(),De())}),y.addEventListener("input",()=>{let e=Date.now();Y()&&e-Ee>_t&&(Ee=e,_.send(JSON.stringify({type:"typing"})))}),$.addEventListener("click",()=>{u=null;try{o.sessionStorage.removeItem(we)}catch{}try{_?.close(1e3)}catch{}_=null,A=!1,F=0,ie.clear(),k.clear(),K.clear(),E.textContent="",J="bot",V=null,q();for(let e of[...x.querySelectorAll("li[data-id]")])e.remove();B.hidden=!0,w.hidden=!1,y.focus()});let rt=()=>[...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=rt();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())}),U.addEventListener("click",()=>he());function st(){m||f.load(),b.hidden=!1,r.setAttribute("data-open",""),r.removeAttribute("data-unread"),u&&M(),(m?U:w.hidden?g.hidden?$:g.querySelector("input"):y)?.focus(),N()}function he(){b.hidden=!0,r.removeAttribute("data-open"),Qe?.focus()}return r.pmwChat={open:st,close:he,panel:b},r.pmwChat}export{Ye as BACKOFF_MS,ft as GOAL,mt as MAX_PHOTOS,Ge as MAX_TRIES,wt as PHOTO_EDGE,gt as PHOTO_QUALITY,we as SESSION_KEY,kt as demoQuestion,Ut as mountChat};
|
package/manifest.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chat",
|
|
3
|
+
"package": "@pmeweb/plugin-chat",
|
|
4
|
+
"title": { "fr": "Assistant virtuel", "en": "Virtual assistant" },
|
|
5
|
+
"summary": { "fr": "Répond aux questions des visiteurs et vous transmet leurs demandes.", "en": "Answers your visitors’ questions and forwards their requests." },
|
|
6
|
+
"category": "visitors",
|
|
7
|
+
"icon": "message-circle",
|
|
8
|
+
"load": "interaction",
|
|
9
|
+
"inlineBudget": 2048,
|
|
10
|
+
"preview": "test",
|
|
11
|
+
"activeWhen": ["enabled"],
|
|
12
|
+
"config": {
|
|
13
|
+
"enabled": { "type": "boolean", "optional": true },
|
|
14
|
+
"provider": { "pattern": "^workers-ai$" },
|
|
15
|
+
"sms": { "pattern": "^voipms$", "optional": true },
|
|
16
|
+
"photos": { "type": "boolean", "optional": true },
|
|
17
|
+
"avatar": { "pattern": "^(?:photos-[1-5]|illustration-[1-5]|sans-visage-[1-5]|logo)$" },
|
|
18
|
+
"avatarBg": { "pattern": "^#[0-9a-f]{6}$", "optional": true }
|
|
19
|
+
},
|
|
20
|
+
"bindings": [
|
|
21
|
+
{ "kind": "service", "binding": "CHAT", "service": "pmeweb-chat", "entrypoint": "ChatApi" },
|
|
22
|
+
{ "kind": "ratelimit", "binding": "CHAT_LIMIT" },
|
|
23
|
+
{ "kind": "ratelimit", "binding": "CHAT_START_LIMIT" }
|
|
24
|
+
],
|
|
25
|
+
"secrets": ["TURNSTILE_SECRET"],
|
|
26
|
+
"goals": ["chat-lead"],
|
|
27
|
+
"privacy": {
|
|
28
|
+
"fr": "privacy/fr.json",
|
|
29
|
+
"en": "privacy/en.json",
|
|
30
|
+
"providers": {
|
|
31
|
+
"*": { "name": "Cloudflare", "location": { "fr": "extérieur du Québec", "en": "outside Quebec" } },
|
|
32
|
+
"sms=voipms": { "name": "voip.ms", "location": { "fr": "Montréal", "en": "Montreal" } }
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"markup": { "pages": "all", "required": ["#pmw-chat", "[data-pmw-chat-opener]"] },
|
|
36
|
+
"build": ["plugins/chat/context.json"],
|
|
37
|
+
"portal": {
|
|
38
|
+
"screens": [
|
|
39
|
+
{ "key": "appearance",
|
|
40
|
+
"title": { "fr": "Apparence de l’assistant", "en": "Assistant’s appearance" },
|
|
41
|
+
"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." },
|
|
42
|
+
"crumb": { "fr": "Apparence", "en": "Appearance" },
|
|
43
|
+
"views": [ { "key": "bubble", "label": { "fr": "Bulle", "en": "Bubble" } },
|
|
44
|
+
{ "key": "panel", "label": { "fr": "Panneau ouvert", "en": "Open panel" } } ] }
|
|
45
|
+
],
|
|
46
|
+
"settings": [
|
|
47
|
+
{ "key": "botName", "kind": "text", "editor": "client", "screen": "appearance", "applies": "publish",
|
|
48
|
+
"label": { "fr": "Nom de l’assistant", "en": "Assistant’s name" },
|
|
49
|
+
"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." } },
|
|
50
|
+
{ "key": "ownerName", "kind": "text", "editor": "client",
|
|
51
|
+
"label": { "fr": "Votre nom quand vous prenez le relais", "en": "Your name when you take over" },
|
|
52
|
+
"help": { "fr": "Remplace le nom de l’assistant quand vous répondez vous-même au visiteur.", "en": "Replaces the assistant’s name when you answer the visitor yourself." } },
|
|
53
|
+
{ "key": "alertEmails", "kind": "emails", "shownToClient": true,
|
|
54
|
+
"label": { "fr": "Courriels d’alerte", "en": "Alert emails" },
|
|
55
|
+
"help": { "fr": "Reçoivent chaque demande laissée au clavardage.", "en": "Receive each request left in the chat." } },
|
|
56
|
+
{ "key": "alertPhone", "kind": "phone", "shownToClient": true,
|
|
57
|
+
"label": { "fr": "Téléphone d’alerte", "en": "Alert phone" },
|
|
58
|
+
"help": { "fr": "Texto court à chaque demande, si les textos sont activés.", "en": "A short text for each request, when texts are on." } },
|
|
59
|
+
{ "key": "consignes", "kind": "longtext",
|
|
60
|
+
"label": { "fr": "Consignes", "en": "Instructions" },
|
|
61
|
+
"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." } },
|
|
62
|
+
{ "key": "dailyCap", "kind": "money", "required": true,
|
|
63
|
+
"label": { "fr": "Plafond quotidien", "en": "Daily cap" },
|
|
64
|
+
"help": { "fr": "Au-delà, le site affiche le formulaire de contact jusqu’à minuit.", "en": "Past it, the site shows the contact form until midnight." } }
|
|
65
|
+
],
|
|
66
|
+
"config": [
|
|
67
|
+
{ "key": "enabled", "kind": "boolean", "editor": "client",
|
|
68
|
+
"label": { "fr": "Afficher l’assistant sur votre site", "en": "Show the assistant on your site" },
|
|
69
|
+
"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." } },
|
|
70
|
+
{ "key": "avatar", "kind": "choice", "editor": "client", "screen": "appearance",
|
|
71
|
+
"label": { "fr": "Visage", "en": "Face" },
|
|
72
|
+
"groups": [
|
|
73
|
+
{ "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}" } },
|
|
74
|
+
{ "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}" } },
|
|
75
|
+
{ "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" } ] },
|
|
76
|
+
{ "key": "avatarBg", "kind": "color", "editor": "client", "screen": "appearance",
|
|
77
|
+
"label": { "fr": "Couleur de fond", "en": "Background colour" },
|
|
78
|
+
"default": { "design": "accent" }, "showWhen": { "key": "avatar", "group": "sans-visage" } }
|
|
79
|
+
] },
|
|
80
|
+
"rpc": { "service": "pmeweb-chat", "entrypoint": "ChatApi", "version": 1, "accepts": "pmeweb-chat/plugin-rpc.json" },
|
|
81
|
+
"health": true
|
|
82
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/plugin-chat",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"dist",
|
|
13
|
+
"avatars",
|
|
14
|
+
"privacy",
|
|
15
|
+
"manifest.json",
|
|
16
|
+
"INSTALL.md",
|
|
17
|
+
"LICENSE-LUCIDE"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": { "types": "./src/worker.d.ts", "default": "./src/worker.js" },
|
|
21
|
+
"./csp": { "types": "./src/csp.d.ts", "default": "./src/csp.js" },
|
|
22
|
+
"./build": { "types": "./src/build.d.ts", "default": "./src/build.js" },
|
|
23
|
+
"./Chat.astro": "./src/Chat.astro",
|
|
24
|
+
"./chat.css": "./src/chat.css",
|
|
25
|
+
"./dist/widget.js": "./dist/widget.js",
|
|
26
|
+
"./dist/trigger.iife.js": "./dist/trigger.iife.js",
|
|
27
|
+
"./avatars/*": "./avatars/*",
|
|
28
|
+
"./manifest.json": "./manifest.json"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "node build.mjs",
|
|
32
|
+
"prepare": "node build.mjs",
|
|
33
|
+
"test": "node build.mjs && vitest run",
|
|
34
|
+
"prepublishOnly": "node build.mjs && vitest run"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@pmeweb/loader": "1.0.0",
|
|
38
|
+
"@pmeweb/plugin-kit": "1.1.0",
|
|
39
|
+
"@pmeweb/turnstile": "2.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"astro": "7.2.9",
|
|
43
|
+
"esbuild": "0.25.12",
|
|
44
|
+
"happy-dom": "20.14.5",
|
|
45
|
+
"vitest": "4.1.11"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.12"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/privacy/en.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"heading": "Virtual assistant (chat)",
|
|
3
|
+
"paragraphs": [
|
|
4
|
+
"This site’s virtual assistant is an automated program: it answers from the content published on the site. When someone from the business takes over, their name shows in the window.",
|
|
5
|
+
"If you use it, we collect your messages, the page you write from and, if you give them, your name, your phone number or email and your request. They are used to answer you and to pass your request on to the business, which receives it by email to get back to you.",
|
|
6
|
+
"Answers are produced by an artificial intelligence model run by Cloudflare, which also hosts the conversation and sends the emails. Cloudflare processes this information on its global network, so possibly outside Quebec. According to Cloudflare, this content is not used to train its models.",
|
|
7
|
+
"With the first message, a Cloudflare anti-robot check (Turnstile) takes place. Your IP address is used for that check and to limit abuse; it is not sent with the conversation.",
|
|
8
|
+
"Your browser keeps the conversation’s identifier while the tab is open (session storage), to find it again if you reload the page.",
|
|
9
|
+
"The conversation is kept 90 days, then deleted. During that time the business may export it; the exported copy is then its responsibility.",
|
|
10
|
+
"To see or have a conversation deleted, contact the person in charge of the protection of personal information named on this page."
|
|
11
|
+
],
|
|
12
|
+
"byKey": {
|
|
13
|
+
"sms": [
|
|
14
|
+
"When you leave a request, a short text message summarizing it is also sent to the business through voip.ms, a telephone provider in Montreal."
|
|
15
|
+
],
|
|
16
|
+
"photos": [
|
|
17
|
+
"You may attach up to three photos. They are shrunk in your browser, which removes the GPS location a phone writes into a photo, then kept with the conversation in private storage at Cloudflare and deleted with it. Do not send photos of people or of identity documents."
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
package/privacy/fr.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"heading": "Assistant virtuel (clavardage)",
|
|
3
|
+
"paragraphs": [
|
|
4
|
+
"L’assistant virtuel de ce site est un programme automatisé : il répond à partir du contenu publié sur le site. Quand une personne de l’entreprise prend le relais, son nom s’affiche dans la fenêtre.",
|
|
5
|
+
"Si vous l’utilisez, nous recueillons vos messages, la page d’où vous écrivez et, si vous les donnez, votre nom, votre numéro de téléphone ou votre courriel et votre demande. Ils servent à vous répondre et à transmettre votre demande à l’entreprise, qui la reçoit par courriel pour vous recontacter.",
|
|
6
|
+
"Les réponses sont produites par un modèle d’intelligence artificielle exécuté par Cloudflare, qui héberge aussi la conversation et envoie les courriels. Cloudflare traite ces renseignements sur son réseau mondial, donc possiblement à l’extérieur du Québec. Selon Cloudflare, ces contenus ne servent pas à entraîner ses modèles.",
|
|
7
|
+
"Au premier message, une vérification anti-robot de Cloudflare (Turnstile) a lieu. Votre adresse IP sert à cette vérification et à limiter les abus; elle n’est pas transmise avec la conversation.",
|
|
8
|
+
"Votre navigateur garde l’identifiant de la conversation tant que l’onglet est ouvert (stockage de session), pour la retrouver si vous rechargez la page.",
|
|
9
|
+
"La conversation est conservée 90 jours, puis supprimée. Pendant ce délai, l’entreprise peut l’exporter; la copie exportée est alors sous sa responsabilité.",
|
|
10
|
+
"Pour consulter ou faire supprimer une conversation, communiquez avec la personne responsable de la protection des renseignements personnels indiquée sur cette page."
|
|
11
|
+
],
|
|
12
|
+
"byKey": {
|
|
13
|
+
"sms": [
|
|
14
|
+
"Quand vous laissez une demande, un court texto qui la résume est aussi envoyé à l’entreprise par voip.ms, un fournisseur de téléphonie de Montréal."
|
|
15
|
+
],
|
|
16
|
+
"photos": [
|
|
17
|
+
"Vous pouvez joindre jusqu’à trois photos. Elles sont réduites dans votre navigateur, ce qui retire la position GPS qu’un téléphone inscrit dans une photo, puis conservées avec la conversation dans un espace privé chez Cloudflare et supprimées avec elle. N’envoyez pas de photos de personnes ni de pièces d’identité."
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|