AkurAI Build
Menu

Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f

AkurAI-Framework / site/frontend/ai/components/input/option_list.js

Latest change 63ae119f1bdd5ddffd3f9a2b3b3216aa9d21449b - feat(ai): Input component batch — option_list, parameter_slider, preferences_panel, question_flow by Ólafur Búi Ólafsson

/**
 * Option List — tool-UI input component.
 * Payload: { prompt, options: [{id, label, description?}], multi? }
 * Receipt: { component: 'option_list', selected: string | string[] | null }
 */

function emit(el, detail) {
  el.dispatchEvent(new CustomEvent('toolui:input', { bubbles: true, detail }));
}

export const def = {
  name: 'option_list',
  category: 'Input',
  title: 'Option List',

  match(p) {
    return p != null && Array.isArray(p.options) && p.options.length > 0;
  },

  render(p, ctx) {
    const { prompt = '', options = [], multi = false } = p;
    const selected = new Set();
    // Unique name so multiple instances on the same page don't collide.
    const radioName = 'tc-ol-' + Math.random().toString(36).slice(2);

    const root = document.createElement('div');
    root.className = 'card';
    root.style.cssText = 'display:grid;gap:14px;max-width:480px;';

    if (prompt) {
      const h = document.createElement('p');
      h.style.cssText = 'margin:0;font-weight:600;font-size:15px;';
      h.textContent = prompt;
      root.appendChild(h);
    }

    const list = document.createElement('div');
    list.style.cssText = 'display:grid;gap:8px;';
    root.appendChild(list);

    options.forEach(opt => {
      const row = document.createElement('label');
      row.style.cssText =
        'display:flex;align-items:flex-start;gap:10px;padding:10px 12px;' +
        'border:1px solid var(--border);border-radius:10px;cursor:pointer;' +
        'transition:border-color .15s,background .15s;';

      const inp = document.createElement('input');
      inp.type = multi ? 'checkbox' : 'radio';
      inp.name = radioName;
      inp.value = String(opt.id);
      inp.style.marginTop = '3px';
      inp.style.flexShrink = '0';
      row.appendChild(inp);

      const textWrap = document.createElement('span');
      const labelEl = document.createElement('strong');
      labelEl.style.fontWeight = '600';
      labelEl.textContent = opt.label;
      textWrap.appendChild(labelEl);

      if (opt.description) {
        const desc = document.createElement('span');
        desc.className = 'muted small';
        desc.style.display = 'block';
        desc.style.marginTop = '2px';
        desc.textContent = opt.description;
        textWrap.appendChild(desc);
      }
      row.appendChild(textWrap);

      function refreshHighlight() {
        if (inp.checked) {
          row.style.borderColor = 'var(--accent)';
          row.style.background = 'color-mix(in srgb,var(--accent) 8%,var(--panel))';
        } else {
          row.style.borderColor = '';
          row.style.background = '';
        }
      }

      inp.addEventListener('change', () => {
        if (!multi) {
          // clear all siblings
          list.querySelectorAll('label').forEach(l => {
            l.style.borderColor = '';
            l.style.background = '';
          });
          selected.clear();
        }
        if (inp.checked) {
          selected.add(opt.id);
        } else {
          selected.delete(opt.id);
        }
        refreshHighlight();
      });

      list.appendChild(row);
    });

    const btn = document.createElement('button');
    btn.className = 'btn btn-primary btn-sm';
    btn.textContent = 'Confirm';

    btn.addEventListener('click', () => {
      const ids = multi ? [...selected] : ([...selected][0] ?? null);
      const receipt = { component: 'option_list', selected: ids };
      if (ctx && typeof ctx.sendReceipt === 'function') ctx.sendReceipt(receipt);
      emit(root, receipt);
      btn.disabled = true;
      btn.textContent = 'Sent ✓';
    });

    root.appendChild(btn);
    return root;
  },

  example: {
    prompt: 'Which deployment target?',
    options: [
      { id: 'bare', label: 'Bare metal', description: 'Full control, no overhead' },
      { id: 'docker', label: 'Docker', description: 'Containerised, portable' },
      { id: 'k8s', label: 'Kubernetes', description: 'Orchestrated at scale' }
    ],
    multi: false
  }
};