@oyehello/chat 0.5.2 → 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 +7 -5
- package/dist/index.cjs +116 -8
- 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 +116 -8
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +116 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -56,4 +56,4 @@ otherwise using the Software, You agree to be bound by this Agreement.
|
|
|
56
56
|
subject matter. If any provision is held unenforceable, the remaining
|
|
57
57
|
provisions remain in full force.
|
|
58
58
|
|
|
59
|
-
For licensing enquiries, contact: legal@
|
|
59
|
+
For licensing enquiries, contact: legal@oyehello.ai
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Framework-agnostic embeddable chat for oyehello agents — a `<oyehello-chat>` web component that renders a **floating chat launcher** (bottom-right; click to open a chat window), plus a headless `createChatClient()` for custom UIs. Zero framework dependencies.
|
|
4
4
|
|
|
5
|
-
The widget connects lazily (a session opens the first time the visitor opens the panel), renders assistant replies as **markdown** (sanitized), and has a full launcher → panel experience: an unread badge, expand/maximize, an emoji picker, file attach, a jump-to-latest control, smart auto-scroll, an optional teaser nudge, quick-reply suggestions, and a privacy footer.
|
|
5
|
+
The widget connects lazily (a session opens the first time the visitor opens the panel), renders assistant replies as **markdown** (sanitized), and has a full launcher → panel experience: an unread badge, expand/maximize, an emoji picker, file attach, a jump-to-latest control, smart auto-scroll, an optional teaser nudge, quick-reply suggestions, and a privacy footer. By default it follows your page's own light/dark theme (live, as your theme toggle changes it), and the visitor's OS theme when your page doesn't declare one. Theme the accent with the `--oc-accent` CSS variable. Just drop the tag anywhere on the page; it positions itself as a floating launcher.
|
|
6
6
|
|
|
7
7
|
### Attributes
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ The **accent/logo/name** (workspace brand), the **teaser** (a chat-agent setting
|
|
|
14
14
|
| `agent-id` | yes | The chat agent (`agt_…`) |
|
|
15
15
|
| `base-url` | yes | API origin, e.g. `https://api.oyehello.ai` |
|
|
16
16
|
| `title` | no | Header title (defaults to the agent's name) |
|
|
17
|
-
| `theme` | no | `auto` (default
|
|
17
|
+
| `theme` | no | `auto` (default: follows your page's theme - a `data-theme` or `data-mode` of `dark`/`light`, a `dark`/`light` class on `<html>`, `<body>` or any parent of the widget, or `color-scheme` - and the visitor's OS when the page declares none), `light`, or `dark` |
|
|
18
18
|
| `teaser` | no | Text for a one-time nudge bubble; omit to disable |
|
|
19
19
|
| `suggestions` | no | Comma-separated quick-reply chips, e.g. `"Book a demo, Pricing"` |
|
|
20
20
|
| `privacy-url` | no | Shows a Privacy-Policy footer linking here |
|
|
@@ -36,7 +36,7 @@ npm install @oyehello/chat
|
|
|
36
36
|
```
|
|
37
37
|
Or via a plain script tag (no bundler):
|
|
38
38
|
```html
|
|
39
|
-
<script src="https://unpkg.com/@oyehello/chat@0.
|
|
39
|
+
<script src="https://unpkg.com/@oyehello/chat@0.6.0"></script>
|
|
40
40
|
<oyehello-chat api-key="ohk_pub_…" agent-id="agt_…" base-url="https://api.oyehello.ai"></oyehello-chat>
|
|
41
41
|
```
|
|
42
42
|
Pin a specific version (`@0.5.0` above) rather than the bare package name, so a customer's site loads the exact widget it was tested against instead of silently auto-upgrading on every publish.
|
|
@@ -83,8 +83,10 @@ target passes the agent's navigate allowlist (also set in the console), the SDK
|
|
|
83
83
|
<oyehello-chat api-key="ohk_pub_…" agent-id="agt_…" base-url="https://api.oyehello.ai"></oyehello-chat>
|
|
84
84
|
```
|
|
85
85
|
That's it — a plain embed with no JS wiring can navigate the visitor or open a link when the agent calls
|
|
86
|
-
the tool. `navigate` calls `window.location.assign(path)
|
|
87
|
-
|
|
86
|
+
the tool. `navigate` calls `window.location.assign(path)` (in-app path). `openUrl` opens the URL — which
|
|
87
|
+
is restricted server-side to the agent's allowlisted base domain(s) and their subdomains — choosing the
|
|
88
|
+
tab itself: the exact same host as the embedding page opens in the same tab, any allowed subdomain opens
|
|
89
|
+
in a new tab (`window.open(url, …, 'noopener,noreferrer')`). The agent has no say over the tab target.
|
|
88
90
|
|
|
89
91
|
A host page MAY still register its own `navigate`/`openUrl` handler to override the default behavior
|
|
90
92
|
(e.g. push through a client-side router instead of a full navigation):
|
package/dist/index.cjs
CHANGED
|
@@ -67,6 +67,16 @@ async function postMessage(opts) {
|
|
|
67
67
|
});
|
|
68
68
|
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
69
69
|
}
|
|
70
|
+
async function getHistory(opts) {
|
|
71
|
+
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/messages`, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
credentials: "omit",
|
|
74
|
+
headers: { Authorization: `Bearer ${opts.sessionToken}` }
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok) throw await asError(res, res.status === 401 ? "session_expired" : void 0);
|
|
77
|
+
const body = await res.json();
|
|
78
|
+
return body.messages;
|
|
79
|
+
}
|
|
70
80
|
async function postPageViews(opts) {
|
|
71
81
|
const res = await opts.fetchImpl(`${opts.baseUrl}/chat/sessions/${opts.sessionId}/page-views`, {
|
|
72
82
|
method: "POST",
|
|
@@ -323,17 +333,21 @@ var isSafeHttpUrl = (u) => {
|
|
|
323
333
|
return false;
|
|
324
334
|
}
|
|
325
335
|
};
|
|
336
|
+
var AFTER_RESULT = "__ohAfterResult";
|
|
326
337
|
var BUILTIN_TOOL_HANDLERS = {
|
|
327
338
|
navigate: (args) => {
|
|
328
339
|
const path = String(args.path ?? "");
|
|
329
340
|
if (!isSafeRelativePath(path)) return { ok: false, error: "unsafe navigate path" };
|
|
330
|
-
window.location.
|
|
331
|
-
|
|
341
|
+
const here = window.location.pathname + window.location.search + window.location.hash;
|
|
342
|
+
const target = new URL(path, window.location.href);
|
|
343
|
+
if (target.pathname + target.search + target.hash === here) return { ok: true, note: "already on this page" };
|
|
344
|
+
return { ok: true, [AFTER_RESULT]: () => window.location.assign(path) };
|
|
332
345
|
},
|
|
333
346
|
openUrl: (args) => {
|
|
334
347
|
const url = String(args.url ?? "");
|
|
335
348
|
if (!isSafeHttpUrl(url)) return { ok: false, error: "unsafe url" };
|
|
336
|
-
|
|
349
|
+
const sameHost = new URL(url, window.location.origin).hostname === window.location.hostname;
|
|
350
|
+
window.open(url, sameHost ? "_self" : "_blank", "noopener,noreferrer");
|
|
337
351
|
return { ok: true };
|
|
338
352
|
}
|
|
339
353
|
};
|
|
@@ -445,6 +459,23 @@ function createChatClient(options) {
|
|
|
445
459
|
} catch {
|
|
446
460
|
}
|
|
447
461
|
};
|
|
462
|
+
const callsKey = `oyehello-chat-calls:${options.agentId}`;
|
|
463
|
+
const handledCallIds = () => {
|
|
464
|
+
try {
|
|
465
|
+
const raw = store2?.getItem(callsKey);
|
|
466
|
+
const v = raw ? JSON.parse(raw) : null;
|
|
467
|
+
return v && session && v.sessionId === session.sessionId && Array.isArray(v.ids) ? v.ids : [];
|
|
468
|
+
} catch {
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
const rememberHandledCall = (callId) => {
|
|
473
|
+
if (!session) return;
|
|
474
|
+
try {
|
|
475
|
+
store2?.setItem(callsKey, JSON.stringify({ sessionId: session.sessionId, ids: [...handledCallIds(), callId].slice(-50) }));
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
};
|
|
448
479
|
const emit = (e) => {
|
|
449
480
|
for (const l of listeners) l(e);
|
|
450
481
|
};
|
|
@@ -493,8 +524,9 @@ function createChatClient(options) {
|
|
|
493
524
|
} else if (ev.type === "error") {
|
|
494
525
|
emit({ type: "error", error: new Error(ev.message) });
|
|
495
526
|
} else if (ev.type === "tool_call") {
|
|
496
|
-
if (seenCallIds.has(ev.callId)) return;
|
|
527
|
+
if (seenCallIds.has(ev.callId) || handledCallIds().includes(ev.callId)) return;
|
|
497
528
|
seenCallIds.add(ev.callId);
|
|
529
|
+
rememberHandledCall(ev.callId);
|
|
498
530
|
const handler = toolHandlers.get(ev.name) ?? BUILTIN_TOOL_HANDLERS[ev.name];
|
|
499
531
|
void (async () => {
|
|
500
532
|
if (!session) return;
|
|
@@ -504,8 +536,16 @@ function createChatClient(options) {
|
|
|
504
536
|
return;
|
|
505
537
|
}
|
|
506
538
|
try {
|
|
507
|
-
const
|
|
539
|
+
const raw = await handler(ev.args);
|
|
540
|
+
let after = null;
|
|
541
|
+
let out = raw;
|
|
542
|
+
if (raw && typeof raw === "object" && typeof raw[AFTER_RESULT] === "function") {
|
|
543
|
+
const { [AFTER_RESULT]: run, ...rest } = raw;
|
|
544
|
+
after = run;
|
|
545
|
+
out = rest;
|
|
546
|
+
}
|
|
508
547
|
await postToolResult({ ...base, ok: true, result: typeof out === "string" ? out : JSON.stringify(out ?? { ok: true }) });
|
|
548
|
+
after?.();
|
|
509
549
|
} catch (err) {
|
|
510
550
|
await postToolResult({ ...base, ok: false, result: err.message });
|
|
511
551
|
}
|
|
@@ -587,6 +627,16 @@ function createChatClient(options) {
|
|
|
587
627
|
if (resumed) {
|
|
588
628
|
session = resumed;
|
|
589
629
|
emit({ type: "agent", agent: resumed.agent });
|
|
630
|
+
try {
|
|
631
|
+
const history2 = await getHistory({ baseUrl: options.baseUrl, sessionId: resumed.sessionId, sessionToken: resumed.sessionToken, fetchImpl });
|
|
632
|
+
for (const h of history2) {
|
|
633
|
+
const turnId = h.role === "assistant" || h.role === "human" ? h.turnId : void 0;
|
|
634
|
+
messages.push({ id: uid("m"), role: h.role, text: h.text, streaming: false, turnId });
|
|
635
|
+
if (h.turnId) seenTurnIds.add(h.turnId);
|
|
636
|
+
}
|
|
637
|
+
emitMessages();
|
|
638
|
+
} catch {
|
|
639
|
+
}
|
|
590
640
|
await openStream();
|
|
591
641
|
scheduleRefresh();
|
|
592
642
|
flushJourney();
|
|
@@ -727,6 +777,44 @@ function createChatClient(options) {
|
|
|
727
777
|
}
|
|
728
778
|
};
|
|
729
779
|
}
|
|
780
|
+
|
|
781
|
+
// src/page-theme.ts
|
|
782
|
+
var WATCHED = ["class", "data-theme", "data-mode", "style"];
|
|
783
|
+
function themeOf(node) {
|
|
784
|
+
for (const attr of ["data-theme", "data-mode"]) {
|
|
785
|
+
const v = node.getAttribute(attr);
|
|
786
|
+
if (v === "dark" || v === "light") return v;
|
|
787
|
+
}
|
|
788
|
+
if (node.classList.contains("dark")) return "dark";
|
|
789
|
+
if (node.classList.contains("light")) return "light";
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
function resolvePageTheme(el) {
|
|
793
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
794
|
+
const t = themeOf(node);
|
|
795
|
+
if (t) return t;
|
|
796
|
+
}
|
|
797
|
+
const scheme = (document.documentElement.style.colorScheme || getComputedStyle(document.documentElement).colorScheme || "").trim();
|
|
798
|
+
return scheme === "dark" || scheme === "light" ? scheme : null;
|
|
799
|
+
}
|
|
800
|
+
function watchPageTheme(el, onChange) {
|
|
801
|
+
let last;
|
|
802
|
+
const check = () => {
|
|
803
|
+
const t = resolvePageTheme(el);
|
|
804
|
+
if (t !== last) {
|
|
805
|
+
last = t;
|
|
806
|
+
onChange(t);
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
check();
|
|
810
|
+
if (typeof MutationObserver === "undefined") return () => {
|
|
811
|
+
};
|
|
812
|
+
const mo = new MutationObserver(check);
|
|
813
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
814
|
+
mo.observe(node, { attributes: true, attributeFilter: WATCHED });
|
|
815
|
+
}
|
|
816
|
+
return () => mo.disconnect();
|
|
817
|
+
}
|
|
730
818
|
marked.marked.setOptions({ gfm: true, breaks: true });
|
|
731
819
|
var hookInstalled = false;
|
|
732
820
|
function installLinkHook() {
|
|
@@ -809,11 +897,15 @@ var STYLES = `
|
|
|
809
897
|
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
810
898
|
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
811
899
|
display: block; visibility: visible; opacity: 1;
|
|
900
|
+
/* The closed panel still takes up its full size in the layout (it fades, it is not removed), so
|
|
901
|
+
* the host box covers ~384x620px of the page. The host and its wrapper must not catch clicks;
|
|
902
|
+
* only the visible parts opt back in (launcher, teaser while shown, panel while open). */
|
|
903
|
+
pointer-events: none;
|
|
812
904
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
813
905
|
-webkit-font-smoothing: antialiased;
|
|
814
906
|
}
|
|
815
907
|
@media (prefers-color-scheme: dark) {
|
|
816
|
-
:host(:not([theme="light"])) {
|
|
908
|
+
:host(:not([theme="light"]):not([data-oc-page-theme="light"])) {
|
|
817
909
|
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
818
910
|
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
819
911
|
--oc-border: rgba(255,255,255,0.09);
|
|
@@ -823,7 +915,9 @@ var STYLES = `
|
|
|
823
915
|
--oc-glass-border: rgba(255,255,255,0.12);
|
|
824
916
|
}
|
|
825
917
|
}
|
|
826
|
-
|
|
918
|
+
/* Explicit dark, or auto on a page that declares dark (data-oc-page-theme, set by page-theme.ts). The OS rule above
|
|
919
|
+
* only applies when the page declares nothing; a page that declares light keeps the light base. */
|
|
920
|
+
:host([theme="dark"]), :host(:not([theme="light"])[data-oc-page-theme="dark"]) {
|
|
827
921
|
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
828
922
|
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
829
923
|
--oc-border: rgba(255,255,255,0.09);
|
|
@@ -835,7 +929,7 @@ var STYLES = `
|
|
|
835
929
|
* { box-sizing: border-box; }
|
|
836
930
|
button { font-family: inherit; }
|
|
837
931
|
|
|
838
|
-
.oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
|
|
932
|
+
.oc { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; pointer-events: none; }
|
|
839
933
|
|
|
840
934
|
/* \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 */
|
|
841
935
|
.launcher {
|
|
@@ -844,6 +938,7 @@ var STYLES = `
|
|
|
844
938
|
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);
|
|
845
939
|
color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
846
940
|
transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
|
|
941
|
+
pointer-events: auto;
|
|
847
942
|
}
|
|
848
943
|
.launcher:hover { transform: scale(1.06); }
|
|
849
944
|
.launcher:active { transform: scale(0.93); }
|
|
@@ -1211,6 +1306,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1211
1306
|
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
1212
1307
|
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
1213
1308
|
uninstallSpaHooks = null;
|
|
1309
|
+
unwatchPageTheme = null;
|
|
1214
1310
|
// ── shadow refs ──
|
|
1215
1311
|
oc;
|
|
1216
1312
|
bodyEl;
|
|
@@ -1244,6 +1340,11 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1244
1340
|
this.uninstallSpaHooks = null;
|
|
1245
1341
|
}
|
|
1246
1342
|
this.renderShell();
|
|
1343
|
+
this.unwatchPageTheme?.();
|
|
1344
|
+
this.unwatchPageTheme = watchPageTheme(this, (t) => {
|
|
1345
|
+
if (t) this.setAttribute("data-oc-page-theme", t);
|
|
1346
|
+
else this.removeAttribute("data-oc-page-theme");
|
|
1347
|
+
});
|
|
1247
1348
|
this.style.setProperty("display", "block", "important");
|
|
1248
1349
|
this.style.setProperty("visibility", "visible", "important");
|
|
1249
1350
|
this.style.setProperty("opacity", "1", "important");
|
|
@@ -1354,6 +1455,8 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1354
1455
|
this.teardownIdle();
|
|
1355
1456
|
this.uninstallSpaHooks?.();
|
|
1356
1457
|
this.uninstallSpaHooks = null;
|
|
1458
|
+
this.unwatchPageTheme?.();
|
|
1459
|
+
this.unwatchPageTheme = null;
|
|
1357
1460
|
for (const t of this.timers) clearTimeout(t);
|
|
1358
1461
|
for (const url of this.imagePreviewCache.values()) {
|
|
1359
1462
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -1364,6 +1467,11 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1364
1467
|
if (this.client) this.client.registerTools(handlers);
|
|
1365
1468
|
else Object.assign(this.pendingTools, handlers);
|
|
1366
1469
|
}
|
|
1470
|
+
/** Open the chat window, exactly as a launcher click does. No-op if already open or not rendered. */
|
|
1471
|
+
open() {
|
|
1472
|
+
if (!this.oc || this.uiOpen) return;
|
|
1473
|
+
this.openPanel();
|
|
1474
|
+
}
|
|
1367
1475
|
key(k) {
|
|
1368
1476
|
return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
|
|
1369
1477
|
}
|