@adonis-agora/agent
Version:
Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.
187 lines • 7.21 kB
JavaScript
import { useEffect, useMemo, useRef, useState } from 'react';
import { collectHostThemeVars, hostModeAttributes, hostThemeCss, isHostDark, tailwindThemeCss, watchHostTheme, } from '../../../genui/sandbox-theme.js';
import { useAgentConfig } from '../config/use-agent-config.js';
/** The event the Vite plugin's HMR bridge dispatches on `window` when the kit was rebuilt (dev). */
export const SANDBOX_KIT_UPDATE_EVENT = 'genui-sandbox-kit:update';
const fetched = new Map();
function fromUrl(url) {
let pending = fetched.get(url);
if (pending === undefined) {
pending = fetch(url, { credentials: 'same-origin', headers: { accept: 'application/json' } })
.then((response) => (response.ok ? response.json() : undefined))
.then((body) => {
const record = body;
if (record?.genui?.sandbox !== undefined)
return record.genui.sandbox;
return typeof body?.theme === 'boolean'
? body
: undefined;
})
.catch(() => {
fetched.delete(url);
return undefined;
});
fetched.set(url, pending);
}
return pending;
}
/** The sandbox's client config — `undefined` while it loads. */
export function useSandboxClientConfig(source) {
const agent = useAgentConfig({ enabled: source === undefined });
const [fromSource, setFromSource] = useState(undefined);
const [loadingUrl, setLoadingUrl] = useState(typeof source === 'string');
useEffect(() => {
if (typeof source !== 'string')
return;
let cancelled = false;
setLoadingUrl(true);
void fromUrl(source).then((config) => {
if (cancelled)
return;
setFromSource(config);
setLoadingUrl(false);
});
return () => {
cancelled = true;
};
}, [source]);
// The agent's config is pending until it answers or fails — or, for a backend that never will
// (no `getConfig`), for a few seconds.
const [gaveUp, setGaveUp] = useState(false);
const pending = source === undefined && agent.config === undefined && agent.error === null;
useEffect(() => {
if (!pending)
return;
const timer = setTimeout(() => setGaveUp(true), 4000);
return () => clearTimeout(timer);
}, [pending]);
if (source === false)
return { config: { theme: true }, loading: false };
if (typeof source === 'object')
return { config: source, loading: false };
if (typeof source === 'string')
return { config: fromSource, loading: loadingUrl };
const loading = pending && !gaveUp;
return {
config: agent.config?.genui?.sandbox ?? (loading ? undefined : { theme: true }),
loading,
};
}
const assets = new Map();
/** The kit url the last HMR update named (dev), per kit name (`''`: the unnamed kit): newer than what `/config` said. */
const kitUrlOverride = new Map();
/** The same for the compiled styles. */
const cssUrlOverride = new Map();
let kitVersion = 0;
const kitListeners = new Set();
if (typeof window !== 'undefined') {
window.addEventListener(SANDBOX_KIT_UPDATE_EVENT, (event) => {
const detail = event.detail;
const name = typeof detail?.name === 'string' ? detail.name : '';
if (typeof detail?.url === 'string')
kitUrlOverride.set(name, detail.url);
if (typeof detail?.css === 'string')
cssUrlOverride.set(name, detail.css);
kitVersion += 1;
for (const notify of kitListeners)
notify();
});
}
/** Fetch an asset's text once per url (same origin), shared by every sandbox on the page. */
export function loadSandboxAsset(url) {
let pending = assets.get(url);
if (pending === undefined) {
pending = fetch(url, { credentials: 'same-origin' })
.then((response) => (response.ok ? response.text() : null))
.catch(() => null)
.then((text) => {
if (text === null)
assets.delete(url);
return text;
});
assets.set(url, pending);
}
return pending;
}
/**
* An asset's text: `undefined` while it loads, `null` when it could not be had (or no url). The kit
* follows the dev server's rebuilds (`genui-sandbox-kit:update`): a new url, fetched afresh.
*/
export function useSandboxAsset(url, options = {}) {
const [version, setVersion] = useState(kitVersion);
const follows = options.kit === true || options.css === true;
useEffect(() => {
if (!follows)
return;
const notify = () => setVersion(kitVersion);
kitListeners.add(notify);
return () => {
kitListeners.delete(notify);
};
}, [follows]);
const effective = url === undefined
? url
: options.kit === true
? (kitUrlOverride.get(options.name ?? '') ?? url)
: options.css === true
? (cssUrlOverride.get(options.name ?? '') ?? url)
: url;
const [state, setState] = useState({
url: undefined,
text: undefined,
});
// biome-ignore lint/correctness/useExhaustiveDependencies: `version` refetches after a rebuild
useEffect(() => {
if (effective === undefined) {
setState({ url: undefined, text: null });
return;
}
let cancelled = false;
void loadSandboxAsset(effective).then((text) => {
if (!cancelled)
setState({ url: effective, text });
});
return () => {
cancelled = true;
};
}, [effective, version]);
if (effective === undefined)
return null;
return state.url === effective ? state.text : undefined;
}
function readHostTheme() {
if (typeof document === 'undefined')
return null;
const vars = collectHostThemeVars(document);
const dark = isHostDark(document);
const colorScheme = document.defaultView?.getComputedStyle(document.documentElement).colorScheme;
return {
css: hostThemeCss(vars, colorScheme !== undefined ? { colorScheme } : {}),
dark,
attributes: hostModeAttributes(document),
tailwind: tailwindThemeCss(vars),
};
}
/** The host's theme, kept current (class / `data-theme` / color-scheme changes). `null` when off. */
export function useHostTheme(enabled) {
const [theme, setTheme] = useState(() => (enabled ? readHostTheme() : null));
const last = useRef('');
useEffect(() => {
if (!enabled || typeof document === 'undefined') {
setTheme(null);
return;
}
const update = () => {
const next = readHostTheme();
const key = next === null ? '' : `${next.dark}|${JSON.stringify(next.attributes)}|${next.css}`;
if (key === last.current)
return;
last.current = key;
setTheme(next);
};
update();
return watchHostTheme(document, update);
}, [enabled]);
return useMemo(() => theme, [theme]);
}
//# sourceMappingURL=sandbox-assets.js.map