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