@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.d.cts
CHANGED
|
@@ -140,6 +140,7 @@ interface ChatClient {
|
|
|
140
140
|
start(): Promise<void>;
|
|
141
141
|
sendMessage(text: string, files?: File[]): Promise<void>;
|
|
142
142
|
end(): Promise<void>;
|
|
143
|
+
detach(): void;
|
|
143
144
|
reset(): void;
|
|
144
145
|
getMessages(): ChatMessage[];
|
|
145
146
|
getState(): ChatClientState;
|
|
@@ -147,6 +148,7 @@ interface ChatClient {
|
|
|
147
148
|
registerTools(handlers: Record<string, ChatToolHandler>): void;
|
|
148
149
|
getAttachmentUrl(storageRef: string): Promise<string>;
|
|
149
150
|
getWidgetConfig(): Promise<WidgetConfig>;
|
|
151
|
+
recordPageView(path: string): void;
|
|
150
152
|
}
|
|
151
153
|
declare function createChatClient(options: ChatClientOptions): ChatClient;
|
|
152
154
|
|
|
@@ -189,9 +191,11 @@ declare class OyehelloChatElement extends HTMLElement {
|
|
|
189
191
|
private configLoaded;
|
|
190
192
|
private idleTimer;
|
|
191
193
|
private idleReset;
|
|
194
|
+
private uninstallSpaHooks;
|
|
192
195
|
private oc;
|
|
193
196
|
private bodyEl;
|
|
194
197
|
private textareaEl;
|
|
198
|
+
private composerEl;
|
|
195
199
|
private fileEl;
|
|
196
200
|
private titleEl;
|
|
197
201
|
private statusEl;
|
package/dist/index.d.ts
CHANGED
|
@@ -140,6 +140,7 @@ interface ChatClient {
|
|
|
140
140
|
start(): Promise<void>;
|
|
141
141
|
sendMessage(text: string, files?: File[]): Promise<void>;
|
|
142
142
|
end(): Promise<void>;
|
|
143
|
+
detach(): void;
|
|
143
144
|
reset(): void;
|
|
144
145
|
getMessages(): ChatMessage[];
|
|
145
146
|
getState(): ChatClientState;
|
|
@@ -147,6 +148,7 @@ interface ChatClient {
|
|
|
147
148
|
registerTools(handlers: Record<string, ChatToolHandler>): void;
|
|
148
149
|
getAttachmentUrl(storageRef: string): Promise<string>;
|
|
149
150
|
getWidgetConfig(): Promise<WidgetConfig>;
|
|
151
|
+
recordPageView(path: string): void;
|
|
150
152
|
}
|
|
151
153
|
declare function createChatClient(options: ChatClientOptions): ChatClient;
|
|
152
154
|
|
|
@@ -189,9 +191,11 @@ declare class OyehelloChatElement extends HTMLElement {
|
|
|
189
191
|
private configLoaded;
|
|
190
192
|
private idleTimer;
|
|
191
193
|
private idleReset;
|
|
194
|
+
private uninstallSpaHooks;
|
|
192
195
|
private oc;
|
|
193
196
|
private bodyEl;
|
|
194
197
|
private textareaEl;
|
|
198
|
+
private composerEl;
|
|
195
199
|
private fileEl;
|
|
196
200
|
private titleEl;
|
|
197
201
|
private statusEl;
|
package/dist/index.global.js
CHANGED
|
@@ -61,6 +61,15 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 j2 = await res.json();
|
|
122
|
+
return { storageRef: j2.storageRef, contentType: j2.contentType, name: j2.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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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((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
|
+
};
|
|
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 = (u3) => {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = new URL(u3, 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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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: text2, attachments: freshAttachments, fetchImpl });
|
|
@@ -415,18 +686,29 @@ var OyehelloChat = (function (exports) {
|
|
|
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";
|
|
@@ -3397,6 +3679,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3397
3679
|
--oc-glass-border: rgba(9,13,20,0.12);
|
|
3398
3680
|
|
|
3399
3681
|
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
3682
|
+
/* Explicit display/visibility so a host-page reset or consent/privacy tool that blanket-hides
|
|
3683
|
+
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
3684
|
+
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
3685
|
+
display: block; visibility: visible; opacity: 1;
|
|
3400
3686
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
3401
3687
|
-webkit-font-smoothing: antialiased;
|
|
3402
3688
|
}
|
|
@@ -3436,7 +3722,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3436
3722
|
.launcher:hover { transform: scale(1.06); }
|
|
3437
3723
|
.launcher:active { transform: scale(0.93); }
|
|
3438
3724
|
.ring { pointer-events: none; position: absolute; inset: -1px; border-radius: var(--oc-r-full); border: 2px solid var(--oc-ac); opacity: 0; }
|
|
3439
|
-
.oc.ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
3725
|
+
.oc.show-ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
3440
3726
|
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
3441
3727
|
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
3442
3728
|
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
@@ -3458,7 +3744,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3458
3744
|
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
3459
3745
|
}
|
|
3460
3746
|
.oc.has-teaser .teaser { display: flex; }
|
|
3461
|
-
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3747
|
+
.oc.show-teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3462
3748
|
.teaser-btn {
|
|
3463
3749
|
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
3464
3750
|
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
@@ -3555,6 +3841,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3555
3841
|
|
|
3556
3842
|
/* \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 */
|
|
3557
3843
|
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
3844
|
+
.composer[hidden] { display: none; }
|
|
3558
3845
|
.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); }
|
|
3559
3846
|
.oc.focus .field { border-color: var(--oc-ac); }
|
|
3560
3847
|
.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; }
|
|
@@ -3795,10 +4082,14 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3795
4082
|
// widget-config resolved (so we know online/offline before starting)
|
|
3796
4083
|
idleTimer = null;
|
|
3797
4084
|
idleReset = null;
|
|
4085
|
+
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
4086
|
+
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
4087
|
+
uninstallSpaHooks = null;
|
|
3798
4088
|
// ── shadow refs ──
|
|
3799
4089
|
oc;
|
|
3800
4090
|
bodyEl;
|
|
3801
4091
|
textareaEl;
|
|
4092
|
+
composerEl;
|
|
3802
4093
|
fileEl;
|
|
3803
4094
|
titleEl;
|
|
3804
4095
|
statusEl;
|
|
@@ -3820,7 +4111,16 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3820
4111
|
this.pendingTools = {};
|
|
3821
4112
|
}
|
|
3822
4113
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
4114
|
+
recordView(agentId, location.pathname);
|
|
4115
|
+
try {
|
|
4116
|
+
this.uninstallSpaHooks = installSpaHooks(() => this.client?.recordPageView(location.pathname));
|
|
4117
|
+
} catch {
|
|
4118
|
+
this.uninstallSpaHooks = null;
|
|
4119
|
+
}
|
|
3823
4120
|
this.renderShell();
|
|
4121
|
+
this.style.setProperty("display", "block", "important");
|
|
4122
|
+
this.style.setProperty("visibility", "visible", "important");
|
|
4123
|
+
this.style.setProperty("opacity", "1", "important");
|
|
3824
4124
|
this.renderMessages();
|
|
3825
4125
|
this.updateHeader();
|
|
3826
4126
|
this.updateStatus();
|
|
@@ -3837,7 +4137,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3837
4137
|
}
|
|
3838
4138
|
this.updateClasses(true);
|
|
3839
4139
|
this.timers.push(setTimeout(() => {
|
|
3840
|
-
this.oc?.classList.remove("ring");
|
|
4140
|
+
this.oc?.classList.remove("show-ring");
|
|
3841
4141
|
}, 6e3));
|
|
3842
4142
|
this.armIdleTeaser();
|
|
3843
4143
|
}
|
|
@@ -3924,8 +4224,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3924
4224
|
}
|
|
3925
4225
|
disconnectedCallback() {
|
|
3926
4226
|
this.off?.();
|
|
3927
|
-
|
|
4227
|
+
this.client?.detach();
|
|
3928
4228
|
this.teardownIdle();
|
|
4229
|
+
this.uninstallSpaHooks?.();
|
|
4230
|
+
this.uninstallSpaHooks = null;
|
|
3929
4231
|
for (const t of this.timers) clearTimeout(t);
|
|
3930
4232
|
for (const url of this.imagePreviewCache.values()) {
|
|
3931
4233
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -4003,6 +4305,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4003
4305
|
this.oc = root.querySelector(".oc");
|
|
4004
4306
|
this.bodyEl = root.querySelector(".body");
|
|
4005
4307
|
this.textareaEl = root.querySelector("textarea.input");
|
|
4308
|
+
this.composerEl = root.querySelector(".composer");
|
|
4006
4309
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
4007
4310
|
this.titleEl = root.querySelector(".hd-title");
|
|
4008
4311
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
@@ -4061,18 +4364,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4061
4364
|
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>`;
|
|
4062
4365
|
}
|
|
4063
4366
|
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
4367
|
+
// State classes on `.oc` MUST NOT share a name with any element class that has a bare base rule
|
|
4368
|
+
// (e.g. `.teaser`, `.ring`): the bare selector would also match `.oc.<state>` and, tying `.oc`'s
|
|
4369
|
+
// own rule on specificity, win on source order — e.g. `.teaser{display:none}` would hide the whole
|
|
4370
|
+
// widget. Hence the `show-` prefix on the teaser/ring state classes.
|
|
4064
4371
|
updateClasses(initialRing = false) {
|
|
4065
4372
|
if (!this.oc) return;
|
|
4066
4373
|
const c = this.oc.classList;
|
|
4067
4374
|
c.toggle("open", this.uiOpen);
|
|
4068
4375
|
c.toggle("max", this.uiMax);
|
|
4069
4376
|
c.toggle("emoji", this.emojiOpen);
|
|
4070
|
-
c.toggle("teaser", this.teaserActive);
|
|
4377
|
+
c.toggle("show-teaser", this.teaserActive);
|
|
4071
4378
|
c.toggle("away", this.away);
|
|
4072
4379
|
c.toggle("focus", this.focused);
|
|
4073
4380
|
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
4074
4381
|
c.toggle("unread", this.unseen > 0);
|
|
4075
|
-
if (initialRing) c.add("ring");
|
|
4382
|
+
if (initialRing) c.add("show-ring");
|
|
4076
4383
|
const badge = this.oc.querySelector(".badge");
|
|
4077
4384
|
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
4078
4385
|
const launcher = this.oc.querySelector(".launcher");
|
|
@@ -4141,6 +4448,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4141
4448
|
if (!this.textareaEl) return;
|
|
4142
4449
|
const disabled = this.state !== "ready" || this.offline;
|
|
4143
4450
|
this.textareaEl.disabled = disabled;
|
|
4451
|
+
if (this.composerEl) this.composerEl.hidden = this.state === "ended" || this.offline;
|
|
4144
4452
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
4145
4453
|
const text2 = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
4146
4454
|
if (this.statusEl) this.statusEl.textContent = text2;
|