@heroui/agent 0.2.0-beta.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.
@@ -0,0 +1,954 @@
1
+ import {
2
+ ComposerControlTooltip,
3
+ PanelShell,
4
+ PoweredByHero,
5
+ clearAgentComposerDraftsForProject,
6
+ isHeroUIAgentAttachmentContentType,
7
+ readAgentShellHandoff,
8
+ readSidebarWidth,
9
+ resizeComposerTextarea,
10
+ resolveOptions,
11
+ scheduleIdleTask,
12
+ writeAgentShellHandoff
13
+ } from "./chunk-RQCTC4JB.js";
14
+
15
+ // src/embed/provider.tsx
16
+ import {
17
+ useCallback,
18
+ useEffect as useEffect2,
19
+ useLayoutEffect,
20
+ useMemo,
21
+ useRef as useRef2,
22
+ useState as useState2,
23
+ useSyncExternalStore
24
+ } from "react";
25
+ import { createRoot } from "react-dom/client";
26
+
27
+ // src/embed/launcher-spark-icon.tsx
28
+ import { SparklesFill } from "@gravity-ui/icons";
29
+ import { jsx } from "react/jsx-runtime";
30
+ function LauncherSparkIcon(props) {
31
+ return /* @__PURE__ */ jsx(SparklesFill, { ...props });
32
+ }
33
+
34
+ // src/embed/launcher-icon.tsx
35
+ import { jsx as jsx2 } from "react/jsx-runtime";
36
+ function isSvgIcon(icon) {
37
+ return /\.svg(?:[?#]|$)/i.test(icon) || /^data:image\/svg\+xml[;,]/i.test(icon);
38
+ }
39
+ function cssUrl(value) {
40
+ return `url("${value.replace(/["\\\n\r]/g, encodeURIComponent)}")`;
41
+ }
42
+ function LauncherIcon({ icon }) {
43
+ if (!icon) return /* @__PURE__ */ jsx2(LauncherSparkIcon, {});
44
+ if (isSvgIcon(icon)) {
45
+ return /* @__PURE__ */ jsx2(
46
+ "span",
47
+ {
48
+ "aria-hidden": "true",
49
+ className: "ha-launcher__icon ha-launcher__icon--tinted",
50
+ style: { WebkitMaskImage: cssUrl(icon), maskImage: cssUrl(icon) }
51
+ }
52
+ );
53
+ }
54
+ return /* @__PURE__ */ jsx2("img", { alt: "", className: "ha-launcher__icon", src: icon });
55
+ }
56
+
57
+ // src/embed/merge-remote-config.ts
58
+ function isPlainObject(value) {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
61
+ function deepMerge(base, override) {
62
+ if (override === void 0) return base;
63
+ if (!isPlainObject(base) || !isPlainObject(override)) return override;
64
+ const merged = { ...base };
65
+ for (const [key, value] of Object.entries(override)) {
66
+ if (value === void 0) continue;
67
+ merged[key] = key in base ? deepMerge(base[key], value) : value;
68
+ }
69
+ return merged;
70
+ }
71
+ function mergeRemoteConfig(remote, props) {
72
+ if (!remote) return props;
73
+ const groups = {
74
+ appearance: remote.appearance,
75
+ capabilities: remote.capabilities,
76
+ composer: remote.composer,
77
+ markdown: remote.markdown,
78
+ permissions: remote.permissions,
79
+ responseActions: remote.responseActions,
80
+ startScreen: remote.startScreen
81
+ };
82
+ const merged = deepMerge(groups, {
83
+ appearance: props.appearance,
84
+ capabilities: props.capabilities,
85
+ composer: props.composer,
86
+ // Markdown renderer plugins are functions and never survive a JSON
87
+ // document, so they are merged back by identity in the provider.
88
+ markdown: props.markdown ? { ...props.markdown, plugins: void 0 } : void 0,
89
+ permissions: props.permissions,
90
+ responseActions: props.responseActions,
91
+ startScreen: props.startScreen
92
+ });
93
+ return { ...props, ...merged };
94
+ }
95
+ function hostSuppliesFont(props) {
96
+ return Boolean(props.appearance?.theme?.typography?.fontFamily?.trim());
97
+ }
98
+
99
+ // src/contracts/appearance.ts
100
+ var HEROUI_AGENT_REMOTE_CONFIG_VERSION = 1;
101
+ var INVALID = Symbol("invalid");
102
+ function isRecord(value) {
103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
104
+ }
105
+ function optionalString(value) {
106
+ if (value === void 0) return void 0;
107
+ return typeof value === "string" ? value : INVALID;
108
+ }
109
+ function optionalBoolean(value) {
110
+ if (value === void 0) return void 0;
111
+ return typeof value === "boolean" ? value : INVALID;
112
+ }
113
+ function optionalNumber(value) {
114
+ if (value === void 0) return void 0;
115
+ return typeof value === "number" && Number.isFinite(value) ? value : INVALID;
116
+ }
117
+ function optionalEnum(allowed) {
118
+ return (value) => {
119
+ if (value === void 0) return void 0;
120
+ return typeof value === "string" && allowed.includes(value) ? value : INVALID;
121
+ };
122
+ }
123
+ function optionalThemeColor(value) {
124
+ if (value === void 0) return void 0;
125
+ if (typeof value === "string") return value;
126
+ if (!isRecord(value)) return INVALID;
127
+ const dark = optionalString(value["dark"]);
128
+ const light = optionalString(value["light"]);
129
+ if (dark === INVALID || light === INVALID) return INVALID;
130
+ return { ...dark === void 0 ? {} : { dark }, ...light === void 0 ? {} : { light } };
131
+ }
132
+ function optionalStringRecord(value) {
133
+ if (value === void 0) return void 0;
134
+ if (!isRecord(value)) return INVALID;
135
+ const record = {};
136
+ for (const [key, entry] of Object.entries(value)) {
137
+ if (typeof entry !== "string") return INVALID;
138
+ record[key] = entry;
139
+ }
140
+ return record;
141
+ }
142
+ function optionalStringArray(value) {
143
+ if (value === void 0) return void 0;
144
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) return INVALID;
145
+ return value;
146
+ }
147
+ function shape(value, validators) {
148
+ if (value === void 0) return INVALID;
149
+ if (!isRecord(value)) return INVALID;
150
+ const result = {};
151
+ for (const [key, validate] of Object.entries(validators)) {
152
+ const parsed = validate(value[key]);
153
+ if (parsed === INVALID) return INVALID;
154
+ if (parsed !== void 0) result[key] = parsed;
155
+ }
156
+ return result;
157
+ }
158
+ function group(parsed) {
159
+ return parsed === INVALID ? void 0 : parsed;
160
+ }
161
+ var COLOR_KEYS = [
162
+ "accent",
163
+ "background",
164
+ "foreground",
165
+ "overlay",
166
+ "surface",
167
+ "surfaceSecondary",
168
+ "tooltip"
169
+ ];
170
+ function parseAppearance(value) {
171
+ return shape(value, {
172
+ launcher: (launcher) => launcher === void 0 ? void 0 : shape(launcher, {
173
+ background: optionalThemeColor,
174
+ icon: optionalString,
175
+ position: optionalEnum(["bottom-left", "bottom-right"]),
176
+ style: optionalStringRecord
177
+ }),
178
+ panel: (panel) => panel === void 0 ? void 0 : shape(panel, {
179
+ expandable: optionalBoolean,
180
+ expanded: optionalBoolean,
181
+ initialHeight: (entry) => entry === void 0 || typeof entry === "string" ? entry : optionalNumber(entry),
182
+ initialWidth: (entry) => entry === void 0 || typeof entry === "string" ? entry : optionalNumber(entry)
183
+ }),
184
+ theme: (theme) => theme === void 0 ? void 0 : shape(theme, {
185
+ colorScheme: optionalEnum(["dark", "light", "system"]),
186
+ colors: (colors) => colors === void 0 ? void 0 : shape(
187
+ colors,
188
+ Object.fromEntries(COLOR_KEYS.map((key) => [key, optionalThemeColor]))
189
+ ),
190
+ designTheme: optionalEnum(["base", "brutalism", "glass", "mouve"]),
191
+ radius: optionalEnum(["pill", "round", "sharp", "soft"]),
192
+ typography: (typography) => typography === void 0 ? void 0 : shape(typography, { baseSize: optionalNumber, fontFamily: optionalString })
193
+ }),
194
+ viewMode: optionalEnum(["floating", "sidebar"])
195
+ });
196
+ }
197
+ function parseComposer(value) {
198
+ return shape(value, {
199
+ attachments: (attachments) => {
200
+ if (attachments === void 0 || attachments === false) return attachments;
201
+ if (!Array.isArray(attachments)) return INVALID;
202
+ const types = attachments.filter(
203
+ (entry) => typeof entry === "string" && isHeroUIAgentAttachmentContentType(entry)
204
+ );
205
+ return types.length === attachments.length ? types : INVALID;
206
+ },
207
+ defaultModel: optionalString,
208
+ dictation: optionalBoolean,
209
+ disclaimer: (disclaimer) => disclaimer === false ? false : optionalString(disclaimer),
210
+ modelPicker: optionalBoolean,
211
+ placeholder: optionalString
212
+ });
213
+ }
214
+ function parseMarkdown(value) {
215
+ return shape(value, {
216
+ animated: (animated) => {
217
+ if (animated === void 0 || animated === false) return animated;
218
+ if (!isRecord(animated)) return INVALID;
219
+ const animation = optionalEnum(["blurIn", "fadeIn", "slideUp"])(
220
+ animated["animation"]
221
+ );
222
+ if (animation === INVALID || animation === void 0) return INVALID;
223
+ return { ...animated, animation };
224
+ },
225
+ caret: (caret) => caret === false ? false : optionalEnum(["block", "circle"])(caret)
226
+ });
227
+ }
228
+ function parseResponseActions(value) {
229
+ if (value === void 0 || value === false) return value;
230
+ if (!Array.isArray(value)) return INVALID;
231
+ const allowed = ["copy", "feedback", "retry"];
232
+ const actions = value.filter(
233
+ (entry) => typeof entry === "string" && allowed.includes(entry)
234
+ );
235
+ return actions.length === value.length ? actions : INVALID;
236
+ }
237
+ function parseAgentRemoteConfig(value) {
238
+ if (!isRecord(value)) return null;
239
+ if (value["version"] !== HEROUI_AGENT_REMOTE_CONFIG_VERSION) return null;
240
+ const revision = value["revision"];
241
+ if (typeof revision !== "string" || !revision) return null;
242
+ const appearance = group(parseAppearance(value["appearance"]));
243
+ const capabilities = group(
244
+ shape(value["capabilities"], {
245
+ imageSearch: optionalBoolean,
246
+ webSearch: optionalBoolean
247
+ })
248
+ );
249
+ const composer = group(parseComposer(value["composer"]));
250
+ const markdown = group(parseMarkdown(value["markdown"]));
251
+ const permissions = group(
252
+ shape(value["permissions"], {
253
+ defaultMode: optionalEnum(["ask", "auto", "full"]),
254
+ showPicker: optionalBoolean
255
+ })
256
+ );
257
+ const responseActions = group(parseResponseActions(value["responseActions"]));
258
+ const startScreen = group(
259
+ shape(value["startScreen"], {
260
+ greeting: optionalString,
261
+ promptShortcuts: optionalBoolean,
262
+ prompts: optionalStringArray
263
+ })
264
+ );
265
+ const webfont = group(
266
+ shape(value["webfont"], {
267
+ familyName: (familyName) => typeof familyName === "string" && familyName ? familyName : INVALID,
268
+ fontFaceUrl: optionalString,
269
+ stylesheetUrl: optionalString
270
+ })
271
+ );
272
+ return {
273
+ ...appearance ? { appearance } : {},
274
+ ...capabilities ? { capabilities } : {},
275
+ ...composer ? { composer } : {},
276
+ ...markdown ? { markdown } : {},
277
+ ...permissions ? { permissions } : {},
278
+ ...responseActions === void 0 ? {} : { responseActions },
279
+ ...startScreen ? { startScreen } : {},
280
+ ...webfont ? { webfont } : {},
281
+ revision,
282
+ version: HEROUI_AGENT_REMOTE_CONFIG_VERSION
283
+ };
284
+ }
285
+
286
+ // src/embed/remote-config.ts
287
+ function cacheKey(agentId) {
288
+ return `heroui-agent:config:${agentId}`;
289
+ }
290
+ function readCachedRemoteConfig(agentId) {
291
+ try {
292
+ const raw = window.localStorage.getItem(cacheKey(agentId));
293
+ return raw ? parseAgentRemoteConfig(JSON.parse(raw)) : null;
294
+ } catch {
295
+ return null;
296
+ }
297
+ }
298
+ function writeCachedRemoteConfig(agentId, config) {
299
+ try {
300
+ window.localStorage.setItem(cacheKey(agentId), JSON.stringify(config));
301
+ } catch {
302
+ }
303
+ }
304
+ function clearCachedRemoteConfig(agentId) {
305
+ try {
306
+ window.localStorage.removeItem(cacheKey(agentId));
307
+ } catch {
308
+ }
309
+ }
310
+ async function fetchRemoteConfig(apiBaseUrl, agentId, options = {}) {
311
+ try {
312
+ const response = await fetch(
313
+ `${apiBaseUrl}/v1/agents/${encodeURIComponent(agentId)}/appearance`,
314
+ {
315
+ headers: options.revision ? { "If-None-Match": `"${options.revision}"` } : {},
316
+ ...options.signal ? { signal: options.signal } : {}
317
+ }
318
+ );
319
+ if (response.status === 304) return { status: "not-modified" };
320
+ if (response.status === 404 || response.status === 410) return { status: "missing" };
321
+ if (!response.ok) return { status: "unavailable" };
322
+ const config = parseAgentRemoteConfig(await response.json());
323
+ return config ? { config, status: "ok" } : { status: "unavailable" };
324
+ } catch {
325
+ return { status: "unavailable" };
326
+ }
327
+ }
328
+
329
+ // src/embed/shell-conversation.tsx
330
+ import { useEffect, useRef, useState } from "react";
331
+ import { Fragment, jsx as jsx3, jsxs } from "react/jsx-runtime";
332
+ function ShellConversation({
333
+ agentId,
334
+ greeting,
335
+ onIntent,
336
+ placeholder,
337
+ suggestedPrompts
338
+ }) {
339
+ const [input, setInput] = useState(() => readAgentShellHandoff(agentId)?.prompt ?? "");
340
+ const [submitted, setSubmitted] = useState(false);
341
+ const textareaRef = useRef(null);
342
+ useEffect(() => {
343
+ if (submitted) return;
344
+ writeAgentShellHandoff(agentId, { prompt: input, submitted: false });
345
+ }, [input, agentId, submitted]);
346
+ const submit = (prompt) => {
347
+ const trimmed = prompt.trim();
348
+ if (!trimmed) return;
349
+ setSubmitted(true);
350
+ setInput(trimmed);
351
+ writeAgentShellHandoff(agentId, { prompt: trimmed, submitted: true });
352
+ onIntent();
353
+ };
354
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
355
+ /* @__PURE__ */ jsx3("div", { className: "ha-conversation", children: /* @__PURE__ */ jsxs("div", { "aria-live": "polite", className: "ha-messages", role: "log", children: [
356
+ /* @__PURE__ */ jsxs("div", { className: "ha-empty", children: [
357
+ /* @__PURE__ */ jsx3("h2", { children: greeting }),
358
+ /* @__PURE__ */ jsx3("p", { children: "Live answers with charts, metrics, and tables." })
359
+ ] }),
360
+ submitted ? /* @__PURE__ */ jsx3("div", { className: "ha-message", "data-role": "user", children: /* @__PURE__ */ jsx3("div", { className: "ha-message-body", children: input }) }) : null
361
+ ] }) }),
362
+ /* @__PURE__ */ jsxs(
363
+ "form",
364
+ {
365
+ className: "ha-composer-wrap",
366
+ onSubmit: (event) => {
367
+ event.preventDefault();
368
+ submit(input);
369
+ },
370
+ children: [
371
+ !submitted && suggestedPrompts.length > 0 ? /* @__PURE__ */ jsx3("div", { "aria-label": "Suggested prompts", className: "ha-suggestions", role: "group", children: suggestedPrompts.map((prompt) => /* @__PURE__ */ jsxs(
372
+ "button",
373
+ {
374
+ className: "ha-suggestion",
375
+ type: "button",
376
+ onClick: () => submit(prompt),
377
+ children: [
378
+ /* @__PURE__ */ jsx3("span", { children: prompt }),
379
+ /* @__PURE__ */ jsx3(ArrowUpRightGlyph, {})
380
+ ]
381
+ },
382
+ prompt
383
+ )) }) : null,
384
+ /* @__PURE__ */ jsxs("div", { className: "@container ha-composer", children: [
385
+ /* @__PURE__ */ jsx3(
386
+ "textarea",
387
+ {
388
+ ref: textareaRef,
389
+ "aria-label": "Message the assistant",
390
+ placeholder,
391
+ rows: 1,
392
+ value: input,
393
+ onFocus: onIntent,
394
+ onChange: (event) => {
395
+ setInput(event.currentTarget.value);
396
+ resizeComposerTextarea(event.currentTarget);
397
+ onIntent();
398
+ },
399
+ onKeyDown: (event) => {
400
+ if (event.key !== "Enter" || event.shiftKey) return;
401
+ event.preventDefault();
402
+ submit(input);
403
+ }
404
+ }
405
+ ),
406
+ /* @__PURE__ */ jsxs(
407
+ "button",
408
+ {
409
+ "aria-label": "Send message",
410
+ className: "ha-send",
411
+ "data-shortcut": "\u21B5",
412
+ "data-tooltip": "Send message",
413
+ disabled: !input.trim() || submitted,
414
+ type: "submit",
415
+ children: [
416
+ /* @__PURE__ */ jsx3(ArrowUpGlyph, {}),
417
+ /* @__PURE__ */ jsx3(ComposerControlTooltip, { shortcut: "\u21B5", children: "Send message" })
418
+ ]
419
+ }
420
+ )
421
+ ] }),
422
+ /* @__PURE__ */ jsx3(PoweredByHero, {})
423
+ ]
424
+ }
425
+ )
426
+ ] });
427
+ }
428
+ function ArrowUpGlyph() {
429
+ return /* @__PURE__ */ jsx3("svg", { "aria-hidden": "true", fill: "none", height: "18", viewBox: "0 0 18 18", width: "18", children: /* @__PURE__ */ jsx3(
430
+ "path",
431
+ {
432
+ d: "M9 14.5V3.5m0 0L4.5 8M9 3.5 13.5 8",
433
+ stroke: "currentColor",
434
+ strokeLinecap: "round",
435
+ strokeLinejoin: "round",
436
+ strokeWidth: "1.6"
437
+ }
438
+ ) });
439
+ }
440
+ function ArrowUpRightGlyph() {
441
+ return /* @__PURE__ */ jsx3("svg", { "aria-hidden": "true", fill: "none", height: "16", viewBox: "0 0 16 16", width: "16", children: /* @__PURE__ */ jsx3(
442
+ "path",
443
+ {
444
+ d: "M5 11 11 5m0 0H6m5 0v5",
445
+ stroke: "currentColor",
446
+ strokeLinecap: "round",
447
+ strokeLinejoin: "round",
448
+ strokeWidth: "1.5"
449
+ }
450
+ ) });
451
+ }
452
+
453
+ // src/embed/webfont.ts
454
+ var FONT_FILE_EXTENSIONS = [".woff2", ".woff", ".ttf", ".otf", ".eot"];
455
+ function elementId(font) {
456
+ return `heroui-agent-font-${font.familyName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
457
+ }
458
+ function fontFormat(url) {
459
+ const isVariable = url.includes(":vf@") || url.includes("-wght-") || url.includes("-opsz-");
460
+ if (url.endsWith(".woff")) return "woff";
461
+ if (url.endsWith(".ttf")) return "truetype";
462
+ if (url.endsWith(".otf")) return "opentype";
463
+ return isVariable ? "woff2-variations" : "woff2";
464
+ }
465
+ function isSafeFontUrl(url, requireFontFile) {
466
+ try {
467
+ const parsed = new URL(url);
468
+ if (parsed.protocol !== "https:") return false;
469
+ if (!requireFontFile) return true;
470
+ const pathname = parsed.pathname.toLowerCase();
471
+ return FONT_FILE_EXTENSIONS.some((extension) => pathname.endsWith(extension));
472
+ } catch {
473
+ return false;
474
+ }
475
+ }
476
+ function loadAgentWebfont(font) {
477
+ if (!font || typeof document === "undefined") return () => void 0;
478
+ const id = elementId(font);
479
+ if (document.getElementById(id)) return () => void 0;
480
+ if (font.stylesheetUrl && isSafeFontUrl(font.stylesheetUrl, false)) {
481
+ const link = document.createElement("link");
482
+ link.id = id;
483
+ link.rel = "stylesheet";
484
+ link.href = font.stylesheetUrl;
485
+ document.head.append(link);
486
+ return () => link.remove();
487
+ }
488
+ if (font.fontFaceUrl && isSafeFontUrl(font.fontFaceUrl, true)) {
489
+ const style = document.createElement("style");
490
+ const isVariable = font.fontFaceUrl.includes(":vf@") || font.fontFaceUrl.includes("-wght-") || font.fontFaceUrl.includes("-opsz-");
491
+ style.id = id;
492
+ style.textContent = [
493
+ "@font-face {",
494
+ ` font-family: '${font.familyName.replaceAll("'", "")}';`,
495
+ " font-style: normal;",
496
+ " font-display: swap;",
497
+ ` font-weight: ${isVariable ? "100 900" : "400"};`,
498
+ ` src: url(${font.fontFaceUrl}) format('${fontFormat(font.fontFaceUrl)}');`,
499
+ "}"
500
+ ].join("\n");
501
+ document.head.append(style);
502
+ return () => style.remove();
503
+ }
504
+ return () => void 0;
505
+ }
506
+
507
+ // src/embed/provider.tsx
508
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
509
+ var REMOTE_CONFIG_PAINT_BUDGET_MS = 600;
510
+ var RUNTIME_PREFETCH_IDLE_TIMEOUT_MS = 1200;
511
+ var embedRuntimePromise;
512
+ function loadEmbedRuntime() {
513
+ embedRuntimePromise ??= import(
514
+ /* webpackPrefetch: true */
515
+ "./embed-runtime-XOPQY7Z5.js"
516
+ ).then(
517
+ (module) => module.default
518
+ );
519
+ return embedRuntimePromise;
520
+ }
521
+ var controllers = /* @__PURE__ */ new Map();
522
+ function subscribeToColorScheme(onChange) {
523
+ const media = window.matchMedia?.("(prefers-color-scheme: dark)");
524
+ media?.addEventListener?.("change", onChange);
525
+ return () => media?.removeEventListener?.("change", onChange);
526
+ }
527
+ function prefersDarkColorScheme() {
528
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches === true;
529
+ }
530
+ function dispatch(agentId, method) {
531
+ const controller = agentId ? controllers.get(agentId) : [...controllers.values()][controllers.size - 1];
532
+ if (!controller) {
533
+ console.warn("HeroUI Agent is not mounted yet. Add <HeroUIAgent /> to your layout.");
534
+ return;
535
+ }
536
+ controller[method]();
537
+ }
538
+ function useRemoteConfig(agentId, apiBaseUrl, enabled) {
539
+ const [state, setState] = useState2(() => {
540
+ if (!enabled || typeof window === "undefined") return { config: null, resolved: false };
541
+ const cached = readCachedRemoteConfig(agentId);
542
+ return { config: cached, resolved: Boolean(cached) };
543
+ });
544
+ useEffect2(() => {
545
+ if (!enabled) return;
546
+ const cached = readCachedRemoteConfig(agentId);
547
+ const controller = new AbortController();
548
+ const budget = window.setTimeout(
549
+ () => setState((current) => ({ ...current, resolved: true })),
550
+ REMOTE_CONFIG_PAINT_BUDGET_MS
551
+ );
552
+ void fetchRemoteConfig(apiBaseUrl, agentId, {
553
+ ...cached?.revision ? { revision: cached.revision } : {},
554
+ signal: controller.signal
555
+ }).then((result) => {
556
+ if (controller.signal.aborted) return;
557
+ if (result.status === "ok") {
558
+ setState({ config: result.config, resolved: true });
559
+ writeCachedRemoteConfig(agentId, result.config);
560
+ }
561
+ if (result.status === "missing") {
562
+ setState({ config: null, resolved: true });
563
+ clearCachedRemoteConfig(agentId);
564
+ }
565
+ }).finally(() => {
566
+ if (controller.signal.aborted) return;
567
+ window.clearTimeout(budget);
568
+ setState((current) => ({ ...current, resolved: true }));
569
+ });
570
+ return () => {
571
+ controller.abort();
572
+ window.clearTimeout(budget);
573
+ };
574
+ }, [apiBaseUrl, enabled, agentId]);
575
+ return enabled ? state : { config: null, resolved: true };
576
+ }
577
+ function useAgent(agentId) {
578
+ return useMemo(
579
+ () => ({
580
+ hide: () => dispatch(agentId, "hide"),
581
+ newConversation: () => dispatch(agentId, "newConversation"),
582
+ preload: () => dispatch(agentId, "preload"),
583
+ refreshAuth: () => dispatch(agentId, "refreshAuth"),
584
+ show: () => dispatch(agentId, "show"),
585
+ shutdown: () => dispatch(agentId, "shutdown"),
586
+ toggle: () => dispatch(agentId, "toggle")
587
+ }),
588
+ [agentId]
589
+ );
590
+ }
591
+ function HeroUIAgent(props) {
592
+ const {
593
+ _api,
594
+ agentId,
595
+ appearance,
596
+ capabilities,
597
+ componentExports,
598
+ composer,
599
+ context,
600
+ getAuthToken,
601
+ markdown,
602
+ onFeedback,
603
+ permissions,
604
+ preload: preloadEnabled,
605
+ remoteConfig: remoteConfigEnabled = true,
606
+ responseActions,
607
+ showLauncher,
608
+ startScreen,
609
+ tools
610
+ } = props;
611
+ const markdownPlugins = markdown?.plugins;
612
+ const apiBaseUrl = (_api?.baseUrl ?? "https://api.heroui.com").replace(/\/$/, "");
613
+ const { config: remoteConfig, resolved: remoteConfigResolved } = useRemoteConfig(
614
+ agentId,
615
+ apiBaseUrl,
616
+ remoteConfigEnabled
617
+ );
618
+ const serializedConfig = JSON.stringify({
619
+ _api,
620
+ appearance,
621
+ capabilities,
622
+ componentExports,
623
+ composer,
624
+ markdown: markdown ? { ...markdown, plugins: void 0 } : void 0,
625
+ permissions,
626
+ preload: preloadEnabled,
627
+ remote: remoteConfig,
628
+ responseActions,
629
+ showLauncher,
630
+ startScreen
631
+ });
632
+ const options = useMemo(() => {
633
+ const { remote, ...config } = JSON.parse(serializedConfig);
634
+ const merged = mergeRemoteConfig(remote, {
635
+ ...config,
636
+ agentId,
637
+ getAuthToken
638
+ });
639
+ return resolveOptions({
640
+ ...merged,
641
+ context,
642
+ markdown: merged.markdown ? { ...merged.markdown, plugins: markdownPlugins } : markdownPlugins ? { plugins: markdownPlugins } : void 0,
643
+ onFeedback,
644
+ tools
645
+ });
646
+ }, [context, getAuthToken, markdownPlugins, onFeedback, agentId, serializedConfig, tools]);
647
+ const hostFont = hostSuppliesFont(props);
648
+ const remoteWebfont = remoteConfig?.webfont;
649
+ useEffect2(() => {
650
+ if (hostFont) return;
651
+ return loadAgentWebfont(remoteWebfont);
652
+ }, [hostFont, remoteWebfont]);
653
+ const [isOpen, setIsOpen] = useState2(false);
654
+ const [conversationEpoch, setConversationEpoch] = useState2(1);
655
+ const [identityEpoch, setIdentityEpoch] = useState2(1);
656
+ const [identityReset, setIdentityReset] = useState2(false);
657
+ const [requestedConversationId, setRequestedConversationId] = useState2(
658
+ void 0
659
+ );
660
+ const [portalRoot, setPortalRoot] = useState2(null);
661
+ const [EmbedRuntime, setEmbedRuntime] = useState2(null);
662
+ const conversationEpochRef = useRef2(1);
663
+ const launcherRef = useRef2(null);
664
+ const portalRootRef = useRef2(null);
665
+ const readyEpochRef = useRef2(null);
666
+ const shouldOpenRef = useRef2(false);
667
+ const hostRef = useRef2(null);
668
+ const embedReactRootRef = useRef2(null);
669
+ const rootPropertyNamesRef = useRef2([]);
670
+ const prefersDark = useSyncExternalStore(
671
+ subscribeToColorScheme,
672
+ prefersDarkColorScheme,
673
+ () => false
674
+ );
675
+ const designThemeClass = options.designTheme === "base" ? null : `${options.designTheme}-${options.colorScheme === "system" ? prefersDark ? "dark" : "light" : options.colorScheme}`;
676
+ const [shouldLoadRuntime, setShouldLoadRuntime] = useState2(false);
677
+ const requestRuntime = useCallback(() => setShouldLoadRuntime(true), []);
678
+ const [preloadRequested, setPreloadRequested] = useState2(false);
679
+ const shouldWarmSession = options.preload || preloadRequested;
680
+ useEffect2(() => {
681
+ if (!options.preload || shouldLoadRuntime) return;
682
+ return scheduleIdleTask(requestRuntime, RUNTIME_PREFETCH_IDLE_TIMEOUT_MS);
683
+ }, [options.preload, requestRuntime, shouldLoadRuntime]);
684
+ useEffect2(() => {
685
+ if (!shouldLoadRuntime) return;
686
+ let active = true;
687
+ void loadEmbedRuntime().then((runtime) => {
688
+ if (active) setEmbedRuntime(() => runtime);
689
+ }).catch(() => void 0);
690
+ return () => {
691
+ active = false;
692
+ };
693
+ }, [shouldLoadRuntime]);
694
+ useEffect2(() => {
695
+ if (!isOpen || options.viewMode !== "sidebar") return;
696
+ if (window.innerWidth < 640) return;
697
+ const target = document.documentElement;
698
+ const previousMargin = target.style.marginRight;
699
+ const previousTransition = target.style.transition;
700
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
701
+ const width = Math.min(readSidebarWidth(options.agentId, window.innerWidth), window.innerWidth);
702
+ if (!reducedMotion) {
703
+ target.style.transition = [previousTransition, "margin-right .28s cubic-bezier(.2,.8,.2,1)"].filter(Boolean).join(", ");
704
+ }
705
+ target.style.marginRight = `${width}px`;
706
+ return () => {
707
+ target.style.marginRight = previousMargin;
708
+ window.setTimeout(() => {
709
+ target.style.transition = previousTransition;
710
+ }, 300);
711
+ };
712
+ }, [isOpen, options.agentId, options.viewMode]);
713
+ useEffect2(() => {
714
+ const host = document.createElement("div");
715
+ const root = document.createElement("div");
716
+ host.dataset["herouiAgent"] = options.agentId;
717
+ root.className = "ha-root";
718
+ hostRef.current = host;
719
+ portalRootRef.current = root;
720
+ embedReactRootRef.current = createRoot(root);
721
+ host.append(root);
722
+ document.body.append(host);
723
+ let active = true;
724
+ queueMicrotask(() => {
725
+ if (active) setPortalRoot(root);
726
+ });
727
+ return () => {
728
+ active = false;
729
+ const reactRoot = embedReactRootRef.current;
730
+ portalRootRef.current = null;
731
+ hostRef.current = null;
732
+ embedReactRootRef.current = null;
733
+ rootPropertyNamesRef.current = [];
734
+ queueMicrotask(() => {
735
+ reactRoot?.unmount();
736
+ host.remove();
737
+ });
738
+ };
739
+ }, [options.agentId]);
740
+ useLayoutEffect(() => {
741
+ const root = portalRootRef.current;
742
+ const host = hostRef.current;
743
+ if (!root || !host) return;
744
+ root.className = designThemeClass ? `ha-root ${designThemeClass}` : "ha-root";
745
+ root.dataset["theme"] = options.colorScheme;
746
+ root.dataset["viewMode"] = options.viewMode;
747
+ root.dataset["position"] = options.launcherPosition;
748
+ host.dataset["open"] = isOpen ? "true" : "false";
749
+ host.dataset["viewMode"] = options.viewMode;
750
+ host.dataset["position"] = options.launcherPosition;
751
+ if (options.launcherOffset) {
752
+ host.style.setProperty("--ha-launcher-x", `${options.launcherOffset.x}px`);
753
+ host.style.setProperty("--ha-launcher-y", `${options.launcherOffset.y}px`);
754
+ } else {
755
+ host.style.removeProperty("--ha-launcher-x");
756
+ host.style.removeProperty("--ha-launcher-y");
757
+ }
758
+ if (options.viewMode === "sidebar") {
759
+ host.style.setProperty(
760
+ "--ha-sidebar-width",
761
+ `${readSidebarWidth(options.agentId, window.innerWidth)}px`
762
+ );
763
+ }
764
+ const rootProperties = { ...options.themeStyle, ...options.panelStyle };
765
+ for (const property of rootPropertyNamesRef.current) {
766
+ if (!(property in rootProperties)) root.style.removeProperty(property);
767
+ }
768
+ for (const [property, value] of Object.entries(rootProperties)) {
769
+ root.style.setProperty(property, value);
770
+ }
771
+ rootPropertyNamesRef.current = Object.keys(rootProperties);
772
+ }, [
773
+ designThemeClass,
774
+ isOpen,
775
+ options.colorScheme,
776
+ options.launcherOffset,
777
+ options.launcherPosition,
778
+ options.panelStyle,
779
+ options.themeStyle,
780
+ options.viewMode,
781
+ portalRoot
782
+ ]);
783
+ const prepareNextConversation = useCallback((conversationId) => {
784
+ readyEpochRef.current = null;
785
+ setRequestedConversationId(conversationId);
786
+ setConversationEpoch((epoch) => {
787
+ const nextEpoch = epoch + 1;
788
+ conversationEpochRef.current = nextEpoch;
789
+ return nextEpoch;
790
+ });
791
+ }, []);
792
+ const handleRuntimeReady = useCallback((readyEpoch) => {
793
+ if (conversationEpochRef.current !== readyEpoch) return;
794
+ readyEpochRef.current = readyEpoch;
795
+ if (shouldOpenRef.current) setIsOpen(true);
796
+ }, []);
797
+ const hide = useCallback(() => {
798
+ shouldOpenRef.current = false;
799
+ setIsOpen(false);
800
+ prepareNextConversation();
801
+ requestAnimationFrame(() => launcherRef.current?.focus());
802
+ }, [prepareNextConversation]);
803
+ const show = useCallback(() => {
804
+ shouldOpenRef.current = true;
805
+ requestRuntime();
806
+ setIsOpen(true);
807
+ }, [requestRuntime]);
808
+ const preload = useCallback(() => {
809
+ setPreloadRequested(true);
810
+ requestRuntime();
811
+ }, [requestRuntime]);
812
+ const newConversation = useCallback(() => {
813
+ shouldOpenRef.current = true;
814
+ requestRuntime();
815
+ prepareNextConversation(null);
816
+ }, [prepareNextConversation, requestRuntime]);
817
+ const selectConversation = useCallback(
818
+ (conversationId) => {
819
+ shouldOpenRef.current = true;
820
+ requestRuntime();
821
+ prepareNextConversation(conversationId);
822
+ },
823
+ [prepareNextConversation, requestRuntime]
824
+ );
825
+ const toggle = useCallback(() => {
826
+ if (isOpen || shouldOpenRef.current) hide();
827
+ else show();
828
+ }, [hide, isOpen, show]);
829
+ const refreshAuth = useCallback(() => {
830
+ setIdentityReset(false);
831
+ setIdentityEpoch((epoch) => epoch + 1);
832
+ }, []);
833
+ const shutdown = useCallback(() => {
834
+ const prefixes = [
835
+ `heroui-agent:anonymous:${options.agentId}`,
836
+ `heroui-agent:conversation:${options.agentId}:`,
837
+ `heroui-agent:history:${options.agentId}:`,
838
+ `heroui-agent:session:${options.agentId}:`
839
+ ];
840
+ for (let index = localStorage.length - 1; index >= 0; index -= 1) {
841
+ const key = localStorage.key(index);
842
+ if (key && prefixes.some((prefix) => key.startsWith(prefix))) localStorage.removeItem(key);
843
+ }
844
+ shouldOpenRef.current = false;
845
+ setIsOpen(false);
846
+ setIdentityReset(true);
847
+ setIdentityEpoch((epoch) => epoch + 1);
848
+ prepareNextConversation();
849
+ window.setTimeout(() => {
850
+ void clearAgentComposerDraftsForProject(options.agentId);
851
+ }, 0);
852
+ }, [options.agentId, prepareNextConversation]);
853
+ useEffect2(() => {
854
+ const controller = {
855
+ hide,
856
+ newConversation,
857
+ preload,
858
+ refreshAuth,
859
+ show,
860
+ shutdown,
861
+ toggle
862
+ };
863
+ controllers.set(options.agentId, controller);
864
+ return () => {
865
+ if (controllers.get(options.agentId) === controller) {
866
+ controllers.delete(options.agentId);
867
+ }
868
+ };
869
+ }, [hide, newConversation, options.agentId, preload, refreshAuth, show, shutdown, toggle]);
870
+ useLayoutEffect(() => {
871
+ const reactRoot = embedReactRootRef.current;
872
+ if (!portalRoot || !reactRoot) return;
873
+ reactRoot.render(
874
+ /* @__PURE__ */ jsxs2(Fragment2, { children: [
875
+ options.showLauncher && remoteConfigResolved && !isOpen ? /* @__PURE__ */ jsx4(
876
+ "button",
877
+ {
878
+ ref: launcherRef,
879
+ "aria-expanded": false,
880
+ "aria-haspopup": "dialog",
881
+ "aria-label": "Open data assistant",
882
+ className: "ha-launcher",
883
+ style: options.launcherStyle,
884
+ type: "button",
885
+ onClick: show,
886
+ onFocus: options.preload ? requestRuntime : void 0,
887
+ onPointerEnter: options.preload ? requestRuntime : void 0,
888
+ children: /* @__PURE__ */ jsx4(LauncherIcon, { icon: options.launcherIcon })
889
+ }
890
+ ) : null,
891
+ EmbedRuntime ? /* @__PURE__ */ jsx4(
892
+ EmbedRuntime,
893
+ {
894
+ conversationEpoch,
895
+ identityEpoch,
896
+ identityReset,
897
+ open: isOpen,
898
+ options,
899
+ requestedConversationId,
900
+ warmSession: shouldWarmSession,
901
+ onClose: hide,
902
+ onNewConversation: newConversation,
903
+ onReady: handleRuntimeReady,
904
+ onSelectConversation: selectConversation
905
+ }
906
+ ) : /* @__PURE__ */ jsx4(
907
+ PanelShell,
908
+ {
909
+ defaultExpanded: options.panelExpanded,
910
+ expandable: options.panelExpandable,
911
+ modal: options.viewMode !== "sidebar",
912
+ name: "Data assistant",
913
+ open: isOpen,
914
+ agentId: options.agentId,
915
+ onClose: hide,
916
+ children: /* @__PURE__ */ jsx4(
917
+ ShellConversation,
918
+ {
919
+ greeting: options.greeting,
920
+ placeholder: options.composerPlaceholder,
921
+ agentId: options.agentId,
922
+ suggestedPrompts: options.suggestedPrompts ?? [],
923
+ onIntent: requestRuntime
924
+ }
925
+ )
926
+ }
927
+ )
928
+ ] })
929
+ );
930
+ }, [
931
+ EmbedRuntime,
932
+ conversationEpoch,
933
+ handleRuntimeReady,
934
+ hide,
935
+ identityEpoch,
936
+ identityReset,
937
+ isOpen,
938
+ newConversation,
939
+ options,
940
+ portalRoot,
941
+ remoteConfigResolved,
942
+ requestRuntime,
943
+ requestedConversationId,
944
+ selectConversation,
945
+ shouldWarmSession,
946
+ show
947
+ ]);
948
+ return null;
949
+ }
950
+
951
+ export {
952
+ useAgent,
953
+ HeroUIAgent
954
+ };