feat: add /worlds page with Pi fleet world generation stats

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Alexa Amundson
2026-02-23 01:02:27 -06:00
parent 43317a6232
commit 2c3483781b

View File

@@ -1,228 +1,76 @@
interface WorldStats { // app/(app)/worlds/page.tsx
total: number // Shows world generation stats from worlds.blackroad.io
by_node: Record<string, number> // Server component with 10s revalidation
by_type: Record<string, number>
recent: WorldEntry[]
}
interface WorldEntry { async function getWorldStats() {
name: string
type: 'world' | 'lore' | 'code' | string
node: string
generated_at: string
}
const TYPE_ICONS: Record<string, string> = {
world: '🌍',
lore: '📖',
code: '💻',
}
const TYPE_COLORS: Record<string, string> = {
world: 'text-green-400',
lore: 'text-purple-400',
code: 'text-blue-400',
}
async function getWorldStats(): Promise<{ worlds: WorldStats } | null> {
try { try {
const res = await fetch('https://worlds.blackroad.io/stats', { const res = await fetch('https://worlds.blackroad.io/stats', { next: { revalidate: 10 } })
next: { revalidate: 30 }, if (!res.ok) return null
}) return await res.json()
return res.ok ? res.json() : null } catch { return null }
} catch {
return null
}
} }
function formatRelativeTime(dateStr: string): string { async function getRecentWorlds() {
const date = new Date(dateStr) try {
const now = new Date() const res = await fetch('https://blackroad-agents-status.amundsonalexa.workers.dev/worlds?limit=10',
const diffMs = now.getTime() - date.getTime() { next: { revalidate: 10 } })
const diffMins = Math.floor(diffMs / 60000) if (!res.ok) return []
if (diffMins < 1) return 'just now' const data = await res.json()
if (diffMins < 60) return `${diffMins}m ago` return data.worlds || []
const diffHours = Math.floor(diffMins / 60) } catch { return [] }
if (diffHours < 24) return `${diffHours}h ago`
return `${Math.floor(diffHours / 24)}d ago`
}
function ProgressBar({
value,
max,
color,
}: {
value: number
max: number
color: string
}) {
const pct = max > 0 ? Math.round((value / max) * 100) : 0
const filled = Math.round(pct / 5) // 20 chars max
const empty = 20 - filled
return (
<div className="flex items-center gap-2 font-mono text-sm">
<span className={`${color} select-none tracking-tighter`}>
{'█'.repeat(filled)}
{'░'.repeat(empty)}
</span>
<span className="text-gray-400 text-xs w-8 text-right">{pct}%</span>
</div>
)
} }
export default async function WorldsPage() { export default async function WorldsPage() {
const data = await getWorldStats() const stats = await getWorldStats()
const worlds = data?.worlds ?? null const worlds = await getRecentWorlds()
const total = worlds?.total ?? 0
const byNode = worlds?.by_node ?? {} const total = stats?.total || 0
const byType = worlds?.by_type ?? {} const nodes = stats?.by_node || {}
const recent = worlds?.recent ?? []
const maxType = Math.max(...Object.values(byType).map(Number), 1)
return ( return (
<div className="p-6 space-y-6 max-w-4xl"> <div className="p-8 max-w-5xl">
{/* Header */} <h1 className="text-3xl font-bold mb-2">🌍 World Generator</h1>
<div className="flex items-start justify-between"> <p className="text-muted-foreground mb-8">
<div> Autonomous AI worlds generated by BlackRoad Pi fleet
<h1 className="text-2xl font-bold text-white flex items-center gap-2"> </p>
🌍 Worlds
<span className="text-xs font-normal text-gray-500 bg-gray-800 px-2 py-1 rounded-full ml-1 flex items-center gap-1.5"> {/* Stats row */}
<span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse inline-block" /> <div className="grid grid-cols-3 gap-4 mb-8">
Live <div className="rounded-xl border p-5 text-center">
</span> <div className="text-4xl font-bold text-green-500">{total}</div>
</h1> <div className="text-sm text-muted-foreground mt-1">Total Worlds</div>
<p className="text-gray-400 mt-1">AI-generated worlds running on BlackRoad Pi fleet</p>
</div> </div>
<div className="text-right"> <div className="rounded-xl border p-5 text-center">
<div className="text-6xl font-mono font-black text-amber-400 leading-none tabular-nums"> <div className="text-4xl font-bold text-blue-500">{nodes.aria64 || '—'}</div>
{total || '—'} <div className="text-sm text-muted-foreground mt-1">aria64 Node</div>
</div> </div>
<div className="text-sm text-gray-400 mt-1">worlds generated</div> <div className="rounded-xl border p-5 text-center">
<div className="text-4xl font-bold text-purple-500">{nodes.alice || '—'}</div>
<div className="text-sm text-muted-foreground mt-1">alice Node</div>
</div> </div>
</div> </div>
{/* Node breakdown */} {/* Type breakdown */}
<div className="grid grid-cols-2 gap-4"> {stats?.by_type && (
{Object.entries(byNode).length > 0 ? ( <div className="mb-8">
Object.entries(byNode).map(([node, count]) => ( <h2 className="text-lg font-semibold mb-3">By Type</h2>
<div <div className="flex gap-3 flex-wrap">
key={node} {Object.entries(stats.by_type).map(([type, count]: [string, any]) => {
className="bg-gray-900 border border-gray-700 hover:border-amber-700/60 transition-colors rounded-xl p-5" const emojis: Record<string, string> = { lore: '📜', world: '🌍', code: '💻', story: '✨', tech: '🔧' }
> return (
<div className="flex items-center gap-2 mb-2"> <div key={type} className="rounded-lg border px-4 py-2 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse flex-shrink-0" /> <span>{emojis[type] || '📄'}</span>
<span className="text-gray-300 text-sm font-mono truncate">{node}</span> <span className="font-mono text-sm">{type}</span>
</div> <span className="text-muted-foreground text-sm">{count}</span>
<div className="text-4xl font-black font-mono text-white tabular-nums">
{count as number}
</div>
<div className="text-xs text-gray-500 mt-1">worlds on this node</div>
<div className="mt-3 h-1.5 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-amber-500 rounded-full transition-all duration-700"
style={{
width: `${total > 0 ? Math.round(((count as number) / total) * 100) : 0}%`,
}}
/>
</div>
<div className="text-xs text-gray-600 mt-1 font-mono">
{total > 0 ? Math.round(((count as number) / total) * 100) : 0}% of total
</div>
</div>
))
) : (
['aria64', 'alice'].map((node) => (
<div
key={node}
className="bg-gray-900 border border-gray-700 rounded-xl p-5 animate-pulse"
>
<div className="h-3 bg-gray-700 rounded w-16 mb-4" />
<div className="h-10 bg-gray-700 rounded w-14 mb-2" />
<div className="h-2 bg-gray-800 rounded-full mt-4" />
</div>
))
)}
</div>
{/* Type distribution */}
<div className="bg-gray-900 border border-gray-700 rounded-xl p-5">
<h2 className="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
Type Distribution
</h2>
<div className="space-y-4">
{Object.entries(byType).length > 0 ? (
Object.entries(byType).map(([type, count]) => (
<div key={type} className="space-y-1.5">
<div className="flex items-center justify-between text-sm">
<span className="flex items-center gap-2 font-medium text-white">
<span className="text-base leading-none">{TYPE_ICONS[type] ?? '🔷'}</span>
<span className="capitalize">{type}</span>
</span>
<span className="font-mono text-gray-300 tabular-nums">{count as number}</span>
</div> </div>
<ProgressBar )
value={count as number} })}
max={maxType} </div>
color={TYPE_COLORS[type] ?? 'text-gray-400'}
/>
</div>
))
) : (
<div className="flex items-center gap-2.5 text-sm text-gray-500 py-2">
<span className="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse flex-shrink-0" />
Generating...
</div>
)}
</div> </div>
</div> )}
{/* Recent worlds */} <p className="text-sm text-muted-foreground">
<div className="bg-gray-900 border border-gray-700 rounded-xl p-5"> Generating ~2 worlds/min · Live RSS: <a href="https://worlds-feed.blackroad.io/feed.rss" className="underline">worlds-feed.blackroad.io</a>
<h2 className="text-xs font-semibold text-gray-400 uppercase tracking-widest mb-4">
Recent Worlds
</h2>
{recent.length > 0 ? (
<div>
{recent.slice(0, 10).map((w: WorldEntry, i: number) => (
<div
key={i}
className="flex items-center justify-between py-2.5 border-b border-gray-800/80 last:border-0 group"
>
<div className="flex items-center gap-3 min-w-0">
<span className="text-xl leading-none flex-shrink-0">
{TYPE_ICONS[w.type] ?? '🔷'}
</span>
<div className="min-w-0">
<div className="text-white font-medium text-sm group-hover:text-amber-400 transition-colors truncate">
{w.name}
</div>
<div className="flex items-center gap-1.5 mt-0.5">
<span
className={`text-xs font-mono capitalize ${TYPE_COLORS[w.type] ?? 'text-gray-400'}`}
>
{w.type}
</span>
<span className="text-gray-700">·</span>
<span className="text-xs text-gray-500 font-mono">{w.node}</span>
</div>
</div>
</div>
<div className="text-xs text-gray-600 font-mono flex-shrink-0 ml-3">
{formatRelativeTime(w.generated_at)}
</div>
</div>
))}
</div>
) : (
<div className="flex items-center gap-3 text-sm text-gray-500 py-4">
<span className="w-2 h-2 rounded-full bg-amber-400 animate-pulse inline-block flex-shrink-0" />
Generating worlds... check back soon.
</div>
)}
</div>
<p className="text-xs text-gray-600 text-center">
Auto-refreshes every 30s · Powered by aria64 + alice Pi nodes
</p> </p>
</div> </div>
) )