@oyehello/chat 0.5.1 → 0.6.0
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/LICENSE +1 -1
- package/README.md +24 -12
- package/dist/index.cjs +139 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.global.js +139 -7
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +139 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -61,6 +61,16 @@ 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 getHistory(opts) {
|
|
65
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/messages`, {
|
|
66
|
+
method: "GET",
|
|
67
|
+
credentials: "omit",
|
|
68
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` }
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
71
|
+
const body = await res.json();
|
|
72
|
+
return body.messages;
|
|
73
|
+
}
|
|
64
74
|
async function postPageViews(opts) {
|
|
65
75
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/page-views`, {
|
|
66
76
|
method: "POST",
|
|
@@ -308,6 +318,33 @@ function installSpaHooks(onNav) {
|
|
|
308
318
|
// src/client.ts
|
|
309
319
|
var seq = 0;
|
|
310
320
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
321
|
+
var isSafeRelativePath = (p) => p.startsWith("/") && !p.startsWith("//");
|
|
322
|
+
var isSafeHttpUrl = (u) => {
|
|
323
|
+
try {
|
|
324
|
+
const parsed = new URL(u, window.location.origin);
|
|
325
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var AFTER_RESULT = "__ohAfterResult";
|
|
331
|
+
var BUILTIN_TOOL_HANDLERS = {
|
|
332
|
+
navigate: (args) => {
|
|
333
|
+
const path = String(args.path ?? "");
|
|
334
|
+
if (!isSafeRelativePath(path)) return { ok: false, error: "unsafe navigate path" };
|
|
335
|
+
const here = window.location.pathname + window.location.search + window.location.hash;
|
|
336
|
+
const target = new URL(path, window.location.href);
|
|
337
|
+
if (target.pathname + target.search + target.hash === here) return { ok: true, note: "already on this page" };
|
|
338
|
+
return { ok: true, [AFTER_RESULT]: () => window.location.assign(path) };
|
|
339
|
+
},
|
|
340
|
+
openUrl: (args) => {
|
|
341
|
+
const url = String(args.url ?? "");
|
|
342
|
+
if (!isSafeHttpUrl(url)) return { ok: false, error: "unsafe url" };
|
|
343
|
+
const sameHost = new URL(url, window.location.origin).hostname === window.location.hostname;
|
|
344
|
+
window.open(url, sameHost ? "_self" : "_blank", "noopener,noreferrer");
|
|
345
|
+
return { ok: true };
|
|
346
|
+
}
|
|
347
|
+
};
|
|
311
348
|
function createChatClient(options) {
|
|
312
349
|
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
313
350
|
const ES = options.eventSource ?? globalThis.EventSource;
|
|
@@ -416,6 +453,23 @@ function createChatClient(options) {
|
|
|
416
453
|
} catch {
|
|
417
454
|
}
|
|
418
455
|
};
|
|
456
|
+
const callsKey = `oyehello-chat-calls:${options.agentId}`;
|
|
457
|
+
const handledCallIds = () => {
|
|
458
|
+
try {
|
|
459
|
+
const raw = store2?.getItem(callsKey);
|
|
460
|
+
const v = raw ? JSON.parse(raw) : null;
|
|
461
|
+
return v && session && v.sessionId === session.sessionId && Array.isArray(v.ids) ? v.ids : [];
|
|
462
|
+
} catch {
|
|
463
|
+
return [];
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const rememberHandledCall = (callId) => {
|
|
467
|
+
if (!session) return;
|
|
468
|
+
try {
|
|
469
|
+
store2?.setItem(callsKey, JSON.stringify({ sessionId: session.sessionId, ids: [...handledCallIds(), callId].slice(-50) }));
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
};
|
|
419
473
|
const emit = (e) => {
|
|
420
474
|
for (const l of listeners) l(e);
|
|
421
475
|
};
|
|
@@ -464,9 +518,10 @@ function createChatClient(options) {
|
|
|
464
518
|
} else if (ev.type === "error") {
|
|
465
519
|
emit({ type: "error", error: new Error(ev.message) });
|
|
466
520
|
} else if (ev.type === "tool_call") {
|
|
467
|
-
if (seenCallIds.has(ev.callId)) return;
|
|
521
|
+
if (seenCallIds.has(ev.callId) || handledCallIds().includes(ev.callId)) return;
|
|
468
522
|
seenCallIds.add(ev.callId);
|
|
469
|
-
|
|
523
|
+
rememberHandledCall(ev.callId);
|
|
524
|
+
const handler = toolHandlers.get(ev.name) ?? BUILTIN_TOOL_HANDLERS[ev.name];
|
|
470
525
|
void (async () => {
|
|
471
526
|
if (!session) return;
|
|
472
527
|
const base = { baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, callId: ev.callId, fetchImpl };
|
|
@@ -475,8 +530,16 @@ function createChatClient(options) {
|
|
|
475
530
|
return;
|
|
476
531
|
}
|
|
477
532
|
try {
|
|
478
|
-
const
|
|
533
|
+
const raw = await handler(ev.args);
|
|
534
|
+
let after = null;
|
|
535
|
+
let out = raw;
|
|
536
|
+
if (raw && typeof raw === "object" && typeof raw[AFTER_RESULT] === "function") {
|
|
537
|
+
const { [AFTER_RESULT]: run, ...rest } = raw;
|
|
538
|
+
after = run;
|
|
539
|
+
out = rest;
|
|
540
|
+
}
|
|
479
541
|
await postToolResult({ ...base, ok: true, result: typeof out === "string" ? out : JSON.stringify(out ?? { ok: true }) });
|
|
542
|
+
after?.();
|
|
480
543
|
} catch (err) {
|
|
481
544
|
await postToolResult({ ...base, ok: false, result: err.message });
|
|
482
545
|
}
|
|
@@ -558,13 +621,24 @@ function createChatClient(options) {
|
|
|
558
621
|
if (resumed) {
|
|
559
622
|
session = resumed;
|
|
560
623
|
emit({ type: "agent", agent: resumed.agent });
|
|
624
|
+
try {
|
|
625
|
+
const history2 = await getHistory({ baseUrl: options.baseUrl, sessionId: resumed.sessionId, sessionToken: resumed.sessionToken, fetchImpl });
|
|
626
|
+
for (const h of history2) {
|
|
627
|
+
const turnId = h.role === "assistant" || h.role === "human" ? h.turnId : void 0;
|
|
628
|
+
messages.push({ id: uid("m"), role: h.role, text: h.text, streaming: false, turnId });
|
|
629
|
+
if (h.turnId) seenTurnIds.add(h.turnId);
|
|
630
|
+
}
|
|
631
|
+
emitMessages();
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
561
634
|
await openStream();
|
|
562
635
|
scheduleRefresh();
|
|
563
636
|
flushJourney();
|
|
564
637
|
return;
|
|
565
638
|
}
|
|
566
639
|
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
567
|
-
|
|
640
|
+
const advertisedTools = /* @__PURE__ */ new Set([...toolHandlers.keys(), ...Object.keys(BUILTIN_TOOL_HANDLERS)]);
|
|
641
|
+
session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(advertisedTools), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
|
|
568
642
|
persistSession(session);
|
|
569
643
|
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
570
644
|
warnedProtocol = true;
|
|
@@ -697,6 +771,44 @@ function createChatClient(options) {
|
|
|
697
771
|
}
|
|
698
772
|
};
|
|
699
773
|
}
|
|
774
|
+
|
|
775
|
+
// src/page-theme.ts
|
|
776
|
+
var WATCHED = ["class", "data-theme", "data-mode", "style"];
|
|
777
|
+
function themeOf(node) {
|
|
778
|
+
for (const attr of ["data-theme", "data-mode"]) {
|
|
779
|
+
const v = node.getAttribute(attr);
|
|
780
|
+
if (v === "dark" || v === "light") return v;
|
|
781
|
+
}
|
|
782
|
+
if (node.classList.contains("dark")) return "dark";
|
|
783
|
+
if (node.classList.contains("light")) return "light";
|
|
784
|
+
return null;
|
|
785
|
+
}
|
|
786
|
+
function resolvePageTheme(el) {
|
|
787
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
788
|
+
const t = themeOf(node);
|
|
789
|
+
if (t) return t;
|
|
790
|
+
}
|
|
791
|
+
const scheme = (document.documentElement.style.colorScheme || getComputedStyle(document.documentElement).colorScheme || "").trim();
|
|
792
|
+
return scheme === "dark" || scheme === "light" ? scheme : null;
|
|
793
|
+
}
|
|
794
|
+
function watchPageTheme(el, onChange) {
|
|
795
|
+
let last;
|
|
796
|
+
const check = () => {
|
|
797
|
+
const t = resolvePageTheme(el);
|
|
798
|
+
if (t !== last) {
|
|
799
|
+
last = t;
|
|
800
|
+
onChange(t);
|
|
801
|
+
}
|
|
802
|
+
};
|
|
803
|
+
check();
|
|
804
|
+
if (typeof MutationObserver === "undefined") return () => {
|
|
805
|
+
};
|
|
806
|
+
const mo = new MutationObserver(check);
|
|
807
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
808
|
+
mo.observe(node, { attributes: true, attributeFilter: WATCHED });
|
|
809
|
+
}
|
|
810
|
+
return () => mo.disconnect();
|
|
811
|
+
}
|
|
700
812
|
marked.setOptions({ gfm: true, breaks: true });
|
|
701
813
|
var hookInstalled = false;
|
|
702
814
|
function installLinkHook() {
|
|
@@ -779,11 +891,15 @@ var STYLES = `
|
|
|
779
891
|
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
780
892
|
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
781
893
|
display: block; visibility: visible; opacity: 1;
|
|
894
|
+
/* The closed panel still takes up its full size in the layout (it fades, it is not removed), so
|
|
895
|
+
* the host box covers ~384x620px of the page. The host and its wrapper must not catch clicks;
|
|
896
|
+
* only the visible parts opt back in (launcher, teaser while shown, panel while open). */
|
|
897
|
+
pointer-events: none;
|
|
782
898
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
783
899
|
-webkit-font-smoothing: antialiased;
|
|
784
900
|
}
|
|
785
901
|
@media (prefers-color-scheme: dark) {
|
|
786
|
-
:host(:not([theme="light"])) {
|
|
902
|
+
:host(:not([theme="light"]):not([data-oc-page-theme="light"])) {
|
|
787
903
|
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
788
904
|
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
789
905
|
--oc-border: rgba(255,255,255,0.09);
|
|
@@ -793,7 +909,9 @@ var STYLES = `
|
|
|
793
909
|
--oc-glass-border: rgba(255,255,255,0.12);
|
|
794
910
|
}
|
|
795
911
|
}
|
|
796
|
-
|
|
912
|
+
/* Explicit dark, or auto on a page that declares dark (data-oc-page-theme, set by page-theme.ts). The OS rule above
|
|
913
|
+
* only applies when the page declares nothing; a page that declares light keeps the light base. */
|
|
914
|
+
:host([theme="dark"]), :host(:not([theme="light"])[data-oc-page-theme="dark"]) {
|
|
797
915
|
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
798
916
|
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
799
917
|
--oc-border: rgba(255,255,255,0.09);
|
|
@@ -805,7 +923,7 @@ var STYLES = `
|
|
|
805
923
|
* { box-sizing: border-box; }
|
|
806
924
|
button { font-family: inherit; }
|
|
807
925
|
|
|
808
|
-
.oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
|
|
926
|
+
.oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; pointer-events: none; }
|
|
809
927
|
|
|
810
928
|
/* \u2500\u2500 Launcher \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 */
|
|
811
929
|
.launcher {
|
|
@@ -814,6 +932,7 @@ var STYLES = `
|
|
|
814
932
|
box-shadow: 0 8px 22px -8px color-mix(in oklab, var(--oc-ac) 70%, transparent), 0 18px 48px -18px rgba(0,0,0,0.6);
|
|
815
933
|
color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
816
934
|
transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
|
|
935
|
+
pointer-events: auto;
|
|
817
936
|
}
|
|
818
937
|
.launcher:hover { transform: scale(1.06); }
|
|
819
938
|
.launcher:active { transform: scale(0.93); }
|
|
@@ -1181,6 +1300,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1181
1300
|
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
1182
1301
|
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
1183
1302
|
uninstallSpaHooks = null;
|
|
1303
|
+
unwatchPageTheme = null;
|
|
1184
1304
|
// ── shadow refs ──
|
|
1185
1305
|
oc;
|
|
1186
1306
|
bodyEl;
|
|
@@ -1214,6 +1334,11 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1214
1334
|
this.uninstallSpaHooks = null;
|
|
1215
1335
|
}
|
|
1216
1336
|
this.renderShell();
|
|
1337
|
+
this.unwatchPageTheme?.();
|
|
1338
|
+
this.unwatchPageTheme = watchPageTheme(this, (t) => {
|
|
1339
|
+
if (t) this.setAttribute("data-oc-page-theme", t);
|
|
1340
|
+
else this.removeAttribute("data-oc-page-theme");
|
|
1341
|
+
});
|
|
1217
1342
|
this.style.setProperty("display", "block", "important");
|
|
1218
1343
|
this.style.setProperty("visibility", "visible", "important");
|
|
1219
1344
|
this.style.setProperty("opacity", "1", "important");
|
|
@@ -1324,6 +1449,8 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1324
1449
|
this.teardownIdle();
|
|
1325
1450
|
this.uninstallSpaHooks?.();
|
|
1326
1451
|
this.uninstallSpaHooks = null;
|
|
1452
|
+
this.unwatchPageTheme?.();
|
|
1453
|
+
this.unwatchPageTheme = null;
|
|
1327
1454
|
for (const t of this.timers) clearTimeout(t);
|
|
1328
1455
|
for (const url of this.imagePreviewCache.values()) {
|
|
1329
1456
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -1334,6 +1461,11 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1334
1461
|
if (this.client) this.client.registerTools(handlers);
|
|
1335
1462
|
else Object.assign(this.pendingTools, handlers);
|
|
1336
1463
|
}
|
|
1464
|
+
/** Open the chat window, exactly as a launcher click does. No-op if already open or not rendered. */
|
|
1465
|
+
open() {
|
|
1466
|
+
if (!this.oc || this.uiOpen) return;
|
|
1467
|
+
this.openPanel();
|
|
1468
|
+
}
|
|
1337
1469
|
key(k) {
|
|
1338
1470
|
return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
|
|
1339
1471
|
}
|