@kavchat/widget 0.0.2 → 0.0.3

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/index.mjs CHANGED
@@ -1,3 +1,839 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/dom.ts
17
+ function h(tag, attrs, ...children) {
18
+ const el = document.createElement(tag);
19
+ if (attrs) {
20
+ for (const [k, v] of Object.entries(attrs)) {
21
+ if (v === void 0 || v === null || v === false) continue;
22
+ el.setAttribute(k, v === true ? "" : v);
23
+ }
24
+ }
25
+ append(el, children);
26
+ return el;
27
+ }
28
+ function append(el, children) {
29
+ for (const c of children) {
30
+ if (c === null || c === void 0 || c === false) continue;
31
+ el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
32
+ }
33
+ }
34
+ function icon(d) {
35
+ const svg = document.createElementNS(SVG_NS, "svg");
36
+ svg.setAttribute("viewBox", "0 0 24 24");
37
+ svg.setAttribute("width", "24");
38
+ svg.setAttribute("height", "24");
39
+ svg.setAttribute("aria-hidden", "true");
40
+ svg.setAttribute("focusable", "false");
41
+ const path = document.createElementNS(SVG_NS, "path");
42
+ path.setAttribute("d", d);
43
+ path.setAttribute("fill", "currentColor");
44
+ svg.appendChild(path);
45
+ return svg;
46
+ }
47
+ function safeUrl(raw, schemes) {
48
+ try {
49
+ const url = new URL(String(raw || "").trim());
50
+ return schemes.includes(url.protocol) ? url.href : null;
51
+ } catch (e) {
52
+ return null;
53
+ }
54
+ }
55
+ function richText(text) {
56
+ const frag = document.createDocumentFragment();
57
+ let last = 0;
58
+ URL_RE.lastIndex = 0;
59
+ let m;
60
+ while (m = URL_RE.exec(text)) {
61
+ const raw = m[0].replace(TRAILING, "");
62
+ const href = safeUrl(raw, ["https:"]);
63
+ if (!href || !raw) continue;
64
+ if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
65
+ const a = h("a", { href, target: "_blank", rel: "noopener noreferrer" }, raw);
66
+ frag.appendChild(a);
67
+ last = m.index + raw.length;
68
+ URL_RE.lastIndex = last;
69
+ }
70
+ if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
71
+ return frag;
72
+ }
73
+ var SVG_NS, ICONS, URL_RE, TRAILING;
74
+ var init_dom = __esm({
75
+ "src/dom.ts"() {
76
+ "use strict";
77
+ SVG_NS = "http://www.w3.org/2000/svg";
78
+ ICONS = {
79
+ chat: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z",
80
+ close: "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
81
+ send: "M2.01 21 23 12 2.01 3 2 10l15 2-15 2z"
82
+ };
83
+ URL_RE = /https:\/\/[^\s<>"'`]+/g;
84
+ TRAILING = /[.,;:!?)\]}'"»]+$/;
85
+ }
86
+ });
87
+
88
+ // src/storage.ts
89
+ function read(store, key) {
90
+ try {
91
+ const raw = store().getItem(key);
92
+ return raw ? JSON.parse(raw) : null;
93
+ } catch (e) {
94
+ return null;
95
+ }
96
+ }
97
+ function write(store, key, value) {
98
+ try {
99
+ store().setItem(key, JSON.stringify(value));
100
+ } catch (e) {
101
+ }
102
+ }
103
+ function randomId(length = 22) {
104
+ const bytes = new Uint8Array(length);
105
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(bytes);
106
+ else for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
107
+ let out = "";
108
+ for (let i = 0; i < length; i++) out += ID_CHARS[bytes[i] & 63];
109
+ return out;
110
+ }
111
+ var RELEASE_KEY, SESSION_KEY, SCREENS_KEY, local, session, readCachedRelease, writeCachedRelease, readScreensMark, writeScreensMark, readSession, writeSession, ID_CHARS;
112
+ var init_storage = __esm({
113
+ "src/storage.ts"() {
114
+ "use strict";
115
+ RELEASE_KEY = (pk) => `kavchat:release:${pk}`;
116
+ SESSION_KEY = (pk) => `kavchat:session:${pk}`;
117
+ SCREENS_KEY = (pk) => `kavchat:screens:${pk}`;
118
+ local = () => window.localStorage;
119
+ session = () => window.sessionStorage;
120
+ readCachedRelease = (pk) => read(local, RELEASE_KEY(pk));
121
+ writeCachedRelease = (pk, value) => write(local, RELEASE_KEY(pk), value);
122
+ readScreensMark = (pk) => read(local, SCREENS_KEY(pk));
123
+ writeScreensMark = (pk, value) => write(local, SCREENS_KEY(pk), value);
124
+ readSession = (pk) => read(session, SESSION_KEY(pk));
125
+ writeSession = (pk, value) => write(session, SESSION_KEY(pk), value);
126
+ ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
127
+ }
128
+ });
129
+
130
+ // src/live-client.ts
131
+ import {
132
+ LIVE_MESSAGE_MAX,
133
+ LIVE_TYPING_HIDE_MS,
134
+ LIVE_TYPING_INTERVAL_MS,
135
+ isLiveFinal,
136
+ liveAddPending,
137
+ liveApplyConversation,
138
+ liveApplyEvent,
139
+ liveApplySync,
140
+ liveBackoff,
141
+ liveSetPending,
142
+ liveStart
143
+ } from "@kavchat/core";
144
+ async function call(c, path, body) {
145
+ try {
146
+ const res = await fetch(liveUrl(c, path), {
147
+ method: body === void 0 ? "GET" : "POST",
148
+ cache: "no-store",
149
+ credentials: "omit",
150
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
151
+ });
152
+ const b = await res.json().catch(() => null);
153
+ return { status: res.status, ok: res.ok && !!b && b.success === true, data: b && b.data, code: String(b && b.code || ""), message: String(b && b.message || "") };
154
+ } catch (e) {
155
+ return { status: 0, ok: false, data: null, code: "network", message: "" };
156
+ }
157
+ }
158
+ var liveUrl, livePost, liveGet, retryable, sleep, LIVE_IDLE_MS, BUSY_POLL_MS, LiveConnection;
159
+ var init_live_client = __esm({
160
+ "src/live-client.ts"() {
161
+ "use strict";
162
+ init_storage();
163
+ liveUrl = (c, path) => `${c.apiBase.replace(/\/+$/, "")}/v1/client/${encodeURIComponent(c.publicKey)}/live/${path}`;
164
+ livePost = (c, path, body) => call(c, path, body);
165
+ liveGet = (c, path) => call(c, path);
166
+ retryable = (r) => r.status === 0 || r.status === 429 || r.status >= 500;
167
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms));
168
+ LIVE_IDLE_MS = 9e4;
169
+ BUSY_POLL_MS = 12e4;
170
+ LiveConnection = class {
171
+ constructor(api, credential, conversation, cb) {
172
+ this.api = api;
173
+ this.credential = credential;
174
+ this.cb = cb;
175
+ this.stopped = false;
176
+ this.random = Math.random;
177
+ this.es = null;
178
+ this.attempt = 0;
179
+ /** כישלונות זרם לפני ready, כשכרטיס הזרם התקבל: כנראה פרוקסי שחוסם SSE */
180
+ this.blocked = 0;
181
+ this.pollUntil = 0;
182
+ this.polling = false;
183
+ this.typingOn = false;
184
+ this.typingAt = 0;
185
+ this.numberTried = false;
186
+ this.onOnline = () => {
187
+ if (!this.es && !this.polling) this.schedule(0);
188
+ };
189
+ this.chat = liveStart(conversation);
190
+ window.addEventListener("online", this.onOnline);
191
+ }
192
+ get final() {
193
+ return isLiveFinal(this.chat.conversation.state);
194
+ }
195
+ body(extra) {
196
+ return { credential: this.credential, ...extra };
197
+ }
198
+ set(chat) {
199
+ if (chat === this.chat) return;
200
+ const before = this.chat;
201
+ this.chat = chat;
202
+ if (chat.events.length > before.events.length && chat.events.some((e) => e.type === "message" && e.author.kind === "agent" && !before.events.includes(e))) this.cb.typing(false);
203
+ this.cb.change(chat);
204
+ if (this.final) this.finish();
205
+ }
206
+ /** POST /live/sync עד hasMore=false. false: נכשל */
207
+ async sync() {
208
+ for (let i = 0; i < 50; i++) {
209
+ const r = await livePost(this.api, "sync", this.body({ after: this.chat.lastSeq }));
210
+ if (this.stopped) return false;
211
+ if (!r.ok) {
212
+ this.fail(r);
213
+ return false;
214
+ }
215
+ this.set(liveApplySync(this.chat, r.data));
216
+ if (!r.data.hasMore) return true;
217
+ }
218
+ return true;
219
+ }
220
+ /** קודים שמשנים את הזרימה (סעיף 2.5). true: טופל */
221
+ fail(r) {
222
+ if (r.code === "live_credential_invalid") {
223
+ if (!this.final) this.set(liveApplyConversation(this.chat, { ...this.chat.conversation, state: "closed", closeReason: this.chat.conversation.closeReason || "expired" }));
224
+ else this.finish();
225
+ return true;
226
+ }
227
+ if (r.code === "live_conversation_closed") {
228
+ void this.sync();
229
+ return true;
230
+ }
231
+ return false;
232
+ }
233
+ connect() {
234
+ if (this.stopped || this.final) return;
235
+ clearTimeout(this.timer);
236
+ if (navigator.onLine === false) return;
237
+ if (typeof EventSource !== "function" || Date.now() < this.pollUntil) void this.poll();
238
+ else void this.openStream();
239
+ }
240
+ async openStream() {
241
+ const r = await livePost(this.api, "stream-ticket", this.body());
242
+ if (this.stopped || this.final) return;
243
+ if (!r.ok) {
244
+ if (this.fail(r)) return;
245
+ if (r.code === "live_busy") this.pollUntil = Date.now() + BUSY_POLL_MS;
246
+ return this.retry();
247
+ }
248
+ const es = new EventSource(`${liveUrl(this.api, "stream")}?ticket=${encodeURIComponent(String(r.data.ticket))}&after=${this.chat.lastSeq}`);
249
+ this.es = es;
250
+ let ready = false;
251
+ const on = (name, fn) => es.addEventListener(name, (e) => {
252
+ if (this.es !== es) return;
253
+ this.touch();
254
+ let d;
255
+ try {
256
+ d = JSON.parse(e.data);
257
+ } catch (e2) {
258
+ return;
259
+ }
260
+ if (d && typeof d === "object") fn(d);
261
+ });
262
+ on("ready", (d) => {
263
+ ready = true;
264
+ this.blocked = 0;
265
+ this.attempt = 0;
266
+ this.cb.notice(null);
267
+ this.set(liveApplyConversation(this.chat, d.conversation));
268
+ });
269
+ on("event", (d) => this.set(liveApplyEvent(this.chat, d)));
270
+ on("state", (d) => this.set(liveApplyConversation(this.chat, d)));
271
+ on("typing", (d) => this.showTyping(d.on !== false));
272
+ on("bye", (d) => {
273
+ this.closeStream();
274
+ this.bye(d.reason);
275
+ });
276
+ es.onerror = () => {
277
+ if (this.es !== es) return;
278
+ this.closeStream();
279
+ if (!ready && ++this.blocked >= 3) this.pollUntil = Infinity;
280
+ this.cb.notice("reconnecting");
281
+ this.retry();
282
+ };
283
+ this.touch();
284
+ }
285
+ touch() {
286
+ clearTimeout(this.idle);
287
+ this.idle = setTimeout(() => {
288
+ if (!this.es) return;
289
+ this.closeStream();
290
+ this.schedule(0);
291
+ }, LIVE_IDLE_MS);
292
+ }
293
+ closeStream() {
294
+ clearTimeout(this.idle);
295
+ if (this.es) this.es.close();
296
+ this.es = null;
297
+ }
298
+ bye(reason) {
299
+ if (reason === "replaced") {
300
+ this.stop();
301
+ this.cb.notice("replaced");
302
+ } else if (reason === "expired" || reason === "deleted") {
303
+ this.set(liveApplyConversation(this.chat, { ...this.chat.conversation, state: "closed", closeReason: "expired" }));
304
+ } else this.retry();
305
+ }
306
+ retry() {
307
+ this.schedule(liveBackoff(this.attempt++, this.random));
308
+ }
309
+ /** אחרי השהיה: סנכרון לפי lastSeq, ואז זרם חדש */
310
+ schedule(ms) {
311
+ clearTimeout(this.timer);
312
+ if (this.stopped || this.final) return;
313
+ this.timer = setTimeout(async () => {
314
+ if (this.stopped || navigator.onLine === false) return;
315
+ if (await this.sync()) this.connect();
316
+ else if (!this.stopped && !this.final) this.retry();
317
+ }, ms);
318
+ }
319
+ /** long-poll כשאין EventSource, כשהזרם חסום, או לזמן מה אחרי live_busy */
320
+ async poll() {
321
+ if (this.polling) return;
322
+ this.polling = true;
323
+ while (!this.stopped && !this.final) {
324
+ if (typeof EventSource === "function" && Date.now() >= this.pollUntil) break;
325
+ const at = Date.now();
326
+ const before = this.chat;
327
+ const r = await livePost(this.api, "sync", this.body({ after: this.chat.lastSeq, wait: 25 }));
328
+ if (this.stopped) break;
329
+ if (!r.ok) {
330
+ this.polling = false;
331
+ if (!this.fail(r)) this.retry();
332
+ return;
333
+ }
334
+ this.attempt = 0;
335
+ this.set(liveApplySync(this.chat, r.data));
336
+ const same = this.chat.lastSeq === before.lastSeq && this.chat.conversation.state === before.conversation.state;
337
+ if (!(r.data && r.data.hasMore) && same && Date.now() - at < 1e3) await sleep(1e3);
338
+ }
339
+ this.polling = false;
340
+ if (!this.stopped && !this.final) this.connect();
341
+ }
342
+ showTyping(on) {
343
+ clearTimeout(this.typingTimer);
344
+ this.cb.typing(on);
345
+ if (on) this.typingTimer = setTimeout(() => this.cb.typing(false), LIVE_TYPING_HIDE_MS);
346
+ }
347
+ /** שליחת הודעה: מוצגת מיד כ"נשלח", עם clientId שנוצר פעם אחת ומשמש לכל ניסיון חוזר */
348
+ send(raw) {
349
+ const text = raw.trim();
350
+ if (!text) return "empty";
351
+ if (text.length > LIVE_MESSAGE_MAX) return "too_long";
352
+ if (this.final || this.stopped) return "closed";
353
+ const clientId = randomId(22);
354
+ this.set(liveAddPending(this.chat, clientId, text, (/* @__PURE__ */ new Date()).toISOString()));
355
+ this.typing(false);
356
+ void this.deliver(clientId, text, 0);
357
+ return "ok";
358
+ }
359
+ retrySend(clientId) {
360
+ const p = this.chat.pending.find((x) => x.clientId === clientId);
361
+ if (!p || p.status !== "failed" || this.final || this.stopped) return;
362
+ this.set(liveSetPending(this.chat, clientId, "sending"));
363
+ void this.deliver(clientId, p.text, 0);
364
+ }
365
+ async deliver(clientId, text, attempt) {
366
+ const r = await livePost(this.api, "messages", this.body({ clientId, text }));
367
+ if (this.stopped) return;
368
+ if (r.ok) {
369
+ this.set(liveApplyEvent(this.chat, r.data.event));
370
+ return;
371
+ }
372
+ if (retryable(r) && attempt < 5) {
373
+ await sleep(liveBackoff(attempt, this.random));
374
+ return this.deliver(clientId, text, attempt + 1);
375
+ }
376
+ this.set(liveSetPending(this.chat, clientId, "failed"));
377
+ this.fail(r);
378
+ }
379
+ /** "מקליד" לנציג: לכל היותר פעם ב-3 שניות, false כשהשדה התרוקן או נשלח. בלי ניסיונות חוזרים */
380
+ typing(on) {
381
+ if (this.chat.conversation.state !== "active" || this.stopped) return;
382
+ const now = Date.now();
383
+ if (on ? this.typingOn && now - this.typingAt < LIVE_TYPING_INTERVAL_MS : !this.typingOn) return;
384
+ this.typingOn = on;
385
+ this.typingAt = now;
386
+ void livePost(this.api, "typing", this.body({ on }));
387
+ }
388
+ /** "להשאיר הודעה במקום" או "סיום השיחה". בטוח לחזור עליהן */
389
+ async command(path) {
390
+ for (let a = 0; a < 4 && !this.stopped; a++) {
391
+ const r = await livePost(this.api, path, this.body());
392
+ if (r.ok) {
393
+ this.set(liveApplyConversation(this.chat, r.data && r.data.conversation));
394
+ return true;
395
+ }
396
+ if (!retryable(r)) return this.fail(r);
397
+ await sleep(liveBackoff(a, this.random));
398
+ }
399
+ return false;
400
+ }
401
+ /** מצב סופי: סוגרים את הזרם ומוחקים את הקרדנשל. אם מספר הפנייה עוד לא ידוע, סנכרון אחד אחרי 2 שניות */
402
+ finish() {
403
+ if (this.stopped) return;
404
+ this.closeStream();
405
+ clearTimeout(this.timer);
406
+ const ticket = this.chat.conversation.ticket;
407
+ if (this.chat.conversation.state === "ticket" && (!ticket || ticket.number == null) && !this.numberTried) {
408
+ this.numberTried = true;
409
+ this.timer = setTimeout(() => void this.sync().then(() => this.end()), 2e3);
410
+ return;
411
+ }
412
+ this.end();
413
+ }
414
+ end() {
415
+ if (this.stopped) return;
416
+ this.stop();
417
+ this.cb.ended();
418
+ }
419
+ stop() {
420
+ this.stopped = true;
421
+ this.closeStream();
422
+ clearTimeout(this.timer);
423
+ clearTimeout(this.typingTimer);
424
+ window.removeEventListener("online", this.onOnline);
425
+ }
426
+ };
427
+ }
428
+ });
429
+
430
+ // src/live-styles.ts
431
+ var LIVE_CSS;
432
+ var init_live_styles = __esm({
433
+ "src/live-styles.ts"() {
434
+ "use strict";
435
+ LIVE_CSS = `
436
+ .lstatus{flex:none;padding:8px 14px;font-size:13px;line-height:1.6;color:#374151;background:#fff;border-block-end:1px solid #e3e6ea}
437
+ .lsys{align-self:center;max-width:90%;text-align:center;font-size:12px;color:#6b7280;padding:2px 8px}
438
+ .lsep{margin-block:6px;border-block-start:1px solid #e3e6ea;padding-block-start:8px;align-self:stretch;max-width:none}
439
+ .lname{display:block;font-size:11px;font-weight:600;color:#6b7280}
440
+ .lp-sending{opacity:.65}
441
+ .lp-failed{opacity:.85;outline:1px solid #b42318}
442
+ .lp-failed .btn{display:block;margin-block-start:6px;padding:3px 8px;font-size:12px;color:#b42318;background:#fff;border-color:#b42318}
443
+ .ltyping{flex:none;padding:0 14px 4px;font-size:12px;color:#6b7280}
444
+ .lerror{flex:none;padding:0 14px}
445
+ .lerror:empty{display:none}
446
+ .lactions{flex:none;display:flex;flex-wrap:wrap;gap:6px;padding:4px 14px 8px}
447
+ .lactions:empty{display:none}
448
+ .lactions .btn{padding:6px 12px;font-size:14px}
449
+ .note{margin:0;font-size:14px;color:#4b5563}
450
+ .composer input:disabled{opacity:.5}
451
+ .send:disabled{opacity:.5;cursor:default}
452
+ .launcher.kc-unread::after{content:"";position:absolute;inset-block-start:4px;inset-inline-end:4px;width:12px;height:12px;border-radius:50%;background:#e11d48;border:2px solid #fff}
453
+ `;
454
+ }
455
+ });
456
+
457
+ // src/live.ts
458
+ var live_exports = {};
459
+ __export(live_exports, {
460
+ createLive: () => createLive
461
+ });
462
+ import { isLiveFinal as isLiveFinal2, liveBackoff as liveBackoff2, liveLines, liveStatus, liveUnavailableText, liveWaitLeft, uiTexts, withLiveChip, LIVE_AVAILABILITY_TTL_MS, LIVE_MESSAGE_MAX as LIVE_MESSAGE_MAX2 } from "@kavchat/core";
463
+ function readStored(pk) {
464
+ try {
465
+ const id = localStorage.getItem(INDEX(pk));
466
+ const raw = id && localStorage.getItem(RECORD(pk, id));
467
+ const s = raw ? JSON.parse(raw) : null;
468
+ if (s && s.id === id && /^lvc_/.test(s.credential) && Date.parse(s.expiresAt) > Date.now()) return s;
469
+ if (id) clearStored(pk, id);
470
+ } catch (e) {
471
+ }
472
+ return null;
473
+ }
474
+ function writeStored(pk, s) {
475
+ try {
476
+ localStorage.setItem(RECORD(pk, s.id), JSON.stringify(s));
477
+ localStorage.setItem(INDEX(pk), s.id);
478
+ } catch (e) {
479
+ }
480
+ }
481
+ function clearStored(pk, id, credential) {
482
+ try {
483
+ const raw = localStorage.getItem(RECORD(pk, id));
484
+ if (credential && raw && JSON.parse(raw).credential !== credential) return;
485
+ localStorage.removeItem(RECORD(pk, id));
486
+ if (localStorage.getItem(INDEX(pk)) === id) localStorage.removeItem(INDEX(pk));
487
+ } catch (e) {
488
+ }
489
+ }
490
+ function createLive(host) {
491
+ const pk = host.api.publicKey;
492
+ const t = { ...host.t, ...uiTexts(host.locale) };
493
+ const ui = host.ui;
494
+ const style = h("style", null, LIVE_CSS);
495
+ ui.root.appendChild(style);
496
+ let avail = null;
497
+ let inflight = null;
498
+ let conn = null;
499
+ let stored = null;
500
+ let view = null;
501
+ let typingOn = false;
502
+ let notice = null;
503
+ let seen = /* @__PURE__ */ new Set();
504
+ let lastState = "";
505
+ let ticker;
506
+ let deadlineSync = 0;
507
+ let destroyed = false;
508
+ let quiet = false;
509
+ const els = {};
510
+ function refresh() {
511
+ var _a;
512
+ if (avail && Date.now() - avail.at < LIVE_AVAILABILITY_TTL_MS) return Promise.resolve();
513
+ if (!inflight) {
514
+ const wasOpen = !!((_a = avail == null ? void 0 : avail.data) == null ? void 0 : _a.open);
515
+ inflight = liveGet(host.api, "availability").then((r) => {
516
+ var _a2;
517
+ inflight = null;
518
+ avail = { at: Date.now(), data: r.ok ? r.data : null };
519
+ if (!destroyed && wasOpen !== !!((_a2 = avail.data) == null ? void 0 : _a2.open)) host.renderChips();
520
+ });
521
+ }
522
+ return inflight;
523
+ }
524
+ function nextOpen(data) {
525
+ var _a;
526
+ const iso = (_a = data == null ? void 0 : data.departments.find((d) => d.nextOpenAt)) == null ? void 0 : _a.nextOpenAt;
527
+ const at = iso ? new Date(iso) : null;
528
+ if (!at || isNaN(at.getTime())) return null;
529
+ const today = at.toDateString() === (/* @__PURE__ */ new Date()).toDateString();
530
+ try {
531
+ return new Intl.DateTimeFormat(host.locale, { ...today ? {} : { weekday: "short" }, hour: "2-digit", minute: "2-digit" }).format(at);
532
+ } catch (e) {
533
+ return null;
534
+ }
535
+ }
536
+ const note = () => {
537
+ const d = avail == null ? void 0 : avail.data;
538
+ return d && d.enabled && !d.open ? liveUnavailableText(d.reason, t, d.reason === "closed" ? nextOpen(d) : null) : "";
539
+ };
540
+ function requestForm(effect, data) {
541
+ const guest = !host.opts.userToken;
542
+ const requestId = randomId(22);
543
+ const messageClientId = randomId(22);
544
+ const field = (id, label, control) => h("div", { class: "field" }, h("label", { for: id }, label), control);
545
+ const name = h("input", { id: "kl-name", type: "text", autocomplete: "name", maxlength: "100", dir: "auto" });
546
+ name.value = host.opts.userName || "";
547
+ const email = h("input", { id: "kl-email", type: "email", autocomplete: "email", dir: "auto" });
548
+ const phone = h("input", { id: "kl-phone", type: "tel", autocomplete: "tel", dir: "auto" });
549
+ const departments = data.departments.filter((d) => d.open);
550
+ let dept = null;
551
+ if (departments.length > 1) {
552
+ dept = h("select", { id: "kl-dept" });
553
+ for (const d of departments) dept.appendChild(h("option", { value: d.id }, d.name));
554
+ }
555
+ const message = h("textarea", { id: "kl-message", maxlength: String(LIVE_MESSAGE_MAX2), dir: "auto" });
556
+ message.value = effect.prefill || "";
557
+ const error = h("div", { class: "error", role: "alert" });
558
+ const submit = h("button", { type: "submit", class: "btn btn-primary" }, t["live.start"]);
559
+ const cancel = h("button", { type: "button", class: "btn" }, t["form.cancel"]);
560
+ cancel.addEventListener("click", () => host.showChat());
561
+ const form = h(
562
+ "form",
563
+ { class: "form", novalidate: true },
564
+ h("h2", null, t["live.talk"]),
565
+ guest ? h("p", { class: "note" }, t["live.intro"]) : null,
566
+ guest ? field("kl-name", t["form.name"], name) : null,
567
+ guest ? field("kl-email", t["form.email"], email) : null,
568
+ guest ? field("kl-phone", t["form.phone"], phone) : null,
569
+ dept ? field("kl-dept", t["live.department"], dept) : null,
570
+ field("kl-message", t["live.message"], message),
571
+ error,
572
+ h("div", { class: "actions" }, submit, cancel)
573
+ );
574
+ form.addEventListener("submit", async (e) => {
575
+ var _a, _b;
576
+ e.preventDefault();
577
+ const pre = { name: name.value.trim(), email: email.value.trim(), phone: phone.value.trim(), message: message.value.trim() };
578
+ const problem = guest && !pre.email && !pre.phone ? t["form.contactRequired"] : guest && pre.email && !EMAIL_RE.test(pre.email) ? t["form.emailInvalid"] : "";
579
+ if (problem) {
580
+ error.textContent = problem;
581
+ return;
582
+ }
583
+ error.textContent = "";
584
+ submit.disabled = true;
585
+ submit.textContent = t["live.connecting"];
586
+ const body = {
587
+ requestId,
588
+ sessionId: (_a = host.session) == null ? void 0 : _a.id,
589
+ identityToken: host.opts.userToken,
590
+ ...guest ? { name: pre.name || void 0, email: pre.email || void 0, phone: pre.phone || void 0 } : {},
591
+ departmentId: dept ? dept.value : (_b = departments[0]) == null ? void 0 : _b.id,
592
+ topicId: effect.contactTopicId || void 0,
593
+ ...pre.message ? { message: pre.message, messageClientId } : {},
594
+ // השרת מוחק query ו-fragment, ובכל זאת לא שולחים אותם
595
+ page: location.href.split(/[?#]/)[0],
596
+ transcript: effect.transcript.slice(-200).map((x) => ({ ...x, text: x.text.slice(0, 1e3) }))
597
+ };
598
+ let r = await livePost(host.api, "requests", body);
599
+ for (let a = 0; a < 2 && retryable(r); a++) {
600
+ await sleep(liveBackoff2(a));
601
+ r = await livePost(host.api, "requests", body);
602
+ }
603
+ if (destroyed) return;
604
+ submit.disabled = false;
605
+ submit.textContent = t["live.start"];
606
+ if (r.ok && r.data.accepted) {
607
+ const d = r.data;
608
+ stored = { id: d.conversation.id, credential: d.credential, expiresAt: d.credentialExpiresAt, transcript: body.transcript };
609
+ writeStored(pk, stored);
610
+ begin(d.conversation, false);
611
+ } else if (r.ok) {
612
+ const known = avail == null ? void 0 : avail.data;
613
+ avail = null;
614
+ host.showForm(effect, { ...pre, note: liveUnavailableText(r.data.reason, t, r.data.reason === "closed" ? nextOpen(known) : null) });
615
+ } else {
616
+ error.textContent = r.code === "ticket_contact_required" ? t["form.contactRequired"] : r.code === "email_invalid" ? t["form.emailInvalid"] : r.code === "live_conversation_open" ? t["live.elsewhere"] : r.code === "network" ? t["form.error"] : r.message || t["form.error"];
617
+ }
618
+ });
619
+ ui.formView.replaceChildren(form);
620
+ ui.chatView.hidden = true;
621
+ ui.formView.hidden = false;
622
+ if (host.isOpen) (form.querySelector("input,select,textarea") || ui.panel).focus();
623
+ }
624
+ function begin(conversation, restoring) {
625
+ const mine = stored;
626
+ conn == null ? void 0 : conn.stop();
627
+ seen = /* @__PURE__ */ new Set();
628
+ notice = null;
629
+ typingOn = false;
630
+ const c = new LiveConnection(host.api, mine.credential, conversation, {
631
+ change: () => render(),
632
+ typing: (on) => {
633
+ typingOn = on;
634
+ renderTyping();
635
+ },
636
+ notice: (kind) => {
637
+ notice = kind;
638
+ render();
639
+ },
640
+ ended: () => {
641
+ clearStored(pk, mine.id, mine.credential);
642
+ render();
643
+ }
644
+ });
645
+ conn = c;
646
+ mountView();
647
+ if (!restoring) {
648
+ render();
649
+ c.connect();
650
+ }
651
+ return c;
652
+ }
653
+ function mountView() {
654
+ if (view) return;
655
+ els.status = h("div", { class: "lstatus" });
656
+ els.msgs = h("div", { class: "messages" });
657
+ els.typing = h("div", { class: "ltyping", "aria-hidden": "true" }, t["live.typing"]);
658
+ els.actions = h("div", { class: "lactions" });
659
+ els.error = h("div", { class: "error lerror", role: "alert" });
660
+ els.input = h("input", { type: "text", dir: "auto", maxlength: String(LIVE_MESSAGE_MAX2), autocomplete: "off", placeholder: t["live.placeholder"], "aria-label": t["live.placeholder"], enterkeyhint: "send" });
661
+ els.send = h("button", { type: "submit", class: "send", "aria-label": t["widget.send"] }, icon(ICONS.send));
662
+ const composer = h("form", { class: "composer" }, els.input, els.send);
663
+ composer.addEventListener("submit", (e) => {
664
+ e.preventDefault();
665
+ if (!conn) return;
666
+ const r = conn.send(els.input.value);
667
+ if (r === "ok") {
668
+ els.input.value = "";
669
+ els.error.textContent = "";
670
+ } else if (r === "too_long") els.error.textContent = t["live.tooLong"];
671
+ });
672
+ els.input.addEventListener("input", () => conn == null ? void 0 : conn.typing(els.input.value.trim() !== ""));
673
+ view = h("div", { class: "view lv" }, els.status, els.msgs, els.typing, els.error, els.actions, composer);
674
+ ui.panel.insertBefore(view, ui.live);
675
+ }
676
+ function button(label, fn, primary = false) {
677
+ const b = h("button", { type: "button", class: primary ? "btn btn-primary" : "btn" }, label);
678
+ b.addEventListener("click", fn);
679
+ return b;
680
+ }
681
+ function backToBot() {
682
+ if (conn) {
683
+ conn.stop();
684
+ if (stored) clearStored(pk, stored.id, stored.credential);
685
+ }
686
+ conn = null;
687
+ stored = null;
688
+ clearInterval(ticker);
689
+ ticker = void 0;
690
+ view == null ? void 0 : view.remove();
691
+ view = null;
692
+ host.showChat();
693
+ }
694
+ function renderTyping() {
695
+ if (!view || !conn) return;
696
+ els.typing.hidden = !(typingOn && conn.chat.conversation.state === "active");
697
+ }
698
+ function render() {
699
+ if (quiet || !view || !conn || !stored) return;
700
+ const c = conn.chat;
701
+ const conv = c.conversation;
702
+ const final = isLiveFinal2(conv.state);
703
+ const off = final || notice === "replaced";
704
+ ui.chatView.hidden = true;
705
+ ui.formView.hidden = true;
706
+ view.hidden = false;
707
+ renderStatus();
708
+ const atBottom = els.msgs.scrollHeight - els.msgs.scrollTop - els.msgs.clientHeight < 40;
709
+ els.msgs.textContent = "";
710
+ for (const m of stored.transcript) els.msgs.appendChild(h("div", { class: `msg msg-${m.from === "user" ? "user" : "bot"}`, dir: "auto" }, m.text));
711
+ if (stored.transcript.length) els.msgs.appendChild(h("div", { class: "lsys lsep" }, t["live.talk"]));
712
+ const fresh = [];
713
+ for (const l of liveLines(c, t)) {
714
+ let el;
715
+ if (l.kind === "system") el = h("div", { class: "lsys" }, l.text);
716
+ else if (l.kind === "agent") el = h("div", { class: "msg msg-bot", dir: "auto" }, h("span", { class: "lname" }, l.name || ""), l.text);
717
+ else {
718
+ el = h("div", { class: `msg msg-user${l.status ? ` lp-${l.status}` : ""}`, dir: "auto" }, l.text);
719
+ if (l.status === "failed") {
720
+ const id = l.clientId;
721
+ el.appendChild(button(t["live.failed"], () => conn == null ? void 0 : conn.retrySend(id)));
722
+ } else if (l.status === "sending") el.setAttribute("aria-busy", "true");
723
+ }
724
+ els.msgs.appendChild(el);
725
+ if (!seen.has(l.key)) {
726
+ seen.add(l.key);
727
+ if (l.kind !== "visitor") fresh.push(l.kind === "agent" ? `${l.name}: ${l.text}` : l.text);
728
+ }
729
+ }
730
+ if (fresh.length) {
731
+ ui.live.textContent = fresh.join("\n");
732
+ if (!host.isOpen) ui.launcher.classList.add("kc-unread");
733
+ }
734
+ if (atBottom || fresh.length) els.msgs.scrollTop = els.msgs.scrollHeight;
735
+ renderTyping();
736
+ els.actions.textContent = "";
737
+ if (off) els.actions.appendChild(button(t["live.back"], backToBot, true));
738
+ else {
739
+ if (conv.state === "queued") els.actions.appendChild(button(t["live.leave"], () => void (conn == null ? void 0 : conn.command("leave-message"))));
740
+ els.actions.appendChild(button(t["live.end"], () => void (conn == null ? void 0 : conn.command("end"))));
741
+ }
742
+ els.input.disabled = off;
743
+ els.send.disabled = off;
744
+ if (conv.state !== lastState) {
745
+ lastState = conv.state;
746
+ host.emit({ type: "live", state: conv.state, id: conv.id });
747
+ }
748
+ if (conv.state === "queued" && conv.queueDeadline && !ticker) ticker = setInterval(tick, 1e3);
749
+ if (conv.state !== "queued" && ticker) {
750
+ clearInterval(ticker);
751
+ ticker = void 0;
752
+ }
753
+ }
754
+ function renderStatus() {
755
+ if (!conn) return;
756
+ const conv = conn.chat.conversation;
757
+ els.status.textContent = notice === "replaced" ? t["live.replaced"] : `${liveStatus(conv, t, Date.now())}${notice === "reconnecting" && !isLiveFinal2(conv.state) ? ` \xB7 ${t["live.reconnecting"]}` : ""}`;
758
+ }
759
+ function tick() {
760
+ if (!conn) return;
761
+ renderStatus();
762
+ const left = liveWaitLeft(conn.chat.conversation, Date.now());
763
+ if (left === 0 && Date.now() - deadlineSync > 1e4) {
764
+ deadlineSync = Date.now();
765
+ void conn.sync();
766
+ }
767
+ }
768
+ return {
769
+ chips(chips) {
770
+ var _a, _b, _c;
771
+ if (!((_b = (_a = host.release) == null ? void 0 : _a.liveChat) == null ? void 0 : _b.enabled) || !chips.some((c) => c.kind === "contact")) return chips;
772
+ void refresh();
773
+ return ((_c = avail == null ? void 0 : avail.data) == null ? void 0 : _c.open) ? withLiveChip(chips, t["live.talk"]) : chips;
774
+ },
775
+ note,
776
+ async start(effect) {
777
+ await refresh();
778
+ if (destroyed) return;
779
+ const d = avail == null ? void 0 : avail.data;
780
+ if (d && d.open) requestForm(effect, d);
781
+ else host.showForm(effect, { note: note() });
782
+ },
783
+ async restore() {
784
+ const s = readStored(pk);
785
+ if (!s) return;
786
+ stored = s;
787
+ quiet = true;
788
+ const c = begin({ id: s.id, state: "queued", lastSeq: 0, credentialExpiresAt: s.expiresAt }, true);
789
+ const ok = await c.sync();
790
+ quiet = false;
791
+ if (destroyed || conn !== c) return;
792
+ if (!ok && c.final && c.chat.events.length === 0) {
793
+ c.stop();
794
+ clearStored(pk, s.id, s.credential);
795
+ conn = null;
796
+ stored = null;
797
+ view == null ? void 0 : view.remove();
798
+ view = null;
799
+ return;
800
+ }
801
+ for (const l of liveLines(c.chat, t)) seen.add(l.key);
802
+ render();
803
+ c.connect();
804
+ },
805
+ focus() {
806
+ if (!view || view.hidden) return false;
807
+ ui.launcher.classList.remove("kc-unread");
808
+ if (coarse()) ui.panel.focus();
809
+ else if (!els.input.disabled) els.input.focus();
810
+ else ui.panel.focus();
811
+ return true;
812
+ },
813
+ destroy() {
814
+ destroyed = true;
815
+ conn == null ? void 0 : conn.stop();
816
+ clearInterval(ticker);
817
+ view == null ? void 0 : view.remove();
818
+ style.remove();
819
+ }
820
+ };
821
+ }
822
+ var INDEX, RECORD, EMAIL_RE, coarse;
823
+ var init_live = __esm({
824
+ "src/live.ts"() {
825
+ "use strict";
826
+ init_dom();
827
+ init_live_client();
828
+ init_live_styles();
829
+ init_storage();
830
+ INDEX = (pk) => `kavchat:live:${pk}`;
831
+ RECORD = (pk, id) => `kavchat:live:${pk}:${id}`;
832
+ EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
833
+ coarse = () => typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
834
+ }
835
+ });
836
+
1
837
  // src/widget.ts
2
838
  import {
3
839
  UI_TEXTS,
@@ -11,7 +847,7 @@ import {
11
847
  pressChip,
12
848
  sendText,
13
849
  startConversation,
14
- uiTexts
850
+ uiTexts as uiTexts2
15
851
  } from "@kavchat/core";
16
852
 
17
853
  // src/api.ts
@@ -121,107 +957,9 @@ function themeFor(primaryColor) {
121
957
  return { primary, onPrimary, accent };
122
958
  }
123
959
 
124
- // src/dom.ts
125
- function h(tag, attrs, ...children) {
126
- const el = document.createElement(tag);
127
- if (attrs) {
128
- for (const [k, v] of Object.entries(attrs)) {
129
- if (v === void 0 || v === null || v === false) continue;
130
- el.setAttribute(k, v === true ? "" : v);
131
- }
132
- }
133
- append(el, children);
134
- return el;
135
- }
136
- function append(el, children) {
137
- for (const c of children) {
138
- if (c === null || c === void 0 || c === false) continue;
139
- el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
140
- }
141
- }
142
- var SVG_NS = "http://www.w3.org/2000/svg";
143
- function icon(d) {
144
- const svg = document.createElementNS(SVG_NS, "svg");
145
- svg.setAttribute("viewBox", "0 0 24 24");
146
- svg.setAttribute("width", "24");
147
- svg.setAttribute("height", "24");
148
- svg.setAttribute("aria-hidden", "true");
149
- svg.setAttribute("focusable", "false");
150
- const path = document.createElementNS(SVG_NS, "path");
151
- path.setAttribute("d", d);
152
- path.setAttribute("fill", "currentColor");
153
- svg.appendChild(path);
154
- return svg;
155
- }
156
- var ICONS = {
157
- chat: "M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z",
158
- close: "M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
159
- send: "M2.01 21 23 12 2.01 3 2 10l15 2-15 2z"
160
- };
161
- function safeUrl(raw, schemes) {
162
- try {
163
- const url = new URL(String(raw || "").trim());
164
- return schemes.includes(url.protocol) ? url.href : null;
165
- } catch (e) {
166
- return null;
167
- }
168
- }
169
- var URL_RE = /https:\/\/[^\s<>"'`]+/g;
170
- var TRAILING = /[.,;:!?)\]}'"»]+$/;
171
- function richText(text) {
172
- const frag = document.createDocumentFragment();
173
- let last = 0;
174
- URL_RE.lastIndex = 0;
175
- let m;
176
- while (m = URL_RE.exec(text)) {
177
- const raw = m[0].replace(TRAILING, "");
178
- const href = safeUrl(raw, ["https:"]);
179
- if (!href || !raw) continue;
180
- if (m.index > last) frag.appendChild(document.createTextNode(text.slice(last, m.index)));
181
- const a = h("a", { href, target: "_blank", rel: "noopener noreferrer" }, raw);
182
- frag.appendChild(a);
183
- last = m.index + raw.length;
184
- URL_RE.lastIndex = last;
185
- }
186
- if (last < text.length) frag.appendChild(document.createTextNode(text.slice(last)));
187
- return frag;
188
- }
189
-
190
- // src/storage.ts
191
- var RELEASE_KEY = (pk) => `kavchat:release:${pk}`;
192
- var SESSION_KEY = (pk) => `kavchat:session:${pk}`;
193
- var SCREENS_KEY = (pk) => `kavchat:screens:${pk}`;
194
- function read(store, key) {
195
- try {
196
- const raw = store().getItem(key);
197
- return raw ? JSON.parse(raw) : null;
198
- } catch (e) {
199
- return null;
200
- }
201
- }
202
- function write(store, key, value) {
203
- try {
204
- store().setItem(key, JSON.stringify(value));
205
- } catch (e) {
206
- }
207
- }
208
- var local = () => window.localStorage;
209
- var session = () => window.sessionStorage;
210
- var readCachedRelease = (pk) => read(local, RELEASE_KEY(pk));
211
- var writeCachedRelease = (pk, value) => write(local, RELEASE_KEY(pk), value);
212
- var readScreensMark = (pk) => read(local, SCREENS_KEY(pk));
213
- var writeScreensMark = (pk, value) => write(local, SCREENS_KEY(pk), value);
214
- var readSession = (pk) => read(session, SESSION_KEY(pk));
215
- var writeSession = (pk, value) => write(session, SESSION_KEY(pk), value);
216
- var ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";
217
- function randomId(length = 22) {
218
- const bytes = new Uint8Array(length);
219
- if (typeof crypto !== "undefined" && crypto.getRandomValues) crypto.getRandomValues(bytes);
220
- else for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
221
- let out = "";
222
- for (let i = 0; i < length; i++) out += ID_CHARS[bytes[i] & 63];
223
- return out;
224
- }
960
+ // src/widget.ts
961
+ init_dom();
962
+ init_storage();
225
963
 
226
964
  // src/styles.ts
227
965
  var CSS = `
@@ -288,6 +1026,9 @@ var CSS = `
288
1026
  @media (prefers-reduced-motion:reduce){.kc *{animation:none!important;transition:none!important}}
289
1027
  `;
290
1028
 
1029
+ // src/live-load.ts
1030
+ var loadLive = () => Promise.resolve().then(() => (init_live(), live_exports));
1031
+
291
1032
  // src/widget.ts
292
1033
  var DEFAULT_API = "https://api.kavchat.com";
293
1034
  var BRANDING_URL = "https://kavchat.com";
@@ -323,7 +1064,7 @@ function pickLocale(requested, releaseLocale) {
323
1064
  const fallback = baseLocale(releaseLocale);
324
1065
  return UI_TEXTS[fallback] ? fallback : "en";
325
1066
  }
326
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
1067
+ var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
327
1068
  var KavchatWidget = class {
328
1069
  constructor(options) {
329
1070
  this.release = null;
@@ -331,7 +1072,7 @@ var KavchatWidget = class {
331
1072
  this.ui = null;
332
1073
  this.state = null;
333
1074
  this.session = null;
334
- this.t = uiTexts("en");
1075
+ this.t = uiTexts2("en");
335
1076
  this.locale = "en";
336
1077
  this.isOpen = false;
337
1078
  this.prevFocus = null;
@@ -340,6 +1081,7 @@ var KavchatWidget = class {
340
1081
  this.destroyed = false;
341
1082
  /** מחכים לתשובת AI: מציגים "מקליד" ומתעלמים מקלט חדש */
342
1083
  this.waiting = false;
1084
+ this.live = null;
343
1085
  this.onPageHide = () => this.flush(true);
344
1086
  this.onVisibility = () => {
345
1087
  if (document.visibilityState === "hidden") this.flush(true);
@@ -410,6 +1152,7 @@ var KavchatWidget = class {
410
1152
  if (this.reportTimer) clearTimeout(this.reportTimer);
411
1153
  window.removeEventListener("pagehide", this.onPageHide);
412
1154
  document.removeEventListener("visibilitychange", this.onVisibility);
1155
+ this.setupLive();
413
1156
  this.unmount();
414
1157
  }
415
1158
  // ---------- טעינה ----------
@@ -456,16 +1199,40 @@ var KavchatWidget = class {
456
1199
  const changed = !this.release || this.release.version !== release.version;
457
1200
  this.release = release;
458
1201
  this.locale = pickLocale(this.opts.locale, release.locale);
459
- this.t = uiTexts(this.locale);
1202
+ this.t = uiTexts2(this.locale);
460
1203
  if (changed || !this.ui) {
461
1204
  this.state = null;
462
1205
  this.restoreSession();
463
1206
  this.mount();
1207
+ this.setupLive();
464
1208
  }
465
1209
  if (this.ui) this.ui.banner.hidden = !this.offline;
466
1210
  this.emit({ type: "ready", version: release.version, offline });
467
1211
  if (this.wantOpen) this.open();
468
1212
  }
1213
+ /**
1214
+ * הצ'אט עם נציג נטען רק כשהוא מופעל בגרסה, או כשיש שיחה פתוחה שמורה. כל mount מקבל בקר חדש,
1215
+ * שמשחזר את השיחה מהאחסון. כישלון בטעינה משאיר את הבוט ואת טופס הפנייה כמו שהם
1216
+ */
1217
+ setupLive() {
1218
+ if (this.live) this.live.destroy();
1219
+ this.live = null;
1220
+ const ui = this.ui;
1221
+ const lc = this.release && this.release.liveChat;
1222
+ let saved;
1223
+ try {
1224
+ saved = localStorage.getItem(`kavchat:live:${this.opts.publicKey}`);
1225
+ } catch (e) {
1226
+ }
1227
+ if (this.destroyed || !ui || !(lc && lc.enabled || saved)) return;
1228
+ loadLive().then((m) => {
1229
+ if (this.ui !== ui || this.destroyed) return;
1230
+ this.live = m.createLive(this);
1231
+ void this.live.restore();
1232
+ this.renderChips();
1233
+ }, () => {
1234
+ });
1235
+ }
469
1236
  restoreSession() {
470
1237
  const release = this.release;
471
1238
  if (!release) return;
@@ -541,7 +1308,9 @@ var KavchatWidget = class {
541
1308
  handleEffect(effect) {
542
1309
  var _a, _b, _c;
543
1310
  this.emit({ type: "effect", effect });
544
- if (effect.type === "contact") {
1311
+ if (effect.type === "live" && this.live) {
1312
+ void this.live.start(effect);
1313
+ } else if (effect.type === "contact" || effect.type === "live") {
545
1314
  this.showForm(effect);
546
1315
  } else if (effect.type === "link") {
547
1316
  openLink(effect.url, (_a = this.ui) == null ? void 0 : _a.root);
@@ -652,12 +1421,12 @@ var KavchatWidget = class {
652
1421
  }
653
1422
  focusInside() {
654
1423
  const ui = this.ui;
655
- if (!ui) return;
656
- const coarse = typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
1424
+ if (!ui || this.live && this.live.focus()) return;
1425
+ const coarse2 = typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
657
1426
  if (!ui.formView.hidden) {
658
1427
  const first = ui.formView.querySelector("input,select,textarea");
659
1428
  (first || ui.panel).focus();
660
- } else if (coarse) ui.panel.focus();
1429
+ } else if (coarse2) ui.panel.focus();
661
1430
  else ui.input.focus();
662
1431
  }
663
1432
  render() {
@@ -690,12 +1459,13 @@ var KavchatWidget = class {
690
1459
  const state = this.state;
691
1460
  if (!ui || !state) return;
692
1461
  ui.chips.textContent = "";
693
- for (const chip of state.chips) {
1462
+ const chips = this.live ? this.live.chips(state.chips) : state.chips;
1463
+ for (const chip of chips) {
694
1464
  const b = h("button", { type: "button", class: `chip chip-${chip.kind}` }, chip.label);
695
1465
  b.addEventListener("click", () => this.onChip(chip));
696
1466
  ui.chips.appendChild(b);
697
1467
  }
698
- ui.chips.hidden = state.chips.length === 0;
1468
+ ui.chips.hidden = chips.length === 0;
699
1469
  }
700
1470
  showChat() {
701
1471
  const ui = this.ui;
@@ -706,7 +1476,8 @@ var KavchatWidget = class {
706
1476
  this.render();
707
1477
  if (this.isOpen) this.focusInside();
708
1478
  }
709
- showForm(effect) {
1479
+ /** pre: מה שהמשתמש כבר כתב בבקשה לנציג, ולמה אין נציג (note) */
1480
+ showForm(effect, pre = {}) {
710
1481
  const ui = this.ui;
711
1482
  const release = this.release;
712
1483
  if (!ui || !release) return;
@@ -714,9 +1485,12 @@ var KavchatWidget = class {
714
1485
  const topics = release.settings.contactTopics || [];
715
1486
  const field = (id, label, control) => h("div", { class: "field" }, h("label", { for: id }, label), control);
716
1487
  const name = h("input", { id: "kc-name", type: "text", autocomplete: "name", maxlength: "100", dir: "auto" });
717
- name.value = this.opts.userName || "";
1488
+ name.value = pre.name || this.opts.userName || "";
718
1489
  const email = h("input", { id: "kc-email", type: "email", autocomplete: "email", dir: "auto" });
1490
+ email.value = pre.email || "";
719
1491
  const phone = h("input", { id: "kc-phone", type: "tel", autocomplete: "tel", dir: "auto" });
1492
+ phone.value = pre.phone || "";
1493
+ const note = "note" in pre ? pre.note : this.live && this.live.note();
720
1494
  let topic = null;
721
1495
  if (topics.length) {
722
1496
  topic = h("select", { id: "kc-topic" }, h("option", { value: "" }, ""));
@@ -724,7 +1498,7 @@ var KavchatWidget = class {
724
1498
  if (effect.contactTopicId && topics.some((tp) => tp.id === effect.contactTopicId)) topic.value = effect.contactTopicId;
725
1499
  }
726
1500
  const message = h("textarea", { id: "kc-message", required: true, maxlength: "2000", dir: "auto" });
727
- message.value = effect.prefill || "";
1501
+ message.value = pre.message || effect.prefill || "";
728
1502
  const error = h("div", { class: "error", role: "alert" });
729
1503
  const submit = h("button", { type: "submit", class: "btn btn-primary" }, t["form.submit"]);
730
1504
  const cancel = h("button", { type: "button", class: "btn" }, t["form.cancel"]);
@@ -733,6 +1507,7 @@ var KavchatWidget = class {
733
1507
  "form",
734
1508
  { class: "form", novalidate: true },
735
1509
  h("h2", null, t["form.title"]),
1510
+ note ? h("p", { class: "note", role: "status" }, note) : null,
736
1511
  field("kc-name", t["form.name"], name),
737
1512
  field("kc-email", t["form.email"], email),
738
1513
  field("kc-phone", t["form.phone"], phone),
@@ -754,7 +1529,7 @@ var KavchatWidget = class {
754
1529
  sessionId: (_a = this.session) == null ? void 0 : _a.id,
755
1530
  transcript: effect.transcript.slice(-200).map((x) => ({ ...x, text: x.text.slice(0, 1e3) }))
756
1531
  };
757
- const problem = !input.message ? t["form.required"] : !input.email && !input.phone && !this.opts.userToken ? t["form.contactRequired"] : input.email && !EMAIL_RE.test(input.email) ? t["form.emailInvalid"] : "";
1532
+ const problem = !input.message ? t["form.required"] : !input.email && !input.phone && !this.opts.userToken ? t["form.contactRequired"] : input.email && !EMAIL_RE2.test(input.email) ? t["form.emailInvalid"] : "";
758
1533
  if (problem) {
759
1534
  error.textContent = problem;
760
1535
  return;
@@ -871,8 +1646,8 @@ function installGlobal(win = window) {
871
1646
  win.Kavchat = api;
872
1647
  return { api, queue };
873
1648
  }
874
- function run(api, call) {
875
- const [name, ...args] = call;
1649
+ function run(api, call2) {
1650
+ const [name, ...args] = call2;
876
1651
  if (!METHODS.includes(name)) {
877
1652
  console.warn(`Kavchat: unknown method ${name}`);
878
1653
  return;