@kavchat/widget 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +162 -2
- package/dist/index.mjs +901 -0
- package/dist/kavchat.js +65 -0
- package/dist/types/api.d.ts +68 -0
- package/dist/types/boot.d.ts +7 -0
- package/dist/types/color.d.ts +15 -0
- package/dist/types/dom.d.ts +22 -0
- package/dist/types/embed.d.ts +1 -0
- package/dist/types/global.d.ts +36 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/storage.d.ts +26 -0
- package/dist/types/styles.d.ts +7 -0
- package/dist/types/widget.d.ts +134 -0
- package/package.json +51 -4
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
// src/widget.ts
|
|
2
|
+
import {
|
|
3
|
+
UI_TEXTS,
|
|
4
|
+
aiQuestion,
|
|
5
|
+
answerWithAi,
|
|
6
|
+
configureEngine,
|
|
7
|
+
eventsPayload,
|
|
8
|
+
format,
|
|
9
|
+
isRtl,
|
|
10
|
+
pendingQuestion,
|
|
11
|
+
pressChip,
|
|
12
|
+
sendText,
|
|
13
|
+
startConversation,
|
|
14
|
+
uiTexts
|
|
15
|
+
} from "@kavchat/core";
|
|
16
|
+
|
|
17
|
+
// src/api.ts
|
|
18
|
+
var endpoint = (c, path) => `${c.apiBase.replace(/\/+$/, "")}/v1/client/${encodeURIComponent(c.publicKey)}/${path}`;
|
|
19
|
+
function headers(c, extra = {}) {
|
|
20
|
+
const token = c.getToken();
|
|
21
|
+
return token ? { ...extra, Authorization: `Bearer ${token}` } : extra;
|
|
22
|
+
}
|
|
23
|
+
async function fetchRelease(c, etag) {
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(endpoint(c, "release"), {
|
|
27
|
+
method: "GET",
|
|
28
|
+
cache: "no-store",
|
|
29
|
+
credentials: "omit",
|
|
30
|
+
headers: headers(c, etag ? { "If-None-Match": etag } : {})
|
|
31
|
+
});
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { kind: "offline" };
|
|
34
|
+
}
|
|
35
|
+
if (res.status === 304) return { kind: "not-modified" };
|
|
36
|
+
if (res.status >= 500 || res.status === 429) return { kind: "offline" };
|
|
37
|
+
if (!res.ok) return { kind: "denied", status: res.status };
|
|
38
|
+
const body = await res.json().catch(() => null);
|
|
39
|
+
if (!body || body.success !== true || !body.data || typeof body.data.enabled !== "boolean") return { kind: "offline" };
|
|
40
|
+
return { kind: "ok", release: body.data, etag: res.headers.get("ETag") || "" };
|
|
41
|
+
}
|
|
42
|
+
function postJson(c, path, body, keepalive = false, signal) {
|
|
43
|
+
return fetch(endpoint(c, path), {
|
|
44
|
+
method: "POST",
|
|
45
|
+
cache: "no-store",
|
|
46
|
+
credentials: "omit",
|
|
47
|
+
keepalive,
|
|
48
|
+
signal,
|
|
49
|
+
headers: headers(c, { "Content-Type": "application/json" }),
|
|
50
|
+
body: JSON.stringify(body)
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
var AI_TIMEOUT_MS = 2e4;
|
|
54
|
+
async function postAiAnswer(c, sessionId, question) {
|
|
55
|
+
const ctl = new AbortController();
|
|
56
|
+
const timer = setTimeout(() => ctl.abort(), AI_TIMEOUT_MS);
|
|
57
|
+
try {
|
|
58
|
+
const res = await postJson(c, "ai/answer", { sessionId, question }, false, ctl.signal);
|
|
59
|
+
const d = res.ok && (await res.json()).data;
|
|
60
|
+
return d && typeof d.answer === "string" && d.answer.trim() ? { answer: d.answer, scriptId: d.scriptId || null } : null;
|
|
61
|
+
} catch (e) {
|
|
62
|
+
return null;
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function postEvents(c, payload, keepalive = false) {
|
|
68
|
+
try {
|
|
69
|
+
const res = await postJson(c, "events", payload, keepalive);
|
|
70
|
+
return res.ok;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function postScreens(c, screens) {
|
|
76
|
+
try {
|
|
77
|
+
const res = await postJson(c, "screens", { platform: "web", screens });
|
|
78
|
+
return res.ok;
|
|
79
|
+
} catch (e) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function postTicket(c, input) {
|
|
84
|
+
try {
|
|
85
|
+
const res = await postJson(c, "tickets", input);
|
|
86
|
+
const body = await res.json().catch(() => null);
|
|
87
|
+
if (res.ok && body && body.success && body.data) return { ok: true, id: String(body.data.id), number: Number(body.data.number) };
|
|
88
|
+
return { ok: false, code: String((body == null ? void 0 : body.code) || `http_${res.status}`), message: String((body == null ? void 0 : body.message) || "") };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return { ok: false, code: "network", message: "" };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/color.ts
|
|
95
|
+
var DEFAULT_PRIMARY = "#2563eb";
|
|
96
|
+
var DARK = "#111827";
|
|
97
|
+
var WHITE = "#ffffff";
|
|
98
|
+
function parseHex(color) {
|
|
99
|
+
const m = /^#([0-9a-f]{6})$/i.exec(String(color || "").trim());
|
|
100
|
+
if (!m) return null;
|
|
101
|
+
const n = parseInt(m[1], 16);
|
|
102
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
103
|
+
}
|
|
104
|
+
function luminance(rgb) {
|
|
105
|
+
const [r, g, b] = rgb.map((v) => {
|
|
106
|
+
const c = v / 255;
|
|
107
|
+
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
108
|
+
});
|
|
109
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
110
|
+
}
|
|
111
|
+
function contrast(a, b) {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
const la = luminance((_a = parseHex(a)) != null ? _a : [0, 0, 0]);
|
|
114
|
+
const lb = luminance((_b = parseHex(b)) != null ? _b : [0, 0, 0]);
|
|
115
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
116
|
+
}
|
|
117
|
+
function themeFor(primaryColor) {
|
|
118
|
+
const primary = parseHex(primaryColor) ? primaryColor.toLowerCase() : DEFAULT_PRIMARY;
|
|
119
|
+
const onPrimary = contrast(primary, WHITE) >= contrast(primary, DARK) ? WHITE : DARK;
|
|
120
|
+
const accent = contrast(primary, WHITE) >= 3 ? primary : DARK;
|
|
121
|
+
return { primary, onPrimary, accent };
|
|
122
|
+
}
|
|
123
|
+
|
|
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
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/styles.ts
|
|
227
|
+
var CSS = `
|
|
228
|
+
:host{all:initial}
|
|
229
|
+
.kc{font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Noto Sans Hebrew","Noto Sans Arabic","Noto Sans Thai",Arial,sans-serif;font-size:15px;line-height:1.45;color:#1f2328;-webkit-font-smoothing:antialiased}
|
|
230
|
+
.kc *,.kc *::before,.kc *::after{box-sizing:border-box}
|
|
231
|
+
.kc [hidden]{display:none!important}
|
|
232
|
+
.kc button,.kc input,.kc select,.kc textarea{font:inherit;color:inherit;margin:0}
|
|
233
|
+
.kc :focus-visible{outline:2px solid var(--kc-accent);outline-offset:2px}
|
|
234
|
+
.launcher{position:fixed;z-index:2147483000;inset-block-end:20px;inset-inline-end:20px;width:56px;height:56px;border-radius:50%;border:0;padding:0;background:var(--kc-primary);color:var(--kc-on-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 6px 20px rgba(0,0,0,.22);transition:transform .15s ease}
|
|
235
|
+
.launcher:hover{transform:scale(1.05)}
|
|
236
|
+
.launcher svg{width:28px;height:28px}
|
|
237
|
+
.kc[data-pos=start] .launcher,.kc[data-pos=start] .panel{inset-inline-end:auto;inset-inline-start:20px}
|
|
238
|
+
.panel{position:fixed;z-index:2147483001;inset-block-end:88px;inset-inline-end:20px;width:380px;max-width:calc(100vw - 40px);height:620px;max-height:calc(100vh - 108px);display:flex;flex-direction:column;background:#fff;border-radius:16px;box-shadow:0 12px 40px rgba(0,0,0,.24);overflow:hidden;animation:kc-in .18s ease-out}
|
|
239
|
+
@keyframes kc-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:none}}
|
|
240
|
+
.header{display:flex;align-items:center;gap:10px;padding:12px 14px;background:var(--kc-primary);color:var(--kc-on-primary);flex:none}
|
|
241
|
+
.avatar{width:38px;height:38px;border-radius:50%;flex:none;display:flex;align-items:center;justify-content:center;overflow:hidden;background:rgba(255,255,255,.22);font-weight:600;font-size:15px}
|
|
242
|
+
.avatar img{width:100%;height:100%;object-fit:cover}
|
|
243
|
+
.titles{min-width:0;flex:1}
|
|
244
|
+
.name{font-weight:600;font-size:16px;line-height:1.6;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
245
|
+
.subtitle{font-size:13px;line-height:1.6;opacity:.85;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
246
|
+
.close{flex:none;width:36px;height:36px;border:0;border-radius:50%;background:transparent;color:inherit;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
|
|
247
|
+
.close:hover{background:rgba(127,127,127,.2)}
|
|
248
|
+
.close svg{width:22px;height:22px}
|
|
249
|
+
.banner{flex:none;background:#fff4e5;color:#7a4100;font-size:13px;padding:6px 14px}
|
|
250
|
+
.view{flex:1;min-height:0;display:flex;flex-direction:column;background:#f6f7f9}
|
|
251
|
+
.messages{flex:1;min-height:0;overflow-y:auto;padding:16px 14px 8px;display:flex;flex-direction:column;gap:6px;overscroll-behavior:contain}
|
|
252
|
+
.msg{max-width:85%;padding:9px 13px;border-radius:16px;white-space:pre-wrap;overflow-wrap:anywhere}
|
|
253
|
+
.msg-bot{align-self:flex-start;background:#fff;border:1px solid #e3e6ea;border-end-start-radius:4px}
|
|
254
|
+
.msg-user{align-self:flex-end;background:var(--kc-primary);color:var(--kc-on-primary);border-end-end-radius:4px}
|
|
255
|
+
.msg-user+.msg-bot,.msg-bot+.msg-user{margin-block-start:6px}
|
|
256
|
+
.msg a{color:inherit;text-decoration:underline}
|
|
257
|
+
.ai{display:block;font-size:11px;font-weight:600;color:#6b7280}
|
|
258
|
+
.typing{color:#9ca3af;font-weight:700;animation:kc-dot 1s infinite}
|
|
259
|
+
@keyframes kc-dot{50%{opacity:.3}}
|
|
260
|
+
.msg img{display:block;max-width:100%;height:auto;border-radius:10px;margin-block-end:6px}
|
|
261
|
+
.chips{flex:none;display:flex;flex-wrap:wrap;gap:6px;padding:4px 14px 10px;max-height:40%;overflow-y:auto}
|
|
262
|
+
.chip{border:1px solid var(--kc-accent);background:#fff;color:var(--kc-accent);border-radius:999px;padding:6px 13px;cursor:pointer;font-size:14px;line-height:1.4;text-align:start}
|
|
263
|
+
.chip:hover{background:var(--kc-primary);color:var(--kc-on-primary);border-color:var(--kc-primary)}
|
|
264
|
+
.chip-home,.chip-back{border-color:#cfd4da;color:#4b5563}
|
|
265
|
+
.composer{flex:none;display:flex;align-items:center;gap:8px;padding:8px 10px;border-block-start:1px solid #e3e6ea;background:#fff}
|
|
266
|
+
.composer input{flex:1;min-width:0;border:0;outline:0;background:transparent;padding:8px 6px;font-size:16px}
|
|
267
|
+
.kc .composer input:focus-visible{outline:0}
|
|
268
|
+
.composer:focus-within{box-shadow:inset 0 2px 0 var(--kc-accent)}
|
|
269
|
+
.send{flex:none;width:40px;height:40px;border:0;border-radius:50%;background:var(--kc-primary);color:var(--kc-on-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0}
|
|
270
|
+
.send svg{width:20px;height:20px}
|
|
271
|
+
.kc[dir=rtl] .send svg{transform:scaleX(-1)}
|
|
272
|
+
.form{flex:1;min-height:0;overflow-y:auto;padding:16px 16px 12px;background:#fff;display:flex;flex-direction:column;gap:10px}
|
|
273
|
+
.form h2{margin:0 0 4px;font-size:17px;font-weight:600}
|
|
274
|
+
.field label{display:block;font-size:13px;color:#4b5563;margin-block-end:3px}
|
|
275
|
+
.field input,.field select,.field textarea{display:block;width:100%;border:1px solid #cfd4da;border-radius:8px;padding:8px 10px;background:#fff;font-size:16px}
|
|
276
|
+
.field textarea{min-height:96px;resize:vertical}
|
|
277
|
+
.field input:focus,.field select:focus,.field textarea:focus{border-color:var(--kc-accent);outline:0;box-shadow:0 0 0 2px color-mix(in srgb,var(--kc-accent) 25%,transparent)}
|
|
278
|
+
.error{color:#b42318;font-size:14px}
|
|
279
|
+
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-block-start:4px}
|
|
280
|
+
.btn{border-radius:8px;padding:9px 16px;cursor:pointer;border:1px solid #cfd4da;background:#fff}
|
|
281
|
+
.btn-primary{background:var(--kc-primary);color:var(--kc-on-primary);border-color:var(--kc-primary)}
|
|
282
|
+
.btn:disabled{opacity:.6;cursor:default}
|
|
283
|
+
.sent{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px;padding:24px;text-align:center;background:#fff}
|
|
284
|
+
.badge{flex:none;display:block;text-align:center;font-size:12px;color:#6b7280;padding:6px;background:#fff;border-block-start:1px solid #eef0f2;text-decoration:none}
|
|
285
|
+
.badge:hover{color:#1f2328}
|
|
286
|
+
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
287
|
+
@media (max-width:480px){.panel,.kc[data-pos=start] .panel{inset:0;width:100%;max-width:none;height:100%;max-height:none;border-radius:0}}
|
|
288
|
+
@media (prefers-reduced-motion:reduce){.kc *{animation:none!important;transition:none!important}}
|
|
289
|
+
`;
|
|
290
|
+
|
|
291
|
+
// src/widget.ts
|
|
292
|
+
var DEFAULT_API = "https://api.kavchat.com";
|
|
293
|
+
var BRANDING_URL = "https://kavchat.com";
|
|
294
|
+
var REPORT_DELAY_MS = 2e3;
|
|
295
|
+
var SCREEN_ID_RE = /^[A-Za-z0-9_.-]{1,40}$/;
|
|
296
|
+
var SCREENS_RESEND_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
297
|
+
function normalizeScreens(list) {
|
|
298
|
+
const seen = /* @__PURE__ */ new Set();
|
|
299
|
+
const out = [];
|
|
300
|
+
for (const s of list != null ? list : []) {
|
|
301
|
+
const id = typeof (s == null ? void 0 : s.id) === "string" ? s.id : "";
|
|
302
|
+
const label = typeof (s == null ? void 0 : s.label) === "string" ? s.label.trim().slice(0, 60) : "";
|
|
303
|
+
if (!SCREEN_ID_RE.test(id) || !label || seen.has(id)) continue;
|
|
304
|
+
seen.add(id);
|
|
305
|
+
out.push({ id, label, needsTarget: s.needsTarget === true });
|
|
306
|
+
if (out.length === 100) break;
|
|
307
|
+
}
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
function screensFingerprint(list) {
|
|
311
|
+
const text = [...list].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map((s) => `${s.id}|${s.label}|${s.needsTarget ? 1 : 0}`).join("\n");
|
|
312
|
+
let h2 = 2166136261;
|
|
313
|
+
for (let i = 0; i < text.length; i++) {
|
|
314
|
+
h2 ^= text.charCodeAt(i);
|
|
315
|
+
h2 = Math.imul(h2, 16777619) >>> 0;
|
|
316
|
+
}
|
|
317
|
+
return h2.toString(16);
|
|
318
|
+
}
|
|
319
|
+
var baseLocale = (l) => String(l || "").toLowerCase().split(/[-_]/)[0].replace(/^iw$/, "he");
|
|
320
|
+
function pickLocale(requested, releaseLocale) {
|
|
321
|
+
const wanted = baseLocale(requested || (typeof navigator !== "undefined" ? navigator.language : "") || "");
|
|
322
|
+
if (UI_TEXTS[wanted]) return wanted;
|
|
323
|
+
const fallback = baseLocale(releaseLocale);
|
|
324
|
+
return UI_TEXTS[fallback] ? fallback : "en";
|
|
325
|
+
}
|
|
326
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
327
|
+
var KavchatWidget = class {
|
|
328
|
+
constructor(options) {
|
|
329
|
+
this.release = null;
|
|
330
|
+
this.offline = false;
|
|
331
|
+
this.ui = null;
|
|
332
|
+
this.state = null;
|
|
333
|
+
this.session = null;
|
|
334
|
+
this.t = uiTexts("en");
|
|
335
|
+
this.locale = "en";
|
|
336
|
+
this.isOpen = false;
|
|
337
|
+
this.prevFocus = null;
|
|
338
|
+
this.reportTimer = null;
|
|
339
|
+
this.dirty = false;
|
|
340
|
+
this.destroyed = false;
|
|
341
|
+
/** מחכים לתשובת AI: מציגים "מקליד" ומתעלמים מקלט חדש */
|
|
342
|
+
this.waiting = false;
|
|
343
|
+
this.onPageHide = () => this.flush(true);
|
|
344
|
+
this.onVisibility = () => {
|
|
345
|
+
if (document.visibilityState === "hidden") this.flush(true);
|
|
346
|
+
};
|
|
347
|
+
if (!options || !options.publicKey) throw new Error("Kavchat: publicKey is required");
|
|
348
|
+
this.opts = { ...options };
|
|
349
|
+
this.wantOpen = !!options.open;
|
|
350
|
+
this.api = {
|
|
351
|
+
apiBase: options.apiBase || DEFAULT_API,
|
|
352
|
+
publicKey: options.publicKey,
|
|
353
|
+
getToken: () => this.opts.userToken || void 0
|
|
354
|
+
};
|
|
355
|
+
window.addEventListener("pagehide", this.onPageHide);
|
|
356
|
+
document.addEventListener("visibilitychange", this.onVisibility);
|
|
357
|
+
this.ready = this.load();
|
|
358
|
+
}
|
|
359
|
+
/** ה-ShadowRoot, לבדיקות ולאוטומציה באתר המארח */
|
|
360
|
+
get shadowRoot() {
|
|
361
|
+
return this.ui ? this.ui.root : null;
|
|
362
|
+
}
|
|
363
|
+
get opened() {
|
|
364
|
+
return this.isOpen;
|
|
365
|
+
}
|
|
366
|
+
open() {
|
|
367
|
+
this.wantOpen = true;
|
|
368
|
+
const ui = this.ui;
|
|
369
|
+
if (!ui || this.isOpen) return;
|
|
370
|
+
this.isOpen = true;
|
|
371
|
+
this.prevFocus = document.activeElement;
|
|
372
|
+
if (!this.state) this.startFresh();
|
|
373
|
+
ui.panel.hidden = false;
|
|
374
|
+
ui.launcher.setAttribute("aria-expanded", "true");
|
|
375
|
+
ui.launcher.setAttribute("aria-label", this.t["widget.close"]);
|
|
376
|
+
ui.launcher.replaceChildren(icon(ICONS.close));
|
|
377
|
+
this.render();
|
|
378
|
+
this.focusInside();
|
|
379
|
+
this.emit({ type: "open" });
|
|
380
|
+
}
|
|
381
|
+
close() {
|
|
382
|
+
this.wantOpen = false;
|
|
383
|
+
const ui = this.ui;
|
|
384
|
+
if (!ui || !this.isOpen) return;
|
|
385
|
+
this.isOpen = false;
|
|
386
|
+
ui.panel.hidden = true;
|
|
387
|
+
ui.launcher.setAttribute("aria-expanded", "false");
|
|
388
|
+
ui.launcher.setAttribute("aria-label", this.t["widget.open"]);
|
|
389
|
+
ui.launcher.replaceChildren(icon(ICONS.chat));
|
|
390
|
+
const prev = this.prevFocus;
|
|
391
|
+
if (prev && prev !== ui.host && prev.isConnected && typeof prev.focus === "function") prev.focus();
|
|
392
|
+
else ui.launcher.focus();
|
|
393
|
+
this.emit({ type: "close" });
|
|
394
|
+
}
|
|
395
|
+
toggle() {
|
|
396
|
+
if (this.isOpen) this.close();
|
|
397
|
+
else this.open();
|
|
398
|
+
}
|
|
399
|
+
/** החלפת משתמש (התחברות או התנתקות). הגרסה נטענת מחדש, כי הקהל testers תלוי בזהות */
|
|
400
|
+
setUser(user) {
|
|
401
|
+
this.opts.userToken = (user == null ? void 0 : user.token) || void 0;
|
|
402
|
+
this.opts.userName = (user == null ? void 0 : user.name) || void 0;
|
|
403
|
+
this.ready = this.load();
|
|
404
|
+
return this.ready;
|
|
405
|
+
}
|
|
406
|
+
destroy() {
|
|
407
|
+
if (this.destroyed) return;
|
|
408
|
+
this.flush(true);
|
|
409
|
+
this.destroyed = true;
|
|
410
|
+
if (this.reportTimer) clearTimeout(this.reportTimer);
|
|
411
|
+
window.removeEventListener("pagehide", this.onPageHide);
|
|
412
|
+
document.removeEventListener("visibilitychange", this.onVisibility);
|
|
413
|
+
this.unmount();
|
|
414
|
+
}
|
|
415
|
+
// ---------- טעינה ----------
|
|
416
|
+
async load() {
|
|
417
|
+
var _a, _b;
|
|
418
|
+
const pk = this.opts.publicKey;
|
|
419
|
+
const cached = readCachedRelease(pk);
|
|
420
|
+
const result = await fetchRelease(this.api, (cached == null ? void 0 : cached.etag) || void 0);
|
|
421
|
+
if (this.destroyed) return;
|
|
422
|
+
let release = null;
|
|
423
|
+
let offline = false;
|
|
424
|
+
if (result.kind === "ok") {
|
|
425
|
+
writeCachedRelease(pk, { etag: result.etag, release: result.release });
|
|
426
|
+
release = result.release;
|
|
427
|
+
} else if (result.kind === "not-modified") {
|
|
428
|
+
release = (_a = cached == null ? void 0 : cached.release) != null ? _a : null;
|
|
429
|
+
} else if (result.kind === "offline") {
|
|
430
|
+
release = (_b = cached == null ? void 0 : cached.release) != null ? _b : null;
|
|
431
|
+
offline = !!release;
|
|
432
|
+
}
|
|
433
|
+
this.apply(release, offline);
|
|
434
|
+
void this.registerScreens();
|
|
435
|
+
}
|
|
436
|
+
/** שולח את רשימת המסכים רק כשהשתנתה או פעם בשבוע. שגיאה נבלעת: ננסה שוב בטעינה הבאה */
|
|
437
|
+
async registerScreens() {
|
|
438
|
+
const screens = normalizeScreens(this.opts.screens);
|
|
439
|
+
if (screens.length === 0) return;
|
|
440
|
+
const pk = this.opts.publicKey;
|
|
441
|
+
const fp = screensFingerprint(screens);
|
|
442
|
+
const mark = readScreensMark(pk);
|
|
443
|
+
if (mark && mark.fp === fp && Date.now() - mark.at < SCREENS_RESEND_MS) return;
|
|
444
|
+
if (await postScreens(this.api, screens)) writeScreensMark(pk, { fp, at: Date.now() });
|
|
445
|
+
}
|
|
446
|
+
apply(release, offline) {
|
|
447
|
+
if (!release || !release.enabled) {
|
|
448
|
+
this.release = null;
|
|
449
|
+
this.state = null;
|
|
450
|
+
this.isOpen = false;
|
|
451
|
+
this.unmount();
|
|
452
|
+
this.emit({ type: "unavailable", reason: release && "reason" in release ? release.reason : void 0 });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
this.offline = offline;
|
|
456
|
+
const changed = !this.release || this.release.version !== release.version;
|
|
457
|
+
this.release = release;
|
|
458
|
+
this.locale = pickLocale(this.opts.locale, release.locale);
|
|
459
|
+
this.t = uiTexts(this.locale);
|
|
460
|
+
if (changed || !this.ui) {
|
|
461
|
+
this.state = null;
|
|
462
|
+
this.restoreSession();
|
|
463
|
+
this.mount();
|
|
464
|
+
}
|
|
465
|
+
if (this.ui) this.ui.banner.hidden = !this.offline;
|
|
466
|
+
this.emit({ type: "ready", version: release.version, offline });
|
|
467
|
+
if (this.wantOpen) this.open();
|
|
468
|
+
}
|
|
469
|
+
restoreSession() {
|
|
470
|
+
const release = this.release;
|
|
471
|
+
if (!release) return;
|
|
472
|
+
const saved = readSession(this.opts.publicKey);
|
|
473
|
+
if (saved && saved.version === release.version && saved.state) {
|
|
474
|
+
const scripts = {};
|
|
475
|
+
for (const sc of release.scripts) scripts[sc.id] = sc;
|
|
476
|
+
this.session = saved;
|
|
477
|
+
this.state = { ...saved.state, scripts };
|
|
478
|
+
} else {
|
|
479
|
+
this.session = null;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// ---------- מנוע ----------
|
|
483
|
+
/**
|
|
484
|
+
* התסריט נבדק בשפת הבוט (מילות עצירה וכו'), והטקסטים הקבועים של הצ'אט מוצגים בשפת הממשק.
|
|
485
|
+
* ההגדרה של המנוע גלובלית, ולכן קובעים אותה לפני כל קריאה
|
|
486
|
+
*/
|
|
487
|
+
run(fn) {
|
|
488
|
+
configureEngine({ texts: this.t });
|
|
489
|
+
return fn();
|
|
490
|
+
}
|
|
491
|
+
startFresh() {
|
|
492
|
+
const release = this.release;
|
|
493
|
+
if (!release) return;
|
|
494
|
+
configureEngine({ texts: this.t });
|
|
495
|
+
this.session = { id: randomId(), startedAt: (/* @__PURE__ */ new Date()).toISOString(), version: release.version, state: null };
|
|
496
|
+
this.setState(
|
|
497
|
+
startConversation({
|
|
498
|
+
scripts: release.scripts,
|
|
499
|
+
openingScriptId: release.openingScriptId,
|
|
500
|
+
locale: release.locale,
|
|
501
|
+
userName: this.opts.userName
|
|
502
|
+
})
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
setState(state) {
|
|
506
|
+
this.state = state;
|
|
507
|
+
if (this.session) {
|
|
508
|
+
const { scripts: _omit, ...rest } = state;
|
|
509
|
+
this.session.state = rest;
|
|
510
|
+
writeSession(this.opts.publicKey, this.session);
|
|
511
|
+
}
|
|
512
|
+
this.render();
|
|
513
|
+
}
|
|
514
|
+
onChip(chip) {
|
|
515
|
+
if (!this.state || this.waiting) return;
|
|
516
|
+
const state = this.state;
|
|
517
|
+
const r = this.run(() => pressChip(state, chip));
|
|
518
|
+
this.afterAction(r);
|
|
519
|
+
}
|
|
520
|
+
async onSend(text) {
|
|
521
|
+
var _a;
|
|
522
|
+
if (!this.state || !text.trim() || this.waiting) return;
|
|
523
|
+
const state = this.state;
|
|
524
|
+
const session2 = this.session;
|
|
525
|
+
const question = ((_a = this.release) == null ? void 0 : _a.settings.aiAnswers) && session2 ? aiQuestion(state, text) : null;
|
|
526
|
+
if (!question || !session2) return this.afterAction(this.run(() => sendText(state, text)));
|
|
527
|
+
const pending = pendingQuestion(state, text);
|
|
528
|
+
this.state = pending;
|
|
529
|
+
this.waiting = true;
|
|
530
|
+
this.render();
|
|
531
|
+
const ai = await postAiAnswer(this.api, session2.id, question);
|
|
532
|
+
this.waiting = false;
|
|
533
|
+
if (this.destroyed || this.state !== pending) return this.render();
|
|
534
|
+
this.afterAction(this.run(() => answerWithAi(state, text, ai && ai.answer, ai && ai.scriptId)));
|
|
535
|
+
}
|
|
536
|
+
afterAction(r) {
|
|
537
|
+
this.setState(r.state);
|
|
538
|
+
this.scheduleReport();
|
|
539
|
+
if (r.effect) this.handleEffect(r.effect);
|
|
540
|
+
}
|
|
541
|
+
handleEffect(effect) {
|
|
542
|
+
var _a, _b, _c;
|
|
543
|
+
this.emit({ type: "effect", effect });
|
|
544
|
+
if (effect.type === "contact") {
|
|
545
|
+
this.showForm(effect);
|
|
546
|
+
} else if (effect.type === "link") {
|
|
547
|
+
openLink(effect.url, (_a = this.ui) == null ? void 0 : _a.root);
|
|
548
|
+
} else if (effect.type === "screen") {
|
|
549
|
+
try {
|
|
550
|
+
(_c = (_b = this.opts).onScreen) == null ? void 0 : _c.call(_b, effect.screen, effect.targetId);
|
|
551
|
+
} catch (e) {
|
|
552
|
+
console.error("Kavchat: onScreen failed", e);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// ---------- דיווח ----------
|
|
557
|
+
scheduleReport() {
|
|
558
|
+
this.dirty = true;
|
|
559
|
+
if (this.reportTimer) clearTimeout(this.reportTimer);
|
|
560
|
+
this.reportTimer = setTimeout(() => this.flush(false), REPORT_DELAY_MS);
|
|
561
|
+
}
|
|
562
|
+
flush(keepalive) {
|
|
563
|
+
if (this.reportTimer) {
|
|
564
|
+
clearTimeout(this.reportTimer);
|
|
565
|
+
this.reportTimer = null;
|
|
566
|
+
}
|
|
567
|
+
if (!this.dirty || !this.state || !this.release || !this.session || this.destroyed) return;
|
|
568
|
+
this.dirty = false;
|
|
569
|
+
const payload = eventsPayload(this.state, this.session.id, this.release.version, this.session.startedAt);
|
|
570
|
+
void postEvents(this.api, payload, keepalive).then((ok) => {
|
|
571
|
+
if (!ok) this.dirty = true;
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
emit(event) {
|
|
575
|
+
var _a, _b;
|
|
576
|
+
try {
|
|
577
|
+
(_b = (_a = this.opts).onEvent) == null ? void 0 : _b.call(_a, event);
|
|
578
|
+
} catch (e) {
|
|
579
|
+
console.error("Kavchat: onEvent failed", e);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// ---------- ממשק ----------
|
|
583
|
+
unmount() {
|
|
584
|
+
if (this.ui) this.ui.host.remove();
|
|
585
|
+
this.ui = null;
|
|
586
|
+
this.isOpen = false;
|
|
587
|
+
}
|
|
588
|
+
mount() {
|
|
589
|
+
const release = this.release;
|
|
590
|
+
if (!release) return;
|
|
591
|
+
this.unmount();
|
|
592
|
+
const t = this.t;
|
|
593
|
+
const s = release.settings;
|
|
594
|
+
const theme = themeFor(s.primaryColor);
|
|
595
|
+
const host = h("div", { "data-kavchat": "" });
|
|
596
|
+
const root = host.attachShadow({ mode: "open" });
|
|
597
|
+
const wrap = h("div", {
|
|
598
|
+
class: "kc",
|
|
599
|
+
dir: isRtl(this.locale) ? "rtl" : "ltr",
|
|
600
|
+
lang: this.locale,
|
|
601
|
+
"data-pos": s.position === "start" ? "start" : "end"
|
|
602
|
+
});
|
|
603
|
+
wrap.style.setProperty("--kc-primary", theme.primary);
|
|
604
|
+
wrap.style.setProperty("--kc-on-primary", theme.onPrimary);
|
|
605
|
+
wrap.style.setProperty("--kc-accent", theme.accent);
|
|
606
|
+
const launcher = h("button", { type: "button", class: "launcher", "aria-label": t["widget.open"], "aria-expanded": "false", "aria-controls": "kc-panel" }, icon(ICONS.chat));
|
|
607
|
+
launcher.addEventListener("click", () => this.toggle());
|
|
608
|
+
const avatarImg = s.avatarImageUrl ? safeUrl(s.avatarImageUrl, ["https:"]) : null;
|
|
609
|
+
const avatar = h("div", { class: "avatar", "aria-hidden": "true" }, avatarImg ? h("img", { src: avatarImg, alt: "" }) : (s.avatarText || s.botName || "").slice(0, 2));
|
|
610
|
+
const closeBtn = h("button", { type: "button", class: "close", "aria-label": t["widget.close"] }, icon(ICONS.close));
|
|
611
|
+
closeBtn.addEventListener("click", () => this.close());
|
|
612
|
+
const header = h(
|
|
613
|
+
"div",
|
|
614
|
+
{ class: "header" },
|
|
615
|
+
avatar,
|
|
616
|
+
h("div", { class: "titles" }, h("h2", { class: "name", id: "kc-title" }, s.botName || ""), s.subtitle ? h("div", { class: "subtitle" }, s.subtitle) : null),
|
|
617
|
+
closeBtn
|
|
618
|
+
);
|
|
619
|
+
const banner = h("div", { class: "banner", role: "status" }, t["widget.offline"]);
|
|
620
|
+
banner.hidden = !this.offline;
|
|
621
|
+
const messages = h("div", { class: "messages" });
|
|
622
|
+
const chips = h("div", { class: "chips", role: "group" });
|
|
623
|
+
const input = h("input", { type: "text", dir: "auto", maxlength: "500", autocomplete: "off", placeholder: t["widget.placeholder"], "aria-label": t["widget.placeholder"], enterkeyhint: "send" });
|
|
624
|
+
const composer = h("form", { class: "composer" }, input, h("button", { type: "submit", class: "send", "aria-label": t["widget.send"] }, icon(ICONS.send)));
|
|
625
|
+
composer.addEventListener("submit", (e) => {
|
|
626
|
+
e.preventDefault();
|
|
627
|
+
const text = input.value;
|
|
628
|
+
input.value = "";
|
|
629
|
+
this.onSend(text);
|
|
630
|
+
});
|
|
631
|
+
const chatView = h("div", { class: "view" }, messages, chips, composer);
|
|
632
|
+
const formView = h("div", { class: "view" });
|
|
633
|
+
formView.hidden = true;
|
|
634
|
+
const live = h("div", { class: "sr-only", "aria-live": "polite", "aria-atomic": "false" });
|
|
635
|
+
const typing = h("div", { class: "msg msg-bot typing", "aria-hidden": "true" }, "...");
|
|
636
|
+
const panel = h("div", { class: "panel", id: "kc-panel", role: "dialog", "aria-labelledby": "kc-title", tabindex: "-1" }, header, banner, chatView, formView, live);
|
|
637
|
+
if (!s.removeBranding) {
|
|
638
|
+
panel.appendChild(h("a", { class: "badge", href: BRANDING_URL, target: "_blank", rel: "noopener noreferrer" }, t["widget.poweredBy"]));
|
|
639
|
+
}
|
|
640
|
+
panel.hidden = true;
|
|
641
|
+
panel.addEventListener("keydown", (e) => {
|
|
642
|
+
if (e.key === "Escape") {
|
|
643
|
+
e.stopPropagation();
|
|
644
|
+
this.close();
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
append(wrap, [launcher, panel]);
|
|
648
|
+
append(root, [h("style", null, CSS), wrap]);
|
|
649
|
+
(this.opts.container || document.body).appendChild(host);
|
|
650
|
+
this.ui = { host, root, wrap, launcher, panel, banner, chatView, messages, chips, input, formView, live, typing, rendered: 0 };
|
|
651
|
+
if (this.state) this.renderMessages(false);
|
|
652
|
+
}
|
|
653
|
+
focusInside() {
|
|
654
|
+
const ui = this.ui;
|
|
655
|
+
if (!ui) return;
|
|
656
|
+
const coarse = typeof window.matchMedia === "function" && window.matchMedia("(pointer: coarse)").matches;
|
|
657
|
+
if (!ui.formView.hidden) {
|
|
658
|
+
const first = ui.formView.querySelector("input,select,textarea");
|
|
659
|
+
(first || ui.panel).focus();
|
|
660
|
+
} else if (coarse) ui.panel.focus();
|
|
661
|
+
else ui.input.focus();
|
|
662
|
+
}
|
|
663
|
+
render() {
|
|
664
|
+
if (!this.ui || !this.state) return;
|
|
665
|
+
this.renderMessages(true);
|
|
666
|
+
this.renderChips();
|
|
667
|
+
}
|
|
668
|
+
renderMessages(announce) {
|
|
669
|
+
const ui = this.ui;
|
|
670
|
+
const state = this.state;
|
|
671
|
+
if (!ui || !state) return;
|
|
672
|
+
if (ui.rendered > state.messages.length) {
|
|
673
|
+
ui.messages.textContent = "";
|
|
674
|
+
ui.rendered = 0;
|
|
675
|
+
}
|
|
676
|
+
const fresh = [];
|
|
677
|
+
for (let i = ui.rendered; i < state.messages.length; i++) {
|
|
678
|
+
const m = state.messages[i];
|
|
679
|
+
ui.messages.appendChild(bubble(m, this.t));
|
|
680
|
+
if (m.from === "bot") fresh.push(m.ai ? `${this.t["chat.aiLabel"]}: ${m.text}` : m.text);
|
|
681
|
+
}
|
|
682
|
+
ui.rendered = state.messages.length;
|
|
683
|
+
if (this.waiting) ui.messages.appendChild(ui.typing);
|
|
684
|
+
else ui.typing.remove();
|
|
685
|
+
if (announce && fresh.length) ui.live.textContent = fresh.join("\n");
|
|
686
|
+
ui.messages.scrollTop = ui.messages.scrollHeight;
|
|
687
|
+
}
|
|
688
|
+
renderChips() {
|
|
689
|
+
const ui = this.ui;
|
|
690
|
+
const state = this.state;
|
|
691
|
+
if (!ui || !state) return;
|
|
692
|
+
ui.chips.textContent = "";
|
|
693
|
+
for (const chip of state.chips) {
|
|
694
|
+
const b = h("button", { type: "button", class: `chip chip-${chip.kind}` }, chip.label);
|
|
695
|
+
b.addEventListener("click", () => this.onChip(chip));
|
|
696
|
+
ui.chips.appendChild(b);
|
|
697
|
+
}
|
|
698
|
+
ui.chips.hidden = state.chips.length === 0;
|
|
699
|
+
}
|
|
700
|
+
showChat() {
|
|
701
|
+
const ui = this.ui;
|
|
702
|
+
if (!ui) return;
|
|
703
|
+
ui.formView.hidden = true;
|
|
704
|
+
ui.formView.textContent = "";
|
|
705
|
+
ui.chatView.hidden = false;
|
|
706
|
+
this.render();
|
|
707
|
+
if (this.isOpen) this.focusInside();
|
|
708
|
+
}
|
|
709
|
+
showForm(effect) {
|
|
710
|
+
const ui = this.ui;
|
|
711
|
+
const release = this.release;
|
|
712
|
+
if (!ui || !release) return;
|
|
713
|
+
const t = this.t;
|
|
714
|
+
const topics = release.settings.contactTopics || [];
|
|
715
|
+
const field = (id, label, control) => h("div", { class: "field" }, h("label", { for: id }, label), control);
|
|
716
|
+
const name = h("input", { id: "kc-name", type: "text", autocomplete: "name", maxlength: "100", dir: "auto" });
|
|
717
|
+
name.value = this.opts.userName || "";
|
|
718
|
+
const email = h("input", { id: "kc-email", type: "email", autocomplete: "email", dir: "auto" });
|
|
719
|
+
const phone = h("input", { id: "kc-phone", type: "tel", autocomplete: "tel", dir: "auto" });
|
|
720
|
+
let topic = null;
|
|
721
|
+
if (topics.length) {
|
|
722
|
+
topic = h("select", { id: "kc-topic" }, h("option", { value: "" }, ""));
|
|
723
|
+
for (const tp of topics) topic.appendChild(h("option", { value: tp.id }, tp.name));
|
|
724
|
+
if (effect.contactTopicId && topics.some((tp) => tp.id === effect.contactTopicId)) topic.value = effect.contactTopicId;
|
|
725
|
+
}
|
|
726
|
+
const message = h("textarea", { id: "kc-message", required: true, maxlength: "2000", dir: "auto" });
|
|
727
|
+
message.value = effect.prefill || "";
|
|
728
|
+
const error = h("div", { class: "error", role: "alert" });
|
|
729
|
+
const submit = h("button", { type: "submit", class: "btn btn-primary" }, t["form.submit"]);
|
|
730
|
+
const cancel = h("button", { type: "button", class: "btn" }, t["form.cancel"]);
|
|
731
|
+
cancel.addEventListener("click", () => this.showChat());
|
|
732
|
+
const form = h(
|
|
733
|
+
"form",
|
|
734
|
+
{ class: "form", novalidate: true },
|
|
735
|
+
h("h2", null, t["form.title"]),
|
|
736
|
+
field("kc-name", t["form.name"], name),
|
|
737
|
+
field("kc-email", t["form.email"], email),
|
|
738
|
+
field("kc-phone", t["form.phone"], phone),
|
|
739
|
+
topic ? field("kc-topic", t["form.topic"], topic) : null,
|
|
740
|
+
field("kc-message", t["form.message"], message),
|
|
741
|
+
error,
|
|
742
|
+
h("div", { class: "actions" }, submit, cancel)
|
|
743
|
+
);
|
|
744
|
+
form.addEventListener("submit", async (e) => {
|
|
745
|
+
var _a;
|
|
746
|
+
e.preventDefault();
|
|
747
|
+
const input = {
|
|
748
|
+
message: message.value.trim(),
|
|
749
|
+
name: name.value.trim() || void 0,
|
|
750
|
+
email: email.value.trim() || void 0,
|
|
751
|
+
phone: phone.value.trim() || void 0,
|
|
752
|
+
topicId: (topic == null ? void 0 : topic.value) || void 0,
|
|
753
|
+
scriptId: effect.scriptId || void 0,
|
|
754
|
+
sessionId: (_a = this.session) == null ? void 0 : _a.id,
|
|
755
|
+
transcript: effect.transcript.slice(-200).map((x) => ({ ...x, text: x.text.slice(0, 1e3) }))
|
|
756
|
+
};
|
|
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"] : "";
|
|
758
|
+
if (problem) {
|
|
759
|
+
error.textContent = problem;
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
error.textContent = "";
|
|
763
|
+
submit.disabled = true;
|
|
764
|
+
submit.textContent = t["form.sending"];
|
|
765
|
+
const res = await postTicket(this.api, input);
|
|
766
|
+
if (this.destroyed) return;
|
|
767
|
+
submit.disabled = false;
|
|
768
|
+
submit.textContent = t["form.submit"];
|
|
769
|
+
if (res.ok) {
|
|
770
|
+
this.flush(false);
|
|
771
|
+
this.emit({ type: "ticket", id: res.id, number: res.number });
|
|
772
|
+
this.showSent(res.number);
|
|
773
|
+
} else {
|
|
774
|
+
error.textContent = ticketError(res.code, res.message, t);
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
ui.formView.replaceChildren(form);
|
|
778
|
+
ui.chatView.hidden = true;
|
|
779
|
+
ui.formView.hidden = false;
|
|
780
|
+
if (this.isOpen) this.focusInside();
|
|
781
|
+
}
|
|
782
|
+
showSent(number) {
|
|
783
|
+
const ui = this.ui;
|
|
784
|
+
if (!ui) return;
|
|
785
|
+
const t = this.t;
|
|
786
|
+
const back = h("button", { type: "button", class: "btn btn-primary" }, t["form.backToChat"]);
|
|
787
|
+
back.addEventListener("click", () => this.showChat());
|
|
788
|
+
const text = h("p", { role: "status" }, format(t["form.sent"], { number }));
|
|
789
|
+
ui.formView.replaceChildren(h("div", { class: "sent" }, text, back));
|
|
790
|
+
back.focus();
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
function ticketError(code, message, t) {
|
|
794
|
+
if (code === "ticket_contact_required") return t["form.contactRequired"];
|
|
795
|
+
if (code === "email_invalid") return t["form.emailInvalid"];
|
|
796
|
+
if (code === "network") return t["form.error"];
|
|
797
|
+
return message || t["form.error"];
|
|
798
|
+
}
|
|
799
|
+
function bubble(m, t) {
|
|
800
|
+
const el = h("div", { class: `msg msg-${m.from}`, dir: "auto" });
|
|
801
|
+
if (m.ai) el.appendChild(h("span", { class: "ai" }, t["chat.aiLabel"]));
|
|
802
|
+
const img = m.from === "bot" && m.imageUrl ? safeUrl(m.imageUrl, ["https:"]) : null;
|
|
803
|
+
if (img) el.appendChild(h("img", { src: img, alt: "", loading: "lazy" }));
|
|
804
|
+
el.appendChild(m.from === "bot" && !m.ai ? richText(m.text) : document.createTextNode(m.text));
|
|
805
|
+
return el;
|
|
806
|
+
}
|
|
807
|
+
function openLink(url, root) {
|
|
808
|
+
const href = safeUrl(url, ["https:", "tel:"]);
|
|
809
|
+
if (!href) return false;
|
|
810
|
+
const a = h("a", { href, target: "_blank", rel: "noopener noreferrer" });
|
|
811
|
+
a.style.display = "none";
|
|
812
|
+
(root || document.body).appendChild(a);
|
|
813
|
+
a.click();
|
|
814
|
+
a.remove();
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/global.ts
|
|
819
|
+
var METHODS = ["init", "open", "close", "toggle", "setUser", "destroy"];
|
|
820
|
+
function installGlobal(win = window) {
|
|
821
|
+
const existing = win.Kavchat;
|
|
822
|
+
if (existing && existing.loaded) return { api: existing, queue: [] };
|
|
823
|
+
const queue = Array.isArray(existing == null ? void 0 : existing.q) ? existing.q.filter(Array.isArray) : [];
|
|
824
|
+
let widget = null;
|
|
825
|
+
let pendingUser;
|
|
826
|
+
let pendingOpen;
|
|
827
|
+
const api = {
|
|
828
|
+
loaded: true,
|
|
829
|
+
init(options) {
|
|
830
|
+
if (widget) widget.destroy();
|
|
831
|
+
const opts = { ...options };
|
|
832
|
+
if (pendingOpen !== void 0) opts.open = pendingOpen;
|
|
833
|
+
if (pendingUser !== void 0) {
|
|
834
|
+
opts.userToken = (pendingUser == null ? void 0 : pendingUser.token) || void 0;
|
|
835
|
+
opts.userName = (pendingUser == null ? void 0 : pendingUser.name) || void 0;
|
|
836
|
+
}
|
|
837
|
+
pendingOpen = pendingUser = void 0;
|
|
838
|
+
widget = new KavchatWidget(opts);
|
|
839
|
+
return widget.ready;
|
|
840
|
+
},
|
|
841
|
+
open() {
|
|
842
|
+
if (widget) widget.open();
|
|
843
|
+
else pendingOpen = true;
|
|
844
|
+
},
|
|
845
|
+
close() {
|
|
846
|
+
if (widget) widget.close();
|
|
847
|
+
else pendingOpen = false;
|
|
848
|
+
},
|
|
849
|
+
toggle() {
|
|
850
|
+
if (widget) widget.toggle();
|
|
851
|
+
else pendingOpen = !pendingOpen;
|
|
852
|
+
},
|
|
853
|
+
setUser(user) {
|
|
854
|
+
if (widget) void widget.setUser(user);
|
|
855
|
+
else pendingUser = user;
|
|
856
|
+
},
|
|
857
|
+
destroy() {
|
|
858
|
+
if (widget) widget.destroy();
|
|
859
|
+
widget = null;
|
|
860
|
+
},
|
|
861
|
+
q: {
|
|
862
|
+
push: (...calls) => {
|
|
863
|
+
calls.forEach((c) => run(api, c));
|
|
864
|
+
return 0;
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
get widget() {
|
|
868
|
+
return widget;
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
win.Kavchat = api;
|
|
872
|
+
return { api, queue };
|
|
873
|
+
}
|
|
874
|
+
function run(api, call) {
|
|
875
|
+
const [name, ...args] = call;
|
|
876
|
+
if (!METHODS.includes(name)) {
|
|
877
|
+
console.warn(`Kavchat: unknown method ${name}`);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
try {
|
|
881
|
+
const result = api[name](...args);
|
|
882
|
+
if (result && typeof result.catch === "function") result.catch((e) => console.error("Kavchat:", e));
|
|
883
|
+
} catch (e) {
|
|
884
|
+
console.error("Kavchat:", e);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// src/index.ts
|
|
889
|
+
function createWidget(options) {
|
|
890
|
+
return new KavchatWidget(options);
|
|
891
|
+
}
|
|
892
|
+
export {
|
|
893
|
+
BRANDING_URL,
|
|
894
|
+
DEFAULT_API,
|
|
895
|
+
KavchatWidget,
|
|
896
|
+
contrast,
|
|
897
|
+
createWidget,
|
|
898
|
+
installGlobal,
|
|
899
|
+
pickLocale,
|
|
900
|
+
themeFor
|
|
901
|
+
};
|