@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/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.
|
|
@@ -74,23 +74,35 @@ await chat.sendMessage('Hello');
|
|
|
74
74
|
|
|
75
75
|
## Client tools (browser actions)
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
The SDK supports two browser-side tools: `navigate` (go to a path on the current site) and `openUrl`
|
|
78
|
+
(open a URL, by default in a new tab). Once a tool is enabled for the agent in the console **and** the
|
|
79
|
+
target passes the agent's navigate allowlist (also set in the console), the SDK executes it itself —
|
|
80
|
+
**no host integration required**:
|
|
81
|
+
```html
|
|
82
|
+
<script type="module">import '@oyehello/chat';</script>
|
|
83
|
+
<oyehello-chat api-key="ohk_pub_…" agent-id="agt_…" base-url="https://api.oyehello.ai"></oyehello-chat>
|
|
84
|
+
```
|
|
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)` (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.
|
|
90
|
+
|
|
91
|
+
A host page MAY still register its own `navigate`/`openUrl` handler to override the default behavior
|
|
92
|
+
(e.g. push through a client-side router instead of a full navigation):
|
|
79
93
|
```js
|
|
80
94
|
const chat = createChatClient({ apiKey, agentId, baseUrl });
|
|
81
95
|
chat.registerTools({
|
|
82
96
|
navigate: ({ path }) => router.push(path),
|
|
83
|
-
fillField: ({ selector, value }) => { /* your DOM logic */ },
|
|
84
97
|
});
|
|
85
98
|
// or on the web component: document.querySelector('oyehello-chat').registerTools({ … })
|
|
86
99
|
```
|
|
87
|
-
Register handlers BEFORE `start()
|
|
100
|
+
Register handlers BEFORE `start()`. A host-registered handler always takes priority over the SDK's
|
|
101
|
+
built-in; any other tool name has no handler and fails cleanly.
|
|
88
102
|
|
|
89
|
-
> **Security.**
|
|
90
|
-
>
|
|
91
|
-
>
|
|
92
|
-
> Because a visitor can influence the conversation, treat these as capable of arbitrary DOM action —
|
|
93
|
-
> validate selectors/values and scope what each handler can do before enabling them.
|
|
103
|
+
> **Security.** Navigation targets (both the SDK's built-in execution and a host-overridden handler)
|
|
104
|
+
> are validated against the agent's navigate allowlist **server-side** before the tool call ever
|
|
105
|
+
> reaches the browser — the SDK/host is not the trust boundary for where a visitor can be sent.
|
|
94
106
|
|
|
95
107
|
## Requirements
|
|
96
108
|
- `api-key` is a **publishable** key (`ohk_pub_…`) created in the oyehello console.
|
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",
|
|
@@ -314,6 +324,33 @@ function installSpaHooks(onNav) {
|
|
|
314
324
|
// src/client.ts
|
|
315
325
|
var seq = 0;
|
|
316
326
|
var uid = (p) => `${p}_${Date.now().toString(36)}_${(seq++).toString(36)}`;
|
|
327
|
+
var isSafeRelativePath = (p) => p.startsWith("/") && !p.startsWith("//");
|
|
328
|
+
var isSafeHttpUrl = (u) => {
|
|
329
|
+
try {
|
|
330
|
+
const parsed = new URL(u, window.location.origin);
|
|
331
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
332
|
+
} catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var AFTER_RESULT = "__ohAfterResult";
|
|
337
|
+
var BUILTIN_TOOL_HANDLERS = {
|
|
338
|
+
navigate: (args) => {
|
|
339
|
+
const path = String(args.path ?? "");
|
|
340
|
+
if (!isSafeRelativePath(path)) return { ok: false, error: "unsafe navigate path" };
|
|
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) };
|
|
345
|
+
},
|
|
346
|
+
openUrl: (args) => {
|
|
347
|
+
const url = String(args.url ?? "");
|
|
348
|
+
if (!isSafeHttpUrl(url)) return { ok: false, error: "unsafe url" };
|
|
349
|
+
const sameHost = new URL(url, window.location.origin).hostname === window.location.hostname;
|
|
350
|
+
window.open(url, sameHost ? "_self" : "_blank", "noopener,noreferrer");
|
|
351
|
+
return { ok: true };
|
|
352
|
+
}
|
|
353
|
+
};
|
|
317
354
|
function createChatClient(options) {
|
|
318
355
|
const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
319
356
|
const ES = options.eventSource ?? globalThis.EventSource;
|
|
@@ -422,6 +459,23 @@ function createChatClient(options) {
|
|
|
422
459
|
} catch {
|
|
423
460
|
}
|
|
424
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
|
+
};
|
|
425
479
|
const emit = (e) => {
|
|
426
480
|
for (const l of listeners) l(e);
|
|
427
481
|
};
|
|
@@ -470,9 +524,10 @@ function createChatClient(options) {
|
|
|
470
524
|
} else if (ev.type === "error") {
|
|
471
525
|
emit({ type: "error", error: new Error(ev.message) });
|
|
472
526
|
} else if (ev.type === "tool_call") {
|
|
473
|
-
if (seenCallIds.has(ev.callId)) return;
|
|
527
|
+
if (seenCallIds.has(ev.callId) || handledCallIds().includes(ev.callId)) return;
|
|
474
528
|
seenCallIds.add(ev.callId);
|
|
475
|
-
|
|
529
|
+
rememberHandledCall(ev.callId);
|
|
530
|
+
const handler = toolHandlers.get(ev.name) ?? BUILTIN_TOOL_HANDLERS[ev.name];
|
|
476
531
|
void (async () => {
|
|
477
532
|
if (!session) return;
|
|
478
533
|
const base = { baseUrl: options.baseUrl, sessionId: session.sessionId, sessionToken: session.sessionToken, callId: ev.callId, fetchImpl };
|
|
@@ -481,8 +536,16 @@ function createChatClient(options) {
|
|
|
481
536
|
return;
|
|
482
537
|
}
|
|
483
538
|
try {
|
|
484
|
-
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
|
+
}
|
|
485
547
|
await postToolResult({ ...base, ok: true, result: typeof out === "string" ? out : JSON.stringify(out ?? { ok: true }) });
|
|
548
|
+
after?.();
|
|
486
549
|
} catch (err) {
|
|
487
550
|
await postToolResult({ ...base, ok: false, result: err.message });
|
|
488
551
|
}
|
|
@@ -564,13 +627,24 @@ function createChatClient(options) {
|
|
|
564
627
|
if (resumed) {
|
|
565
628
|
session = resumed;
|
|
566
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
|
+
}
|
|
567
640
|
await openStream();
|
|
568
641
|
scheduleRefresh();
|
|
569
642
|
flushJourney();
|
|
570
643
|
return;
|
|
571
644
|
}
|
|
572
645
|
if (deviceFingerprint === void 0) deviceFingerprint = await computeDeviceFingerprint().catch(() => null);
|
|
573
|
-
|
|
646
|
+
const advertisedTools = /* @__PURE__ */ new Set([...toolHandlers.keys(), ...Object.keys(BUILTIN_TOOL_HANDLERS)]);
|
|
647
|
+
session = await openSession({ baseUrl: options.baseUrl, apiKey: options.apiKey, agentId: options.agentId, user: options.user, clientTools: Array.from(advertisedTools), fingerprint: deviceFingerprint ?? void 0, fetchImpl });
|
|
574
648
|
persistSession(session);
|
|
575
649
|
if (!warnedProtocol && typeof session.protocol === "number" && session.protocol !== CHAT_PROTOCOL_VERSION) {
|
|
576
650
|
warnedProtocol = true;
|
|
@@ -703,6 +777,44 @@ function createChatClient(options) {
|
|
|
703
777
|
}
|
|
704
778
|
};
|
|
705
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
|
+
}
|
|
706
818
|
marked.marked.setOptions({ gfm: true, breaks: true });
|
|
707
819
|
var hookInstalled = false;
|
|
708
820
|
function installLinkHook() {
|
|
@@ -785,11 +897,15 @@ var STYLES = `
|
|
|
785
897
|
* body children (oyehello-chat { display:none }) can't silently kill the widget. Also pinned
|
|
786
898
|
* inline with !important at mount (see element.ts) to beat an external !important rule. */
|
|
787
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;
|
|
788
904
|
font-family: 'Geist', system-ui, -apple-system, 'Helvetica Neue', sans-serif;
|
|
789
905
|
-webkit-font-smoothing: antialiased;
|
|
790
906
|
}
|
|
791
907
|
@media (prefers-color-scheme: dark) {
|
|
792
|
-
:host(:not([theme="light"])) {
|
|
908
|
+
:host(:not([theme="light"]):not([data-oc-page-theme="light"])) {
|
|
793
909
|
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
794
910
|
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
795
911
|
--oc-border: rgba(255,255,255,0.09);
|
|
@@ -799,7 +915,9 @@ var STYLES = `
|
|
|
799
915
|
--oc-glass-border: rgba(255,255,255,0.12);
|
|
800
916
|
}
|
|
801
917
|
}
|
|
802
|
-
|
|
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"]) {
|
|
803
921
|
--oc-bg: #000000; --oc-bg-sunk: #000000;
|
|
804
922
|
--oc-fg: #FFFFFF; --oc-fg-muted: rgba(255,255,255,0.68); --oc-fg-dim: rgba(255,255,255,0.42);
|
|
805
923
|
--oc-border: rgba(255,255,255,0.09);
|
|
@@ -811,7 +929,7 @@ var STYLES = `
|
|
|
811
929
|
* { box-sizing: border-box; }
|
|
812
930
|
button { font-family: inherit; }
|
|
813
931
|
|
|
814
|
-
.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; }
|
|
815
933
|
|
|
816
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 */
|
|
817
935
|
.launcher {
|
|
@@ -820,6 +938,7 @@ var STYLES = `
|
|
|
820
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);
|
|
821
939
|
color: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer;
|
|
822
940
|
transition: transform .26s var(--oc-bounce), filter .2s var(--oc-ease); align-self: flex-end;
|
|
941
|
+
pointer-events: auto;
|
|
823
942
|
}
|
|
824
943
|
.launcher:hover { transform: scale(1.06); }
|
|
825
944
|
.launcher:active { transform: scale(0.93); }
|
|
@@ -1187,6 +1306,7 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1187
1306
|
// Uninstaller for the SPA history-navigation hooks (journey tracking), installed in
|
|
1188
1307
|
// connectedCallback and torn down in disconnectedCallback alongside everything else.
|
|
1189
1308
|
uninstallSpaHooks = null;
|
|
1309
|
+
unwatchPageTheme = null;
|
|
1190
1310
|
// ── shadow refs ──
|
|
1191
1311
|
oc;
|
|
1192
1312
|
bodyEl;
|
|
@@ -1220,6 +1340,11 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1220
1340
|
this.uninstallSpaHooks = null;
|
|
1221
1341
|
}
|
|
1222
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
|
+
});
|
|
1223
1348
|
this.style.setProperty("display", "block", "important");
|
|
1224
1349
|
this.style.setProperty("visibility", "visible", "important");
|
|
1225
1350
|
this.style.setProperty("opacity", "1", "important");
|
|
@@ -1330,6 +1455,8 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1330
1455
|
this.teardownIdle();
|
|
1331
1456
|
this.uninstallSpaHooks?.();
|
|
1332
1457
|
this.uninstallSpaHooks = null;
|
|
1458
|
+
this.unwatchPageTheme?.();
|
|
1459
|
+
this.unwatchPageTheme = null;
|
|
1333
1460
|
for (const t of this.timers) clearTimeout(t);
|
|
1334
1461
|
for (const url of this.imagePreviewCache.values()) {
|
|
1335
1462
|
if (typeof URL.revokeObjectURL === "function") URL.revokeObjectURL(url);
|
|
@@ -1340,6 +1467,11 @@ var OyehelloChatElement = class extends HTMLElement {
|
|
|
1340
1467
|
if (this.client) this.client.registerTools(handlers);
|
|
1341
1468
|
else Object.assign(this.pendingTools, handlers);
|
|
1342
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
|
+
}
|
|
1343
1475
|
key(k) {
|
|
1344
1476
|
return `oyehello-chat:${k}:${this.getAttribute("agent-id") ?? ""}`;
|
|
1345
1477
|
}
|