// Interactive gallery state manager (demo.html).
// Loads gallery/manifest.json and drives category filter + sidebar + main video player.
(async function () {
const shell = document.querySelector('.gallery-shell');
if (!shell) return;
const sidebar = shell.querySelector('.gallery-sidebar .gallery-list');
const main = shell.querySelector('.gallery-main');
const titleEl = main.querySelector('.gm-title');
const promptEl = main.querySelector('.mprompt');
const metaEl = main.querySelector('.mmeta');
const promptPillsEl = main.querySelector('.prompt-pills');
const camTabsEl = main.querySelector('.cam-tabs');
const playerEl = main.querySelector('.gallery-player video');
const noteEl = main.querySelector('.gm-note');
const filterBarEl = document.querySelector('.cat-filter-bar');
const statsStripEl = document.querySelector('.gallery-stats-strip');
let manifest = [];
try {
const r = await fetch('gallery/manifest.json', { cache: 'no-store' });
manifest = await r.json();
} catch (e) {
sidebar.innerHTML = '
Failed to load manifest.json
';
return;
}
// Category metadata. Order matters (drives filter bar + sidebar order).
const CATEGORIES = [
{ id: 'simple-pick', label: 'Simple pick', desc: 'Training and held-out red-cube pick episodes.' },
{ id: 'mid-episode', label: 'Mid-episode phases', desc: 'Model resumes mid-trajectory at the descend, grasp, or lift phase.' },
{ id: 'pick-place', label: 'Pick & place', desc: 'Pick a cube and place it in a container.' },
{ id: 'stack', label: 'Stack', desc: 'Stack one cube on top of another.' },
{ id: 'language-novel', label: 'Novel prompts', desc: 'Same scene, a novel language instruction the model never saw in training.' },
{ id: 'zero-shot', label: 'Zero-shot task', desc: 'Completely unseen dataset and task ("box → red bowl") — not in the training mix.' },
];
const TYPE_LABELS = {
dreamgen: { label: 'Imagined rollout', short: 'Imagined', color: '#C96840' },
policy: { label: 'Real-robot chunk', short: 'Policy', color: '#2E6A99' },
zeroshot: { label: 'Unseen dataset', short: 'Zero-shot', color: '#6A4EA8' },
};
const state = {
activeCategory: 'all',
activeTag: manifest[0] && manifest[0].tag,
activeCam: 'front',
};
// ========== Stats strip ==========
function renderStatsStrip() {
if (!statsStripEl) return;
const catCount = new Set(manifest.map(m => m.category)).size;
const dreamgen = manifest.filter(m => m.type === 'dreamgen').length;
const policy = manifest.filter(m => m.type === 'policy').length;
const zeroshot = manifest.filter(m => m.type === 'zeroshot').length;
statsStripEl.innerHTML = `
${manifest.length}Rollouts
${catCount}Capabilities
${dreamgen}Imagined rollouts
${policy}Real-robot chunks
${zeroshot}Unseen datasets
`;
}
// ========== Filter bar ==========
function renderFilterBar() {
if (!filterBarEl) return;
filterBarEl.innerHTML = '';
const counts = {};
manifest.forEach(m => { counts[m.category] = (counts[m.category] || 0) + 1; });
const allBtn = document.createElement('button');
allBtn.className = 'cat-chip' + (state.activeCategory === 'all' ? ' active' : '');
allBtn.innerHTML = `All${manifest.length}`;
allBtn.addEventListener('click', () => setCategory('all'));
filterBarEl.appendChild(allBtn);
CATEGORIES.forEach(c => {
if (!counts[c.id]) return;
const b = document.createElement('button');
b.className = 'cat-chip cat-' + c.id + (state.activeCategory === c.id ? ' active' : '');
b.innerHTML = `${c.label}${counts[c.id]}`;
b.addEventListener('click', () => setCategory(c.id));
filterBarEl.appendChild(b);
});
}
function setCategory(id) {
state.activeCategory = id;
const visible = manifest.filter(m => id === 'all' || m.category === id);
if (visible.length && !visible.find(v => v.tag === state.activeTag)) {
state.activeTag = visible[0].tag;
}
renderFilterBar();
renderSidebar();
renderMain();
}
// ========== Sidebar ==========
function renderSidebar() {
sidebar.innerHTML = '';
const catsToShow = state.activeCategory === 'all'
? CATEGORIES
: CATEGORIES.filter(c => c.id === state.activeCategory);
catsToShow.forEach(cat => {
const items = manifest.filter(m => m.category === cat.id);
if (!items.length) return;
const group = document.createElement('div');
group.className = 'gallery-group';
const label = document.createElement('div');
label.className = 'label cat-' + cat.id;
label.innerHTML = `${cat.label}${items.length}`;
group.appendChild(label);
items.forEach(item => {
const thumb = document.createElement('div');
thumb.className = 'gallery-thumb' + (item.tag === state.activeTag ? ' active' : '');
thumb.dataset.tag = item.tag;
const typeInfo = TYPE_LABELS[item.type] || { short: '', color: '#8A8278' };
const typeBadge = item.type
? `${typeInfo.short}`
: '';
thumb.innerHTML = `
${typeBadge} ${item.prompt_kind}
${item.prompt}
`;
thumb.addEventListener('click', () => selectTag(item.tag));
group.appendChild(thumb);
});
sidebar.appendChild(group);
});
}
// ========== Prompt pills in main panel ==========
function renderPromptPills(item) {
// Show alternative rollouts from the same episode (if any).
const sameEp = manifest.filter(x => x.episode === item.episode);
promptPillsEl.innerHTML = '';
if (sameEp.length <= 1) return;
const hdr = document.createElement('div');
hdr.style.cssText = 'font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--ink-faint);font-family:var(--font-mono);margin:0 0 6px';
hdr.textContent = 'Other rollouts of this scene';
promptPillsEl.appendChild(hdr);
sameEp.forEach(x => {
const p = document.createElement('button');
p.className = 'prompt-pill' + (x.tag === state.activeTag ? ' active' : '');
const typeInfo = TYPE_LABELS[x.type] || { label: '', color: '#8A8278' };
p.innerHTML = `${typeInfo.label}${x.prompt_kind}`;
p.addEventListener('click', () => selectTag(x.tag));
promptPillsEl.appendChild(p);
});
}
// ========== Camera tabs ==========
function renderCamTabs(item) {
camTabsEl.innerHTML = '';
const cams = ['front', 'top', 'gripper'];
cams.forEach(cam => {
if (!item.videos || !item.videos[cam]) return;
const t = document.createElement('button');
t.className = 'cam-tab' + (cam === state.activeCam ? ' active' : '');
t.textContent = cam;
t.addEventListener('click', () => {
state.activeCam = cam;
renderCamTabs(item);
updatePlayer(item);
});
camTabsEl.appendChild(t);
});
if (!item.videos || !item.videos[state.activeCam]) {
const fallback = Object.keys(item.videos || {})[0];
if (fallback) state.activeCam = fallback;
}
}
function updatePlayer(item) {
const src = (item.videos || {})[state.activeCam];
if (!src) return;
if (playerEl.getAttribute('src') !== src) {
playerEl.setAttribute('src', src);
playerEl.load();
playerEl.play().catch(() => {});
}
}
// ========== Main panel ==========
function renderMain() {
const item = manifest.find(x => x.tag === state.activeTag);
if (!item) return;
const catMeta = CATEGORIES.find(c => c.id === item.category);
// Set the category class on the main panel so the accent border reflects the task.
main.dataset.cat = item.category || '';
titleEl.innerHTML = `${catMeta ? catMeta.label : item.category} ${item.task_summary}`;
promptEl.textContent = '"' + item.prompt + '"';
const dur = item.duration_sec ? item.duration_sec.toFixed(1) + 's' : '—';
const typeInfo = TYPE_LABELS[item.type] || { label: item.type || '—', color: '#8A8278' };
metaEl.innerHTML = `
mode ${typeInfo.label}
instruction ${item.prompt_kind}
duration ${dur}
cameras 3 views
`;
renderPromptPills(item);
renderCamTabs(item);
updatePlayer(item);
if (noteEl) {
const notes = {
dreamgen: 'Imagined rollout — the model is given only a single initial frame and then hallucinates six seconds of video and joint commands autoregressively, with no further real-world observations.',
policy: 'Real-robot chunk — one real camera frame in, nine predicted frames and twenty-four joint-action commands out. This is the mode suitable for deployment on a physical SO-101 arm.',
zeroshot: 'Unseen dataset — the model was never trained on this task or scene. The rollout is raw generalization from the aggregated SO-101 pretraining mix.',
};
noteEl.textContent = notes[item.type] || '';
}
}
function selectTag(tag) {
state.activeTag = tag;
renderSidebar();
renderMain();
}
renderStatsStrip();
renderFilterBar();
renderSidebar();
renderMain();
})();