@oyehello/chat 0.4.0 → 0.5.1
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/README.md +2 -1
- package/dist/index.cjs +494 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.global.js +494 -26
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +494 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.global.js
CHANGED
|
@@ -15,6 +15,9 @@ var OyehelloChat = (function (exports) {
|
|
|
15
15
|
meta;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// src/protocol.ts
|
|
19
|
+
var CHAT_PROTOCOL_VERSION = 1;
|
|
20
|
+
|
|
18
21
|
// src/transport.ts
|
|
19
22
|
async function asError(res, fallbackCode) {
|
|
20
23
|
let body = {};
|
|
@@ -25,9 +28,10 @@ var OyehelloChat = (function (exports) {
|
|
|
25
28
|
return new ChatApiError(body.error ?? `HTTP ${res.status}`, res.status, body.code ?? fallbackCode, body.meta);
|
|
26
29
|
}
|
|
27
30
|
async function openSession(opts) {
|
|
28
|
-
const body = { agentId: opts.agentId };
|
|
31
|
+
const body = { agentId: opts.agentId, protocol: CHAT_PROTOCOL_VERSION };
|
|
29
32
|
if (opts.user) body.user = opts.user;
|
|
30
33
|
if (opts.clientTools && opts.clientTools.length > 0) body.clientTools = opts.clientTools;
|
|
34
|
+
if (opts.fingerprint) body.fingerprint = opts.fingerprint;
|
|
31
35
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions`, {
|
|
32
36
|
method: "POST",
|
|
33
37
|
credentials: "omit",
|
|
@@ -57,6 +61,15 @@ var OyehelloChat = (function (exports) {
|
|
|
57
61
|
});
|
|
58
62
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
59
63
|
}
|
|
64
|
+
async function postPageViews(opts) {
|
|
65
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/page-views`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
credentials: "omit",
|
|
68
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${opts.sessionToken}` },
|
|
69
|
+
body: JSON.stringify({ views: opts.views })
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
72
|
+
}
|
|
60
73
|
async function endSession(opts) {
|
|
61
74
|
await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/end`, {
|
|
62
75
|
method: "POST",
|
|
@@ -74,6 +87,15 @@ var OyehelloChat = (function (exports) {
|
|
|
74
87
|
}).catch(() => {
|
|
75
88
|
});
|
|
76
89
|
}
|
|
90
|
+
async function refreshSessionToken(opts) {
|
|
91
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/refresh-token`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
credentials: "omit",
|
|
94
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` }
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
97
|
+
return await res.json();
|
|
98
|
+
}
|
|
77
99
|
async function mintStreamTicket(opts) {
|
|
78
100
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/stream-ticket`, {
|
|
79
101
|
method: "POST",
|
|
@@ -86,15 +108,18 @@ var OyehelloChat = (function (exports) {
|
|
|
86
108
|
function streamUrl(baseUrl, sessionId, ticket) {
|
|
87
109
|
return `${baseUrl}/chat/sessions/${sessionId}/stream?ticket=${encodeURIComponent(ticket)}`;
|
|
88
110
|
}
|
|
89
|
-
async function
|
|
90
|
-
const
|
|
111
|
+
async function uploadAttachment(opts) {
|
|
112
|
+
const form = new FormData();
|
|
113
|
+
form.append("file", opts.file, opts.file.name);
|
|
114
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/upload`, {
|
|
91
115
|
method: "POST",
|
|
92
116
|
credentials: "omit",
|
|
93
|
-
headers: {
|
|
94
|
-
body:
|
|
117
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` },
|
|
118
|
+
body: form
|
|
95
119
|
});
|
|
96
120
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
97
|
-
|
|
121
|
+
const j2 = await res.json();
|
|
122
|
+
return { storageRef: j2.storageRef, contentType: j2.contentType, name: j2.name };
|
|
98
123
|
}
|
|
99
124
|
async function getAttachmentUrl(opts) {
|
|
100
125
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/attachment-url`, {
|
|
@@ -106,9 +131,178 @@ var OyehelloChat = (function (exports) {
|
|
|
106
131
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
107
132
|
return (await res.json()).url;
|
|
108
133
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
|
|
135
|
+
// src/fingerprint.ts
|
|
136
|
+
async function sha256Hex(input) {
|
|
137
|
+
const g = globalThis;
|
|
138
|
+
try {
|
|
139
|
+
if (g.crypto?.subtle && typeof TextEncoder !== "undefined") {
|
|
140
|
+
const buf = await g.crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
141
|
+
return Array.from(new Uint8Array(buf)).map((b2) => b2.toString(16).padStart(2, "0")).join("").slice(0, 32);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
let h = 5381;
|
|
146
|
+
for (let i = 0; i < input.length; i++) h = (h << 5) + h + input.charCodeAt(i) >>> 0;
|
|
147
|
+
return h.toString(16).padStart(8, "0");
|
|
148
|
+
}
|
|
149
|
+
async function computeDeviceFingerprint() {
|
|
150
|
+
try {
|
|
151
|
+
const nav = typeof navigator !== "undefined" ? navigator : null;
|
|
152
|
+
if (!nav) return null;
|
|
153
|
+
const scr = typeof screen !== "undefined" ? screen : null;
|
|
154
|
+
const tz = (() => {
|
|
155
|
+
try {
|
|
156
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
157
|
+
} catch {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
160
|
+
})();
|
|
161
|
+
const dpr = typeof devicePixelRatio !== "undefined" ? devicePixelRatio : "";
|
|
162
|
+
const signals = [
|
|
163
|
+
nav.userAgent,
|
|
164
|
+
Array.isArray(nav.languages) ? nav.languages.join(",") : "",
|
|
165
|
+
nav.language,
|
|
166
|
+
nav.platform,
|
|
167
|
+
nav.hardwareConcurrency,
|
|
168
|
+
nav.deviceMemory,
|
|
169
|
+
scr ? `${scr.width}x${scr.height}x${scr.colorDepth}` : "",
|
|
170
|
+
dpr,
|
|
171
|
+
tz
|
|
172
|
+
].map((v2) => String(v2 ?? "")).join("|");
|
|
173
|
+
return await sha256Hex(signals);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/journey.ts
|
|
180
|
+
var MAX_BUFFER = 25;
|
|
181
|
+
var storageKey = (agentId) => `oyehello-chat:journey:${agentId}`;
|
|
182
|
+
var store = () => {
|
|
183
|
+
try {
|
|
184
|
+
return globalThis.sessionStorage ?? null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
function load(agentId) {
|
|
190
|
+
const s = store();
|
|
191
|
+
if (!s) return { views: [], flushed: 0 };
|
|
192
|
+
try {
|
|
193
|
+
const raw = s.getItem(storageKey(agentId));
|
|
194
|
+
if (!raw) return { views: [], flushed: 0 };
|
|
195
|
+
const parsed = JSON.parse(raw);
|
|
196
|
+
const views = Array.isArray(parsed?.views) ? parsed.views.filter((v2) => !!v2 && typeof v2.path === "string" && typeof v2.at === "string") : [];
|
|
197
|
+
const flushedRaw = typeof parsed?.flushed === "number" && parsed.flushed >= 0 ? parsed.flushed : 0;
|
|
198
|
+
return { views, flushed: Math.min(flushedRaw, views.length) };
|
|
199
|
+
} catch {
|
|
200
|
+
return { views: [], flushed: 0 };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function save(agentId, data) {
|
|
204
|
+
const s = store();
|
|
205
|
+
if (!s) return;
|
|
206
|
+
try {
|
|
207
|
+
s.setItem(storageKey(agentId), JSON.stringify(data));
|
|
208
|
+
} catch {
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
var sanitizePath = (raw) => (raw || "/").split("?")[0].split("#")[0];
|
|
212
|
+
function recordView(agentId, path) {
|
|
213
|
+
try {
|
|
214
|
+
const p = sanitizePath(path);
|
|
215
|
+
const data = load(agentId);
|
|
216
|
+
const last = data.views[data.views.length - 1];
|
|
217
|
+
if (last && last.path === p) return;
|
|
218
|
+
data.views.push({ path: p, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
219
|
+
if (data.views.length > MAX_BUFFER) {
|
|
220
|
+
const drop = data.views.length - MAX_BUFFER;
|
|
221
|
+
data.views = data.views.slice(drop);
|
|
222
|
+
data.flushed = Math.max(0, data.flushed - drop);
|
|
223
|
+
}
|
|
224
|
+
save(agentId, data);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function unsentViews(agentId) {
|
|
229
|
+
try {
|
|
230
|
+
const data = load(agentId);
|
|
231
|
+
return data.views.slice(data.flushed);
|
|
232
|
+
} catch {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function markFlushed(agentId, count) {
|
|
237
|
+
if (count <= 0) return;
|
|
238
|
+
try {
|
|
239
|
+
const data = load(agentId);
|
|
240
|
+
data.flushed = Math.min(data.views.length, data.flushed + count);
|
|
241
|
+
save(agentId, data);
|
|
242
|
+
} catch {
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function installSpaHooks(onNav) {
|
|
246
|
+
const originalPush = history.pushState;
|
|
247
|
+
const originalReplace = history.replaceState;
|
|
248
|
+
let pushInstalled = false;
|
|
249
|
+
let replaceInstalled = false;
|
|
250
|
+
try {
|
|
251
|
+
history.pushState = function(...args) {
|
|
252
|
+
const ret = originalPush.apply(this, args);
|
|
253
|
+
try {
|
|
254
|
+
onNav();
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
return ret;
|
|
258
|
+
};
|
|
259
|
+
pushInstalled = true;
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
history.replaceState = function(...args) {
|
|
264
|
+
const ret = originalReplace.apply(this, args);
|
|
265
|
+
try {
|
|
266
|
+
onNav();
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
return ret;
|
|
270
|
+
};
|
|
271
|
+
replaceInstalled = true;
|
|
272
|
+
} catch {
|
|
273
|
+
}
|
|
274
|
+
const onPopState = () => {
|
|
275
|
+
try {
|
|
276
|
+
onNav();
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
let popstateInstalled = false;
|
|
281
|
+
try {
|
|
282
|
+
window.addEventListener("popstate", onPopState);
|
|
283
|
+
popstateInstalled = true;
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
return () => {
|
|
287
|
+
if (pushInstalled) {
|
|
288
|
+
try {
|
|
289
|
+
history.pushState = originalPush;
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (replaceInstalled) {
|
|
294
|
+
try {
|
|
295
|
+
history.replaceState = originalReplace;
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (popstateInstalled) {
|
|
300
|
+
try {
|
|
301
|
+
window.removeEventListener("popstate", onPopState);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
112
306
|
}
|
|
113
307
|
|
|
114
308
|
// src/client.ts
|
|
@@ -131,13 +325,97 @@ var OyehelloChat = (function (exports) {
|
|
|
131
325
|
let reconnectTimer = null;
|
|
132
326
|
let reconnectAttempts = 0;
|
|
133
327
|
let closed = false;
|
|
328
|
+
let warnedProtocol = false;
|
|
329
|
+
let deviceFingerprint;
|
|
330
|
+
let refreshTimer = null;
|
|
134
331
|
const toolHandlers = /* @__PURE__ */ new Map();
|
|
332
|
+
let journeyFlushChain = Promise.resolve();
|
|
333
|
+
let journeyFlushQueued = false;
|
|
334
|
+
const flushJourney = () => {
|
|
335
|
+
if (!session) return;
|
|
336
|
+
if (journeyFlushQueued) return;
|
|
337
|
+
journeyFlushQueued = true;
|
|
338
|
+
journeyFlushChain = journeyFlushChain.then(async () => {
|
|
339
|
+
journeyFlushQueued = false;
|
|
340
|
+
if (!session) return;
|
|
341
|
+
const pending = unsentViews(options.agentId);
|
|
342
|
+
if (pending.length === 0) return;
|
|
343
|
+
const s = session;
|
|
344
|
+
try {
|
|
345
|
+
await postPageViews({ baseUrl: options.baseUrl, sessionId: s.sessionId, sessionToken: s.sessionToken, views: pending, fetchImpl });
|
|
346
|
+
markFlushed(options.agentId, pending.length);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
};
|
|
135
351
|
const clearReconnect = () => {
|
|
136
352
|
if (reconnectTimer) {
|
|
137
353
|
clearTimeout(reconnectTimer);
|
|
138
354
|
reconnectTimer = null;
|
|
139
355
|
}
|
|
140
356
|
};
|
|
357
|
+
const clearRefresh = () => {
|
|
358
|
+
if (refreshTimer) {
|
|
359
|
+
clearTimeout(refreshTimer);
|
|
360
|
+
refreshTimer = null;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const scheduleRefresh = () => {
|
|
364
|
+
clearRefresh();
|
|
365
|
+
if (closed || !session) return;
|
|
366
|
+
const untilExpiry = Date.parse(session.expiresAt) - Date.now();
|
|
367
|
+
const delay = Number.isNaN(untilExpiry) ? 10 * 6e4 : Math.max(5e3, untilExpiry - 6e4);
|
|
368
|
+
refreshTimer = setTimeout(async () => {
|
|
369
|
+
if (closed || !session) return;
|
|
370
|
+
try {
|
|
371
|
+
const r = await refreshSessionToken({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
372
|
+
session = { ...session, sessionToken: r.sessionToken, expiresAt: r.expiresAt };
|
|
373
|
+
persistSession(session);
|
|
374
|
+
scheduleRefresh();
|
|
375
|
+
} catch (err) {
|
|
376
|
+
const code = err?.code;
|
|
377
|
+
if (code === "session_ended" || code === "session_expired") clearStoredSession();
|
|
378
|
+
}
|
|
379
|
+
}, delay);
|
|
380
|
+
refreshTimer.unref?.();
|
|
381
|
+
};
|
|
382
|
+
const sessKey = `oyehello-chat-sess:${options.agentId}`;
|
|
383
|
+
const store2 = (() => {
|
|
384
|
+
try {
|
|
385
|
+
return globalThis.sessionStorage ?? null;
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
const loadStoredSession = () => {
|
|
391
|
+
if (!store2) return null;
|
|
392
|
+
try {
|
|
393
|
+
const raw = store2.getItem(sessKey);
|
|
394
|
+
if (!raw) return null;
|
|
395
|
+
const s = JSON.parse(raw);
|
|
396
|
+
if (!s?.sessionId || !s?.sessionToken || !s?.expiresAt || !s?.agent) return null;
|
|
397
|
+
const exp = Date.parse(s.expiresAt);
|
|
398
|
+
if (Number.isNaN(exp) || Date.now() >= exp - 5e3) {
|
|
399
|
+
store2.removeItem(sessKey);
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
return s;
|
|
403
|
+
} catch {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
const persistSession = (s) => {
|
|
408
|
+
try {
|
|
409
|
+
store2?.setItem(sessKey, JSON.stringify({ sessionId: s.sessionId, sessionToken: s.sessionToken, expiresAt: s.expiresAt, agent: s.agent }));
|
|
410
|
+
} catch {
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const clearStoredSession = () => {
|
|
414
|
+
try {
|
|
415
|
+
store2?.removeItem(sessKey);
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
};
|
|
141
419
|
const emit = (e) => {
|
|
142
420
|
for (const l of listeners) l(e);
|
|
143
421
|
};
|
|
@@ -225,6 +503,14 @@ var OyehelloChat = (function (exports) {
|
|
|
225
503
|
emit({ type: "agentTyping", active: !!ev.active });
|
|
226
504
|
} else if (ev.type === "suggestions") {
|
|
227
505
|
emit({ type: "suggestions", items: Array.isArray(ev.items) ? ev.items : [] });
|
|
506
|
+
} else if (ev.type === "done") {
|
|
507
|
+
closed = true;
|
|
508
|
+
clearReconnect();
|
|
509
|
+
clearRefresh();
|
|
510
|
+
es?.close();
|
|
511
|
+
es = null;
|
|
512
|
+
clearStoredSession();
|
|
513
|
+
setState("ended");
|
|
228
514
|
}
|
|
229
515
|
};
|
|
230
516
|
const openStream = async () => {
|
|
@@ -256,6 +542,7 @@ var OyehelloChat = (function (exports) {
|
|
|
256
542
|
reconnectTimer = setTimeout(() => {
|
|
257
543
|
openStream().catch((err) => {
|
|
258
544
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
545
|
+
clearStoredSession();
|
|
259
546
|
open().catch((e) => {
|
|
260
547
|
emit({ type: "error", error: e });
|
|
261
548
|
scheduleReconnect();
|
|
@@ -267,16 +554,32 @@ var OyehelloChat = (function (exports) {
|
|
|
267
554
|
}, delay);
|
|
268
555
|
};
|
|
269
556
|
const open = async () => {
|
|
270
|
-
|
|
557
|
+
const resumed = loadStoredSession();
|
|
558
|
+
if (resumed) {
|
|
559
|
+
session = resumed;
|
|
560
|
+
emit({ type: "agent", agent: resumed.agent });
|
|
561
|
+
await openStream();
|
|
562
|
+
scheduleRefresh();
|
|
563
|
+
flushJourney();
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
567
|
+
session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(toolHandlers.keys()), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
|
|
568
|
+
persistSession(session);
|
|
569
|
+
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
570
|
+
warnedProtocol = true;
|
|
571
|
+
try {
|
|
572
|
+
console.warn(`[oyehello-chat] SDK protocol v${CHAT_PROTOCOL_VERSION} vs server v${session.protocol} - update @oyehello/chat to the latest version.`);
|
|
573
|
+
} catch {
|
|
574
|
+
}
|
|
575
|
+
}
|
|
271
576
|
emit({ type: "agent", agent: session.agent });
|
|
272
577
|
await openStream();
|
|
578
|
+
scheduleRefresh();
|
|
579
|
+
flushJourney();
|
|
273
580
|
};
|
|
274
581
|
const uploadFiles = async (files) => {
|
|
275
|
-
return Promise.all(files.map(
|
|
276
|
-
const { storageRef, uploadUrl } = await presignAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, contentType: file.type, fetchImpl });
|
|
277
|
-
await uploadToPresigned(uploadUrl, file, fetchImpl);
|
|
278
|
-
return { storageRef, contentType: file.type, name: file.name };
|
|
279
|
-
}));
|
|
582
|
+
return Promise.all(files.map((file) => uploadAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, file, fetchImpl })));
|
|
280
583
|
};
|
|
281
584
|
return {
|
|
282
585
|
getMessages: () => messages.slice(),
|
|
@@ -295,6 +598,13 @@ var OyehelloChat = (function (exports) {
|
|
|
295
598
|
getWidgetConfig() {
|
|
296
599
|
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
297
600
|
},
|
|
601
|
+
recordPageView(path) {
|
|
602
|
+
try {
|
|
603
|
+
recordView(options.agentId, path);
|
|
604
|
+
flushJourney();
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
},
|
|
298
608
|
async start() {
|
|
299
609
|
if (state === "opening" || state === "ready") return;
|
|
300
610
|
closed = false;
|
|
@@ -340,6 +650,7 @@ var OyehelloChat = (function (exports) {
|
|
|
340
650
|
} catch (err) {
|
|
341
651
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
342
652
|
try {
|
|
653
|
+
clearStoredSession();
|
|
343
654
|
await open();
|
|
344
655
|
const freshAttachments = fileList.length ? await uploadFiles(fileList) : [];
|
|
345
656
|
await postMessage({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, text: text2, attachments: freshAttachments, fetchImpl });
|
|
@@ -351,13 +662,38 @@ var OyehelloChat = (function (exports) {
|
|
|
351
662
|
}
|
|
352
663
|
}
|
|
353
664
|
},
|
|
665
|
+
detach() {
|
|
666
|
+
closed = true;
|
|
667
|
+
clearReconnect();
|
|
668
|
+
clearRefresh();
|
|
669
|
+
es?.close();
|
|
670
|
+
es = null;
|
|
671
|
+
},
|
|
354
672
|
async end() {
|
|
355
673
|
closed = true;
|
|
356
674
|
clearReconnect();
|
|
675
|
+
clearRefresh();
|
|
676
|
+
clearStoredSession();
|
|
357
677
|
if (session) await endSession({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
358
678
|
es?.close();
|
|
359
679
|
es = null;
|
|
360
680
|
setState("ended");
|
|
681
|
+
},
|
|
682
|
+
reset() {
|
|
683
|
+
clearStoredSession();
|
|
684
|
+
es?.close();
|
|
685
|
+
es = null;
|
|
686
|
+
clearReconnect();
|
|
687
|
+
clearRefresh();
|
|
688
|
+
session = null;
|
|
689
|
+
streamingId = null;
|
|
690
|
+
handoffState = "ai";
|
|
691
|
+
messages.length = 0;
|
|
692
|
+
seenTurnIds.clear();
|
|
693
|
+
seenCallIds.clear();
|
|
694
|
+
emit({ type: "messages", messages: [] });
|
|
695
|
+
emit({ type: "handoff", state: "ai" });
|
|
696
|
+
setState("idle");
|
|
361
697
|
}
|
|
362
698
|
};
|
|
363
699
|
}
|
|
@@ -3319,6 +3655,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3319
3655
|
--oc-glass-border: rgba(9,13,20,0.12);
|
|
3320
3656
|
|
|
3321
3657
|
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
3658
|
+
/* Explicit display/visibility so a host-page reset or consent/privacy tool that blanket-hides
|
|
3659
|
+
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
3660
|
+
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
3661
|
+
display: block; visibility: visible; opacity: 1;
|
|
3322
3662
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
3323
3663
|
-webkit-font-smoothing: antialiased;
|
|
3324
3664
|
}
|
|
@@ -3358,7 +3698,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3358
3698
|
.launcher:hover { transform: scale(1.06); }
|
|
3359
3699
|
.launcher:active { transform: scale(0.93); }
|
|
3360
3700
|
.ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
|
|
3361
|
-
.oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
3701
|
+
.oc.show-ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
3362
3702
|
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
3363
3703
|
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
3364
3704
|
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
@@ -3380,7 +3720,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3380
3720
|
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
3381
3721
|
}
|
|
3382
3722
|
.oc.has-teaser .teaser { display: flex; }
|
|
3383
|
-
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3723
|
+
.oc.show-teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3384
3724
|
.teaser-btn {
|
|
3385
3725
|
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
3386
3726
|
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
@@ -3427,6 +3767,12 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3427
3767
|
.oc-err-retry { padding: 7px 15px; font-size: 12.5px; font-weight: 500; border-radius: var(--oc-r-full); border: 1px solid var(--oc-border); background: var(--oc-tint-2); color: var(--oc-fg); cursor: pointer; transition: background .18s var(--oc-ease); }
|
|
3428
3768
|
.oc-err-retry:hover { background: var(--oc-tint-3); }
|
|
3429
3769
|
|
|
3770
|
+
/* Session-ended state, inside the chat window */
|
|
3771
|
+
.oc-ended { margin: auto; display: flex; flex-direction: column; align-items: center; gap: 12px; padding: 24px 16px; text-align: center; }
|
|
3772
|
+
.oc-ended-t { font-size: 13.5px; line-height: 1.5; color: var(--oc-fg-muted); text-wrap: pretty; }
|
|
3773
|
+
.oc-newmsg { padding: 9px 18px; font-size: 13px; font-weight: 600; border-radius: var(--oc-r-full); border: 1px solid var(--oc-btn-rim); background: var(--oc-ac); color: #fff; box-shadow: var(--oc-btn-edge); cursor: pointer; transition: filter .18s var(--oc-ease); }
|
|
3774
|
+
.oc-newmsg:hover { filter: brightness(1.08); }
|
|
3775
|
+
|
|
3430
3776
|
/* \u2500\u2500 Message list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3431
3777
|
.body { flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; padding: 16px 16px 26px; background: var(--oc-bg); scroll-behavior: smooth; }
|
|
3432
3778
|
.turn { display: flex; flex-direction: column; gap: 6px; animation: w-in .42s var(--oc-ease) both; }
|
|
@@ -3471,6 +3817,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3471
3817
|
|
|
3472
3818
|
/* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3473
3819
|
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
3820
|
+
.composer[hidden] { display: none; }
|
|
3474
3821
|
.field { display: flex; flex-direction: column; gap: 11px; padding: 13px 14px 11px; border-radius: 18px; border: 1px solid var(--oc-field-border); background: var(--oc-field-bg); transition: border-color .18s var(--oc-ease); }
|
|
3475
3822
|
.oc.focus .field { border-color: var(--oc-ac); }
|
|
3476
3823
|
.input { width: 100%; border: none; background: transparent; color: var(--oc-fg); font-size: 13.5px; line-height: 1.4; outline: none; padding: 0; resize: none; max-height: 108px; font-family: inherit; }
|
|
@@ -3696,14 +4043,29 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3696
4043
|
// Server (flow) suggestions for the latest turn; null until the first `suggestions` event, then the
|
|
3697
4044
|
// widget prefers these over the static `suggestions` attribute.
|
|
3698
4045
|
serverSuggestions = null;
|
|
4046
|
+
// The live streaming assistant bubble's markdown element + message id — so growing tokens patch just
|
|
4047
|
+
// that element in place instead of rebuilding the whole message list on every token.
|
|
4048
|
+
streamId = null;
|
|
4049
|
+
streamEl = null;
|
|
4050
|
+
audioCtx = null;
|
|
3699
4051
|
teaserText = "Need a hand? Ask me anything.";
|
|
3700
4052
|
teaserShownOnce = false;
|
|
4053
|
+
// Staffed-hours: set from widget-config. When offline, the panel shows offlineMessage and the
|
|
4054
|
+
// composer is disabled (opening a session is refused server-side anyway).
|
|
4055
|
+
offline = false;
|
|
4056
|
+
offlineMessage = "We're offline right now. Please check back during business hours.";
|
|
4057
|
+
configLoaded = false;
|
|
4058
|
+
// widget-config resolved (so we know online/offline before starting)
|
|
3701
4059
|
idleTimer = null;
|
|
3702
4060
|
idleReset = null;
|
|
4061
|
+
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
4062
|
+
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
4063
|
+
uninstallSpaHooks = null;
|
|
3703
4064
|
// ── shadow refs ──
|
|
3704
4065
|
oc;
|
|
3705
4066
|
bodyEl;
|
|
3706
4067
|
textareaEl;
|
|
4068
|
+
composerEl;
|
|
3707
4069
|
fileEl;
|
|
3708
4070
|
titleEl;
|
|
3709
4071
|
statusEl;
|
|
@@ -3725,7 +4087,16 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3725
4087
|
this.pendingTools = {};
|
|
3726
4088
|
}
|
|
3727
4089
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
4090
|
+
recordView(agentId, location.pathname);
|
|
4091
|
+
try {
|
|
4092
|
+
this.uninstallSpaHooks = installSpaHooks(() => this.client?.recordPageView(location.pathname));
|
|
4093
|
+
} catch {
|
|
4094
|
+
this.uninstallSpaHooks = null;
|
|
4095
|
+
}
|
|
3728
4096
|
this.renderShell();
|
|
4097
|
+
this.style.setProperty("display", "block", "important");
|
|
4098
|
+
this.style.setProperty("visibility", "visible", "important");
|
|
4099
|
+
this.style.setProperty("opacity", "1", "important");
|
|
3729
4100
|
this.renderMessages();
|
|
3730
4101
|
this.updateHeader();
|
|
3731
4102
|
this.updateStatus();
|
|
@@ -3742,7 +4113,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3742
4113
|
}
|
|
3743
4114
|
this.updateClasses(true);
|
|
3744
4115
|
this.timers.push(setTimeout(() => {
|
|
3745
|
-
this.oc?.classList.remove("ring");
|
|
4116
|
+
this.oc?.classList.remove("show-ring");
|
|
3746
4117
|
}, 6e3));
|
|
3747
4118
|
this.armIdleTeaser();
|
|
3748
4119
|
}
|
|
@@ -3795,7 +4166,19 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3795
4166
|
}
|
|
3796
4167
|
if (cfg.brand.logoUrl) this.applyBrandLogo(cfg.brand.logoUrl);
|
|
3797
4168
|
}
|
|
4169
|
+
if (cfg.offline) {
|
|
4170
|
+
this.offline = true;
|
|
4171
|
+
if (cfg.offlineMessage) this.offlineMessage = cfg.offlineMessage;
|
|
4172
|
+
this.renderMessages();
|
|
4173
|
+
this.updateStatus();
|
|
4174
|
+
}
|
|
3798
4175
|
} catch {
|
|
4176
|
+
} finally {
|
|
4177
|
+
this.configLoaded = true;
|
|
4178
|
+
if (this.uiOpen && !this.started && !this.offline && this.client) {
|
|
4179
|
+
this.started = true;
|
|
4180
|
+
void this.client.start();
|
|
4181
|
+
}
|
|
3799
4182
|
}
|
|
3800
4183
|
}
|
|
3801
4184
|
applyTeaserText() {
|
|
@@ -3817,8 +4200,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3817
4200
|
}
|
|
3818
4201
|
disconnectedCallback() {
|
|
3819
4202
|
this.off?.();
|
|
3820
|
-
|
|
4203
|
+
this.client?.detach();
|
|
3821
4204
|
this.teardownIdle();
|
|
4205
|
+
this.uninstallSpaHooks?.();
|
|
4206
|
+
this.uninstallSpaHooks = null;
|
|
3822
4207
|
for (const t of this.timers) clearTimeout(t);
|
|
3823
4208
|
for (const url of this.imagePreviewCache.values()) {
|
|
3824
4209
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -3896,6 +4281,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3896
4281
|
this.oc = root.querySelector(".oc");
|
|
3897
4282
|
this.bodyEl = root.querySelector(".body");
|
|
3898
4283
|
this.textareaEl = root.querySelector("textarea.input");
|
|
4284
|
+
this.composerEl = root.querySelector(".composer");
|
|
3899
4285
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
3900
4286
|
this.titleEl = root.querySelector(".hd-title");
|
|
3901
4287
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
@@ -3954,18 +4340,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3954
4340
|
this.shadowRoot.innerHTML = `<div style="position:fixed;right:16px;bottom:16px;color:#b91c1c;font:12px system-ui;background:#fef2f2;border:1px solid #fecaca;padding:8px 12px;border-radius:8px">${escapeHtml(msg)}</div>`;
|
|
3955
4341
|
}
|
|
3956
4342
|
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
4343
|
+
// State classes on `.oc` MUST NOT share a name with any element class that has a bare base rule
|
|
4344
|
+
// (e.g. `.teaser`, `.ring`): the bare selector would also match `.oc.<state>` and, tying `.oc`'s
|
|
4345
|
+
// own rule on specificity, win on source order — e.g. `.teaser{display:none}` would hide the whole
|
|
4346
|
+
// widget. Hence the `show-` prefix on the teaser/ring state classes.
|
|
3957
4347
|
updateClasses(initialRing = false) {
|
|
3958
4348
|
if (!this.oc) return;
|
|
3959
4349
|
const c = this.oc.classList;
|
|
3960
4350
|
c.toggle("open", this.uiOpen);
|
|
3961
4351
|
c.toggle("max", this.uiMax);
|
|
3962
4352
|
c.toggle("emoji", this.emojiOpen);
|
|
3963
|
-
c.toggle("teaser", this.teaserActive);
|
|
4353
|
+
c.toggle("show-teaser", this.teaserActive);
|
|
3964
4354
|
c.toggle("away", this.away);
|
|
3965
4355
|
c.toggle("focus", this.focused);
|
|
3966
4356
|
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
3967
4357
|
c.toggle("unread", this.unseen > 0);
|
|
3968
|
-
if (initialRing) c.add("ring");
|
|
4358
|
+
if (initialRing) c.add("show-ring");
|
|
3969
4359
|
const badge = this.oc.querySelector(".badge");
|
|
3970
4360
|
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
3971
4361
|
const launcher = this.oc.querySelector(".launcher");
|
|
@@ -3984,7 +4374,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3984
4374
|
sessionStorage.setItem(this.key("open"), "1");
|
|
3985
4375
|
} catch {
|
|
3986
4376
|
}
|
|
3987
|
-
if (!this.started && this.client) {
|
|
4377
|
+
if (!this.started && this.client && this.configLoaded && !this.offline) {
|
|
3988
4378
|
this.started = true;
|
|
3989
4379
|
void this.client.start();
|
|
3990
4380
|
}
|
|
@@ -4032,16 +4422,23 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4032
4422
|
}
|
|
4033
4423
|
updateStatus() {
|
|
4034
4424
|
if (!this.textareaEl) return;
|
|
4035
|
-
const disabled = this.state !== "ready";
|
|
4425
|
+
const disabled = this.state !== "ready" || this.offline;
|
|
4036
4426
|
this.textareaEl.disabled = disabled;
|
|
4427
|
+
if (this.composerEl) this.composerEl.hidden = this.state === "ended" || this.offline;
|
|
4037
4428
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
4038
|
-
const text2 = this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
4429
|
+
const text2 = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
4039
4430
|
if (this.statusEl) this.statusEl.textContent = text2;
|
|
4040
4431
|
}
|
|
4041
4432
|
// ── Message list + scroll ─────────────────────────────────────────────────────────────────────
|
|
4042
4433
|
renderMessages() {
|
|
4043
4434
|
if (!this.bodyEl) return;
|
|
4044
4435
|
const body = this.bodyEl;
|
|
4436
|
+
const last = this.messages[this.messages.length - 1];
|
|
4437
|
+
if (this.uiOpen && this.streamEl && last && last.id === this.streamId && last.role === "assistant" && last.streaming && this.messages.length === this.renderedCount) {
|
|
4438
|
+
this.streamEl.innerHTML = renderMarkdown(last.text || "");
|
|
4439
|
+
if (!this.away) this.scrollToBottom();
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4045
4442
|
const prevTop = body.scrollTop;
|
|
4046
4443
|
const added = this.messages.length - this.renderedCount;
|
|
4047
4444
|
const botFinal = this.messages.filter((m2) => m2.role === "assistant" && !m2.streaming).length;
|
|
@@ -4061,13 +4458,29 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4061
4458
|
if (this.state === "error" && this.lastError) {
|
|
4062
4459
|
html2 += `<div class="oc-err"><span class="oc-err-t">${escapeHtml(this.lastError)}</span><button type="button" class="oc-err-retry">Try again</button></div>`;
|
|
4063
4460
|
}
|
|
4461
|
+
if (this.state === "ended") {
|
|
4462
|
+
html2 += `<div class="oc-ended"><span class="oc-ended-t">Your conversation has ended.</span><button type="button" class="oc-newmsg">New message</button></div>`;
|
|
4463
|
+
}
|
|
4464
|
+
if (this.offline && this.messages.length === 0 && this.state !== "ended") {
|
|
4465
|
+
html2 += `<div class="oc-ended"><span class="oc-ended-t">${escapeHtml(this.offlineMessage)}</span></div>`;
|
|
4466
|
+
}
|
|
4064
4467
|
body.innerHTML = html2;
|
|
4065
4468
|
this.hydrateAttachmentImages();
|
|
4066
4469
|
this.bindChips();
|
|
4067
4470
|
this.renderedCount = this.messages.length;
|
|
4068
4471
|
this.renderedBotFinal = botFinal;
|
|
4472
|
+
this.streamId = null;
|
|
4473
|
+
this.streamEl = null;
|
|
4474
|
+
if (last && last.role === "assistant" && last.streaming) {
|
|
4475
|
+
const mds = this.bodyEl.querySelectorAll(".turn.bot .md");
|
|
4476
|
+
this.streamEl = mds[mds.length - 1] ?? null;
|
|
4477
|
+
if (this.streamEl) this.streamId = last.id;
|
|
4478
|
+
}
|
|
4069
4479
|
if (!this.uiOpen) {
|
|
4070
|
-
if (added > 0)
|
|
4480
|
+
if (added > 0) {
|
|
4481
|
+
this.unseen += added;
|
|
4482
|
+
this.maybeChime();
|
|
4483
|
+
}
|
|
4071
4484
|
this.updateClasses();
|
|
4072
4485
|
return;
|
|
4073
4486
|
}
|
|
@@ -4075,7 +4488,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4075
4488
|
else if (!this.away) this.scrollToBottom();
|
|
4076
4489
|
else {
|
|
4077
4490
|
body.scrollTop = prevTop;
|
|
4078
|
-
if (added > 0)
|
|
4491
|
+
if (added > 0) {
|
|
4492
|
+
this.unseen += added;
|
|
4493
|
+
this.maybeChime();
|
|
4494
|
+
}
|
|
4079
4495
|
}
|
|
4080
4496
|
this.updateClasses();
|
|
4081
4497
|
}
|
|
@@ -4126,6 +4542,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4126
4542
|
}
|
|
4127
4543
|
this.away = false;
|
|
4128
4544
|
this.emojiOpen = false;
|
|
4545
|
+
this.serverSuggestions = [];
|
|
4129
4546
|
void this.client?.sendMessage(text2, files.length > 0 ? files : void 0);
|
|
4130
4547
|
this.textareaEl.value = "";
|
|
4131
4548
|
this.textareaEl.style.height = "auto";
|
|
@@ -4134,6 +4551,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4134
4551
|
sendChip(text2) {
|
|
4135
4552
|
this.away = false;
|
|
4136
4553
|
this.emojiOpen = false;
|
|
4554
|
+
this.serverSuggestions = [];
|
|
4137
4555
|
void this.client?.sendMessage(text2);
|
|
4138
4556
|
this.updateClasses();
|
|
4139
4557
|
}
|
|
@@ -4147,6 +4565,56 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4147
4565
|
this.bodyEl.querySelector(".oc-err-retry")?.addEventListener("click", () => {
|
|
4148
4566
|
void this.client?.start();
|
|
4149
4567
|
});
|
|
4568
|
+
this.bodyEl.querySelector(".oc-newmsg")?.addEventListener("click", () => this.startFresh());
|
|
4569
|
+
}
|
|
4570
|
+
// Reset the thread and open a brand-new session (the widget's "New message" after a session ended).
|
|
4571
|
+
startFresh() {
|
|
4572
|
+
this.serverSuggestions = null;
|
|
4573
|
+
this.receivedAt.clear();
|
|
4574
|
+
this.mdCache.clear();
|
|
4575
|
+
this.streamId = null;
|
|
4576
|
+
this.streamEl = null;
|
|
4577
|
+
this.renderedCount = 0;
|
|
4578
|
+
this.renderedBotFinal = 0;
|
|
4579
|
+
this.away = false;
|
|
4580
|
+
this.unseen = 0;
|
|
4581
|
+
this.client?.reset();
|
|
4582
|
+
this.started = true;
|
|
4583
|
+
void this.client?.start();
|
|
4584
|
+
setTimeout(() => this.textareaEl?.focus(), 60);
|
|
4585
|
+
}
|
|
4586
|
+
// A short two-tone chime for an incoming message the visitor hasn't seen (panel closed or scrolled
|
|
4587
|
+
// up). Off when the `sound="off"` attribute is set. Uses WebAudio (no asset); silent if unavailable
|
|
4588
|
+
// or the browser hasn't allowed audio yet.
|
|
4589
|
+
maybeChime() {
|
|
4590
|
+
const last = this.messages[this.messages.length - 1];
|
|
4591
|
+
if (last && (last.role === "assistant" || last.role === "human")) this.playChime();
|
|
4592
|
+
}
|
|
4593
|
+
playChime() {
|
|
4594
|
+
if (this.getAttribute("sound") === "off") return;
|
|
4595
|
+
try {
|
|
4596
|
+
const Ctx = window.AudioContext ?? window.webkitAudioContext;
|
|
4597
|
+
if (!Ctx) return;
|
|
4598
|
+
if (!this.audioCtx) this.audioCtx = new Ctx();
|
|
4599
|
+
const ctx = this.audioCtx;
|
|
4600
|
+
if (ctx.state === "suspended") void ctx.resume();
|
|
4601
|
+
const now = ctx.currentTime;
|
|
4602
|
+
[880, 1174.66].forEach((freq, i) => {
|
|
4603
|
+
const osc = ctx.createOscillator();
|
|
4604
|
+
const gain = ctx.createGain();
|
|
4605
|
+
osc.type = "sine";
|
|
4606
|
+
osc.frequency.value = freq;
|
|
4607
|
+
const t = now + i * 0.11;
|
|
4608
|
+
gain.gain.setValueAtTime(1e-4, t);
|
|
4609
|
+
gain.gain.exponentialRampToValueAtTime(0.12, t + 0.02);
|
|
4610
|
+
gain.gain.exponentialRampToValueAtTime(1e-4, t + 0.18);
|
|
4611
|
+
osc.connect(gain);
|
|
4612
|
+
gain.connect(ctx.destination);
|
|
4613
|
+
osc.start(t);
|
|
4614
|
+
osc.stop(t + 0.2);
|
|
4615
|
+
});
|
|
4616
|
+
} catch {
|
|
4617
|
+
}
|
|
4150
4618
|
}
|
|
4151
4619
|
// ── Emoji ─────────────────────────────────────────────────────────────────────────────────────
|
|
4152
4620
|
toggleEmoji() {
|