UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

153 lines • 5.59 kB
/** * The theme / Tailwind / kit settings and assets of a React Native sandbox. A web host reads them * from its own origin (`GET /agent/config`, then the asset urls it names); an app has no origin, so * it passes them in, or names the agent by an absolute `baseUrl` — relative urls resolve against it. */ import { useEffect, useState } from 'react'; import { resolveSandboxUrl } from './sandbox-host.js'; /** Largest config or asset text accepted (the kit bundle is the big one). */ const MAX_ASSET_CHARS = 8 * 1024 * 1024; function defaultFetch() { const f = globalThis.fetch; return typeof f === 'function' ? f : undefined; } function toConfig(body) { if (typeof body !== 'object' || body === null) return undefined; const record = body; const sandbox = typeof record.genui?.sandbox === 'object' && record.genui.sandbox !== null ? record.genui.sandbox : typeof record.theme === 'boolean' ? record : undefined; if (sandbox === undefined) return undefined; const config = { theme: sandbox.theme !== false }; const tailwind = sandbox.tailwind; if (typeof tailwind === 'object' && tailwind !== null && typeof tailwind.url === 'string') { config.tailwind = { url: tailwind.url, ...(typeof tailwind.css === 'string' ? { css: tailwind.css } : {}), }; } const kit = sandbox.kit; if (typeof kit === 'object' && kit !== null && typeof kit.url === 'string') { config.kit = { url: kit.url }; } const css = sandbox.css; if (typeof css === 'object' && css !== null && typeof css.url === 'string') { config.css = { url: css.url }; } return config; } const THEME_ONLY = Object.freeze({ theme: true }); const texts = new Map(); /** Fetch a text once per url, shared by every sandbox in the app; a failure is forgotten. */ export function loadNativeSandboxText(url, fetchImpl = defaultFetch(), init = { credentials: 'omit' }) { if (fetchImpl === undefined) return Promise.resolve(null); let pending = texts.get(url); if (pending === undefined) { pending = Promise.resolve() .then(() => fetchImpl(url, init)) .then((response) => (response.ok ? response.text() : null)) .then((text) => (typeof text === 'string' && text.length <= MAX_ASSET_CHARS ? text : null)) .catch(() => null) .then((text) => { if (text === null) texts.delete(url); return text; }); texts.set(url, pending); } return pending; } async function loadConfig(source, baseUrl, fetchImpl) { if (typeof source === 'function') { try { const loaded = await source(); return typeof loaded === 'object' && loaded !== null ? loaded : undefined; } catch { return undefined; } } const url = typeof source === 'string' ? resolveSandboxUrl(source, baseUrl) : baseUrl !== undefined ? resolveSandboxUrl(`${baseUrl.replace(/\/+$/, '')}/config`, undefined) : undefined; if (url === undefined) return undefined; const text = await loadNativeSandboxText(url, fetchImpl, { headers: { accept: 'application/json' }, }); if (text === null) return undefined; try { return toConfig(JSON.parse(text)); } catch { return undefined; } } /** The sandbox's config: `undefined` (and `loading`) until it is known. */ export function useNativeSandboxConfig(source, baseUrl, fetchImpl) { const immediate = source === false || (source === undefined && baseUrl === undefined) ? THEME_ONLY : typeof source === 'object' ? source : undefined; const [state, setState] = useState({ config: undefined, done: false, }); useEffect(() => { if (immediate !== undefined) return; let cancelled = false; setState({ config: undefined, done: false }); void loadConfig(source, baseUrl, fetchImpl).then((config) => { if (!cancelled) setState({ config: config ?? THEME_ONLY, done: true }); }); return () => { cancelled = true; }; }, [immediate, source, baseUrl, fetchImpl]); if (immediate !== undefined) return { config: immediate, loading: false }; return { config: state.config, loading: !state.done }; } /** * An asset's text: inline if given, else fetched from its url (resolved against `baseUrl`). * `undefined` while it loads, `null` when it could not be had (or there is none). */ export function useNativeSandboxAsset(asset, baseUrl, fetchImpl) { const inline = asset?.inline; const url = inline === undefined && asset?.url !== undefined ? resolveSandboxUrl(asset.url, baseUrl) : undefined; const [state, setState] = useState({ url: undefined, text: null, }); useEffect(() => { if (url === undefined) return; let cancelled = false; void loadNativeSandboxText(url, fetchImpl).then((text) => { if (!cancelled) setState({ url, text }); }); return () => { cancelled = true; }; }, [url, fetchImpl]); if (typeof inline === 'string') return inline; if (url === undefined) return null; return state.url === url ? state.text : undefined; } //# sourceMappingURL=sandbox-assets.js.map