Menu
Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f
AkurAI-Framework / site/frontend/ai/components/progress/approval_card.js
Latest change d49e9bb8beb5dc669fe029e034a13342d358a4f3 - feat(ai): tool-ui Progress + Confirmation components (AB4-B) by Ólafur Búi Ólafsson
// Approval Card component — tool-ui Confirmation category.
// Payload: { title, summary, details?, danger? }
// Receipt: { approved: boolean }
// Emits: CustomEvent('toolui:confirm', { detail: { approved: boolean }, bubbles: true })
function emit(el, detail) {
el.dispatchEvent(new CustomEvent('toolui:confirm', { detail, bubbles: true }));
}
export const def = {
name: 'approval_card',
category: 'Confirmation',
title: 'Approval Card',
match(payload) {
return payload?.summary !== undefined && payload?.title !== undefined;
},
render(payload, ctx) {
const isDanger = Boolean(payload.danger);
const root = document.createElement('div');
root.className = isDanger ? 'card alert alert-danger tc-progress-approval' : 'card tc-progress-approval';
root.style.cssText = 'display:flex;flex-direction:column;gap:12px;';
// Title
const title = document.createElement('h3');
title.style.cssText = 'margin:0;font-size:16px;display:flex;align-items:center;gap:8px;';
if (isDanger) {
const warn = document.createElement('span');
warn.setAttribute('aria-hidden', 'true');
warn.textContent = '⚠';
warn.style.color = 'var(--danger)';
title.appendChild(warn);
}
const titleText = document.createTextNode(payload.title);
title.appendChild(titleText);
root.appendChild(title);
// Summary
const summary = document.createElement('p');
summary.style.cssText = 'margin:0;font-size:14px;color:var(--muted);';
summary.textContent = payload.summary;
root.appendChild(summary);
// Optional details
if (payload.details) {
const details = document.createElement('p');
details.style.cssText = 'margin:0;font-size:13px;color:var(--dim);border-top:1px solid var(--border);padding-top:10px;';
details.textContent = payload.details;
root.appendChild(details);
}
// Buttons
const actions = document.createElement('div');
actions.className = 'btn-row';
actions.style.marginTop = '4px';
let decided = false;
const approve = document.createElement('button');
approve.type = 'button';
approve.className = isDanger ? 'btn btn-primary' : 'btn btn-primary';
if (isDanger) {
approve.style.background = 'linear-gradient(180deg, var(--danger), #cc3333)';
approve.style.boxShadow = '0 10px 26px -12px var(--danger)';
}
approve.textContent = 'Approve';
const reject = document.createElement('button');
reject.type = 'button';
reject.className = 'btn btn-secondary';
reject.textContent = 'Reject';
function decide(approved) {
if (decided) return;
decided = true;
approve.disabled = true;
reject.disabled = true;
const detail = { approved };
if (ctx?.sendReceipt) ctx.sendReceipt(detail);
emit(root, detail);
// Visual feedback
const chosen = approved ? approve : reject;
chosen.style.opacity = '1';
const other = approved ? reject : approve;
other.style.opacity = '0.35';
const badge = document.createElement('span');
badge.className = approved ? 'badge badge-ok' : 'badge badge-muted';
badge.style.marginLeft = '8px';
badge.textContent = approved ? 'Approved' : 'Rejected';
chosen.appendChild(badge);
}
approve.addEventListener('click', () => decide(true));
reject.addEventListener('click', () => decide(false));
actions.appendChild(approve);
actions.appendChild(reject);
root.appendChild(actions);
return root;
},
example: {
title: 'Deploy to production?',
summary: 'This will restart the server and apply the pending migration.',
details: 'Estimated downtime: ~30 seconds. 3 migrations will run.',
danger: true,
},
};