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