AkurAI Build
Menu

Public repository / 439f0dcabf5cdf2b120c7724be895cf1df30507f

AkurAI-Framework / site/frontend/ai/components/artifacts/data_table.js

Latest change 8847e6f4a315fb27eba8cb69b87397dd08a068eb - feat(ai/artifacts): add Chart, DataTable, CodeBlock, CodeDiff ES modules by Ólafur Búi Ólafsson

/**
 * Artifact: Data Table
 * Payload: { columns: string[], rows: any[][] }
 * Renders a sortable table with zebra rows and numeric right-alignment.
 */

function isNumericCol(rows, ci) {
  return rows.every(row => {
    const v = row[ci];
    return v === null || v === undefined || v === '' || !isNaN(Number(v));
  });
}

export const def = {
  name: 'data_table',
  category: 'Artifacts',
  title: 'Data Table',

  match(p) {
    return p != null && Array.isArray(p.columns) && Array.isArray(p.rows);
  },

  render(payload) {
    const { columns, rows } = payload;
    const numericCol = columns.map((_, ci) => isNumericCol(rows, ci));

    let sortCol = -1;
    let sortAsc = true;
    let sorted = rows.slice();

    const wrap = document.createElement('div');
    wrap.style.overflowX = 'auto';

    const table = document.createElement('table');
    table.className = 'table';

    /* Head */
    const thead = document.createElement('thead');
    const headRow = document.createElement('tr');

    columns.forEach((col, ci) => {
      const th = document.createElement('th');
      if (numericCol[ci]) th.style.textAlign = 'right';
      th.style.cursor = 'pointer';
      th.style.userSelect = 'none';
      th.title = 'Sort';

      const label = document.createElement('span');
      label.textContent = String(col);

      const arrow = document.createElement('span');
      arrow.style.cssText = 'margin-left:4px;color:var(--dim)';
      arrow.textContent = ' ⇅';

      th.appendChild(label);
      th.appendChild(arrow);

      th.addEventListener('click', () => {
        if (sortCol === ci) {
          sortAsc = !sortAsc;
        } else {
          sortCol = ci;
          sortAsc = true;
        }
        headRow.querySelectorAll('th').forEach((h, i) => {
          const a = h.querySelector('span:last-child');
          if (i === sortCol) {
            a.textContent = sortAsc ? ' ↑' : ' ↓';
            a.style.color = 'var(--accent)';
          } else {
            a.textContent = ' ⇅';
            a.style.color = 'var(--dim)';
          }
        });
        rebuildBody();
      });

      headRow.appendChild(th);
    });

    thead.appendChild(headRow);
    table.appendChild(thead);

    const tbody = document.createElement('tbody');
    table.appendChild(tbody);
    wrap.appendChild(table);

    function rebuildBody() {
      if (sortCol >= 0) {
        sorted = rows.slice().sort((a, b) => {
          let av = a[sortCol], bv = b[sortCol];
          if (numericCol[sortCol]) {
            av = Number(av);
            bv = Number(bv);
          } else {
            av = String(av ?? '');
            bv = String(bv ?? '');
          }
          if (av < bv) return sortAsc ? -1 : 1;
          if (av > bv) return sortAsc ? 1 : -1;
          return 0;
        });
      } else {
        sorted = rows.slice();
      }

      tbody.textContent = '';
      sorted.forEach((row, ri) => {
        const tr = document.createElement('tr');
        if (ri % 2 === 1) {
          tr.style.background = 'color-mix(in srgb, var(--panel) 40%, transparent)';
        }
        columns.forEach((_, ci) => {
          const td = document.createElement('td');
          if (numericCol[ci]) {
            td.style.textAlign = 'right';
            td.style.fontVariantNumeric = 'tabular-nums';
            td.style.fontFamily = 'var(--mono)';
          }
          const v = row[ci];
          td.textContent = v == null ? '' : String(v);
          tr.appendChild(td);
        });
        tbody.appendChild(tr);
      });
    }

    rebuildBody();
    return wrap;
  },

  example: {
    columns: ['Name', 'Score', 'Rank'],
    rows: [
      ['Alice', 92, 1],
      ['Bob', 87, 2],
      ['Carol', 78, 3],
      ['David', 95, 0]
    ]
  }
};