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
}
};