@chatcops/widget 0.5.0 → 0.7.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/README.md +4 -3
- package/dist/api/client.d.ts +21 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/protocol.d.ts +39 -0
- package/dist/api/protocol.d.ts.map +1 -0
- package/dist/api/turnstile.d.ts +8 -0
- package/dist/api/turnstile.d.ts.map +1 -0
- package/dist/api/types.d.ts +17 -1
- package/dist/api/types.d.ts.map +1 -1
- package/dist/chatcops.esm.js +1217 -537
- package/dist/chatcops.full.min.js +1 -0
- package/dist/chatcops.min.js +1 -1
- package/dist/chatcops.panel.js +1 -0
- package/dist/chunks/prechat-form-DiHgHUnU.js +1 -0
- package/dist/chunks/turnstile-CAnVL3fK.js +1 -0
- package/dist/dom/bubble.d.ts +3 -0
- package/dist/dom/bubble.d.ts.map +1 -1
- package/dist/dom/fab.d.ts +4 -2
- package/dist/dom/fab.d.ts.map +1 -1
- package/dist/dom/icons.d.ts +4 -0
- package/dist/dom/icons.d.ts.map +1 -0
- package/dist/dom/input.d.ts +1 -0
- package/dist/dom/input.d.ts.map +1 -1
- package/dist/dom/messages.d.ts +39 -2
- package/dist/dom/messages.d.ts.map +1 -1
- package/dist/dom/panel.d.ts +8 -0
- package/dist/dom/panel.d.ts.map +1 -1
- package/dist/dom/prechat-form.d.ts.map +1 -1
- package/dist/dom/shadow.d.ts +10 -1
- package/dist/dom/shadow.d.ts.map +1 -1
- package/dist/i18n.d.ts +13 -1
- package/dist/i18n.d.ts.map +1 -1
- package/dist/index.d.ts +41 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/loader/index.d.ts +2 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/loader.d.ts +71 -0
- package/dist/loader/loader.d.ts.map +1 -0
- package/dist/locales/ar.d.ts +4 -0
- package/dist/locales/ar.d.ts.map +1 -0
- package/dist/locales/ar.js +1 -0
- package/dist/locales/de.d.ts +4 -0
- package/dist/locales/de.d.ts.map +1 -0
- package/dist/locales/de.js +1 -0
- package/dist/locales/es.d.ts +4 -0
- package/dist/locales/es.d.ts.map +1 -0
- package/dist/locales/es.js +1 -0
- package/dist/locales/fr.d.ts +4 -0
- package/dist/locales/fr.d.ts.map +1 -0
- package/dist/locales/fr.js +1 -0
- package/dist/locales/he.d.ts +4 -0
- package/dist/locales/he.d.ts.map +1 -0
- package/dist/locales/he.js +1 -0
- package/dist/locales/hi.d.ts +4 -0
- package/dist/locales/hi.d.ts.map +1 -0
- package/dist/locales/hi.js +1 -0
- package/dist/locales/it.d.ts +4 -0
- package/dist/locales/it.d.ts.map +1 -0
- package/dist/locales/it.js +1 -0
- package/dist/locales/ja.d.ts +4 -0
- package/dist/locales/ja.d.ts.map +1 -0
- package/dist/locales/ja.js +1 -0
- package/dist/locales/ko.d.ts +4 -0
- package/dist/locales/ko.d.ts.map +1 -0
- package/dist/locales/ko.js +1 -0
- package/dist/locales/nl.d.ts +4 -0
- package/dist/locales/nl.d.ts.map +1 -0
- package/dist/locales/nl.js +1 -0
- package/dist/locales/pt.d.ts +4 -0
- package/dist/locales/pt.d.ts.map +1 -0
- package/dist/locales/pt.js +1 -0
- package/dist/locales/ur.d.ts +4 -0
- package/dist/locales/ur.d.ts.map +1 -0
- package/dist/locales/ur.js +1 -0
- package/dist/locales/zh.d.ts +4 -0
- package/dist/locales/zh.d.ts.map +1 -0
- package/dist/locales/zh.js +1 -0
- package/dist/markdown.d.ts.map +1 -1
- package/dist/namespace.d.ts.map +1 -1
- package/dist/panel.d.ts +6 -0
- package/dist/panel.d.ts.map +1 -0
- package/dist/script-config.d.ts +5 -0
- package/dist/script-config.d.ts.map +1 -0
- package/dist/storage.d.ts +30 -10
- package/dist/storage.d.ts.map +1 -1
- package/dist/theme.d.ts +15 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/widget.d.ts +132 -14
- package/dist/widget.d.ts.map +1 -1
- package/package.json +25 -7
package/dist/chatcops.esm.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
var
|
|
2
|
-
|
|
3
|
-
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
-
const widgetStyles = ":host{--cc-accent: #6366f1;--cc-accent-hover: #4f46e5;--cc-on-accent: #ffffff;--cc-accent-contrast: #6164ec;--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-font: system-ui, -apple-system, sans-serif;--cc-radius: 12px;--cc-shadow: 0 8px 32px rgba(0, 0, 0, .4);--cc-fab-size: 56px;--cc-panel-width: 400px;--cc-panel-height: 560px;all:initial;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);-webkit-font-smoothing:antialiased}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}.cc-fab{position:fixed;bottom:20px;right:20px;width:var(--cc-fab-size);height:var(--cc-fab-size);border-radius:50%;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #0000004d;transition:transform .2s ease,box-shadow .2s ease;z-index:10000;outline:none}.cc-fab:hover{transform:scale(1.05);box-shadow:0 6px 24px #0006}.cc-fab:active{transform:scale(.95)}.cc-fab.cc-left{right:auto;left:20px}.cc-fab svg{width:24px;height:24px;fill:var(--cc-on-accent);transition:transform .3s ease}.cc-fab.cc-open svg{transform:rotate(90deg)}.cc-fab-badge{position:absolute;top:-2px;right:-2px;width:12px;height:12px;border-radius:50%;background:#ef4444;border:2px solid var(--cc-bg);display:none}.cc-fab-badge.cc-visible{display:block}@keyframes cc-pulse{0%,to{box-shadow:0 4px 16px #0000004d}50%{box-shadow:0 4px 16px #6366f180}}.cc-fab.cc-pulse{animation:cc-pulse 2s ease-in-out 3}.cc-panel{position:fixed;bottom:88px;right:20px;width:var(--cc-panel-width);height:var(--cc-panel-height);max-height:calc(100vh - 100px);background:var(--cc-bg);border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);display:flex;flex-direction:column;overflow:hidden;z-index:9999;opacity:0;transform:translateY(16px) scale(.95);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.cc-panel.cc-visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.cc-panel.cc-left{right:auto;left:20px}.cc-panel.cc-inline{position:relative;bottom:auto;right:auto;left:auto;width:100%;height:100%;max-height:100%;z-index:auto;transform:none;transition:none}.cc-panel.cc-inline:not(.cc-visible){visibility:hidden}.cc-panel.cc-inline.cc-visible{transform:none}.cc-header{display:flex;align-items:center;padding:16px;background:var(--cc-bg-surface);border-bottom:1px solid var(--cc-border);flex-shrink:0}.cc-header-avatar{width:36px;height:36px;border-radius:50%;margin-right:12px;object-fit:cover}.cc-header-avatar-placeholder{width:36px;height:36px;border-radius:50%;margin-right:12px;background:var(--cc-accent);display:flex;align-items:center;justify-content:center}.cc-header-avatar-placeholder svg{width:20px;height:20px;fill:var(--cc-on-accent)}.cc-header-info{flex:1}.cc-header-name{font-size:15px;font-weight:600;color:var(--cc-text)}.cc-header-subtitle{font-size:12px;color:var(--cc-text-secondary)}.cc-header-close{width:32px;height:32px;border-radius:8px;background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.cc-header-close:hover{background:var(--cc-bg-input)}.cc-header-close svg{width:16px;height:16px;fill:var(--cc-text-secondary)}.cc-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.cc-messages::-webkit-scrollbar{width:4px}.cc-messages::-webkit-scrollbar-track{background:transparent}.cc-messages::-webkit-scrollbar-thumb{background:var(--cc-border);border-radius:2px}.cc-message{max-width:85%;padding:10px 14px;border-radius:var(--cc-radius);word-wrap:break-word;overflow-wrap:break-word;font-size:14px;line-height:1.5;display:flex;flex-direction:column;gap:8px}.cc-message-user{align-self:flex-end;background:var(--cc-accent-contrast);color:var(--cc-on-accent);border-bottom-right-radius:4px}.cc-message-assistant{align-self:flex-start;background:var(--cc-bg-surface);color:var(--cc-text);border-bottom-left-radius:4px}.cc-message a{color:var(--cc-accent);text-decoration:underline}.cc-message-user a{color:var(--cc-on-accent)}.cc-message-error-note{margin-top:6px;font-size:12px;color:var(--cc-text-secondary)}.cc-message code{background:#0003;padding:1px 4px;border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:13px}.cc-message pre{background:#0000004d;padding:8px 12px;border-radius:8px;overflow-x:auto;margin:4px 0}.cc-message pre code{background:transparent;padding:0}.cc-message ul,.cc-message ol{padding-left:20px;margin:4px 0}.cc-message h1,.cc-message h2,.cc-message h3,.cc-message h4,.cc-message h5,.cc-message h6{font-size:15px;font-weight:600;line-height:1.4;margin:4px 0 0}.cc-message-actions{display:flex;align-items:center}.cc-message-retry,.cc-message-regenerate{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:transparent;border:1px solid var(--cc-border);border-radius:6px;color:var(--cc-text-secondary);font-family:var(--cc-font);font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,opacity .15s ease}.cc-message-retry:hover,.cc-message-regenerate:hover{background:var(--cc-bg-input);color:var(--cc-text)}.cc-message-retry svg,.cc-message-regenerate svg{fill:currentColor}.cc-message-regenerate{opacity:0}.cc-message:hover .cc-message-regenerate{opacity:1}.cc-message:focus-within .cc-message-regenerate,.cc-message-regenerate:focus-visible{opacity:1}@media(hover:none){.cc-message-regenerate{opacity:1}}.cc-message-retry{border-color:#ef4444;color:#ef4444}.cc-message-retry:hover{background:#ef44441a;color:#ef4444}.cc-typing{display:flex;align-items:center;gap:4px;padding:10px 14px;align-self:flex-start;background:var(--cc-bg-surface);border-radius:var(--cc-radius);border-bottom-left-radius:4px}.cc-typing-dot{width:6px;height:6px;border-radius:50%;background:var(--cc-text-secondary);animation:cc-bounce 1.2s infinite}.cc-typing-dot:nth-child(2){animation-delay:.2s}.cc-typing-dot:nth-child(3){animation-delay:.4s}@keyframes cc-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-4px)}}.cc-input-area{display:flex;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--cc-border);background:var(--cc-bg);gap:8px}.cc-input{flex:1;background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;line-height:1.5;resize:none;outline:none;max-height:96px;overflow-y:auto;transition:border-color .15s ease}.cc-input:focus{border-color:var(--cc-accent);outline:2px solid var(--cc-accent);outline-offset:1px}.cc-input::placeholder{color:var(--cc-text-secondary)}.cc-send-btn{width:36px;height:36px;border-radius:8px;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s ease,opacity .15s ease}.cc-send-btn:hover{background:var(--cc-accent-hover)}.cc-send-btn:disabled{opacity:.4;cursor:not-allowed}.cc-send-btn svg{width:16px;height:16px;fill:var(--cc-on-accent)}.cc-footer{padding:6px 16px;text-align:center;font-size:11px;color:var(--cc-text-secondary);border-top:1px solid var(--cc-border)}.cc-footer a{color:var(--cc-text-secondary);text-decoration:none}.cc-footer a:hover{text-decoration:underline}.cc-bubble{position:fixed;bottom:84px;right:20px;background:var(--cc-bg-surface);color:var(--cc-text);padding:10px 16px;border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);max-width:260px;font-size:13px;line-height:1.4;z-index:9998;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .3s ease,transform .3s ease;cursor:pointer}.cc-bubble.cc-visible{opacity:1;transform:translateY(0);pointer-events:auto}.cc-bubble.cc-left{right:auto;left:20px}.cc-bubble-close{position:absolute;top:-8px;right:-8px;width:20px;height:20px;border-radius:50%;background:var(--cc-bg);border:1px solid var(--cc-border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--cc-text-secondary)}.cc-hidden{display:none}.cc-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cc-messages-wrapper{flex:1;display:flex;flex-direction:column;min-height:0}.cc-prechat{flex:1;overflow-y:auto;padding:24px 16px;display:flex;flex-direction:column;gap:16px}.cc-prechat-title{font-size:18px;font-weight:600;color:var(--cc-text)}.cc-prechat-subtitle{font-size:13px;color:var(--cc-text-secondary);margin-top:-8px}.cc-prechat-fields{display:flex;flex-direction:column;gap:14px}.cc-prechat-field{display:flex;flex-direction:column;gap:4px}.cc-prechat-label{font-size:13px;font-weight:500;color:var(--cc-text)}.cc-prechat-label .cc-required{color:#ef4444;margin-left:2px}.cc-prechat-input,.cc-prechat-select,.cc-prechat-textarea{background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;outline:none;transition:border-color .15s ease}.cc-prechat-input:focus,.cc-prechat-select:focus,.cc-prechat-textarea:focus{border-color:var(--cc-accent)}.cc-prechat-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}.cc-prechat-textarea{resize:none;line-height:1.5}.cc-prechat-error{font-size:12px;color:#ef4444;min-height:0}.cc-prechat-error:empty{display:none}.cc-prechat-submit{background:var(--cc-accent-contrast);color:var(--cc-on-accent);border:none;border-radius:8px;padding:10px 16px;font-family:var(--cc-font);font-size:14px;font-weight:500;cursor:pointer;transition:background .15s ease;margin-top:8px}.cc-prechat-submit:hover{background:var(--cc-accent-hover)}.cc-prechat-submit:disabled{opacity:.5;cursor:not-allowed}@media(max-width:768px){.cc-fab.cc-open{display:none}.cc-panel:not(.cc-inline){width:100%;height:100%;max-height:100%;bottom:0;right:0;left:0;border-radius:0;border:none}.cc-panel.cc-left:not(.cc-inline){left:0}.cc-bubble{bottom:84px;right:16px;left:16px;max-width:none}}.cc-fab:focus-visible,.cc-header-close:focus-visible,.cc-send-btn:focus-visible,.cc-message-retry:focus-visible,.cc-message-regenerate:focus-visible,.cc-bubble-close:focus-visible,.cc-prechat-submit:focus-visible{outline:2px solid var(--cc-accent);outline-offset:3px}@media(prefers-reduced-motion:reduce){.cc-fab.cc-pulse,.cc-typing-dot{animation:none}.cc-panel,.cc-bubble{transition:none}}";
|
|
1
|
+
const launcherStyles = ':host{--cc-accent: var(--cc-theme-accent, #4f46e5);--cc-accent-hover: var(--cc-theme-accent-hover, #4338ca);--cc-on-accent: var(--cc-theme-on-accent, #ffffff);--cc-accent-contrast: var(--cc-theme-accent-contrast, #4f46e5);--cc-text: #1a1d23;--cc-bg: #ffffff;--cc-bg-surface: #f4f5f7;--cc-bg-input: #ffffff;--cc-border: #d9dce1;--cc-text-secondary: #5b616e;--cc-danger: #c62828;--cc-code-bg: rgb(0 0 0 / .07);--cc-pre-bg: rgb(0 0 0 / .05);--cc-shadow: 0 12px 40px rgb(16 24 40 / .18);--cc-font: system-ui, -apple-system, sans-serif;--cc-radius: 12px;--cc-fab-size: 56px;--cc-panel-width: 400px;--cc-panel-height: 560px;--cc-focus-ring: 0 0 0 2px var(--cc-bg), 0 0 0 4px var(--cc-accent);all:initial;color-scheme:light;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);-webkit-font-smoothing:antialiased}:host([data-cc-mode="dark"]){--cc-accent: var(--cc-theme-accent-dark, var(--cc-theme-accent, #6366f1));--cc-accent-hover: var(--cc-theme-accent-dark-hover, var(--cc-theme-accent-hover, #4f46e5));--cc-on-accent: var(--cc-theme-on-accent-dark, var(--cc-theme-on-accent, #ffffff));--cc-accent-contrast: var(--cc-theme-accent-dark-contrast, var(--cc-theme-accent-contrast, #6164ec));--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-danger: #ef4444;--cc-code-bg: rgba(0, 0, 0, .2);--cc-pre-bg: rgba(0, 0, 0, .3);--cc-shadow: 0 8px 32px rgba(0,0,0,.4);color-scheme:dark}@media(prefers-color-scheme:dark){:host(:not([data-cc-mode])),:host([data-cc-mode="auto"]){--cc-accent: var(--cc-theme-accent-dark, var(--cc-theme-accent, #6366f1));--cc-accent-hover: var(--cc-theme-accent-dark-hover, var(--cc-theme-accent-hover, #4f46e5));--cc-on-accent: var(--cc-theme-on-accent-dark, var(--cc-theme-on-accent, #ffffff));--cc-accent-contrast: var(--cc-theme-accent-dark-contrast, var(--cc-theme-accent-contrast, #6164ec));--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-danger: #ef4444;--cc-code-bg: rgba(0, 0, 0, .2);--cc-pre-bg: rgba(0, 0, 0, .3);--cc-shadow: 0 8px 32px rgba(0,0,0,.4);color-scheme:dark}}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}.cc-fab{position:fixed;bottom:20px;right:20px;width:var(--cc-fab-size);height:var(--cc-fab-size);border-radius:50%;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #0000004d;transition:transform .2s,box-shadow .2s;z-index:10000;outline:none}.cc-fab:hover{transform:scale(1.05);box-shadow:0 6px 24px #0006}.cc-fab:active{transform:scale(.95)}.cc-fab.cc-left{right:auto;left:20px}.cc-fab svg{width:24px;height:24px;fill:var(--cc-on-accent);transition:transform .3s}.cc-fab.cc-open svg{transform:rotate(90deg)}.cc-fab-badge{position:absolute;top:-2px;inset-inline-end:-2px;min-width:18px;height:18px;padding:0 4px;border-radius:9px;background:#ef4444;border:2px solid var(--cc-bg);color:#fff;font-size:11px;font-weight:700;line-height:1;display:none;align-items:center;justify-content:center;pointer-events:none}.cc-fab-badge.cc-visible{display:flex}@keyframes cc-pulse{0%,to{box-shadow:0 4px 16px #0000004d}50%{box-shadow:0 4px 16px color-mix(in srgb,var(--cc-accent) 50%,transparent)}}.cc-fab.cc-pulse{animation:cc-pulse 2s ease-in-out 3}.cc-fab:focus-visible{outline:2px solid transparent;box-shadow:var(--cc-focus-ring),0 4px 16px #0000004d;animation:none}@media(prefers-reduced-motion:reduce){.cc-fab.cc-pulse{animation:none}}';
|
|
2
|
+
const widgetStyles = '.cc-panel{position:fixed;bottom:88px;right:20px;width:var(--cc-panel-width);height:var(--cc-panel-height);max-height:calc(100vh - 100px);background:var(--cc-bg);border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);display:flex;flex-direction:column;overflow:hidden;z-index:9999;opacity:0;transform:translateY(16px) scale(.95);pointer-events:none;transition:opacity .2s,transform .2s}.cc-panel.cc-visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.cc-panel.cc-left{right:auto;left:20px}.cc-panel.cc-inline{position:relative;bottom:auto;inset-inline:auto;width:100%;height:100%;max-height:100%;z-index:auto;transform:none;transition:none}.cc-panel.cc-inline:not(.cc-visible){visibility:hidden}.cc-header{display:flex;align-items:center;padding:16px;background:var(--cc-bg-surface);border-bottom:1px solid var(--cc-border);flex-shrink:0}.cc-header-avatar,.cc-header-avatar-placeholder{width:36px;height:36px;border-radius:50%;margin-inline-end:12px}.cc-header-avatar{object-fit:cover}.cc-header-avatar-placeholder{background:var(--cc-accent);display:flex;align-items:center;justify-content:center}.cc-header-avatar-placeholder svg{width:20px;height:20px;fill:var(--cc-on-accent)}.cc-header-info{flex:1}.cc-header-name{font-size:15px;font-weight:600;color:var(--cc-text)}.cc-header-subtitle{font-size:12px;color:var(--cc-text-secondary)}.cc-header-close{width:32px;height:32px;border-radius:8px;background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s}.cc-header-close:hover{background:var(--cc-bg-input)}.cc-header-close svg{width:16px;height:16px;fill:var(--cc-text-secondary)}.cc-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.cc-messages::-webkit-scrollbar{width:4px}.cc-messages::-webkit-scrollbar-track{background:transparent}.cc-messages::-webkit-scrollbar-thumb{background:var(--cc-border);border-radius:2px}.cc-message{max-width:85%;padding:10px 14px;border-radius:var(--cc-radius);word-wrap:break-word;overflow-wrap:break-word;font-size:14px;line-height:1.5;display:flex;flex-direction:column;gap:8px}.cc-message-user{align-self:flex-end;background:var(--cc-accent-contrast);color:var(--cc-on-accent);border-end-end-radius:4px}.cc-message-assistant{align-self:flex-start;background:var(--cc-bg-surface);color:var(--cc-text);border-end-start-radius:4px}.cc-suggestions{display:flex;flex-wrap:wrap;gap:8px;padding:0 16px 12px}.cc-suggestion{border:1px solid var(--cc-border);border-radius:var(--cc-radius);padding:8px 12px;background:var(--cc-bg-surface);color:var(--cc-text);cursor:pointer;font:inherit}.cc-suggestion:hover{border-color:var(--cc-accent)}.cc-message a{color:var(--cc-accent);text-decoration:underline}.cc-message-user a{color:var(--cc-on-accent)}.cc-disclosure{margin-block-end:4px;padding:8px 12px;border:1px solid var(--cc-border);border-radius:10px;font-size:12px;color:var(--cc-text-secondary)}.cc-disclosure a{color:inherit;text-decoration:underline}.cc-message-error-note{margin-top:6px;font-size:12px;color:var(--cc-text-secondary)}.cc-message-sources{margin-block-start:8px;font-size:12px;color:var(--cc-text-secondary)}.cc-message-sources ol{margin:0;padding-inline-start:1.2em}.cc-message-sources a{color:var(--cc-text-secondary);text-decoration:underline}.cc-message code{background:var(--cc-code-bg);padding:1px 4px;border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:13px}.cc-message pre{background:var(--cc-pre-bg);padding:8px 12px;border-radius:8px;overflow-x:auto;margin:4px 0}.cc-message pre code{background:transparent;padding:0}.cc-message ul,.cc-message ol{padding-inline-start:20px;margin:4px 0}.cc-message h1,.cc-message h2,.cc-message h3,.cc-message h4,.cc-message h5,.cc-message h6{font-size:15px;font-weight:600;line-height:1.4;margin:4px 0 0}.cc-message-actions{display:flex;align-items:center}.cc-message-retry,.cc-message-regenerate{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:transparent;border:1px solid var(--cc-border);border-radius:6px;color:var(--cc-text-secondary);font-family:var(--cc-font);font-size:12px;cursor:pointer;transition:background .15s,color .15s,opacity .15s}.cc-message-retry:hover,.cc-message-regenerate:hover{background:var(--cc-bg-input);color:var(--cc-text)}.cc-message-retry svg,.cc-message-regenerate svg{fill:currentColor}.cc-message-regenerate{opacity:0}.cc-message:hover .cc-message-regenerate{opacity:1}.cc-message:focus-within .cc-message-regenerate,.cc-message-regenerate:focus-visible{opacity:1}@media(hover:none){.cc-message-regenerate{opacity:1}}.cc-message-retry{border-color:var(--cc-danger);color:var(--cc-danger)}.cc-message-retry:hover{background:color-mix(in srgb,var(--cc-danger) 10%,transparent);color:var(--cc-danger)}.cc-typing{display:flex;align-items:center;gap:4px;padding:10px 14px;align-self:flex-start;background:var(--cc-bg-surface);border-radius:var(--cc-radius);border-end-start-radius:4px}.cc-typing-dot{width:6px;height:6px;border-radius:50%;background:var(--cc-text-secondary);animation:cc-bounce 1.2s infinite}.cc-typing-dot:nth-child(2){animation-delay:.2s}.cc-typing-dot:nth-child(3){animation-delay:.4s}@keyframes cc-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-4px)}}.cc-input-area{display:flex;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--cc-border);background:var(--cc-bg);gap:8px}.cc-input{flex:1;background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;line-height:1.5;resize:none;outline:none;max-height:96px;overflow-y:auto;transition:border-color .15s}.cc-input:focus{border-color:var(--cc-accent)}.cc-input::placeholder{color:var(--cc-text-secondary)}.cc-send-btn{width:36px;height:36px;border-radius:8px;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s,opacity .15s}.cc-send-btn:hover{background:var(--cc-accent-hover)}.cc-send-btn:disabled{opacity:.4;cursor:not-allowed}.cc-send-btn svg{width:16px;height:16px;fill:var(--cc-on-accent)}:host([dir="rtl"]) .cc-send-btn svg{transform:scaleX(-1)}.cc-footer{padding:6px 16px;text-align:center;font-size:11px;color:var(--cc-text-secondary);border-top:1px solid var(--cc-border)}.cc-footer a{color:var(--cc-text-secondary);text-decoration:none}.cc-footer a:hover{text-decoration:underline}.cc-bubble{position:fixed;bottom:84px;right:20px;background:var(--cc-bg-surface);color:var(--cc-text);padding:10px 16px;border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);max-width:260px;font-size:13px;line-height:1.4;z-index:9998;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .3s,transform .3s}.cc-bubble.cc-visible{opacity:1;transform:translateY(0);pointer-events:auto}.cc-bubble.cc-left{right:auto;left:20px}.cc-bubble-trigger{-webkit-appearance:none;-moz-appearance:none;appearance:none;display:block;width:100%;padding:0;margin:0;border:0;background:transparent;color:inherit;font:inherit;line-height:inherit;text-align:start;cursor:pointer}.cc-bubble-close{position:absolute;top:-8px;inset-inline-end:-8px;width:20px;height:20px;border-radius:50%;background:var(--cc-bg);border:1px solid var(--cc-border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--cc-text-secondary)}.cc-hidden{display:none}.cc-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cc-messages-wrapper{flex:1;display:flex;flex-direction:column;min-height:0}@media(max-width:640px){.cc-fab.cc-open{display:none}.cc-panel:not(.cc-inline){width:100%;height:100%;height:100dvh;max-height:100%;bottom:0;inset-inline:0;border-radius:0;border:none;padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom)}.cc-panel.cc-left:not(.cc-inline){left:0}.cc-input{font-size:16px}.cc-bubble{bottom:84px;inset-inline:16px;max-width:none}}.cc-header-close:focus-visible,.cc-send-btn:focus-visible,.cc-suggestion:focus-visible,.cc-message-retry:focus-visible,.cc-message-regenerate:focus-visible,.cc-bubble-trigger:focus-visible,.cc-bubble-close:focus-visible,.cc-input:focus-visible{outline:2px solid transparent;box-shadow:var(--cc-focus-ring)}@media(prefers-reduced-motion:reduce){.cc-typing-dot{animation:none}.cc-panel,.cc-bubble{transition:none}}';
|
|
5
3
|
const HOST_ID = "chatcops-root";
|
|
6
|
-
|
|
4
|
+
const RTL_LANGUAGES = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur"]);
|
|
5
|
+
function setLanguageDirection(code, dir) {
|
|
6
|
+
const language = code.toLowerCase().split(/[-_]/)[0];
|
|
7
|
+
if (dir === "rtl") RTL_LANGUAGES.add(language);
|
|
8
|
+
else RTL_LANGUAGES.delete(language);
|
|
9
|
+
}
|
|
10
|
+
function resolveDirection(configured, locale) {
|
|
11
|
+
if (configured) return configured;
|
|
12
|
+
const language = locale == null ? void 0 : locale.toLowerCase().split(/[-_]/)[0];
|
|
13
|
+
if (language && RTL_LANGUAGES.has(language)) return "rtl";
|
|
14
|
+
return (document.dir || document.documentElement.dir) === "rtl" ? "rtl" : "ltr";
|
|
15
|
+
}
|
|
16
|
+
function createShadowRoot(container, options = {}) {
|
|
7
17
|
const host = document.createElement("div");
|
|
8
18
|
host.id = nextHostId();
|
|
9
19
|
host.className = HOST_ID;
|
|
20
|
+
host.dir = resolveDirection(options.dir, options.locale);
|
|
21
|
+
if (options.locale) host.lang = options.locale;
|
|
10
22
|
if (container) {
|
|
11
23
|
container.appendChild(host);
|
|
12
24
|
} else {
|
|
@@ -14,7 +26,7 @@ function createShadowRoot(container) {
|
|
|
14
26
|
}
|
|
15
27
|
const shadow = host.attachShadow({ mode: "open" });
|
|
16
28
|
const style = document.createElement("style");
|
|
17
|
-
style.textContent = widgetStyles;
|
|
29
|
+
style.textContent = launcherStyles + widgetStyles;
|
|
18
30
|
shadow.appendChild(style);
|
|
19
31
|
return shadow;
|
|
20
32
|
}
|
|
@@ -27,6 +39,8 @@ function nextHostId() {
|
|
|
27
39
|
while (document.getElementById(`${HOST_ID}-${n}`)) n += 1;
|
|
28
40
|
return `${HOST_ID}-${n}`;
|
|
29
41
|
}
|
|
42
|
+
const CHAT_ICON = `<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h10v2H7zm0-3h10v2H7z"/></svg>`;
|
|
43
|
+
const CLOSE_ICON$1 = `<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
|
|
30
44
|
const ESCAPE_MAP = {
|
|
31
45
|
"&": "&",
|
|
32
46
|
"<": "<",
|
|
@@ -91,31 +105,108 @@ function renderMarkdown(input) {
|
|
|
91
105
|
}
|
|
92
106
|
const RETRY_ICON = '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M17.65 6.35A7.96 7.96 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>';
|
|
93
107
|
const REGENERATE_ICON = '<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0020 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 004 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>';
|
|
108
|
+
const FOLLOW_SLACK = 48;
|
|
109
|
+
const STREAM_INTERVAL_MS = 50;
|
|
94
110
|
class Messages {
|
|
95
|
-
constructor(parent, actions) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
111
|
+
constructor(parent, actions, options = {}) {
|
|
112
|
+
this.streams = /* @__PURE__ */ new Map();
|
|
113
|
+
this.typingEl = null;
|
|
114
|
+
this.suggestionsEl = null;
|
|
115
|
+
this.disclosureEl = null;
|
|
99
116
|
this.actions = actions;
|
|
117
|
+
this.streamIntervalMs = options.streamIntervalMs ?? STREAM_INTERVAL_MS;
|
|
100
118
|
this.container = document.createElement("div");
|
|
101
119
|
this.container.className = "cc-messages";
|
|
102
120
|
this.container.setAttribute("role", "log");
|
|
103
121
|
this.container.setAttribute("aria-live", "polite");
|
|
104
122
|
parent.appendChild(this.container);
|
|
123
|
+
this.announcement = document.createElement("div");
|
|
124
|
+
this.announcement.className = "cc-reply-announcement cc-sr-only";
|
|
125
|
+
this.announcement.setAttribute("aria-live", "polite");
|
|
126
|
+
this.announcement.setAttribute("aria-atomic", "true");
|
|
127
|
+
parent.appendChild(this.announcement);
|
|
128
|
+
}
|
|
129
|
+
beginReply() {
|
|
130
|
+
this.announcement.textContent = "";
|
|
131
|
+
this.container.setAttribute("aria-live", "off");
|
|
132
|
+
}
|
|
133
|
+
finishReply(msg) {
|
|
134
|
+
var _a, _b, _c, _d;
|
|
135
|
+
if ((msg == null ? void 0 : msg.role) === "assistant" && (msg.status === "complete" || msg.status === "error")) {
|
|
136
|
+
const rendered = this.findMessage(msg.id);
|
|
137
|
+
const content = ((_b = (_a = rendered == null ? void 0 : rendered.querySelector(".cc-message-content")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) ?? "";
|
|
138
|
+
const error = ((_d = (_c = rendered == null ? void 0 : rendered.querySelector(".cc-message-error-note")) == null ? void 0 : _c.textContent) == null ? void 0 : _d.trim()) ?? "";
|
|
139
|
+
this.announcement.textContent = [content, error && error !== content ? error : ""].filter(Boolean).join(" ");
|
|
140
|
+
}
|
|
141
|
+
this.container.setAttribute("aria-live", "polite");
|
|
105
142
|
}
|
|
106
143
|
addMessage(msg) {
|
|
144
|
+
const follow = msg.role === "user" || this.nearBottom();
|
|
107
145
|
this.removeTyping();
|
|
146
|
+
if (msg.role === "user") this.removeSuggestions();
|
|
108
147
|
const el = this.createMessageElement(msg);
|
|
109
148
|
this.container.appendChild(el);
|
|
110
|
-
this.scrollToBottom();
|
|
149
|
+
if (follow) this.scrollToBottom();
|
|
111
150
|
return el;
|
|
112
151
|
}
|
|
113
152
|
updateMessage(msg) {
|
|
114
153
|
const el = this.findMessage(msg.id);
|
|
115
|
-
if (el)
|
|
116
|
-
|
|
117
|
-
this.
|
|
154
|
+
if (!el) return;
|
|
155
|
+
if (msg.role === "assistant" && msg.status === "streaming" && el.dataset.status === "streaming") {
|
|
156
|
+
this.stream(el, msg);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.endStream(msg.id);
|
|
160
|
+
const follow = this.nearBottom();
|
|
161
|
+
this.renderMessageElement(el, msg);
|
|
162
|
+
if (follow) this.scrollToBottom();
|
|
163
|
+
}
|
|
164
|
+
/** Takes the reply's text so far, and renders what changed on the next frame. */
|
|
165
|
+
stream(el, msg) {
|
|
166
|
+
let stream = this.streams.get(msg.id);
|
|
167
|
+
if (!stream || !appended(stream.text, msg.content)) {
|
|
168
|
+
this.endStream(msg.id);
|
|
169
|
+
const content = el.querySelector(".cc-message-content");
|
|
170
|
+
if (!content) return;
|
|
171
|
+
content.innerHTML = "";
|
|
172
|
+
const tail = document.createElement("div");
|
|
173
|
+
tail.className = "cc-stream-tail";
|
|
174
|
+
content.appendChild(tail);
|
|
175
|
+
stream = { text: msg.content, committed: 0, tail, renderedAt: 0 };
|
|
176
|
+
this.streams.set(msg.id, stream);
|
|
177
|
+
this.renderStream(stream);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
stream.text = msg.content;
|
|
181
|
+
if (stream.frame === void 0) this.schedule(stream);
|
|
182
|
+
}
|
|
183
|
+
/** Renders on the next frame that's at least the interval after the last render. */
|
|
184
|
+
schedule(stream) {
|
|
185
|
+
stream.frame = nextFrame(() => {
|
|
186
|
+
stream.frame = void 0;
|
|
187
|
+
if (performance.now() - stream.renderedAt < this.streamIntervalMs) this.schedule(stream);
|
|
188
|
+
else this.renderStream(stream);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
renderStream(stream) {
|
|
192
|
+
stream.renderedAt = performance.now();
|
|
193
|
+
const follow = this.nearBottom();
|
|
194
|
+
const boundary = lastBlockBoundary(stream.text, stream.committed);
|
|
195
|
+
if (boundary > stream.committed) {
|
|
196
|
+
stream.tail.insertAdjacentHTML("beforebegin", `<div class="cc-block">${renderMarkdown(stream.text.slice(stream.committed, boundary))}</div>`);
|
|
197
|
+
stream.committed = boundary;
|
|
118
198
|
}
|
|
199
|
+
stream.tail.innerHTML = renderMarkdown(stream.text.slice(stream.committed));
|
|
200
|
+
if (follow) this.scrollToBottom();
|
|
201
|
+
}
|
|
202
|
+
endStream(id) {
|
|
203
|
+
const stream = this.streams.get(id);
|
|
204
|
+
if ((stream == null ? void 0 : stream.frame) !== void 0) cancelFrame(stream.frame);
|
|
205
|
+
this.streams.delete(id);
|
|
206
|
+
}
|
|
207
|
+
nearBottom() {
|
|
208
|
+
const { scrollHeight, scrollTop, clientHeight } = this.container;
|
|
209
|
+
return scrollHeight - scrollTop - clientHeight <= FOLLOW_SLACK;
|
|
119
210
|
}
|
|
120
211
|
showTyping() {
|
|
121
212
|
var _a;
|
|
@@ -132,16 +223,68 @@ class Messages {
|
|
|
132
223
|
this.typingEl = null;
|
|
133
224
|
}
|
|
134
225
|
}
|
|
226
|
+
showSuggestions(suggestions, label, onSelect) {
|
|
227
|
+
this.removeSuggestions();
|
|
228
|
+
if (suggestions.length === 0) return;
|
|
229
|
+
const group = document.createElement("div");
|
|
230
|
+
group.className = "cc-suggestions";
|
|
231
|
+
group.setAttribute("role", "group");
|
|
232
|
+
group.setAttribute("aria-label", label);
|
|
233
|
+
for (const suggestion of suggestions) {
|
|
234
|
+
const button = document.createElement("button");
|
|
235
|
+
button.type = "button";
|
|
236
|
+
button.className = "cc-suggestion";
|
|
237
|
+
button.textContent = suggestion;
|
|
238
|
+
button.addEventListener("click", () => onSelect(suggestion));
|
|
239
|
+
group.appendChild(button);
|
|
240
|
+
}
|
|
241
|
+
this.container.appendChild(group);
|
|
242
|
+
this.suggestionsEl = group;
|
|
243
|
+
}
|
|
244
|
+
removeSuggestions() {
|
|
245
|
+
var _a;
|
|
246
|
+
(_a = this.suggestionsEl) == null ? void 0 : _a.remove();
|
|
247
|
+
this.suggestionsEl = null;
|
|
248
|
+
}
|
|
135
249
|
clear() {
|
|
250
|
+
for (const id of [...this.streams.keys()]) this.endStream(id);
|
|
136
251
|
this.container.innerHTML = "";
|
|
137
252
|
this.typingEl = null;
|
|
253
|
+
if (this.disclosureEl) this.container.prepend(this.disclosureEl);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The note that visitors are talking to an AI, always first in the log.
|
|
257
|
+
* Only http(s) links become a "Learn more" link.
|
|
258
|
+
*/
|
|
259
|
+
setDisclosure(text, learnMore) {
|
|
260
|
+
var _a;
|
|
261
|
+
(_a = this.disclosureEl) == null ? void 0 : _a.remove();
|
|
262
|
+
this.disclosureEl = null;
|
|
263
|
+
if (!text) return;
|
|
264
|
+
const note = document.createElement("div");
|
|
265
|
+
note.className = "cc-disclosure";
|
|
266
|
+
note.setAttribute("role", "note");
|
|
267
|
+
note.textContent = text;
|
|
268
|
+
if (learnMore && /^https?:\/\//i.test(learnMore.url)) {
|
|
269
|
+
const link = document.createElement("a");
|
|
270
|
+
link.href = learnMore.url;
|
|
271
|
+
link.target = "_blank";
|
|
272
|
+
link.rel = "noopener noreferrer";
|
|
273
|
+
link.textContent = learnMore.label;
|
|
274
|
+
note.append(" ", link);
|
|
275
|
+
}
|
|
276
|
+
this.disclosureEl = note;
|
|
277
|
+
this.container.prepend(note);
|
|
138
278
|
}
|
|
139
279
|
removeMessage(id) {
|
|
280
|
+
this.endStream(id);
|
|
140
281
|
const el = this.findMessage(id);
|
|
141
282
|
el == null ? void 0 : el.remove();
|
|
142
283
|
}
|
|
143
284
|
clearRegenerateButtons() {
|
|
144
|
-
this.container.querySelectorAll(".cc-message-regenerate").forEach((button) =>
|
|
285
|
+
this.container.querySelectorAll(".cc-message-regenerate").forEach((button) => {
|
|
286
|
+
button.remove();
|
|
287
|
+
});
|
|
145
288
|
this.cleanupEmptyActionContainers();
|
|
146
289
|
}
|
|
147
290
|
showRegenerateButton(messageId) {
|
|
@@ -177,8 +320,12 @@ class Messages {
|
|
|
177
320
|
return el;
|
|
178
321
|
}
|
|
179
322
|
renderMessageElement(el, msg) {
|
|
323
|
+
var _a;
|
|
180
324
|
el.className = `cc-message cc-message-${msg.role}`;
|
|
181
325
|
el.dataset.id = msg.id;
|
|
326
|
+
if (msg.role === "assistant" && msg.id !== "welcome") {
|
|
327
|
+
el.dataset.aiGenerated = "true";
|
|
328
|
+
}
|
|
182
329
|
if (msg.status) {
|
|
183
330
|
el.dataset.status = msg.status;
|
|
184
331
|
} else {
|
|
@@ -199,6 +346,31 @@ class Messages {
|
|
|
199
346
|
content.className = "cc-message-content";
|
|
200
347
|
content.innerHTML = msg.role === "assistant" ? renderMarkdown(msg.content) : this.escapeHtml(msg.content);
|
|
201
348
|
el.appendChild(content);
|
|
349
|
+
if (msg.role === "assistant" && Array.isArray(msg.sources) && msg.sources.length > 0) {
|
|
350
|
+
const safeUrls = msg.sources.filter((s) => typeof s.url === "string" && (s.url.startsWith("http://") || s.url.startsWith("https://")));
|
|
351
|
+
if (safeUrls.length > 0) {
|
|
352
|
+
const nav = document.createElement("nav");
|
|
353
|
+
nav.className = "cc-message-sources";
|
|
354
|
+
nav.setAttribute("aria-label", ((_a = this.actions) == null ? void 0 : _a.sourcesLabel) ?? "Sources");
|
|
355
|
+
const ol = document.createElement("ol");
|
|
356
|
+
for (const source of safeUrls) {
|
|
357
|
+
const li = document.createElement("li");
|
|
358
|
+
const a = document.createElement("a");
|
|
359
|
+
a.href = source.url;
|
|
360
|
+
a.target = "_blank";
|
|
361
|
+
a.rel = "noopener noreferrer";
|
|
362
|
+
try {
|
|
363
|
+
a.textContent = source.title ?? new URL(source.url).hostname;
|
|
364
|
+
} catch {
|
|
365
|
+
a.textContent = source.title ?? source.url;
|
|
366
|
+
}
|
|
367
|
+
li.appendChild(a);
|
|
368
|
+
ol.appendChild(li);
|
|
369
|
+
}
|
|
370
|
+
nav.appendChild(ol);
|
|
371
|
+
el.appendChild(nav);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
202
374
|
if (msg.status === "error" && msg.errorText) {
|
|
203
375
|
const note = document.createElement("div");
|
|
204
376
|
note.className = "cc-message-error-note";
|
|
@@ -259,13 +431,34 @@ class Messages {
|
|
|
259
431
|
return div.innerHTML;
|
|
260
432
|
}
|
|
261
433
|
}
|
|
434
|
+
function lastBlockBoundary(text, from) {
|
|
435
|
+
let inFence = false;
|
|
436
|
+
let last = from;
|
|
437
|
+
let i = from;
|
|
438
|
+
while (i < text.length) {
|
|
439
|
+
if (text.startsWith("```", i)) {
|
|
440
|
+
inFence = !inFence;
|
|
441
|
+
i += 3;
|
|
442
|
+
} else if (!inFence && text.startsWith("\n\n", i)) {
|
|
443
|
+
i += 2;
|
|
444
|
+
while (text[i] === "\n") i++;
|
|
445
|
+
last = i;
|
|
446
|
+
} else {
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return last;
|
|
451
|
+
}
|
|
452
|
+
function appended(previous, next) {
|
|
453
|
+
if (next.length < previous.length) return false;
|
|
454
|
+
const from = Math.max(0, previous.length - 32);
|
|
455
|
+
return next.slice(from, previous.length) === previous.slice(from);
|
|
456
|
+
}
|
|
457
|
+
const nextFrame = (fn) => typeof requestAnimationFrame === "function" ? requestAnimationFrame(fn) : setTimeout(fn, 16);
|
|
458
|
+
const cancelFrame = (id) => typeof cancelAnimationFrame === "function" ? cancelAnimationFrame(id) : clearTimeout(id);
|
|
262
459
|
const MAX_MESSAGE_LENGTH = 1e4;
|
|
263
460
|
class Input {
|
|
264
461
|
constructor(parent, options) {
|
|
265
|
-
__publicField(this, "area");
|
|
266
|
-
__publicField(this, "textarea");
|
|
267
|
-
__publicField(this, "sendBtn");
|
|
268
|
-
__publicField(this, "onSend");
|
|
269
462
|
this.onSend = options.onSend;
|
|
270
463
|
this.area = document.createElement("div");
|
|
271
464
|
this.area.className = "cc-input-area";
|
|
@@ -281,7 +474,7 @@ class Input {
|
|
|
281
474
|
this.sendBtn.className = "cc-send-btn";
|
|
282
475
|
this.sendBtn.disabled = true;
|
|
283
476
|
this.sendBtn.setAttribute("aria-label", options.sendLabel);
|
|
284
|
-
this.sendBtn.innerHTML = '<svg viewBox="0 0 24 24"
|
|
477
|
+
this.sendBtn.innerHTML = '<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>';
|
|
285
478
|
this.sendBtn.addEventListener("click", () => this.send());
|
|
286
479
|
this.textarea.addEventListener("input", () => {
|
|
287
480
|
this.sendBtn.disabled = this.textarea.value.trim().length === 0;
|
|
@@ -290,6 +483,11 @@ class Input {
|
|
|
290
483
|
this.area.appendChild(this.sendBtn);
|
|
291
484
|
parent.appendChild(this.area);
|
|
292
485
|
}
|
|
486
|
+
setLabels(placeholder, sendLabel) {
|
|
487
|
+
this.textarea.placeholder = placeholder;
|
|
488
|
+
this.textarea.setAttribute("aria-label", placeholder);
|
|
489
|
+
this.sendBtn.setAttribute("aria-label", sendLabel);
|
|
490
|
+
}
|
|
293
491
|
focus() {
|
|
294
492
|
this.textarea.focus();
|
|
295
493
|
}
|
|
@@ -325,15 +523,10 @@ class Input {
|
|
|
325
523
|
}
|
|
326
524
|
}
|
|
327
525
|
const PANEL_ID = "cc-panel";
|
|
328
|
-
const BOT_ICON = `<svg viewBox="0 0 24 24"
|
|
329
|
-
const CLOSE_ICON
|
|
526
|
+
const BOT_ICON = `<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg>`;
|
|
527
|
+
const CLOSE_ICON = `<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
|
|
330
528
|
class Panel {
|
|
331
529
|
constructor(root, options) {
|
|
332
|
-
__publicField(this, "el");
|
|
333
|
-
__publicField(this, "inline");
|
|
334
|
-
__publicField(this, "messages");
|
|
335
|
-
__publicField(this, "input");
|
|
336
|
-
__publicField(this, "messagesContainer");
|
|
337
530
|
this.inline = options.inline ?? false;
|
|
338
531
|
this.el = document.createElement("div");
|
|
339
532
|
this.el.className = "cc-panel";
|
|
@@ -346,39 +539,38 @@ class Panel {
|
|
|
346
539
|
if (!options.inline) {
|
|
347
540
|
this.el.id = PANEL_ID;
|
|
348
541
|
this.el.setAttribute("role", "dialog");
|
|
349
|
-
this.el.setAttribute("aria-
|
|
542
|
+
this.el.setAttribute("aria-modal", "true");
|
|
350
543
|
this.el.addEventListener("keydown", (e) => {
|
|
351
|
-
if (e.key === "Escape")
|
|
544
|
+
if (e.key === "Escape") {
|
|
545
|
+
options.onClose();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (e.key !== "Tab" || this.el.hasAttribute("inert")) return;
|
|
549
|
+
const focusable = Array.from(this.el.querySelectorAll("*")).filter(
|
|
550
|
+
(element) => element.tabIndex >= 0 && !element.matches(":disabled") && !element.closest('[hidden], [inert], [aria-hidden="true"], .cc-hidden')
|
|
551
|
+
);
|
|
552
|
+
if (focusable.length === 0) {
|
|
553
|
+
e.preventDefault();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const activeElement = this.el.getRootNode().activeElement;
|
|
557
|
+
const first = focusable[0];
|
|
558
|
+
const last = focusable[focusable.length - 1];
|
|
559
|
+
if (e.shiftKey && (activeElement === first || !this.el.contains(activeElement))) {
|
|
560
|
+
e.preventDefault();
|
|
561
|
+
last.focus();
|
|
562
|
+
} else if (!e.shiftKey && (activeElement === last || !this.el.contains(activeElement))) {
|
|
563
|
+
e.preventDefault();
|
|
564
|
+
first.focus();
|
|
565
|
+
}
|
|
352
566
|
});
|
|
353
567
|
this.setHidden(true);
|
|
354
568
|
}
|
|
355
|
-
|
|
356
|
-
header
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
img.src = options.branding.avatar;
|
|
361
|
-
img.alt = options.branding.name;
|
|
362
|
-
header.appendChild(img);
|
|
363
|
-
} else {
|
|
364
|
-
const placeholder = document.createElement("div");
|
|
365
|
-
placeholder.className = "cc-header-avatar-placeholder";
|
|
366
|
-
placeholder.innerHTML = BOT_ICON;
|
|
367
|
-
header.appendChild(placeholder);
|
|
368
|
-
}
|
|
369
|
-
const info = document.createElement("div");
|
|
370
|
-
info.className = "cc-header-info";
|
|
371
|
-
info.innerHTML = `<div class="cc-header-name">${this.escapeHtml(options.branding.name)}</div><div class="cc-header-subtitle">${this.escapeHtml(options.branding.subtitle)}</div>`;
|
|
372
|
-
header.appendChild(info);
|
|
373
|
-
if (!options.inline) {
|
|
374
|
-
const closeBtn = document.createElement("button");
|
|
375
|
-
closeBtn.className = "cc-header-close";
|
|
376
|
-
closeBtn.setAttribute("aria-label", options.closeLabel);
|
|
377
|
-
closeBtn.innerHTML = CLOSE_ICON$1;
|
|
378
|
-
closeBtn.addEventListener("click", options.onClose);
|
|
379
|
-
header.appendChild(closeBtn);
|
|
380
|
-
}
|
|
381
|
-
this.el.appendChild(header);
|
|
569
|
+
this.options = options;
|
|
570
|
+
this.header = document.createElement("div");
|
|
571
|
+
this.header.className = "cc-header";
|
|
572
|
+
this.renderHeader();
|
|
573
|
+
this.el.appendChild(this.header);
|
|
382
574
|
if (options.preChatEnabled) {
|
|
383
575
|
this.messagesContainer = document.createElement("div");
|
|
384
576
|
this.messagesContainer.className = "cc-messages-wrapper";
|
|
@@ -392,12 +584,51 @@ class Panel {
|
|
|
392
584
|
sendLabel: options.sendLabel,
|
|
393
585
|
onSend: options.onSend
|
|
394
586
|
});
|
|
395
|
-
|
|
396
|
-
footer.className = "cc-footer";
|
|
397
|
-
footer.innerHTML = options.footerText;
|
|
398
|
-
this.el.appendChild(footer);
|
|
587
|
+
this.footer = document.createElement("div");
|
|
588
|
+
this.footer.className = "cc-footer";
|
|
589
|
+
this.footer.innerHTML = options.footerText;
|
|
590
|
+
this.el.appendChild(this.footer);
|
|
399
591
|
root.appendChild(this.el);
|
|
400
592
|
}
|
|
593
|
+
/** New wording or branding, applied in place. */
|
|
594
|
+
update(labels, actionLabels) {
|
|
595
|
+
Object.assign(this.options, labels);
|
|
596
|
+
if (this.options.messageActions) Object.assign(this.options.messageActions, actionLabels);
|
|
597
|
+
this.renderHeader();
|
|
598
|
+
this.input.setLabels(labels.placeholder, labels.sendLabel);
|
|
599
|
+
this.footer.innerHTML = labels.footerText;
|
|
600
|
+
}
|
|
601
|
+
renderHeader() {
|
|
602
|
+
const { branding, closeLabel, inline, onClose } = this.options;
|
|
603
|
+
const hadFocus = this.header.contains(this.el.getRootNode().activeElement);
|
|
604
|
+
this.header.replaceChildren();
|
|
605
|
+
if (!inline) this.el.setAttribute("aria-label", branding.name);
|
|
606
|
+
if (branding.avatar) {
|
|
607
|
+
const img = document.createElement("img");
|
|
608
|
+
img.className = "cc-header-avatar";
|
|
609
|
+
img.src = branding.avatar;
|
|
610
|
+
img.alt = branding.name;
|
|
611
|
+
this.header.appendChild(img);
|
|
612
|
+
} else {
|
|
613
|
+
const placeholder = document.createElement("div");
|
|
614
|
+
placeholder.className = "cc-header-avatar-placeholder";
|
|
615
|
+
placeholder.innerHTML = BOT_ICON;
|
|
616
|
+
this.header.appendChild(placeholder);
|
|
617
|
+
}
|
|
618
|
+
const info = document.createElement("div");
|
|
619
|
+
info.className = "cc-header-info";
|
|
620
|
+
info.innerHTML = `<div class="cc-header-name">${this.escapeHtml(branding.name)}</div><div class="cc-header-subtitle">${this.escapeHtml(branding.subtitle)}</div>`;
|
|
621
|
+
this.header.appendChild(info);
|
|
622
|
+
if (!inline) {
|
|
623
|
+
const closeBtn = document.createElement("button");
|
|
624
|
+
closeBtn.className = "cc-header-close";
|
|
625
|
+
closeBtn.setAttribute("aria-label", closeLabel);
|
|
626
|
+
closeBtn.innerHTML = CLOSE_ICON;
|
|
627
|
+
closeBtn.addEventListener("click", onClose);
|
|
628
|
+
this.header.appendChild(closeBtn);
|
|
629
|
+
if (hadFocus) closeBtn.focus();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
401
632
|
show() {
|
|
402
633
|
this.el.classList.add("cc-visible");
|
|
403
634
|
if (!this.inline) {
|
|
@@ -439,17 +670,11 @@ class Panel {
|
|
|
439
670
|
return div.innerHTML;
|
|
440
671
|
}
|
|
441
672
|
}
|
|
442
|
-
const CHAT_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h10v2H7zm0-3h10v2H7z"/></svg>`;
|
|
443
|
-
const CLOSE_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
|
|
444
673
|
class FAB {
|
|
445
674
|
constructor(root, options) {
|
|
446
|
-
__publicField(this, "el");
|
|
447
|
-
__publicField(this, "badge");
|
|
448
|
-
__publicField(this, "isOpen", false);
|
|
449
|
-
__publicField(this, "openLabel");
|
|
450
|
-
__publicField(this, "closeLabel");
|
|
451
675
|
this.openLabel = options.openLabel;
|
|
452
676
|
this.closeLabel = options.closeLabel;
|
|
677
|
+
this.unreadLabel = options.unreadLabel;
|
|
453
678
|
this.el = document.createElement("button");
|
|
454
679
|
this.el.className = "cc-fab cc-pulse";
|
|
455
680
|
this.el.setAttribute("aria-label", this.openLabel);
|
|
@@ -469,21 +694,45 @@ class FAB {
|
|
|
469
694
|
root.appendChild(this.el);
|
|
470
695
|
}
|
|
471
696
|
setOpen(open) {
|
|
472
|
-
this.
|
|
473
|
-
this.el.innerHTML = open ? CLOSE_ICON : CHAT_ICON;
|
|
697
|
+
if (open) this.hideBadge();
|
|
698
|
+
this.el.innerHTML = open ? CLOSE_ICON$1 : CHAT_ICON;
|
|
474
699
|
if (!open) this.el.appendChild(this.badge);
|
|
475
700
|
this.el.classList.toggle("cc-open", open);
|
|
476
701
|
this.el.setAttribute("aria-label", open ? this.closeLabel : this.openLabel);
|
|
477
702
|
this.el.setAttribute("aria-expanded", String(open));
|
|
478
703
|
}
|
|
704
|
+
setLabels(openLabel, closeLabel, unreadLabel) {
|
|
705
|
+
this.openLabel = openLabel;
|
|
706
|
+
this.closeLabel = closeLabel;
|
|
707
|
+
this.unreadLabel = unreadLabel;
|
|
708
|
+
const shown = this.badge.textContent;
|
|
709
|
+
this.el.setAttribute(
|
|
710
|
+
"aria-label",
|
|
711
|
+
this.el.classList.contains("cc-open") ? closeLabel : shown ? `${openLabel}, ${unreadLabel.replace("{count}", shown)}` : openLabel
|
|
712
|
+
);
|
|
713
|
+
}
|
|
479
714
|
focus() {
|
|
480
715
|
this.el.focus();
|
|
481
716
|
}
|
|
482
|
-
showBadge() {
|
|
717
|
+
showBadge(count = 1) {
|
|
718
|
+
if (count <= 0) {
|
|
719
|
+
this.hideBadge();
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const display = count > 9 ? "9+" : String(count);
|
|
723
|
+
this.badge.textContent = display;
|
|
724
|
+
if (!this.el.classList.contains("cc-open")) {
|
|
725
|
+
const unread = this.unreadLabel.replace("{count}", display);
|
|
726
|
+
this.el.setAttribute("aria-label", `${this.openLabel}, ${unread}`);
|
|
727
|
+
}
|
|
483
728
|
this.badge.classList.add("cc-visible");
|
|
484
729
|
}
|
|
485
730
|
hideBadge() {
|
|
486
731
|
this.badge.classList.remove("cc-visible");
|
|
732
|
+
this.badge.textContent = "";
|
|
733
|
+
if (!this.el.classList.contains("cc-open")) {
|
|
734
|
+
this.el.setAttribute("aria-label", this.openLabel);
|
|
735
|
+
}
|
|
487
736
|
}
|
|
488
737
|
destroy() {
|
|
489
738
|
this.el.remove();
|
|
@@ -492,15 +741,21 @@ class FAB {
|
|
|
492
741
|
const BUBBLE_SHOWN_KEY = "chatcops_bubble_shown";
|
|
493
742
|
class WelcomeBubble {
|
|
494
743
|
constructor(root, options) {
|
|
495
|
-
|
|
496
|
-
__publicField(this, "timerId", null);
|
|
744
|
+
this.timerId = null;
|
|
497
745
|
this.el = document.createElement("div");
|
|
498
746
|
this.el.className = "cc-bubble";
|
|
499
747
|
if (options.position === "bottom-left") {
|
|
500
748
|
this.el.classList.add("cc-left");
|
|
501
749
|
}
|
|
502
|
-
this.el.textContent = options.text;
|
|
503
750
|
this.setHidden(true);
|
|
751
|
+
const trigger = document.createElement("button");
|
|
752
|
+
trigger.type = "button";
|
|
753
|
+
trigger.className = "cc-bubble-trigger";
|
|
754
|
+
trigger.textContent = options.text;
|
|
755
|
+
trigger.addEventListener("click", () => {
|
|
756
|
+
this.hide();
|
|
757
|
+
options.onClick();
|
|
758
|
+
});
|
|
504
759
|
const closeBtn = document.createElement("button");
|
|
505
760
|
closeBtn.type = "button";
|
|
506
761
|
closeBtn.className = "cc-bubble-close";
|
|
@@ -510,20 +765,13 @@ class WelcomeBubble {
|
|
|
510
765
|
e.stopPropagation();
|
|
511
766
|
this.hide();
|
|
512
767
|
});
|
|
513
|
-
this.el.
|
|
514
|
-
this.el.addEventListener("click", () => {
|
|
515
|
-
this.hide();
|
|
516
|
-
options.onClick();
|
|
517
|
-
});
|
|
768
|
+
this.el.append(trigger, closeBtn);
|
|
518
769
|
root.appendChild(this.el);
|
|
519
|
-
if (options.showOnce) {
|
|
520
|
-
try {
|
|
521
|
-
if (sessionStorage.getItem(BUBBLE_SHOWN_KEY)) return;
|
|
522
|
-
sessionStorage.setItem(BUBBLE_SHOWN_KEY, "1");
|
|
523
|
-
} catch {
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
770
|
this.timerId = setTimeout(() => {
|
|
771
|
+
if (options.showOnce) {
|
|
772
|
+
if (options.store.get(BUBBLE_SHOWN_KEY)) return;
|
|
773
|
+
options.store.set(BUBBLE_SHOWN_KEY, "1");
|
|
774
|
+
}
|
|
527
775
|
this.el.classList.add("cc-visible");
|
|
528
776
|
this.setHidden(false);
|
|
529
777
|
}, options.delay ?? 3e3);
|
|
@@ -546,12 +794,126 @@ class WelcomeBubble {
|
|
|
546
794
|
this.el.remove();
|
|
547
795
|
}
|
|
548
796
|
}
|
|
797
|
+
const KEY_PREFIX = "chatcops_conversation";
|
|
798
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
799
|
+
function keyStore(persistence) {
|
|
800
|
+
if (persistence === "none") {
|
|
801
|
+
const memory = /* @__PURE__ */ new Map();
|
|
802
|
+
return {
|
|
803
|
+
get: (key) => memory.get(key) ?? null,
|
|
804
|
+
set: (key, value) => void memory.set(key, value),
|
|
805
|
+
remove: (key) => void memory.delete(key)
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const storage = () => persistence === "local" ? localStorage : sessionStorage;
|
|
809
|
+
return {
|
|
810
|
+
get(key) {
|
|
811
|
+
try {
|
|
812
|
+
return storage().getItem(key);
|
|
813
|
+
} catch {
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
},
|
|
817
|
+
set(key, value) {
|
|
818
|
+
try {
|
|
819
|
+
storage().setItem(key, value);
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
},
|
|
823
|
+
remove(key) {
|
|
824
|
+
try {
|
|
825
|
+
storage().removeItem(key);
|
|
826
|
+
} catch {
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
class ConversationStorage {
|
|
832
|
+
/** Without a siteId the key is the one the widget has always used. */
|
|
833
|
+
constructor(siteId, persistence = "local", historyTtlDays = 30) {
|
|
834
|
+
this.store = keyStore(persistence);
|
|
835
|
+
this.key = siteId ? `${KEY_PREFIX}_${siteId}` : KEY_PREFIX;
|
|
836
|
+
this.ttlMs = (Number.isFinite(historyTtlDays) && historyTtlDays >= 0 ? historyTtlDays : 30) * DAY_MS;
|
|
837
|
+
}
|
|
838
|
+
/** The saved conversation, or null when there is none, it's unreadable, or it expired. */
|
|
839
|
+
load(now = Date.now()) {
|
|
840
|
+
const raw = this.store.get(this.key);
|
|
841
|
+
if (raw === null) return null;
|
|
842
|
+
let parsed;
|
|
843
|
+
try {
|
|
844
|
+
parsed = JSON.parse(raw);
|
|
845
|
+
} catch {
|
|
846
|
+
this.clear();
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.id !== "string" || !Array.isArray(parsed.messages)) {
|
|
850
|
+
this.clear();
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
const record = parsed;
|
|
854
|
+
if (typeof record.updatedAt === "number" && now - record.updatedAt > this.ttlMs) {
|
|
855
|
+
this.clear();
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
const messages = record.messages.flatMap(
|
|
859
|
+
(msg) => {
|
|
860
|
+
if (typeof msg !== "object" || msg === null || Array.isArray(msg) || msg.role !== "user" && msg.role !== "assistant" || typeof msg.content !== "string") {
|
|
861
|
+
return [];
|
|
862
|
+
}
|
|
863
|
+
const m = msg;
|
|
864
|
+
let sources;
|
|
865
|
+
if (Array.isArray(m.sources)) {
|
|
866
|
+
const clean = m.sources.filter(
|
|
867
|
+
(s) => typeof s === "object" && s !== null && !Array.isArray(s) && typeof s.url === "string" && (typeof s.title === "undefined" || typeof s.title === "string")
|
|
868
|
+
);
|
|
869
|
+
if (clean.length > 0) sources = clean;
|
|
870
|
+
}
|
|
871
|
+
return [{ ...m, sources }];
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
const userData = record.userData;
|
|
875
|
+
return {
|
|
876
|
+
...record,
|
|
877
|
+
id: record.id,
|
|
878
|
+
messages,
|
|
879
|
+
userData: userData && typeof userData === "object" && !Array.isArray(userData) ? userData : void 0,
|
|
880
|
+
preChatDone: record.preChatDone === true
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
save(conversation) {
|
|
884
|
+
const filtered = {
|
|
885
|
+
...conversation,
|
|
886
|
+
messages: conversation.messages.filter((m) => m.status !== "error").map(({ status, ...message }) => message)
|
|
887
|
+
};
|
|
888
|
+
this.store.set(this.key, JSON.stringify(filtered));
|
|
889
|
+
}
|
|
890
|
+
clear() {
|
|
891
|
+
this.store.remove(this.key);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
549
894
|
class ChatClient {
|
|
550
895
|
constructor(options) {
|
|
551
|
-
__publicField(this, "apiUrl");
|
|
552
|
-
__publicField(this, "timeout");
|
|
553
896
|
this.apiUrl = options.apiUrl;
|
|
897
|
+
this.turnstileSiteKey = options.turnstileSiteKey;
|
|
898
|
+
this.userToken = options.userToken;
|
|
554
899
|
this.timeout = options.timeout ?? 3e4;
|
|
900
|
+
this.sessionKey = `chatcops-session:${options.apiUrl}`;
|
|
901
|
+
this.store = options.store ?? keyStore("local");
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* The signed-in visitor's identity token for the next session, or none.
|
|
905
|
+
* The session token from before is dropped, so the next message gets one
|
|
906
|
+
* for the new identity (the server starts a new conversation for a
|
|
907
|
+
* different user).
|
|
908
|
+
*/
|
|
909
|
+
setUserToken(token) {
|
|
910
|
+
if (token === this.userToken) return;
|
|
911
|
+
this.userToken = token;
|
|
912
|
+
this.saved = {};
|
|
913
|
+
this.store.remove(this.sessionKey);
|
|
914
|
+
}
|
|
915
|
+
get identity() {
|
|
916
|
+
return this.userToken;
|
|
555
917
|
}
|
|
556
918
|
async *sendMessage(request, options = {}) {
|
|
557
919
|
var _a;
|
|
@@ -596,19 +958,48 @@ class ChatClient {
|
|
|
596
958
|
}
|
|
597
959
|
resetTimeout();
|
|
598
960
|
try {
|
|
599
|
-
const
|
|
961
|
+
const post = (url, body, token) => fetch(url, {
|
|
600
962
|
method: "POST",
|
|
601
|
-
headers: { "Content-Type": "application/json" },
|
|
602
|
-
body: JSON.stringify(
|
|
963
|
+
headers: { "Content-Type": "application/json", ...token && { Authorization: `Bearer ${token}` } },
|
|
964
|
+
body: JSON.stringify(body),
|
|
603
965
|
signal: controller.signal
|
|
604
966
|
});
|
|
967
|
+
let saved = this.saved;
|
|
968
|
+
if (!saved) {
|
|
969
|
+
try {
|
|
970
|
+
saved = JSON.parse(this.store.get(this.sessionKey) || "{}") ?? {};
|
|
971
|
+
} catch {
|
|
972
|
+
saved = {};
|
|
973
|
+
}
|
|
974
|
+
this.saved = saved;
|
|
975
|
+
}
|
|
976
|
+
const resumable = saved.conversationId === request.conversationId;
|
|
977
|
+
let response = await post(this.apiUrl, { protocol: 1, ...request }, resumable ? saved.token : void 0);
|
|
978
|
+
if (response.status === 401) {
|
|
979
|
+
const minted = await post(this.apiUrl.replace(/\/?(\?.*)?$/, "/session$1"), {
|
|
980
|
+
conversationId: request.conversationId,
|
|
981
|
+
previousToken: saved.token,
|
|
982
|
+
userToken: this.userToken,
|
|
983
|
+
// Its own chunk: only sites with a Turnstile key download it.
|
|
984
|
+
turnstileToken: this.turnstileSiteKey && !resumable ? await (await Promise.resolve().then(() => turnstile)).turnstileToken(this.turnstileSiteKey) : void 0
|
|
985
|
+
});
|
|
986
|
+
if (minted.ok) {
|
|
987
|
+
const session = await minted.json();
|
|
988
|
+
this.saved = session;
|
|
989
|
+
this.store.set(this.sessionKey, JSON.stringify(session));
|
|
990
|
+
if (session.conversationId !== request.conversationId) {
|
|
991
|
+
request = { ...request, conversationId: session.conversationId };
|
|
992
|
+
yield { conversationId: session.conversationId };
|
|
993
|
+
}
|
|
994
|
+
response = await post(this.apiUrl, { protocol: 1, ...request }, session.token);
|
|
995
|
+
} else {
|
|
996
|
+
this.saved = {};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
605
999
|
if (!response.ok) {
|
|
1000
|
+
const { error } = await response.json().catch(() => ({}));
|
|
606
1001
|
await cleanup();
|
|
607
|
-
|
|
608
|
-
yield { error: "rate_limit" };
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
yield { error: `HTTP ${response.status}` };
|
|
1002
|
+
yield (error == null ? void 0 : error.code) ? errorChunk(error.code, error.retryAfterMs) : { error: response.status === 429 ? "rate_limit" : `HTTP ${response.status}` };
|
|
612
1003
|
return;
|
|
613
1004
|
}
|
|
614
1005
|
reader = (_a = response.body) == null ? void 0 : _a.getReader();
|
|
@@ -619,6 +1010,8 @@ class ChatClient {
|
|
|
619
1010
|
}
|
|
620
1011
|
const decoder = new TextDecoder();
|
|
621
1012
|
let buffer = "";
|
|
1013
|
+
let v1 = false;
|
|
1014
|
+
let finished = false;
|
|
622
1015
|
while (true) {
|
|
623
1016
|
const { done, value } = await reader.read();
|
|
624
1017
|
if (done) break;
|
|
@@ -632,17 +1025,27 @@ class ChatClient {
|
|
|
632
1025
|
const data = trimmed.slice(6);
|
|
633
1026
|
if (data === "[DONE]") {
|
|
634
1027
|
await cleanup();
|
|
635
|
-
yield { done: true };
|
|
1028
|
+
yield v1 && !finished ? { error: "cut_off" } : { done: true };
|
|
636
1029
|
return;
|
|
637
1030
|
}
|
|
1031
|
+
let chunk;
|
|
638
1032
|
try {
|
|
639
|
-
|
|
1033
|
+
chunk = JSON.parse(data);
|
|
640
1034
|
} catch {
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
if ("type" in chunk) {
|
|
1038
|
+
v1 = true;
|
|
1039
|
+
if (chunk.type === "finish") finished = true;
|
|
1040
|
+
const mapped = fromPart(chunk);
|
|
1041
|
+
if (mapped) yield mapped;
|
|
1042
|
+
} else {
|
|
1043
|
+
yield chunk;
|
|
641
1044
|
}
|
|
642
1045
|
}
|
|
643
1046
|
}
|
|
644
1047
|
await cleanup();
|
|
645
|
-
yield { done: true };
|
|
1048
|
+
yield buffer.trim() === "data: [DONE]" ? { done: true } : { error: v1 ? "cut_off" : "network" };
|
|
646
1049
|
} catch (err) {
|
|
647
1050
|
await cleanup();
|
|
648
1051
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
@@ -655,93 +1058,42 @@ class ChatClient {
|
|
|
655
1058
|
}
|
|
656
1059
|
}
|
|
657
1060
|
}
|
|
658
|
-
const
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
}
|
|
687
|
-
clear() {
|
|
688
|
-
try {
|
|
689
|
-
localStorage.removeItem(this.key);
|
|
690
|
-
} catch {
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
getSessionId() {
|
|
694
|
-
try {
|
|
695
|
-
let id = sessionStorage.getItem(this.sessionKey);
|
|
696
|
-
if (!id) {
|
|
697
|
-
id = crypto.randomUUID();
|
|
698
|
-
sessionStorage.setItem(this.sessionKey, id);
|
|
699
|
-
}
|
|
700
|
-
return id;
|
|
701
|
-
} catch {
|
|
702
|
-
return crypto.randomUUID();
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
isPreChatCompleted(sessionId) {
|
|
706
|
-
try {
|
|
707
|
-
return sessionStorage.getItem(`chatcops-prechat-${sessionId}`) === "true";
|
|
708
|
-
} catch {
|
|
709
|
-
return false;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
setPreChatCompleted(sessionId, data) {
|
|
713
|
-
try {
|
|
714
|
-
sessionStorage.setItem(`chatcops-prechat-${sessionId}`, "true");
|
|
715
|
-
} catch {
|
|
716
|
-
}
|
|
717
|
-
if (data) {
|
|
718
|
-
this.setPreChatData(sessionId, data);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
getPreChatData(sessionId) {
|
|
722
|
-
try {
|
|
723
|
-
const raw = sessionStorage.getItem(`chatcops-prechat-data-${sessionId}`);
|
|
724
|
-
if (!raw) return null;
|
|
725
|
-
const parsed = JSON.parse(raw);
|
|
726
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
return parsed;
|
|
730
|
-
} catch {
|
|
1061
|
+
const ERROR_TYPES = {
|
|
1062
|
+
rate_limited: "rate_limit",
|
|
1063
|
+
timeout: "timeout",
|
|
1064
|
+
budget_exceeded: "budget",
|
|
1065
|
+
budget_daily: "budget_daily",
|
|
1066
|
+
provider_unavailable: "unavailable"
|
|
1067
|
+
};
|
|
1068
|
+
function errorChunk(code, retryAfterMs) {
|
|
1069
|
+
return { error: ERROR_TYPES[code] ?? "provider_error", retryAfterMs };
|
|
1070
|
+
}
|
|
1071
|
+
function fromPart(part) {
|
|
1072
|
+
switch (part.type) {
|
|
1073
|
+
case "start":
|
|
1074
|
+
return { messageId: part.messageId };
|
|
1075
|
+
case "text-delta":
|
|
1076
|
+
return { content: part.delta };
|
|
1077
|
+
case "lead-captured":
|
|
1078
|
+
return { leadCaptured: true, leadData: part.lead };
|
|
1079
|
+
case "source": {
|
|
1080
|
+
const { url, title } = part;
|
|
1081
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) return null;
|
|
1082
|
+
return { source: { url, title } };
|
|
1083
|
+
}
|
|
1084
|
+
case "error":
|
|
1085
|
+
return errorChunk(part.code === "budget_exceeded" && part.scope !== "conversation" ? "budget_daily" : part.code, part.retryAfterMs);
|
|
1086
|
+
case "finish":
|
|
1087
|
+
return part.reason === "error" || part.reason === "aborted" ? null : { done: true };
|
|
1088
|
+
default:
|
|
731
1089
|
return null;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
setPreChatData(sessionId, data) {
|
|
735
|
-
try {
|
|
736
|
-
sessionStorage.setItem(`chatcops-prechat-data-${sessionId}`, JSON.stringify(data));
|
|
737
|
-
} catch {
|
|
738
|
-
}
|
|
739
1090
|
}
|
|
740
1091
|
}
|
|
741
1092
|
const DEFAULT = "";
|
|
742
1093
|
const CLAIMS_KEY = Symbol.for("chatcops.storageClaims");
|
|
743
1094
|
const store = globalThis;
|
|
744
|
-
|
|
1095
|
+
store[CLAIMS_KEY] ?? (store[CLAIMS_KEY] = /* @__PURE__ */ new Map());
|
|
1096
|
+
const claims = store[CLAIMS_KEY];
|
|
745
1097
|
function inUse(name) {
|
|
746
1098
|
const live = claims.get(name);
|
|
747
1099
|
if (!live) return false;
|
|
@@ -773,19 +1125,14 @@ function claimNamespace(siteId, mode, isLive) {
|
|
|
773
1125
|
}
|
|
774
1126
|
};
|
|
775
1127
|
}
|
|
1128
|
+
const MODES = ["light", "dark", "auto"];
|
|
776
1129
|
function applyTheme(root, theme) {
|
|
777
1130
|
const host = root.host;
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
host.style.setProperty("--cc-on-accent", pair.onAccent);
|
|
784
|
-
host.style.setProperty("--cc-accent-contrast", pair.background);
|
|
785
|
-
} else {
|
|
786
|
-
host.style.setProperty("--cc-accent-contrast", theme.accent);
|
|
787
|
-
}
|
|
788
|
-
}
|
|
1131
|
+
const mode = themeMode(theme);
|
|
1132
|
+
if (mode) host.dataset.ccMode = mode;
|
|
1133
|
+
else delete host.dataset.ccMode;
|
|
1134
|
+
if (theme.accent) setAccent(host, "", theme.accent);
|
|
1135
|
+
if (theme.accentDark) setAccent(host, "-dark", theme.accentDark);
|
|
789
1136
|
if (theme.textColor) {
|
|
790
1137
|
host.style.setProperty("--cc-text", theme.textColor);
|
|
791
1138
|
}
|
|
@@ -802,6 +1149,23 @@ function applyTheme(root, theme) {
|
|
|
802
1149
|
if (radius) host.style.setProperty("--cc-radius", radius);
|
|
803
1150
|
}
|
|
804
1151
|
}
|
|
1152
|
+
function themeMode(theme) {
|
|
1153
|
+
if (theme.mode && MODES.includes(theme.mode)) return theme.mode;
|
|
1154
|
+
if (theme.bgColor) return isHex(theme.bgColor) && relativeLuminance(theme.bgColor) > 0.4 ? "light" : "dark";
|
|
1155
|
+
if (theme.textColor) return isHex(theme.textColor) && relativeLuminance(theme.textColor) < 0.4 ? "light" : "dark";
|
|
1156
|
+
return void 0;
|
|
1157
|
+
}
|
|
1158
|
+
function setAccent(host, suffix, accent) {
|
|
1159
|
+
host.style.setProperty(`--cc-theme-accent${suffix}`, accent);
|
|
1160
|
+
host.style.setProperty(`--cc-theme-accent${suffix}-hover`, darken(accent, 10));
|
|
1161
|
+
const pair = accentTextColors(accent);
|
|
1162
|
+
if (pair) {
|
|
1163
|
+
host.style.setProperty(`--cc-theme-on-accent${suffix}`, pair.onAccent);
|
|
1164
|
+
host.style.setProperty(`--cc-theme-accent${suffix}-contrast`, pair.background);
|
|
1165
|
+
} else {
|
|
1166
|
+
host.style.setProperty(`--cc-theme-accent${suffix}-contrast`, accent);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
805
1169
|
function toCssLength(value) {
|
|
806
1170
|
if (typeof value === "number" && !Number.isFinite(value)) return "";
|
|
807
1171
|
const text = String(value).trim();
|
|
@@ -868,6 +1232,11 @@ const en = {
|
|
|
868
1232
|
errorGeneric: "Something went wrong. Please try again.",
|
|
869
1233
|
errorNetwork: "Connection error. Please check your network.",
|
|
870
1234
|
errorRateLimit: "Too many messages. Please wait a moment.",
|
|
1235
|
+
errorRateLimitWait: "Too many messages. Please try again in {seconds} seconds.",
|
|
1236
|
+
errorBudget: "This chat has reached its limit.",
|
|
1237
|
+
errorBudgetDaily: "The assistant has reached its limit for today. Please try again later.",
|
|
1238
|
+
errorUnavailable: "The assistant is unavailable right now. Please try again later.",
|
|
1239
|
+
errorCutOff: "The reply was cut off.",
|
|
871
1240
|
typingIndicator: "Typing...",
|
|
872
1241
|
poweredBy: "Powered by ChatCops",
|
|
873
1242
|
newConversation: "New conversation",
|
|
@@ -878,250 +1247,92 @@ const en = {
|
|
|
878
1247
|
preChatRequired: "This field is required",
|
|
879
1248
|
preChatInvalidEmail: "Please enter a valid email",
|
|
880
1249
|
retryButton: "Retry",
|
|
881
|
-
regenerateButton: "Regenerate"
|
|
1250
|
+
regenerateButton: "Regenerate",
|
|
1251
|
+
suggestionsLabel: "Suggested questions",
|
|
1252
|
+
unreadCount: "{count} unread",
|
|
1253
|
+
sourcesLabel: "Sources",
|
|
1254
|
+
disclosure: "You're chatting with an AI assistant. It can make mistakes.",
|
|
1255
|
+
disclosureLearnMore: "Learn more"
|
|
882
1256
|
};
|
|
883
1257
|
const locales = { en };
|
|
884
1258
|
function getWidgetLocale(code, overrides) {
|
|
885
1259
|
const base = locales[code] ?? en;
|
|
886
1260
|
return overrides ? { ...base, ...overrides } : base;
|
|
887
1261
|
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1262
|
+
function registerWidgetLocale(code, strings) {
|
|
1263
|
+
locales[code] = { ...en, ...strings };
|
|
1264
|
+
}
|
|
1265
|
+
const ERROR_MESSAGES = {
|
|
1266
|
+
network: "errorNetwork",
|
|
1267
|
+
timeout: "errorNetwork",
|
|
1268
|
+
provider_error: "errorGeneric",
|
|
1269
|
+
budget: "errorBudget",
|
|
1270
|
+
budget_daily: "errorBudgetDaily",
|
|
1271
|
+
unavailable: "errorUnavailable",
|
|
1272
|
+
cut_off: "errorCutOff"
|
|
1273
|
+
};
|
|
1274
|
+
let preChatChunk;
|
|
1275
|
+
function loadPreChatForm() {
|
|
1276
|
+
preChatChunk ?? (preChatChunk = Promise.resolve().then(() => prechatForm).catch((error) => {
|
|
1277
|
+
preChatChunk = void 0;
|
|
1278
|
+
throw error;
|
|
1279
|
+
}));
|
|
1280
|
+
return preChatChunk;
|
|
1281
|
+
}
|
|
1282
|
+
function prefetch(config) {
|
|
1283
|
+
var _a;
|
|
1284
|
+
if ((_a = config.preChatForm) == null ? void 0 : _a.enabled) loadPreChatForm().catch(() => void 0);
|
|
1285
|
+
}
|
|
1286
|
+
class Widget {
|
|
1287
|
+
constructor(config) {
|
|
1288
|
+
this.mounted = false;
|
|
1289
|
+
this.conversationId = crypto.randomUUID();
|
|
1290
|
+
this.preChatDone = false;
|
|
1291
|
+
this.hydrated = false;
|
|
1292
|
+
this.messages = [];
|
|
1293
|
+
this.isStreaming = false;
|
|
1294
|
+
this.v1 = false;
|
|
1295
|
+
this.destroyed = false;
|
|
1296
|
+
this.generation = 0;
|
|
1297
|
+
this.userFromSite = false;
|
|
1298
|
+
this.retryRequests = /* @__PURE__ */ new Map();
|
|
1299
|
+
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
1300
|
+
this.unreadReplies = 0;
|
|
1301
|
+
this.config = config;
|
|
1302
|
+
this.namespace = claimNamespace(
|
|
1303
|
+
config.siteId,
|
|
1304
|
+
config.mode ?? "popup",
|
|
1305
|
+
() => !this.destroyed && (!this.mounted || this.shadow.host.isConnected)
|
|
1306
|
+
);
|
|
1307
|
+
const persistence = config.persistence ?? (config.persistHistory === false ? "none" : "local");
|
|
1308
|
+
this.storage = new ConversationStorage(this.namespace.siteId, persistence, config.historyTtlDays);
|
|
1309
|
+
this.client = new ChatClient({ ...config, store: this.storage.store });
|
|
1310
|
+
this.locale = getWidgetLocale(config.locale ?? "en", config.strings);
|
|
1311
|
+
}
|
|
1312
|
+
get isInline() {
|
|
1313
|
+
return this.config.mode === "inline";
|
|
1314
|
+
}
|
|
1315
|
+
resolveContainer() {
|
|
1316
|
+
if (!this.config.container) {
|
|
1317
|
+
throw new Error('ChatCops: "container" is required when mode is "inline"');
|
|
910
1318
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
});
|
|
918
|
-
for (const field of options.fields) {
|
|
919
|
-
form.appendChild(this.createField(field));
|
|
920
|
-
}
|
|
921
|
-
this.submitBtn = document.createElement("button");
|
|
922
|
-
this.submitBtn.type = "submit";
|
|
923
|
-
this.submitBtn.className = "cc-prechat-submit";
|
|
924
|
-
this.submitBtn.textContent = options.submitLabel;
|
|
925
|
-
form.appendChild(this.submitBtn);
|
|
926
|
-
this.container.appendChild(form);
|
|
927
|
-
parent.appendChild(this.container);
|
|
928
|
-
this.updateSubmitState();
|
|
929
|
-
const firstInput = this.fieldElements.values().next().value;
|
|
930
|
-
if (firstInput) {
|
|
931
|
-
setTimeout(() => firstInput.focus(), 100);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
createField(field) {
|
|
935
|
-
const wrapper = document.createElement("div");
|
|
936
|
-
wrapper.className = "cc-prechat-field";
|
|
937
|
-
const inputId = `cc-prechat-${field.name}`;
|
|
938
|
-
const errorId = `cc-prechat-${field.name}-error`;
|
|
939
|
-
const label = document.createElement("label");
|
|
940
|
-
label.className = "cc-prechat-label";
|
|
941
|
-
label.htmlFor = inputId;
|
|
942
|
-
label.textContent = field.label;
|
|
943
|
-
if (field.required) {
|
|
944
|
-
const req = document.createElement("span");
|
|
945
|
-
req.className = "cc-required";
|
|
946
|
-
req.textContent = "*";
|
|
947
|
-
label.appendChild(req);
|
|
948
|
-
}
|
|
949
|
-
wrapper.appendChild(label);
|
|
950
|
-
let input;
|
|
951
|
-
if (field.type === "select") {
|
|
952
|
-
const select = document.createElement("select");
|
|
953
|
-
select.className = "cc-prechat-select";
|
|
954
|
-
const defaultOpt = document.createElement("option");
|
|
955
|
-
defaultOpt.value = "";
|
|
956
|
-
defaultOpt.textContent = field.placeholder ?? this.options.locale.selectPlaceholder.replace("{label}", field.label.toLowerCase());
|
|
957
|
-
defaultOpt.disabled = true;
|
|
958
|
-
defaultOpt.selected = true;
|
|
959
|
-
select.appendChild(defaultOpt);
|
|
960
|
-
for (const opt of field.options ?? []) {
|
|
961
|
-
const option = document.createElement("option");
|
|
962
|
-
option.value = opt;
|
|
963
|
-
option.textContent = opt;
|
|
964
|
-
select.appendChild(option);
|
|
965
|
-
}
|
|
966
|
-
input = select;
|
|
967
|
-
} else if (field.type === "textarea") {
|
|
968
|
-
const textarea = document.createElement("textarea");
|
|
969
|
-
textarea.className = "cc-prechat-textarea";
|
|
970
|
-
textarea.placeholder = field.placeholder ?? "";
|
|
971
|
-
textarea.rows = 3;
|
|
972
|
-
textarea.maxLength = MAX_PRECHAT_FIELD_LENGTH;
|
|
973
|
-
input = textarea;
|
|
974
|
-
} else {
|
|
975
|
-
const textInput = document.createElement("input");
|
|
976
|
-
textInput.type = field.type;
|
|
977
|
-
textInput.className = "cc-prechat-input";
|
|
978
|
-
textInput.maxLength = MAX_PRECHAT_FIELD_LENGTH;
|
|
979
|
-
textInput.placeholder = field.placeholder ?? "";
|
|
980
|
-
input = textInput;
|
|
981
|
-
}
|
|
982
|
-
input.name = field.name;
|
|
983
|
-
input.id = inputId;
|
|
984
|
-
if (field.required) {
|
|
985
|
-
input.setAttribute("aria-required", "true");
|
|
986
|
-
}
|
|
987
|
-
input.addEventListener("input", () => {
|
|
988
|
-
this.clearError(field.name);
|
|
989
|
-
this.updateSubmitState();
|
|
990
|
-
});
|
|
991
|
-
if (input instanceof HTMLSelectElement) {
|
|
992
|
-
input.addEventListener("change", () => {
|
|
993
|
-
this.clearError(field.name);
|
|
994
|
-
this.updateSubmitState();
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
wrapper.appendChild(input);
|
|
998
|
-
this.fieldElements.set(field.name, input);
|
|
999
|
-
const error = document.createElement("div");
|
|
1000
|
-
error.className = "cc-prechat-error";
|
|
1001
|
-
error.id = errorId;
|
|
1002
|
-
wrapper.appendChild(error);
|
|
1003
|
-
this.errorElements.set(field.name, error);
|
|
1004
|
-
return wrapper;
|
|
1005
|
-
}
|
|
1006
|
-
updateSubmitState() {
|
|
1007
|
-
const allRequiredFilled = this.options.fields.every((field) => {
|
|
1008
|
-
if (!field.required) return true;
|
|
1009
|
-
const el = this.fieldElements.get(field.name);
|
|
1010
|
-
return el ? el.value.trim().length > 0 : false;
|
|
1011
|
-
});
|
|
1012
|
-
this.submitBtn.disabled = !allRequiredFilled;
|
|
1013
|
-
}
|
|
1014
|
-
handleSubmit() {
|
|
1015
|
-
const data = {};
|
|
1016
|
-
let valid = true;
|
|
1017
|
-
for (const field of this.options.fields) {
|
|
1018
|
-
const el = this.fieldElements.get(field.name);
|
|
1019
|
-
if (!el) continue;
|
|
1020
|
-
const value = el.value.trim();
|
|
1021
|
-
data[field.name] = value;
|
|
1022
|
-
if (field.required && !value) {
|
|
1023
|
-
this.showError(field.name, this.options.locale.preChatRequired);
|
|
1024
|
-
valid = false;
|
|
1025
|
-
continue;
|
|
1026
|
-
}
|
|
1027
|
-
if (field.type === "email" && value && !this.isValidEmail(value)) {
|
|
1028
|
-
this.showError(field.name, this.options.locale.preChatInvalidEmail);
|
|
1029
|
-
valid = false;
|
|
1030
|
-
continue;
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
if (valid) {
|
|
1034
|
-
this.options.onSubmit(data);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
showError(fieldName, message) {
|
|
1038
|
-
const el = this.errorElements.get(fieldName);
|
|
1039
|
-
if (el) el.textContent = message;
|
|
1040
|
-
const fieldEl = this.fieldElements.get(fieldName);
|
|
1041
|
-
if (fieldEl && el) {
|
|
1042
|
-
fieldEl.setAttribute("aria-invalid", "true");
|
|
1043
|
-
fieldEl.setAttribute("aria-describedby", el.id);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
clearError(fieldName) {
|
|
1047
|
-
const el = this.errorElements.get(fieldName);
|
|
1048
|
-
if (el) el.textContent = "";
|
|
1049
|
-
const fieldEl = this.fieldElements.get(fieldName);
|
|
1050
|
-
if (fieldEl && el) {
|
|
1051
|
-
if (fieldEl.getAttribute("aria-describedby") === el.id) {
|
|
1052
|
-
fieldEl.removeAttribute("aria-describedby");
|
|
1053
|
-
}
|
|
1054
|
-
if (fieldEl.getAttribute("aria-invalid") === "true") {
|
|
1055
|
-
fieldEl.removeAttribute("aria-invalid");
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
isValidEmail(email) {
|
|
1060
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
1061
|
-
}
|
|
1062
|
-
destroy() {
|
|
1063
|
-
this.container.remove();
|
|
1064
|
-
this.fieldElements.clear();
|
|
1065
|
-
this.errorElements.clear();
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
class Widget {
|
|
1069
|
-
constructor(config) {
|
|
1070
|
-
__publicField(this, "config");
|
|
1071
|
-
__publicField(this, "shadow");
|
|
1072
|
-
__publicField(this, "mounted", false);
|
|
1073
|
-
__publicField(this, "fab");
|
|
1074
|
-
__publicField(this, "panel");
|
|
1075
|
-
__publicField(this, "bubble");
|
|
1076
|
-
__publicField(this, "preChatFormEl");
|
|
1077
|
-
__publicField(this, "client");
|
|
1078
|
-
__publicField(this, "storage");
|
|
1079
|
-
__publicField(this, "namespace");
|
|
1080
|
-
__publicField(this, "locale");
|
|
1081
|
-
__publicField(this, "conversationId");
|
|
1082
|
-
__publicField(this, "messages", []);
|
|
1083
|
-
__publicField(this, "userData");
|
|
1084
|
-
__publicField(this, "isStreaming", false);
|
|
1085
|
-
__publicField(this, "destroyed", false);
|
|
1086
|
-
__publicField(this, "abortController");
|
|
1087
|
-
__publicField(this, "retryRequests", /* @__PURE__ */ new Map());
|
|
1088
|
-
__publicField(this, "eventHandlers", /* @__PURE__ */ new Map());
|
|
1089
|
-
__publicField(this, "containerEl");
|
|
1090
|
-
__publicField(this, "autoOpenTimer");
|
|
1091
|
-
this.config = config;
|
|
1092
|
-
this.client = new ChatClient({ apiUrl: config.apiUrl, timeout: config.timeout });
|
|
1093
|
-
this.namespace = claimNamespace(
|
|
1094
|
-
config.siteId,
|
|
1095
|
-
config.mode ?? "popup",
|
|
1096
|
-
() => !this.destroyed && (!this.mounted || this.shadow.host.isConnected)
|
|
1097
|
-
);
|
|
1098
|
-
this.storage = new ConversationStorage(this.namespace.siteId);
|
|
1099
|
-
this.locale = getWidgetLocale(config.locale ?? "en", config.strings);
|
|
1100
|
-
this.conversationId = this.storage.getSessionId();
|
|
1101
|
-
}
|
|
1102
|
-
get isInline() {
|
|
1103
|
-
return this.config.mode === "inline";
|
|
1104
|
-
}
|
|
1105
|
-
resolveContainer() {
|
|
1106
|
-
if (!this.config.container) {
|
|
1107
|
-
throw new Error('ChatCops: "container" is required when mode is "inline"');
|
|
1108
|
-
}
|
|
1109
|
-
if (typeof this.config.container === "string") {
|
|
1110
|
-
const el = document.querySelector(this.config.container);
|
|
1111
|
-
if (!el) {
|
|
1112
|
-
throw new Error(`ChatCops: container element not found: ${this.config.container}`);
|
|
1113
|
-
}
|
|
1114
|
-
return el;
|
|
1319
|
+
if (typeof this.config.container === "string") {
|
|
1320
|
+
const el = document.querySelector(this.config.container);
|
|
1321
|
+
if (!el) {
|
|
1322
|
+
throw new Error(`ChatCops: container element not found: ${this.config.container}`);
|
|
1323
|
+
}
|
|
1324
|
+
return el;
|
|
1115
1325
|
}
|
|
1116
1326
|
return this.config.container;
|
|
1117
1327
|
}
|
|
1118
1328
|
init() {
|
|
1119
|
-
var _a, _b
|
|
1329
|
+
var _a, _b;
|
|
1330
|
+
prefetch(this.config);
|
|
1120
1331
|
if (this.isInline) {
|
|
1121
1332
|
this.containerEl = this.resolveContainer();
|
|
1122
|
-
this.shadow = createShadowRoot(this.containerEl);
|
|
1333
|
+
this.shadow = createShadowRoot(this.containerEl, { dir: this.config.dir, locale: this.config.locale });
|
|
1123
1334
|
} else {
|
|
1124
|
-
this.shadow = createShadowRoot();
|
|
1335
|
+
this.shadow = createShadowRoot(void 0, { dir: this.config.dir, locale: this.config.locale });
|
|
1125
1336
|
}
|
|
1126
1337
|
this.mounted = true;
|
|
1127
1338
|
if (this.config.theme) {
|
|
@@ -1133,6 +1344,7 @@ class Widget {
|
|
|
1133
1344
|
position,
|
|
1134
1345
|
openLabel: this.locale.openChat,
|
|
1135
1346
|
closeLabel: this.locale.closeButton,
|
|
1347
|
+
unreadLabel: this.locale.unreadCount,
|
|
1136
1348
|
onClick: () => this.toggle()
|
|
1137
1349
|
});
|
|
1138
1350
|
}
|
|
@@ -1140,15 +1352,7 @@ class Widget {
|
|
|
1140
1352
|
position,
|
|
1141
1353
|
inline: this.isInline,
|
|
1142
1354
|
preChatEnabled: ((_b = this.config.preChatForm) == null ? void 0 : _b.enabled) ?? false,
|
|
1143
|
-
|
|
1144
|
-
name: ((_c = this.config.branding) == null ? void 0 : _c.name) ?? this.locale.defaultName,
|
|
1145
|
-
avatar: (_d = this.config.branding) == null ? void 0 : _d.avatar,
|
|
1146
|
-
subtitle: ((_e = this.config.branding) == null ? void 0 : _e.subtitle) ?? this.locale.defaultSubtitle
|
|
1147
|
-
},
|
|
1148
|
-
placeholder: this.config.placeholder ?? this.locale.inputPlaceholder,
|
|
1149
|
-
footerText: this.locale.poweredBy,
|
|
1150
|
-
closeLabel: this.locale.closeButton,
|
|
1151
|
-
sendLabel: this.locale.sendButton,
|
|
1355
|
+
...this.panelLabels(),
|
|
1152
1356
|
messageActions: {
|
|
1153
1357
|
onRetry: (messageId) => {
|
|
1154
1358
|
void this.handleRetry(messageId);
|
|
@@ -1156,41 +1360,24 @@ class Widget {
|
|
|
1156
1360
|
onRegenerate: (messageId) => {
|
|
1157
1361
|
void this.handleRegenerate(messageId);
|
|
1158
1362
|
},
|
|
1159
|
-
|
|
1160
|
-
regenerateLabel: this.locale.regenerateButton,
|
|
1161
|
-
typingLabel: this.locale.typingIndicator
|
|
1363
|
+
...this.actionLabels()
|
|
1162
1364
|
},
|
|
1163
1365
|
onSend: (text) => this.handleSend(text),
|
|
1164
1366
|
onClose: () => this.close()
|
|
1165
1367
|
});
|
|
1166
|
-
if (this.config.
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
for (const msg of saved.messages) {
|
|
1171
|
-
this.messages.push(msg);
|
|
1172
|
-
this.panel.addMessage(msg);
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
if ((_f = this.config.preChatForm) == null ? void 0 : _f.enabled) {
|
|
1177
|
-
const savedUserData = this.storage.getPreChatData(this.conversationId);
|
|
1178
|
-
if (savedUserData) {
|
|
1179
|
-
this.userData = savedUserData;
|
|
1180
|
-
}
|
|
1368
|
+
if (this.config.disclosure === false) {
|
|
1369
|
+
console.info(
|
|
1370
|
+
"[chatcops] The AI disclosure is off (disclosure: false). Under the EU AI Act (Article 50), telling visitors they're talking to an AI is then up to you."
|
|
1371
|
+
);
|
|
1181
1372
|
}
|
|
1182
|
-
|
|
1183
|
-
if (
|
|
1184
|
-
this.showPreChatForm();
|
|
1185
|
-
} else {
|
|
1186
|
-
this.showChatView();
|
|
1187
|
-
}
|
|
1188
|
-
this.showLatestRegenerateButton();
|
|
1373
|
+
this.renderDisclosure();
|
|
1374
|
+
if (this.isInline) this.hydrate();
|
|
1189
1375
|
if (!this.isInline && this.config.welcomeBubble) {
|
|
1190
1376
|
this.bubble = new WelcomeBubble(this.shadow, {
|
|
1191
1377
|
text: this.config.welcomeBubble.text,
|
|
1192
1378
|
delay: this.config.welcomeBubble.delay,
|
|
1193
1379
|
showOnce: this.config.welcomeBubble.showOnce ?? true,
|
|
1380
|
+
store: this.storage.store,
|
|
1194
1381
|
position,
|
|
1195
1382
|
dismissLabel: this.locale.dismissBubble,
|
|
1196
1383
|
onClick: () => this.open()
|
|
@@ -1204,15 +1391,39 @@ class Widget {
|
|
|
1204
1391
|
this.autoOpenTimer = setTimeout(() => this.open(), delay);
|
|
1205
1392
|
}
|
|
1206
1393
|
}
|
|
1394
|
+
/** Reads the saved conversation and shows it, or the pre-chat form. Runs once. */
|
|
1395
|
+
hydrate() {
|
|
1396
|
+
var _a, _b;
|
|
1397
|
+
if (this.hydrated) return;
|
|
1398
|
+
this.hydrated = true;
|
|
1399
|
+
const saved = this.storage.load();
|
|
1400
|
+
if (saved) {
|
|
1401
|
+
this.conversationId = saved.id;
|
|
1402
|
+
this.preChatDone = saved.preChatDone ?? false;
|
|
1403
|
+
if ((_a = this.config.preChatForm) == null ? void 0 : _a.enabled) this.userData = saved.userData;
|
|
1404
|
+
for (const msg of saved.messages) {
|
|
1405
|
+
this.messages.push(msg);
|
|
1406
|
+
this.panel.addMessage(msg);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (((_b = this.config.preChatForm) == null ? void 0 : _b.enabled) && !this.preChatDone && this.messages.length === 0) {
|
|
1410
|
+
this.showPreChatForm();
|
|
1411
|
+
} else {
|
|
1412
|
+
this.showChatView();
|
|
1413
|
+
}
|
|
1414
|
+
this.showLatestRegenerateButton();
|
|
1415
|
+
}
|
|
1207
1416
|
open() {
|
|
1208
|
-
var _a, _b, _c, _d
|
|
1417
|
+
var _a, _b, _c, _d;
|
|
1418
|
+
this.unreadReplies = 0;
|
|
1419
|
+
this.hydrate();
|
|
1209
1420
|
this.panel.show();
|
|
1210
1421
|
if (!this.isInline) {
|
|
1211
1422
|
(_a = this.fab) == null ? void 0 : _a.setOpen(true);
|
|
1212
|
-
(_b = this.
|
|
1213
|
-
(_c = this.bubble) == null ? void 0 : _c.hide();
|
|
1423
|
+
(_b = this.bubble) == null ? void 0 : _b.hide();
|
|
1214
1424
|
}
|
|
1215
|
-
|
|
1425
|
+
this.lockScroll();
|
|
1426
|
+
(_d = (_c = this.config).onOpen) == null ? void 0 : _d.call(_c);
|
|
1216
1427
|
this.emit("open");
|
|
1217
1428
|
}
|
|
1218
1429
|
close() {
|
|
@@ -1223,6 +1434,7 @@ class Widget {
|
|
|
1223
1434
|
(_a = this.fab) == null ? void 0 : _a.setOpen(false);
|
|
1224
1435
|
if (hadFocus) (_b = this.fab) == null ? void 0 : _b.focus();
|
|
1225
1436
|
}
|
|
1437
|
+
this.restoreScroll();
|
|
1226
1438
|
(_d = (_c = this.config).onClose) == null ? void 0 : _d.call(_c);
|
|
1227
1439
|
this.emit("close");
|
|
1228
1440
|
}
|
|
@@ -1233,12 +1445,157 @@ class Widget {
|
|
|
1233
1445
|
this.open();
|
|
1234
1446
|
}
|
|
1235
1447
|
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Sends `text` as the visitor, as if they typed it, and opens the chat
|
|
1450
|
+
* first unless `open: false`. Resolves when the reply has finished. A
|
|
1451
|
+
* message sent while a reply is still coming is ignored, as in the input.
|
|
1452
|
+
*/
|
|
1453
|
+
async sendMessage(text, options = {}) {
|
|
1454
|
+
if (options.open === false) this.hydrate();
|
|
1455
|
+
else this.open();
|
|
1456
|
+
await this.handleSend(text);
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Starts a fresh conversation: a new id, and the log back to the welcome
|
|
1460
|
+
* message. A reply that's still coming is stopped. Pre-chat answers stay.
|
|
1461
|
+
*/
|
|
1462
|
+
newConversation() {
|
|
1463
|
+
this.reset(false);
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Like newConversation(), and forgets what this browser saved: the
|
|
1467
|
+
* transcript and the pre-chat answers, so the form shows again.
|
|
1468
|
+
*/
|
|
1469
|
+
clear() {
|
|
1470
|
+
this.reset(true);
|
|
1471
|
+
}
|
|
1472
|
+
reset(forget) {
|
|
1473
|
+
var _a, _b, _c;
|
|
1474
|
+
this.generation++;
|
|
1475
|
+
(_a = this.abortController) == null ? void 0 : _a.abort();
|
|
1476
|
+
this.abortController = void 0;
|
|
1477
|
+
this.isStreaming = false;
|
|
1478
|
+
this.retryRequests.clear();
|
|
1479
|
+
this.messages = [];
|
|
1480
|
+
this.conversationId = crypto.randomUUID();
|
|
1481
|
+
this.hydrated = true;
|
|
1482
|
+
if (forget) {
|
|
1483
|
+
this.userData = void 0;
|
|
1484
|
+
this.preChatDone = false;
|
|
1485
|
+
this.storage.clear();
|
|
1486
|
+
}
|
|
1487
|
+
(_b = this.preChatFormEl) == null ? void 0 : _b.destroy();
|
|
1488
|
+
this.preChatFormEl = void 0;
|
|
1489
|
+
this.panel.messages.clear();
|
|
1490
|
+
this.panel.input.setDisabled(false);
|
|
1491
|
+
if (((_c = this.config.preChatForm) == null ? void 0 : _c.enabled) && !this.preChatDone) this.showPreChatForm();
|
|
1492
|
+
else this.showChatView();
|
|
1493
|
+
if (!forget) this.saveHistory();
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* The signed-in visitor. `token` is the identity token for the server
|
|
1497
|
+
* (signed-in visitors); `name` and `email` are sent as visitor details, and
|
|
1498
|
+
* the name greets them. Switching to another user, or signing out with
|
|
1499
|
+
* `null`, clears the chat, so one person's conversation never shows for
|
|
1500
|
+
* the next.
|
|
1501
|
+
*/
|
|
1502
|
+
setUser(user) {
|
|
1503
|
+
const previous = this.client.identity;
|
|
1504
|
+
const token = user == null ? void 0 : user.token;
|
|
1505
|
+
if (previous !== void 0 && previous !== token) this.clear();
|
|
1506
|
+
this.client.setUserToken(token);
|
|
1507
|
+
if (!user) return;
|
|
1508
|
+
const details = Object.fromEntries(
|
|
1509
|
+
Object.entries({ name: user.name, email: user.email }).filter((entry) => typeof entry[1] === "string" && entry[1] !== "")
|
|
1510
|
+
);
|
|
1511
|
+
if (Object.keys(details).length) {
|
|
1512
|
+
this.userData = { ...this.userData, ...details };
|
|
1513
|
+
this.userFromSite = true;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Details about the page or the visitor that the bot should know for the
|
|
1518
|
+
* next messages, such as `{ plan: 'Team' }`. Sent with each message as
|
|
1519
|
+
* page context; `null` removes them. Ignored when `pageContext: false`.
|
|
1520
|
+
*/
|
|
1521
|
+
setContext(data) {
|
|
1522
|
+
this.pageData = data && Object.keys(data).length ? { ...data } : void 0;
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Changes the look or wording of the running chat without loading it
|
|
1526
|
+
* again: the theme (not `position`), branding, strings, placeholder,
|
|
1527
|
+
* disclosure, welcome message and suggestions. What you leave out stays
|
|
1528
|
+
* as it is. A conversation that has only the welcome message shows the
|
|
1529
|
+
* new one; otherwise the welcome message and suggestions are for the
|
|
1530
|
+
* next conversation.
|
|
1531
|
+
*/
|
|
1532
|
+
update(changes) {
|
|
1533
|
+
var _a, _b;
|
|
1534
|
+
const current = this.config;
|
|
1535
|
+
const theme = { ...current.theme, ...changes.theme, position: (_a = current.theme) == null ? void 0 : _a.position };
|
|
1536
|
+
this.config = {
|
|
1537
|
+
...current,
|
|
1538
|
+
...changes,
|
|
1539
|
+
theme,
|
|
1540
|
+
branding: { ...current.branding, ...changes.branding },
|
|
1541
|
+
strings: { ...current.strings, ...changes.strings }
|
|
1542
|
+
};
|
|
1543
|
+
this.locale = getWidgetLocale(this.config.locale ?? "en", this.config.strings);
|
|
1544
|
+
if (!this.mounted) return;
|
|
1545
|
+
if (changes.theme) applyTheme(this.shadow, theme);
|
|
1546
|
+
const l = this.locale;
|
|
1547
|
+
(_b = this.fab) == null ? void 0 : _b.setLabels(l.openChat, l.closeButton, l.unreadCount);
|
|
1548
|
+
this.panel.update(this.panelLabels(), this.actionLabels());
|
|
1549
|
+
this.renderDisclosure();
|
|
1550
|
+
if (this.messages.length === 1 && this.messages[0].id === "welcome") {
|
|
1551
|
+
this.messages = [];
|
|
1552
|
+
this.panel.messages.clear();
|
|
1553
|
+
this.showChatView();
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
panelLabels() {
|
|
1557
|
+
const { branding, placeholder } = this.config;
|
|
1558
|
+
const l = this.locale;
|
|
1559
|
+
return {
|
|
1560
|
+
branding: {
|
|
1561
|
+
name: (branding == null ? void 0 : branding.name) ?? l.defaultName,
|
|
1562
|
+
avatar: branding == null ? void 0 : branding.avatar,
|
|
1563
|
+
subtitle: (branding == null ? void 0 : branding.subtitle) ?? l.defaultSubtitle
|
|
1564
|
+
},
|
|
1565
|
+
placeholder: placeholder ?? l.inputPlaceholder,
|
|
1566
|
+
footerText: l.poweredBy,
|
|
1567
|
+
closeLabel: l.closeButton,
|
|
1568
|
+
sendLabel: l.sendButton
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
actionLabels() {
|
|
1572
|
+
const l = this.locale;
|
|
1573
|
+
return {
|
|
1574
|
+
retryLabel: l.retryButton,
|
|
1575
|
+
regenerateLabel: l.regenerateButton,
|
|
1576
|
+
typingLabel: l.typingIndicator,
|
|
1577
|
+
sourcesLabel: l.sourcesLabel
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
renderDisclosure() {
|
|
1581
|
+
const { disclosure } = this.config;
|
|
1582
|
+
if (disclosure === false) this.panel.messages.setDisclosure();
|
|
1583
|
+
else
|
|
1584
|
+
this.panel.messages.setDisclosure(
|
|
1585
|
+
(disclosure == null ? void 0 : disclosure.text) ?? this.locale.disclosure,
|
|
1586
|
+
(disclosure == null ? void 0 : disclosure.learnMoreUrl) ? { url: disclosure.learnMoreUrl, label: this.locale.disclosureLearnMore } : void 0
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
isStale(generation) {
|
|
1590
|
+
return this.destroyed || generation !== this.generation;
|
|
1591
|
+
}
|
|
1236
1592
|
destroy() {
|
|
1237
1593
|
var _a, _b, _c, _d;
|
|
1238
1594
|
this.destroyed = true;
|
|
1239
1595
|
if (this.autoOpenTimer !== void 0) clearTimeout(this.autoOpenTimer);
|
|
1240
1596
|
(_a = this.abortController) == null ? void 0 : _a.abort();
|
|
1241
1597
|
this.abortController = void 0;
|
|
1598
|
+
this.restoreScroll();
|
|
1242
1599
|
(_b = this.preChatFormEl) == null ? void 0 : _b.destroy();
|
|
1243
1600
|
(_c = this.bubble) == null ? void 0 : _c.destroy();
|
|
1244
1601
|
this.panel.destroy();
|
|
@@ -1249,6 +1606,28 @@ class Widget {
|
|
|
1249
1606
|
this.messages = [];
|
|
1250
1607
|
this.userData = void 0;
|
|
1251
1608
|
}
|
|
1609
|
+
lockScroll() {
|
|
1610
|
+
var _a, _b;
|
|
1611
|
+
if (this.savedOverflow !== void 0 || this.isInline) return;
|
|
1612
|
+
if (typeof window !== "undefined" && ((_b = (_a = window.matchMedia) == null ? void 0 : _a.call(window, "(max-width: 640px)")) == null ? void 0 : _b.matches)) {
|
|
1613
|
+
const root = document.documentElement;
|
|
1614
|
+
this.savedOverflow = {
|
|
1615
|
+
value: root.style.getPropertyValue("overflow"),
|
|
1616
|
+
priority: root.style.getPropertyPriority("overflow")
|
|
1617
|
+
};
|
|
1618
|
+
root.style.setProperty("overflow", "hidden");
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
restoreScroll() {
|
|
1622
|
+
if (this.savedOverflow === void 0) return;
|
|
1623
|
+
const { value, priority } = this.savedOverflow;
|
|
1624
|
+
if (value) {
|
|
1625
|
+
document.documentElement.style.setProperty("overflow", value, priority);
|
|
1626
|
+
} else {
|
|
1627
|
+
document.documentElement.style.removeProperty("overflow");
|
|
1628
|
+
}
|
|
1629
|
+
this.savedOverflow = void 0;
|
|
1630
|
+
}
|
|
1252
1631
|
on(event, handler) {
|
|
1253
1632
|
if (!this.eventHandlers.has(event)) {
|
|
1254
1633
|
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
@@ -1263,32 +1642,43 @@ class Widget {
|
|
|
1263
1642
|
emit(event, ...args) {
|
|
1264
1643
|
var _a;
|
|
1265
1644
|
if (this.destroyed) return;
|
|
1266
|
-
(_a = this.eventHandlers.get(event)) == null ? void 0 : _a.forEach((h) =>
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
return this.storage.isPreChatCompleted(this.conversationId);
|
|
1270
|
-
}
|
|
1271
|
-
setPreChatCompleted(data) {
|
|
1272
|
-
this.storage.setPreChatCompleted(this.conversationId, data);
|
|
1645
|
+
(_a = this.eventHandlers.get(event)) == null ? void 0 : _a.forEach((h) => {
|
|
1646
|
+
h(...args);
|
|
1647
|
+
});
|
|
1273
1648
|
}
|
|
1274
1649
|
showPreChatForm() {
|
|
1275
1650
|
const formConfig = this.config.preChatForm;
|
|
1276
1651
|
this.panel.hideMessages();
|
|
1277
1652
|
this.panel.input.setVisible(false);
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1653
|
+
loadPreChatForm().then(
|
|
1654
|
+
({ PreChatForm: PreChatForm2 }) => {
|
|
1655
|
+
if (this.destroyed || this.preChatDone || this.preChatFormEl) return;
|
|
1656
|
+
this.preChatFormEl = new PreChatForm2(this.panel.messagesContainer, {
|
|
1657
|
+
title: formConfig.title ?? this.locale.preChatTitle,
|
|
1658
|
+
subtitle: formConfig.subtitle ?? this.locale.preChatSubtitle,
|
|
1659
|
+
fields: formConfig.fields,
|
|
1660
|
+
submitLabel: formConfig.submitLabel ?? this.locale.preChatSubmit,
|
|
1661
|
+
locale: this.locale,
|
|
1662
|
+
onSubmit: (data) => this.handlePreChatSubmit(data)
|
|
1663
|
+
});
|
|
1664
|
+
},
|
|
1665
|
+
(error) => {
|
|
1666
|
+
console.error("[chatcops] The pre-chat form could not load:", error instanceof Error ? error.message : error);
|
|
1667
|
+
if (this.destroyed || this.preChatDone) return;
|
|
1668
|
+
this.preChatDone = true;
|
|
1669
|
+
this.panel.showMessages();
|
|
1670
|
+
this.panel.input.setVisible(true);
|
|
1671
|
+
this.showChatView();
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1286
1674
|
}
|
|
1287
1675
|
handlePreChatSubmit(data) {
|
|
1288
1676
|
var _a, _b, _c, _d;
|
|
1289
1677
|
if (!((_a = this.config.preChatForm) == null ? void 0 : _a.enabled)) return;
|
|
1678
|
+
this.hydrate();
|
|
1290
1679
|
this.userData = data;
|
|
1291
|
-
this.
|
|
1680
|
+
this.preChatDone = true;
|
|
1681
|
+
this.saveHistory();
|
|
1292
1682
|
(_b = this.preChatFormEl) == null ? void 0 : _b.destroy();
|
|
1293
1683
|
this.preChatFormEl = void 0;
|
|
1294
1684
|
this.panel.showMessages();
|
|
@@ -1313,11 +1703,23 @@ class Widget {
|
|
|
1313
1703
|
};
|
|
1314
1704
|
this.messages.push(welcomeMsg);
|
|
1315
1705
|
this.panel.addMessage(welcomeMsg);
|
|
1706
|
+
this.renderSuggestions();
|
|
1316
1707
|
}
|
|
1317
1708
|
}
|
|
1709
|
+
renderSuggestions(suggestions = this.config.suggestions ?? []) {
|
|
1710
|
+
if (this.messages.some((message) => message.role === "user")) return;
|
|
1711
|
+
this.panel.messages.showSuggestions(
|
|
1712
|
+
suggestions,
|
|
1713
|
+
this.locale.suggestionsLabel,
|
|
1714
|
+
(suggestion) => {
|
|
1715
|
+
void this.handleSend(suggestion);
|
|
1716
|
+
}
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1318
1719
|
async handleSend(text) {
|
|
1319
1720
|
var _a, _b;
|
|
1320
1721
|
if (this.isStreaming) return;
|
|
1722
|
+
this.hydrate();
|
|
1321
1723
|
const userMsg = {
|
|
1322
1724
|
id: crypto.randomUUID(),
|
|
1323
1725
|
role: "user",
|
|
@@ -1365,22 +1767,26 @@ class Widget {
|
|
|
1365
1767
|
this.retryRequests.delete(assistantMessageId);
|
|
1366
1768
|
this.messages = this.messages.filter((message) => message.id !== assistantMessageId);
|
|
1367
1769
|
await this.sendToServer(userMessage.content, {
|
|
1368
|
-
|
|
1770
|
+
// A v1 server saved the reply under this id; a 0.5 server only knows `true`.
|
|
1771
|
+
regenerate: this.v1 ? { assistantMessageId } : true,
|
|
1369
1772
|
userMessageId: userMessage.id
|
|
1370
1773
|
});
|
|
1371
1774
|
}
|
|
1372
1775
|
async sendToServer(text, options = {}) {
|
|
1373
1776
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
1374
1777
|
if (this.isStreaming) return;
|
|
1778
|
+
const generation = this.generation;
|
|
1375
1779
|
this.isStreaming = true;
|
|
1376
1780
|
this.panel.input.setDisabled(true);
|
|
1377
1781
|
this.panel.messages.clearRegenerateButtons();
|
|
1782
|
+
this.panel.messages.beginReply();
|
|
1378
1783
|
this.panel.messages.showTyping();
|
|
1379
1784
|
const assistantMsg = {
|
|
1380
1785
|
id: crypto.randomUUID(),
|
|
1381
1786
|
role: "assistant",
|
|
1382
1787
|
content: "",
|
|
1383
|
-
status: "streaming"
|
|
1788
|
+
status: "streaming",
|
|
1789
|
+
sources: []
|
|
1384
1790
|
};
|
|
1385
1791
|
let firstChunk = true;
|
|
1386
1792
|
let requestCompleted = false;
|
|
@@ -1392,10 +1798,10 @@ class Widget {
|
|
|
1392
1798
|
this.createRequest(text, options),
|
|
1393
1799
|
{ signal: abortController.signal }
|
|
1394
1800
|
)) {
|
|
1395
|
-
if (this.
|
|
1801
|
+
if (this.isStale(generation)) break;
|
|
1396
1802
|
if (chunk.error) {
|
|
1397
1803
|
const errorType = this.resolveErrorType(chunk.error);
|
|
1398
|
-
this.markFailed(assistantMsg, this.getErrorMessage(errorType));
|
|
1804
|
+
this.markFailed(assistantMsg, this.getErrorMessage(errorType, chunk.retryAfterMs));
|
|
1399
1805
|
assistantMsg.errorType = errorType;
|
|
1400
1806
|
hasError = true;
|
|
1401
1807
|
this.retryRequests.set(assistantMsg.id, {
|
|
@@ -1418,10 +1824,19 @@ class Widget {
|
|
|
1418
1824
|
requestCompleted = true;
|
|
1419
1825
|
break;
|
|
1420
1826
|
}
|
|
1827
|
+
if (chunk.conversationId) this.conversationId = chunk.conversationId;
|
|
1828
|
+
if (chunk.messageId) {
|
|
1829
|
+
assistantMsg.id = chunk.messageId;
|
|
1830
|
+
this.v1 = true;
|
|
1831
|
+
}
|
|
1421
1832
|
if (chunk.leadCaptured) {
|
|
1422
1833
|
(_d = (_c = this.config).onLeadCaptured) == null ? void 0 : _d.call(_c, chunk.leadData);
|
|
1423
1834
|
this.emit("leadCaptured", chunk.leadData);
|
|
1424
1835
|
}
|
|
1836
|
+
if (chunk.source) {
|
|
1837
|
+
assistantMsg.sources ?? (assistantMsg.sources = []);
|
|
1838
|
+
assistantMsg.sources.push(chunk.source);
|
|
1839
|
+
}
|
|
1425
1840
|
if (chunk.content) {
|
|
1426
1841
|
if (firstChunk) {
|
|
1427
1842
|
this.panel.messages.removeTyping();
|
|
@@ -1432,7 +1847,7 @@ class Widget {
|
|
|
1432
1847
|
this.panel.messages.updateMessage(assistantMsg);
|
|
1433
1848
|
}
|
|
1434
1849
|
}
|
|
1435
|
-
if (this.
|
|
1850
|
+
if (this.isStale(generation)) return;
|
|
1436
1851
|
if (!hasError && !requestCompleted) {
|
|
1437
1852
|
requestCompleted = true;
|
|
1438
1853
|
}
|
|
@@ -1447,11 +1862,12 @@ class Widget {
|
|
|
1447
1862
|
this.emit("message", assistantMsg);
|
|
1448
1863
|
this.showLatestRegenerateButton();
|
|
1449
1864
|
if (!this.isInline && !this.panel.isVisible) {
|
|
1450
|
-
|
|
1865
|
+
this.unreadReplies++;
|
|
1866
|
+
(_g = this.fab) == null ? void 0 : _g.showBadge(this.unreadReplies);
|
|
1451
1867
|
}
|
|
1452
1868
|
}
|
|
1453
1869
|
} catch (err) {
|
|
1454
|
-
if (this.
|
|
1870
|
+
if (this.isStale(generation)) return;
|
|
1455
1871
|
this.markFailed(assistantMsg, this.locale.errorGeneric);
|
|
1456
1872
|
assistantMsg.errorType = "network";
|
|
1457
1873
|
this.retryRequests.set(assistantMsg.id, {
|
|
@@ -1472,7 +1888,8 @@ class Widget {
|
|
|
1472
1888
|
if (this.abortController === abortController) {
|
|
1473
1889
|
this.abortController = void 0;
|
|
1474
1890
|
}
|
|
1475
|
-
if (!this.
|
|
1891
|
+
if (!this.isStale(generation)) {
|
|
1892
|
+
this.panel.messages.finishReply(assistantMsg);
|
|
1476
1893
|
this.isStreaming = false;
|
|
1477
1894
|
this.panel.input.setDisabled(false);
|
|
1478
1895
|
this.panel.input.focus();
|
|
@@ -1482,8 +1899,11 @@ class Widget {
|
|
|
1482
1899
|
}
|
|
1483
1900
|
createRequest(text, options) {
|
|
1484
1901
|
var _a;
|
|
1485
|
-
const pageContext = this.config.pageContext !== false ? { url: window.location.href, title: document.title } : void 0;
|
|
1486
|
-
const includeUserData = Boolean(
|
|
1902
|
+
const pageContext = this.config.pageContext !== false ? { url: window.location.href, title: document.title, ...this.pageData ? { data: this.pageData } : {} } : void 0;
|
|
1903
|
+
const includeUserData = Boolean(
|
|
1904
|
+
(((_a = this.config.preChatForm) == null ? void 0 : _a.enabled) || this.userFromSite) && this.userData && (!this.v1 || this.userDataSentWith !== this.conversationId)
|
|
1905
|
+
);
|
|
1906
|
+
if (includeUserData) this.userDataSentWith = this.conversationId;
|
|
1487
1907
|
return {
|
|
1488
1908
|
conversationId: this.conversationId,
|
|
1489
1909
|
message: text,
|
|
@@ -1524,15 +1944,7 @@ class Widget {
|
|
|
1524
1944
|
}
|
|
1525
1945
|
}
|
|
1526
1946
|
resolveErrorType(error) {
|
|
1527
|
-
|
|
1528
|
-
case "rate_limit":
|
|
1529
|
-
case "network":
|
|
1530
|
-
case "provider_error":
|
|
1531
|
-
case "timeout":
|
|
1532
|
-
return error;
|
|
1533
|
-
default:
|
|
1534
|
-
return "provider_error";
|
|
1535
|
-
}
|
|
1947
|
+
return error in ERROR_MESSAGES || error === "rate_limit" ? error : "provider_error";
|
|
1536
1948
|
}
|
|
1537
1949
|
/**
|
|
1538
1950
|
* Marks a reply as failed. Text that already streamed stays, and the error
|
|
@@ -1546,19 +1958,13 @@ class Widget {
|
|
|
1546
1958
|
message.content = errorMessage;
|
|
1547
1959
|
}
|
|
1548
1960
|
}
|
|
1549
|
-
getErrorMessage(errorType) {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
return this.locale.errorRateLimit;
|
|
1553
|
-
case "network":
|
|
1554
|
-
case "timeout":
|
|
1555
|
-
return this.locale.errorNetwork;
|
|
1556
|
-
default:
|
|
1557
|
-
return this.locale.errorGeneric;
|
|
1961
|
+
getErrorMessage(errorType, retryAfterMs) {
|
|
1962
|
+
if (errorType === "rate_limit") {
|
|
1963
|
+
return retryAfterMs ? this.locale.errorRateLimitWait.replace("{seconds}", String(Math.ceil(retryAfterMs / 1e3))) : this.locale.errorRateLimit;
|
|
1558
1964
|
}
|
|
1965
|
+
return this.locale[ERROR_MESSAGES[errorType] ?? "errorGeneric"];
|
|
1559
1966
|
}
|
|
1560
1967
|
saveHistory() {
|
|
1561
|
-
if (this.config.persistHistory === false) return;
|
|
1562
1968
|
const maxMessages = this.config.maxMessages ?? 50;
|
|
1563
1969
|
const toSave = this.messages.filter((m) => m.id !== "welcome").slice(-maxMessages);
|
|
1564
1970
|
this.storage.save({
|
|
@@ -1569,14 +1975,15 @@ class Widget {
|
|
|
1569
1975
|
content: m.content,
|
|
1570
1976
|
status: m.status,
|
|
1571
1977
|
errorType: m.errorType,
|
|
1572
|
-
timestamp: Date.now()
|
|
1978
|
+
timestamp: Date.now(),
|
|
1979
|
+
...m.sources && m.sources.length > 0 ? { sources: m.sources } : {}
|
|
1573
1980
|
})),
|
|
1981
|
+
...this.userData ? { userData: this.userData } : {},
|
|
1982
|
+
...this.preChatDone ? { preChatDone: true } : {},
|
|
1574
1983
|
updatedAt: Date.now()
|
|
1575
1984
|
});
|
|
1576
1985
|
}
|
|
1577
1986
|
}
|
|
1578
|
-
let instance = null;
|
|
1579
|
-
const persistentHandlers = /* @__PURE__ */ new Map();
|
|
1580
1987
|
function parseAutoOpen(value) {
|
|
1581
1988
|
if (value === void 0) return void 0;
|
|
1582
1989
|
if (value === "" || value === "true") return true;
|
|
@@ -1584,6 +1991,49 @@ function parseAutoOpen(value) {
|
|
|
1584
1991
|
if (/^\d+$/.test(value)) return Number(value);
|
|
1585
1992
|
return void 0;
|
|
1586
1993
|
}
|
|
1994
|
+
function configFromScript(script) {
|
|
1995
|
+
const apiUrl = script.dataset.apiUrl;
|
|
1996
|
+
if (!apiUrl) return void 0;
|
|
1997
|
+
const mode = script.dataset.mode;
|
|
1998
|
+
const container = script.dataset.container;
|
|
1999
|
+
return {
|
|
2000
|
+
apiUrl,
|
|
2001
|
+
turnstileSiteKey: script.dataset.turnstileSiteKey,
|
|
2002
|
+
userToken: script.dataset.userToken,
|
|
2003
|
+
mode: mode || void 0,
|
|
2004
|
+
container: container || void 0,
|
|
2005
|
+
theme: {
|
|
2006
|
+
mode: script.dataset.themeMode || void 0,
|
|
2007
|
+
accent: script.dataset.accent,
|
|
2008
|
+
accentDark: script.dataset.accentDark,
|
|
2009
|
+
textColor: script.dataset.textColor,
|
|
2010
|
+
bgColor: script.dataset.bgColor,
|
|
2011
|
+
fontFamily: script.dataset.fontFamily,
|
|
2012
|
+
borderRadius: script.dataset.borderRadius,
|
|
2013
|
+
position: script.dataset.position || void 0
|
|
2014
|
+
},
|
|
2015
|
+
branding: {
|
|
2016
|
+
name: script.dataset.brandName,
|
|
2017
|
+
avatar: script.dataset.brandAvatar,
|
|
2018
|
+
subtitle: script.dataset.brandSubtitle
|
|
2019
|
+
},
|
|
2020
|
+
welcomeMessage: script.dataset.welcomeMessage,
|
|
2021
|
+
welcomeBubble: script.dataset.welcomeBubble ? {
|
|
2022
|
+
text: script.dataset.welcomeBubble,
|
|
2023
|
+
delay: /^\d+$/.test(script.dataset.welcomeBubbleDelay ?? "") ? Number(script.dataset.welcomeBubbleDelay) : void 0
|
|
2024
|
+
} : void 0,
|
|
2025
|
+
autoOpen: parseAutoOpen(script.dataset.autoOpen),
|
|
2026
|
+
placeholder: script.dataset.placeholder,
|
|
2027
|
+
disclosure: script.dataset.disclosure === "false" ? false : { text: script.dataset.disclosureText, learnMoreUrl: script.dataset.disclosureUrl },
|
|
2028
|
+
locale: script.dataset.locale,
|
|
2029
|
+
dir: ["ltr", "rtl", "auto"].find((dir) => dir === script.dataset.dir),
|
|
2030
|
+
persistence: ["local", "session", "none"].find((mode2) => mode2 === script.dataset.persistence),
|
|
2031
|
+
historyTtlDays: /^\d+$/.test(script.dataset.historyTtlDays ?? "") ? Number(script.dataset.historyTtlDays) : void 0
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
const VERSION = "0.7.0";
|
|
2035
|
+
let instance = null;
|
|
2036
|
+
const persistentHandlers = /* @__PURE__ */ new Map();
|
|
1587
2037
|
const ChatCops = {
|
|
1588
2038
|
init(config) {
|
|
1589
2039
|
if (instance) {
|
|
@@ -1604,6 +2054,46 @@ const ChatCops = {
|
|
|
1604
2054
|
close() {
|
|
1605
2055
|
instance == null ? void 0 : instance.close();
|
|
1606
2056
|
},
|
|
2057
|
+
toggle() {
|
|
2058
|
+
instance == null ? void 0 : instance.toggle();
|
|
2059
|
+
},
|
|
2060
|
+
/** Sends `text` as the visitor (opening the chat unless `open: false`); resolves when the reply is done. */
|
|
2061
|
+
sendMessage(text, options) {
|
|
2062
|
+
return (instance == null ? void 0 : instance.sendMessage(text, options)) ?? Promise.resolve();
|
|
2063
|
+
},
|
|
2064
|
+
/** A fresh conversation: new id, the log back to the welcome message. */
|
|
2065
|
+
newConversation() {
|
|
2066
|
+
instance == null ? void 0 : instance.newConversation();
|
|
2067
|
+
},
|
|
2068
|
+
/** A fresh conversation, and the saved transcript and pre-chat answers removed from this browser. */
|
|
2069
|
+
clear() {
|
|
2070
|
+
instance == null ? void 0 : instance.clear();
|
|
2071
|
+
},
|
|
2072
|
+
/**
|
|
2073
|
+
* Adds a language, or replaces one: its strings, with English for any it
|
|
2074
|
+
* leaves out, and `dir: 'rtl'` for right-to-left. Use the code as `locale`.
|
|
2075
|
+
*/
|
|
2076
|
+
registerLocale(code, strings, options) {
|
|
2077
|
+
registerWidgetLocale(code, strings);
|
|
2078
|
+
if (options == null ? void 0 : options.dir) setLanguageDirection(code, options.dir);
|
|
2079
|
+
},
|
|
2080
|
+
/**
|
|
2081
|
+
* The signed-in visitor: `token` for the server, `name` and `email` as
|
|
2082
|
+
* visitor details. Another user, or `null` to sign out, clears the chat.
|
|
2083
|
+
*/
|
|
2084
|
+
setUser(user) {
|
|
2085
|
+
instance == null ? void 0 : instance.setUser(user);
|
|
2086
|
+
},
|
|
2087
|
+
/** Details the bot should know for the next messages, such as `{ plan: 'Team' }`. `null` removes them. */
|
|
2088
|
+
setContext(data) {
|
|
2089
|
+
instance == null ? void 0 : instance.setContext(data);
|
|
2090
|
+
},
|
|
2091
|
+
/** Changes the theme, branding or wording of the running chat. What you leave out stays as it is. */
|
|
2092
|
+
update(changes) {
|
|
2093
|
+
instance == null ? void 0 : instance.update(changes);
|
|
2094
|
+
},
|
|
2095
|
+
/** The widget's version. */
|
|
2096
|
+
version: VERSION,
|
|
1607
2097
|
destroy() {
|
|
1608
2098
|
instance == null ? void 0 : instance.destroy();
|
|
1609
2099
|
instance = null;
|
|
@@ -1627,37 +2117,8 @@ const ChatCops = {
|
|
|
1627
2117
|
}
|
|
1628
2118
|
};
|
|
1629
2119
|
function autoInit(script) {
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
if (!apiUrl) return;
|
|
1633
|
-
const mode = script.dataset.mode;
|
|
1634
|
-
const container = script.dataset.container;
|
|
1635
|
-
ChatCops.init({
|
|
1636
|
-
apiUrl,
|
|
1637
|
-
mode: mode || void 0,
|
|
1638
|
-
container: container || void 0,
|
|
1639
|
-
theme: {
|
|
1640
|
-
accent: script.dataset.accent,
|
|
1641
|
-
textColor: script.dataset.textColor,
|
|
1642
|
-
bgColor: script.dataset.bgColor,
|
|
1643
|
-
fontFamily: script.dataset.fontFamily,
|
|
1644
|
-
borderRadius: script.dataset.borderRadius,
|
|
1645
|
-
position: script.dataset.position || void 0
|
|
1646
|
-
},
|
|
1647
|
-
branding: {
|
|
1648
|
-
name: script.dataset.brandName,
|
|
1649
|
-
avatar: script.dataset.brandAvatar,
|
|
1650
|
-
subtitle: script.dataset.brandSubtitle
|
|
1651
|
-
},
|
|
1652
|
-
welcomeMessage: script.dataset.welcomeMessage,
|
|
1653
|
-
welcomeBubble: script.dataset.welcomeBubble ? {
|
|
1654
|
-
text: script.dataset.welcomeBubble,
|
|
1655
|
-
delay: script.dataset.welcomeBubbleDelay ? parseInt(script.dataset.welcomeBubbleDelay, 10) : void 0
|
|
1656
|
-
} : void 0,
|
|
1657
|
-
autoOpen: parseAutoOpen(script.dataset.autoOpen),
|
|
1658
|
-
placeholder: script.dataset.placeholder,
|
|
1659
|
-
locale: script.dataset.locale
|
|
1660
|
-
});
|
|
2120
|
+
const config = script && configFromScript(script);
|
|
2121
|
+
if (config) ChatCops.init(config);
|
|
1661
2122
|
}
|
|
1662
2123
|
const currentScript = typeof document !== "undefined" ? document.currentScript : null;
|
|
1663
2124
|
if (currentScript && currentScript.dataset.apiUrl) {
|
|
@@ -1670,6 +2131,225 @@ if (currentScript && currentScript.dataset.apiUrl) {
|
|
|
1670
2131
|
if (typeof window !== "undefined") {
|
|
1671
2132
|
window.ChatCops = ChatCops;
|
|
1672
2133
|
}
|
|
2134
|
+
let loading;
|
|
2135
|
+
async function turnstileToken(siteKey) {
|
|
2136
|
+
loading ?? (loading = new Promise((resolve, reject) => {
|
|
2137
|
+
const script = document.createElement("script");
|
|
2138
|
+
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
2139
|
+
script.onload = resolve;
|
|
2140
|
+
script.onerror = reject;
|
|
2141
|
+
document.head.append(script);
|
|
2142
|
+
}));
|
|
2143
|
+
await loading;
|
|
2144
|
+
const box = document.createElement("div");
|
|
2145
|
+
box.style.cssText = "position:fixed;bottom:96px;right:24px;z-index:2147483647";
|
|
2146
|
+
document.body.append(box);
|
|
2147
|
+
try {
|
|
2148
|
+
return await new Promise((resolve, reject) => {
|
|
2149
|
+
window.turnstile.render(box, {
|
|
2150
|
+
sitekey: siteKey,
|
|
2151
|
+
appearance: "interaction-only",
|
|
2152
|
+
callback: resolve,
|
|
2153
|
+
"error-callback": reject
|
|
2154
|
+
});
|
|
2155
|
+
});
|
|
2156
|
+
} finally {
|
|
2157
|
+
box.remove();
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
const turnstile = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2161
|
+
__proto__: null,
|
|
2162
|
+
turnstileToken
|
|
2163
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
2164
|
+
const prechatStyles = ".cc-prechat{flex:1;overflow-y:auto;padding:24px 16px;display:flex;flex-direction:column;gap:16px}.cc-prechat-title{font-size:18px;font-weight:600;color:var(--cc-text)}.cc-prechat-subtitle{font-size:13px;color:var(--cc-text-secondary);margin-top:-8px}.cc-prechat-fields{display:flex;flex-direction:column;gap:14px}.cc-prechat-field{display:flex;flex-direction:column;gap:4px}.cc-prechat-label{font-size:13px;font-weight:500;color:var(--cc-text)}.cc-prechat-label .cc-required{color:var(--cc-danger);margin-inline-start:2px}.cc-prechat-input,.cc-prechat-select,.cc-prechat-textarea{background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;outline:none;transition:border-color .15s}.cc-prechat-input:focus,.cc-prechat-select:focus,.cc-prechat-textarea:focus{border-color:var(--cc-accent)}.cc-prechat-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}.cc-prechat-textarea{resize:none;line-height:1.5}.cc-prechat-error{font-size:12px;color:var(--cc-danger);min-height:0}.cc-prechat-error:empty{display:none}.cc-prechat-submit{background:var(--cc-accent-contrast);color:var(--cc-on-accent);border:none;border-radius:8px;padding:10px 16px;font-family:var(--cc-font);font-size:14px;font-weight:500;cursor:pointer;transition:background .15s;margin-top:8px}.cc-prechat-submit:hover{background:var(--cc-accent-hover)}.cc-prechat-submit:disabled{opacity:.5;cursor:not-allowed}.cc-prechat-input:focus-visible,.cc-prechat-select:focus-visible,.cc-prechat-textarea:focus-visible,.cc-prechat-submit:focus-visible{outline:2px solid transparent;box-shadow:var(--cc-focus-ring)}@media(max-width:640px){.cc-prechat-input,.cc-prechat-select,.cc-prechat-textarea{font-size:16px}}";
|
|
2165
|
+
const MAX_PRECHAT_FIELD_LENGTH = 500;
|
|
2166
|
+
class PreChatForm {
|
|
2167
|
+
constructor(parent, options) {
|
|
2168
|
+
this.fieldElements = /* @__PURE__ */ new Map();
|
|
2169
|
+
this.errorElements = /* @__PURE__ */ new Map();
|
|
2170
|
+
this.options = options;
|
|
2171
|
+
this.container = document.createElement("div");
|
|
2172
|
+
this.container.className = "cc-prechat";
|
|
2173
|
+
if (options.title) {
|
|
2174
|
+
const title = document.createElement("div");
|
|
2175
|
+
title.className = "cc-prechat-title";
|
|
2176
|
+
title.textContent = options.title;
|
|
2177
|
+
this.container.appendChild(title);
|
|
2178
|
+
}
|
|
2179
|
+
if (options.subtitle) {
|
|
2180
|
+
const subtitle = document.createElement("div");
|
|
2181
|
+
subtitle.className = "cc-prechat-subtitle";
|
|
2182
|
+
subtitle.textContent = options.subtitle;
|
|
2183
|
+
this.container.appendChild(subtitle);
|
|
2184
|
+
}
|
|
2185
|
+
const form = document.createElement("form");
|
|
2186
|
+
form.className = "cc-prechat-fields";
|
|
2187
|
+
form.noValidate = true;
|
|
2188
|
+
form.addEventListener("submit", (e) => {
|
|
2189
|
+
e.preventDefault();
|
|
2190
|
+
this.handleSubmit();
|
|
2191
|
+
});
|
|
2192
|
+
for (const field of options.fields) {
|
|
2193
|
+
form.appendChild(this.createField(field));
|
|
2194
|
+
}
|
|
2195
|
+
this.submitBtn = document.createElement("button");
|
|
2196
|
+
this.submitBtn.type = "submit";
|
|
2197
|
+
this.submitBtn.className = "cc-prechat-submit";
|
|
2198
|
+
this.submitBtn.textContent = options.submitLabel;
|
|
2199
|
+
form.appendChild(this.submitBtn);
|
|
2200
|
+
this.container.appendChild(form);
|
|
2201
|
+
parent.appendChild(this.container);
|
|
2202
|
+
addStyles(parent.getRootNode());
|
|
2203
|
+
this.updateSubmitState();
|
|
2204
|
+
const firstInput = this.fieldElements.values().next().value;
|
|
2205
|
+
if (firstInput) {
|
|
2206
|
+
setTimeout(() => firstInput.focus(), 100);
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
createField(field) {
|
|
2210
|
+
const wrapper = document.createElement("div");
|
|
2211
|
+
wrapper.className = "cc-prechat-field";
|
|
2212
|
+
const inputId = `cc-prechat-${field.name}`;
|
|
2213
|
+
const errorId = `cc-prechat-${field.name}-error`;
|
|
2214
|
+
const label = document.createElement("label");
|
|
2215
|
+
label.className = "cc-prechat-label";
|
|
2216
|
+
label.htmlFor = inputId;
|
|
2217
|
+
label.textContent = field.label;
|
|
2218
|
+
if (field.required) {
|
|
2219
|
+
const req = document.createElement("span");
|
|
2220
|
+
req.className = "cc-required";
|
|
2221
|
+
req.textContent = "*";
|
|
2222
|
+
label.appendChild(req);
|
|
2223
|
+
}
|
|
2224
|
+
wrapper.appendChild(label);
|
|
2225
|
+
let input;
|
|
2226
|
+
if (field.type === "select") {
|
|
2227
|
+
const select = document.createElement("select");
|
|
2228
|
+
select.className = "cc-prechat-select";
|
|
2229
|
+
const defaultOpt = document.createElement("option");
|
|
2230
|
+
defaultOpt.value = "";
|
|
2231
|
+
defaultOpt.textContent = field.placeholder ?? this.options.locale.selectPlaceholder.replace("{label}", field.label.toLowerCase());
|
|
2232
|
+
defaultOpt.disabled = true;
|
|
2233
|
+
defaultOpt.selected = true;
|
|
2234
|
+
select.appendChild(defaultOpt);
|
|
2235
|
+
for (const opt of field.options ?? []) {
|
|
2236
|
+
const option = document.createElement("option");
|
|
2237
|
+
option.value = opt;
|
|
2238
|
+
option.textContent = opt;
|
|
2239
|
+
select.appendChild(option);
|
|
2240
|
+
}
|
|
2241
|
+
input = select;
|
|
2242
|
+
} else if (field.type === "textarea") {
|
|
2243
|
+
const textarea = document.createElement("textarea");
|
|
2244
|
+
textarea.className = "cc-prechat-textarea";
|
|
2245
|
+
textarea.placeholder = field.placeholder ?? "";
|
|
2246
|
+
textarea.rows = 3;
|
|
2247
|
+
textarea.maxLength = MAX_PRECHAT_FIELD_LENGTH;
|
|
2248
|
+
input = textarea;
|
|
2249
|
+
} else {
|
|
2250
|
+
const textInput = document.createElement("input");
|
|
2251
|
+
textInput.type = field.type;
|
|
2252
|
+
textInput.className = "cc-prechat-input";
|
|
2253
|
+
textInput.maxLength = MAX_PRECHAT_FIELD_LENGTH;
|
|
2254
|
+
textInput.placeholder = field.placeholder ?? "";
|
|
2255
|
+
input = textInput;
|
|
2256
|
+
}
|
|
2257
|
+
input.name = field.name;
|
|
2258
|
+
input.id = inputId;
|
|
2259
|
+
if (field.required) {
|
|
2260
|
+
input.setAttribute("aria-required", "true");
|
|
2261
|
+
}
|
|
2262
|
+
input.addEventListener("input", () => {
|
|
2263
|
+
this.clearError(field.name);
|
|
2264
|
+
this.updateSubmitState();
|
|
2265
|
+
});
|
|
2266
|
+
if (input instanceof HTMLSelectElement) {
|
|
2267
|
+
input.addEventListener("change", () => {
|
|
2268
|
+
this.clearError(field.name);
|
|
2269
|
+
this.updateSubmitState();
|
|
2270
|
+
});
|
|
2271
|
+
}
|
|
2272
|
+
wrapper.appendChild(input);
|
|
2273
|
+
this.fieldElements.set(field.name, input);
|
|
2274
|
+
const error = document.createElement("div");
|
|
2275
|
+
error.className = "cc-prechat-error";
|
|
2276
|
+
error.id = errorId;
|
|
2277
|
+
wrapper.appendChild(error);
|
|
2278
|
+
this.errorElements.set(field.name, error);
|
|
2279
|
+
return wrapper;
|
|
2280
|
+
}
|
|
2281
|
+
updateSubmitState() {
|
|
2282
|
+
const allRequiredFilled = this.options.fields.every((field) => {
|
|
2283
|
+
if (!field.required) return true;
|
|
2284
|
+
const el = this.fieldElements.get(field.name);
|
|
2285
|
+
return el ? el.value.trim().length > 0 : false;
|
|
2286
|
+
});
|
|
2287
|
+
this.submitBtn.disabled = !allRequiredFilled;
|
|
2288
|
+
}
|
|
2289
|
+
handleSubmit() {
|
|
2290
|
+
const data = {};
|
|
2291
|
+
let valid = true;
|
|
2292
|
+
for (const field of this.options.fields) {
|
|
2293
|
+
const el = this.fieldElements.get(field.name);
|
|
2294
|
+
if (!el) continue;
|
|
2295
|
+
const value = el.value.trim();
|
|
2296
|
+
data[field.name] = value;
|
|
2297
|
+
if (field.required && !value) {
|
|
2298
|
+
this.showError(field.name, this.options.locale.preChatRequired);
|
|
2299
|
+
valid = false;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (field.type === "email" && value && !this.isValidEmail(value)) {
|
|
2303
|
+
this.showError(field.name, this.options.locale.preChatInvalidEmail);
|
|
2304
|
+
valid = false;
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
if (valid) {
|
|
2308
|
+
this.options.onSubmit(data);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
showError(fieldName, message) {
|
|
2312
|
+
const el = this.errorElements.get(fieldName);
|
|
2313
|
+
if (el) el.textContent = message;
|
|
2314
|
+
const fieldEl = this.fieldElements.get(fieldName);
|
|
2315
|
+
if (fieldEl && el) {
|
|
2316
|
+
fieldEl.setAttribute("aria-invalid", "true");
|
|
2317
|
+
fieldEl.setAttribute("aria-describedby", el.id);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
clearError(fieldName) {
|
|
2321
|
+
const el = this.errorElements.get(fieldName);
|
|
2322
|
+
if (el) el.textContent = "";
|
|
2323
|
+
const fieldEl = this.fieldElements.get(fieldName);
|
|
2324
|
+
if (fieldEl && el) {
|
|
2325
|
+
if (fieldEl.getAttribute("aria-describedby") === el.id) {
|
|
2326
|
+
fieldEl.removeAttribute("aria-describedby");
|
|
2327
|
+
}
|
|
2328
|
+
if (fieldEl.getAttribute("aria-invalid") === "true") {
|
|
2329
|
+
fieldEl.removeAttribute("aria-invalid");
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
isValidEmail(email) {
|
|
2334
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
2335
|
+
}
|
|
2336
|
+
destroy() {
|
|
2337
|
+
this.container.remove();
|
|
2338
|
+
this.fieldElements.clear();
|
|
2339
|
+
this.errorElements.clear();
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function addStyles(root) {
|
|
2343
|
+
if (!(root instanceof ShadowRoot) || root.querySelector("style[data-cc-prechat]")) return;
|
|
2344
|
+
const style = document.createElement("style");
|
|
2345
|
+
style.dataset.ccPrechat = "";
|
|
2346
|
+
style.textContent = prechatStyles;
|
|
2347
|
+
root.appendChild(style);
|
|
2348
|
+
}
|
|
2349
|
+
const prechatForm = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
|
|
2350
|
+
__proto__: null,
|
|
2351
|
+
PreChatForm
|
|
2352
|
+
}, Symbol.toStringTag, { value: "Module" }));
|
|
1673
2353
|
if (typeof window !== "undefined" && window.ChatCops && window.ChatCops.default) {
|
|
1674
2354
|
Object.assign(window.ChatCops, window.ChatCops.default);
|
|
1675
2355
|
}
|