AkurAI Build
Menu

Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8

AkurAI-Framework / site/frontend/ai/gallery.js

Latest change d2de65b4934c81cfa0049cf6b956720e001fe1ec - feat(ai): add AI components gallery page (stub manifest + gallery.js + ai-gallery.html) by Ólafur Búi Ólafsson

// AI Components Gallery
// Imports the component manifest, groups defs by category, and renders
// a showcase card for each — live component + collapsible payload viewer.

import { components } from './components-manifest.js';

// Canonical category order (mirrors the docs).
const CATEGORY_ORDER = ['Progress', 'Input', 'Display', 'Artifacts', 'Confirmation', 'Media'];

// Gallery context passed to every component's render().
const ctx = {
  sendReceipt: (o) => console.log('[gallery] receipt', o),
  onAction: (name, data) => console.log('[gallery] action', name, data),
};

// ── Helpers ───────────────────────────────────────────────────────────────────

function groupByCategory(defs) {
  const map = new Map(CATEGORY_ORDER.map(c => [c, []]));
  for (const def of defs) {
    const cat = def.category ?? 'Uncategorised';
    if (!map.has(cat)) map.set(cat, []);
    map.get(cat).push(def);
  }
  return map;
}

function slugId(str) {
  return str.toLowerCase().replace(/\s+/g, '-');
}

// ── Nav (sticky category index at top) ───────────────────────────────────────

function buildNav(grouped) {
  const nav = document.createElement('nav');
  nav.className = 'comp-index gallery-nav';
  nav.setAttribute('aria-label', 'Component categories');
  for (const [cat, defs] of grouped) {
    if (!defs.length) continue;
    const a = document.createElement('a');
    a.href = `#gallery-cat-${slugId(cat)}`;
    a.textContent = `${cat} (${defs.length})`;
    nav.appendChild(a);
  }
  return nav;
}

// ── Payload viewer ────────────────────────────────────────────────────────────

function buildPayloadViewer(example) {
  const json = JSON.stringify(example, null, 2);
  const details = document.createElement('details');
  details.className = 'gallery-payload';

  const summary = document.createElement('summary');
  summary.textContent = 'Payload (example)';
  details.appendChild(summary);

  const pre = document.createElement('pre');
  pre.className = 'gallery-payload-pre';
  const code = document.createElement('code');
  code.textContent = json;
  pre.appendChild(code);

  const copyBtn = document.createElement('button');
  copyBtn.className = 'btn btn-ghost btn-sm gallery-copy';
  copyBtn.textContent = 'Copy';
  copyBtn.addEventListener('click', () => {
    navigator.clipboard.writeText(json).then(() => {
      copyBtn.textContent = 'Copied!';
      setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1600);
    }).catch(() => {
      copyBtn.textContent = 'Error';
      setTimeout(() => { copyBtn.textContent = 'Copy'; }, 1600);
    });
  });

  const footer = document.createElement('div');
  footer.className = 'gallery-payload-footer';
  footer.appendChild(copyBtn);

  details.append(pre, footer);
  return details;
}

// ── Showcase card ─────────────────────────────────────────────────────────────

function buildCard(def) {
  const card = document.createElement('article');
  card.className = 'card gallery-card';
  card.id = `gallery-comp-${def.name}`;

  // Header: title + name badge
  const header = document.createElement('header');
  header.className = 'gallery-card-head';

  const title = document.createElement('h3');
  title.textContent = def.title;

  const badge = document.createElement('span');
  badge.className = 'badge badge-muted gallery-badge';
  badge.textContent = def.name;

  header.append(title, badge);

  // Live render area
  const liveWrap = document.createElement('div');
  liveWrap.className = 'gallery-live';

  try {
    const rendered = def.render(def.example, ctx);
    if (rendered instanceof HTMLElement) {
      liveWrap.appendChild(rendered);
    } else {
      throw new Error('render() did not return an HTMLElement');
    }
  } catch (err) {
    const chip = document.createElement('span');
    chip.className = 'badge gallery-err';
    chip.textContent = `Error: ${err.message}`;
    liveWrap.appendChild(chip);
    console.warn(`[gallery] ${def.name} render error:`, err);
  }

  card.append(header, liveWrap, buildPayloadViewer(def.example));
  return card;
}

// ── Category section ──────────────────────────────────────────────────────────

function buildSection(cat, defs) {
  const section = document.createElement('section');
  section.className = 'gallery-section';
  section.id = `gallery-cat-${slugId(cat)}`;

  const heading = document.createElement('h2');
  heading.className = 'comp-group';
  heading.textContent = cat;
  section.appendChild(heading);

  const grid = document.createElement('div');
  grid.className = 'gallery-grid';
  for (const def of defs) {
    grid.appendChild(buildCard(def));
  }
  section.appendChild(grid);
  return section;
}

// ── Mount ─────────────────────────────────────────────────────────────────────

function mount() {
  const root = document.getElementById('ai-gallery-root');
  if (!root) {
    console.warn('[gallery] mount point #ai-gallery-root not found');
    return;
  }

  const grouped = groupByCategory(components);
  root.appendChild(buildNav(grouped));

  let total = 0;
  for (const [cat, defs] of grouped) {
    if (!defs.length) continue;
    root.appendChild(buildSection(cat, defs));
    total += defs.length;
  }

  console.log(`[gallery] rendered ${total} components`);
}

// Guard on readyState.
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', mount);
} else {
  mount();
}