AkurAI Build
Menu

Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f

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

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

/**
 * Preferences Panel — tool-UI input component.
 * Payload: { groups: [{ label, fields: [{ key, type, label?, value?, options? }] }] }
 *   field.type: 'toggle' | 'select' | 'text'
 *   select fields carry an options array: [{ id, label }] or string[]
 * Receipt: { component: 'preferences_panel', values: { [key]: value } }
 */

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

function buildToggle(field, state) {
  const lbl = document.createElement('label');
  lbl.className = 'switch';
  lbl.style.cssText =
    'justify-content:space-between;padding:10px 12px;' +
    'border:1px solid var(--border);border-radius:10px;';

  const text = document.createElement('span');
  text.className = 'switch-text';
  text.textContent = field.label || field.key;
  lbl.appendChild(text);

  const inp = document.createElement('input');
  inp.type = 'checkbox';
  inp.className = 'switch-input';
  inp.checked = Boolean(field.value);
  inp.addEventListener('change', () => { state[field.key] = inp.checked; });
  lbl.appendChild(inp);

  return lbl;
}

function buildSelect(field, state) {
  const wrapper = document.createElement('div');
  wrapper.className = 'field';

  const lbl = document.createElement('label');
  lbl.style.cssText = 'color:var(--muted);font-size:13px;';
  lbl.textContent = field.label || field.key;
  wrapper.appendChild(lbl);

  const sel = document.createElement('select');
  const opts = field.options || [];
  opts.forEach(opt => {
    const o = document.createElement('option');
    const id = (opt && typeof opt === 'object') ? String(opt.id ?? opt) : String(opt);
    const text = (opt && typeof opt === 'object') ? String(opt.label ?? opt.id ?? opt) : String(opt);
    o.value = id;
    o.textContent = text;
    if (id === String(field.value)) o.selected = true;
    sel.appendChild(o);
  });
  sel.addEventListener('change', () => { state[field.key] = sel.value; });
  wrapper.appendChild(sel);

  return wrapper;
}

function buildText(field, state) {
  const wrapper = document.createElement('div');
  wrapper.className = 'field';

  const lbl = document.createElement('label');
  lbl.style.cssText = 'color:var(--muted);font-size:13px;';
  lbl.textContent = field.label || field.key;
  wrapper.appendChild(lbl);

  const inp = document.createElement('input');
  inp.type = 'text';
  inp.value = field.value != null ? String(field.value) : '';
  inp.placeholder = field.placeholder || '';
  inp.addEventListener('input', () => { state[field.key] = inp.value; });
  wrapper.appendChild(inp);

  return wrapper;
}

export const def = {
  name: 'preferences_panel',
  category: 'Input',
  title: 'Preferences Panel',

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

  render(p, ctx) {
    const { groups = [] } = p;
    const state = {};

    // Seed state from initial field values.
    groups.forEach(g => {
      (g.fields || []).forEach(f => {
        state[f.key] = f.value != null ? f.value : (f.type === 'toggle' ? false : '');
      });
    });

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

    groups.forEach(group => {
      const section = document.createElement('div');
      section.style.cssText = 'display:grid;gap:10px;';

      if (group.label) {
        const heading = document.createElement('div');
        heading.style.cssText =
          'font-size:12px;font-weight:700;letter-spacing:.08em;' +
          'text-transform:uppercase;color:var(--accent);' +
          'padding-bottom:8px;border-bottom:1px solid var(--border);';
        heading.textContent = group.label;
        section.appendChild(heading);
      }

      (group.fields || []).forEach(field => {
        let el;
        if (field.type === 'toggle') {
          el = buildToggle(field, state);
        } else if (field.type === 'select') {
          el = buildSelect(field, state);
        } else {
          el = buildText(field, state);
        }
        section.appendChild(el);
      });

      root.appendChild(section);
    });

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

    btn.addEventListener('click', () => {
      const receipt = { component: 'preferences_panel', values: Object.assign({}, state) };
      if (ctx && typeof ctx.sendReceipt === 'function') ctx.sendReceipt(receipt);
      emit(root, receipt);
      btn.disabled = true;
      btn.textContent = 'Saved ✓';
    });

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

  example: {
    groups: [
      {
        label: 'Appearance',
        fields: [
          { key: 'dark_mode', type: 'toggle', label: 'Dark mode', value: true },
          { key: 'compact', type: 'toggle', label: 'Compact layout', value: false }
        ]
      },
      {
        label: 'Locale',
        fields: [
          {
            key: 'language', type: 'select', label: 'Language', value: 'en',
            options: [{ id: 'en', label: 'English' }, { id: 'is', label: 'Íslenska' }]
          },
          { key: 'display_name', type: 'text', label: 'Display name', value: '' }
        ]
      }
    ]
  }
};