Menu
Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8
AkurAI-Framework / site/frontend/ai/runtime/registry.js
Latest change 22a320a8e40a1822b386e2d5fd9bcf3537282876 - feat(ai): generative-UI / tool-call runtime + primitives by Ólafur Búi Ólafsson
// registry.js — the generative-UI component registry + dispatcher.
//
// Native ES module. No bundler, no transpiler, no dependencies — runs as-is in
// any modern browser. This is the engine behind "tool-ui": it maps a tool-call
// payload to a DOM component and renders it. Component authors register their
// components against the contract pinned below; everything else conforms to it.
//
// ── THE COMPONENT CONTRACT (pinned — do not vary) ────────────────────────────
//
// def = {
// name: string, // unique id, e.g. "weather-card"
// category: string, // grouping, e.g. "data" | "form"
// title: string, // human label for catalogs/docs
// match: (payload) => boolean, // true if this def renders payload
// render: (payload, ctx) => HTMLElement,// build & return the DOM node
// example: any, // sample payload (docs/demos/tests)
// }
//
// ── THE CTX SHAPE (passed to every render) ───────────────────────────────────
//
// ctx = {
// sendReceipt(obj), // interactive components call this to reply to
// // the assistant (see receipts.js)
// onAction(name, data), // optional generic action callback (clicks etc.)
// theme, // "dark" (default) | "light" | author value
// }
//
// ── DISPATCH ─────────────────────────────────────────────────────────────────
//
// renderToolPayload(payload, ctx) walks registered defs in registration order
// and returns `def.render(payload, ctx)` for the FIRST def whose match()
// returns true. If none match (or a render throws), it FAILS SAFE to a `.card`
// showing the pretty-printed JSON in a `.code-block`. The UI never crashes.
// First-registered-wins ordering is intentional: register specific components
// before generic ones. A Set-backed Map keeps insertion order and dedupes names.
const registry = new Map();
// Normalize ctx so components can always call ctx.sendReceipt / ctx.onAction
// without guarding for undefined.
function normalizeCtx(ctx) {
const base = ctx || {};
return {
sendReceipt:
typeof base.sendReceipt === "function"
? base.sendReceipt
: (obj) => {
// No transport wired in: surface it as a DOM event so something can
// still observe the receipt. receipts.js provides the real one.
console.log("[toolui] sendReceipt (no transport)", obj);
document.dispatchEvent(
new CustomEvent("toolui:receipt", { detail: obj })
);
},
onAction:
typeof base.onAction === "function"
? base.onAction
: (name, data) => console.log("[toolui] action:", name, data),
theme: base.theme || "dark",
};
}
// Validate a def against the pinned contract. Returns an error string or null.
function validateDef(def) {
if (!def || typeof def !== "object") return "def must be an object";
if (typeof def.name !== "string" || !def.name) return "def.name (string) required";
if (typeof def.match !== "function") return "def.match (function) required";
if (typeof def.render !== "function") return "def.render (function) required";
return null;
}
// Register one component. Re-registering the same name replaces it (last write
// wins for that name) but keeps its original position in the dispatch order.
function registerToolComponent(def) {
const err = validateDef(def);
if (err) {
console.warn(`[toolui] ignoring invalid component: ${err}`, def);
return false;
}
const full = {
name: def.name,
category: def.category || "uncategorized",
title: def.title || def.name,
match: def.match,
render: def.render,
example: def.example,
};
const replacing = registry.has(def.name);
registry.set(def.name, full);
console.log(
`[toolui] ${replacing ? "replaced" : "registered"} component "${def.name}" (${full.category})`
);
return true;
}
// Register many at once. Returns the count successfully registered.
function registerAll(defs) {
if (!Array.isArray(defs)) {
console.warn("[toolui] registerAll expects an array");
return 0;
}
let n = 0;
for (const def of defs) if (registerToolComponent(def)) n++;
return n;
}
// Build the fail-safe fallback: a card with the pretty-printed JSON payload.
// Used when nothing matches or a component's render throws.
function renderFallback(payload, reason) {
const card = document.createElement("div");
card.className = "card toolui-fallback";
const head = document.createElement("h3");
head.textContent = "Unrendered tool payload";
card.appendChild(head);
if (reason) {
const note = document.createElement("p");
note.className = "muted small";
note.textContent = reason;
card.appendChild(note);
}
const block = document.createElement("div");
block.className = "code-block";
block.style.marginTop = "12px";
const bar = document.createElement("div");
bar.className = "code-block-bar";
const nm = document.createElement("span");
nm.className = "code-block-name";
nm.textContent = "payload.json";
bar.appendChild(nm);
block.appendChild(bar);
const pre = document.createElement("pre");
const code = document.createElement("code");
let text;
try {
text = JSON.stringify(payload, null, 2);
} catch {
text = String(payload);
}
code.textContent = text;
pre.appendChild(code);
block.appendChild(pre);
card.appendChild(block);
return card;
}
// THE DISPATCHER. Pick the first matching def and render it; fail safe on no
// match or on a thrown render. Always returns an HTMLElement.
function renderToolPayload(payload, ctx) {
const safeCtx = normalizeCtx(ctx);
for (const def of registry.values()) {
let hit = false;
try {
hit = !!def.match(payload);
} catch (err) {
console.warn(`[toolui] match() threw in "${def.name}":`, err);
continue;
}
if (!hit) continue;
try {
const el = def.render(payload, safeCtx);
if (el instanceof HTMLElement) {
console.log(`[toolui] rendered with "${def.name}"`);
return el;
}
console.warn(`[toolui] "${def.name}" render() did not return an HTMLElement`);
return renderFallback(payload, `Component "${def.name}" returned no element.`);
} catch (err) {
console.warn(`[toolui] render() threw in "${def.name}":`, err);
return renderFallback(payload, `Component "${def.name}" failed to render.`);
}
}
console.log("[toolui] no component matched — JSON fail-safe");
return renderFallback(payload, "No registered component matched this payload.");
}
// Introspection helpers (catalogs, demos, tests).
function getRegistered() {
return Array.from(registry.values());
}
function clearRegistry() {
registry.clear();
console.log("[toolui] registry cleared");
}
const ToolUiRegistry = {
registerToolComponent,
registerAll,
renderToolPayload,
getRegistered,
clearRegistry,
normalizeCtx,
version: "1.0.0",
};
if (typeof window !== "undefined") window.ToolUiRegistry = ToolUiRegistry;
export {
registerToolComponent,
registerAll,
renderToolPayload,
getRegistered,
clearRegistry,
normalizeCtx,
};
export default ToolUiRegistry;