@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.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,6 +176,135 @@ 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)}`;
|
|
@@ -181,13 +327,95 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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 @@ var OyehelloChat = (function (exports) {
|
|
|
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: text2, attachments: freshAttachments, fetchImpl });
|
|
@@ -415,18 +662,29 @@ var OyehelloChat = (function (exports) {
|
|
|
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";
|
|
@@ -3397,6 +3655,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3397
3655
|
--oc-glass-border: rgba(9,13,20,0.12);
|
|
3398
3656
|
|
|
3399
3657
|
position: fixed; right: 24px; bottom: 24px; z-index: 2147483000;
|
|
3658
|
+
/* Explicit display/visibility so a host-page reset or consent/privacy tool that blanket-hides
|
|
3659
|
+
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
3660
|
+
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
3661
|
+
display: block; visibility: visible; opacity: 1;
|
|
3400
3662
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
3401
3663
|
-webkit-font-smoothing: antialiased;
|
|
3402
3664
|
}
|
|
@@ -3436,7 +3698,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3436
3698
|
.launcher:hover { transform: scale(1.06); }
|
|
3437
3699
|
.launcher:active { transform: scale(0.93); }
|
|
3438
3700
|
.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; }
|
|
3701
|
+
.oc.show-ring:not(.open) .ring { animation: w-ring 2.4s ease-out 1.1s 3; }
|
|
3440
3702
|
.ic-chat, .ic-close { position: absolute; display: flex; align-items: center; justify-content: center; transition: all .3s var(--oc-bounce); }
|
|
3441
3703
|
.ic-chat { width: 28px; height: 28px; opacity: 1; transform: rotate(0) scale(1); }
|
|
3442
3704
|
.ic-close { opacity: 0; transform: rotate(90deg) scale(0.6); }
|
|
@@ -3458,7 +3720,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3458
3720
|
transition: opacity .3s var(--oc-ease), transform .38s var(--oc-bounce);
|
|
3459
3721
|
}
|
|
3460
3722
|
.oc.has-teaser .teaser { display: flex; }
|
|
3461
|
-
.oc.teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3723
|
+
.oc.show-teaser:not(.open) .teaser { opacity: 1; transform: none; pointer-events: auto; }
|
|
3462
3724
|
.teaser-btn {
|
|
3463
3725
|
max-width: 250px; display: flex; align-items: center; gap: 10px; padding: 11px 15px;
|
|
3464
3726
|
border-radius: 16px 16px 5px 16px; border: 1px solid var(--oc-glass-border); background: var(--oc-bg);
|
|
@@ -3555,6 +3817,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3555
3817
|
|
|
3556
3818
|
/* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
3557
3819
|
.composer { flex: none; display: flex; flex-direction: column; gap: 9px; padding: 12px 14px 13px; background: var(--oc-bg); }
|
|
3820
|
+
.composer[hidden] { display: none; }
|
|
3558
3821
|
.field { display: flex; flex-direction: column; gap: 11px; padding: 13px 14px 11px; border-radius: 18px; border: 1px solid var(--oc-field-border); background: var(--oc-field-bg); transition: border-color .18s var(--oc-ease); }
|
|
3559
3822
|
.oc.focus .field { border-color: var(--oc-ac); }
|
|
3560
3823
|
.input { width: 100%; border: none; background: transparent; color: var(--oc-fg); font-size: 13.5px; line-height: 1.4; outline: none; padding: 0; resize: none; max-height: 108px; font-family: inherit; }
|
|
@@ -3795,10 +4058,14 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3795
4058
|
// widget-config resolved (so we know online/offline before starting)
|
|
3796
4059
|
idleTimer = null;
|
|
3797
4060
|
idleReset = null;
|
|
4061
|
+
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
4062
|
+
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
4063
|
+
uninstallSpaHooks = null;
|
|
3798
4064
|
// ── shadow refs ──
|
|
3799
4065
|
oc;
|
|
3800
4066
|
bodyEl;
|
|
3801
4067
|
textareaEl;
|
|
4068
|
+
composerEl;
|
|
3802
4069
|
fileEl;
|
|
3803
4070
|
titleEl;
|
|
3804
4071
|
statusEl;
|
|
@@ -3820,7 +4087,16 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3820
4087
|
this.pendingTools = {};
|
|
3821
4088
|
}
|
|
3822
4089
|
this.off = this.client.on((e) => this.onClientEvent(e));
|
|
4090
|
+
recordView(agentId, location.pathname);
|
|
4091
|
+
try {
|
|
4092
|
+
this.uninstallSpaHooks = installSpaHooks(() => this.client?.recordPageView(location.pathname));
|
|
4093
|
+
} catch {
|
|
4094
|
+
this.uninstallSpaHooks = null;
|
|
4095
|
+
}
|
|
3823
4096
|
this.renderShell();
|
|
4097
|
+
this.style.setProperty("display", "block", "important");
|
|
4098
|
+
this.style.setProperty("visibility", "visible", "important");
|
|
4099
|
+
this.style.setProperty("opacity", "1", "important");
|
|
3824
4100
|
this.renderMessages();
|
|
3825
4101
|
this.updateHeader();
|
|
3826
4102
|
this.updateStatus();
|
|
@@ -3837,7 +4113,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3837
4113
|
}
|
|
3838
4114
|
this.updateClasses(true);
|
|
3839
4115
|
this.timers.push(setTimeout(() => {
|
|
3840
|
-
this.oc?.classList.remove("ring");
|
|
4116
|
+
this.oc?.classList.remove("show-ring");
|
|
3841
4117
|
}, 6e3));
|
|
3842
4118
|
this.armIdleTeaser();
|
|
3843
4119
|
}
|
|
@@ -3924,8 +4200,10 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
3924
4200
|
}
|
|
3925
4201
|
disconnectedCallback() {
|
|
3926
4202
|
this.off?.();
|
|
3927
|
-
|
|
4203
|
+
this.client?.detach();
|
|
3928
4204
|
this.teardownIdle();
|
|
4205
|
+
this.uninstallSpaHooks?.();
|
|
4206
|
+
this.uninstallSpaHooks = null;
|
|
3929
4207
|
for (const t of this.timers) clearTimeout(t);
|
|
3930
4208
|
for (const url of this.imagePreviewCache.values()) {
|
|
3931
4209
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -4003,6 +4281,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4003
4281
|
this.oc = root.querySelector(".oc");
|
|
4004
4282
|
this.bodyEl = root.querySelector(".body");
|
|
4005
4283
|
this.textareaEl = root.querySelector("textarea.input");
|
|
4284
|
+
this.composerEl = root.querySelector(".composer");
|
|
4006
4285
|
this.fileEl = root.querySelector('input[type="file"]');
|
|
4007
4286
|
this.titleEl = root.querySelector(".hd-title");
|
|
4008
4287
|
this.statusEl = root.querySelector(".hd-status .t");
|
|
@@ -4061,18 +4340,22 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4061
4340
|
this.shadowRoot.innerHTML = `<div style="position:fixed;right:16px;bottom:16px;color:#b91c1c;font:12px system-ui;background:#fef2f2;border:1px solid #fecaca;padding:8px 12px;border-radius:8px">${escapeHtml(msg)}</div>`;
|
|
4062
4341
|
}
|
|
4063
4342
|
// ── State-class sync ──────────────────────────────────────────────────────────────────────────
|
|
4343
|
+
// State classes on `.oc` MUST NOT share a name with any element class that has a bare base rule
|
|
4344
|
+
// (e.g. `.teaser`, `.ring`): the bare selector would also match `.oc.<state>` and, tying `.oc`'s
|
|
4345
|
+
// own rule on specificity, win on source order — e.g. `.teaser{display:none}` would hide the whole
|
|
4346
|
+
// widget. Hence the `show-` prefix on the teaser/ring state classes.
|
|
4064
4347
|
updateClasses(initialRing = false) {
|
|
4065
4348
|
if (!this.oc) return;
|
|
4066
4349
|
const c = this.oc.classList;
|
|
4067
4350
|
c.toggle("open", this.uiOpen);
|
|
4068
4351
|
c.toggle("max", this.uiMax);
|
|
4069
4352
|
c.toggle("emoji", this.emojiOpen);
|
|
4070
|
-
c.toggle("teaser", this.teaserActive);
|
|
4353
|
+
c.toggle("show-teaser", this.teaserActive);
|
|
4071
4354
|
c.toggle("away", this.away);
|
|
4072
4355
|
c.toggle("focus", this.focused);
|
|
4073
4356
|
c.toggle("ready", (this.textareaEl?.value.trim().length ?? 0) > 0);
|
|
4074
4357
|
c.toggle("unread", this.unseen > 0);
|
|
4075
|
-
if (initialRing) c.add("ring");
|
|
4358
|
+
if (initialRing) c.add("show-ring");
|
|
4076
4359
|
const badge = this.oc.querySelector(".badge");
|
|
4077
4360
|
if (badge) badge.textContent = this.unseen > 9 ? "9+" : String(this.unseen);
|
|
4078
4361
|
const launcher = this.oc.querySelector(".launcher");
|
|
@@ -4141,6 +4424,7 @@ Please report this to https://github.com/markedjs/marked.`, e) {
|
|
|
4141
4424
|
if (!this.textareaEl) return;
|
|
4142
4425
|
const disabled = this.state !== "ready" || this.offline;
|
|
4143
4426
|
this.textareaEl.disabled = disabled;
|
|
4427
|
+
if (this.composerEl) this.composerEl.hidden = this.state === "ended" || this.offline;
|
|
4144
4428
|
const handoff = HANDOFF_STATUS[this.handoffState];
|
|
4145
4429
|
const text2 = this.offline ? "Offline" : this.state === "opening" ? "Connecting\u2026" : handoff ?? (this.getAttribute("status") ?? DEFAULT_STATUS);
|
|
4146
4430
|
if (this.statusEl) this.statusEl.textContent = text2;
|