optigami_ / app /page.tsx
sissississi's picture
Redesign frontend as training dashboard + add live activity feed
d662461
'use client';
import { useState, useCallback, useMemo } from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Grid } from '@react-three/drei';
import { patterns, Pattern } from '@/lib/patterns';
import { OrigamiMesh } from '@/components/OrigamiMesh';
import { TrainingDashboard, TrainingEntry } from '@/components/TrainingDashboard';
import { parseFoldFile } from '@/lib/foldParser';
export default function Optigami() {
const [selectedEntry, setSelectedEntry] = useState<TrainingEntry | null>(null);
const [foldPercent, setFoldPercent] = useState(1);
const [key, setKey] = useState(0);
// Convert training entry's fold_data into a Pattern for the 3D viewer
const activePattern: Pattern | null = useMemo(() => {
if (!selectedEntry?.fold_data) return null;
try {
const parsed = parseFoldFile(JSON.stringify(selectedEntry.fold_data), selectedEntry.task_name);
return parsed;
} catch {
return null;
}
}, [selectedEntry]);
// Fallback to a default pattern when no training entry is selected
const displayPattern = activePattern || patterns[0];
const handleEntrySelect = useCallback((entry: TrainingEntry) => {
setSelectedEntry(entry);
setFoldPercent(1);
setKey(k => k + 1);
}, []);
return (
<div className="flex h-screen w-full bg-zinc-950 text-zinc-100 font-sans overflow-hidden">
{/* Left panel — Training Dashboard */}
<div className="w-96 flex-shrink-0 border-r border-zinc-800 bg-zinc-900 flex flex-col">
<div className="px-5 py-4 border-b border-zinc-800 flex items-center justify-between">
<div>
<h1 className="text-lg font-semibold tracking-tight">Optigami</h1>
<p className="text-[11px] text-zinc-500">RL Training Environment</p>
</div>
<div className="flex items-center gap-2">
<a
href="https://huggingface.co/spaces/openenv-community/optigami_"
target="_blank"
rel="noopener noreferrer"
className="text-[10px] text-zinc-500 hover:text-zinc-300 bg-zinc-800 px-2 py-1 rounded border border-zinc-700"
>
OpenEnv 0.2.1
</a>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4">
<TrainingDashboard onEntrySelect={handleEntrySelect} />
</div>
</div>
{/* Right side — 3D viewer + detail */}
<div className="flex-1 flex flex-col">
{/* Top bar with context about selected entry */}
{selectedEntry && (
<div className="flex-shrink-0 border-b border-zinc-800 bg-zinc-900/50 px-6 py-3 flex items-center gap-6 text-xs">
<div>
<span className="text-zinc-500">Step</span>{' '}
<span className="font-mono text-zinc-200">#{selectedEntry.step}</span>
</div>
<div>
<span className="text-zinc-500">Task</span>{' '}
<span className="text-zinc-200">{selectedEntry.task_name}</span>
</div>
<div>
<span className="text-zinc-500">Reward</span>{' '}
<span className={`font-mono font-semibold ${
selectedEntry.reward >= 15 ? 'text-green-400' :
selectedEntry.reward >= 5 ? 'text-yellow-400' :
selectedEntry.reward >= 0 ? 'text-orange-400' : 'text-red-400'
}`}>{selectedEntry.reward.toFixed(2)}</span>
</div>
<div>
<span className="text-zinc-500">Similarity</span>{' '}
<span className="font-mono text-indigo-400">
{(selectedEntry.shape_similarity * 100).toFixed(1)}%
</span>
</div>
{selectedEntry.error && (
<div className="text-red-400/80 truncate flex-1">{selectedEntry.error}</div>
)}
<div className="ml-auto flex items-center gap-2">
<span className="text-zinc-500">Fold</span>
<input
type="range"
min="0"
max="1"
step="0.01"
value={foldPercent}
onChange={(e) => setFoldPercent(parseFloat(e.target.value))}
className="w-24 accent-indigo-500"
/>
<span className="font-mono text-zinc-400 w-8">{Math.round(foldPercent * 100)}%</span>
</div>
</div>
)}
{/* 3D Canvas */}
<div className="flex-1 relative">
{!selectedEntry && (
<div className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none">
<div className="text-center">
<div className="text-zinc-500 text-sm mb-1">Waiting for training data</div>
<div className="text-zinc-600 text-xs">
Start a GRPO training run in the Colab notebook
</div>
</div>
</div>
)}
<Canvas camera={{ position: [0, 0, 3], fov: 45 }}>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} intensity={1} castShadow />
<directionalLight position={[-5, -5, -5]} intensity={0.2} />
<group key={key}>
<OrigamiMesh pattern={displayPattern} foldPercent={foldPercent} />
</group>
<OrbitControls makeDefault />
<Grid
infiniteGrid
fadeDistance={10}
sectionColor="#333"
cellColor="#222"
position={[0, 0, -0.01]}
/>
</Canvas>
{/* 2D crease pattern overlay */}
{activePattern && (
<div className="absolute bottom-4 left-4 w-40 h-40 bg-zinc-900/90 rounded-lg border border-zinc-700/50 p-2 backdrop-blur-sm">
<div className="text-[9px] uppercase text-zinc-500 tracking-wider mb-1">Crease Pattern</div>
<svg viewBox="-1.2 -1.2 2.4 2.4" className="w-full h-full">
<g transform="scale(1, -1)">
{activePattern.faces.map((face, i) => {
const v1 = activePattern.vertices[face[0]];
const v2 = activePattern.vertices[face[1]];
const v3 = activePattern.vertices[face[2]];
return (
<polygon
key={`f-${i}`}
points={`${v1[0]},${v1[1]} ${v2[0]},${v2[1]} ${v3[0]},${v3[1]}`}
fill="#3f3f46"
stroke="#52525b"
strokeWidth="0.01"
/>
);
})}
{activePattern.creases.map((crease, i) => {
const v1 = activePattern.vertices[crease.edge[0]];
const v2 = activePattern.vertices[crease.edge[1]];
const color = crease.type === 'mountain' ? '#ef4444' : '#3b82f6';
return (
<line
key={`c-${i}`}
x1={v1[0]} y1={v1[1]}
x2={v2[0]} y2={v2[1]}
stroke={color}
strokeWidth="0.03"
strokeLinecap="round"
/>
);
})}
</g>
</svg>
</div>
)}
</div>
</div>
</div>
);
}