@spilki/widget 0.1.4 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/widget.es.js CHANGED
@@ -1,9 +1,16 @@
1
- const O = `
1
+ const W = `
2
2
  <style>
3
3
  :host {
4
4
  all: initial;
5
5
  position: fixed;
6
6
  z-index: 2147483000;
7
+ bottom: 24px;
8
+ }
9
+ :host([data-position="bottom-right"]) {
10
+ right: 24px;
11
+ }
12
+ :host([data-position="bottom-left"]) {
13
+ left: 24px;
7
14
  }
8
15
  button {
9
16
  all: unset;
@@ -44,11 +51,11 @@ const O = `
44
51
  </span>
45
52
  </button>
46
53
  `;
47
- function A(r) {
54
+ function L(r) {
48
55
  const e = document.createElement("div");
49
- e.setAttribute("part", "bubble-root"), e.style.setProperty("--spilki-accent", r.color), e.style.position = "fixed", e.style.bottom = "24px", e.style[r.position === "bottom-right" ? "right" : "left"] = "24px";
56
+ e.setAttribute("part", "bubble-root"), e.setAttribute("data-position", r.position), e.style.setProperty("--spilki-accent", r.color);
50
57
  const t = e.attachShadow({ mode: "open" });
51
- t.innerHTML = O;
58
+ t.innerHTML = W;
52
59
  const s = t.querySelector("button");
53
60
  return s.addEventListener("click", () => r.onClick()), {
54
61
  element: e,
@@ -58,41 +65,103 @@ function A(r) {
58
65
  destroy() {
59
66
  e.remove();
60
67
  },
61
- setOpen(n) {
62
- s.setAttribute("aria-expanded", String(n));
68
+ setOpen(o) {
69
+ s.setAttribute("aria-expanded", String(o)), s.setAttribute("aria-label", o ? "Close chat" : "Open chat");
63
70
  }
64
71
  };
65
72
  }
66
- const M = ':host{--spilki-bg-light: #ffffff;--spilki-bg-dark: #0f172a;--spilki-text-light: #0f172a;--spilki-text-dark: #f8fafc;--spilki-border-light: rgba(15, 23, 42, .1);--spilki-border-dark: rgba(148, 163, 184, .25);--spilki-shadow: 0 10px 40px rgba(15, 23, 42, .2);--spilki-radius: 16px;--spilki-font: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;color:inherit;font-family:var(--spilki-font)}.wrapper{width:100%;height:100%;display:flex;flex-direction:column;background:var(--spilki-surface);color:var(--spilki-text);border-radius:var(--spilki-radius);box-shadow:var(--spilki-shadow);border:1px solid var(--spilki-border);overflow:hidden}header{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background:var(--spilki-surface);border-bottom:1px solid var(--spilki-border)}header h1{font-size:1rem;margin:0;display:flex;align-items:center;gap:.5rem}header button.close{border:none;background:transparent;color:inherit;font-size:1.25rem;cursor:pointer}header .status-dot{width:10px;height:10px;border-radius:999px;background:var(--spilki-accent)}.messages{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:.5rem}.message{display:flex;flex-direction:column;gap:.25rem;max-width:85%;line-height:1.4;word-wrap:break-word;overflow-wrap:anywhere;white-space:pre-wrap}.message.user{align-self:flex-end;text-align:right}.message .bubble{padding:.6rem .8rem;border-radius:1rem;background:#6366f126}.message.user .bubble{background:var(--spilki-accent);color:#fff}.message.bot .bubble{background:#94a3b826}.input-area{display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--spilki-border)}.input-area textarea{flex:1;resize:none;min-height:2.5rem;max-height:6rem;border-radius:.75rem;border:1px solid var(--spilki-border);padding:.6rem .75rem;font-family:inherit;font-size:.95rem;background:var(--spilki-surface);color:var(--spilki-text)}.input-area button{border:none;border-radius:999px;padding:.6rem 1.1rem;background:var(--spilki-accent);color:#fff;font-weight:600;cursor:pointer}.typing{font-size:.75rem;color:#94a3b8e6;padding:0 1rem .75rem}.offline{font-size:.8rem;padding:0 1rem;color:#f97316}:host([data-theme="dark"]){--spilki-surface: var(--spilki-bg-dark);--spilki-text: var(--spilki-text-dark);--spilki-border: var(--spilki-border-dark)}:host([data-theme="light"]){--spilki-surface: var(--spilki-bg-light);--spilki-text: var(--spilki-text-light);--spilki-border: var(--spilki-border-light)}:host([data-theme="dark"]) .message .bubble{background:#94a3b81f}:host([data-theme="dark"]) .message.bot .bubble{background:#6366f126}:host([data-theme="dark"]) .input-area textarea{background:#0f172ad9}.messages::-webkit-scrollbar,.input-area textarea::-webkit-scrollbar{width:6px}.messages::-webkit-scrollbar-track,.input-area textarea::-webkit-scrollbar-track{background:transparent}.messages::-webkit-scrollbar-thumb,.input-area textarea::-webkit-scrollbar-thumb{background:#94a3b84d;border-radius:999px}.messages::-webkit-scrollbar-thumb:hover,.input-area textarea::-webkit-scrollbar-thumb:hover{background:#94a3b880}:host([data-theme="dark"]) .messages::-webkit-scrollbar-thumb,:host([data-theme="dark"]) .input-area textarea::-webkit-scrollbar-thumb{background:#fff3}:host([data-theme="dark"]) .messages::-webkit-scrollbar-thumb:hover,:host([data-theme="dark"]) .input-area textarea::-webkit-scrollbar-thumb:hover{background:#ffffff59}.messages{scroll-behavior:smooth}';
67
- class C {
73
+ const B = "https://api.spilki.ai", S = {
74
+ welcome: "Hi! I'm your assistant.",
75
+ placeholder: "Type a message…",
76
+ sendLabel: "Send",
77
+ typing: "Assistant is typing…",
78
+ offline: "Unable to connect. Please try again later.",
79
+ title: "Spilki Assistant"
80
+ }, g = {
81
+ apiBase: B,
82
+ position: "bottom-right",
83
+ theme: "auto",
84
+ color: "#6366f1",
85
+ welcome: S.welcome,
86
+ persist: !0,
87
+ i18n: S
88
+ };
89
+ function K(r) {
90
+ var t, s, i, o, c, a, n;
91
+ const e = { ...S, ...(t = r.i18n) != null ? t : {} };
92
+ return {
93
+ ...g,
94
+ ...r,
95
+ apiBase: (s = r.apiBase) != null ? s : g.apiBase,
96
+ i18n: e,
97
+ welcome: (i = r.welcome) != null ? i : e.welcome,
98
+ position: (o = r.position) != null ? o : g.position,
99
+ theme: (c = r.theme) != null ? c : g.theme,
100
+ color: (a = r.color) != null ? a : g.color,
101
+ persist: (n = r.persist) != null ? n : g.persist
102
+ };
103
+ }
104
+ function P(r = "msg") {
105
+ return typeof crypto != "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${r}-${Math.random().toString(16).slice(2)}`;
106
+ }
107
+ function D() {
108
+ var r, e;
109
+ return (e = (r = window.matchMedia) == null ? void 0 : r.call(window, "(prefers-color-scheme: dark)").matches) != null ? e : !1;
110
+ }
111
+ function C(r) {
112
+ return r === "light" || r === "dark" ? r : D() ? "dark" : "light";
113
+ }
114
+ function y(r, e = 30) {
115
+ return r.slice(-e);
116
+ }
117
+ function N(r) {
118
+ if (!r || r.length === 0) return;
119
+ const e = window.location.origin;
120
+ r.includes(e) || console.warn(
121
+ `SpilkiWidget: current origin ${e} not in allowedOriginsHint: ${r.join(", ")}`
122
+ );
123
+ }
124
+ const F = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
125
+ function H(r) {
126
+ const e = new Date(r), t = /* @__PURE__ */ new Date(), s = (n) => String(n).padStart(2, "0"), i = `${s(e.getHours())}:${s(e.getMinutes())}`, o = new Date(t.getFullYear(), t.getMonth(), t.getDate()).getTime(), c = o - 864e5, a = new Date(e.getFullYear(), e.getMonth(), e.getDate()).getTime();
127
+ return a === o ? `Today at ${i}` : a === c ? `Yesterday at ${i}` : `${F[e.getMonth()]} ${e.getDate()}, ${i}`;
128
+ }
129
+ const U = ':host{--spilki-bg-light: #ffffff;--spilki-bg-dark: #0f172a;--spilki-text-light: #0f172a;--spilki-text-dark: #f8fafc;--spilki-border-light: rgba(15, 23, 42, .1);--spilki-border-dark: rgba(148, 163, 184, .25);--spilki-shadow: 0 10px 40px rgba(15, 23, 42, .2);--spilki-radius: 16px;--spilki-font: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;color:inherit;font-family:var(--spilki-font)}.wrapper{width:100%;height:100%;display:flex;flex-direction:column;background:var(--spilki-surface);color:var(--spilki-text);border-radius:var(--spilki-radius);box-shadow:var(--spilki-shadow);border:1px solid var(--spilki-border);overflow:hidden}header{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background:var(--spilki-surface);border-bottom:1px solid var(--spilki-border)}header h1{font-size:1rem;margin:0;display:flex;align-items:center;gap:.5rem}header button.close{border:none;background:transparent;color:inherit;font-size:1.25rem;cursor:pointer}header button.close:focus-visible{outline:2px solid var(--spilki-accent);outline-offset:2px;border-radius:4px}header .status-dot{width:10px;height:10px;border-radius:999px;background:var(--spilki-accent)}.messages{flex:1;overflow-y:auto;padding:1rem;display:flex;flex-direction:column;gap:.5rem;scrollbar-width:thin;scrollbar-color:rgba(148,163,184,.3) transparent}.message{display:flex;flex-direction:column;gap:.25rem;max-width:85%;line-height:1.4;word-wrap:break-word;overflow-wrap:anywhere;white-space:pre-wrap}.message.user{align-self:flex-end;text-align:right}.message .bubble{padding:.6rem .8rem;border-radius:1rem;background:#6366f126}.message.user .bubble{background:var(--spilki-accent);color:#fff}.message.bot .bubble{background:#94a3b826}.input-area{display:flex;align-items:flex-end;gap:.5rem;padding:.75rem 1rem;border-top:1px solid var(--spilki-border)}.input-area textarea{flex:1;resize:none;min-height:2.5rem;max-height:6rem;border-radius:.75rem;border:1px solid var(--spilki-border);padding:.6rem .75rem;font-family:inherit;font-size:.95rem;background:var(--spilki-surface);color:var(--spilki-text);scrollbar-width:thin;.input-area textarea:focus-visible{outline:2px solid var(--spilki-accent);outline-offset:-1px}scrollbar-color:rgba(148,163,184,.3) transparent}.input-area button{border:none;border-radius:999px;padding:.6rem 1.1rem;background:var(--spilki-accent);color:#fff;font-weight:600;cursor:pointer}.input-area button:focus-visible{outline:2px solid #fff;outline-offset:2px}.typing{font-size:.75rem;color:#94a3b8e6;padding:0 1rem .75rem}.offline{font-size:.8rem;padding:0 1rem;color:#f97316}:host([data-theme="dark"]){--spilki-surface: var(--spilki-bg-dark);--spilki-text: var(--spilki-text-dark);--spilki-border: var(--spilki-border-dark)}:host([data-theme="dark"]) .messages,:host([data-theme="dark"]) .input-area textarea{scrollbar-color:rgba(255,255,255,.2) transparent}:host([data-theme="light"]){--spilki-surface: var(--spilki-bg-light);--spilki-text: var(--spilki-text-light);--spilki-border: var(--spilki-border-light)}:host([data-theme="dark"]) .message .bubble{background:#94a3b81f}:host([data-theme="dark"]) .message.bot .bubble{background:#6366f126}:host([data-theme="dark"]) .input-area textarea{background:#0f172ad9}.messages::-webkit-scrollbar,.input-area textarea::-webkit-scrollbar{width:6px}.messages::-webkit-scrollbar-track,.input-area textarea::-webkit-scrollbar-track{background:transparent}.messages::-webkit-scrollbar-thumb,.input-area textarea::-webkit-scrollbar-thumb{background:#94a3b84d;border-radius:999px}.messages::-webkit-scrollbar-thumb:hover,.input-area textarea::-webkit-scrollbar-thumb:hover{background:#94a3b880}:host([data-theme="dark"]) .messages::-webkit-scrollbar-thumb,:host([data-theme="dark"]) .input-area textarea::-webkit-scrollbar-thumb{background:#fff3}:host([data-theme="dark"]) .messages::-webkit-scrollbar-thumb:hover,:host([data-theme="dark"]) .input-area textarea::-webkit-scrollbar-thumb:hover{background:#ffffff59}.messages{scroll-behavior:smooth}.conversation-separator{display:flex;align-items:center;gap:.5rem;padding:.5rem 0;cursor:pointer;user-select:none;font-size:.7rem;color:#94a3b8b3;white-space:nowrap}.conversation-separator:before,.conversation-separator:after{content:"";flex:1;height:1px;background:var(--spilki-border)}.conversation-separator:hover{color:#94a3b8e6}.conversation-separator:focus-visible{outline:2px solid var(--spilki-accent);outline-offset:2px;border-radius:4px}.conversation-history{display:none;flex-direction:column;gap:.5rem}.conversation-history.expanded{display:flex}';
130
+ function b(r) {
131
+ return r.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
132
+ }
133
+ class z {
68
134
  constructor(e) {
69
- this.options = e, this.focusable = [], this.open = !1, this.host = document.createElement("div"), this.host.setAttribute("part", "panel-root"), this.host.style.position = "fixed", this.host.style.bottom = "96px", this.host.style[e.position === "bottom-right" ? "right" : "left"] = "24px", this.host.style.width = "360px", this.host.style.maxWidth = "calc(100vw - 32px)", this.host.style.height = "520px", this.host.style.display = "none", this.host.style.zIndex = "2147483001", this.shadow = this.host.attachShadow({ mode: "open" }), this.shadow.innerHTML = `
70
- <style>${M}</style>
71
- <div class="wrapper" role="dialog" aria-modal="true" aria-label="${e.i18n.title}">
135
+ this.options = e, this.focusable = [], this.seenIds = /* @__PURE__ */ new Set(), this.open = !1, this.host = document.createElement("div"), this.host.setAttribute("part", "panel-root"), this.host.style.position = "fixed", this.host.style.bottom = "96px", this.host.style[e.position === "bottom-right" ? "right" : "left"] = "24px", this.host.style.width = "360px", this.host.style.maxWidth = "calc(100vw - 32px)", this.host.style.height = "520px", this.host.style.display = "none", this.host.style.zIndex = "2147483001", this.shadow = this.host.attachShadow({ mode: "open" });
136
+ const t = b(e.i18n.title), s = b(e.i18n.typing), i = b(e.i18n.offline), o = b(e.i18n.placeholder), c = b(e.i18n.sendLabel);
137
+ this.shadow.innerHTML = `
138
+ <style>${U}</style>
139
+ <div class="wrapper" role="dialog" aria-modal="true" aria-label="${t}">
72
140
  <header>
73
- <h1><span class="status-dot" aria-hidden="true"></span>${e.i18n.title}</h1>
141
+ <h1><span class="status-dot" aria-hidden="true"></span>${t}</h1>
74
142
  <button class="close" type="button" aria-label="Close">×</button>
75
143
  </header>
76
- <div class="messages" part="messages"></div>
77
- <div class="typing" hidden>${e.i18n.typing}</div>
78
- <div class="offline" hidden>${e.i18n.offline}</div>
144
+ <div class="messages" part="messages" role="log" aria-live="polite" aria-label="Chat messages"></div>
145
+ <div class="typing" hidden aria-live="polite" aria-atomic="true">${s}</div>
146
+ <div class="offline" hidden aria-live="assertive" aria-atomic="true">${i}</div>
79
147
  <div class="input-area">
80
- <textarea rows="2" placeholder="${e.i18n.placeholder}" aria-label="${e.i18n.placeholder}"></textarea>
81
- <button type="button">${e.i18n.sendLabel}</button>
148
+ <textarea rows="2" placeholder="${o}" aria-label="${o}"></textarea>
149
+ <button type="button">${c}</button>
82
150
  </div>
83
151
  </div>
84
- `, this.host.dataset.theme = e.theme, this.host.style.setProperty("--spilki-accent", e.color), this.messagesEl = this.shadow.querySelector(".messages"), this.typingEl = this.shadow.querySelector(".typing"), this.input = this.shadow.querySelector("textarea"), this.offlineEl = this.shadow.querySelector(".offline"), this.sendButton = this.shadow.querySelector(".input-area button"), this.shadow.querySelector("header .close").addEventListener("click", () => this.options.onClose()), this.sendButton.addEventListener("click", () => this.send()), this.input.addEventListener("keydown", (s) => {
85
- s.key === "Enter" && !s.shiftKey ? (s.preventDefault(), this.send()) : s.key === "Escape" && this.options.onClose();
86
- }), this.shadow.addEventListener("keydown", (s) => {
87
- const i = s;
88
- i.key === "Escape" && this.options.onClose(), i.key === "Tab" && this.trapFocus(i);
89
- }), this.shadow.addEventListener("focusin", () => this.collectFocusable()), this.collectFocusable();
152
+ `, this.host.dataset.theme = e.theme, this.host.style.setProperty("--spilki-accent", e.color), this.messagesEl = this.shadow.querySelector(".messages"), this.typingEl = this.shadow.querySelector(".typing"), this.input = this.shadow.querySelector("textarea"), this.offlineEl = this.shadow.querySelector(".offline"), this.sendButton = this.shadow.querySelector(".input-area button"), this.closeButton = this.shadow.querySelector("header .close"), this.handleCloseClick = () => this.options.onClose(), this.handleSendClick = () => this.send(), this.handleInputKeydown = (a) => {
153
+ a.key === "Enter" && !a.shiftKey ? (a.preventDefault(), this.send()) : a.key === "Escape" && this.options.onClose();
154
+ }, this.handleShadowKeydown = (a) => {
155
+ const n = a;
156
+ n.key === "Escape" && this.options.onClose(), n.key === "Tab" && this.trapFocus(n);
157
+ }, this.handleFocusin = () => this.collectFocusable(), this.closeButton.addEventListener("click", this.handleCloseClick), this.sendButton.addEventListener("click", this.handleSendClick), this.input.addEventListener("keydown", this.handleInputKeydown), this.shadow.addEventListener("keydown", this.handleShadowKeydown), this.shadow.addEventListener("focusin", this.handleFocusin), this.collectFocusable();
90
158
  }
91
159
  mount() {
92
160
  document.body.appendChild(this.host);
93
161
  }
162
+ // #4: remove all event listeners before removing DOM
94
163
  destroy() {
95
- this.host.remove();
164
+ this.closeButton.removeEventListener("click", this.handleCloseClick), this.sendButton.removeEventListener("click", this.handleSendClick), this.input.removeEventListener("keydown", this.handleInputKeydown), this.shadow.removeEventListener("keydown", this.handleShadowKeydown), this.shadow.removeEventListener("focusin", this.handleFocusin), this.host.remove();
96
165
  }
97
166
  show() {
98
167
  this.open || (this.open = !0, this.host.style.display = "block", this.focusInput());
@@ -108,19 +177,27 @@ class C {
108
177
  updateTheme(e) {
109
178
  this.host.dataset.theme = e;
110
179
  }
180
+ // #16: rebuild seen IDs on full re-render
111
181
  updateMessages(e) {
112
- this.messagesEl.innerHTML = "", e.forEach((t) => {
113
- const s = document.createElement("div");
114
- s.className = `message ${t.author}`, s.setAttribute("data-author", t.author);
115
- const i = document.createElement("div");
116
- i.className = "bubble", i.textContent = t.text, s.appendChild(i), this.messagesEl.appendChild(s);
117
- }), this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
182
+ this.messagesEl.innerHTML = "", this.seenIds.clear(), e.forEach((t) => {
183
+ this.seenIds.add(t.id), this.messagesEl.appendChild(this.createMessageElement(t));
184
+ }), this.messagesEl.scrollTop = this.messagesEl.scrollHeight, this.collectFocusable();
185
+ }
186
+ // Render conversation groups with collapsed history separators
187
+ renderWithConversations(e, t) {
188
+ this.messagesEl.innerHTML = "", this.seenIds.clear();
189
+ for (const s of e) {
190
+ s.messages.forEach((a) => this.seenIds.add(a.id));
191
+ const i = this.createHistoryContainer(s.messages), o = s.messages[s.messages.length - 1].ts, c = this.createSeparatorElement(o, i);
192
+ this.messagesEl.appendChild(i), this.messagesEl.appendChild(c);
193
+ }
194
+ t.forEach((s) => {
195
+ this.seenIds.add(s.id), this.messagesEl.appendChild(this.createMessageElement(s));
196
+ }), this.messagesEl.scrollTop = this.messagesEl.scrollHeight, this.collectFocusable();
118
197
  }
198
+ // #16: deduplicate; #19: smart scroll; #21: update focus trap
119
199
  appendMessage(e) {
120
- const t = document.createElement("div");
121
- t.className = `message ${e.author}`, t.setAttribute("data-author", e.author);
122
- const s = document.createElement("div");
123
- s.className = "bubble", s.textContent = e.text, t.appendChild(s), this.messagesEl.appendChild(t), this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
200
+ this.seenIds.has(e.id) || (this.seenIds.add(e.id), this.messagesEl.appendChild(this.createMessageElement(e)), this.scrollToBottomIfNeeded(), this.collectFocusable());
124
201
  }
125
202
  setTyping(e) {
126
203
  this.typingEl.toggleAttribute("hidden", !e);
@@ -135,6 +212,43 @@ class C {
135
212
  const e = this.input.value.trim();
136
213
  e && (this.options.onSend(e), this.clearInput());
137
214
  }
215
+ // #29 #34: extract shared message element creation; #25: semantic labels
216
+ createMessageElement(e) {
217
+ const t = document.createElement("div");
218
+ t.className = `message ${e.author}`, t.setAttribute("data-author", e.author), t.setAttribute("role", "article"), t.setAttribute(
219
+ "aria-label",
220
+ // #25
221
+ e.author === "user" ? "You" : "Assistant"
222
+ );
223
+ const s = document.createElement("div");
224
+ return s.className = "bubble", s.textContent = e.text, t.appendChild(s), t;
225
+ }
226
+ createSeparatorElement(e, t) {
227
+ const s = document.createElement("div");
228
+ s.className = "conversation-separator", s.setAttribute("role", "button"), s.setAttribute("tabindex", "0"), s.setAttribute("aria-expanded", "false"), s.setAttribute("aria-label", "Show previous conversation"), s.textContent = H(e);
229
+ const i = () => {
230
+ const o = t.classList.toggle("expanded");
231
+ s.setAttribute("aria-expanded", String(o)), s.setAttribute(
232
+ "aria-label",
233
+ o ? "Hide previous conversation" : "Show previous conversation"
234
+ );
235
+ };
236
+ return s.addEventListener("click", i), s.addEventListener("keydown", (o) => {
237
+ const c = o;
238
+ (c.key === "Enter" || c.key === " ") && (c.preventDefault(), i());
239
+ }), s;
240
+ }
241
+ createHistoryContainer(e) {
242
+ const t = document.createElement("div");
243
+ return t.className = "conversation-history", e.forEach((s) => {
244
+ t.appendChild(this.createMessageElement(s));
245
+ }), t;
246
+ }
247
+ // #19: only auto-scroll if user is near the bottom
248
+ scrollToBottomIfNeeded() {
249
+ const e = this.messagesEl;
250
+ e.scrollHeight - e.scrollTop - e.clientHeight < 100 && (e.scrollTop = e.scrollHeight);
251
+ }
138
252
  collectFocusable() {
139
253
  const e = this.shadow.querySelectorAll(
140
254
  'button, textarea, [href], [tabindex]:not([tabindex="-1"])'
@@ -149,61 +263,10 @@ class C {
149
263
  e.shiftKey && i === t ? (e.preventDefault(), s.focus()) : !e.shiftKey && i === s && (e.preventDefault(), t.focus());
150
264
  }
151
265
  }
152
- const W = "https://api.spilki.ai", g = {
153
- welcome: "Hi! I'm your assistant.",
154
- placeholder: "Type a message…",
155
- sendLabel: "Send",
156
- typing: "Assistant is typing…",
157
- offline: "Unable to connect. Please try again later.",
158
- title: "Spilki Assistant"
159
- }, p = {
160
- apiBase: W,
161
- position: "bottom-right",
162
- theme: "auto",
163
- color: "#6366f1",
164
- welcome: g.welcome,
165
- persist: !0,
166
- i18n: g
167
- };
168
- function $(r) {
169
- var t, s, i, n, l, a, h;
170
- const e = { ...g, ...(t = r.i18n) != null ? t : {} };
171
- return {
172
- ...p,
173
- ...r,
174
- apiBase: (s = r.apiBase) != null ? s : p.apiBase,
175
- i18n: e,
176
- welcome: (i = r.welcome) != null ? i : e.welcome,
177
- position: (n = r.position) != null ? n : p.position,
178
- theme: (l = r.theme) != null ? l : p.theme,
179
- color: (a = r.color) != null ? a : p.color,
180
- persist: (h = r.persist) != null ? h : p.persist
181
- };
182
- }
183
- function B(r = "msg") {
184
- return typeof crypto != "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${r}-${Math.random().toString(16).slice(2)}`;
185
- }
186
- function L() {
187
- var r, e;
188
- return (e = (r = window.matchMedia) == null ? void 0 : r.call(window, "(prefers-color-scheme: dark)").matches) != null ? e : !1;
189
- }
190
- function x(r) {
191
- return r === "light" || r === "dark" ? r : L() ? "dark" : "light";
192
- }
193
- function u(r, e = 30) {
194
- return r.slice(-e);
195
- }
196
- function K(r) {
197
- if (!r || r.length === 0) return;
198
- const e = window.location.origin;
199
- r.includes(e) || console.warn(
200
- `SpilkiWidget: current origin ${e} not in allowedOriginsHint: ${r.join(", ")}`
201
- );
202
- }
203
- const d = 1500, v = 8e3;
204
- class P {
266
+ const f = 1500, I = 8e3;
267
+ class J {
205
268
  constructor(e, t) {
206
- this.sessionId = null, this.currentKind = null, this.stopped = !1, this.backoff = d, this.options = e, this.handlers = t;
269
+ this.sessionId = null, this.currentKind = null, this.stopped = !1, this.backoff = f, this.connectPromise = null, this.options = e, this.handlers = t;
207
270
  }
208
271
  setAccessToken(e) {
209
272
  this.options.accessToken = e;
@@ -215,10 +278,20 @@ class P {
215
278
  var e, t;
216
279
  return (t = (e = this.sessionId) != null ? e : this.options.sessionId) != null ? t : null;
217
280
  }
281
+ // #3: connect lock — return in-flight promise if already connecting
218
282
  async connect() {
219
- var l, a;
283
+ if (this.connectPromise) return this.connectPromise;
284
+ this.connectPromise = this.doConnect();
285
+ try {
286
+ return await this.connectPromise;
287
+ } finally {
288
+ this.connectPromise = null;
289
+ }
290
+ }
291
+ async doConnect() {
292
+ var c, a;
220
293
  this.stopped = !1;
221
- const e = `${this.options.apiBase.replace(/\/$/, "")}/widget/session`, t = (a = (l = this.sessionId) != null ? l : this.options.sessionId) != null ? a : void 0, s = {
294
+ const e = `${this.options.apiBase.replace(/\/$/, "")}/widget/session`, t = (a = (c = this.sessionId) != null ? c : this.options.sessionId) != null ? a : void 0, s = {
222
295
  organisationId: this.options.org,
223
296
  sessionId: t,
224
297
  userAgent: typeof navigator != "undefined" ? navigator.userAgent : "",
@@ -234,34 +307,36 @@ class P {
234
307
  });
235
308
  if (!i.ok)
236
309
  throw new Error(`SpilkiWidget: connect failed (${i.status})`);
237
- const n = await i.json();
238
- return this.sessionId = n.sessionId, this.options.sessionId = n.sessionId, this.backoff = d, await this.startTransport(n), n;
239
- }
240
- async send(e) {
241
- var n, l;
242
- const t = {
243
- sessionId: (l = (n = this.sessionId) != null ? n : this.options.sessionId) != null ? l : "",
244
- text: e
310
+ const o = await i.json();
311
+ return this.sessionId = o.sessionId, this.options.sessionId = o.sessionId, this.backoff = f, await this.startTransport(o), o;
312
+ }
313
+ async send(e, t) {
314
+ var c, a;
315
+ const s = {
316
+ sessionId: (a = (c = this.sessionId) != null ? c : this.options.sessionId) != null ? a : "",
317
+ text: e,
318
+ ...t ? { messageId: t } : {}
245
319
  };
246
- if (!t.sessionId)
320
+ if (!s.sessionId)
247
321
  throw new Error("SpilkiWidget: missing session id");
248
322
  if (this.currentKind === "ws" && this.ws && this.ws.readyState === WebSocket.OPEN) {
249
- this.ws.send(JSON.stringify({ type: "message", payload: t }));
323
+ this.ws.send(JSON.stringify({ type: "message", payload: s }));
250
324
  return;
251
325
  }
252
- const s = `${this.options.apiBase.replace(/\/$/, "")}/widget/message`, i = await fetch(s, {
326
+ const i = `${this.options.apiBase.replace(/\/$/, "")}/widget/message`, o = await fetch(i, {
253
327
  method: "POST",
254
328
  headers: {
255
329
  "Content-Type": "application/json",
256
- ...this.options.accessToken ? { "X-Authorization": `Bearer ${this.options.accessToken}` } : {}
330
+ "X-Authorization": `Bearer ${this.options.accessToken}`
257
331
  },
258
- body: JSON.stringify(t)
332
+ body: JSON.stringify(s)
259
333
  });
260
- if (!i.ok)
261
- throw new Error(`SpilkiWidget: send failed (${i.status})`);
334
+ if (!o.ok)
335
+ throw new Error(`SpilkiWidget: send failed (${o.status})`);
262
336
  }
263
- stop() {
264
- this.stopped = !0, this.backoff = d, this.ws && (this.ws.close(), this.ws = void 0), this.sse && (this.sse.close(), this.sse = void 0), this.pollTimer && (clearTimeout(this.pollTimer), this.pollTimer = void 0), this.currentKind = null;
337
+ // #1: accept resetBackoff param; #8 #9: clear retryTimer
338
+ stop(e = !0) {
339
+ this.stopped = !0, e && (this.backoff = f), this.retryTimer && (clearTimeout(this.retryTimer), this.retryTimer = void 0), this.ws && (this.wsOnOpen && this.ws.removeEventListener("open", this.wsOnOpen), this.wsOnMessage && this.ws.removeEventListener("message", this.wsOnMessage), this.wsOnClose && this.ws.removeEventListener("close", this.wsOnClose), this.wsOnError && this.ws.removeEventListener("error", this.wsOnError), this.ws.close(), this.ws = void 0), this.wsOnOpen = this.wsOnMessage = this.wsOnClose = this.wsOnError = void 0, this.sseAbort && (this.sseAbort.abort(), this.sseAbort = void 0), this.pollTimer && (clearTimeout(this.pollTimer), this.pollTimer = void 0), this.currentKind = null;
265
340
  }
266
341
  async startTransport(e) {
267
342
  if (this.stopped) return;
@@ -276,72 +351,105 @@ class P {
276
351
  }
277
352
  throw new Error("SpilkiWidget: unable to establish transport");
278
353
  }
354
+ // #5: store named handler refs for WS cleanup
279
355
  startWs(e) {
280
356
  return new Promise((t, s) => {
281
357
  try {
282
358
  const i = new WebSocket(e);
283
- this.ws = i, i.addEventListener("open", () => {
359
+ this.ws = i, this.wsOnOpen = () => {
284
360
  if (this.stopped) {
285
361
  i.close();
286
362
  return;
287
363
  }
288
- this.currentKind = "ws", this.handlers.onOpen("ws"), this.backoff = d, t();
289
- }), i.addEventListener("message", (n) => this.handleIncoming(n.data)), i.addEventListener("close", () => {
364
+ this.currentKind = "ws", this.handlers.onOpen("ws"), this.backoff = f, t();
365
+ }, this.wsOnMessage = (o) => this.handleIncoming(o.data), this.wsOnClose = () => {
290
366
  this.stopped || this.retryFallback("ws");
291
- }), i.addEventListener("error", () => {
367
+ }, this.wsOnError = () => {
292
368
  this.handlers.onError(new Error("SpilkiWidget: websocket error")), i.readyState !== WebSocket.OPEN && s(new Error("WebSocket failed"));
293
- });
369
+ }, i.addEventListener("open", this.wsOnOpen), i.addEventListener("message", this.wsOnMessage), i.addEventListener("close", this.wsOnClose), i.addEventListener("error", this.wsOnError);
294
370
  } catch (i) {
295
371
  s(i);
296
372
  }
297
373
  });
298
374
  }
375
+ // SPLK-271: fetch-based SSE with X-Authorization header (EventSource can't send headers)
299
376
  startSse(e) {
300
377
  return new Promise((t, s) => {
301
- if (typeof EventSource == "undefined") {
302
- s(new Error("SSE not supported"));
303
- return;
304
- }
305
- const i = new EventSource(e);
306
- this.sse = i;
307
- let n = !1;
308
- i.addEventListener("open", () => {
309
- if (n = !0, this.stopped) {
310
- i.close();
378
+ const i = new AbortController();
379
+ this.sseAbort = i, fetch(e, {
380
+ headers: {
381
+ Accept: "text/event-stream",
382
+ "X-Authorization": `Bearer ${this.options.accessToken}`
383
+ },
384
+ signal: i.signal
385
+ }).then((o) => {
386
+ if (!o.ok || !o.body) {
387
+ s(new Error("SSE failed"));
311
388
  return;
312
389
  }
313
- this.currentKind = "sse", this.handlers.onOpen("sse"), this.backoff = d, t();
314
- }), i.addEventListener("message", (l) => this.handleIncoming(l.data)), i.addEventListener("error", () => {
315
- if (!n) {
316
- s(new Error("SSE failed"));
390
+ if (this.stopped) {
391
+ i.abort();
317
392
  return;
318
393
  }
319
- this.handlers.onError(new Error("SpilkiWidget: SSE error")), !this.stopped && this.retryFallback("sse");
394
+ this.currentKind = "sse", this.handlers.onOpen("sse"), this.backoff = f, t();
395
+ const c = o.body.getReader(), a = new TextDecoder();
396
+ let n = "";
397
+ const h = () => {
398
+ c.read().then(({ done: u, value: v }) => {
399
+ var w;
400
+ if (u || this.stopped) {
401
+ this.stopped || (this.handlers.onError(new Error("SpilkiWidget: SSE stream ended")), this.retryFallback("sse"));
402
+ return;
403
+ }
404
+ n += a.decode(v, { stream: !0 });
405
+ const d = n.split(`
406
+
407
+ `);
408
+ n = (w = d.pop()) != null ? w : "";
409
+ for (const k of d)
410
+ for (const m of k.split(`
411
+ `))
412
+ m.startsWith("data:") && this.handleIncoming(m.slice(5).trim());
413
+ h();
414
+ }).catch((u) => {
415
+ i.signal.aborted || (this.handlers.onError(u), this.stopped || this.retryFallback("sse"));
416
+ });
417
+ };
418
+ h();
419
+ }).catch((o) => {
420
+ i.signal.aborted || s(o);
320
421
  });
321
422
  });
322
423
  }
424
+ // #15: add auth header to poll fetch
323
425
  async startPoll(e) {
324
- this.currentKind = "poll", this.handlers.onOpen("poll"), this.backoff = d;
426
+ this.currentKind = "poll", this.handlers.onOpen("poll"), this.backoff = f;
325
427
  const t = async () => {
326
428
  if (!this.stopped)
327
429
  try {
328
- const s = await fetch(e);
430
+ const s = await fetch(e, {
431
+ headers: {
432
+ "X-Authorization": `Bearer ${this.options.accessToken}`
433
+ }
434
+ });
329
435
  if (!s.ok) throw new Error(`Poll failed ${s.status}`);
330
436
  const i = await s.json();
331
- u(i).forEach((n) => this.handlers.onMessage(n)), this.backoff = d;
437
+ y(i).forEach((o) => this.handlers.onMessage(o)), this.backoff = f;
332
438
  } catch (s) {
333
- this.handlers.onError(s), this.backoff = Math.min(this.backoff * 1.5, v);
439
+ this.handlers.onError(s), this.backoff = Math.min(this.backoff * 1.5, I);
334
440
  } finally {
335
441
  this.stopped || (this.pollTimer = window.setTimeout(t, this.backoff));
336
442
  }
337
443
  };
338
444
  await t();
339
445
  }
446
+ // #1: preserve backoff by calling stop(false); #8: check stopped before reconnect
340
447
  retryFallback(e) {
341
- this.stopped || (this.stop(), this.backoff = Math.min(this.backoff * 1.5, v), setTimeout(() => {
342
- this.handlers.onError(new Error(`SpilkiWidget: retrying after ${e}`)), this.connect().catch((t) => this.handlers.onError(t));
448
+ this.stopped || (this.stop(!1), this.backoff = Math.min(this.backoff * 1.5, I), this.retryTimer = window.setTimeout(() => {
449
+ this.stopped || (this.handlers.onError(new Error(`SpilkiWidget: retrying after ${e}`)), this.connect().catch((t) => this.handlers.onError(t)));
343
450
  }, this.backoff));
344
451
  }
452
+ // #17: don't display raw garbage on parse failure
345
453
  handleIncoming(e) {
346
454
  try {
347
455
  const t = JSON.parse(e);
@@ -351,13 +459,7 @@ class P {
351
459
  }
352
460
  this.dispatchIncoming(t);
353
461
  } catch {
354
- const t = {
355
- id: `${Date.now()}`,
356
- author: "bot",
357
- text: e,
358
- ts: Date.now()
359
- };
360
- this.handlers.onMessage(t);
462
+ console.error("SpilkiWidget: failed to parse incoming message", e);
361
463
  }
362
464
  }
363
465
  dispatchIncoming(e) {
@@ -369,18 +471,23 @@ class P {
369
471
  this.handlers.onMessage(e);
370
472
  }
371
473
  }
372
- const f = 30;
373
- class U {
474
+ const A = 30 * 60 * 1e3, x = 30;
475
+ class q {
374
476
  constructor(e, t) {
375
477
  this.org = e, this.listeners = /* @__PURE__ */ new Set(), this.state = {
376
478
  isOpen: !1,
377
479
  isTyping: !1,
378
480
  isConnected: !1,
379
481
  messages: []
380
- }, this.historyKey = `spilki-history:${e}`, this.sessionKey = `spilki-session:${e}`, this.tokenKey = `spilki-token:${e}`, this.persist = t.persist, this.state.messages = this.loadMessages();
482
+ }, this.historyKey = `spilki-history:${e}`, this.sessionKey = `spilki-session:${e}`, this.tokenKey = `spilki-token:${e}`, this.activityKey = `spilki-activity:${e}`, this.persist = t.persist, this.state.messages = this.loadMessages();
381
483
  }
382
484
  get snapshot() {
383
- return { ...this.state, messages: [...this.state.messages] };
485
+ return {
486
+ isOpen: this.state.isOpen,
487
+ isTyping: this.state.isTyping,
488
+ isConnected: this.state.isConnected,
489
+ messages: this.state.messages
490
+ };
384
491
  }
385
492
  subscribe(e) {
386
493
  return this.listeners.add(e), () => this.listeners.delete(e);
@@ -400,15 +507,15 @@ class U {
400
507
  addMessage(e) {
401
508
  var s, i;
402
509
  const t = {
403
- id: (s = e.id) != null ? s : B("msg"),
510
+ id: (s = e.id) != null ? s : P("msg"),
404
511
  ts: (i = e.ts) != null ? i : Date.now(),
405
512
  author: e.author,
406
513
  text: e.text
407
514
  };
408
- return this.state.messages = u([...this.state.messages, t], f), this.persistMessages(), this.emit(), t;
515
+ return this.state.messages = y([...this.state.messages, t], x), this.persistMessages(), this.touchActivity(), this.emit(), t;
409
516
  }
410
517
  setMessages(e) {
411
- this.state.messages = u(e, f), this.persistMessages(), this.emit();
518
+ this.state.messages = y(e, x), this.persistMessages(), this.emit();
412
519
  }
413
520
  clearMessages() {
414
521
  this.state.messages = [], this.persistMessages(), this.emit();
@@ -461,6 +568,36 @@ class U {
461
568
  console.error("SpilkiWidget: unable to remove item", e);
462
569
  }
463
570
  }
571
+ get lastActivityTs() {
572
+ if (!this.persist) return 0;
573
+ try {
574
+ const e = localStorage.getItem(this.activityKey);
575
+ return e ? Number(e) : 0;
576
+ } catch {
577
+ return 0;
578
+ }
579
+ }
580
+ touchActivity() {
581
+ if (this.persist)
582
+ try {
583
+ localStorage.setItem(this.activityKey, String(Date.now()));
584
+ } catch (e) {
585
+ console.error("SpilkiWidget: unable to set item", e);
586
+ }
587
+ }
588
+ isSessionExpired() {
589
+ const e = this.lastActivityTs;
590
+ return e === 0 ? !1 : Date.now() - e >= A;
591
+ }
592
+ getConversationGroups() {
593
+ const e = this.state.messages;
594
+ if (e.length === 0) return [];
595
+ const t = [];
596
+ let s = { startTs: e[0].ts, messages: [e[0]] };
597
+ for (let i = 1; i < e.length; i++)
598
+ e[i].ts - e[i - 1].ts >= A ? (t.push(s), s = { startTs: e[i].ts, messages: [e[i]] }) : s.messages.push(e[i]);
599
+ return t.push(s), t;
600
+ }
464
601
  emit() {
465
602
  this.listeners.forEach((e) => e());
466
603
  }
@@ -478,13 +615,13 @@ class U {
478
615
  const e = localStorage.getItem(this.historyKey);
479
616
  if (!e) return [];
480
617
  const t = JSON.parse(e);
481
- return Array.isArray(t) ? u(t, f) : [];
618
+ return Array.isArray(t) ? y(t, x) : [];
482
619
  } catch (e) {
483
620
  return console.error("SpilkiWidget: unable to load messages", e), [];
484
621
  }
485
622
  }
486
623
  }
487
- function N(r) {
624
+ function _(r) {
488
625
  const e = r.replace(/-/g, "+").replace(/_/g, "/"), t = e.padEnd(e.length + (4 - e.length % 4) % 4, "=");
489
626
  if (typeof atob == "function")
490
627
  return decodeURIComponent(
@@ -495,24 +632,24 @@ function N(r) {
495
632
  return s.from(t, "base64").toString("utf8");
496
633
  throw new Error("SpilkiWidget: no base64 decoder available");
497
634
  }
498
- function D(r) {
635
+ function j(r) {
499
636
  if (!r) return null;
500
637
  const e = r.split(".");
501
638
  if (e.length < 2) return null;
502
639
  try {
503
- const t = N(e[1]);
640
+ const t = _(e[1]);
504
641
  return JSON.parse(t);
505
642
  } catch (t) {
506
643
  return console.error("SpilkiWidget: unable to parse JWT", t), null;
507
644
  }
508
645
  }
509
- function F(r) {
510
- const e = D(r);
511
- if (!(e != null && e.exp)) return !1;
646
+ function Y(r) {
647
+ const e = j(r);
648
+ if (!(e != null && e.exp) || typeof e.exp != "number") return !0;
512
649
  const t = Math.floor(Date.now() / 1e3);
513
- return e.exp < t;
650
+ return e.exp < t + 60;
514
651
  }
515
- const z = {
652
+ const R = {
516
653
  onOpen() {
517
654
  },
518
655
  onClose() {
@@ -524,7 +661,7 @@ const z = {
524
661
  onTransportChange() {
525
662
  }
526
663
  };
527
- async function H(r, e, t) {
664
+ async function X(r, e, t) {
528
665
  const s = `${r.replace(/\/$/, "")}/widget/install`, i = await fetch(s, {
529
666
  method: "POST",
530
667
  headers: { "Content-Type": "application/json", Origin: window.location.origin },
@@ -533,7 +670,7 @@ async function H(r, e, t) {
533
670
  if (!i.ok) throw new Error(`SpilkiWidget: install failed (${i.status})`);
534
671
  return (await i.json()).accessToken;
535
672
  }
536
- async function q(r, e) {
673
+ async function G(r, e) {
537
674
  const t = `${r.replace(/\/$/, "")}/widget/refresh`, s = await fetch(t, {
538
675
  method: "POST",
539
676
  headers: { "X-Authorization": `Bearer ${e}`, Origin: window.location.origin }
@@ -541,43 +678,52 @@ async function q(r, e) {
541
678
  if (!s.ok) throw new Error(`SpilkiWidget: refresh failed (${s.status})`);
542
679
  return (await s.json()).accessToken;
543
680
  }
544
- function E(r) {
545
- var k, w, y, S;
681
+ function M(r) {
682
+ var m, E, T, O;
546
683
  if (!r.org)
547
684
  throw new Error("SpilkiWidget: org is required");
548
- const e = $(r);
549
- K(e.allowedOriginsHint);
550
- const t = { ...z, ...(k = e.hooks) != null ? k : {} }, s = new U(e.org, { persist: e.persist });
551
- let i = (w = s.accessToken) != null ? w : void 0;
552
- const n = async () => {
553
- if (!i) {
554
- if (!r.installationToken) throw new Error("SpilkiWidget: missing installationToken");
555
- if (!r.org) throw new Error("SpilkiWidget: missing org");
556
- i = await H(e.apiBase, r.installationToken, r.org), s.persistAccessToken(i), h.setAccessToken(i);
557
- return;
685
+ const e = K(r);
686
+ N(e.allowedOriginsHint);
687
+ const t = { ...R, ...(m = e.hooks) != null ? m : {} }, s = new q(e.org, { persist: e.persist });
688
+ let i = (E = s.accessToken) != null ? E : void 0, o = null;
689
+ const c = async () => o || (o = (async () => {
690
+ try {
691
+ if (i && Y(i))
692
+ try {
693
+ i = await G(e.apiBase, i), s.persistAccessToken(i), h.setAccessToken(i);
694
+ return;
695
+ } catch {
696
+ i = void 0, s.clearAccessToken();
697
+ }
698
+ if (!i) {
699
+ if (!r.installationToken) throw new Error("SpilkiWidget: missing installationToken");
700
+ if (!r.org) throw new Error("SpilkiWidget: missing org");
701
+ i = await X(e.apiBase, r.installationToken, r.org), s.persistAccessToken(i), h.setAccessToken(i);
702
+ }
703
+ } finally {
704
+ o = null;
558
705
  }
559
- F(i) && (i = await q(e.apiBase, i), s.persistAccessToken(i), h.setAccessToken(i));
560
- }, l = A({
706
+ })(), o), a = L({
561
707
  color: e.color,
562
- position: (y = e.position) != null ? y : "bottom-right",
708
+ position: (T = e.position) != null ? T : "bottom-right",
563
709
  onClick: () => {
564
710
  s.snapshot.isOpen ? (s.close(), t.onClose()) : (s.open(), t.onOpen());
565
711
  }
566
- }), a = new C({
712
+ }), n = new z({
567
713
  color: e.color,
568
- theme: x(e.theme),
569
- position: (S = e.position) != null ? S : "bottom-right",
714
+ theme: C(e.theme),
715
+ position: (O = e.position) != null ? O : "bottom-right",
570
716
  i18n: e.i18n,
571
717
  onClose: () => {
572
718
  s.close(), t.onClose();
573
719
  },
574
- onSend: (o) => {
575
- const c = s.addMessage({ author: "user", text: o });
576
- a.appendMessage(c), n().then(() => h.send(o)).catch((I) => {
577
- t.onError(I), s.setConnected(!1), a.setOffline(!0);
720
+ onSend: (l) => {
721
+ const p = s.addMessage({ author: "user", text: l });
722
+ n.appendMessage(p), c().then(() => h.send(l, p.id)).catch(($) => {
723
+ t.onError($), s.setConnected(!1), n.setOffline(!0);
578
724
  });
579
725
  }
580
- }), h = new P(
726
+ }), h = new J(
581
727
  {
582
728
  apiBase: e.apiBase,
583
729
  accessToken: i,
@@ -585,46 +731,45 @@ function E(r) {
585
731
  sessionId: s.sessionId
586
732
  },
587
733
  {
588
- onOpen(o) {
589
- m.transport = o, t.onTransportChange(o), s.setConnected(!0), a.setOffline(!1);
734
+ onOpen(l) {
735
+ k.transport = l, t.onTransportChange(l), s.setConnected(!0), n.setOffline(!1);
590
736
  },
591
- onMessage(o) {
592
- const c = s.addMessage(o);
593
- a.appendMessage(c), t.onMessage(c);
737
+ onMessage(l) {
738
+ const p = s.addMessage(l);
739
+ n.appendMessage(p), t.onMessage(p);
594
740
  },
595
- onTyping(o) {
596
- s.setTyping(o);
741
+ onTyping(l) {
742
+ s.setTyping(l);
597
743
  },
598
- onError(o) {
599
- t.onError(o), s.setConnected(!1), s.snapshot.isOpen && a.setOffline(!0);
744
+ onError(l) {
745
+ t.onError(l), s.setConnected(!1), s.snapshot.isOpen && n.setOffline(!0);
600
746
  }
601
747
  }
602
748
  );
603
- l.mount(), a.mount();
604
- const b = s.snapshot.messages;
605
- if (b.length === 0 && e.welcome) {
606
- const o = {
749
+ a.mount(), n.mount();
750
+ const u = s.snapshot.messages, v = s.isSessionExpired(), d = s.getConversationGroups();
751
+ if (u.length === 0 && e.welcome) {
752
+ const l = {
607
753
  id: "welcome",
608
754
  author: "bot",
609
755
  text: e.welcome,
610
756
  ts: Date.now()
611
757
  };
612
- s.addMessage(o), a.appendMessage(o);
613
- } else
614
- a.updateMessages(b);
615
- const T = s.subscribe(() => {
616
- const o = s.snapshot;
617
- l.setOpen(o.isOpen), a.setTyping(o.isTyping), a.setOffline(!o.isConnected), a.updateTheme(x(e.theme)), o.isOpen ? a.show() : a.hide();
758
+ s.addMessage(l), n.appendMessage(l);
759
+ } else v && u.length > 0 ? n.renderWithConversations(d, []) : d.length > 1 ? n.renderWithConversations(d.slice(0, -1), d[d.length - 1].messages) : n.updateMessages(u);
760
+ const w = s.subscribe(() => {
761
+ const l = s.snapshot;
762
+ a.setOpen(l.isOpen), n.setTyping(l.isTyping), n.setOffline(!l.isConnected), n.updateTheme(C(e.theme)), l.isOpen ? n.show() : n.hide();
618
763
  });
619
- n().then(
620
- () => h.connect().then((o) => {
621
- var c;
622
- e.persist && s.persistSession(o.sessionId), s.setConnected(!0), t.onTransportChange((c = h.kind) != null ? c : "ws");
764
+ c().then(
765
+ () => h.connect().then((l) => {
766
+ var p;
767
+ e.persist && s.persistSession(l.sessionId), s.setConnected(!0), t.onTransportChange((p = h.kind) != null ? p : "ws");
623
768
  })
624
- ).catch((o) => {
625
- t.onError(o), s.setConnected(!1), a.setOffline(!0);
769
+ ).catch((l) => {
770
+ t.onError(l), s.setConnected(!1), n.setOffline(!0);
626
771
  });
627
- const m = {
772
+ const k = {
628
773
  transport: null,
629
774
  open() {
630
775
  s.open(), t.onOpen();
@@ -633,12 +778,12 @@ function E(r) {
633
778
  s.close(), t.onClose();
634
779
  },
635
780
  destroy() {
636
- T(), h.stop(), l.destroy(), a.destroy();
781
+ w(), h.stop(), a.destroy(), n.destroy();
637
782
  }
638
783
  };
639
- return m;
784
+ return k;
640
785
  }
641
- function J() {
786
+ function V() {
642
787
  var s, i;
643
788
  if (typeof document == "undefined") return;
644
789
  const r = document.currentScript;
@@ -650,7 +795,7 @@ function J() {
650
795
  console.error("SpilkiWidget: data-org and is required for auto init");
651
796
  return;
652
797
  }
653
- E({
798
+ M({
654
799
  org: t,
655
800
  installationToken: e.installationToken,
656
801
  apiBase: e.apiBase,
@@ -658,13 +803,10 @@ function J() {
658
803
  theme: (i = e.theme) != null ? i : void 0
659
804
  });
660
805
  }
661
- if (typeof window != "undefined") {
662
- const r = window;
663
- r.SpilkiWidget = r.SpilkiWidget || {}, r.SpilkiWidget.init = (e) => E(e);
664
- }
665
- J();
806
+ typeof window != "undefined" && (window.SpilkiWidget = window.SpilkiWidget || {}, window.SpilkiWidget.init = (r) => M(r));
807
+ V();
666
808
  export {
667
- J as autoInit,
668
- E as initSpilkiWidget
809
+ V as autoInit,
810
+ M as initSpilkiWidget
669
811
  };
670
812
  //# sourceMappingURL=widget.es.js.map