@chatcops/widget 0.5.0 → 0.7.0

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