Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import axios from 'axios'; | |
| import { Search, Share2, Activity, Database, Zap, Wallet, ChevronRight, Terminal, Layers, Hash, Info, X } from 'lucide-react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import PersonaChart from './components/PersonaChart'; | |
| import RoastCard from './components/RoastCard'; | |
| import Logo from './assets/logo.svg'; | |
| const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:8000"; | |
| function InfoModal({ onClose }) { | |
| return ( | |
| <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"> | |
| <div className="w-full max-w-2xl bg-bg-panel border border-border shadow-2xl relative max-h-[90vh] overflow-y-auto"> | |
| <button | |
| onClick={onClose} | |
| className="absolute top-3 right-3 text-text-secondary hover:text-text-primary" | |
| > | |
| <X size={20} /> | |
| </button> | |
| <div className="p-6 space-y-6"> | |
| <div className="border-b border-border pb-4"> | |
| <h2 className="text-xl font-mono font-bold text-text-primary flex items-center gap-2"> | |
| <Info size={20} className="text-accent" /> | |
| SYSTEM PROTOCOLS | |
| </h2> | |
| <p className="text-xs text-text-secondary font-mono mt-1">OPERATIONAL CONSTRAINTS & SPECIFICATIONS</p> | |
| </div> | |
| <div className="space-y-4 font-mono text-xs md:text-sm text-text-primary/90"> | |
| <section> | |
| <h3 className="font-bold text-accent mb-1">1. NETWORK: ETHEREUM MAINNET ONLY</h3> | |
| <ul className="list-disc pl-5 space-y-1 text-text-secondary"> | |
| <li>Compatible: Ethereum Mainnet (L1).</li> | |
| <li>Incompatible: Base, Arbitrum, Optimism, Polygon, Solana, Bitcoin.</li> | |
| <li>Result: Wallets active only on L2s will show 0 activity.</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h3 className="font-bold text-accent mb-1">2. ENTITY RECOGNITION</h3> | |
| <ul className="list-disc pl-5 space-y-1 text-text-secondary"> | |
| <li>Optimized for: <strong>User Wallets (EOA)</strong>.</li> | |
| <li>Supported: Smart Contracts (data retrieval works).</li> | |
| <li>Warning: Analyzing Contracts (e.g., Uniswap Router) may yield skewed "Whale" personas due to pooled funds.</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h3 className="font-bold text-accent mb-1">3. ASSET COVERAGE</h3> | |
| <ul className="list-disc pl-5 space-y-1 text-text-secondary"> | |
| <li>Native ETH: 100% Coverage.</li> | |
| <li>ERC-20 Tokens: 100% Coverage (USDC, PEPE, UNI, etc.).</li> | |
| <li>NFTs: ERC-721 & ERC-1155 Standard.</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h3 className="font-bold text-accent mb-1">4. TEMPORAL HORIZON (OPTIMIZATION)</h3> | |
| <ul className="list-disc pl-5 space-y-1 text-text-secondary"> | |
| <li>ETH Txs: <strong>Full History (Since 2015)</strong>. Accurate age/gas stats.</li> | |
| <li>DeFi/NFTs: <strong>Post-2018 Analysis</strong>. Ignores experimental pre-2018 token activity for query speed.</li> | |
| </ul> | |
| </section> | |
| </div> | |
| <div className="pt-4 border-t border-border flex justify-end"> | |
| <button | |
| onClick={onClose} | |
| className="px-6 py-2 bg-accent text-black font-bold font-mono text-xs uppercase tracking-wider hover:bg-amber-400 transition-colors" | |
| > | |
| ACKNOWLEDGE | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function StatusCycler() { | |
| const [msgIndex, setMsgIndex] = useState(0); | |
| const messages = [ | |
| "> INITIALIZING_NEURAL_UPLINK...", | |
| "> ESTABLISHING_DUNE_CONNECTION...", | |
| "> SCANNING_ETHEREUM_HISTORY...", | |
| "> AGGREGATING_NFT_VECTORS...", | |
| "> COMPUTING_CLUSTERING_TENSORS...", | |
| "> DECODING_BEHAVIORAL_PATTERNS...", | |
| "> FINALIZING_AI_ANALYSIS..." | |
| ]; | |
| useEffect(() => { | |
| const interval = setInterval(() => { | |
| setMsgIndex((prev) => (prev + 1) % messages.length); | |
| }, 3500); | |
| return () => clearInterval(interval); | |
| }, []); | |
| return ( | |
| <div className="flex justify-between text-[10px] font-mono text-text-secondary uppercase tracking-widest"> | |
| <span className="animate-pulse">{messages[msgIndex]}</span> | |
| <span className="animate-pulse">_</span> | |
| </div> | |
| ); | |
| } | |
| function App() { | |
| const [wallet, setWallet] = useState(""); | |
| const [status, setStatus] = useState("idle"); | |
| const [data, setData] = useState(null); | |
| const [errorMsg, setErrorMsg] = useState(""); | |
| const [showInfo, setShowInfo] = useState(false); | |
| const analyzeWallet = async () => { | |
| if (!wallet.startsWith("0x")) { | |
| setErrorMsg("INVALID_ADDRESS: Must start with 0x"); | |
| return; | |
| } | |
| setStatus("loading"); | |
| setErrorMsg(""); | |
| setData(null); | |
| try { | |
| const startRes = await axios.post(`${API_BASE}/analyze/start/${wallet}`); | |
| pollStatus(startRes.data.job_id); | |
| } catch (err) { | |
| console.error(err); | |
| setErrorMsg("CONNECTION_ERR: API Unreachable"); | |
| setStatus("error"); | |
| } | |
| }; | |
| const pollStatus = (jobId) => { | |
| const interval = setInterval(async () => { | |
| try { | |
| const res = await axios.get(`${API_BASE}/analyze/status/${jobId}`); | |
| const result = res.data; | |
| if (result.status === "completed") { | |
| clearInterval(interval); | |
| setData(result); | |
| setStatus("success"); | |
| } else if (result.status === "failed") { | |
| clearInterval(interval); | |
| setErrorMsg(result.error || "Analysis Failed"); | |
| setStatus("error"); | |
| } | |
| } catch (err) { | |
| clearInterval(interval); | |
| setErrorMsg("POLLING_ERR: Lost connection"); | |
| setStatus("error"); | |
| } | |
| }, 2000); | |
| }; | |
| const handleExport = () => { | |
| if (!data) return; | |
| const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( | |
| JSON.stringify(data, null, 2) | |
| )}`; | |
| const link = document.createElement("a"); | |
| link.href = jsonString; | |
| link.download = `analysis_${data.wallet_address || "wallet"}.json`; | |
| link.click(); | |
| }; | |
| return ( | |
| <div className="h-screen flex flex-col overflow-hidden bg-bg-main text-sm relative"> | |
| {showInfo && <InfoModal onClose={() => setShowInfo(false)} />} | |
| {/* 1. Compact Top Navigation Bar (Command Deck Layout) */} | |
| <header className="h-auto md:h-14 border-b border-border bg-bg-panel flex flex-col md:flex-row items-stretch md:items-center px-4 py-3 md:py-0 gap-3 md:gap-0 justify-between shrink-0 z-20"> | |
| {/* Deck 1: Brand HUD */} | |
| <div className="flex items-center justify-between md:justify-start gap-3"> | |
| <div className="flex items-center gap-3"> | |
| <div className="w-8 h-8 text-accent flex items-center justify-center"> | |
| <img src={Logo} alt="Cluster Protocol" className="w-full h-full text-accent" /> | |
| </div> | |
| <h1 className="font-mono font-semibold tracking-tight text-text-primary"> | |
| CLUSTER<span className="text-text-secondary">PROTOCOL</span> | |
| </h1> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <span className="px-2 py-0.5 rounded-full bg-border text-[10px] text-text-secondary font-mono">v2.1.0</span> | |
| <button onClick={() => setShowInfo(true)} className="text-text-secondary hover:text-accent transition-colors"> | |
| <Info size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Deck 2: Command Line (Search) */} | |
| <div className="flex items-center gap-2 w-full md:max-w-md"> | |
| <div className="relative flex-grow group"> | |
| <div className="absolute left-3 top-1/2 -translate-y-1/2 text-text-secondary"> | |
| <Terminal size={14} /> | |
| </div> | |
| <input | |
| type="text" | |
| value={wallet} | |
| onChange={(e) => setWallet(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && analyzeWallet()} | |
| placeholder="0x..." | |
| className="w-full bg-bg-main border border-border text-text-primary pl-9 pr-3 py-1.5 font-mono text-xs focus:outline-none focus:border-accent transition-colors rounded-sm" | |
| disabled={status === 'loading'} | |
| /> | |
| </div> | |
| <button | |
| onClick={analyzeWallet} | |
| disabled={status === 'loading'} | |
| className="px-4 py-1.5 bg-accent hover:bg-amber-400 text-black font-semibold text-xs uppercase tracking-wide rounded-sm transition-colors disabled:opacity-50" | |
| > | |
| {status === 'loading' ? "..." : "RUN"} | |
| </button> | |
| </div> | |
| </header> | |
| {/* Error Toast */} | |
| {errorMsg && ( | |
| <div className="bg-red-900/20 border-b border-red-900/50 text-red-400 px-4 py-2 text-xs font-mono text-center"> | |
| ! {errorMsg} | |
| </div> | |
| )} | |
| {/* Main Content - Flex Layout to avoid scroll */} | |
| <main className="flex-grow flex flex-col md:flex-row items-center justify-center p-4 md:p-6 overflow-y-auto md:overflow-hidden relative"> | |
| {/* Empty / Error State */} | |
| {(status === 'idle' || status === 'error') && ( | |
| <div className="text-center text-text-secondary space-y-4 max-w-md mt-10 md:mt-0"> | |
| <Layers className={`w-12 h-12 mx-auto ${status === 'error' ? 'text-red-500 opacity-50' : 'opacity-20'}`} /> | |
| <div className="space-y-1"> | |
| <h2 className={`font-medium ${status === 'error' ? 'text-red-400' : 'text-text-primary'}`}> | |
| {status === 'error' ? 'System Failure' : 'Ready to Process'} | |
| </h2> | |
| <p className="text-xs"> | |
| {status === 'error' | |
| ? "The neural uplink encountered an error. Check address and retry." | |
| : "Enter a wallet address above to initiate the segmentation engine."} | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| {/* Loading State */} | |
| {status === 'loading' && ( | |
| <div className="w-64 space-y-2 mt-10 md:mt-0"> | |
| <div className="h-1 bg-border overflow-hidden rounded-full"> | |
| <div className="h-full bg-accent w-1/3 animate-[shimmer_1s_infinite_linear]"></div> | |
| </div> | |
| <StatusCycler /> | |
| </div> | |
| )} | |
| {/* Dashboard Grid */} | |
| <AnimatePresence> | |
| {status === 'success' && data && ( | |
| <motion.div | |
| initial={{ opacity: 0, scale: 0.98 }} | |
| animate={{ opacity: 1, scale: 1 }} | |
| transition={{ duration: 0.2 }} | |
| className="w-full max-w-6xl h-auto md:h-full grid grid-cols-1 md:grid-cols-12 gap-4 grid-rows-[auto_1fr] md:grid-rows-1 pb-10 md:pb-0" | |
| > | |
| {/* Col 1: Visuals (Radar) - 5 Cols */} | |
| <div className="md:col-span-5 bg-bg-panel border border-border flex flex-col min-h-[350px] md:min-h-0"> | |
| <div className="p-3 border-b border-border flex justify-between items-center"> | |
| <span className="text-xs font-mono text-text-primary font-semibold uppercase tracking-wider">Behavioral Topology</span> | |
| <Activity size={12} className="text-text-secondary" /> | |
| </div> | |
| <div className="flex-grow min-h-[250px] relative p-4"> | |
| <PersonaChart scores={data.confidence_scores} /> | |
| </div> | |
| <div className="p-3 border-t border-border bg-bg-main"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <span className="text-xs text-text-secondary">Primary Classification</span> | |
| <span className="text-accent text-xs font-mono font-bold">{data.persona}</span> | |
| </div> | |
| <div className="w-full h-1.5 bg-border rounded-full overflow-hidden"> | |
| <div | |
| className="h-full bg-accent" | |
| style={{ width: `${Math.max(...Object.values(data.confidence_scores)) * 100}%` }} | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* Col 2: Metrics & Insights - 7 Cols */} | |
| <div className="md:col-span-7 flex flex-col gap-4 overflow-visible md:overflow-y-auto pr-1"> | |
| {/* Top Row: Metrics */} | |
| <div className="grid grid-cols-3 gap-2 md:gap-4 h-20 md:h-24 shrink-0"> | |
| <MetricCard | |
| label="TX_COUNT" | |
| value={data.stats.tx_count} | |
| icon={<Hash size={14} />} | |
| /> | |
| <MetricCard | |
| label="NFT_VAL_USD" | |
| value={`$${Math.round(data.stats.total_nft_volume_usd).toLocaleString()}`} | |
| icon={<Database size={14} />} | |
| /> | |
| <MetricCard | |
| label="GAS_ETH" | |
| value={data.stats.total_gas_spent.toFixed(4)} | |
| icon={<Zap size={14} />} | |
| /> | |
| </div> | |
| {/* Bottom Row: Text Analysis */} | |
| <div className="flex-grow bg-bg-panel border border-border flex flex-col"> | |
| <div className="p-3 border-b border-border flex justify-between items-center bg-bg-main/50"> | |
| <span className="text-xs font-mono text-text-primary font-semibold uppercase tracking-wider">Identity Narrative</span> | |
| <div className="flex gap-2"> | |
| <div className="w-2 h-2 rounded-full bg-red-500/20 border border-red-500/50"></div> | |
| <div className="w-2 h-2 rounded-full bg-yellow-500/20 border border-yellow-500/50"></div> | |
| <div className="w-2 h-2 rounded-full bg-green-500/20 border border-green-500/50"></div> | |
| </div> | |
| </div> | |
| <div className="p-0 flex-grow relative overflow-hidden"> | |
| <RoastCard explanation={data.explanation} /> | |
| </div> | |
| <div className="p-3 border-t border-border flex justify-between items-center"> | |
| <button | |
| onClick={handleExport} | |
| className="flex items-center gap-2 text-xs text-text-secondary hover:text-white transition-colors" | |
| > | |
| <Share2 size={12} /> | |
| <span>EXPORT_JSON</span> | |
| </button> | |
| <span className="text-[10px] text-text-secondary font-mono">LATENCY: 42ms</span> | |
| </div> | |
| </div> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </main> | |
| {/* System Status Footer (Visible only in Idle/Error) */} | |
| {(status === 'idle' || status === 'error') && ( | |
| <footer className="h-6 bg-bg-panel border-t border-border flex items-center px-4 justify-between text-[10px] font-mono text-text-secondary uppercase shrink-0 z-20"> | |
| <div className="flex gap-4 md:gap-6"> | |
| <span className="flex items-center gap-1.5"> | |
| <div className={`w-1.5 h-1.5 rounded-full ${status === 'error' ? 'bg-red-500' : 'bg-green-500'}`}></div> | |
| {status === 'error' ? 'SYSTEM_OFFLINE' : 'SYSTEM_ONLINE'} | |
| </span> | |
| <span className="text-text-primary/70">TARGET: ETH_MAINNET</span> | |
| </div> | |
| <div className="flex gap-4"> | |
| <span className="hidden md:block opacity-50">SECURE_UPLINK</span> | |
| <span className="opacity-50">V2.1.0</span> | |
| </div> | |
| </footer> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function MetricCard({ label, value, icon }) { | |
| // Metric display component | |
| return ( | |
| <div className="bg-bg-panel border border-border p-3 flex flex-col justify-between"> | |
| <div className="flex justify-between items-start text-text-secondary"> | |
| <span className="text-[10px] font-mono tracking-wider">{label}</span> | |
| {icon} | |
| </div> | |
| <div className="text-lg font-semibold text-text-primary tracking-tight font-mono"> | |
| {value} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default App; |