AkurAI Build
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;