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;