@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
JavaScript
/**
* 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