@oyehello/chat 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -9
- package/dist/index.cjs +330 -22
- 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 +330 -22
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +330 -22
- 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,9 +176,161 @@ 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)}`;
|
|
311
|
+
var isSafeRelativePath = (p) => p.startsWith("/") && !p.startsWith("//");
|
|
312
|
+
var isSafeHttpUrl = (u) => {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = new URL(u, window.location.origin);
|
|
315
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
316
|
+
} catch {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
var BUILTIN_TOOL_HANDLERS = {
|
|
321
|
+
navigate: (args) => {
|
|
322
|
+
const path = String(args.path ?? "");
|
|
323
|
+
if (!isSafeRelativePath(path)) return { ok: false, error: "unsafe navigate path" };
|
|
324
|
+
window.location.assign(path);
|
|
325
|
+
return { ok: true };
|
|
326
|
+
},
|
|
327
|
+
openUrl: (args) => {
|
|
328
|
+
const url = String(args.url ?? "");
|
|
329
|
+
if (!isSafeHttpUrl(url)) return { ok: false, error: "unsafe url" };
|
|
330
|
+
window.open(url, args.newTab === false ? "_self" : "_blank", "noopener,noreferrer");
|
|
331
|
+
return { ok: true };
|
|
332
|
+
}
|
|
333
|
+
};
|
|
165
334
|
function createChatClient(options) {
|
|
166
335
|
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
167
336
|
const ES = options.eventSource ?? globalThis.EventSource;
|
|
@@ -181,13 +350,95 @@ function createChatClient(options) {
|
|
|
181
350
|
let closed = false;
|
|
182
351
|
let warnedProtocol = false;
|
|
183
352
|
let deviceFingerprint;
|
|
353
|
+
let refreshTimer = null;
|
|
184
354
|
const toolHandlers = /* @__PURE__ */ new Map();
|
|
355
|
+
let journeyFlushChain = Promise.resolve();
|
|
356
|
+
let journeyFlushQueued = false;
|
|
357
|
+
const flushJourney = () => {
|
|
358
|
+
if (!session) return;
|
|
359
|
+
if (journeyFlushQueued) return;
|
|
360
|
+
journeyFlushQueued = true;
|
|
361
|
+
journeyFlushChain = journeyFlushChain.then(async () => {
|
|
362
|
+
journeyFlushQueued = false;
|
|
363
|
+
if (!session) return;
|
|
364
|
+
const pending = unsentViews(options.agentId);
|
|
365
|
+
if (pending.length === 0) return;
|
|
366
|
+
const s = session;
|
|
367
|
+
try {
|
|
368
|
+
await postPageViews({ baseUrl: options.baseUrl, sessionId: s.sessionId, sessionToken: s.sessionToken, views: pending, fetchImpl });
|
|
369
|
+
markFlushed(options.agentId, pending.length);
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
};
|
|
185
374
|
const clearReconnect = () => {
|
|
186
375
|
if (reconnectTimer) {
|
|
187
376
|
clearTimeout(reconnectTimer);
|
|
188
377
|
reconnectTimer = null;
|
|
189
378
|
}
|
|
190
379
|
};
|
|
380
|
+
const clearRefresh = () => {
|
|
381
|
+
if (refreshTimer) {
|
|
382
|
+
clearTimeout(refreshTimer);
|
|
383
|
+
refreshTimer = null;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const scheduleRefresh = () => {
|
|
387
|
+
clearRefresh();
|
|
388
|
+
if (closed || !session) return;
|
|
389
|
+
const untilExpiry = Date.parse(session.expiresAt) - Date.now();
|
|
390
|
+
const delay = Number.isNaN(untilExpiry) ? 10 * 6e4 : Math.max(5e3, untilExpiry - 6e4);
|
|
391
|
+
refreshTimer = setTimeout(async () => {
|
|
392
|
+
if (closed || !session) return;
|
|
393
|
+
try {
|
|
394
|
+
const r = await refreshSessionToken({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
395
|
+
session = { ...session, sessionToken: r.sessionToken, expiresAt: r.expiresAt };
|
|
396
|
+
persistSession(session);
|
|
397
|
+
scheduleRefresh();
|
|
398
|
+
} catch (err) {
|
|
399
|
+
const code = err?.code;
|
|
400
|
+
if (code === "session_ended" || code === "session_expired") clearStoredSession();
|
|
401
|
+
}
|
|
402
|
+
}, delay);
|
|
403
|
+
refreshTimer.unref?.();
|
|
404
|
+
};
|
|
405
|
+
const sessKey = `oyehello-chat-sess:${options.agentId}`;
|
|
406
|
+
const store2 = (() => {
|
|
407
|
+
try {
|
|
408
|
+
return globalThis.sessionStorage ?? null;
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
})();
|
|
413
|
+
const loadStoredSession = () => {
|
|
414
|
+
if (!store2) return null;
|
|
415
|
+
try {
|
|
416
|
+
const raw = store2.getItem(sessKey);
|
|
417
|
+
if (!raw) return null;
|
|
418
|
+
const s = JSON.parse(raw);
|
|
419
|
+
if (!s?.sessionId || !s?.sessionToken || !s?.expiresAt || !s?.agent) return null;
|
|
420
|
+
const exp = Date.parse(s.expiresAt);
|
|
421
|
+
if (Number.isNaN(exp) || Date.now() >= exp - 5e3) {
|
|
422
|
+
store2.removeItem(sessKey);
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
return s;
|
|
426
|
+
} catch {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
const persistSession = (s) => {
|
|
431
|
+
try {
|
|
432
|
+
store2?.setItem(sessKey, JSON.stringify({ sessionId: s.sessionId, sessionToken: s.sessionToken, expiresAt: s.expiresAt, agent: s.agent }));
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const clearStoredSession = () => {
|
|
437
|
+
try {
|
|
438
|
+
store2?.removeItem(sessKey);
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
};
|
|
191
442
|
const emit = (e) => {
|
|
192
443
|
for (const l of listeners) l(e);
|
|
193
444
|
};
|
|
@@ -238,7 +489,7 @@ function createChatClient(options) {
|
|
|
238
489
|
} else if (ev.type === "tool_call") {
|
|
239
490
|
if (seenCallIds.has(ev.callId)) return;
|
|
240
491
|
seenCallIds.add(ev.callId);
|
|
241
|
-
const handler = toolHandlers.get(ev.name);
|
|
492
|
+
const handler = toolHandlers.get(ev.name) ?? BUILTIN_TOOL_HANDLERS[ev.name];
|
|
242
493
|
void (async () => {
|
|
243
494
|
if (!session) return;
|
|
244
495
|
const base = { baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, callId: ev.callId, fetchImpl };
|
|
@@ -278,8 +529,10 @@ function createChatClient(options) {
|
|
|
278
529
|
} else if (ev.type === "done") {
|
|
279
530
|
closed = true;
|
|
280
531
|
clearReconnect();
|
|
532
|
+
clearRefresh();
|
|
281
533
|
es?.close();
|
|
282
534
|
es = null;
|
|
535
|
+
clearStoredSession();
|
|
283
536
|
setState("ended");
|
|
284
537
|
}
|
|
285
538
|
};
|
|
@@ -312,6 +565,7 @@ function createChatClient(options) {
|
|
|
312
565
|
reconnectTimer = setTimeout(() => {
|
|
313
566
|
openStream().catch((err) => {
|
|
314
567
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
568
|
+
clearStoredSession();
|
|
315
569
|
open().catch((e) => {
|
|
316
570
|
emit({ type: "error", error: e });
|
|
317
571
|
scheduleReconnect();
|
|
@@ -323,8 +577,19 @@ function createChatClient(options) {
|
|
|
323
577
|
}, delay);
|
|
324
578
|
};
|
|
325
579
|
const open = async () => {
|
|
580
|
+
const resumed = loadStoredSession();
|
|
581
|
+
if (resumed) {
|
|
582
|
+
session = resumed;
|
|
583
|
+
emit({ type: "agent", agent: resumed.agent });
|
|
584
|
+
await openStream();
|
|
585
|
+
scheduleRefresh();
|
|
586
|
+
flushJourney();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
326
589
|
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
327
|
-
|
|
590
|
+
const advertisedTools = /* @__PURE__ */ new Set([...toolHandlers.keys(), ...Object.keys(BUILTIN_TOOL_HANDLERS)]);
|
|
591
|
+
session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(advertisedTools), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
|
|
592
|
+
persistSession(session);
|
|
328
593
|
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
329
594
|
warnedProtocol = true;
|
|
330
595
|
try {
|
|
@@ -334,13 +599,11 @@ function createChatClient(options) {
|
|
|
334
599
|
}
|
|
335
600
|
emit({ type: "agent", agent: session.agent });
|
|
336
601
|
await openStream();
|
|
602
|
+
scheduleRefresh();
|
|
603
|
+
flushJourney();
|
|
337
604
|
};
|
|
338
605
|
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
|
-
}));
|
|
606
|
+
return Promise.all(files.map((file) => uploadAttachment({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, file, fetchImpl })));
|
|
344
607
|
};
|
|
345
608
|
return {
|
|
346
609
|
getMessages: () => messages.slice(),
|
|
@@ -359,6 +622,13 @@ function createChatClient(options) {
|
|
|
359
622
|
getWidgetConfig() {
|
|
360
623
|
return getWidgetConfig({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, fetchImpl });
|
|
361
624
|
},
|
|
625
|
+
recordPageView(path) {
|
|
626
|
+
try {
|
|
627
|
+
recordView(options.agentId, path);
|
|
628
|
+
flushJourney();
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
},
|
|
362
632
|
async start() {
|
|
363
633
|
if (state === "opening" || state === "ready") return;
|
|
364
634
|
closed = false;
|
|
@@ -404,6 +674,7 @@ function createChatClient(options) {
|
|
|
404
674
|
} catch (err) {
|
|
405
675
|
if (err instanceof ChatApiError && err.code === "session_expired") {
|
|
406
676
|
try {
|
|
677
|
+
clearStoredSession();
|
|
407
678
|
await open();
|
|
408
679
|
const freshAttachments = fileList.length ? await uploadFiles(fileList) : [];
|
|
409
680
|
await postMessage({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, text, attachments: freshAttachments, fetchImpl });
|
|
@@ -415,18 +686,29 @@ function createChatClient(options) {
|
|
|
415
686
|
}
|
|
416
687
|
}
|
|
417
688
|
},
|
|
689
|
+
detach() {
|
|
690
|
+
closed = true;
|
|
691
|
+
clearReconnect();
|
|
692
|
+
clearRefresh();
|
|
693
|
+
es?.close();
|
|
694
|
+
es = null;
|
|
695
|
+
},
|
|
418
696
|
async end() {
|
|
419
697
|
closed = true;
|
|
420
698
|
clearReconnect();
|
|
699
|
+
clearRefresh();
|
|
700
|
+
clearStoredSession();
|
|
421
701
|
if (session) await endSession({ baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, fetchImpl });
|
|
422
702
|
es?.close();
|
|
423
703
|
es = null;
|
|
424
704
|
setState("ended");
|
|
425
705
|
},
|
|
426
706
|
reset() {
|
|
707
|
+
clearStoredSession();
|
|
427
708
|
es?.close();
|
|
428
709
|
es = null;
|
|
429
710
|
clearReconnect();
|
|
711
|
+
clearRefresh();
|
|
430
712
|
session = null;
|
|
431
713
|
streamingId = null;
|
|
432
714
|
handoffState = "ai";
|
|
@@ -517,6 +799,10 @@ var STYLES = `
|
|
|
517
799
|
--oc-glass-border: rgba(9,13,20,0.12);
|
|
518
800
|
|
|
519
801
|
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
802
|
+
/* Explicit display/visibility so a host-page reset or consent/privacy tool that blanket-hides
|
|
803
|
+
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
804
|
+
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
805
|
+
display: block; visibility: visible; opacity: 1;
|
|
520
806
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
521
807
|
-webkit-font-smoothing: antialiased;
|
|
522
808
|
}
|
|
@@ -556,7 +842,7 @@ var STYLES = `
|
|
|
556
842
|
.launcher:hover { transform: scale(1.06); }
|
|
557
843
|
.launcher:active { transform: scale(0.93); }
|
|
558
844
|
.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; }
|
|
845
|
+
.oc.show-ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
560
846
|
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
561
847
|
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
562
848
|
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
@@ -578,7 +864,7 @@ var STYLES = `
|
|
|
578
864
|
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
579
865
|
}
|
|
580
866
|
.oc.has-teaser .teaser { display: flex; }
|
|
581
|
-
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
867
|
+
.oc.show-teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
582
868
|
.teaser-btn {
|
|
583
869
|
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
584
870
|
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
@@ -675,6 +961,7 @@ var STYLES = `
|
|
|
675
961
|
|
|
676
962
|
/* \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
963
|
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
964
|
+
.composer[hidden] { display: none; }
|
|
678
965
|
.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
966
|
.oc.focus .field { border-color: var(--oc-ac); }
|
|
680
967
|
.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 +1202,14 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
915
1202
|
// widget-config resolved (so we know online/offline before starting)
|
|
916
1203
|
idleTimer = null;
|
|
917
1204
|
idleReset = null;
|
|
1205
|
+
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
1206
|
+
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
1207
|
+
uninstallSpaHooks = null;
|
|
918
1208
|
// ── shadow refs ──
|
|
919
1209
|
oc;
|
|
920
1210
|
bodyEl;
|
|
921
1211
|
textareaEl;
|
|
1212
|
+
composerEl;
|
|
922
1213
|
fileEl;
|
|
923
1214
|
titleEl;
|
|
924
1215
|
statusEl;
|
|
@@ -940,7 +1231,16 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
940
1231
|
this.pendingTools = {};
|
|
941
1232
|
}
|
|
942
1233
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
1234
|
+
recordView(agentId, location.pathname);
|
|
1235
|
+
try {
|
|
1236
|
+
this.uninstallSpaHooks = installSpaHooks(() => this.client?.recordPageView(location.pathname));
|
|
1237
|
+
} catch {
|
|
1238
|
+
this.uninstallSpaHooks = null;
|
|
1239
|
+
}
|
|
943
1240
|
this.renderShell();
|
|
1241
|
+
this.style.setProperty("display", "block", "important");
|
|
1242
|
+
this.style.setProperty("visibility", "visible", "important");
|
|
1243
|
+
this.style.setProperty("opacity", "1", "important");
|
|
944
1244
|
this.renderMessages();
|
|
945
1245
|
this.updateHeader();
|
|
946
1246
|
this.updateStatus();
|
|
@@ -957,7 +1257,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
957
1257
|
}
|
|
958
1258
|
this.updateClasses(true);
|
|
959
1259
|
this.timers.push(setTimeout(() => {
|
|
960
|
-
this.oc?.classList.remove("ring");
|
|
1260
|
+
this.oc?.classList.remove("show-ring");
|
|
961
1261
|
}, 6e3));
|
|
962
1262
|
this.armIdleTeaser();
|
|
963
1263
|
}
|
|
@@ -1044,8 +1344,10 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1044
1344
|
}
|
|
1045
1345
|
disconnectedCallback() {
|
|
1046
1346
|
this.off?.();
|
|
1047
|
-
|
|
1347
|
+
this.client?.detach();
|
|
1048
1348
|
this.teardownIdle();
|
|
1349
|
+
this.uninstallSpaHooks?.();
|
|
1350
|
+
this.uninstallSpaHooks = null;
|
|
1049
1351
|
for (const t of this.timers) clearTimeout(t);
|
|
1050
1352
|
for (const url of this.imagePreviewCache.values()) {
|
|
1051
1353
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -1123,6 +1425,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1123
1425
|
this.oc = root.querySelector(".oc");
|
|
1124
1426
|
this.bodyEl = root.querySelector(".body");
|
|
1125
1427
|
this.textareaEl = root.querySelector("textarea.input");
|
|
1428
|
+
this.composerEl = root.querySelector(".composer");
|
|
1126
1429
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
1127
1430
|
this.titleEl = root.querySelector(".hd-title");
|
|
1128
1431
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
@@ -1181,18 +1484,22 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1181
1484
|
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
1485
|
}
|
|
1183
1486
|
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
1487
|
+
// State classes on `.oc` MUST NOT share a name with any element class that has a bare base rule
|
|
1488
|
+
// (e.g. `.teaser`, `.ring`): the bare selector would also match `.oc.<state>` and, tying `.oc`'s
|
|
1489
|
+
// own rule on specificity, win on source order — e.g. `.teaser{display:none}` would hide the whole
|
|
1490
|
+
// widget. Hence the `show-` prefix on the teaser/ring state classes.
|
|
1184
1491
|
updateClasses(initialRing = false) {
|
|
1185
1492
|
if (!this.oc) return;
|
|
1186
1493
|
const c = this.oc.classList;
|
|
1187
1494
|
c.toggle("open", this.uiOpen);
|
|
1188
1495
|
c.toggle("max", this.uiMax);
|
|
1189
1496
|
c.toggle("emoji", this.emojiOpen);
|
|
1190
|
-
c.toggle("teaser", this.teaserActive);
|
|
1497
|
+
c.toggle("show-teaser", this.teaserActive);
|
|
1191
1498
|
c.toggle("away", this.away);
|
|
1192
1499
|
c.toggle("focus", this.focused);
|
|
1193
1500
|
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
1194
1501
|
c.toggle("unread", this.unseen > 0);
|
|
1195
|
-
if (initialRing) c.add("ring");
|
|
1502
|
+
if (initialRing) c.add("show-ring");
|
|
1196
1503
|
const badge = this.oc.querySelector(".badge");
|
|
1197
1504
|
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
1198
1505
|
const launcher = this.oc.querySelector(".launcher");
|
|
@@ -1261,6 +1568,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1261
1568
|
if (!this.textareaEl) return;
|
|
1262
1569
|
const disabled = this.state !== "ready" || this.offline;
|
|
1263
1570
|
this.textareaEl.disabled = disabled;
|
|
1571
|
+
if (this.composerEl) this.composerEl.hidden = this.state === "ended" || this.offline;
|
|
1264
1572
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
1265
1573
|
const text = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
1266
1574
|
if (this.statusEl) this.statusEl.textContent = text;
|