UNPKG

@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
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