@oyehello/chat 0.5.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/dist/index.cjs +304 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.global.js +304 -20
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +304 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -67,6 +67,15 @@ async function postMessage(opts) {
|
|
|
67
67
|
});
|
|
68
68
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
69
69
|
}
|
|
70
|
+
async function postPageViews(opts) {
|
|
71
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/page-views`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
credentials: "omit",
|
|
74
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${opts.sessionToken}` },
|
|
75
|
+
body: JSON.stringify({ views: opts.views })
|
|
76
|
+
});
|
|
77
|
+
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
78
|
+
}
|
|
70
79
|
async function endSession(opts) {
|
|
71
80
|
await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/end`, {
|
|
72
81
|
method: "POST",
|
|
@@ -84,6 +93,15 @@ async function postToolResult(opts) {
|
|
|
84
93
|
}).catch(() => {
|
|
85
94
|
});
|
|
86
95
|
}
|
|
96
|
+
async function refreshSessionToken(opts) {
|
|
97
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/refresh-token`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
credentials: "omit",
|
|
100
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` }
|
|
101
|
+
});
|
|
102
|
+
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
103
|
+
return await res.json();
|
|
104
|
+
}
|
|
87
105
|
async function mintStreamTicket(opts) {
|
|
88
106
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/stream-ticket`, {
|
|
89
107
|
method: "POST",
|
|
@@ -96,15 +114,18 @@ async function mintStreamTicket(opts) {
|
|
|
96
114
|
function streamUrl(baseUrl, sessionId, ticket) {
|
|
97
115
|
return `${baseUrl}/chat/sessions/${sessionId}/stream?ticket=${encodeURIComponent(ticket)}`;
|
|
98
116
|
}
|
|
99
|
-
async function
|
|
100
|
-
const
|
|
117
|
+
async function uploadAttachment(opts) {
|
|
118
|
+
const form = new FormData();
|
|
119
|
+
form.append("file", opts.file, opts.file.name);
|
|
120
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/upload`, {
|
|
101
121
|
method: "POST",
|
|
102
122
|
credentials: "omit",
|
|
103
|
-
headers: {
|
|
104
|
-
body:
|
|
123
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` },
|
|
124
|
+
body: form
|
|
105
125
|
});
|
|
106
126
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
107
|
-
|
|
127
|
+
const j = await res.json();
|
|
128
|
+
return { storageRef: j.storageRef, contentType: j.contentType, name: j.name };
|
|
108
129
|
}
|
|
109
130
|
async function getAttachmentUrl(opts) {
|
|
110
131
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/attachment-url`, {
|
|
@@ -116,10 +137,6 @@ async function getAttachmentUrl(opts) {
|
|
|
116
137
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
117
138
|
return (await res.json()).url;
|
|
118
139
|
}
|
|
119
|
-
async function uploadToPresigned(uploadUrl, file, fetchImpl) {
|
|
120
|
-
const res = await fetchImpl(uploadUrl, { method: "PUT", body: file, headers: { "Content-Type": file.type } });
|
|
121
|
-
if (!res.ok) throw new ChatApiError(`attachment upload failed with HTTP ${res.status}`, res.status);
|
|
122
|
-
}
|
|
123
140
|
|
|
124
141
|
// src/fingerprint.ts
|
|
125
142
|
async function sha256Hex(input) {
|
|
@@ -165,6 +182,135 @@ async function computeDeviceFingerprint() {
|
|
|
165
182
|
}
|
|
166
183
|
}
|
|
167
184
|
|
|
185
|
+
// src/journey.ts
|
|
186
|
+
var MAX_BUFFER = 25;
|
|
187
|
+
var storageKey = (agentId) => `oyehello-chat:journey:${agentId}`;
|
|
188
|
+
var store = () => {
|
|
189
|
+
try {
|
|
190
|
+
return globalThis.sessionStorage ?? null;
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function load(agentId) {
|
|
196
|
+
const s = store();
|
|
197
|
+
if (!s) return { views: [], flushed: 0 };
|
|
198
|
+
try {
|
|
199
|
+
const raw = s.getItem(storageKey(agentId));
|
|
200
|
+
if (!raw) return { views: [], flushed: 0 };
|
|
201
|
+
const parsed = JSON.parse(raw);
|
|
202
|
+
const views = Array.isArray(parsed?.views) ? parsed.views.filter((v) => !!v && typeof v.path === "string" && typeof v.at === "string") : [];
|
|
203
|
+
const flushedRaw = typeof parsed?.flushed === "number" && parsed.flushed >= 0 ? parsed.flushed : 0;
|
|
204
|
+
return { views, flushed: Math.min(flushedRaw, views.length) };
|
|
205
|
+
} catch {
|
|
206
|
+
return { views: [], flushed: 0 };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function save(agentId, data) {
|
|
210
|
+
const s = store();
|
|
211
|
+
if (!s) return;
|
|
212
|
+
try {
|
|
213
|
+
s.setItem(storageKey(agentId), JSON.stringify(data));
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
var sanitizePath = (raw) => (raw || "/").split("?")[0].split("#")[0];
|
|
218
|
+
function recordView(agentId, path) {
|
|
219
|
+
try {
|
|
220
|
+
const p = sanitizePath(path);
|
|
221
|
+
const data = load(agentId);
|
|
222
|
+
const last = data.views[data.views.length - 1];
|
|
223
|
+
if (last && last.path === p) return;
|
|
224
|
+
data.views.push({ path: p, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
225
|
+
if (data.views.length > MAX_BUFFER) {
|
|
226
|
+
const drop = data.views.length - MAX_BUFFER;
|
|
227
|
+
data.views = data.views.slice(drop);
|
|
228
|
+
data.flushed = Math.max(0, data.flushed - drop);
|
|
229
|
+
}
|
|
230
|
+
save(agentId, data);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function unsentViews(agentId) {
|
|
235
|
+
try {
|
|
236
|
+
const data = load(agentId);
|
|
237
|
+
return data.views.slice(data.flushed);
|
|
238
|
+
} catch {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function markFlushed(agentId, count) {
|
|
243
|
+
if (count <= 0) return;
|
|
244
|
+
try {
|
|
245
|
+
const data = load(agentId);
|
|
246
|
+
data.flushed = Math.min(data.views.length, data.flushed + count);
|
|
247
|
+
save(agentId, data);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function installSpaHooks(onNav) {
|
|
252
|
+
const originalPush = history.pushState;
|
|
253
|
+
const originalReplace = history.replaceState;
|
|
254
|
+
let pushInstalled = false;
|
|
255
|
+
let replaceInstalled = false;
|
|
256
|
+
try {
|
|
257
|
+
history.pushState = function(...args) {
|
|
258
|
+
const ret = originalPush.apply(this, args);
|
|
259
|
+
try {
|
|
260
|
+
onNav();
|
|
261
|
+
} catch {
|
|
262
|
+
}
|
|
263
|
+
return ret;
|
|
264
|
+
};
|
|
265
|
+
pushInstalled = true;
|
|
266
|
+
} catch {
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
history.replaceState = function(...args) {
|
|
270
|
+
const ret = originalReplace.apply(this, args);
|
|
271
|
+
try {
|
|
272
|
+
onNav();
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
return ret;
|
|
276
|
+
};
|
|
277
|
+
replaceInstalled = true;
|
|
278
|
+
} catch {
|
|
279
|
+
}
|
|
280
|
+
const onPopState = () => {
|
|
281
|
+
try {
|
|
282
|
+
onNav();
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
let popstateInstalled = false;
|
|
287
|
+
try {
|
|
288
|
+
window.addEventListener("popstate", onPopState);
|
|
289
|
+
popstateInstalled = true;
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
return () => {
|
|
293
|
+
if (pushInstalled) {
|
|
294
|
+
try {
|
|
295
|
+
history.pushState = originalPush;
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (replaceInstalled) {
|
|
300
|
+
try {
|
|
301
|
+
history.replaceState = originalReplace;
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (popstateInstalled) {
|
|
306
|
+
try {
|
|
307
|
+
window.removeEventListener("popstate", onPopState);
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
168
314
|
// src/client.ts
|
|
169
315
|
var seq = 0;
|
|
170
316
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
@@ -187,13 +333,95 @@ function createChatClient(options) {
|
|
|
187
333
|
let closed = false;
|
|
188
334
|
let warnedProtocol = false;
|
|
189
335
|
let deviceFingerprint;
|
|
336
|
+
let refreshTimer = null;
|
|
190
337
|
const toolHandlers = /* @__PURE__ */ new Map();
|
|
338
|
+
let journeyFlushChain = Promise.resolve();
|
|
339
|
+
let journeyFlushQueued = false;
|
|
340
|
+
const flushJourney = () => {
|
|
341
|
+
if (!session) return;
|
|
342
|
+
if (journeyFlushQueued) return;
|
|
343
|
+
journeyFlushQueued = true;
|
|
344
|
+
journeyFlushChain = journeyFlushChain.then(async () => {
|
|
345
|
+
journeyFlushQueued = false;
|
|
346
|
+
if (!session) return;
|
|
347
|
+
const pending = unsentViews(options.agentId);
|
|
348
|
+
if (pending.length === 0) return;
|
|
349
|
+
const s = session;
|
|
350
|
+
try {
|
|
351
|
+
await postPageViews({ baseUrl: options.baseUrl, sessionId: s.sessionId, sessionToken: s.sessionToken, views: pending, fetchImpl });
|
|
352
|
+
markFlushed(options.agentId, pending.length);
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
};
|
|
191
357
|
const clearReconnect = () => {
|
|
192
358
|
if (reconnectTimer) {
|
|
193
359
|
clearTimeout(reconnectTimer);
|
|
194
360
|
reconnectTimer = null;
|
|
195
361
|
}
|
|
196
362
|
};
|
|
363
|
+
const clearRefresh = () => {
|
|
364
|
+
if (refreshTimer) {
|
|
365
|
+
clearTimeout(refreshTimer);
|
|
366
|
+
refreshTimer = null;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const scheduleRefresh = () => {
|
|
370
|
+
clearRefresh();
|
|
371
|
+
if (closed || !session) return;
|
|
372
|
+
const untilExpiry = Date.parse(session.expiresAt) - Date.now();
|
|
373
|
+
const delay = Number.isNaN(untilExpiry) ? 10 * 6e4 : Math.max(5e3, untilExpiry - 6e4);
|
|
374
|
+
refreshTimer = setTimeout(async () => {
|
|
375
|
+
if (closed || !session) return;
|
|
376
|
+
try {
|
|
377
|
+
const r = await refreshSessionToken({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
378
|
+
session = { ...session, sessionToken: r.sessionToken, expiresAt: r.expiresAt };
|
|
379
|
+
persistSession(session);
|
|
380
|
+
scheduleRefresh();
|
|
381
|
+
} catch (err) {
|
|
382
|
+
const code = err?.code;
|
|
383
|
+
if (code === "session_ended" || code === "session_expired") clearStoredSession();
|
|
384
|
+
}
|
|
385
|
+
}, delay);
|
|
386
|
+
refreshTimer.unref?.();
|
|
387
|
+
};
|
|
388
|
+
const sessKey = `oyehello-chat-sess:${options.agentId}`;
|
|
389
|
+
const store2 = (() => {
|
|
390
|
+
try {
|
|
391
|
+
return globalThis.sessionStorage ?? null;
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
})();
|
|
396
|
+
const loadStoredSession = () => {
|
|
397
|
+
if (!store2) return null;
|
|
398
|
+
try {
|
|
399
|
+
const raw = store2.getItem(sessKey);
|
|
400
|
+
if (!raw) return null;
|
|
401
|
+
const s = JSON.parse(raw);
|
|
402
|
+
if (!s?.sessionId || !s?.sessionToken || !s?.expiresAt || !s?.agent) return null;
|
|
403
|
+
const exp = Date.parse(s.expiresAt);
|
|
404
|
+
if (Number.isNaN(exp) || Date.now() >= exp - 5e3) {
|
|
405
|
+
store2.removeItem(sessKey);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
return s;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const persistSession = (s) => {
|
|
414
|
+
try {
|
|
415
|
+
store2?.setItem(sessKey, JSON.stringify({ sessionId: s.sessionId, sessionToken: s.sessionToken, expiresAt: s.expiresAt, agent: s.agent }));
|
|
416
|
+
} catch {
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
const clearStoredSession = () => {
|
|
420
|
+
try {
|
|
421
|
+
store2?.removeItem(sessKey);
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
};
|
|
197
425
|
const emit = (e) => {
|
|
198
426
|
for (const l of listeners) l(e);
|
|
199
427
|
};
|
|
@@ -284,8 +512,10 @@ function createChatClient(options) {
|
|
|
284
512
|
} else if (ev.type === "done") {
|
|
285
513
|
closed = true;
|
|
286
514
|
clearReconnect();
|
|
515
|
+
clearRefresh();
|
|
287
516
|
es?.close();
|
|
288
517
|
es = null;
|
|
518
|
+
clearStoredSession();
|
|
289
519
|
setState("ended");
|
|
290
520
|
}
|
|
291
521
|
};
|
|
@@ -318,6 +548,7 @@ function createChatClient(options) {
|
|
|
318
548
|
reconnectTimer = setTimeout(() => {
|
|
319
549
|
openStream().catch((err) => {
|
|
320
550
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
551
|
+
clearStoredSession();
|
|
321
552
|
open().catch((e) => {
|
|
322
553
|
emit({ type: "error", error: e });
|
|
323
554
|
scheduleReconnect();
|
|
@@ -329,8 +560,18 @@ function createChatClient(options) {
|
|
|
329
560
|
}, delay);
|
|
330
561
|
};
|
|
331
562
|
const open = async () => {
|
|
563
|
+
const resumed = loadStoredSession();
|
|
564
|
+
if (resumed) {
|
|
565
|
+
session = resumed;
|
|
566
|
+
emit({ type: "agent", agent: resumed.agent });
|
|
567
|
+
await openStream();
|
|
568
|
+
scheduleRefresh();
|
|
569
|
+
flushJourney();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
332
572
|
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
333
573
|
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 });
|
|
574
|
+
persistSession(session);
|
|
334
575
|
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
335
576
|
warnedProtocol = true;
|
|
336
577
|
try {
|
|
@@ -340,13 +581,11 @@ function createChatClient(options) {
|
|
|
340
581
|
}
|
|
341
582
|
emit({ type: "agent", agent: session.agent });
|
|
342
583
|
await openStream();
|
|
584
|
+
scheduleRefresh();
|
|
585
|
+
flushJourney();
|
|
343
586
|
};
|
|
344
587
|
const uploadFiles = async (files) => {
|
|
345
|
-
return Promise.all(files.map(
|
|
346
|
-
const { storageRef, uploadUrl } = await presignAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, contentType: file.type, fetchImpl });
|
|
347
|
-
await uploadToPresigned(uploadUrl, file, fetchImpl);
|
|
348
|
-
return { storageRef, contentType: file.type, name: file.name };
|
|
349
|
-
}));
|
|
588
|
+
return Promise.all(files.map((file) => uploadAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, file, fetchImpl })));
|
|
350
589
|
};
|
|
351
590
|
return {
|
|
352
591
|
getMessages: () => messages.slice(),
|
|
@@ -365,6 +604,13 @@ function createChatClient(options) {
|
|
|
365
604
|
getWidgetConfig() {
|
|
366
605
|
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
367
606
|
},
|
|
607
|
+
recordPageView(path) {
|
|
608
|
+
try {
|
|
609
|
+
recordView(options.agentId, path);
|
|
610
|
+
flushJourney();
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
},
|
|
368
614
|
async start() {
|
|
369
615
|
if (state === "opening" || state === "ready") return;
|
|
370
616
|
closed = false;
|
|
@@ -410,6 +656,7 @@ function createChatClient(options) {
|
|
|
410
656
|
} catch (err) {
|
|
411
657
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
412
658
|
try {
|
|
659
|
+
clearStoredSession();
|
|
413
660
|
await open();
|
|
414
661
|
const freshAttachments = fileList.length ? await uploadFiles(fileList) : [];
|
|
415
662
|
await postMessage({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, text, attachments: freshAttachments, fetchImpl });
|
|
@@ -421,18 +668,29 @@ function createChatClient(options) {
|
|
|
421
668
|
}
|
|
422
669
|
}
|
|
423
670
|
},
|
|
671
|
+
detach() {
|
|
672
|
+
closed = true;
|
|
673
|
+
clearReconnect();
|
|
674
|
+
clearRefresh();
|
|
675
|
+
es?.close();
|
|
676
|
+
es = null;
|
|
677
|
+
},
|
|
424
678
|
async end() {
|
|
425
679
|
closed = true;
|
|
426
680
|
clearReconnect();
|
|
681
|
+
clearRefresh();
|
|
682
|
+
clearStoredSession();
|
|
427
683
|
if (session) await endSession({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
428
684
|
es?.close();
|
|
429
685
|
es = null;
|
|
430
686
|
setState("ended");
|
|
431
687
|
},
|
|
432
688
|
reset() {
|
|
689
|
+
clearStoredSession();
|
|
433
690
|
es?.close();
|
|
434
691
|
es = null;
|
|
435
692
|
clearReconnect();
|
|
693
|
+
clearRefresh();
|
|
436
694
|
session = null;
|
|
437
695
|
streamingId = null;
|
|
438
696
|
handoffState = "ai";
|
|
@@ -523,6 +781,10 @@ var STYLES = `
|
|
|
523
781
|
--oc-glass-border: rgba(9,13,20,0.12);
|
|
524
782
|
|
|
525
783
|
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
784
|
+
/* Explicit display/visibility so a host-page reset or consent/privacy tool that blanket-hides
|
|
785
|
+
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
786
|
+
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
787
|
+
display: block; visibility: visible; opacity: 1;
|
|
526
788
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
527
789
|
-webkit-font-smoothing: antialiased;
|
|
528
790
|
}
|
|
@@ -562,7 +824,7 @@ var STYLES = `
|
|
|
562
824
|
.launcher:hover { transform: scale(1.06); }
|
|
563
825
|
.launcher:active { transform: scale(0.93); }
|
|
564
826
|
.ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
|
|
565
|
-
.oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
827
|
+
.oc.show-ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
566
828
|
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
567
829
|
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
568
830
|
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
@@ -584,7 +846,7 @@ var STYLES = `
|
|
|
584
846
|
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
585
847
|
}
|
|
586
848
|
.oc.has-teaser .teaser { display: flex; }
|
|
587
|
-
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
849
|
+
.oc.show-teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
588
850
|
.teaser-btn {
|
|
589
851
|
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
590
852
|
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
@@ -681,6 +943,7 @@ var STYLES = `
|
|
|
681
943
|
|
|
682
944
|
/* \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 */
|
|
683
945
|
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
946
|
+
.composer[hidden] { display: none; }
|
|
684
947
|
.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); }
|
|
685
948
|
.oc.focus .field { border-color: var(--oc-ac); }
|
|
686
949
|
.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; }
|
|
@@ -921,10 +1184,14 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
921
1184
|
// widget-config resolved (so we know online/offline before starting)
|
|
922
1185
|
idleTimer = null;
|
|
923
1186
|
idleReset = null;
|
|
1187
|
+
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
1188
|
+
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
1189
|
+
uninstallSpaHooks = null;
|
|
924
1190
|
// ── shadow refs ──
|
|
925
1191
|
oc;
|
|
926
1192
|
bodyEl;
|
|
927
1193
|
textareaEl;
|
|
1194
|
+
composerEl;
|
|
928
1195
|
fileEl;
|
|
929
1196
|
titleEl;
|
|
930
1197
|
statusEl;
|
|
@@ -946,7 +1213,16 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
946
1213
|
this.pendingTools = {};
|
|
947
1214
|
}
|
|
948
1215
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
1216
|
+
recordView(agentId, location.pathname);
|
|
1217
|
+
try {
|
|
1218
|
+
this.uninstallSpaHooks = installSpaHooks(() => this.client?.recordPageView(location.pathname));
|
|
1219
|
+
} catch {
|
|
1220
|
+
this.uninstallSpaHooks = null;
|
|
1221
|
+
}
|
|
949
1222
|
this.renderShell();
|
|
1223
|
+
this.style.setProperty("display", "block", "important");
|
|
1224
|
+
this.style.setProperty("visibility", "visible", "important");
|
|
1225
|
+
this.style.setProperty("opacity", "1", "important");
|
|
950
1226
|
this.renderMessages();
|
|
951
1227
|
this.updateHeader();
|
|
952
1228
|
this.updateStatus();
|
|
@@ -963,7 +1239,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
963
1239
|
}
|
|
964
1240
|
this.updateClasses(true);
|
|
965
1241
|
this.timers.push(setTimeout(() => {
|
|
966
|
-
this.oc?.classList.remove("ring");
|
|
1242
|
+
this.oc?.classList.remove("show-ring");
|
|
967
1243
|
}, 6e3));
|
|
968
1244
|
this.armIdleTeaser();
|
|
969
1245
|
}
|
|
@@ -1050,8 +1326,10 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1050
1326
|
}
|
|
1051
1327
|
disconnectedCallback() {
|
|
1052
1328
|
this.off?.();
|
|
1053
|
-
|
|
1329
|
+
this.client?.detach();
|
|
1054
1330
|
this.teardownIdle();
|
|
1331
|
+
this.uninstallSpaHooks?.();
|
|
1332
|
+
this.uninstallSpaHooks = null;
|
|
1055
1333
|
for (const t of this.timers) clearTimeout(t);
|
|
1056
1334
|
for (const url of this.imagePreviewCache.values()) {
|
|
1057
1335
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -1129,6 +1407,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1129
1407
|
this.oc = root.querySelector(".oc");
|
|
1130
1408
|
this.bodyEl = root.querySelector(".body");
|
|
1131
1409
|
this.textareaEl = root.querySelector("textarea.input");
|
|
1410
|
+
this.composerEl = root.querySelector(".composer");
|
|
1132
1411
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
1133
1412
|
this.titleEl = root.querySelector(".hd-title");
|
|
1134
1413
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
@@ -1187,18 +1466,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1187
1466
|
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>`;
|
|
1188
1467
|
}
|
|
1189
1468
|
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
1469
|
+
// State classes on `.oc` MUST NOT share a name with any element class that has a bare base rule
|
|
1470
|
+
// (e.g. `.teaser`, `.ring`): the bare selector would also match `.oc.<state>` and, tying `.oc`'s
|
|
1471
|
+
// own rule on specificity, win on source order — e.g. `.teaser{display:none}` would hide the whole
|
|
1472
|
+
// widget. Hence the `show-` prefix on the teaser/ring state classes.
|
|
1190
1473
|
updateClasses(initialRing = false) {
|
|
1191
1474
|
if (!this.oc) return;
|
|
1192
1475
|
const c = this.oc.classList;
|
|
1193
1476
|
c.toggle("open", this.uiOpen);
|
|
1194
1477
|
c.toggle("max", this.uiMax);
|
|
1195
1478
|
c.toggle("emoji", this.emojiOpen);
|
|
1196
|
-
c.toggle("teaser", this.teaserActive);
|
|
1479
|
+
c.toggle("show-teaser", this.teaserActive);
|
|
1197
1480
|
c.toggle("away", this.away);
|
|
1198
1481
|
c.toggle("focus", this.focused);
|
|
1199
1482
|
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
1200
1483
|
c.toggle("unread", this.unseen > 0);
|
|
1201
|
-
if (initialRing) c.add("ring");
|
|
1484
|
+
if (initialRing) c.add("show-ring");
|
|
1202
1485
|
const badge = this.oc.querySelector(".badge");
|
|
1203
1486
|
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
1204
1487
|
const launcher = this.oc.querySelector(".launcher");
|
|
@@ -1267,6 +1550,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1267
1550
|
if (!this.textareaEl) return;
|
|
1268
1551
|
const disabled = this.state !== "ready" || this.offline;
|
|
1269
1552
|
this.textareaEl.disabled = disabled;
|
|
1553
|
+
if (this.composerEl) this.composerEl.hidden = this.state === "ended" || this.offline;
|
|
1270
1554
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1271
1555
|
const text = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1272
1556
|
if (this.statusEl) this.statusEl.textContent = text;
|