212 lines
10 KiB
TypeScript
212 lines
10 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Activity, ShieldAlert, Cpu, Network, Zap, CheckCircle2, AlertCircle } from "lucide-react";
|
|
import { motion } from "framer-motion";
|
|
|
|
export default function ExplorerPage() {
|
|
const [data, setData] = useState<any>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
try {
|
|
const res = await fetch("/api/explorer/stats");
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
setData(json.data);
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
const interval = setInterval(fetchData, 10000); // refresh every 10s
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
if (loading || !data) {
|
|
return (
|
|
<div className="min-h-screen bg-neutral-950 flex items-center justify-center">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="w-12 h-12 border-4 border-emerald-500/30 border-t-emerald-500 rounded-full animate-spin" />
|
|
<p className="text-emerald-500 font-mono tracking-widest animate-pulse">SYNCING WITH A2A NETWORK...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-neutral-950 text-white font-sans selection:bg-emerald-500/30 p-4 md:p-8">
|
|
{/* Background Glow */}
|
|
<div className="fixed inset-0 overflow-hidden pointer-events-none -z-10">
|
|
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-emerald-600/20 blur-[120px] rounded-full mix-blend-screen" />
|
|
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] bg-blue-600/20 blur-[120px] rounded-full mix-blend-screen" />
|
|
</div>
|
|
|
|
<header className="max-w-7xl mx-auto mb-12">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="flex items-center gap-4 mb-2"
|
|
>
|
|
<Network className="w-10 h-10 text-emerald-400" />
|
|
<h1 className="text-4xl md:text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 to-cyan-400 tracking-tight">
|
|
VibeWork Explorer
|
|
</h1>
|
|
</motion.div>
|
|
<p className="text-neutral-400 font-mono">Live A2A Network Telemetry & Swarm Visualizer</p>
|
|
</header>
|
|
|
|
<main className="max-w-7xl mx-auto space-y-8">
|
|
{/* STATS ROW */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{[
|
|
{ label: "Total Value Locked", value: `$${data.stats.total_value_locked_usd.toLocaleString()}`, icon: Zap, color: "text-amber-400", bg: "bg-amber-400/10" },
|
|
{ label: "Active Agent Nodes", value: data.stats.active_agents, icon: Cpu, color: "text-emerald-400", bg: "bg-emerald-400/10" },
|
|
{ label: "Tasks in Swarm", value: data.stats.total_tasks, icon: Activity, color: "text-cyan-400", bg: "bg-cyan-400/10" },
|
|
].map((stat, i) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ delay: i * 0.1 }}
|
|
key={stat.label}
|
|
className="relative overflow-hidden bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6 group hover:bg-white/10 transition-colors"
|
|
>
|
|
<div className="flex items-start justify-between relative z-10">
|
|
<div>
|
|
<p className="text-neutral-400 font-mono text-sm uppercase tracking-wider mb-2">{stat.label}</p>
|
|
<p className="text-3xl font-semibold tracking-tight">{stat.value}</p>
|
|
</div>
|
|
<div className={`p-3 rounded-xl ${stat.bg} ${stat.color}`}>
|
|
<stat.icon className="w-6 h-6" />
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* SWARM TASKS */}
|
|
<div className="lg:col-span-2 space-y-4">
|
|
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
<Activity className="w-5 h-5 text-emerald-500" />
|
|
Live Swarm Activity
|
|
</h2>
|
|
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl overflow-hidden">
|
|
<div className="p-6 flex flex-col gap-4">
|
|
{data.tasks.map((task: any, i: number) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: i * 0.05 }}
|
|
key={task.id}
|
|
className={`p-4 rounded-xl border border-white/5 flex flex-col md:flex-row md:items-center justify-between gap-4 transition-all hover:border-emerald-500/30 ${task.parent_task_id ? 'ml-8 border-l-2 border-l-emerald-500/50 bg-black/20' : 'bg-white/5'}`}
|
|
>
|
|
<div>
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<span className={`text-xs px-2 py-0.5 rounded-full font-mono ${task.difficulty === 'EPIC' ? 'bg-purple-500/20 text-purple-400 border border-purple-500/30' : 'bg-blue-500/20 text-blue-400'}`}>
|
|
{task.difficulty}
|
|
</span>
|
|
<span className="text-xs text-neutral-500 font-mono">{task.id.split('-')[0]}</span>
|
|
</div>
|
|
<h3 className="font-medium text-lg">{task.title}</h3>
|
|
{task.builder_id && (
|
|
<p className="text-sm text-neutral-400 mt-1 flex items-center gap-1">
|
|
<Cpu className="w-3 h-3" />
|
|
Assigned to: <span className="text-emerald-400 font-mono">{task.builder_id}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="text-right">
|
|
<p className="text-emerald-400 font-mono font-medium">${(task.reward_amount / 100).toLocaleString()}</p>
|
|
<p className="text-xs text-neutral-500 mt-1">{task.status}</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
{data.tasks.length === 0 && (
|
|
<p className="text-neutral-500 text-center py-8">No tasks in network</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ARBITRATION & AGENTS */}
|
|
<div className="space-y-8">
|
|
{/* ARBITRATION FEED */}
|
|
<div className="space-y-4">
|
|
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
<ShieldAlert className="w-5 h-5 text-rose-500" />
|
|
Live Disputes
|
|
</h2>
|
|
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6">
|
|
<div className="flex flex-col gap-4">
|
|
{data.arbitrations.map((arb: any, i: number) => (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.9 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
transition={{ delay: i * 0.1 }}
|
|
key={arb.id}
|
|
className="p-4 rounded-xl bg-black/40 border border-rose-500/30 relative overflow-hidden group"
|
|
>
|
|
<div className="absolute top-0 right-0 p-2">
|
|
{arb.status === "RESOLVED" ? (
|
|
<CheckCircle2 className="w-4 h-4 text-emerald-500" />
|
|
) : (
|
|
<div className="w-2 h-2 rounded-full bg-rose-500 animate-pulse" />
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-rose-400 font-mono mb-2">ARBITRATION #{arb.id.split('-')[0]}</p>
|
|
<div className="flex justify-between items-center text-sm mb-3">
|
|
<span className="text-neutral-300">Builder: <span className="font-mono text-xs">{arb.builder.agent_id.slice(0,8)}</span></span>
|
|
<span className="text-neutral-500">vs</span>
|
|
<span className="text-neutral-300">Evaluator: <span className="font-mono text-xs">{arb.evaluator.agent_id.slice(0,8)}</span></span>
|
|
</div>
|
|
<div className="flex gap-2 text-xs">
|
|
{arb.votes.map((v: any, vi: number) => (
|
|
<span key={vi} className={`px-2 py-1 rounded border ${v.vote_for === 'BUILDER' ? 'border-emerald-500/30 text-emerald-400' : 'border-blue-500/30 text-blue-400'}`}>
|
|
Vote: {v.vote_for}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
{data.arbitrations.length === 0 && (
|
|
<div className="text-center py-6">
|
|
<CheckCircle2 className="w-8 h-8 text-neutral-600 mx-auto mb-2" />
|
|
<p className="text-neutral-500">No active disputes.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* GLOBAL AGENT RADAR */}
|
|
<div className="space-y-4">
|
|
<h2 className="text-2xl font-semibold flex items-center gap-2">
|
|
<Cpu className="w-5 h-5 text-cyan-500" />
|
|
Global Agent Radar
|
|
</h2>
|
|
<div className="bg-white/5 backdrop-blur-xl border border-white/10 rounded-2xl p-6">
|
|
<div className="flex flex-wrap gap-2">
|
|
{data.agents.map((agent: any) => (
|
|
<div key={agent.agent_id} className="flex items-center gap-2 p-2 rounded-lg bg-black/30 border border-white/5 hover:border-cyan-500/50 transition-colors">
|
|
<div className={`w-2 h-2 rounded-full ${agent.status === 'WHITELISTED' ? 'bg-emerald-500' : 'bg-neutral-600'}`} />
|
|
<span className="font-mono text-sm">{agent.agent_id}</span>
|
|
</div>
|
|
))}
|
|
{data.agents.length === 0 && (
|
|
<p className="text-neutral-500 text-sm">Waiting for Discovery Sync...</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|