@chatcops/widget 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
- const widgetStyles = ":host{--cc-accent: #6366f1;--cc-accent-hover: #4f46e5;--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-font: system-ui, -apple-system, sans-serif;--cc-radius: 12px;--cc-shadow: 0 8px 32px rgba(0, 0, 0, .4);--cc-fab-size: 56px;--cc-panel-width: 400px;--cc-panel-height: 560px;all:initial;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);-webkit-font-smoothing:antialiased}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}.cc-fab{position:fixed;bottom:20px;right:20px;width:var(--cc-fab-size);height:var(--cc-fab-size);border-radius:50%;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #0000004d;transition:transform .2s ease,box-shadow .2s ease;z-index:10000;outline:none}.cc-fab:hover{transform:scale(1.05);box-shadow:0 6px 24px #0006}.cc-fab:active{transform:scale(.95)}.cc-fab.cc-left{right:auto;left:20px}.cc-fab svg{width:24px;height:24px;fill:var(--cc-text);transition:transform .3s ease}.cc-fab.cc-open svg{transform:rotate(90deg)}.cc-fab-badge{position:absolute;top:-2px;right:-2px;width:12px;height:12px;border-radius:50%;background:#ef4444;border:2px solid var(--cc-bg);display:none}.cc-fab-badge.cc-visible{display:block}@keyframes cc-pulse{0%,to{box-shadow:0 4px 16px #0000004d}50%{box-shadow:0 4px 16px #6366f180}}.cc-fab.cc-pulse{animation:cc-pulse 2s ease-in-out 3}.cc-panel{position:fixed;bottom:88px;right:20px;width:var(--cc-panel-width);height:var(--cc-panel-height);max-height:calc(100vh - 100px);background:var(--cc-bg);border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);display:flex;flex-direction:column;overflow:hidden;z-index:9999;opacity:0;transform:translateY(16px) scale(.95);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.cc-panel.cc-visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.cc-panel.cc-left{right:auto;left:20px}.cc-panel.cc-inline{position:relative;bottom:auto;right:auto;left:auto;width:100%;height:100%;max-height:100%;z-index:auto;opacity:1;transform:none;pointer-events:auto;transition:none}.cc-panel.cc-inline.cc-visible{transform:none}.cc-header{display:flex;align-items:center;padding:16px;background:var(--cc-bg-surface);border-bottom:1px solid var(--cc-border);flex-shrink:0}.cc-header-avatar{width:36px;height:36px;border-radius:50%;margin-right:12px;object-fit:cover}.cc-header-avatar-placeholder{width:36px;height:36px;border-radius:50%;margin-right:12px;background:var(--cc-accent);display:flex;align-items:center;justify-content:center}.cc-header-avatar-placeholder svg{width:20px;height:20px;fill:var(--cc-text)}.cc-header-info{flex:1}.cc-header-name{font-size:15px;font-weight:600;color:var(--cc-text)}.cc-header-subtitle{font-size:12px;color:var(--cc-text-secondary)}.cc-header-close{width:32px;height:32px;border-radius:8px;background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.cc-header-close:hover{background:var(--cc-bg-input)}.cc-header-close svg{width:16px;height:16px;fill:var(--cc-text-secondary)}.cc-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.cc-messages::-webkit-scrollbar{width:4px}.cc-messages::-webkit-scrollbar-track{background:transparent}.cc-messages::-webkit-scrollbar-thumb{background:var(--cc-border);border-radius:2px}.cc-message{max-width:85%;padding:10px 14px;border-radius:var(--cc-radius);word-wrap:break-word;overflow-wrap:break-word;font-size:14px;line-height:1.5}.cc-message-user{align-self:flex-end;background:var(--cc-accent);color:var(--cc-text);border-bottom-right-radius:4px}.cc-message-assistant{align-self:flex-start;background:var(--cc-bg-surface);color:var(--cc-text);border-bottom-left-radius:4px}.cc-message a{color:var(--cc-accent);text-decoration:underline}.cc-message-user a{color:var(--cc-text)}.cc-message code{background:#0003;padding:1px 4px;border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:13px}.cc-message pre{background:#0000004d;padding:8px 12px;border-radius:8px;overflow-x:auto;margin:4px 0}.cc-message pre code{background:transparent;padding:0}.cc-message ul,.cc-message ol{padding-left:20px;margin:4px 0}.cc-typing{display:flex;align-items:center;gap:4px;padding:10px 14px;align-self:flex-start;background:var(--cc-bg-surface);border-radius:var(--cc-radius);border-bottom-left-radius:4px}.cc-typing-dot{width:6px;height:6px;border-radius:50%;background:var(--cc-text-secondary);animation:cc-bounce 1.2s infinite}.cc-typing-dot:nth-child(2){animation-delay:.2s}.cc-typing-dot:nth-child(3){animation-delay:.4s}@keyframes cc-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-4px)}}.cc-input-area{display:flex;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--cc-border);background:var(--cc-bg);gap:8px}.cc-input{flex:1;background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;line-height:1.5;resize:none;outline:none;max-height:96px;overflow-y:auto;transition:border-color .15s ease}.cc-input:focus{border-color:var(--cc-accent);outline:2px solid var(--cc-accent);outline-offset:1px}.cc-input::placeholder{color:var(--cc-text-secondary)}.cc-send-btn{width:36px;height:36px;border-radius:8px;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s ease,opacity .15s ease}.cc-send-btn:hover{background:var(--cc-accent-hover)}.cc-send-btn:disabled{opacity:.4;cursor:not-allowed}.cc-send-btn svg{width:16px;height:16px;fill:var(--cc-text)}.cc-footer{padding:6px 16px;text-align:center;font-size:11px;color:var(--cc-text-secondary);border-top:1px solid var(--cc-border)}.cc-footer a{color:var(--cc-text-secondary);text-decoration:none}.cc-footer a:hover{text-decoration:underline}.cc-bubble{position:fixed;bottom:84px;right:20px;background:var(--cc-bg-surface);color:var(--cc-text);padding:10px 16px;border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);max-width:260px;font-size:13px;line-height:1.4;z-index:9998;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .3s ease,transform .3s ease;cursor:pointer}.cc-bubble.cc-visible{opacity:1;transform:translateY(0);pointer-events:auto}.cc-bubble.cc-left{right:auto;left:20px}.cc-bubble-close{position:absolute;top:-8px;right:-8px;width:20px;height:20px;border-radius:50%;background:var(--cc-bg);border:1px solid var(--cc-border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--cc-text-secondary)}@media(max-width:768px){.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}}";
4
+ const widgetStyles = ":host{--cc-accent: #6366f1;--cc-accent-hover: #4f46e5;--cc-text: #ffffff;--cc-bg: #0a0a0a;--cc-bg-surface: #1a1a1a;--cc-bg-input: #2a2a2a;--cc-border: #333333;--cc-text-secondary: #a0a0a0;--cc-font: system-ui, -apple-system, sans-serif;--cc-radius: 12px;--cc-shadow: 0 8px 32px rgba(0, 0, 0, .4);--cc-fab-size: 56px;--cc-panel-width: 400px;--cc-panel-height: 560px;all:initial;font-family:var(--cc-font);font-size:14px;line-height:1.5;color:var(--cc-text);-webkit-font-smoothing:antialiased}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}.cc-fab{position:fixed;bottom:20px;right:20px;width:var(--cc-fab-size);height:var(--cc-fab-size);border-radius:50%;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 16px #0000004d;transition:transform .2s ease,box-shadow .2s ease;z-index:10000;outline:none}.cc-fab:hover{transform:scale(1.05);box-shadow:0 6px 24px #0006}.cc-fab:active{transform:scale(.95)}.cc-fab.cc-left{right:auto;left:20px}.cc-fab svg{width:24px;height:24px;fill:var(--cc-text);transition:transform .3s ease}.cc-fab.cc-open svg{transform:rotate(90deg)}.cc-fab-badge{position:absolute;top:-2px;right:-2px;width:12px;height:12px;border-radius:50%;background:#ef4444;border:2px solid var(--cc-bg);display:none}.cc-fab-badge.cc-visible{display:block}@keyframes cc-pulse{0%,to{box-shadow:0 4px 16px #0000004d}50%{box-shadow:0 4px 16px #6366f180}}.cc-fab.cc-pulse{animation:cc-pulse 2s ease-in-out 3}.cc-panel{position:fixed;bottom:88px;right:20px;width:var(--cc-panel-width);height:var(--cc-panel-height);max-height:calc(100vh - 100px);background:var(--cc-bg);border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);display:flex;flex-direction:column;overflow:hidden;z-index:9999;opacity:0;transform:translateY(16px) scale(.95);pointer-events:none;transition:opacity .2s ease,transform .2s ease}.cc-panel.cc-visible{opacity:1;transform:translateY(0) scale(1);pointer-events:auto}.cc-panel.cc-left{right:auto;left:20px}.cc-panel.cc-inline{position:relative;bottom:auto;right:auto;left:auto;width:100%;height:100%;max-height:100%;z-index:auto;opacity:1;transform:none;pointer-events:auto;transition:none}.cc-panel.cc-inline.cc-visible{transform:none}.cc-header{display:flex;align-items:center;padding:16px;background:var(--cc-bg-surface);border-bottom:1px solid var(--cc-border);flex-shrink:0}.cc-header-avatar{width:36px;height:36px;border-radius:50%;margin-right:12px;object-fit:cover}.cc-header-avatar-placeholder{width:36px;height:36px;border-radius:50%;margin-right:12px;background:var(--cc-accent);display:flex;align-items:center;justify-content:center}.cc-header-avatar-placeholder svg{width:20px;height:20px;fill:var(--cc-text)}.cc-header-info{flex:1}.cc-header-name{font-size:15px;font-weight:600;color:var(--cc-text)}.cc-header-subtitle{font-size:12px;color:var(--cc-text-secondary)}.cc-header-close{width:32px;height:32px;border-radius:8px;background:transparent;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:background .15s ease}.cc-header-close:hover{background:var(--cc-bg-input)}.cc-header-close svg{width:16px;height:16px;fill:var(--cc-text-secondary)}.cc-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:12px}.cc-messages::-webkit-scrollbar{width:4px}.cc-messages::-webkit-scrollbar-track{background:transparent}.cc-messages::-webkit-scrollbar-thumb{background:var(--cc-border);border-radius:2px}.cc-message{max-width:85%;padding:10px 14px;border-radius:var(--cc-radius);word-wrap:break-word;overflow-wrap:break-word;font-size:14px;line-height:1.5;display:flex;flex-direction:column;gap:8px}.cc-message-user{align-self:flex-end;background:var(--cc-accent);color:var(--cc-text);border-bottom-right-radius:4px}.cc-message-assistant{align-self:flex-start;background:var(--cc-bg-surface);color:var(--cc-text);border-bottom-left-radius:4px}.cc-message a{color:var(--cc-accent);text-decoration:underline}.cc-message-user a{color:var(--cc-text)}.cc-message code{background:#0003;padding:1px 4px;border-radius:4px;font-family:SF Mono,Cascadia Code,Consolas,monospace;font-size:13px}.cc-message pre{background:#0000004d;padding:8px 12px;border-radius:8px;overflow-x:auto;margin:4px 0}.cc-message pre code{background:transparent;padding:0}.cc-message ul,.cc-message ol{padding-left:20px;margin:4px 0}.cc-message-actions{display:flex;align-items:center}.cc-message-retry,.cc-message-regenerate{display:inline-flex;align-items:center;gap:4px;padding:4px 10px;background:transparent;border:1px solid var(--cc-border);border-radius:6px;color:var(--cc-text-secondary);font-family:var(--cc-font);font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,opacity .15s ease}.cc-message-retry:hover,.cc-message-regenerate:hover{background:var(--cc-bg-input);color:var(--cc-text)}.cc-message-retry svg,.cc-message-regenerate svg{fill:currentColor}.cc-message-regenerate{opacity:0}.cc-message:hover .cc-message-regenerate{opacity:1}.cc-message-retry{border-color:#ef4444;color:#ef4444}.cc-message-retry:hover{background:#ef44441a;color:#ef4444}.cc-typing{display:flex;align-items:center;gap:4px;padding:10px 14px;align-self:flex-start;background:var(--cc-bg-surface);border-radius:var(--cc-radius);border-bottom-left-radius:4px}.cc-typing-dot{width:6px;height:6px;border-radius:50%;background:var(--cc-text-secondary);animation:cc-bounce 1.2s infinite}.cc-typing-dot:nth-child(2){animation-delay:.2s}.cc-typing-dot:nth-child(3){animation-delay:.4s}@keyframes cc-bounce{0%,60%,to{transform:translateY(0)}30%{transform:translateY(-4px)}}.cc-input-area{display:flex;align-items:flex-end;padding:12px 16px;border-top:1px solid var(--cc-border);background:var(--cc-bg);gap:8px}.cc-input{flex:1;background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;line-height:1.5;resize:none;outline:none;max-height:96px;overflow-y:auto;transition:border-color .15s ease}.cc-input:focus{border-color:var(--cc-accent);outline:2px solid var(--cc-accent);outline-offset:1px}.cc-input::placeholder{color:var(--cc-text-secondary)}.cc-send-btn{width:36px;height:36px;border-radius:8px;background:var(--cc-accent);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s ease,opacity .15s ease}.cc-send-btn:hover{background:var(--cc-accent-hover)}.cc-send-btn:disabled{opacity:.4;cursor:not-allowed}.cc-send-btn svg{width:16px;height:16px;fill:var(--cc-text)}.cc-footer{padding:6px 16px;text-align:center;font-size:11px;color:var(--cc-text-secondary);border-top:1px solid var(--cc-border)}.cc-footer a{color:var(--cc-text-secondary);text-decoration:none}.cc-footer a:hover{text-decoration:underline}.cc-bubble{position:fixed;bottom:84px;right:20px;background:var(--cc-bg-surface);color:var(--cc-text);padding:10px 16px;border-radius:var(--cc-radius);box-shadow:var(--cc-shadow);border:1px solid var(--cc-border);max-width:260px;font-size:13px;line-height:1.4;z-index:9998;opacity:0;transform:translateY(8px);pointer-events:none;transition:opacity .3s ease,transform .3s ease;cursor:pointer}.cc-bubble.cc-visible{opacity:1;transform:translateY(0);pointer-events:auto}.cc-bubble.cc-left{right:auto;left:20px}.cc-bubble-close{position:absolute;top:-8px;right:-8px;width:20px;height:20px;border-radius:50%;background:var(--cc-bg);border:1px solid var(--cc-border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:10px;color:var(--cc-text-secondary)}.cc-hidden{display:none}.cc-messages-wrapper{flex:1;display:flex;flex-direction:column;min-height:0}.cc-prechat{flex:1;overflow-y:auto;padding:24px 16px;display:flex;flex-direction:column;gap:16px}.cc-prechat-title{font-size:18px;font-weight:600;color:var(--cc-text)}.cc-prechat-subtitle{font-size:13px;color:var(--cc-text-secondary);margin-top:-8px}.cc-prechat-fields{display:flex;flex-direction:column;gap:14px}.cc-prechat-field{display:flex;flex-direction:column;gap:4px}.cc-prechat-label{font-size:13px;font-weight:500;color:var(--cc-text)}.cc-prechat-label .cc-required{color:#ef4444;margin-left:2px}.cc-prechat-input,.cc-prechat-select,.cc-prechat-textarea{background:var(--cc-bg-input);border:1px solid var(--cc-border);border-radius:8px;padding:8px 12px;color:var(--cc-text);font-family:var(--cc-font);font-size:14px;outline:none;transition:border-color .15s ease}.cc-prechat-input:focus,.cc-prechat-select:focus,.cc-prechat-textarea:focus{border-color:var(--cc-accent)}.cc-prechat-select{-webkit-appearance:auto;-moz-appearance:auto;appearance:auto}.cc-prechat-textarea{resize:none;line-height:1.5}.cc-prechat-error{font-size:12px;color:#ef4444;min-height:0}.cc-prechat-error:empty{display:none}.cc-prechat-submit{background:var(--cc-accent);color:var(--cc-text);border:none;border-radius:8px;padding:10px 16px;font-family:var(--cc-font);font-size:14px;font-weight:500;cursor:pointer;transition:background .15s ease;margin-top:8px}.cc-prechat-submit:hover{background:var(--cc-accent-hover)}.cc-prechat-submit:disabled{opacity:.5;cursor:not-allowed}@media(max-width:768px){.cc-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}}";
5
5
  function createShadowRoot(container) {
6
6
  const host = document.createElement("div");
7
7
  host.id = "chatcops-root";
@@ -71,6 +71,12 @@ const ESCAPE_MAP = {
71
71
  function escapeHtml(text) {
72
72
  return text.replace(/[&<>"']/g, (ch) => ESCAPE_MAP[ch] ?? ch);
73
73
  }
74
+ const SAFE_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "mailto", "tel"]);
75
+ function isSafeHref(href) {
76
+ const normalized = href.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, "");
77
+ const scheme = /^([^:/?#]+):/.exec(normalized);
78
+ return !scheme || SAFE_SCHEMES.has(scheme[1].toLowerCase());
79
+ }
74
80
  function renderMarkdown(input) {
75
81
  let html = escapeHtml(input);
76
82
  html = html.replace(/```(?:\w*)\n?([\s\S]*?)```/g, (_match, code) => {
@@ -81,7 +87,7 @@ function renderMarkdown(input) {
81
87
  html = html.replace(/\*(.+?)\*/g, "<em>$1</em>");
82
88
  html = html.replace(
83
89
  /\[([^\]]+)\]\(((?:[^()]*|\([^()]*\))*)\)/g,
84
- '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
90
+ (_match, text, href) => isSafeHref(href) ? `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>` : text
85
91
  );
86
92
  html = html.replace(/^- (.+)$/gm, "<li>$1</li>");
87
93
  html = html.replace(/(<li>.*<\/li>\n?)+/g, (match) => `<ul>${match}</ul>`);
@@ -90,28 +96,29 @@ function renderMarkdown(input) {
90
96
  html = html.replace(/\n/g, "<br>");
91
97
  return html;
92
98
  }
99
+ 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>';
100
+ 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>';
93
101
  class Messages {
94
- constructor(parent) {
102
+ constructor(parent, actions) {
95
103
  __publicField(this, "container");
96
104
  __publicField(this, "typingEl", null);
105
+ __publicField(this, "actions");
106
+ this.actions = actions;
97
107
  this.container = document.createElement("div");
98
108
  this.container.className = "cc-messages";
99
109
  parent.appendChild(this.container);
100
110
  }
101
111
  addMessage(msg) {
102
112
  this.removeTyping();
103
- const el = document.createElement("div");
104
- el.className = `cc-message cc-message-${msg.role}`;
105
- el.dataset.id = msg.id;
106
- el.innerHTML = msg.role === "assistant" ? renderMarkdown(msg.content) : this.escapeHtml(msg.content);
113
+ const el = this.createMessageElement(msg);
107
114
  this.container.appendChild(el);
108
115
  this.scrollToBottom();
109
116
  return el;
110
117
  }
111
- updateMessage(id, content, isAssistant) {
112
- const el = this.container.querySelector(`[data-id="${id}"]`);
118
+ updateMessage(msg) {
119
+ const el = this.container.querySelector(`[data-id="${msg.id}"]`);
113
120
  if (el) {
114
- el.innerHTML = isAssistant ? renderMarkdown(content) : this.escapeHtml(content);
121
+ this.renderMessageElement(el, msg);
115
122
  this.scrollToBottom();
116
123
  }
117
124
  }
@@ -133,9 +140,108 @@ class Messages {
133
140
  this.container.innerHTML = "";
134
141
  this.typingEl = null;
135
142
  }
143
+ removeMessage(id) {
144
+ const el = this.container.querySelector(`[data-id="${id}"]`);
145
+ el == null ? void 0 : el.remove();
146
+ }
147
+ clearRegenerateButtons() {
148
+ this.container.querySelectorAll(".cc-message-regenerate").forEach((button) => button.remove());
149
+ this.cleanupEmptyActionContainers();
150
+ }
151
+ showRegenerateButton(messageId) {
152
+ var _a;
153
+ this.clearRegenerateButtons();
154
+ if (!((_a = this.actions) == null ? void 0 : _a.onRegenerate)) return;
155
+ const msgEl = this.container.querySelector(`[data-id="${messageId}"]`);
156
+ if (!msgEl || !msgEl.classList.contains("cc-message-assistant")) return;
157
+ const actions = this.ensureActionsContainer(msgEl);
158
+ actions.appendChild(this.createActionButton({
159
+ className: "cc-message-regenerate",
160
+ label: this.actions.regenerateLabel ?? "Regenerate",
161
+ icon: REGENERATE_ICON,
162
+ onClick: () => {
163
+ var _a2, _b;
164
+ return (_b = (_a2 = this.actions) == null ? void 0 : _a2.onRegenerate) == null ? void 0 : _b.call(_a2, messageId);
165
+ }
166
+ }));
167
+ }
168
+ setVisible(visible) {
169
+ this.container.classList.toggle("cc-hidden", !visible);
170
+ }
136
171
  scrollToBottom() {
137
172
  this.container.scrollTop = this.container.scrollHeight;
138
173
  }
174
+ createMessageElement(msg) {
175
+ const el = document.createElement("div");
176
+ this.renderMessageElement(el, msg);
177
+ return el;
178
+ }
179
+ renderMessageElement(el, msg) {
180
+ el.className = `cc-message cc-message-${msg.role}`;
181
+ el.dataset.id = msg.id;
182
+ if (msg.status) {
183
+ el.dataset.status = msg.status;
184
+ } else {
185
+ delete el.dataset.status;
186
+ }
187
+ if (msg.errorType) {
188
+ el.dataset.errorType = msg.errorType;
189
+ } else {
190
+ delete el.dataset.errorType;
191
+ }
192
+ el.innerHTML = "";
193
+ const content = document.createElement("div");
194
+ content.className = "cc-message-content";
195
+ content.innerHTML = msg.role === "assistant" ? renderMarkdown(msg.content) : this.escapeHtml(msg.content);
196
+ el.appendChild(content);
197
+ const actions = this.createActionsContainer(msg);
198
+ if (actions) {
199
+ el.appendChild(actions);
200
+ }
201
+ }
202
+ createActionsContainer(msg) {
203
+ var _a;
204
+ if (msg.status !== "error" || !((_a = this.actions) == null ? void 0 : _a.onRetry)) {
205
+ return null;
206
+ }
207
+ const actions = document.createElement("div");
208
+ actions.className = "cc-message-actions";
209
+ actions.appendChild(this.createActionButton({
210
+ className: "cc-message-retry",
211
+ label: this.actions.retryLabel ?? "Retry",
212
+ icon: RETRY_ICON,
213
+ onClick: () => {
214
+ var _a2, _b;
215
+ return (_b = (_a2 = this.actions) == null ? void 0 : _a2.onRetry) == null ? void 0 : _b.call(_a2, msg.id);
216
+ }
217
+ }));
218
+ return actions;
219
+ }
220
+ ensureActionsContainer(msgEl) {
221
+ const existing = msgEl.querySelector(".cc-message-actions");
222
+ if (existing) {
223
+ return existing;
224
+ }
225
+ const actions = document.createElement("div");
226
+ actions.className = "cc-message-actions";
227
+ msgEl.appendChild(actions);
228
+ return actions;
229
+ }
230
+ cleanupEmptyActionContainers() {
231
+ this.container.querySelectorAll(".cc-message-actions").forEach((container) => {
232
+ if (!container.hasChildNodes()) {
233
+ container.remove();
234
+ }
235
+ });
236
+ }
237
+ createActionButton(options) {
238
+ const button = document.createElement("button");
239
+ button.type = "button";
240
+ button.className = options.className;
241
+ button.innerHTML = `${options.icon}<span>${this.escapeHtml(options.label)}</span>`;
242
+ button.addEventListener("click", options.onClick);
243
+ return button;
244
+ }
139
245
  escapeHtml(text) {
140
246
  const div = document.createElement("div");
141
247
  div.textContent = text;
@@ -182,6 +288,9 @@ class Input {
182
288
  this.sendBtn.disabled = true;
183
289
  this.autoResize();
184
290
  }
291
+ setVisible(visible) {
292
+ this.area.classList.toggle("cc-hidden", !visible);
293
+ }
185
294
  send() {
186
295
  const text = this.textarea.value.trim();
187
296
  if (!text) return;
@@ -208,6 +317,7 @@ class Panel {
208
317
  __publicField(this, "inline");
209
318
  __publicField(this, "messages");
210
319
  __publicField(this, "input");
320
+ __publicField(this, "messagesContainer");
211
321
  this.inline = options.inline ?? false;
212
322
  this.el = document.createElement("div");
213
323
  this.el.className = "cc-panel";
@@ -244,7 +354,14 @@ class Panel {
244
354
  header.appendChild(closeBtn);
245
355
  }
246
356
  this.el.appendChild(header);
247
- this.messages = new Messages(this.el);
357
+ if (options.preChatEnabled) {
358
+ this.messagesContainer = document.createElement("div");
359
+ this.messagesContainer.className = "cc-messages-wrapper";
360
+ this.el.appendChild(this.messagesContainer);
361
+ } else {
362
+ this.messagesContainer = this.el;
363
+ }
364
+ this.messages = new Messages(this.messagesContainer, options.messageActions);
248
365
  this.input = new Input(this.el, {
249
366
  placeholder: options.placeholder,
250
367
  onSend: options.onSend
@@ -270,6 +387,12 @@ class Panel {
270
387
  addMessage(msg) {
271
388
  return this.messages.addMessage(msg);
272
389
  }
390
+ hideMessages() {
391
+ this.messages.setVisible(false);
392
+ }
393
+ showMessages() {
394
+ this.messages.setVisible(true);
395
+ }
273
396
  destroy() {
274
397
  this.el.remove();
275
398
  }
@@ -408,7 +531,11 @@ class ConversationStorage {
408
531
  }
409
532
  save(conversation) {
410
533
  try {
411
- localStorage.setItem(this.key, JSON.stringify(conversation));
534
+ const filtered = {
535
+ ...conversation,
536
+ messages: conversation.messages.filter((m) => m.status !== "error").map(({ status, ...message }) => message)
537
+ };
538
+ localStorage.setItem(this.key, JSON.stringify(filtered));
412
539
  } catch {
413
540
  }
414
541
  }
@@ -431,6 +558,19 @@ class ConversationStorage {
431
558
  return crypto.randomUUID();
432
559
  }
433
560
  }
561
+ isPreChatCompleted(sessionId) {
562
+ try {
563
+ return sessionStorage.getItem(`chatcops-prechat-${sessionId}`) === "true";
564
+ } catch {
565
+ return false;
566
+ }
567
+ }
568
+ setPreChatCompleted(sessionId) {
569
+ try {
570
+ sessionStorage.setItem(`chatcops-prechat-${sessionId}`, "true");
571
+ } catch {
572
+ }
573
+ }
434
574
  }
435
575
  function applyTheme(root, theme) {
436
576
  const host = root.host;
@@ -483,13 +623,174 @@ const en = {
483
623
  typingIndicator: "Typing...",
484
624
  poweredBy: "Powered by ChatCops",
485
625
  newConversation: "New conversation",
486
- welcomeBubbleDefault: "Need help? Chat with us!"
626
+ welcomeBubbleDefault: "Need help? Chat with us!",
627
+ preChatTitle: "Before we start...",
628
+ preChatSubtitle: "Tell us a bit about yourself",
629
+ preChatSubmit: "Start Chat",
630
+ preChatRequired: "This field is required",
631
+ preChatInvalidEmail: "Please enter a valid email",
632
+ retryButton: "Retry",
633
+ regenerateButton: "Regenerate"
487
634
  };
488
635
  const locales = { en };
489
636
  function getWidgetLocale(code, overrides) {
490
637
  const base = locales[code] ?? en;
491
638
  return overrides ? { ...base, ...overrides } : base;
492
639
  }
640
+ class PreChatForm {
641
+ constructor(parent, options) {
642
+ __publicField(this, "container");
643
+ __publicField(this, "fieldElements", /* @__PURE__ */ new Map());
644
+ __publicField(this, "errorElements", /* @__PURE__ */ new Map());
645
+ __publicField(this, "submitBtn");
646
+ __publicField(this, "options");
647
+ this.options = options;
648
+ this.container = document.createElement("div");
649
+ this.container.className = "cc-prechat";
650
+ if (options.title) {
651
+ const title = document.createElement("div");
652
+ title.className = "cc-prechat-title";
653
+ title.textContent = options.title;
654
+ this.container.appendChild(title);
655
+ }
656
+ if (options.subtitle) {
657
+ const subtitle = document.createElement("div");
658
+ subtitle.className = "cc-prechat-subtitle";
659
+ subtitle.textContent = options.subtitle;
660
+ this.container.appendChild(subtitle);
661
+ }
662
+ const form = document.createElement("form");
663
+ form.className = "cc-prechat-fields";
664
+ form.addEventListener("submit", (e) => {
665
+ e.preventDefault();
666
+ this.handleSubmit();
667
+ });
668
+ for (const field of options.fields) {
669
+ form.appendChild(this.createField(field));
670
+ }
671
+ this.submitBtn = document.createElement("button");
672
+ this.submitBtn.type = "submit";
673
+ this.submitBtn.className = "cc-prechat-submit";
674
+ this.submitBtn.textContent = options.submitLabel;
675
+ form.appendChild(this.submitBtn);
676
+ this.container.appendChild(form);
677
+ parent.appendChild(this.container);
678
+ this.updateSubmitState();
679
+ const firstInput = this.fieldElements.values().next().value;
680
+ if (firstInput) {
681
+ setTimeout(() => firstInput.focus(), 100);
682
+ }
683
+ }
684
+ createField(field) {
685
+ const wrapper = document.createElement("div");
686
+ wrapper.className = "cc-prechat-field";
687
+ const label = document.createElement("label");
688
+ label.className = "cc-prechat-label";
689
+ label.textContent = field.label;
690
+ if (field.required) {
691
+ const req = document.createElement("span");
692
+ req.className = "cc-required";
693
+ req.textContent = "*";
694
+ label.appendChild(req);
695
+ }
696
+ wrapper.appendChild(label);
697
+ let input;
698
+ if (field.type === "select") {
699
+ const select = document.createElement("select");
700
+ select.className = "cc-prechat-select";
701
+ const defaultOpt = document.createElement("option");
702
+ defaultOpt.value = "";
703
+ defaultOpt.textContent = field.placeholder ?? `Select ${field.label.toLowerCase()}...`;
704
+ defaultOpt.disabled = true;
705
+ defaultOpt.selected = true;
706
+ select.appendChild(defaultOpt);
707
+ for (const opt of field.options ?? []) {
708
+ const option = document.createElement("option");
709
+ option.value = opt;
710
+ option.textContent = opt;
711
+ select.appendChild(option);
712
+ }
713
+ input = select;
714
+ } else if (field.type === "textarea") {
715
+ const textarea = document.createElement("textarea");
716
+ textarea.className = "cc-prechat-textarea";
717
+ textarea.placeholder = field.placeholder ?? "";
718
+ textarea.rows = 3;
719
+ input = textarea;
720
+ } else {
721
+ const textInput = document.createElement("input");
722
+ textInput.type = field.type;
723
+ textInput.className = "cc-prechat-input";
724
+ textInput.placeholder = field.placeholder ?? "";
725
+ input = textInput;
726
+ }
727
+ input.name = field.name;
728
+ input.addEventListener("input", () => {
729
+ this.clearError(field.name);
730
+ this.updateSubmitState();
731
+ });
732
+ if (input instanceof HTMLSelectElement) {
733
+ input.addEventListener("change", () => {
734
+ this.clearError(field.name);
735
+ this.updateSubmitState();
736
+ });
737
+ }
738
+ wrapper.appendChild(input);
739
+ this.fieldElements.set(field.name, input);
740
+ const error = document.createElement("div");
741
+ error.className = "cc-prechat-error";
742
+ wrapper.appendChild(error);
743
+ this.errorElements.set(field.name, error);
744
+ return wrapper;
745
+ }
746
+ updateSubmitState() {
747
+ const allRequiredFilled = this.options.fields.every((field) => {
748
+ if (!field.required) return true;
749
+ const el = this.fieldElements.get(field.name);
750
+ return el ? el.value.trim().length > 0 : false;
751
+ });
752
+ this.submitBtn.disabled = !allRequiredFilled;
753
+ }
754
+ handleSubmit() {
755
+ const data = {};
756
+ let valid = true;
757
+ for (const field of this.options.fields) {
758
+ const el = this.fieldElements.get(field.name);
759
+ if (!el) continue;
760
+ const value = el.value.trim();
761
+ data[field.name] = value;
762
+ if (field.required && !value) {
763
+ this.showError(field.name, this.options.locale.preChatRequired);
764
+ valid = false;
765
+ continue;
766
+ }
767
+ if (field.type === "email" && value && !this.isValidEmail(value)) {
768
+ this.showError(field.name, this.options.locale.preChatInvalidEmail);
769
+ valid = false;
770
+ continue;
771
+ }
772
+ }
773
+ if (valid) {
774
+ this.options.onSubmit(data);
775
+ }
776
+ }
777
+ showError(fieldName, message) {
778
+ const el = this.errorElements.get(fieldName);
779
+ if (el) el.textContent = message;
780
+ }
781
+ clearError(fieldName) {
782
+ const el = this.errorElements.get(fieldName);
783
+ if (el) el.textContent = "";
784
+ }
785
+ isValidEmail(email) {
786
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
787
+ }
788
+ destroy() {
789
+ this.container.remove();
790
+ this.fieldElements.clear();
791
+ this.errorElements.clear();
792
+ }
793
+ }
493
794
  class Widget {
494
795
  constructor(config) {
495
796
  __publicField(this, "config");
@@ -497,12 +798,16 @@ class Widget {
497
798
  __publicField(this, "fab");
498
799
  __publicField(this, "panel");
499
800
  __publicField(this, "bubble");
801
+ __publicField(this, "preChatFormEl");
500
802
  __publicField(this, "client");
501
803
  __publicField(this, "storage");
502
804
  __publicField(this, "locale");
503
805
  __publicField(this, "conversationId");
504
806
  __publicField(this, "messages", []);
807
+ __publicField(this, "userData");
808
+ __publicField(this, "userDataSent", false);
505
809
  __publicField(this, "isStreaming", false);
810
+ __publicField(this, "retryRequests", /* @__PURE__ */ new Map());
506
811
  __publicField(this, "eventHandlers", /* @__PURE__ */ new Map());
507
812
  __publicField(this, "containerEl");
508
813
  this.config = config;
@@ -528,7 +833,7 @@ class Widget {
528
833
  return this.config.container;
529
834
  }
530
835
  init() {
531
- var _a, _b, _c, _d;
836
+ var _a, _b, _c, _d, _e, _f;
532
837
  if (this.isInline) {
533
838
  this.containerEl = this.resolveContainer();
534
839
  this.shadow = createShadowRoot(this.containerEl);
@@ -548,13 +853,24 @@ class Widget {
548
853
  this.panel = new Panel(this.shadow, {
549
854
  position,
550
855
  inline: this.isInline,
856
+ preChatEnabled: ((_b = this.config.preChatForm) == null ? void 0 : _b.enabled) ?? false,
551
857
  branding: {
552
- name: ((_b = this.config.branding) == null ? void 0 : _b.name) ?? "AI Assistant",
553
- avatar: (_c = this.config.branding) == null ? void 0 : _c.avatar,
554
- subtitle: ((_d = this.config.branding) == null ? void 0 : _d.subtitle) ?? "Online"
858
+ name: ((_c = this.config.branding) == null ? void 0 : _c.name) ?? "AI Assistant",
859
+ avatar: (_d = this.config.branding) == null ? void 0 : _d.avatar,
860
+ subtitle: ((_e = this.config.branding) == null ? void 0 : _e.subtitle) ?? "Online"
555
861
  },
556
862
  placeholder: this.config.placeholder ?? this.locale.inputPlaceholder,
557
863
  footerText: this.locale.poweredBy,
864
+ messageActions: {
865
+ onRetry: (messageId) => {
866
+ void this.handleRetry(messageId);
867
+ },
868
+ onRegenerate: (messageId) => {
869
+ void this.handleRegenerate(messageId);
870
+ },
871
+ retryLabel: this.locale.retryButton,
872
+ regenerateLabel: this.locale.regenerateButton
873
+ },
558
874
  onSend: (text) => this.handleSend(text),
559
875
  onClose: () => this.close()
560
876
  });
@@ -568,16 +884,13 @@ class Widget {
568
884
  }
569
885
  }
570
886
  }
571
- if (this.messages.length === 0) {
572
- const welcomeText = this.config.welcomeMessage ?? this.locale.welcomeMessage;
573
- const welcomeMsg = {
574
- id: "welcome",
575
- role: "assistant",
576
- content: welcomeText
577
- };
578
- this.messages.push(welcomeMsg);
579
- this.panel.addMessage(welcomeMsg);
887
+ const shouldShowPreChat = ((_f = this.config.preChatForm) == null ? void 0 : _f.enabled) && !this.isPreChatCompleted() && this.messages.length === 0;
888
+ if (shouldShowPreChat) {
889
+ this.showPreChatForm();
890
+ } else {
891
+ this.showChatView();
580
892
  }
893
+ this.showLatestRegenerateButton();
581
894
  if (!this.isInline && this.config.welcomeBubble) {
582
895
  this.bubble = new WelcomeBubble(this.shadow, {
583
896
  text: this.config.welcomeBubble.text,
@@ -623,10 +936,11 @@ class Widget {
623
936
  }
624
937
  }
625
938
  destroy() {
626
- var _a, _b;
627
- (_a = this.bubble) == null ? void 0 : _a.destroy();
939
+ var _a, _b, _c;
940
+ (_a = this.preChatFormEl) == null ? void 0 : _a.destroy();
941
+ (_b = this.bubble) == null ? void 0 : _b.destroy();
628
942
  this.panel.destroy();
629
- (_b = this.fab) == null ? void 0 : _b.destroy();
943
+ (_c = this.fab) == null ? void 0 : _c.destroy();
630
944
  destroyShadowRoot(this.containerEl);
631
945
  this.eventHandlers.clear();
632
946
  this.messages = [];
@@ -645,8 +959,56 @@ class Widget {
645
959
  var _a;
646
960
  (_a = this.eventHandlers.get(event)) == null ? void 0 : _a.forEach((h) => h(...args));
647
961
  }
962
+ isPreChatCompleted() {
963
+ return this.storage.isPreChatCompleted(this.conversationId);
964
+ }
965
+ setPreChatCompleted() {
966
+ this.storage.setPreChatCompleted(this.conversationId);
967
+ }
968
+ showPreChatForm() {
969
+ const formConfig = this.config.preChatForm;
970
+ this.panel.hideMessages();
971
+ this.panel.input.setVisible(false);
972
+ this.preChatFormEl = new PreChatForm(this.panel.messagesContainer, {
973
+ title: formConfig.title ?? this.locale.preChatTitle,
974
+ subtitle: formConfig.subtitle ?? this.locale.preChatSubtitle,
975
+ fields: formConfig.fields,
976
+ submitLabel: formConfig.submitLabel ?? this.locale.preChatSubmit,
977
+ locale: this.locale,
978
+ onSubmit: (data) => this.handlePreChatSubmit(data)
979
+ });
980
+ }
981
+ handlePreChatSubmit(data) {
982
+ var _a, _b, _c;
983
+ this.userData = data;
984
+ this.setPreChatCompleted();
985
+ (_a = this.preChatFormEl) == null ? void 0 : _a.destroy();
986
+ this.preChatFormEl = void 0;
987
+ this.panel.showMessages();
988
+ this.panel.input.setVisible(true);
989
+ this.showChatView();
990
+ (_c = (_b = this.config).onPreChatSubmit) == null ? void 0 : _c.call(_b, data);
991
+ this.emit("preChatSubmit", data);
992
+ if (data.message) {
993
+ this.handleSend(data.message);
994
+ }
995
+ }
996
+ showChatView() {
997
+ var _a;
998
+ if (this.messages.length === 0) {
999
+ const userName = (_a = this.userData) == null ? void 0 : _a.name;
1000
+ const welcomeText = userName ? `Hi ${userName}! ${this.config.welcomeMessage ?? this.locale.welcomeMessage}` : this.config.welcomeMessage ?? this.locale.welcomeMessage;
1001
+ const welcomeMsg = {
1002
+ id: "welcome",
1003
+ role: "assistant",
1004
+ content: welcomeText
1005
+ };
1006
+ this.messages.push(welcomeMsg);
1007
+ this.panel.addMessage(welcomeMsg);
1008
+ }
1009
+ }
648
1010
  async handleSend(text) {
649
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1011
+ var _a, _b;
650
1012
  if (this.isStreaming) return;
651
1013
  const userMsg = {
652
1014
  id: crypto.randomUUID(),
@@ -657,33 +1019,97 @@ class Widget {
657
1019
  this.panel.addMessage(userMsg);
658
1020
  (_b = (_a = this.config).onMessage) == null ? void 0 : _b.call(_a, userMsg);
659
1021
  this.emit("message", userMsg);
1022
+ await this.sendToServer(text, { userMessageId: userMsg.id });
1023
+ }
1024
+ async handleRetry(errorMessageId) {
1025
+ if (this.isStreaming) return;
1026
+ const errorIndex = this.messages.findIndex((message) => message.id === errorMessageId);
1027
+ if (errorIndex === -1) return;
1028
+ const retryRequest = this.retryRequests.get(errorMessageId);
1029
+ let userMessage = (retryRequest == null ? void 0 : retryRequest.userMessageId) ? this.messages.find((message) => message.id === retryRequest.userMessageId) : void 0;
1030
+ if (!userMessage) {
1031
+ for (let i = errorIndex - 1; i >= 0; i--) {
1032
+ if (this.messages[i].role === "user") {
1033
+ userMessage = this.messages[i];
1034
+ break;
1035
+ }
1036
+ }
1037
+ }
1038
+ if (!userMessage) return;
1039
+ this.panel.messages.removeMessage(errorMessageId);
1040
+ this.retryRequests.delete(errorMessageId);
1041
+ this.messages = this.messages.filter((message) => message.id !== errorMessageId);
1042
+ await this.sendToServer(userMessage.content, {
1043
+ regenerate: retryRequest == null ? void 0 : retryRequest.regenerate,
1044
+ userMessageId: (retryRequest == null ? void 0 : retryRequest.userMessageId) ?? userMessage.id
1045
+ });
1046
+ }
1047
+ async handleRegenerate(assistantMessageId) {
1048
+ if (this.isStreaming) return;
1049
+ const assistantIndex = this.messages.findIndex((message) => message.id === assistantMessageId);
1050
+ if (assistantIndex === -1) return;
1051
+ const assistantMessage = this.messages[assistantIndex];
1052
+ if (assistantMessage.role !== "assistant") return;
1053
+ const userMessage = this.findUserMessageForAssistant(assistantIndex);
1054
+ if (!userMessage) return;
1055
+ this.panel.messages.clearRegenerateButtons();
1056
+ this.panel.messages.removeMessage(assistantMessageId);
1057
+ this.retryRequests.delete(assistantMessageId);
1058
+ this.messages = this.messages.filter((message) => message.id !== assistantMessageId);
1059
+ await this.sendToServer(userMessage.content, {
1060
+ regenerate: true,
1061
+ userMessageId: userMessage.id
1062
+ });
1063
+ }
1064
+ async sendToServer(text, options = {}) {
1065
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1066
+ if (this.isStreaming) return;
660
1067
  this.isStreaming = true;
661
1068
  this.panel.input.setDisabled(true);
1069
+ this.panel.messages.clearRegenerateButtons();
662
1070
  this.panel.messages.showTyping();
663
1071
  const assistantMsg = {
664
1072
  id: crypto.randomUUID(),
665
1073
  role: "assistant",
666
- content: ""
1074
+ content: "",
1075
+ status: "streaming"
667
1076
  };
668
- const pageContext = this.config.pageContext !== false ? { url: window.location.href, title: document.title } : void 0;
1077
+ const includeUserData = !!(this.userData && !this.userDataSent);
1078
+ let firstChunk = true;
1079
+ let requestCompleted = false;
1080
+ let hasError = false;
669
1081
  try {
670
- let firstChunk = true;
671
- for await (const chunk of this.client.sendMessage({
672
- conversationId: this.conversationId,
673
- message: text,
674
- pageContext,
675
- locale: this.config.locale
676
- })) {
1082
+ for await (const chunk of this.client.sendMessage(this.createRequest(text, options, includeUserData))) {
677
1083
  if (chunk.error) {
678
- const errorKey = chunk.error === "rate_limit" ? "errorRateLimit" : chunk.error === "network" ? "errorNetwork" : "errorGeneric";
679
- assistantMsg.content = this.locale[errorKey];
1084
+ const errorType = this.resolveErrorType(chunk.error);
1085
+ assistantMsg.content = this.getErrorMessage(errorType);
1086
+ assistantMsg.status = "error";
1087
+ assistantMsg.errorType = errorType;
1088
+ hasError = true;
1089
+ this.retryRequests.set(assistantMsg.id, {
1090
+ regenerate: options.regenerate,
1091
+ userMessageId: options.userMessageId
1092
+ });
680
1093
  this.panel.messages.removeTyping();
681
- this.panel.addMessage(assistantMsg);
682
- (_d = (_c = this.config).onError) == null ? void 0 : _d.call(_c, new Error(chunk.error));
683
- this.emit("error", new Error(chunk.error));
1094
+ if (firstChunk) {
1095
+ this.panel.addMessage(assistantMsg);
1096
+ } else {
1097
+ this.panel.messages.updateMessage(assistantMsg);
1098
+ }
1099
+ this.messages.push(assistantMsg);
1100
+ const error = new Error(chunk.error);
1101
+ (_b = (_a = this.config).onError) == null ? void 0 : _b.call(_a, error);
1102
+ this.emit("error", error);
684
1103
  break;
685
1104
  }
686
- if (chunk.done) break;
1105
+ if (chunk.done) {
1106
+ requestCompleted = true;
1107
+ break;
1108
+ }
1109
+ if (chunk.leadCaptured) {
1110
+ (_d = (_c = this.config).onLeadCaptured) == null ? void 0 : _d.call(_c, chunk.leadData);
1111
+ this.emit("leadCaptured", chunk.leadData);
1112
+ }
687
1113
  if (chunk.content) {
688
1114
  if (firstChunk) {
689
1115
  this.panel.messages.removeTyping();
@@ -691,29 +1117,113 @@ class Widget {
691
1117
  firstChunk = false;
692
1118
  }
693
1119
  assistantMsg.content += chunk.content;
694
- this.panel.messages.updateMessage(assistantMsg.id, assistantMsg.content, true);
1120
+ this.panel.messages.updateMessage(assistantMsg);
695
1121
  }
696
1122
  }
697
- if (assistantMsg.content) {
1123
+ if (!hasError && !requestCompleted) {
1124
+ requestCompleted = true;
1125
+ }
1126
+ if (!hasError && firstChunk) {
1127
+ this.panel.messages.removeTyping();
1128
+ }
1129
+ if (!hasError && assistantMsg.content) {
1130
+ assistantMsg.status = "complete";
1131
+ this.panel.messages.updateMessage(assistantMsg);
698
1132
  this.messages.push(assistantMsg);
699
1133
  (_f = (_e = this.config).onMessage) == null ? void 0 : _f.call(_e, assistantMsg);
700
1134
  this.emit("message", assistantMsg);
1135
+ this.showLatestRegenerateButton();
701
1136
  if (!this.isInline && !this.panel.isVisible) {
702
1137
  (_g = this.fab) == null ? void 0 : _g.showBadge();
703
1138
  }
704
1139
  }
705
1140
  } catch (err) {
706
- this.panel.messages.removeTyping();
707
1141
  assistantMsg.content = this.locale.errorGeneric;
1142
+ assistantMsg.status = "error";
1143
+ assistantMsg.errorType = "network";
1144
+ this.retryRequests.set(assistantMsg.id, {
1145
+ regenerate: options.regenerate,
1146
+ userMessageId: options.userMessageId
1147
+ });
1148
+ this.panel.messages.removeTyping();
708
1149
  this.panel.addMessage(assistantMsg);
709
- (_i = (_h = this.config).onError) == null ? void 0 : _i.call(_h, err instanceof Error ? err : new Error(String(err)));
1150
+ this.messages.push(assistantMsg);
1151
+ const error = err instanceof Error ? err : new Error(String(err));
1152
+ (_i = (_h = this.config).onError) == null ? void 0 : _i.call(_h, error);
1153
+ this.emit("error", error);
710
1154
  } finally {
1155
+ if (includeUserData && requestCompleted) {
1156
+ this.userDataSent = true;
1157
+ }
711
1158
  this.isStreaming = false;
712
1159
  this.panel.input.setDisabled(false);
713
1160
  this.panel.input.focus();
714
1161
  this.saveHistory();
715
1162
  }
716
1163
  }
1164
+ createRequest(text, options, includeUserData) {
1165
+ const pageContext = this.config.pageContext !== false ? { url: window.location.href, title: document.title } : void 0;
1166
+ return {
1167
+ conversationId: this.conversationId,
1168
+ message: text,
1169
+ messageId: options.userMessageId,
1170
+ regenerate: options.regenerate,
1171
+ pageContext,
1172
+ locale: this.config.locale,
1173
+ ...includeUserData ? { userData: this.userData } : {}
1174
+ };
1175
+ }
1176
+ findUserMessageForAssistant(assistantIndex) {
1177
+ for (let i = assistantIndex - 1; i >= 0; i--) {
1178
+ if (this.messages[i].role === "user") {
1179
+ return this.messages[i];
1180
+ }
1181
+ }
1182
+ return void 0;
1183
+ }
1184
+ findLatestRegeneratableAssistant() {
1185
+ for (let i = this.messages.length - 1; i >= 0; i--) {
1186
+ const message = this.messages[i];
1187
+ if (message.id === "welcome") {
1188
+ continue;
1189
+ }
1190
+ if (message.role !== "assistant" || message.status === "error") {
1191
+ return void 0;
1192
+ }
1193
+ return message;
1194
+ }
1195
+ return void 0;
1196
+ }
1197
+ showLatestRegenerateButton() {
1198
+ const assistantMessage = this.findLatestRegeneratableAssistant();
1199
+ if (assistantMessage) {
1200
+ this.panel.messages.showRegenerateButton(assistantMessage.id);
1201
+ } else {
1202
+ this.panel.messages.clearRegenerateButtons();
1203
+ }
1204
+ }
1205
+ resolveErrorType(error) {
1206
+ switch (error) {
1207
+ case "rate_limit":
1208
+ case "network":
1209
+ case "provider_error":
1210
+ case "timeout":
1211
+ return error;
1212
+ default:
1213
+ return "provider_error";
1214
+ }
1215
+ }
1216
+ getErrorMessage(errorType) {
1217
+ switch (errorType) {
1218
+ case "rate_limit":
1219
+ return this.locale.errorRateLimit;
1220
+ case "network":
1221
+ case "timeout":
1222
+ return this.locale.errorNetwork;
1223
+ default:
1224
+ return this.locale.errorGeneric;
1225
+ }
1226
+ }
717
1227
  saveHistory() {
718
1228
  if (this.config.persistHistory === false) return;
719
1229
  const maxMessages = this.config.maxMessages ?? 50;
@@ -724,6 +1234,8 @@ class Widget {
724
1234
  id: m.id,
725
1235
  role: m.role,
726
1236
  content: m.content,
1237
+ status: m.status,
1238
+ errorType: m.errorType,
727
1239
  timestamp: Date.now()
728
1240
  })),
729
1241
  updatedAt: Date.now()