@chatcops/widget 0.4.3 → 0.5.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.
@@ -1,10 +1,12 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- const widgetStyles = ":host{--cc-accent: #6366f1;--cc-accent-hover: #4f46e5;--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-font: system-ui, -apple-system, sans-serif;--cc-radius: 12px;--cc-shadow: 0 8px 32px rgba(0, 0, 0, .4);--cc-fab-size: 56px;--cc-panel-width: 400px;--cc-panel-height: 560px;all:initial;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);-webkit-font-smoothing:antialiased}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}.cc-fab{position:fixed;bottom:20px;right:20px;width:var(--cc-fab-size);height:var(--cc-fab-size);border-radius:50%;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #0000004d;transition:transform .2s ease,box-shadow .2s ease;z-index:10000;outline:none}.cc-fab:hover{transform:scale(1.05);box-shadow:0 6px 24px #0006}.cc-fab:active{transform:scale(.95)}.cc-fab.cc-left{right:auto;left:20px}.cc-fab svg{width:24px;height:24px;fill:var(--cc-text);transition:transform .3s ease}.cc-fab.cc-open svg{transform:rotate(90deg)}.cc-fab-badge{position:absolute;top:-2px;right:-2px;width:12px;height:12px;border-radius:50%;background:#ef4444;border:2px solid var(--cc-bg);display:none}.cc-fab-badge.cc-visible{display:block}@keyframes cc-pulse{0%,to{box-shadow:0 4px 16px #0000004d}50%{box-shadow:0 4px 16px #6366f180}}.cc-fab.cc-pulse{animation:cc-pulse 2s ease-in-out 3}.cc-panel{position:fixed;bottom:88px;right:20px;width:var(--cc-panel-width);height:var(--cc-panel-height);max-height:calc(100vh - 100px);background:var(--cc-bg);border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);display:flex;flex-direction:column;overflow:hidden;z-index:9999;opacity:0;transform:translateY(16px) scale(.95);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.cc-panel.cc-visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.cc-panel.cc-left{right:auto;left:20px}.cc-panel.cc-inline{position:relative;bottom:auto;right:auto;left:auto;width:100%;height:100%;max-height:100%;z-index:auto;transform:none;transition:none}.cc-panel.cc-inline:not(.cc-visible){visibility:hidden}.cc-panel.cc-inline.cc-visible{transform:none}.cc-header{display:flex;align-items:center;padding:16px;background:var(--cc-bg-surface);border-bottom:1px solid var(--cc-border);flex-shrink:0}.cc-header-avatar{width:36px;height:36px;border-radius:50%;margin-right:12px;object-fit:cover}.cc-header-avatar-placeholder{width:36px;height:36px;border-radius:50%;margin-right:12px;background:var(--cc-accent);display:flex;align-items:center;justify-content:center}.cc-header-avatar-placeholder svg{width:20px;height:20px;fill:var(--cc-text)}.cc-header-info{flex:1}.cc-header-name{font-size:15px;font-weight:600;color:var(--cc-text)}.cc-header-subtitle{font-size:12px;color:var(--cc-text-secondary)}.cc-header-close{width:32px;height:32px;border-radius:8px;background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.cc-header-close:hover{background:var(--cc-bg-input)}.cc-header-close svg{width:16px;height:16px;fill:var(--cc-text-secondary)}.cc-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.cc-messages::-webkit-scrollbar{width:4px}.cc-messages::-webkit-scrollbar-track{background:transparent}.cc-messages::-webkit-scrollbar-thumb{background:var(--cc-border);border-radius:2px}.cc-message{max-width:85%;padding:10px 14px;border-radius:var(--cc-radius);word-wrap:break-word;overflow-wrap:break-word;font-size:14px;line-height:1.5;display:flex;flex-direction:column;gap:8px}.cc-message-user{align-self:flex-end;background:var(--cc-accent);color:var(--cc-text);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-text)}.cc-message code{background:#0003;padding:1px 4px;border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:13px}.cc-message pre{background:#0000004d;padding:8px 12px;border-radius:8px;overflow-x:auto;margin:4px 0}.cc-message pre code{background:transparent;padding:0}.cc-message ul,.cc-message ol{padding-left:20px;margin:4px 0}.cc-message h1,.cc-message h2,.cc-message h3,.cc-message h4,.cc-message h5,.cc-message h6{font-size:15px;font-weight:600;line-height:1.4;margin:4px 0 0}.cc-message-actions{display:flex;align-items:center}.cc-message-retry,.cc-message-regenerate{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:transparent;border:1px solid var(--cc-border);border-radius:6px;color:var(--cc-text-secondary);font-family:var(--cc-font);font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,opacity .15s ease}.cc-message-retry:hover,.cc-message-regenerate:hover{background:var(--cc-bg-input);color:var(--cc-text)}.cc-message-retry svg,.cc-message-regenerate svg{fill:currentColor}.cc-message-regenerate{opacity:0}.cc-message:hover .cc-message-regenerate{opacity:1}.cc-message:focus-within .cc-message-regenerate,.cc-message-regenerate:focus-visible{opacity:1}@media(hover:none){.cc-message-regenerate{opacity:1}}.cc-message-retry{border-color:#ef4444;color:#ef4444}.cc-message-retry:hover{background:#ef44441a;color:#ef4444}.cc-typing{display:flex;align-items:center;gap:4px;padding:10px 14px;align-self:flex-start;background:var(--cc-bg-surface);border-radius:var(--cc-radius);border-bottom-left-radius:4px}.cc-typing-dot{width:6px;height:6px;border-radius:50%;background:var(--cc-text-secondary);animation:cc-bounce 1.2s infinite}.cc-typing-dot:nth-child(2){animation-delay:.2s}.cc-typing-dot:nth-child(3){animation-delay:.4s}@keyframes cc-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-4px)}}.cc-input-area{display:flex;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--cc-border);background:var(--cc-bg);gap:8px}.cc-input{flex:1;background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;line-height:1.5;resize:none;outline:none;max-height:96px;overflow-y:auto;transition:border-color .15s ease}.cc-input:focus{border-color:var(--cc-accent);outline:2px solid var(--cc-accent);outline-offset:1px}.cc-input::placeholder{color:var(--cc-text-secondary)}.cc-send-btn{width:36px;height:36px;border-radius:8px;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s ease,opacity .15s ease}.cc-send-btn:hover{background:var(--cc-accent-hover)}.cc-send-btn:disabled{opacity:.4;cursor:not-allowed}.cc-send-btn svg{width:16px;height:16px;fill:var(--cc-text)}.cc-footer{padding:6px 16px;text-align:center;font-size:11px;color:var(--cc-text-secondary);border-top:1px solid var(--cc-border)}.cc-footer a{color:var(--cc-text-secondary);text-decoration:none}.cc-footer a:hover{text-decoration:underline}.cc-bubble{position:fixed;bottom:84px;right:20px;background:var(--cc-bg-surface);color:var(--cc-text);padding:10px 16px;border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);max-width:260px;font-size:13px;line-height:1.4;z-index:9998;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .3s ease,transform .3s ease;cursor:pointer}.cc-bubble.cc-visible{opacity:1;transform:translateY(0);pointer-events:auto}.cc-bubble.cc-left{right:auto;left:20px}.cc-bubble-close{position:absolute;top:-8px;right:-8px;width:20px;height:20px;border-radius:50%;background:var(--cc-bg);border:1px solid var(--cc-border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--cc-text-secondary)}.cc-hidden{display:none}.cc-messages-wrapper{flex:1;display:flex;flex-direction:column;min-height:0}.cc-prechat{flex:1;overflow-y:auto;padding:24px 16px;display:flex;flex-direction:column;gap:16px}.cc-prechat-title{font-size:18px;font-weight:600;color:var(--cc-text)}.cc-prechat-subtitle{font-size:13px;color:var(--cc-text-secondary);margin-top:-8px}.cc-prechat-fields{display:flex;flex-direction:column;gap:14px}.cc-prechat-field{display:flex;flex-direction:column;gap:4px}.cc-prechat-label{font-size:13px;font-weight:500;color:var(--cc-text)}.cc-prechat-label .cc-required{color:#ef4444;margin-left:2px}.cc-prechat-input,.cc-prechat-select,.cc-prechat-textarea{background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;outline:none;transition:border-color .15s ease}.cc-prechat-input:focus,.cc-prechat-select:focus,.cc-prechat-textarea:focus{border-color:var(--cc-accent)}.cc-prechat-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}.cc-prechat-textarea{resize:none;line-height:1.5}.cc-prechat-error{font-size:12px;color:#ef4444;min-height:0}.cc-prechat-error:empty{display:none}.cc-prechat-submit{background:var(--cc-accent);color:var(--cc-text);border:none;border-radius:8px;padding:10px 16px;font-family:var(--cc-font);font-size:14px;font-weight:500;cursor:pointer;transition:background .15s ease;margin-top:8px}.cc-prechat-submit:hover{background:var(--cc-accent-hover)}.cc-prechat-submit:disabled{opacity:.5;cursor:not-allowed}@media(max-width:768px){.cc-fab.cc-open{display:none}.cc-panel:not(.cc-inline){width:100%;height:100%;max-height:100%;bottom:0;right:0;left:0;border-radius:0;border:none}.cc-panel.cc-left:not(.cc-inline){left:0}.cc-bubble{bottom:84px;right:16px;left:16px;max-width:none}}.cc-fab:focus-visible,.cc-header-close:focus-visible,.cc-send-btn:focus-visible,.cc-message-retry:focus-visible,.cc-message-regenerate:focus-visible,.cc-bubble-close:focus-visible,.cc-prechat-submit:focus-visible{outline:2px solid var(--cc-accent);outline-offset:3px}@media(prefers-reduced-motion:reduce){.cc-fab.cc-pulse,.cc-typing-dot{animation:none}.cc-panel,.cc-bubble{transition:none}}";
4
+ const widgetStyles = ":host{--cc-accent: #6366f1;--cc-accent-hover: #4f46e5;--cc-on-accent: #ffffff;--cc-accent-contrast: #6164ec;--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-font: system-ui, -apple-system, sans-serif;--cc-radius: 12px;--cc-shadow: 0 8px 32px rgba(0, 0, 0, .4);--cc-fab-size: 56px;--cc-panel-width: 400px;--cc-panel-height: 560px;all:initial;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);-webkit-font-smoothing:antialiased}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}.cc-fab{position:fixed;bottom:20px;right:20px;width:var(--cc-fab-size);height:var(--cc-fab-size);border-radius:50%;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #0000004d;transition:transform .2s ease,box-shadow .2s ease;z-index:10000;outline:none}.cc-fab:hover{transform:scale(1.05);box-shadow:0 6px 24px #0006}.cc-fab:active{transform:scale(.95)}.cc-fab.cc-left{right:auto;left:20px}.cc-fab svg{width:24px;height:24px;fill:var(--cc-on-accent);transition:transform .3s ease}.cc-fab.cc-open svg{transform:rotate(90deg)}.cc-fab-badge{position:absolute;top:-2px;right:-2px;width:12px;height:12px;border-radius:50%;background:#ef4444;border:2px solid var(--cc-bg);display:none}.cc-fab-badge.cc-visible{display:block}@keyframes cc-pulse{0%,to{box-shadow:0 4px 16px #0000004d}50%{box-shadow:0 4px 16px #6366f180}}.cc-fab.cc-pulse{animation:cc-pulse 2s ease-in-out 3}.cc-panel{position:fixed;bottom:88px;right:20px;width:var(--cc-panel-width);height:var(--cc-panel-height);max-height:calc(100vh - 100px);background:var(--cc-bg);border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);display:flex;flex-direction:column;overflow:hidden;z-index:9999;opacity:0;transform:translateY(16px) scale(.95);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.cc-panel.cc-visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.cc-panel.cc-left{right:auto;left:20px}.cc-panel.cc-inline{position:relative;bottom:auto;right:auto;left:auto;width:100%;height:100%;max-height:100%;z-index:auto;transform:none;transition:none}.cc-panel.cc-inline:not(.cc-visible){visibility:hidden}.cc-panel.cc-inline.cc-visible{transform:none}.cc-header{display:flex;align-items:center;padding:16px;background:var(--cc-bg-surface);border-bottom:1px solid var(--cc-border);flex-shrink:0}.cc-header-avatar{width:36px;height:36px;border-radius:50%;margin-right:12px;object-fit:cover}.cc-header-avatar-placeholder{width:36px;height:36px;border-radius:50%;margin-right:12px;background:var(--cc-accent);display:flex;align-items:center;justify-content:center}.cc-header-avatar-placeholder svg{width:20px;height:20px;fill:var(--cc-on-accent)}.cc-header-info{flex:1}.cc-header-name{font-size:15px;font-weight:600;color:var(--cc-text)}.cc-header-subtitle{font-size:12px;color:var(--cc-text-secondary)}.cc-header-close{width:32px;height:32px;border-radius:8px;background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.cc-header-close:hover{background:var(--cc-bg-input)}.cc-header-close svg{width:16px;height:16px;fill:var(--cc-text-secondary)}.cc-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.cc-messages::-webkit-scrollbar{width:4px}.cc-messages::-webkit-scrollbar-track{background:transparent}.cc-messages::-webkit-scrollbar-thumb{background:var(--cc-border);border-radius:2px}.cc-message{max-width:85%;padding:10px 14px;border-radius:var(--cc-radius);word-wrap:break-word;overflow-wrap:break-word;font-size:14px;line-height:1.5;display:flex;flex-direction:column;gap:8px}.cc-message-user{align-self:flex-end;background:var(--cc-accent-contrast);color:var(--cc-on-accent);border-bottom-right-radius:4px}.cc-message-assistant{align-self:flex-start;background:var(--cc-bg-surface);color:var(--cc-text);border-bottom-left-radius:4px}.cc-message a{color:var(--cc-accent);text-decoration:underline}.cc-message-user a{color:var(--cc-on-accent)}.cc-message-error-note{margin-top:6px;font-size:12px;color:var(--cc-text-secondary)}.cc-message code{background:#0003;padding:1px 4px;border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:13px}.cc-message pre{background:#0000004d;padding:8px 12px;border-radius:8px;overflow-x:auto;margin:4px 0}.cc-message pre code{background:transparent;padding:0}.cc-message ul,.cc-message ol{padding-left:20px;margin:4px 0}.cc-message h1,.cc-message h2,.cc-message h3,.cc-message h4,.cc-message h5,.cc-message h6{font-size:15px;font-weight:600;line-height:1.4;margin:4px 0 0}.cc-message-actions{display:flex;align-items:center}.cc-message-retry,.cc-message-regenerate{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:transparent;border:1px solid var(--cc-border);border-radius:6px;color:var(--cc-text-secondary);font-family:var(--cc-font);font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,opacity .15s ease}.cc-message-retry:hover,.cc-message-regenerate:hover{background:var(--cc-bg-input);color:var(--cc-text)}.cc-message-retry svg,.cc-message-regenerate svg{fill:currentColor}.cc-message-regenerate{opacity:0}.cc-message:hover .cc-message-regenerate{opacity:1}.cc-message:focus-within .cc-message-regenerate,.cc-message-regenerate:focus-visible{opacity:1}@media(hover:none){.cc-message-regenerate{opacity:1}}.cc-message-retry{border-color:#ef4444;color:#ef4444}.cc-message-retry:hover{background:#ef44441a;color:#ef4444}.cc-typing{display:flex;align-items:center;gap:4px;padding:10px 14px;align-self:flex-start;background:var(--cc-bg-surface);border-radius:var(--cc-radius);border-bottom-left-radius:4px}.cc-typing-dot{width:6px;height:6px;border-radius:50%;background:var(--cc-text-secondary);animation:cc-bounce 1.2s infinite}.cc-typing-dot:nth-child(2){animation-delay:.2s}.cc-typing-dot:nth-child(3){animation-delay:.4s}@keyframes cc-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-4px)}}.cc-input-area{display:flex;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--cc-border);background:var(--cc-bg);gap:8px}.cc-input{flex:1;background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;line-height:1.5;resize:none;outline:none;max-height:96px;overflow-y:auto;transition:border-color .15s ease}.cc-input:focus{border-color:var(--cc-accent);outline:2px solid var(--cc-accent);outline-offset:1px}.cc-input::placeholder{color:var(--cc-text-secondary)}.cc-send-btn{width:36px;height:36px;border-radius:8px;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s ease,opacity .15s ease}.cc-send-btn:hover{background:var(--cc-accent-hover)}.cc-send-btn:disabled{opacity:.4;cursor:not-allowed}.cc-send-btn svg{width:16px;height:16px;fill:var(--cc-on-accent)}.cc-footer{padding:6px 16px;text-align:center;font-size:11px;color:var(--cc-text-secondary);border-top:1px solid var(--cc-border)}.cc-footer a{color:var(--cc-text-secondary);text-decoration:none}.cc-footer a:hover{text-decoration:underline}.cc-bubble{position:fixed;bottom:84px;right:20px;background:var(--cc-bg-surface);color:var(--cc-text);padding:10px 16px;border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);max-width:260px;font-size:13px;line-height:1.4;z-index:9998;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .3s ease,transform .3s ease;cursor:pointer}.cc-bubble.cc-visible{opacity:1;transform:translateY(0);pointer-events:auto}.cc-bubble.cc-left{right:auto;left:20px}.cc-bubble-close{position:absolute;top:-8px;right:-8px;width:20px;height:20px;border-radius:50%;background:var(--cc-bg);border:1px solid var(--cc-border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--cc-text-secondary)}.cc-hidden{display:none}.cc-sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}.cc-messages-wrapper{flex:1;display:flex;flex-direction:column;min-height:0}.cc-prechat{flex:1;overflow-y:auto;padding:24px 16px;display:flex;flex-direction:column;gap:16px}.cc-prechat-title{font-size:18px;font-weight:600;color:var(--cc-text)}.cc-prechat-subtitle{font-size:13px;color:var(--cc-text-secondary);margin-top:-8px}.cc-prechat-fields{display:flex;flex-direction:column;gap:14px}.cc-prechat-field{display:flex;flex-direction:column;gap:4px}.cc-prechat-label{font-size:13px;font-weight:500;color:var(--cc-text)}.cc-prechat-label .cc-required{color:#ef4444;margin-left:2px}.cc-prechat-input,.cc-prechat-select,.cc-prechat-textarea{background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;outline:none;transition:border-color .15s ease}.cc-prechat-input:focus,.cc-prechat-select:focus,.cc-prechat-textarea:focus{border-color:var(--cc-accent)}.cc-prechat-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}.cc-prechat-textarea{resize:none;line-height:1.5}.cc-prechat-error{font-size:12px;color:#ef4444;min-height:0}.cc-prechat-error:empty{display:none}.cc-prechat-submit{background:var(--cc-accent-contrast);color:var(--cc-on-accent);border:none;border-radius:8px;padding:10px 16px;font-family:var(--cc-font);font-size:14px;font-weight:500;cursor:pointer;transition:background .15s ease;margin-top:8px}.cc-prechat-submit:hover{background:var(--cc-accent-hover)}.cc-prechat-submit:disabled{opacity:.5;cursor:not-allowed}@media(max-width:768px){.cc-fab.cc-open{display:none}.cc-panel:not(.cc-inline){width:100%;height:100%;max-height:100%;bottom:0;right:0;left:0;border-radius:0;border:none}.cc-panel.cc-left:not(.cc-inline){left:0}.cc-bubble{bottom:84px;right:16px;left:16px;max-width:none}}.cc-fab:focus-visible,.cc-header-close:focus-visible,.cc-send-btn:focus-visible,.cc-message-retry:focus-visible,.cc-message-regenerate:focus-visible,.cc-bubble-close:focus-visible,.cc-prechat-submit:focus-visible{outline:2px solid var(--cc-accent);outline-offset:3px}@media(prefers-reduced-motion:reduce){.cc-fab.cc-pulse,.cc-typing-dot{animation:none}.cc-panel,.cc-bubble{transition:none}}";
5
+ const HOST_ID = "chatcops-root";
5
6
  function createShadowRoot(container) {
6
7
  const host = document.createElement("div");
7
- host.id = "chatcops-root";
8
+ host.id = nextHostId();
9
+ host.className = HOST_ID;
8
10
  if (container) {
9
11
  container.appendChild(host);
10
12
  } else {
@@ -16,50 +18,14 @@ function createShadowRoot(container) {
16
18
  shadow.appendChild(style);
17
19
  return shadow;
18
20
  }
19
- function destroyShadowRoot(container) {
20
- const searchRoot = container ?? document.body;
21
- const host = searchRoot.querySelector("#chatcops-root") ?? document.getElementById("chatcops-root");
22
- if (host) host.remove();
21
+ function destroyShadowRoot(shadow) {
22
+ shadow == null ? void 0 : shadow.host.remove();
23
23
  }
24
- const CHAT_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h10v2H7zm0-3h10v2H7z"/></svg>`;
25
- const CLOSE_ICON$1 = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
26
- class FAB {
27
- constructor(root, options) {
28
- __publicField(this, "el");
29
- __publicField(this, "badge");
30
- __publicField(this, "isOpen", false);
31
- this.el = document.createElement("button");
32
- this.el.className = "cc-fab cc-pulse";
33
- this.el.setAttribute("aria-label", "Open chat");
34
- if (options.position === "bottom-left") {
35
- this.el.classList.add("cc-left");
36
- }
37
- this.el.innerHTML = CHAT_ICON;
38
- this.badge = document.createElement("div");
39
- this.badge.className = "cc-fab-badge";
40
- this.el.appendChild(this.badge);
41
- this.el.addEventListener("click", () => {
42
- this.el.classList.remove("cc-pulse");
43
- options.onClick();
44
- });
45
- root.appendChild(this.el);
46
- }
47
- setOpen(open) {
48
- this.isOpen = open;
49
- this.el.innerHTML = open ? CLOSE_ICON$1 : CHAT_ICON;
50
- if (!open) this.el.appendChild(this.badge);
51
- this.el.classList.toggle("cc-open", open);
52
- this.el.setAttribute("aria-label", open ? "Close chat" : "Open chat");
53
- }
54
- showBadge() {
55
- this.badge.classList.add("cc-visible");
56
- }
57
- hideBadge() {
58
- this.badge.classList.remove("cc-visible");
59
- }
60
- destroy() {
61
- this.el.remove();
62
- }
24
+ function nextHostId() {
25
+ if (!document.getElementById(HOST_ID)) return HOST_ID;
26
+ let n = 2;
27
+ while (document.getElementById(`${HOST_ID}-${n}`)) n += 1;
28
+ return `${HOST_ID}-${n}`;
63
29
  }
64
30
  const ESCAPE_MAP = {
65
31
  "&": "&amp;",
@@ -133,6 +99,8 @@ class Messages {
133
99
  this.actions = actions;
134
100
  this.container = document.createElement("div");
135
101
  this.container.className = "cc-messages";
102
+ this.container.setAttribute("role", "log");
103
+ this.container.setAttribute("aria-live", "polite");
136
104
  parent.appendChild(this.container);
137
105
  }
138
106
  addMessage(msg) {
@@ -143,17 +111,18 @@ class Messages {
143
111
  return el;
144
112
  }
145
113
  updateMessage(msg) {
146
- const el = this.container.querySelector(`[data-id="${msg.id}"]`);
114
+ const el = this.findMessage(msg.id);
147
115
  if (el) {
148
116
  this.renderMessageElement(el, msg);
149
117
  this.scrollToBottom();
150
118
  }
151
119
  }
152
120
  showTyping() {
121
+ var _a;
153
122
  if (this.typingEl) return;
154
123
  this.typingEl = document.createElement("div");
155
124
  this.typingEl.className = "cc-typing";
156
- this.typingEl.innerHTML = '<div class="cc-typing-dot"></div><div class="cc-typing-dot"></div><div class="cc-typing-dot"></div>';
125
+ this.typingEl.innerHTML = `<span class="cc-sr-only">${this.escapeHtml(((_a = this.actions) == null ? void 0 : _a.typingLabel) ?? "Typing...")}</span><div class="cc-typing-dot" aria-hidden="true"></div><div class="cc-typing-dot" aria-hidden="true"></div><div class="cc-typing-dot" aria-hidden="true"></div>`;
157
126
  this.container.appendChild(this.typingEl);
158
127
  this.scrollToBottom();
159
128
  }
@@ -168,7 +137,7 @@ class Messages {
168
137
  this.typingEl = null;
169
138
  }
170
139
  removeMessage(id) {
171
- const el = this.container.querySelector(`[data-id="${id}"]`);
140
+ const el = this.findMessage(id);
172
141
  el == null ? void 0 : el.remove();
173
142
  }
174
143
  clearRegenerateButtons() {
@@ -179,7 +148,7 @@ class Messages {
179
148
  var _a;
180
149
  this.clearRegenerateButtons();
181
150
  if (!((_a = this.actions) == null ? void 0 : _a.onRegenerate)) return;
182
- const msgEl = this.container.querySelector(`[data-id="${messageId}"]`);
151
+ const msgEl = this.findMessage(messageId);
183
152
  if (!msgEl || !msgEl.classList.contains("cc-message-assistant")) return;
184
153
  const actions = this.ensureActionsContainer(msgEl);
185
154
  actions.appendChild(this.createActionButton({
@@ -195,6 +164,10 @@ class Messages {
195
164
  setVisible(visible) {
196
165
  this.container.classList.toggle("cc-hidden", !visible);
197
166
  }
167
+ findMessage(id) {
168
+ const found = Array.from(this.container.children).find((el) => el.dataset.id === id);
169
+ return found ?? null;
170
+ }
198
171
  scrollToBottom() {
199
172
  this.container.scrollTop = this.container.scrollHeight;
200
173
  }
@@ -211,6 +184,11 @@ class Messages {
211
184
  } else {
212
185
  delete el.dataset.status;
213
186
  }
187
+ if (msg.status === "streaming") {
188
+ el.setAttribute("aria-busy", "true");
189
+ } else {
190
+ el.removeAttribute("aria-busy");
191
+ }
214
192
  if (msg.errorType) {
215
193
  el.dataset.errorType = msg.errorType;
216
194
  } else {
@@ -221,6 +199,12 @@ class Messages {
221
199
  content.className = "cc-message-content";
222
200
  content.innerHTML = msg.role === "assistant" ? renderMarkdown(msg.content) : this.escapeHtml(msg.content);
223
201
  el.appendChild(content);
202
+ if (msg.status === "error" && msg.errorText) {
203
+ const note = document.createElement("div");
204
+ note.className = "cc-message-error-note";
205
+ note.textContent = msg.errorText;
206
+ el.appendChild(note);
207
+ }
224
208
  const actions = this.createActionsContainer(msg);
225
209
  if (actions) {
226
210
  el.appendChild(actions);
@@ -288,6 +272,7 @@ class Input {
288
272
  this.textarea = document.createElement("textarea");
289
273
  this.textarea.className = "cc-input";
290
274
  this.textarea.placeholder = options.placeholder;
275
+ this.textarea.setAttribute("aria-label", options.placeholder);
291
276
  this.textarea.rows = 1;
292
277
  this.textarea.maxLength = MAX_MESSAGE_LENGTH;
293
278
  this.textarea.addEventListener("input", () => this.autoResize());
@@ -295,7 +280,7 @@ class Input {
295
280
  this.sendBtn = document.createElement("button");
296
281
  this.sendBtn.className = "cc-send-btn";
297
282
  this.sendBtn.disabled = true;
298
- this.sendBtn.setAttribute("aria-label", "Send message");
283
+ this.sendBtn.setAttribute("aria-label", options.sendLabel);
299
284
  this.sendBtn.innerHTML = '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>';
300
285
  this.sendBtn.addEventListener("click", () => this.send());
301
286
  this.textarea.addEventListener("input", () => {
@@ -339,8 +324,9 @@ class Input {
339
324
  this.textarea.style.height = Math.min(this.textarea.scrollHeight, maxHeight) + "px";
340
325
  }
341
326
  }
327
+ const PANEL_ID = "cc-panel";
342
328
  const BOT_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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>`;
343
- const CLOSE_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
329
+ const CLOSE_ICON$1 = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
344
330
  class Panel {
345
331
  constructor(root, options) {
346
332
  __publicField(this, "el");
@@ -357,6 +343,15 @@ class Panel {
357
343
  if (options.position === "bottom-left") {
358
344
  this.el.classList.add("cc-left");
359
345
  }
346
+ if (!options.inline) {
347
+ this.el.id = PANEL_ID;
348
+ this.el.setAttribute("role", "dialog");
349
+ this.el.setAttribute("aria-label", options.branding.name);
350
+ this.el.addEventListener("keydown", (e) => {
351
+ if (e.key === "Escape") options.onClose();
352
+ });
353
+ this.setHidden(true);
354
+ }
360
355
  const header = document.createElement("div");
361
356
  header.className = "cc-header";
362
357
  if (options.branding.avatar) {
@@ -378,8 +373,8 @@ class Panel {
378
373
  if (!options.inline) {
379
374
  const closeBtn = document.createElement("button");
380
375
  closeBtn.className = "cc-header-close";
381
- closeBtn.setAttribute("aria-label", "Close chat");
382
- closeBtn.innerHTML = CLOSE_ICON;
376
+ closeBtn.setAttribute("aria-label", options.closeLabel);
377
+ closeBtn.innerHTML = CLOSE_ICON$1;
383
378
  closeBtn.addEventListener("click", options.onClose);
384
379
  header.appendChild(closeBtn);
385
380
  }
@@ -394,6 +389,7 @@ class Panel {
394
389
  this.messages = new Messages(this.messagesContainer, options.messageActions);
395
390
  this.input = new Input(this.el, {
396
391
  placeholder: options.placeholder,
392
+ sendLabel: options.sendLabel,
397
393
  onSend: options.onSend
398
394
  });
399
395
  const footer = document.createElement("div");
@@ -405,11 +401,16 @@ class Panel {
405
401
  show() {
406
402
  this.el.classList.add("cc-visible");
407
403
  if (!this.inline) {
404
+ this.setHidden(false);
408
405
  this.input.focus();
409
406
  }
410
407
  }
411
408
  hide() {
412
409
  this.el.classList.remove("cc-visible");
410
+ if (!this.inline) this.setHidden(true);
411
+ }
412
+ containsFocus() {
413
+ return this.el.contains(this.el.getRootNode().activeElement);
413
414
  }
414
415
  get isVisible() {
415
416
  return this.el.classList.contains("cc-visible");
@@ -426,12 +427,68 @@ class Panel {
426
427
  destroy() {
427
428
  this.el.remove();
428
429
  }
430
+ // A closed popup can't be focused or read by screen readers.
431
+ setHidden(hidden) {
432
+ this.el.toggleAttribute("inert", hidden);
433
+ if (hidden) this.el.setAttribute("aria-hidden", "true");
434
+ else this.el.removeAttribute("aria-hidden");
435
+ }
429
436
  escapeHtml(text) {
430
437
  const div = document.createElement("div");
431
438
  div.textContent = text;
432
439
  return div.innerHTML;
433
440
  }
434
441
  }
442
+ const CHAT_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M7 9h10v2H7zm0-3h10v2H7z"/></svg>`;
443
+ const CLOSE_ICON = `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>`;
444
+ class FAB {
445
+ constructor(root, options) {
446
+ __publicField(this, "el");
447
+ __publicField(this, "badge");
448
+ __publicField(this, "isOpen", false);
449
+ __publicField(this, "openLabel");
450
+ __publicField(this, "closeLabel");
451
+ this.openLabel = options.openLabel;
452
+ this.closeLabel = options.closeLabel;
453
+ this.el = document.createElement("button");
454
+ this.el.className = "cc-fab cc-pulse";
455
+ this.el.setAttribute("aria-label", this.openLabel);
456
+ this.el.setAttribute("aria-expanded", "false");
457
+ this.el.setAttribute("aria-controls", PANEL_ID);
458
+ if (options.position === "bottom-left") {
459
+ this.el.classList.add("cc-left");
460
+ }
461
+ this.el.innerHTML = CHAT_ICON;
462
+ this.badge = document.createElement("div");
463
+ this.badge.className = "cc-fab-badge";
464
+ this.el.appendChild(this.badge);
465
+ this.el.addEventListener("click", () => {
466
+ this.el.classList.remove("cc-pulse");
467
+ options.onClick();
468
+ });
469
+ root.appendChild(this.el);
470
+ }
471
+ setOpen(open) {
472
+ this.isOpen = open;
473
+ this.el.innerHTML = open ? CLOSE_ICON : CHAT_ICON;
474
+ if (!open) this.el.appendChild(this.badge);
475
+ this.el.classList.toggle("cc-open", open);
476
+ this.el.setAttribute("aria-label", open ? this.closeLabel : this.openLabel);
477
+ this.el.setAttribute("aria-expanded", String(open));
478
+ }
479
+ focus() {
480
+ this.el.focus();
481
+ }
482
+ showBadge() {
483
+ this.badge.classList.add("cc-visible");
484
+ }
485
+ hideBadge() {
486
+ this.badge.classList.remove("cc-visible");
487
+ }
488
+ destroy() {
489
+ this.el.remove();
490
+ }
491
+ }
435
492
  const BUBBLE_SHOWN_KEY = "chatcops_bubble_shown";
436
493
  class WelcomeBubble {
437
494
  constructor(root, options) {
@@ -443,8 +500,11 @@ class WelcomeBubble {
443
500
  this.el.classList.add("cc-left");
444
501
  }
445
502
  this.el.textContent = options.text;
503
+ this.setHidden(true);
446
504
  const closeBtn = document.createElement("button");
505
+ closeBtn.type = "button";
447
506
  closeBtn.className = "cc-bubble-close";
507
+ closeBtn.setAttribute("aria-label", options.dismissLabel);
448
508
  closeBtn.innerHTML = "&times;";
449
509
  closeBtn.addEventListener("click", (e) => {
450
510
  e.stopPropagation();
@@ -465,15 +525,22 @@ class WelcomeBubble {
465
525
  }
466
526
  this.timerId = setTimeout(() => {
467
527
  this.el.classList.add("cc-visible");
528
+ this.setHidden(false);
468
529
  }, options.delay ?? 3e3);
469
530
  }
470
531
  hide() {
471
532
  this.el.classList.remove("cc-visible");
533
+ this.setHidden(true);
472
534
  if (this.timerId) {
473
535
  clearTimeout(this.timerId);
474
536
  this.timerId = null;
475
537
  }
476
538
  }
539
+ setHidden(hidden) {
540
+ this.el.toggleAttribute("inert", hidden);
541
+ if (hidden) this.el.setAttribute("aria-hidden", "true");
542
+ else this.el.removeAttribute("aria-hidden");
543
+ }
477
544
  destroy() {
478
545
  this.hide();
479
546
  this.el.remove();
@@ -489,9 +556,37 @@ class ChatClient {
489
556
  async *sendMessage(request, options = {}) {
490
557
  var _a;
491
558
  const controller = new AbortController();
492
- const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.timeout);
559
+ const timeout = options.timeout ?? this.timeout;
560
+ let timeoutId;
561
+ let timedOut = false;
562
+ let reader;
493
563
  const externalSignal = options.signal;
494
- const onExternalAbort = () => controller.abort();
564
+ const onExternalAbort = () => {
565
+ clearTimeout(timeoutId);
566
+ controller.abort();
567
+ };
568
+ const resetTimeout = () => {
569
+ clearTimeout(timeoutId);
570
+ if (controller.signal.aborted) return;
571
+ timeoutId = setTimeout(() => {
572
+ timedOut = true;
573
+ controller.abort();
574
+ }, timeout);
575
+ };
576
+ const cleanup = async () => {
577
+ clearTimeout(timeoutId);
578
+ externalSignal == null ? void 0 : externalSignal.removeEventListener("abort", onExternalAbort);
579
+ if (reader) {
580
+ const currentReader = reader;
581
+ reader = void 0;
582
+ try {
583
+ await currentReader.cancel();
584
+ } catch {
585
+ } finally {
586
+ currentReader.releaseLock();
587
+ }
588
+ }
589
+ };
495
590
  if (externalSignal) {
496
591
  if (externalSignal.aborted) {
497
592
  controller.abort();
@@ -499,6 +594,7 @@ class ChatClient {
499
594
  externalSignal.addEventListener("abort", onExternalAbort);
500
595
  }
501
596
  }
597
+ resetTimeout();
502
598
  try {
503
599
  const response = await fetch(this.apiUrl, {
504
600
  method: "POST",
@@ -507,6 +603,7 @@ class ChatClient {
507
603
  signal: controller.signal
508
604
  });
509
605
  if (!response.ok) {
606
+ await cleanup();
510
607
  if (response.status === 429) {
511
608
  yield { error: "rate_limit" };
512
609
  return;
@@ -514,8 +611,9 @@ class ChatClient {
514
611
  yield { error: `HTTP ${response.status}` };
515
612
  return;
516
613
  }
517
- const reader = (_a = response.body) == null ? void 0 : _a.getReader();
614
+ reader = (_a = response.body) == null ? void 0 : _a.getReader();
518
615
  if (!reader) {
616
+ await cleanup();
519
617
  yield { error: "No response body" };
520
618
  return;
521
619
  }
@@ -524,6 +622,7 @@ class ChatClient {
524
622
  while (true) {
525
623
  const { done, value } = await reader.read();
526
624
  if (done) break;
625
+ if (value.byteLength > 0) resetTimeout();
527
626
  buffer += decoder.decode(value, { stream: true });
528
627
  const lines = buffer.split("\n");
529
628
  buffer = lines.pop() ?? "";
@@ -532,6 +631,7 @@ class ChatClient {
532
631
  if (!trimmed || !trimmed.startsWith("data: ")) continue;
533
632
  const data = trimmed.slice(6);
534
633
  if (data === "[DONE]") {
634
+ await cleanup();
535
635
  yield { done: true };
536
636
  return;
537
637
  }
@@ -541,24 +641,29 @@ class ChatClient {
541
641
  }
542
642
  }
543
643
  }
644
+ await cleanup();
544
645
  yield { done: true };
545
646
  } catch (err) {
647
+ await cleanup();
546
648
  if (err instanceof DOMException && err.name === "AbortError") {
547
- yield (externalSignal == null ? void 0 : externalSignal.aborted) ? { error: "aborted" } : { error: "timeout" };
649
+ yield timedOut ? { error: "timeout" } : { error: "aborted" };
548
650
  } else {
549
651
  yield { error: "network" };
550
652
  }
551
653
  } finally {
552
- clearTimeout(timeoutId);
553
- externalSignal == null ? void 0 : externalSignal.removeEventListener("abort", onExternalAbort);
654
+ await cleanup();
554
655
  }
555
656
  }
556
657
  }
557
658
  const KEY_PREFIX = "chatcops_conversation";
659
+ const SESSION_KEY_PREFIX = "chatcops_session_id";
558
660
  class ConversationStorage {
661
+ /** Without a siteId the keys are the ones the widget has always used. */
559
662
  constructor(siteId) {
560
663
  __publicField(this, "key");
664
+ __publicField(this, "sessionKey");
561
665
  this.key = siteId ? `${KEY_PREFIX}_${siteId}` : KEY_PREFIX;
666
+ this.sessionKey = siteId ? `${SESSION_KEY_PREFIX}_${siteId}` : SESSION_KEY_PREFIX;
562
667
  }
563
668
  load() {
564
669
  try {
@@ -586,12 +691,11 @@ class ConversationStorage {
586
691
  }
587
692
  }
588
693
  getSessionId() {
589
- const SESSION_KEY = "chatcops_session_id";
590
694
  try {
591
- let id = sessionStorage.getItem(SESSION_KEY);
695
+ let id = sessionStorage.getItem(this.sessionKey);
592
696
  if (!id) {
593
697
  id = crypto.randomUUID();
594
- sessionStorage.setItem(SESSION_KEY, id);
698
+ sessionStorage.setItem(this.sessionKey, id);
595
699
  }
596
700
  return id;
597
701
  } catch {
@@ -605,18 +709,82 @@ class ConversationStorage {
605
709
  return false;
606
710
  }
607
711
  }
608
- setPreChatCompleted(sessionId) {
712
+ setPreChatCompleted(sessionId, data) {
609
713
  try {
610
714
  sessionStorage.setItem(`chatcops-prechat-${sessionId}`, "true");
611
715
  } catch {
612
716
  }
717
+ if (data) {
718
+ this.setPreChatData(sessionId, data);
719
+ }
720
+ }
721
+ getPreChatData(sessionId) {
722
+ try {
723
+ const raw = sessionStorage.getItem(`chatcops-prechat-data-${sessionId}`);
724
+ if (!raw) return null;
725
+ const parsed = JSON.parse(raw);
726
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
727
+ return null;
728
+ }
729
+ return parsed;
730
+ } catch {
731
+ return null;
732
+ }
613
733
  }
734
+ setPreChatData(sessionId, data) {
735
+ try {
736
+ sessionStorage.setItem(`chatcops-prechat-data-${sessionId}`, JSON.stringify(data));
737
+ } catch {
738
+ }
739
+ }
740
+ }
741
+ const DEFAULT = "";
742
+ const CLAIMS_KEY = Symbol.for("chatcops.storageClaims");
743
+ const store = globalThis;
744
+ const claims = store[CLAIMS_KEY] ?? (store[CLAIMS_KEY] = /* @__PURE__ */ new Map());
745
+ function inUse(name) {
746
+ const live = claims.get(name);
747
+ if (!live) return false;
748
+ for (const isLive of live) {
749
+ if (!isLive()) live.delete(isLive);
750
+ }
751
+ if (live.size === 0) claims.delete(name);
752
+ return live.size > 0;
753
+ }
754
+ function claimNamespace(siteId, mode, isLive) {
755
+ let name = siteId ?? DEFAULT;
756
+ if (siteId === void 0 && inUse(DEFAULT)) {
757
+ name = mode;
758
+ for (let n = 2; inUse(name); n += 1) name = `${mode}-${n}`;
759
+ console.warn(
760
+ `[chatcops] Another widget on this page already uses the default storage, so this one keeps its conversation under "${name}". Set a \`siteId\` on each widget to name them yourself.`
761
+ );
762
+ } else if (siteId !== void 0 && inUse(siteId)) {
763
+ console.warn(`[chatcops] Two widgets on this page use the siteId "${siteId}", so they share one conversation.`);
764
+ }
765
+ if (!claims.has(name)) claims.set(name, /* @__PURE__ */ new Set());
766
+ claims.get(name).add(isLive);
767
+ return {
768
+ siteId: name === DEFAULT ? void 0 : name,
769
+ release() {
770
+ var _a, _b;
771
+ (_a = claims.get(name)) == null ? void 0 : _a.delete(isLive);
772
+ if (((_b = claims.get(name)) == null ? void 0 : _b.size) === 0) claims.delete(name);
773
+ }
774
+ };
614
775
  }
615
776
  function applyTheme(root, theme) {
616
777
  const host = root.host;
617
778
  if (theme.accent) {
618
779
  host.style.setProperty("--cc-accent", theme.accent);
619
780
  host.style.setProperty("--cc-accent-hover", darken(theme.accent, 10));
781
+ const pair = accentTextColors(theme.accent);
782
+ if (pair) {
783
+ host.style.setProperty("--cc-on-accent", pair.onAccent);
784
+ host.style.setProperty("--cc-accent-contrast", pair.background);
785
+ } else {
786
+ host.style.setProperty("--cc-accent-contrast", theme.accent);
787
+ }
620
788
  }
621
789
  if (theme.textColor) {
622
790
  host.style.setProperty("--cc-text", theme.textColor);
@@ -639,6 +807,34 @@ function toCssLength(value) {
639
807
  const text = String(value).trim();
640
808
  return /^(?:\d+(?:\.\d+)?|\.\d+)$/.test(text) ? `${text}px` : text;
641
809
  }
810
+ const WHITE = "#ffffff";
811
+ const NEAR_BLACK = "#0a0a0a";
812
+ const MIN_TEXT_CONTRAST = 4.5;
813
+ function isHex(color) {
814
+ return /^#?[\da-f]{3}(?:[\da-f]{3})?$/i.test(color.trim());
815
+ }
816
+ function relativeLuminance(hex) {
817
+ const channels = parseHex(hex).map((value) => {
818
+ const c = value / 255;
819
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
820
+ });
821
+ return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
822
+ }
823
+ function contrastRatio(a, b) {
824
+ const [light, dark] = [relativeLuminance(a), relativeLuminance(b)].sort((x, y) => y - x);
825
+ return (light + 0.05) / (dark + 0.05);
826
+ }
827
+ function accentTextColors(accent) {
828
+ if (!isHex(accent)) return null;
829
+ const color = accent.trim().startsWith("#") ? accent.trim() : `#${accent.trim()}`;
830
+ if (contrastRatio(WHITE, color) >= MIN_TEXT_CONTRAST) return { onAccent: WHITE, background: color };
831
+ if (contrastRatio(NEAR_BLACK, color) >= MIN_TEXT_CONTRAST) return { onAccent: NEAR_BLACK, background: color };
832
+ for (let percent = 2; percent <= 60; percent += 2) {
833
+ const darker = darken(color, percent);
834
+ if (contrastRatio(WHITE, darker) >= MIN_TEXT_CONTRAST) return { onAccent: WHITE, background: darker };
835
+ }
836
+ return { onAccent: WHITE, background: NEAR_BLACK };
837
+ }
642
838
  function parseHex(hex) {
643
839
  if (!/^#?[\da-f]{3}(?:[\da-f]{3})?$/i.test(hex)) return [0, 0, 0];
644
840
  const h = hex.replace("#", "");
@@ -659,10 +855,16 @@ function lighten(hex, percent) {
659
855
  return toHex(Math.round(r + (255 - r) * f), Math.round(g + (255 - g) * f), Math.round(b + (255 - b) * f));
660
856
  }
661
857
  const en = {
662
- welcomeMessage: "Hi! How can I help you today?",
858
+ welcomeMessage: "How can I help you today?",
663
859
  inputPlaceholder: "Type a message...",
664
860
  sendButton: "Send",
665
861
  closeButton: "Close",
862
+ openChat: "Open chat",
863
+ defaultName: "AI Assistant",
864
+ defaultSubtitle: "Online",
865
+ greetingWithName: "Hi {name}!",
866
+ selectPlaceholder: "Select {label}...",
867
+ dismissBubble: "Dismiss",
666
868
  errorGeneric: "Something went wrong. Please try again.",
667
869
  errorNetwork: "Connection error. Please check your network.",
668
870
  errorRateLimit: "Too many messages. Please wait a moment.",
@@ -751,7 +953,7 @@ class PreChatForm {
751
953
  select.className = "cc-prechat-select";
752
954
  const defaultOpt = document.createElement("option");
753
955
  defaultOpt.value = "";
754
- defaultOpt.textContent = field.placeholder ?? `Select ${field.label.toLowerCase()}...`;
956
+ defaultOpt.textContent = field.placeholder ?? this.options.locale.selectPlaceholder.replace("{label}", field.label.toLowerCase());
755
957
  defaultOpt.disabled = true;
756
958
  defaultOpt.selected = true;
757
959
  select.appendChild(defaultOpt);
@@ -867,17 +1069,18 @@ class Widget {
867
1069
  constructor(config) {
868
1070
  __publicField(this, "config");
869
1071
  __publicField(this, "shadow");
1072
+ __publicField(this, "mounted", false);
870
1073
  __publicField(this, "fab");
871
1074
  __publicField(this, "panel");
872
1075
  __publicField(this, "bubble");
873
1076
  __publicField(this, "preChatFormEl");
874
1077
  __publicField(this, "client");
875
1078
  __publicField(this, "storage");
1079
+ __publicField(this, "namespace");
876
1080
  __publicField(this, "locale");
877
1081
  __publicField(this, "conversationId");
878
1082
  __publicField(this, "messages", []);
879
1083
  __publicField(this, "userData");
880
- __publicField(this, "userDataSent", false);
881
1084
  __publicField(this, "isStreaming", false);
882
1085
  __publicField(this, "destroyed", false);
883
1086
  __publicField(this, "abortController");
@@ -886,8 +1089,13 @@ class Widget {
886
1089
  __publicField(this, "containerEl");
887
1090
  __publicField(this, "autoOpenTimer");
888
1091
  this.config = config;
889
- this.client = new ChatClient({ apiUrl: config.apiUrl });
890
- this.storage = new ConversationStorage();
1092
+ this.client = new ChatClient({ apiUrl: config.apiUrl, timeout: config.timeout });
1093
+ this.namespace = claimNamespace(
1094
+ config.siteId,
1095
+ config.mode ?? "popup",
1096
+ () => !this.destroyed && (!this.mounted || this.shadow.host.isConnected)
1097
+ );
1098
+ this.storage = new ConversationStorage(this.namespace.siteId);
891
1099
  this.locale = getWidgetLocale(config.locale ?? "en", config.strings);
892
1100
  this.conversationId = this.storage.getSessionId();
893
1101
  }
@@ -908,13 +1116,14 @@ class Widget {
908
1116
  return this.config.container;
909
1117
  }
910
1118
  init() {
911
- var _a, _b, _c, _d, _e, _f;
1119
+ var _a, _b, _c, _d, _e, _f, _g;
912
1120
  if (this.isInline) {
913
1121
  this.containerEl = this.resolveContainer();
914
1122
  this.shadow = createShadowRoot(this.containerEl);
915
1123
  } else {
916
1124
  this.shadow = createShadowRoot();
917
1125
  }
1126
+ this.mounted = true;
918
1127
  if (this.config.theme) {
919
1128
  applyTheme(this.shadow, this.config.theme);
920
1129
  }
@@ -922,6 +1131,8 @@ class Widget {
922
1131
  if (!this.isInline) {
923
1132
  this.fab = new FAB(this.shadow, {
924
1133
  position,
1134
+ openLabel: this.locale.openChat,
1135
+ closeLabel: this.locale.closeButton,
925
1136
  onClick: () => this.toggle()
926
1137
  });
927
1138
  }
@@ -930,12 +1141,14 @@ class Widget {
930
1141
  inline: this.isInline,
931
1142
  preChatEnabled: ((_b = this.config.preChatForm) == null ? void 0 : _b.enabled) ?? false,
932
1143
  branding: {
933
- name: ((_c = this.config.branding) == null ? void 0 : _c.name) ?? "AI Assistant",
1144
+ name: ((_c = this.config.branding) == null ? void 0 : _c.name) ?? this.locale.defaultName,
934
1145
  avatar: (_d = this.config.branding) == null ? void 0 : _d.avatar,
935
- subtitle: ((_e = this.config.branding) == null ? void 0 : _e.subtitle) ?? "Online"
1146
+ subtitle: ((_e = this.config.branding) == null ? void 0 : _e.subtitle) ?? this.locale.defaultSubtitle
936
1147
  },
937
1148
  placeholder: this.config.placeholder ?? this.locale.inputPlaceholder,
938
1149
  footerText: this.locale.poweredBy,
1150
+ closeLabel: this.locale.closeButton,
1151
+ sendLabel: this.locale.sendButton,
939
1152
  messageActions: {
940
1153
  onRetry: (messageId) => {
941
1154
  void this.handleRetry(messageId);
@@ -944,7 +1157,8 @@ class Widget {
944
1157
  void this.handleRegenerate(messageId);
945
1158
  },
946
1159
  retryLabel: this.locale.retryButton,
947
- regenerateLabel: this.locale.regenerateButton
1160
+ regenerateLabel: this.locale.regenerateButton,
1161
+ typingLabel: this.locale.typingIndicator
948
1162
  },
949
1163
  onSend: (text) => this.handleSend(text),
950
1164
  onClose: () => this.close()
@@ -959,7 +1173,13 @@ class Widget {
959
1173
  }
960
1174
  }
961
1175
  }
962
- const shouldShowPreChat = ((_f = this.config.preChatForm) == null ? void 0 : _f.enabled) && !this.isPreChatCompleted() && this.messages.length === 0;
1176
+ if ((_f = this.config.preChatForm) == null ? void 0 : _f.enabled) {
1177
+ const savedUserData = this.storage.getPreChatData(this.conversationId);
1178
+ if (savedUserData) {
1179
+ this.userData = savedUserData;
1180
+ }
1181
+ }
1182
+ const shouldShowPreChat = ((_g = this.config.preChatForm) == null ? void 0 : _g.enabled) && !this.isPreChatCompleted() && this.messages.length === 0;
963
1183
  if (shouldShowPreChat) {
964
1184
  this.showPreChatForm();
965
1185
  } else {
@@ -972,6 +1192,7 @@ class Widget {
972
1192
  delay: this.config.welcomeBubble.delay,
973
1193
  showOnce: this.config.welcomeBubble.showOnce ?? true,
974
1194
  position,
1195
+ dismissLabel: this.locale.dismissBubble,
975
1196
  onClick: () => this.open()
976
1197
  });
977
1198
  }
@@ -995,12 +1216,14 @@ class Widget {
995
1216
  this.emit("open");
996
1217
  }
997
1218
  close() {
998
- var _a, _b, _c;
1219
+ var _a, _b, _c, _d;
1220
+ const hadFocus = this.panel.containsFocus();
999
1221
  this.panel.hide();
1000
1222
  if (!this.isInline) {
1001
1223
  (_a = this.fab) == null ? void 0 : _a.setOpen(false);
1224
+ if (hadFocus) (_b = this.fab) == null ? void 0 : _b.focus();
1002
1225
  }
1003
- (_c = (_b = this.config).onClose) == null ? void 0 : _c.call(_b);
1226
+ (_d = (_c = this.config).onClose) == null ? void 0 : _d.call(_c);
1004
1227
  this.emit("close");
1005
1228
  }
1006
1229
  toggle() {
@@ -1020,15 +1243,18 @@ class Widget {
1020
1243
  (_c = this.bubble) == null ? void 0 : _c.destroy();
1021
1244
  this.panel.destroy();
1022
1245
  (_d = this.fab) == null ? void 0 : _d.destroy();
1023
- destroyShadowRoot(this.containerEl);
1246
+ destroyShadowRoot(this.shadow);
1247
+ this.namespace.release();
1024
1248
  this.eventHandlers.clear();
1025
1249
  this.messages = [];
1250
+ this.userData = void 0;
1026
1251
  }
1027
1252
  on(event, handler) {
1028
1253
  if (!this.eventHandlers.has(event)) {
1029
1254
  this.eventHandlers.set(event, /* @__PURE__ */ new Set());
1030
1255
  }
1031
1256
  this.eventHandlers.get(event).add(handler);
1257
+ return () => this.off(event, handler);
1032
1258
  }
1033
1259
  off(event, handler) {
1034
1260
  var _a;
@@ -1042,8 +1268,8 @@ class Widget {
1042
1268
  isPreChatCompleted() {
1043
1269
  return this.storage.isPreChatCompleted(this.conversationId);
1044
1270
  }
1045
- setPreChatCompleted() {
1046
- this.storage.setPreChatCompleted(this.conversationId);
1271
+ setPreChatCompleted(data) {
1272
+ this.storage.setPreChatCompleted(this.conversationId, data);
1047
1273
  }
1048
1274
  showPreChatForm() {
1049
1275
  const formConfig = this.config.preChatForm;
@@ -1059,15 +1285,16 @@ class Widget {
1059
1285
  });
1060
1286
  }
1061
1287
  handlePreChatSubmit(data) {
1062
- var _a, _b, _c;
1288
+ var _a, _b, _c, _d;
1289
+ if (!((_a = this.config.preChatForm) == null ? void 0 : _a.enabled)) return;
1063
1290
  this.userData = data;
1064
- this.setPreChatCompleted();
1065
- (_a = this.preChatFormEl) == null ? void 0 : _a.destroy();
1291
+ this.setPreChatCompleted(data);
1292
+ (_b = this.preChatFormEl) == null ? void 0 : _b.destroy();
1066
1293
  this.preChatFormEl = void 0;
1067
1294
  this.panel.showMessages();
1068
1295
  this.panel.input.setVisible(true);
1069
1296
  this.showChatView();
1070
- (_c = (_b = this.config).onPreChatSubmit) == null ? void 0 : _c.call(_b, data);
1297
+ (_d = (_c = this.config).onPreChatSubmit) == null ? void 0 : _d.call(_c, data);
1071
1298
  this.emit("preChatSubmit", data);
1072
1299
  if (data.message) {
1073
1300
  this.handleSend(data.message);
@@ -1077,7 +1304,8 @@ class Widget {
1077
1304
  var _a;
1078
1305
  if (this.messages.length === 0) {
1079
1306
  const userName = (_a = this.userData) == null ? void 0 : _a.name;
1080
- const welcomeText = userName ? `Hi ${userName}! ${this.config.welcomeMessage ?? this.locale.welcomeMessage}` : this.config.welcomeMessage ?? this.locale.welcomeMessage;
1307
+ const greeting = this.locale.greetingWithName.replace("{name}", () => userName);
1308
+ const welcomeText = userName ? `${greeting} ${this.config.welcomeMessage ?? this.locale.welcomeMessage}` : this.config.welcomeMessage ?? this.locale.welcomeMessage;
1081
1309
  const welcomeMsg = {
1082
1310
  id: "welcome",
1083
1311
  role: "assistant",
@@ -1154,7 +1382,6 @@ class Widget {
1154
1382
  content: "",
1155
1383
  status: "streaming"
1156
1384
  };
1157
- const includeUserData = !!(this.userData && !this.userDataSent);
1158
1385
  let firstChunk = true;
1159
1386
  let requestCompleted = false;
1160
1387
  let hasError = false;
@@ -1162,14 +1389,13 @@ class Widget {
1162
1389
  this.abortController = abortController;
1163
1390
  try {
1164
1391
  for await (const chunk of this.client.sendMessage(
1165
- this.createRequest(text, options, includeUserData),
1392
+ this.createRequest(text, options),
1166
1393
  { signal: abortController.signal }
1167
1394
  )) {
1168
1395
  if (this.destroyed) break;
1169
1396
  if (chunk.error) {
1170
1397
  const errorType = this.resolveErrorType(chunk.error);
1171
- assistantMsg.content = this.getErrorMessage(errorType);
1172
- assistantMsg.status = "error";
1398
+ this.markFailed(assistantMsg, this.getErrorMessage(errorType));
1173
1399
  assistantMsg.errorType = errorType;
1174
1400
  hasError = true;
1175
1401
  this.retryRequests.set(assistantMsg.id, {
@@ -1226,15 +1452,18 @@ class Widget {
1226
1452
  }
1227
1453
  } catch (err) {
1228
1454
  if (this.destroyed) return;
1229
- assistantMsg.content = this.locale.errorGeneric;
1230
- assistantMsg.status = "error";
1455
+ this.markFailed(assistantMsg, this.locale.errorGeneric);
1231
1456
  assistantMsg.errorType = "network";
1232
1457
  this.retryRequests.set(assistantMsg.id, {
1233
1458
  regenerate: options.regenerate,
1234
1459
  userMessageId: options.userMessageId
1235
1460
  });
1236
1461
  this.panel.messages.removeTyping();
1237
- this.panel.addMessage(assistantMsg);
1462
+ if (firstChunk) {
1463
+ this.panel.addMessage(assistantMsg);
1464
+ } else {
1465
+ this.panel.messages.updateMessage(assistantMsg);
1466
+ }
1238
1467
  this.messages.push(assistantMsg);
1239
1468
  const error = err instanceof Error ? err : new Error(String(err));
1240
1469
  (_i = (_h = this.config).onError) == null ? void 0 : _i.call(_h, error);
@@ -1244,9 +1473,6 @@ class Widget {
1244
1473
  this.abortController = void 0;
1245
1474
  }
1246
1475
  if (!this.destroyed) {
1247
- if (includeUserData && requestCompleted) {
1248
- this.userDataSent = true;
1249
- }
1250
1476
  this.isStreaming = false;
1251
1477
  this.panel.input.setDisabled(false);
1252
1478
  this.panel.input.focus();
@@ -1254,8 +1480,10 @@ class Widget {
1254
1480
  }
1255
1481
  }
1256
1482
  }
1257
- createRequest(text, options, includeUserData) {
1483
+ createRequest(text, options) {
1484
+ var _a;
1258
1485
  const pageContext = this.config.pageContext !== false ? { url: window.location.href, title: document.title } : void 0;
1486
+ const includeUserData = Boolean(((_a = this.config.preChatForm) == null ? void 0 : _a.enabled) && this.userData);
1259
1487
  return {
1260
1488
  conversationId: this.conversationId,
1261
1489
  message: text,
@@ -1306,6 +1534,18 @@ class Widget {
1306
1534
  return "provider_error";
1307
1535
  }
1308
1536
  }
1537
+ /**
1538
+ * Marks a reply as failed. Text that already streamed stays, and the error
1539
+ * shows as a note under it; an empty reply shows the error as its content.
1540
+ */
1541
+ markFailed(message, errorMessage) {
1542
+ message.status = "error";
1543
+ if (message.content.trim()) {
1544
+ message.errorText = errorMessage;
1545
+ } else {
1546
+ message.content = errorMessage;
1547
+ }
1548
+ }
1309
1549
  getErrorMessage(errorType) {
1310
1550
  switch (errorType) {
1311
1551
  case "rate_limit":
@@ -1336,6 +1576,7 @@ class Widget {
1336
1576
  }
1337
1577
  }
1338
1578
  let instance = null;
1579
+ const persistentHandlers = /* @__PURE__ */ new Map();
1339
1580
  function parseAutoOpen(value) {
1340
1581
  if (value === void 0) return void 0;
1341
1582
  if (value === "" || value === "true") return true;
@@ -1349,6 +1590,11 @@ const ChatCops = {
1349
1590
  instance.destroy();
1350
1591
  }
1351
1592
  instance = new Widget(config);
1593
+ for (const [event, handlers] of persistentHandlers) {
1594
+ for (const handler of handlers) {
1595
+ instance.on(event, handler);
1596
+ }
1597
+ }
1352
1598
  instance.init();
1353
1599
  return instance;
1354
1600
  },
@@ -1363,9 +1609,20 @@ const ChatCops = {
1363
1609
  instance = null;
1364
1610
  },
1365
1611
  on(event, handler) {
1612
+ let handlers = persistentHandlers.get(event);
1613
+ if (!handlers) {
1614
+ handlers = /* @__PURE__ */ new Set();
1615
+ persistentHandlers.set(event, handlers);
1616
+ }
1617
+ handlers.add(handler);
1366
1618
  instance == null ? void 0 : instance.on(event, handler);
1619
+ return () => {
1620
+ ChatCops.off(event, handler);
1621
+ };
1367
1622
  },
1368
1623
  off(event, handler) {
1624
+ var _a;
1625
+ (_a = persistentHandlers.get(event)) == null ? void 0 : _a.delete(handler);
1369
1626
  instance == null ? void 0 : instance.off(event, handler);
1370
1627
  }
1371
1628
  };
@@ -1405,7 +1662,7 @@ function autoInit(script) {
1405
1662
  const currentScript = typeof document !== "undefined" ? document.currentScript : null;
1406
1663
  if (currentScript && currentScript.dataset.apiUrl) {
1407
1664
  if (document.readyState === "loading") {
1408
- document.addEventListener("DOMContentLoaded", () => autoInit(currentScript));
1665
+ document.addEventListener("DOMContentLoaded", () => autoInit(currentScript), { once: true });
1409
1666
  } else {
1410
1667
  autoInit(currentScript);
1411
1668
  }