AkurAI Build
Menu

Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8

AkurAI-Framework / site/frontend/ai/runtime/approvals.js

Latest change 22a320a8e40a1822b386e2d5fd9bcf3537282876 - feat(ai): generative-UI / tool-call runtime + primitives by Ólafur Búi Ólafsson

// approvals.js — inline human-in-the-loop approval plumbing.
//
// The polished Approval Card *component* is built by another agent. This module
// is the runtime plumbing underneath it: a generic Approve/Reject prompt that
// resolves a Promise AND fires a receipt, plus a fallback component def the
// dispatcher can use when no purpose-built approval card is registered.
//
// renderApproval(opts, ctx) -> { element, promise }
//   opts = {
//     title, message,                       // copy
//     approveLabel = "Approve",
//     rejectLabel  = "Reject",
//     toolCallId,                           // threaded into the receipt
//     component = "approval",               // receipt.component value
//   }
//   - element: the DOM node to insert
//   - promise: resolves { approved: boolean } after the user chooses; also calls
//     ctx.sendReceipt({ action: "approve"|"reject", ... }).

import { sendReceipt } from "./receipts.js";

function renderApproval(opts, ctx) {
  const o = opts || {};
  const title = o.title || "Approval required";
  const message = o.message || "Do you want to proceed with this action?";
  const approveLabel = o.approveLabel || "Approve";
  const rejectLabel = o.rejectLabel || "Reject";
  const component = o.component || "approval";
  const reply =
    (ctx && typeof ctx.sendReceipt === "function" && ctx.sendReceipt) ||
    sendReceipt;

  const card = document.createElement("div");
  card.className = "card toolui-approval";
  card.setAttribute("role", "group");
  card.setAttribute("aria-label", title);

  const h = document.createElement("h3");
  h.textContent = title;
  card.appendChild(h);

  const p = document.createElement("p");
  p.className = "muted";
  p.textContent = message;
  card.appendChild(p);

  const row = document.createElement("div");
  row.className = "btn-row";
  row.style.marginTop = "16px";

  const approveBtn = document.createElement("button");
  approveBtn.type = "button";
  approveBtn.className = "btn btn-primary btn-sm";
  approveBtn.textContent = approveLabel;

  const rejectBtn = document.createElement("button");
  rejectBtn.type = "button";
  rejectBtn.className = "btn btn-secondary btn-sm";
  rejectBtn.textContent = rejectLabel;

  row.appendChild(approveBtn);
  row.appendChild(rejectBtn);
  card.appendChild(row);

  const status = document.createElement("p");
  status.className = "small muted";
  status.style.marginTop = "12px";
  status.style.display = "none";
  card.appendChild(status);

  let settled = false;
  const promise = new Promise((resolve) => {
    function decide(approved, action) {
      if (settled) return;
      settled = true;
      approveBtn.disabled = true;
      rejectBtn.disabled = true;
      status.style.display = "";
      status.textContent = approved
        ? `Approved — receipt sent.`
        : `Rejected — receipt sent.`;
      card.classList.add(approved ? "is-approved" : "is-rejected");
      console.log(`[runtime] approval ${action}`, { toolCallId: o.toolCallId });
      const receipt = {
        toolCallId: o.toolCallId ?? null,
        component,
        action,
        data: { approved },
      };
      // sendReceipt never throws; fire-and-forget while we resolve.
      Promise.resolve(reply(receipt)).catch(() => {});
      resolve({ approved });
    }
    approveBtn.addEventListener("click", () => decide(true, "approve"));
    rejectBtn.addEventListener("click", () => decide(false, "reject"));
  });

  return { element: card, promise };
}

// A generic fallback approval COMPONENT def. NOT auto-registered — the registry
// is first-match-wins, so auto-registering could shadow a purpose-built card.
// Consumers (or the demo) register it explicitly, ideally LAST, e.g.:
//   registerToolComponent(genericApprovalComponent)
// It matches payloads that smell like an approval request.
const genericApprovalComponent = {
  name: "generic-approval",
  category: "interactive",
  title: "Generic approval prompt",
  match: (payload) =>
    !!payload &&
    typeof payload === "object" &&
    (payload.type === "approval" ||
      payload.kind === "approval" ||
      payload.requiresApproval === true ||
      payload.requires_approval === true),
  render: (payload, ctx) =>
    renderApproval(
      {
        title: payload.title,
        message: payload.message || payload.prompt,
        approveLabel: payload.approveLabel,
        rejectLabel: payload.rejectLabel,
        toolCallId: payload.toolCallId ?? payload.tool_call_id,
        component: "generic-approval",
      },
      ctx
    ).element,
  example: {
    type: "approval",
    title: "Delete 3 records?",
    message: "This permanently removes the selected rows.",
  },
};

const ToolUiApprovals = { renderApproval, genericApprovalComponent };
if (typeof window !== "undefined") window.ToolUiApprovals = ToolUiApprovals;

export { renderApproval, genericApprovalComponent };
export default ToolUiApprovals;