@chatcops/widget 0.6.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 +2 -1
- package/dist/api/client.d.ts +17 -2
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/protocol.d.ts +7 -1
- package/dist/api/protocol.d.ts.map +1 -1
- package/dist/api/turnstile.d.ts.map +1 -1
- package/dist/api/types.d.ts +7 -0
- package/dist/api/types.d.ts.map +1 -1
- package/dist/chatcops.esm.js +1093 -499
- 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 -1
- 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 +38 -1
- 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 +7 -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/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 +125 -14
- package/dist/widget.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/chatcops.esm.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
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,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;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,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;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}.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,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:#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}.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,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)}.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;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}.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;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}}';
|
|
2
3
|
const HOST_ID = "chatcops-root";
|
|
3
|
-
|
|
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 = {}) {
|
|
4
17
|
const host = document.createElement("div");
|
|
5
18
|
host.id = nextHostId();
|
|
6
19
|
host.className = HOST_ID;
|
|
20
|
+
host.dir = resolveDirection(options.dir, options.locale);
|
|
21
|
+
if (options.locale) host.lang = options.locale;
|
|
7
22
|
if (container) {
|
|
8
23
|
container.appendChild(host);
|
|
9
24
|
} else {
|
|
@@ -11,7 +26,7 @@ function createShadowRoot(container) {
|
|
|
11
26
|
}
|
|
12
27
|
const shadow = host.attachShadow({ mode: "open" });
|
|
13
28
|
const style = document.createElement("style");
|
|
14
|
-
style.textContent = widgetStyles;
|
|
29
|
+
style.textContent = launcherStyles + widgetStyles;
|
|
15
30
|
shadow.appendChild(style);
|
|
16
31
|
return shadow;
|
|
17
32
|
}
|
|
@@ -24,6 +39,8 @@ function nextHostId() {
|
|
|
24
39
|
while (document.getElementById(`${HOST_ID}-${n}`)) n += 1;
|
|
25
40
|
return `${HOST_ID}-${n}`;
|
|
26
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>`;
|
|
27
44
|
const ESCAPE_MAP = {
|
|
28
45
|
"&": "&",
|
|
29
46
|
"<": "<",
|
|
@@ -88,29 +105,108 @@ function renderMarkdown(input) {
|
|
|
88
105
|
}
|
|
89
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>';
|
|
90
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;
|
|
91
110
|
class Messages {
|
|
92
|
-
constructor(parent, actions) {
|
|
111
|
+
constructor(parent, actions, options = {}) {
|
|
112
|
+
this.streams = /* @__PURE__ */ new Map();
|
|
93
113
|
this.typingEl = null;
|
|
114
|
+
this.suggestionsEl = null;
|
|
115
|
+
this.disclosureEl = null;
|
|
94
116
|
this.actions = actions;
|
|
117
|
+
this.streamIntervalMs = options.streamIntervalMs ?? STREAM_INTERVAL_MS;
|
|
95
118
|
this.container = document.createElement("div");
|
|
96
119
|
this.container.className = "cc-messages";
|
|
97
120
|
this.container.setAttribute("role", "log");
|
|
98
121
|
this.container.setAttribute("aria-live", "polite");
|
|
99
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");
|
|
100
142
|
}
|
|
101
143
|
addMessage(msg) {
|
|
144
|
+
const follow = msg.role === "user" || this.nearBottom();
|
|
102
145
|
this.removeTyping();
|
|
146
|
+
if (msg.role === "user") this.removeSuggestions();
|
|
103
147
|
const el = this.createMessageElement(msg);
|
|
104
148
|
this.container.appendChild(el);
|
|
105
|
-
this.scrollToBottom();
|
|
149
|
+
if (follow) this.scrollToBottom();
|
|
106
150
|
return el;
|
|
107
151
|
}
|
|
108
152
|
updateMessage(msg) {
|
|
109
153
|
const el = this.findMessage(msg.id);
|
|
110
|
-
if (el)
|
|
111
|
-
|
|
112
|
-
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;
|
|
113
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;
|
|
114
210
|
}
|
|
115
211
|
showTyping() {
|
|
116
212
|
var _a;
|
|
@@ -127,11 +223,61 @@ class Messages {
|
|
|
127
223
|
this.typingEl = null;
|
|
128
224
|
}
|
|
129
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
|
+
}
|
|
130
249
|
clear() {
|
|
250
|
+
for (const id of [...this.streams.keys()]) this.endStream(id);
|
|
131
251
|
this.container.innerHTML = "";
|
|
132
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);
|
|
133
278
|
}
|
|
134
279
|
removeMessage(id) {
|
|
280
|
+
this.endStream(id);
|
|
135
281
|
const el = this.findMessage(id);
|
|
136
282
|
el == null ? void 0 : el.remove();
|
|
137
283
|
}
|
|
@@ -174,6 +320,7 @@ class Messages {
|
|
|
174
320
|
return el;
|
|
175
321
|
}
|
|
176
322
|
renderMessageElement(el, msg) {
|
|
323
|
+
var _a;
|
|
177
324
|
el.className = `cc-message cc-message-${msg.role}`;
|
|
178
325
|
el.dataset.id = msg.id;
|
|
179
326
|
if (msg.role === "assistant" && msg.id !== "welcome") {
|
|
@@ -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,6 +431,31 @@ 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) {
|
|
@@ -286,6 +483,11 @@ class Input {
|
|
|
286
483
|
this.area.appendChild(this.sendBtn);
|
|
287
484
|
parent.appendChild(this.area);
|
|
288
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
|
+
}
|
|
289
491
|
focus() {
|
|
290
492
|
this.textarea.focus();
|
|
291
493
|
}
|
|
@@ -322,7 +524,7 @@ class Input {
|
|
|
322
524
|
}
|
|
323
525
|
const PANEL_ID = "cc-panel";
|
|
324
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>`;
|
|
325
|
-
const CLOSE_ICON
|
|
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>`;
|
|
326
528
|
class Panel {
|
|
327
529
|
constructor(root, options) {
|
|
328
530
|
this.inline = options.inline ?? false;
|
|
@@ -337,39 +539,38 @@ class Panel {
|
|
|
337
539
|
if (!options.inline) {
|
|
338
540
|
this.el.id = PANEL_ID;
|
|
339
541
|
this.el.setAttribute("role", "dialog");
|
|
340
|
-
this.el.setAttribute("aria-
|
|
542
|
+
this.el.setAttribute("aria-modal", "true");
|
|
341
543
|
this.el.addEventListener("keydown", (e) => {
|
|
342
|
-
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
|
+
}
|
|
343
566
|
});
|
|
344
567
|
this.setHidden(true);
|
|
345
568
|
}
|
|
346
|
-
|
|
347
|
-
header
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
img.src = options.branding.avatar;
|
|
352
|
-
img.alt = options.branding.name;
|
|
353
|
-
header.appendChild(img);
|
|
354
|
-
} else {
|
|
355
|
-
const placeholder = document.createElement("div");
|
|
356
|
-
placeholder.className = "cc-header-avatar-placeholder";
|
|
357
|
-
placeholder.innerHTML = BOT_ICON;
|
|
358
|
-
header.appendChild(placeholder);
|
|
359
|
-
}
|
|
360
|
-
const info = document.createElement("div");
|
|
361
|
-
info.className = "cc-header-info";
|
|
362
|
-
info.innerHTML = `<div class="cc-header-name">${this.escapeHtml(options.branding.name)}</div><div class="cc-header-subtitle">${this.escapeHtml(options.branding.subtitle)}</div>`;
|
|
363
|
-
header.appendChild(info);
|
|
364
|
-
if (!options.inline) {
|
|
365
|
-
const closeBtn = document.createElement("button");
|
|
366
|
-
closeBtn.className = "cc-header-close";
|
|
367
|
-
closeBtn.setAttribute("aria-label", options.closeLabel);
|
|
368
|
-
closeBtn.innerHTML = CLOSE_ICON$1;
|
|
369
|
-
closeBtn.addEventListener("click", options.onClose);
|
|
370
|
-
header.appendChild(closeBtn);
|
|
371
|
-
}
|
|
372
|
-
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);
|
|
373
574
|
if (options.preChatEnabled) {
|
|
374
575
|
this.messagesContainer = document.createElement("div");
|
|
375
576
|
this.messagesContainer.className = "cc-messages-wrapper";
|
|
@@ -383,12 +584,51 @@ class Panel {
|
|
|
383
584
|
sendLabel: options.sendLabel,
|
|
384
585
|
onSend: options.onSend
|
|
385
586
|
});
|
|
386
|
-
|
|
387
|
-
footer.className = "cc-footer";
|
|
388
|
-
footer.innerHTML = options.footerText;
|
|
389
|
-
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);
|
|
390
591
|
root.appendChild(this.el);
|
|
391
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
|
+
}
|
|
392
632
|
show() {
|
|
393
633
|
this.el.classList.add("cc-visible");
|
|
394
634
|
if (!this.inline) {
|
|
@@ -430,12 +670,11 @@ class Panel {
|
|
|
430
670
|
return div.innerHTML;
|
|
431
671
|
}
|
|
432
672
|
}
|
|
433
|
-
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>`;
|
|
434
|
-
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>`;
|
|
435
673
|
class FAB {
|
|
436
674
|
constructor(root, options) {
|
|
437
675
|
this.openLabel = options.openLabel;
|
|
438
676
|
this.closeLabel = options.closeLabel;
|
|
677
|
+
this.unreadLabel = options.unreadLabel;
|
|
439
678
|
this.el = document.createElement("button");
|
|
440
679
|
this.el.className = "cc-fab cc-pulse";
|
|
441
680
|
this.el.setAttribute("aria-label", this.openLabel);
|
|
@@ -455,20 +694,45 @@ class FAB {
|
|
|
455
694
|
root.appendChild(this.el);
|
|
456
695
|
}
|
|
457
696
|
setOpen(open) {
|
|
458
|
-
|
|
697
|
+
if (open) this.hideBadge();
|
|
698
|
+
this.el.innerHTML = open ? CLOSE_ICON$1 : CHAT_ICON;
|
|
459
699
|
if (!open) this.el.appendChild(this.badge);
|
|
460
700
|
this.el.classList.toggle("cc-open", open);
|
|
461
701
|
this.el.setAttribute("aria-label", open ? this.closeLabel : this.openLabel);
|
|
462
702
|
this.el.setAttribute("aria-expanded", String(open));
|
|
463
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
|
+
}
|
|
464
714
|
focus() {
|
|
465
715
|
this.el.focus();
|
|
466
716
|
}
|
|
467
|
-
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
|
+
}
|
|
468
728
|
this.badge.classList.add("cc-visible");
|
|
469
729
|
}
|
|
470
730
|
hideBadge() {
|
|
471
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
|
+
}
|
|
472
736
|
}
|
|
473
737
|
destroy() {
|
|
474
738
|
this.el.remove();
|
|
@@ -483,8 +747,15 @@ class WelcomeBubble {
|
|
|
483
747
|
if (options.position === "bottom-left") {
|
|
484
748
|
this.el.classList.add("cc-left");
|
|
485
749
|
}
|
|
486
|
-
this.el.textContent = options.text;
|
|
487
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
|
+
});
|
|
488
759
|
const closeBtn = document.createElement("button");
|
|
489
760
|
closeBtn.type = "button";
|
|
490
761
|
closeBtn.className = "cc-bubble-close";
|
|
@@ -494,20 +765,13 @@ class WelcomeBubble {
|
|
|
494
765
|
e.stopPropagation();
|
|
495
766
|
this.hide();
|
|
496
767
|
});
|
|
497
|
-
this.el.
|
|
498
|
-
this.el.addEventListener("click", () => {
|
|
499
|
-
this.hide();
|
|
500
|
-
options.onClick();
|
|
501
|
-
});
|
|
768
|
+
this.el.append(trigger, closeBtn);
|
|
502
769
|
root.appendChild(this.el);
|
|
503
|
-
if (options.showOnce) {
|
|
504
|
-
try {
|
|
505
|
-
if (sessionStorage.getItem(BUBBLE_SHOWN_KEY)) return;
|
|
506
|
-
sessionStorage.setItem(BUBBLE_SHOWN_KEY, "1");
|
|
507
|
-
} catch {
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
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
|
+
}
|
|
511
775
|
this.el.classList.add("cc-visible");
|
|
512
776
|
this.setHidden(false);
|
|
513
777
|
}, options.delay ?? 3e3);
|
|
@@ -530,43 +794,126 @@ class WelcomeBubble {
|
|
|
530
794
|
this.el.remove();
|
|
531
795
|
}
|
|
532
796
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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);
|
|
557
892
|
}
|
|
558
893
|
}
|
|
559
894
|
class ChatClient {
|
|
560
895
|
constructor(options) {
|
|
561
|
-
this.saved = {};
|
|
562
896
|
this.apiUrl = options.apiUrl;
|
|
563
897
|
this.turnstileSiteKey = options.turnstileSiteKey;
|
|
898
|
+
this.userToken = options.userToken;
|
|
564
899
|
this.timeout = options.timeout ?? 3e4;
|
|
565
900
|
this.sessionKey = `chatcops-session:${options.apiUrl}`;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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;
|
|
570
917
|
}
|
|
571
918
|
async *sendMessage(request, options = {}) {
|
|
572
919
|
var _a;
|
|
@@ -617,22 +964,29 @@ class ChatClient {
|
|
|
617
964
|
body: JSON.stringify(body),
|
|
618
965
|
signal: controller.signal
|
|
619
966
|
});
|
|
620
|
-
|
|
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
|
+
}
|
|
621
976
|
const resumable = saved.conversationId === request.conversationId;
|
|
622
977
|
let response = await post(this.apiUrl, { protocol: 1, ...request }, resumable ? saved.token : void 0);
|
|
623
978
|
if (response.status === 401) {
|
|
624
979
|
const minted = await post(this.apiUrl.replace(/\/?(\?.*)?$/, "/session$1"), {
|
|
625
980
|
conversationId: request.conversationId,
|
|
626
981
|
previousToken: saved.token,
|
|
627
|
-
|
|
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
|
|
628
985
|
});
|
|
629
986
|
if (minted.ok) {
|
|
630
987
|
const session = await minted.json();
|
|
631
988
|
this.saved = session;
|
|
632
|
-
|
|
633
|
-
localStorage.setItem(this.sessionKey, JSON.stringify(session));
|
|
634
|
-
} catch {
|
|
635
|
-
}
|
|
989
|
+
this.store.set(this.sessionKey, JSON.stringify(session));
|
|
636
990
|
if (session.conversationId !== request.conversationId) {
|
|
637
991
|
request = { ...request, conversationId: session.conversationId };
|
|
638
992
|
yield { conversationId: session.conversationId };
|
|
@@ -722,6 +1076,11 @@ function fromPart(part) {
|
|
|
722
1076
|
return { content: part.delta };
|
|
723
1077
|
case "lead-captured":
|
|
724
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
|
+
}
|
|
725
1084
|
case "error":
|
|
726
1085
|
return errorChunk(part.code === "budget_exceeded" && part.scope !== "conversation" ? "budget_daily" : part.code, part.retryAfterMs);
|
|
727
1086
|
case "finish":
|
|
@@ -730,107 +1089,6 @@ function fromPart(part) {
|
|
|
730
1089
|
return null;
|
|
731
1090
|
}
|
|
732
1091
|
}
|
|
733
|
-
const KEY_PREFIX = "chatcops_conversation";
|
|
734
|
-
const SESSION_KEY_PREFIX = "chatcops_session_id";
|
|
735
|
-
class ConversationStorage {
|
|
736
|
-
/** Without a siteId the keys are the ones the widget has always used. */
|
|
737
|
-
constructor(siteId) {
|
|
738
|
-
this.key = siteId ? `${KEY_PREFIX}_${siteId}` : KEY_PREFIX;
|
|
739
|
-
this.sessionKey = siteId ? `${SESSION_KEY_PREFIX}_${siteId}` : SESSION_KEY_PREFIX;
|
|
740
|
-
}
|
|
741
|
-
load() {
|
|
742
|
-
try {
|
|
743
|
-
const raw = localStorage.getItem(this.key);
|
|
744
|
-
if (raw === null) return null;
|
|
745
|
-
let parsed;
|
|
746
|
-
try {
|
|
747
|
-
parsed = JSON.parse(raw);
|
|
748
|
-
} catch {
|
|
749
|
-
this.clear();
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.id !== "string" || !Array.isArray(parsed.messages)) {
|
|
753
|
-
this.clear();
|
|
754
|
-
return null;
|
|
755
|
-
}
|
|
756
|
-
const record = parsed;
|
|
757
|
-
const rawMessages = record.messages;
|
|
758
|
-
const messages = rawMessages.filter(
|
|
759
|
-
(msg) => typeof msg === "object" && msg !== null && !Array.isArray(msg) && (msg.role === "user" || msg.role === "assistant") && typeof msg.content === "string"
|
|
760
|
-
);
|
|
761
|
-
return {
|
|
762
|
-
...record,
|
|
763
|
-
id: record.id,
|
|
764
|
-
messages
|
|
765
|
-
};
|
|
766
|
-
} catch {
|
|
767
|
-
return null;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
save(conversation) {
|
|
771
|
-
try {
|
|
772
|
-
const filtered = {
|
|
773
|
-
...conversation,
|
|
774
|
-
messages: conversation.messages.filter((m) => m.status !== "error").map(({ status, ...message }) => message)
|
|
775
|
-
};
|
|
776
|
-
localStorage.setItem(this.key, JSON.stringify(filtered));
|
|
777
|
-
} catch {
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
clear() {
|
|
781
|
-
try {
|
|
782
|
-
localStorage.removeItem(this.key);
|
|
783
|
-
} catch {
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
getSessionId() {
|
|
787
|
-
try {
|
|
788
|
-
let id = sessionStorage.getItem(this.sessionKey);
|
|
789
|
-
if (!id) {
|
|
790
|
-
id = crypto.randomUUID();
|
|
791
|
-
sessionStorage.setItem(this.sessionKey, id);
|
|
792
|
-
}
|
|
793
|
-
return id;
|
|
794
|
-
} catch {
|
|
795
|
-
return crypto.randomUUID();
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
isPreChatCompleted(sessionId) {
|
|
799
|
-
try {
|
|
800
|
-
return sessionStorage.getItem(`chatcops-prechat-${sessionId}`) === "true";
|
|
801
|
-
} catch {
|
|
802
|
-
return false;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
setPreChatCompleted(sessionId, data) {
|
|
806
|
-
try {
|
|
807
|
-
sessionStorage.setItem(`chatcops-prechat-${sessionId}`, "true");
|
|
808
|
-
} catch {
|
|
809
|
-
}
|
|
810
|
-
if (data) {
|
|
811
|
-
this.setPreChatData(sessionId, data);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
getPreChatData(sessionId) {
|
|
815
|
-
try {
|
|
816
|
-
const raw = sessionStorage.getItem(`chatcops-prechat-data-${sessionId}`);
|
|
817
|
-
if (!raw) return null;
|
|
818
|
-
const parsed = JSON.parse(raw);
|
|
819
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
return parsed;
|
|
823
|
-
} catch {
|
|
824
|
-
return null;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
setPreChatData(sessionId, data) {
|
|
828
|
-
try {
|
|
829
|
-
sessionStorage.setItem(`chatcops-prechat-data-${sessionId}`, JSON.stringify(data));
|
|
830
|
-
} catch {
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
1092
|
const DEFAULT = "";
|
|
835
1093
|
const CLAIMS_KEY = Symbol.for("chatcops.storageClaims");
|
|
836
1094
|
const store = globalThis;
|
|
@@ -867,19 +1125,14 @@ function claimNamespace(siteId, mode, isLive) {
|
|
|
867
1125
|
}
|
|
868
1126
|
};
|
|
869
1127
|
}
|
|
1128
|
+
const MODES = ["light", "dark", "auto"];
|
|
870
1129
|
function applyTheme(root, theme) {
|
|
871
1130
|
const host = root.host;
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
host.style.setProperty("--cc-on-accent", pair.onAccent);
|
|
878
|
-
host.style.setProperty("--cc-accent-contrast", pair.background);
|
|
879
|
-
} else {
|
|
880
|
-
host.style.setProperty("--cc-accent-contrast", theme.accent);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
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);
|
|
883
1136
|
if (theme.textColor) {
|
|
884
1137
|
host.style.setProperty("--cc-text", theme.textColor);
|
|
885
1138
|
}
|
|
@@ -896,6 +1149,23 @@ function applyTheme(root, theme) {
|
|
|
896
1149
|
if (radius) host.style.setProperty("--cc-radius", radius);
|
|
897
1150
|
}
|
|
898
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
|
+
}
|
|
899
1169
|
function toCssLength(value) {
|
|
900
1170
|
if (typeof value === "number" && !Number.isFinite(value)) return "";
|
|
901
1171
|
const text = String(value).trim();
|
|
@@ -977,188 +1247,20 @@ const en = {
|
|
|
977
1247
|
preChatRequired: "This field is required",
|
|
978
1248
|
preChatInvalidEmail: "Please enter a valid email",
|
|
979
1249
|
retryButton: "Retry",
|
|
980
|
-
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"
|
|
981
1256
|
};
|
|
982
1257
|
const locales = { en };
|
|
983
1258
|
function getWidgetLocale(code, overrides) {
|
|
984
1259
|
const base = locales[code] ?? en;
|
|
985
1260
|
return overrides ? { ...base, ...overrides } : base;
|
|
986
1261
|
}
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
constructor(parent, options) {
|
|
990
|
-
this.fieldElements = /* @__PURE__ */ new Map();
|
|
991
|
-
this.errorElements = /* @__PURE__ */ new Map();
|
|
992
|
-
this.options = options;
|
|
993
|
-
this.container = document.createElement("div");
|
|
994
|
-
this.container.className = "cc-prechat";
|
|
995
|
-
if (options.title) {
|
|
996
|
-
const title = document.createElement("div");
|
|
997
|
-
title.className = "cc-prechat-title";
|
|
998
|
-
title.textContent = options.title;
|
|
999
|
-
this.container.appendChild(title);
|
|
1000
|
-
}
|
|
1001
|
-
if (options.subtitle) {
|
|
1002
|
-
const subtitle = document.createElement("div");
|
|
1003
|
-
subtitle.className = "cc-prechat-subtitle";
|
|
1004
|
-
subtitle.textContent = options.subtitle;
|
|
1005
|
-
this.container.appendChild(subtitle);
|
|
1006
|
-
}
|
|
1007
|
-
const form = document.createElement("form");
|
|
1008
|
-
form.className = "cc-prechat-fields";
|
|
1009
|
-
form.noValidate = true;
|
|
1010
|
-
form.addEventListener("submit", (e) => {
|
|
1011
|
-
e.preventDefault();
|
|
1012
|
-
this.handleSubmit();
|
|
1013
|
-
});
|
|
1014
|
-
for (const field of options.fields) {
|
|
1015
|
-
form.appendChild(this.createField(field));
|
|
1016
|
-
}
|
|
1017
|
-
this.submitBtn = document.createElement("button");
|
|
1018
|
-
this.submitBtn.type = "submit";
|
|
1019
|
-
this.submitBtn.className = "cc-prechat-submit";
|
|
1020
|
-
this.submitBtn.textContent = options.submitLabel;
|
|
1021
|
-
form.appendChild(this.submitBtn);
|
|
1022
|
-
this.container.appendChild(form);
|
|
1023
|
-
parent.appendChild(this.container);
|
|
1024
|
-
this.updateSubmitState();
|
|
1025
|
-
const firstInput = this.fieldElements.values().next().value;
|
|
1026
|
-
if (firstInput) {
|
|
1027
|
-
setTimeout(() => firstInput.focus(), 100);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
createField(field) {
|
|
1031
|
-
const wrapper = document.createElement("div");
|
|
1032
|
-
wrapper.className = "cc-prechat-field";
|
|
1033
|
-
const inputId = `cc-prechat-${field.name}`;
|
|
1034
|
-
const errorId = `cc-prechat-${field.name}-error`;
|
|
1035
|
-
const label = document.createElement("label");
|
|
1036
|
-
label.className = "cc-prechat-label";
|
|
1037
|
-
label.htmlFor = inputId;
|
|
1038
|
-
label.textContent = field.label;
|
|
1039
|
-
if (field.required) {
|
|
1040
|
-
const req = document.createElement("span");
|
|
1041
|
-
req.className = "cc-required";
|
|
1042
|
-
req.textContent = "*";
|
|
1043
|
-
label.appendChild(req);
|
|
1044
|
-
}
|
|
1045
|
-
wrapper.appendChild(label);
|
|
1046
|
-
let input;
|
|
1047
|
-
if (field.type === "select") {
|
|
1048
|
-
const select = document.createElement("select");
|
|
1049
|
-
select.className = "cc-prechat-select";
|
|
1050
|
-
const defaultOpt = document.createElement("option");
|
|
1051
|
-
defaultOpt.value = "";
|
|
1052
|
-
defaultOpt.textContent = field.placeholder ?? this.options.locale.selectPlaceholder.replace("{label}", field.label.toLowerCase());
|
|
1053
|
-
defaultOpt.disabled = true;
|
|
1054
|
-
defaultOpt.selected = true;
|
|
1055
|
-
select.appendChild(defaultOpt);
|
|
1056
|
-
for (const opt of field.options ?? []) {
|
|
1057
|
-
const option = document.createElement("option");
|
|
1058
|
-
option.value = opt;
|
|
1059
|
-
option.textContent = opt;
|
|
1060
|
-
select.appendChild(option);
|
|
1061
|
-
}
|
|
1062
|
-
input = select;
|
|
1063
|
-
} else if (field.type === "textarea") {
|
|
1064
|
-
const textarea = document.createElement("textarea");
|
|
1065
|
-
textarea.className = "cc-prechat-textarea";
|
|
1066
|
-
textarea.placeholder = field.placeholder ?? "";
|
|
1067
|
-
textarea.rows = 3;
|
|
1068
|
-
textarea.maxLength = MAX_PRECHAT_FIELD_LENGTH;
|
|
1069
|
-
input = textarea;
|
|
1070
|
-
} else {
|
|
1071
|
-
const textInput = document.createElement("input");
|
|
1072
|
-
textInput.type = field.type;
|
|
1073
|
-
textInput.className = "cc-prechat-input";
|
|
1074
|
-
textInput.maxLength = MAX_PRECHAT_FIELD_LENGTH;
|
|
1075
|
-
textInput.placeholder = field.placeholder ?? "";
|
|
1076
|
-
input = textInput;
|
|
1077
|
-
}
|
|
1078
|
-
input.name = field.name;
|
|
1079
|
-
input.id = inputId;
|
|
1080
|
-
if (field.required) {
|
|
1081
|
-
input.setAttribute("aria-required", "true");
|
|
1082
|
-
}
|
|
1083
|
-
input.addEventListener("input", () => {
|
|
1084
|
-
this.clearError(field.name);
|
|
1085
|
-
this.updateSubmitState();
|
|
1086
|
-
});
|
|
1087
|
-
if (input instanceof HTMLSelectElement) {
|
|
1088
|
-
input.addEventListener("change", () => {
|
|
1089
|
-
this.clearError(field.name);
|
|
1090
|
-
this.updateSubmitState();
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
wrapper.appendChild(input);
|
|
1094
|
-
this.fieldElements.set(field.name, input);
|
|
1095
|
-
const error = document.createElement("div");
|
|
1096
|
-
error.className = "cc-prechat-error";
|
|
1097
|
-
error.id = errorId;
|
|
1098
|
-
wrapper.appendChild(error);
|
|
1099
|
-
this.errorElements.set(field.name, error);
|
|
1100
|
-
return wrapper;
|
|
1101
|
-
}
|
|
1102
|
-
updateSubmitState() {
|
|
1103
|
-
const allRequiredFilled = this.options.fields.every((field) => {
|
|
1104
|
-
if (!field.required) return true;
|
|
1105
|
-
const el = this.fieldElements.get(field.name);
|
|
1106
|
-
return el ? el.value.trim().length > 0 : false;
|
|
1107
|
-
});
|
|
1108
|
-
this.submitBtn.disabled = !allRequiredFilled;
|
|
1109
|
-
}
|
|
1110
|
-
handleSubmit() {
|
|
1111
|
-
const data = {};
|
|
1112
|
-
let valid = true;
|
|
1113
|
-
for (const field of this.options.fields) {
|
|
1114
|
-
const el = this.fieldElements.get(field.name);
|
|
1115
|
-
if (!el) continue;
|
|
1116
|
-
const value = el.value.trim();
|
|
1117
|
-
data[field.name] = value;
|
|
1118
|
-
if (field.required && !value) {
|
|
1119
|
-
this.showError(field.name, this.options.locale.preChatRequired);
|
|
1120
|
-
valid = false;
|
|
1121
|
-
continue;
|
|
1122
|
-
}
|
|
1123
|
-
if (field.type === "email" && value && !this.isValidEmail(value)) {
|
|
1124
|
-
this.showError(field.name, this.options.locale.preChatInvalidEmail);
|
|
1125
|
-
valid = false;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
if (valid) {
|
|
1129
|
-
this.options.onSubmit(data);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
showError(fieldName, message) {
|
|
1133
|
-
const el = this.errorElements.get(fieldName);
|
|
1134
|
-
if (el) el.textContent = message;
|
|
1135
|
-
const fieldEl = this.fieldElements.get(fieldName);
|
|
1136
|
-
if (fieldEl && el) {
|
|
1137
|
-
fieldEl.setAttribute("aria-invalid", "true");
|
|
1138
|
-
fieldEl.setAttribute("aria-describedby", el.id);
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
clearError(fieldName) {
|
|
1142
|
-
const el = this.errorElements.get(fieldName);
|
|
1143
|
-
if (el) el.textContent = "";
|
|
1144
|
-
const fieldEl = this.fieldElements.get(fieldName);
|
|
1145
|
-
if (fieldEl && el) {
|
|
1146
|
-
if (fieldEl.getAttribute("aria-describedby") === el.id) {
|
|
1147
|
-
fieldEl.removeAttribute("aria-describedby");
|
|
1148
|
-
}
|
|
1149
|
-
if (fieldEl.getAttribute("aria-invalid") === "true") {
|
|
1150
|
-
fieldEl.removeAttribute("aria-invalid");
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
isValidEmail(email) {
|
|
1155
|
-
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
1156
|
-
}
|
|
1157
|
-
destroy() {
|
|
1158
|
-
this.container.remove();
|
|
1159
|
-
this.fieldElements.clear();
|
|
1160
|
-
this.errorElements.clear();
|
|
1161
|
-
}
|
|
1262
|
+
function registerWidgetLocale(code, strings) {
|
|
1263
|
+
locales[code] = { ...en, ...strings };
|
|
1162
1264
|
}
|
|
1163
1265
|
const ERROR_MESSAGES = {
|
|
1164
1266
|
network: "errorNetwork",
|
|
@@ -1169,25 +1271,43 @@ const ERROR_MESSAGES = {
|
|
|
1169
1271
|
unavailable: "errorUnavailable",
|
|
1170
1272
|
cut_off: "errorCutOff"
|
|
1171
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
|
+
}
|
|
1172
1286
|
class Widget {
|
|
1173
1287
|
constructor(config) {
|
|
1174
1288
|
this.mounted = false;
|
|
1289
|
+
this.conversationId = crypto.randomUUID();
|
|
1290
|
+
this.preChatDone = false;
|
|
1291
|
+
this.hydrated = false;
|
|
1175
1292
|
this.messages = [];
|
|
1176
1293
|
this.isStreaming = false;
|
|
1177
1294
|
this.v1 = false;
|
|
1178
1295
|
this.destroyed = false;
|
|
1296
|
+
this.generation = 0;
|
|
1297
|
+
this.userFromSite = false;
|
|
1179
1298
|
this.retryRequests = /* @__PURE__ */ new Map();
|
|
1180
1299
|
this.eventHandlers = /* @__PURE__ */ new Map();
|
|
1300
|
+
this.unreadReplies = 0;
|
|
1181
1301
|
this.config = config;
|
|
1182
|
-
this.client = new ChatClient({ apiUrl: config.apiUrl, timeout: config.timeout, turnstileSiteKey: config.turnstileSiteKey });
|
|
1183
1302
|
this.namespace = claimNamespace(
|
|
1184
1303
|
config.siteId,
|
|
1185
1304
|
config.mode ?? "popup",
|
|
1186
1305
|
() => !this.destroyed && (!this.mounted || this.shadow.host.isConnected)
|
|
1187
1306
|
);
|
|
1188
|
-
|
|
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 });
|
|
1189
1310
|
this.locale = getWidgetLocale(config.locale ?? "en", config.strings);
|
|
1190
|
-
this.conversationId = this.storage.getSessionId();
|
|
1191
1311
|
}
|
|
1192
1312
|
get isInline() {
|
|
1193
1313
|
return this.config.mode === "inline";
|
|
@@ -1206,12 +1326,13 @@ class Widget {
|
|
|
1206
1326
|
return this.config.container;
|
|
1207
1327
|
}
|
|
1208
1328
|
init() {
|
|
1209
|
-
var _a, _b
|
|
1329
|
+
var _a, _b;
|
|
1330
|
+
prefetch(this.config);
|
|
1210
1331
|
if (this.isInline) {
|
|
1211
1332
|
this.containerEl = this.resolveContainer();
|
|
1212
|
-
this.shadow = createShadowRoot(this.containerEl);
|
|
1333
|
+
this.shadow = createShadowRoot(this.containerEl, { dir: this.config.dir, locale: this.config.locale });
|
|
1213
1334
|
} else {
|
|
1214
|
-
this.shadow = createShadowRoot();
|
|
1335
|
+
this.shadow = createShadowRoot(void 0, { dir: this.config.dir, locale: this.config.locale });
|
|
1215
1336
|
}
|
|
1216
1337
|
this.mounted = true;
|
|
1217
1338
|
if (this.config.theme) {
|
|
@@ -1223,6 +1344,7 @@ class Widget {
|
|
|
1223
1344
|
position,
|
|
1224
1345
|
openLabel: this.locale.openChat,
|
|
1225
1346
|
closeLabel: this.locale.closeButton,
|
|
1347
|
+
unreadLabel: this.locale.unreadCount,
|
|
1226
1348
|
onClick: () => this.toggle()
|
|
1227
1349
|
});
|
|
1228
1350
|
}
|
|
@@ -1230,15 +1352,7 @@ class Widget {
|
|
|
1230
1352
|
position,
|
|
1231
1353
|
inline: this.isInline,
|
|
1232
1354
|
preChatEnabled: ((_b = this.config.preChatForm) == null ? void 0 : _b.enabled) ?? false,
|
|
1233
|
-
|
|
1234
|
-
name: ((_c = this.config.branding) == null ? void 0 : _c.name) ?? this.locale.defaultName,
|
|
1235
|
-
avatar: (_d = this.config.branding) == null ? void 0 : _d.avatar,
|
|
1236
|
-
subtitle: ((_e = this.config.branding) == null ? void 0 : _e.subtitle) ?? this.locale.defaultSubtitle
|
|
1237
|
-
},
|
|
1238
|
-
placeholder: this.config.placeholder ?? this.locale.inputPlaceholder,
|
|
1239
|
-
footerText: this.locale.poweredBy,
|
|
1240
|
-
closeLabel: this.locale.closeButton,
|
|
1241
|
-
sendLabel: this.locale.sendButton,
|
|
1355
|
+
...this.panelLabels(),
|
|
1242
1356
|
messageActions: {
|
|
1243
1357
|
onRetry: (messageId) => {
|
|
1244
1358
|
void this.handleRetry(messageId);
|
|
@@ -1246,41 +1360,24 @@ class Widget {
|
|
|
1246
1360
|
onRegenerate: (messageId) => {
|
|
1247
1361
|
void this.handleRegenerate(messageId);
|
|
1248
1362
|
},
|
|
1249
|
-
|
|
1250
|
-
regenerateLabel: this.locale.regenerateButton,
|
|
1251
|
-
typingLabel: this.locale.typingIndicator
|
|
1363
|
+
...this.actionLabels()
|
|
1252
1364
|
},
|
|
1253
1365
|
onSend: (text) => this.handleSend(text),
|
|
1254
1366
|
onClose: () => this.close()
|
|
1255
1367
|
});
|
|
1256
|
-
if (this.config.
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
for (const msg of saved.messages) {
|
|
1261
|
-
this.messages.push(msg);
|
|
1262
|
-
this.panel.addMessage(msg);
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
if ((_f = this.config.preChatForm) == null ? void 0 : _f.enabled) {
|
|
1267
|
-
const savedUserData = this.storage.getPreChatData(this.conversationId);
|
|
1268
|
-
if (savedUserData) {
|
|
1269
|
-
this.userData = savedUserData;
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
const shouldShowPreChat = ((_g = this.config.preChatForm) == null ? void 0 : _g.enabled) && !this.isPreChatCompleted() && this.messages.length === 0;
|
|
1273
|
-
if (shouldShowPreChat) {
|
|
1274
|
-
this.showPreChatForm();
|
|
1275
|
-
} else {
|
|
1276
|
-
this.showChatView();
|
|
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
|
+
);
|
|
1277
1372
|
}
|
|
1278
|
-
this.
|
|
1373
|
+
this.renderDisclosure();
|
|
1374
|
+
if (this.isInline) this.hydrate();
|
|
1279
1375
|
if (!this.isInline && this.config.welcomeBubble) {
|
|
1280
1376
|
this.bubble = new WelcomeBubble(this.shadow, {
|
|
1281
1377
|
text: this.config.welcomeBubble.text,
|
|
1282
1378
|
delay: this.config.welcomeBubble.delay,
|
|
1283
1379
|
showOnce: this.config.welcomeBubble.showOnce ?? true,
|
|
1380
|
+
store: this.storage.store,
|
|
1284
1381
|
position,
|
|
1285
1382
|
dismissLabel: this.locale.dismissBubble,
|
|
1286
1383
|
onClick: () => this.open()
|
|
@@ -1294,15 +1391,39 @@ class Widget {
|
|
|
1294
1391
|
this.autoOpenTimer = setTimeout(() => this.open(), delay);
|
|
1295
1392
|
}
|
|
1296
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
|
+
}
|
|
1297
1416
|
open() {
|
|
1298
|
-
var _a, _b, _c, _d
|
|
1417
|
+
var _a, _b, _c, _d;
|
|
1418
|
+
this.unreadReplies = 0;
|
|
1419
|
+
this.hydrate();
|
|
1299
1420
|
this.panel.show();
|
|
1300
1421
|
if (!this.isInline) {
|
|
1301
1422
|
(_a = this.fab) == null ? void 0 : _a.setOpen(true);
|
|
1302
|
-
(_b = this.
|
|
1303
|
-
(_c = this.bubble) == null ? void 0 : _c.hide();
|
|
1423
|
+
(_b = this.bubble) == null ? void 0 : _b.hide();
|
|
1304
1424
|
}
|
|
1305
|
-
|
|
1425
|
+
this.lockScroll();
|
|
1426
|
+
(_d = (_c = this.config).onOpen) == null ? void 0 : _d.call(_c);
|
|
1306
1427
|
this.emit("open");
|
|
1307
1428
|
}
|
|
1308
1429
|
close() {
|
|
@@ -1313,6 +1434,7 @@ class Widget {
|
|
|
1313
1434
|
(_a = this.fab) == null ? void 0 : _a.setOpen(false);
|
|
1314
1435
|
if (hadFocus) (_b = this.fab) == null ? void 0 : _b.focus();
|
|
1315
1436
|
}
|
|
1437
|
+
this.restoreScroll();
|
|
1316
1438
|
(_d = (_c = this.config).onClose) == null ? void 0 : _d.call(_c);
|
|
1317
1439
|
this.emit("close");
|
|
1318
1440
|
}
|
|
@@ -1323,12 +1445,157 @@ class Widget {
|
|
|
1323
1445
|
this.open();
|
|
1324
1446
|
}
|
|
1325
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
|
+
}
|
|
1326
1592
|
destroy() {
|
|
1327
1593
|
var _a, _b, _c, _d;
|
|
1328
1594
|
this.destroyed = true;
|
|
1329
1595
|
if (this.autoOpenTimer !== void 0) clearTimeout(this.autoOpenTimer);
|
|
1330
1596
|
(_a = this.abortController) == null ? void 0 : _a.abort();
|
|
1331
1597
|
this.abortController = void 0;
|
|
1598
|
+
this.restoreScroll();
|
|
1332
1599
|
(_b = this.preChatFormEl) == null ? void 0 : _b.destroy();
|
|
1333
1600
|
(_c = this.bubble) == null ? void 0 : _c.destroy();
|
|
1334
1601
|
this.panel.destroy();
|
|
@@ -1339,6 +1606,28 @@ class Widget {
|
|
|
1339
1606
|
this.messages = [];
|
|
1340
1607
|
this.userData = void 0;
|
|
1341
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
|
+
}
|
|
1342
1631
|
on(event, handler) {
|
|
1343
1632
|
if (!this.eventHandlers.has(event)) {
|
|
1344
1633
|
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
|
@@ -1357,30 +1646,39 @@ class Widget {
|
|
|
1357
1646
|
h(...args);
|
|
1358
1647
|
});
|
|
1359
1648
|
}
|
|
1360
|
-
isPreChatCompleted() {
|
|
1361
|
-
return this.storage.isPreChatCompleted(this.conversationId);
|
|
1362
|
-
}
|
|
1363
|
-
setPreChatCompleted(data) {
|
|
1364
|
-
this.storage.setPreChatCompleted(this.conversationId, data);
|
|
1365
|
-
}
|
|
1366
1649
|
showPreChatForm() {
|
|
1367
1650
|
const formConfig = this.config.preChatForm;
|
|
1368
1651
|
this.panel.hideMessages();
|
|
1369
1652
|
this.panel.input.setVisible(false);
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
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
|
+
);
|
|
1378
1674
|
}
|
|
1379
1675
|
handlePreChatSubmit(data) {
|
|
1380
1676
|
var _a, _b, _c, _d;
|
|
1381
1677
|
if (!((_a = this.config.preChatForm) == null ? void 0 : _a.enabled)) return;
|
|
1678
|
+
this.hydrate();
|
|
1382
1679
|
this.userData = data;
|
|
1383
|
-
this.
|
|
1680
|
+
this.preChatDone = true;
|
|
1681
|
+
this.saveHistory();
|
|
1384
1682
|
(_b = this.preChatFormEl) == null ? void 0 : _b.destroy();
|
|
1385
1683
|
this.preChatFormEl = void 0;
|
|
1386
1684
|
this.panel.showMessages();
|
|
@@ -1405,11 +1703,23 @@ class Widget {
|
|
|
1405
1703
|
};
|
|
1406
1704
|
this.messages.push(welcomeMsg);
|
|
1407
1705
|
this.panel.addMessage(welcomeMsg);
|
|
1706
|
+
this.renderSuggestions();
|
|
1408
1707
|
}
|
|
1409
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
|
+
}
|
|
1410
1719
|
async handleSend(text) {
|
|
1411
1720
|
var _a, _b;
|
|
1412
1721
|
if (this.isStreaming) return;
|
|
1722
|
+
this.hydrate();
|
|
1413
1723
|
const userMsg = {
|
|
1414
1724
|
id: crypto.randomUUID(),
|
|
1415
1725
|
role: "user",
|
|
@@ -1465,15 +1775,18 @@ class Widget {
|
|
|
1465
1775
|
async sendToServer(text, options = {}) {
|
|
1466
1776
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
1467
1777
|
if (this.isStreaming) return;
|
|
1778
|
+
const generation = this.generation;
|
|
1468
1779
|
this.isStreaming = true;
|
|
1469
1780
|
this.panel.input.setDisabled(true);
|
|
1470
1781
|
this.panel.messages.clearRegenerateButtons();
|
|
1782
|
+
this.panel.messages.beginReply();
|
|
1471
1783
|
this.panel.messages.showTyping();
|
|
1472
1784
|
const assistantMsg = {
|
|
1473
1785
|
id: crypto.randomUUID(),
|
|
1474
1786
|
role: "assistant",
|
|
1475
1787
|
content: "",
|
|
1476
|
-
status: "streaming"
|
|
1788
|
+
status: "streaming",
|
|
1789
|
+
sources: []
|
|
1477
1790
|
};
|
|
1478
1791
|
let firstChunk = true;
|
|
1479
1792
|
let requestCompleted = false;
|
|
@@ -1485,7 +1798,7 @@ class Widget {
|
|
|
1485
1798
|
this.createRequest(text, options),
|
|
1486
1799
|
{ signal: abortController.signal }
|
|
1487
1800
|
)) {
|
|
1488
|
-
if (this.
|
|
1801
|
+
if (this.isStale(generation)) break;
|
|
1489
1802
|
if (chunk.error) {
|
|
1490
1803
|
const errorType = this.resolveErrorType(chunk.error);
|
|
1491
1804
|
this.markFailed(assistantMsg, this.getErrorMessage(errorType, chunk.retryAfterMs));
|
|
@@ -1520,6 +1833,10 @@ class Widget {
|
|
|
1520
1833
|
(_d = (_c = this.config).onLeadCaptured) == null ? void 0 : _d.call(_c, chunk.leadData);
|
|
1521
1834
|
this.emit("leadCaptured", chunk.leadData);
|
|
1522
1835
|
}
|
|
1836
|
+
if (chunk.source) {
|
|
1837
|
+
assistantMsg.sources ?? (assistantMsg.sources = []);
|
|
1838
|
+
assistantMsg.sources.push(chunk.source);
|
|
1839
|
+
}
|
|
1523
1840
|
if (chunk.content) {
|
|
1524
1841
|
if (firstChunk) {
|
|
1525
1842
|
this.panel.messages.removeTyping();
|
|
@@ -1530,7 +1847,7 @@ class Widget {
|
|
|
1530
1847
|
this.panel.messages.updateMessage(assistantMsg);
|
|
1531
1848
|
}
|
|
1532
1849
|
}
|
|
1533
|
-
if (this.
|
|
1850
|
+
if (this.isStale(generation)) return;
|
|
1534
1851
|
if (!hasError && !requestCompleted) {
|
|
1535
1852
|
requestCompleted = true;
|
|
1536
1853
|
}
|
|
@@ -1545,11 +1862,12 @@ class Widget {
|
|
|
1545
1862
|
this.emit("message", assistantMsg);
|
|
1546
1863
|
this.showLatestRegenerateButton();
|
|
1547
1864
|
if (!this.isInline && !this.panel.isVisible) {
|
|
1548
|
-
|
|
1865
|
+
this.unreadReplies++;
|
|
1866
|
+
(_g = this.fab) == null ? void 0 : _g.showBadge(this.unreadReplies);
|
|
1549
1867
|
}
|
|
1550
1868
|
}
|
|
1551
1869
|
} catch (err) {
|
|
1552
|
-
if (this.
|
|
1870
|
+
if (this.isStale(generation)) return;
|
|
1553
1871
|
this.markFailed(assistantMsg, this.locale.errorGeneric);
|
|
1554
1872
|
assistantMsg.errorType = "network";
|
|
1555
1873
|
this.retryRequests.set(assistantMsg.id, {
|
|
@@ -1570,7 +1888,8 @@ class Widget {
|
|
|
1570
1888
|
if (this.abortController === abortController) {
|
|
1571
1889
|
this.abortController = void 0;
|
|
1572
1890
|
}
|
|
1573
|
-
if (!this.
|
|
1891
|
+
if (!this.isStale(generation)) {
|
|
1892
|
+
this.panel.messages.finishReply(assistantMsg);
|
|
1574
1893
|
this.isStreaming = false;
|
|
1575
1894
|
this.panel.input.setDisabled(false);
|
|
1576
1895
|
this.panel.input.focus();
|
|
@@ -1580,8 +1899,11 @@ class Widget {
|
|
|
1580
1899
|
}
|
|
1581
1900
|
createRequest(text, options) {
|
|
1582
1901
|
var _a;
|
|
1583
|
-
const pageContext = this.config.pageContext !== false ? { url: window.location.href, title: document.title } : void 0;
|
|
1584
|
-
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;
|
|
1585
1907
|
return {
|
|
1586
1908
|
conversationId: this.conversationId,
|
|
1587
1909
|
message: text,
|
|
@@ -1643,7 +1965,6 @@ class Widget {
|
|
|
1643
1965
|
return this.locale[ERROR_MESSAGES[errorType] ?? "errorGeneric"];
|
|
1644
1966
|
}
|
|
1645
1967
|
saveHistory() {
|
|
1646
|
-
if (this.config.persistHistory === false) return;
|
|
1647
1968
|
const maxMessages = this.config.maxMessages ?? 50;
|
|
1648
1969
|
const toSave = this.messages.filter((m) => m.id !== "welcome").slice(-maxMessages);
|
|
1649
1970
|
this.storage.save({
|
|
@@ -1654,14 +1975,15 @@ class Widget {
|
|
|
1654
1975
|
content: m.content,
|
|
1655
1976
|
status: m.status,
|
|
1656
1977
|
errorType: m.errorType,
|
|
1657
|
-
timestamp: Date.now()
|
|
1978
|
+
timestamp: Date.now(),
|
|
1979
|
+
...m.sources && m.sources.length > 0 ? { sources: m.sources } : {}
|
|
1658
1980
|
})),
|
|
1981
|
+
...this.userData ? { userData: this.userData } : {},
|
|
1982
|
+
...this.preChatDone ? { preChatDone: true } : {},
|
|
1659
1983
|
updatedAt: Date.now()
|
|
1660
1984
|
});
|
|
1661
1985
|
}
|
|
1662
1986
|
}
|
|
1663
|
-
let instance = null;
|
|
1664
|
-
const persistentHandlers = /* @__PURE__ */ new Map();
|
|
1665
1987
|
function parseAutoOpen(value) {
|
|
1666
1988
|
if (value === void 0) return void 0;
|
|
1667
1989
|
if (value === "" || value === "true") return true;
|
|
@@ -1669,26 +1991,109 @@ function parseAutoOpen(value) {
|
|
|
1669
1991
|
if (/^\d+$/.test(value)) return Number(value);
|
|
1670
1992
|
return void 0;
|
|
1671
1993
|
}
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
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();
|
|
2037
|
+
const ChatCops = {
|
|
2038
|
+
init(config) {
|
|
2039
|
+
if (instance) {
|
|
2040
|
+
instance.destroy();
|
|
2041
|
+
}
|
|
2042
|
+
instance = new Widget(config);
|
|
2043
|
+
for (const [event, handlers] of persistentHandlers) {
|
|
2044
|
+
for (const handler of handlers) {
|
|
2045
|
+
instance.on(event, handler);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
instance.init();
|
|
2049
|
+
return instance;
|
|
2050
|
+
},
|
|
1686
2051
|
open() {
|
|
1687
2052
|
instance == null ? void 0 : instance.open();
|
|
1688
2053
|
},
|
|
1689
2054
|
close() {
|
|
1690
2055
|
instance == null ? void 0 : instance.close();
|
|
1691
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,
|
|
1692
2097
|
destroy() {
|
|
1693
2098
|
instance == null ? void 0 : instance.destroy();
|
|
1694
2099
|
instance = null;
|
|
@@ -1712,38 +2117,8 @@ const ChatCops = {
|
|
|
1712
2117
|
}
|
|
1713
2118
|
};
|
|
1714
2119
|
function autoInit(script) {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
if (!apiUrl) return;
|
|
1718
|
-
const mode = script.dataset.mode;
|
|
1719
|
-
const container = script.dataset.container;
|
|
1720
|
-
ChatCops.init({
|
|
1721
|
-
apiUrl,
|
|
1722
|
-
turnstileSiteKey: script.dataset.turnstileSiteKey,
|
|
1723
|
-
mode: mode || void 0,
|
|
1724
|
-
container: container || void 0,
|
|
1725
|
-
theme: {
|
|
1726
|
-
accent: script.dataset.accent,
|
|
1727
|
-
textColor: script.dataset.textColor,
|
|
1728
|
-
bgColor: script.dataset.bgColor,
|
|
1729
|
-
fontFamily: script.dataset.fontFamily,
|
|
1730
|
-
borderRadius: script.dataset.borderRadius,
|
|
1731
|
-
position: script.dataset.position || void 0
|
|
1732
|
-
},
|
|
1733
|
-
branding: {
|
|
1734
|
-
name: script.dataset.brandName,
|
|
1735
|
-
avatar: script.dataset.brandAvatar,
|
|
1736
|
-
subtitle: script.dataset.brandSubtitle
|
|
1737
|
-
},
|
|
1738
|
-
welcomeMessage: script.dataset.welcomeMessage,
|
|
1739
|
-
welcomeBubble: script.dataset.welcomeBubble ? {
|
|
1740
|
-
text: script.dataset.welcomeBubble,
|
|
1741
|
-
delay: script.dataset.welcomeBubbleDelay ? parseInt(script.dataset.welcomeBubbleDelay, 10) : void 0
|
|
1742
|
-
} : void 0,
|
|
1743
|
-
autoOpen: parseAutoOpen(script.dataset.autoOpen),
|
|
1744
|
-
placeholder: script.dataset.placeholder,
|
|
1745
|
-
locale: script.dataset.locale
|
|
1746
|
-
});
|
|
2120
|
+
const config = script && configFromScript(script);
|
|
2121
|
+
if (config) ChatCops.init(config);
|
|
1747
2122
|
}
|
|
1748
2123
|
const currentScript = typeof document !== "undefined" ? document.currentScript : null;
|
|
1749
2124
|
if (currentScript && currentScript.dataset.apiUrl) {
|
|
@@ -1756,6 +2131,225 @@ if (currentScript && currentScript.dataset.apiUrl) {
|
|
|
1756
2131
|
if (typeof window !== "undefined") {
|
|
1757
2132
|
window.ChatCops = ChatCops;
|
|
1758
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" }));
|
|
1759
2353
|
if (typeof window !== "undefined" && window.ChatCops && window.ChatCops.default) {
|
|
1760
2354
|
Object.assign(window.ChatCops, window.ChatCops.default);
|
|
1761
2355
|
}
|