@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.js
CHANGED
|
@@ -61,6 +61,15 @@ async function postMessage(opts) {
|
|
|
61
61
|
});
|
|
62
62
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
63
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
|
+
}
|
|
64
73
|
async function endSession(opts) {
|
|
65
74
|
await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/end`, {
|
|
66
75
|
method: "POST",
|
|
@@ -78,6 +87,15 @@ async function postToolResult(opts) {
|
|
|
78
87
|
}).catch(() => {
|
|
79
88
|
});
|
|
80
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
|
+
}
|
|
81
99
|
async function mintStreamTicket(opts) {
|
|
82
100
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/stream-ticket`, {
|
|
83
101
|
method: "POST",
|
|
@@ -90,15 +108,18 @@ async function mintStreamTicket(opts) {
|
|
|
90
108
|
function streamUrl(baseUrl, sessionId, ticket) {
|
|
91
109
|
return `${baseUrl}/chat/sessions/${sessionId}/stream?ticket=${encodeURIComponent(ticket)}`;
|
|
92
110
|
}
|
|
93
|
-
async function
|
|
94
|
-
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`, {
|
|
95
115
|
method: "POST",
|
|
96
116
|
credentials: "omit",
|
|
97
|
-
headers: {
|
|
98
|
-
body:
|
|
117
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` },
|
|
118
|
+
body: form
|
|
99
119
|
});
|
|
100
120
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
101
|
-
|
|
121
|
+
const j = await res.json();
|
|
122
|
+
return { storageRef: j.storageRef, contentType: j.contentType, name: j.name };
|
|
102
123
|
}
|
|
103
124
|
async function getAttachmentUrl(opts) {
|
|
104
125
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/attachment-url`, {
|
|
@@ -110,10 +131,6 @@ async function getAttachmentUrl(opts) {
|
|
|
110
131
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
111
132
|
return (await res.json()).url;
|
|
112
133
|
}
|
|
113
|
-
async function uploadToPresigned(uploadUrl, file, fetchImpl) {
|
|
114
|
-
const res = await fetchImpl(uploadUrl, { method: "PUT", body: file, headers: { "Content-Type": file.type } });
|
|
115
|
-
if (!res.ok) throw new ChatApiError(`attachment upload failed with HTTP ${res.status}`, res.status);
|
|
116
|
-
}
|
|
117
134
|
|
|
118
135
|
// src/fingerprint.ts
|
|
119
136
|
async function sha256Hex(input) {
|
|
@@ -159,6 +176,135 @@ async function computeDeviceFingerprint() {
|
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
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((v) => !!v && typeof v.path === "string" && typeof v.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
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
162
308
|
// src/client.ts
|
|
163
309
|
var seq = 0;
|
|
164
310
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
@@ -181,13 +327,95 @@ function createChatClient(options) {
|
|
|
181
327
|
let closed = false;
|
|
182
328
|
let warnedProtocol = false;
|
|
183
329
|
let deviceFingerprint;
|
|
330
|
+
let refreshTimer = null;
|
|
184
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
|
+
};
|
|
185
351
|
const clearReconnect = () => {
|
|
186
352
|
if (reconnectTimer) {
|
|
187
353
|
clearTimeout(reconnectTimer);
|
|
188
354
|
reconnectTimer = null;
|
|
189
355
|
}
|
|
190
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
|
+
};
|
|
191
419
|
const emit = (e) => {
|
|
192
420
|
for (const l of listeners) l(e);
|
|
193
421
|
};
|
|
@@ -278,8 +506,10 @@ function createChatClient(options) {
|
|
|
278
506
|
} else if (ev.type === "done") {
|
|
279
507
|
closed = true;
|
|
280
508
|
clearReconnect();
|
|
509
|
+
clearRefresh();
|
|
281
510
|
es?.close();
|
|
282
511
|
es = null;
|
|
512
|
+
clearStoredSession();
|
|
283
513
|
setState("ended");
|
|
284
514
|
}
|
|
285
515
|
};
|
|
@@ -312,6 +542,7 @@ function createChatClient(options) {
|
|
|
312
542
|
reconnectTimer = setTimeout(() => {
|
|
313
543
|
openStream().catch((err) => {
|
|
314
544
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
545
|
+
clearStoredSession();
|
|
315
546
|
open().catch((e) => {
|
|
316
547
|
emit({ type: "error", error: e });
|
|
317
548
|
scheduleReconnect();
|
|
@@ -323,8 +554,18 @@ function createChatClient(options) {
|
|
|
323
554
|
}, delay);
|
|
324
555
|
};
|
|
325
556
|
const open = async () => {
|
|
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
|
+
}
|
|
326
566
|
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
327
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);
|
|
328
569
|
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
329
570
|
warnedProtocol = true;
|
|
330
571
|
try {
|
|
@@ -334,13 +575,11 @@ function createChatClient(options) {
|
|
|
334
575
|
}
|
|
335
576
|
emit({ type: "agent", agent: session.agent });
|
|
336
577
|
await openStream();
|
|
578
|
+
scheduleRefresh();
|
|
579
|
+
flushJourney();
|
|
337
580
|
};
|
|
338
581
|
const uploadFiles = async (files) => {
|
|
339
|
-
return Promise.all(files.map(
|
|
340
|
-
const { storageRef, uploadUrl } = await presignAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, contentType: file.type, fetchImpl });
|
|
341
|
-
await uploadToPresigned(uploadUrl, file, fetchImpl);
|
|
342
|
-
return { storageRef, contentType: file.type, name: file.name };
|
|
343
|
-
}));
|
|
582
|
+
return Promise.all(files.map((file) => uploadAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, file, fetchImpl })));
|
|
344
583
|
};
|
|
345
584
|
return {
|
|
346
585
|
getMessages: () => messages.slice(),
|
|
@@ -359,6 +598,13 @@ function createChatClient(options) {
|
|
|
359
598
|
getWidgetConfig() {
|
|
360
599
|
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
361
600
|
},
|
|
601
|
+
recordPageView(path) {
|
|
602
|
+
try {
|
|
603
|
+
recordView(options.agentId, path);
|
|
604
|
+
flushJourney();
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
},
|
|
362
608
|
async start() {
|
|
363
609
|
if (state === "opening" || state === "ready") return;
|
|
364
610
|
closed = false;
|
|
@@ -404,6 +650,7 @@ function createChatClient(options) {
|
|
|
404
650
|
} catch (err) {
|
|
405
651
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
406
652
|
try {
|
|
653
|
+
clearStoredSession();
|
|
407
654
|
await open();
|
|
408
655
|
const freshAttachments = fileList.length ? await uploadFiles(fileList) : [];
|
|
409
656
|
await postMessage({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, text, attachments: freshAttachments, fetchImpl });
|
|
@@ -415,18 +662,29 @@ function createChatClient(options) {
|
|
|
415
662
|
}
|
|
416
663
|
}
|
|
417
664
|
},
|
|
665
|
+
detach() {
|
|
666
|
+
closed = true;
|
|
667
|
+
clearReconnect();
|
|
668
|
+
clearRefresh();
|
|
669
|
+
es?.close();
|
|
670
|
+
es = null;
|
|
671
|
+
},
|
|
418
672
|
async end() {
|
|
419
673
|
closed = true;
|
|
420
674
|
clearReconnect();
|
|
675
|
+
clearRefresh();
|
|
676
|
+
clearStoredSession();
|
|
421
677
|
if (session) await endSession({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
422
678
|
es?.close();
|
|
423
679
|
es = null;
|
|
424
680
|
setState("ended");
|
|
425
681
|
},
|
|
426
682
|
reset() {
|
|
683
|
+
clearStoredSession();
|
|
427
684
|
es?.close();
|
|
428
685
|
es = null;
|
|
429
686
|
clearReconnect();
|
|
687
|
+
clearRefresh();
|
|
430
688
|
session = null;
|
|
431
689
|
streamingId = null;
|
|
432
690
|
handoffState = "ai";
|
|
@@ -517,6 +775,10 @@ var STYLES = `
|
|
|
517
775
|
--oc-glass-border: rgba(9,13,20,0.12);
|
|
518
776
|
|
|
519
777
|
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
778
|
+
/* Explicit display/visibility so a host-page reset or consent/privacy tool that blanket-hides
|
|
779
|
+
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
780
|
+
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
781
|
+
display: block; visibility: visible; opacity: 1;
|
|
520
782
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
521
783
|
-webkit-font-smoothing: antialiased;
|
|
522
784
|
}
|
|
@@ -556,7 +818,7 @@ var STYLES = `
|
|
|
556
818
|
.launcher:hover { transform: scale(1.06); }
|
|
557
819
|
.launcher:active { transform: scale(0.93); }
|
|
558
820
|
.ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
|
|
559
|
-
.oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
821
|
+
.oc.show-ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
560
822
|
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
561
823
|
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
562
824
|
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
@@ -578,7 +840,7 @@ var STYLES = `
|
|
|
578
840
|
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
579
841
|
}
|
|
580
842
|
.oc.has-teaser .teaser { display: flex; }
|
|
581
|
-
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
843
|
+
.oc.show-teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
582
844
|
.teaser-btn {
|
|
583
845
|
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
584
846
|
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
@@ -675,6 +937,7 @@ var STYLES = `
|
|
|
675
937
|
|
|
676
938
|
/* \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 */
|
|
677
939
|
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
940
|
+
.composer[hidden] { display: none; }
|
|
678
941
|
.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); }
|
|
679
942
|
.oc.focus .field { border-color: var(--oc-ac); }
|
|
680
943
|
.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; }
|
|
@@ -915,10 +1178,14 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
915
1178
|
// widget-config resolved (so we know online/offline before starting)
|
|
916
1179
|
idleTimer = null;
|
|
917
1180
|
idleReset = null;
|
|
1181
|
+
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
1182
|
+
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
1183
|
+
uninstallSpaHooks = null;
|
|
918
1184
|
// ── shadow refs ──
|
|
919
1185
|
oc;
|
|
920
1186
|
bodyEl;
|
|
921
1187
|
textareaEl;
|
|
1188
|
+
composerEl;
|
|
922
1189
|
fileEl;
|
|
923
1190
|
titleEl;
|
|
924
1191
|
statusEl;
|
|
@@ -940,7 +1207,16 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
940
1207
|
this.pendingTools = {};
|
|
941
1208
|
}
|
|
942
1209
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
1210
|
+
recordView(agentId, location.pathname);
|
|
1211
|
+
try {
|
|
1212
|
+
this.uninstallSpaHooks = installSpaHooks(() => this.client?.recordPageView(location.pathname));
|
|
1213
|
+
} catch {
|
|
1214
|
+
this.uninstallSpaHooks = null;
|
|
1215
|
+
}
|
|
943
1216
|
this.renderShell();
|
|
1217
|
+
this.style.setProperty("display", "block", "important");
|
|
1218
|
+
this.style.setProperty("visibility", "visible", "important");
|
|
1219
|
+
this.style.setProperty("opacity", "1", "important");
|
|
944
1220
|
this.renderMessages();
|
|
945
1221
|
this.updateHeader();
|
|
946
1222
|
this.updateStatus();
|
|
@@ -957,7 +1233,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
957
1233
|
}
|
|
958
1234
|
this.updateClasses(true);
|
|
959
1235
|
this.timers.push(setTimeout(() => {
|
|
960
|
-
this.oc?.classList.remove("ring");
|
|
1236
|
+
this.oc?.classList.remove("show-ring");
|
|
961
1237
|
}, 6e3));
|
|
962
1238
|
this.armIdleTeaser();
|
|
963
1239
|
}
|
|
@@ -1044,8 +1320,10 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1044
1320
|
}
|
|
1045
1321
|
disconnectedCallback() {
|
|
1046
1322
|
this.off?.();
|
|
1047
|
-
|
|
1323
|
+
this.client?.detach();
|
|
1048
1324
|
this.teardownIdle();
|
|
1325
|
+
this.uninstallSpaHooks?.();
|
|
1326
|
+
this.uninstallSpaHooks = null;
|
|
1049
1327
|
for (const t of this.timers) clearTimeout(t);
|
|
1050
1328
|
for (const url of this.imagePreviewCache.values()) {
|
|
1051
1329
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -1123,6 +1401,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1123
1401
|
this.oc = root.querySelector(".oc");
|
|
1124
1402
|
this.bodyEl = root.querySelector(".body");
|
|
1125
1403
|
this.textareaEl = root.querySelector("textarea.input");
|
|
1404
|
+
this.composerEl = root.querySelector(".composer");
|
|
1126
1405
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
1127
1406
|
this.titleEl = root.querySelector(".hd-title");
|
|
1128
1407
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
@@ -1181,18 +1460,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1181
1460
|
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>`;
|
|
1182
1461
|
}
|
|
1183
1462
|
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
1463
|
+
// State classes on `.oc` MUST NOT share a name with any element class that has a bare base rule
|
|
1464
|
+
// (e.g. `.teaser`, `.ring`): the bare selector would also match `.oc.<state>` and, tying `.oc`'s
|
|
1465
|
+
// own rule on specificity, win on source order — e.g. `.teaser{display:none}` would hide the whole
|
|
1466
|
+
// widget. Hence the `show-` prefix on the teaser/ring state classes.
|
|
1184
1467
|
updateClasses(initialRing = false) {
|
|
1185
1468
|
if (!this.oc) return;
|
|
1186
1469
|
const c = this.oc.classList;
|
|
1187
1470
|
c.toggle("open", this.uiOpen);
|
|
1188
1471
|
c.toggle("max", this.uiMax);
|
|
1189
1472
|
c.toggle("emoji", this.emojiOpen);
|
|
1190
|
-
c.toggle("teaser", this.teaserActive);
|
|
1473
|
+
c.toggle("show-teaser", this.teaserActive);
|
|
1191
1474
|
c.toggle("away", this.away);
|
|
1192
1475
|
c.toggle("focus", this.focused);
|
|
1193
1476
|
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
1194
1477
|
c.toggle("unread", this.unseen > 0);
|
|
1195
|
-
if (initialRing) c.add("ring");
|
|
1478
|
+
if (initialRing) c.add("show-ring");
|
|
1196
1479
|
const badge = this.oc.querySelector(".badge");
|
|
1197
1480
|
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
1198
1481
|
const launcher = this.oc.querySelector(".launcher");
|
|
@@ -1261,6 +1544,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1261
1544
|
if (!this.textareaEl) return;
|
|
1262
1545
|
const disabled = this.state !== "ready" || this.offline;
|
|
1263
1546
|
this.textareaEl.disabled = disabled;
|
|
1547
|
+
if (this.composerEl) this.composerEl.hidden = this.state === "ended" || this.offline;
|
|
1264
1548
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1265
1549
|
const text = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1266
1550
|
if (this.statusEl) this.statusEl.textContent = text;
|