@chatcops/widget 0.6.0 → 0.7.0

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