Spaces:
Sleeping
Sleeping
| import React, { useMemo, useState, useEffect } from 'react'; | |
| /* Fix: react-router-dom exports may be flaky in this environment, using standard v6 imports */ | |
| import { useNavigate } from 'react-router-dom'; | |
| import { | |
| AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer | |
| } from 'recharts'; | |
| import { | |
| ArrowUpRight, | |
| ArrowDownRight, | |
| Wallet, | |
| Zap, | |
| Activity, | |
| Globe, | |
| ShieldCheck, | |
| ChevronRight, | |
| Terminal, | |
| ShieldAlert, | |
| Building2, | |
| RefreshCw, | |
| Sparkles, | |
| TrendingUp, | |
| Shield, | |
| Loader2 | |
| } from 'lucide-react'; | |
| /* Fix: removed .ts extensions from imports */ | |
| import { getSystemIntelligenceFeed, getPortfolioSuggestions } from '../services/geminiService'; | |
| import { apiClient } from '../services/api'; | |
| import { cryptoService } from '../services/cryptoService'; | |
| import { InternalAccount, AIInsight } from '../types/index'; | |
| import { CardSkeleton, Skeleton } from '../components/Skeleton'; | |
| const MOCK_BALANCE_HISTORY = { | |
| '24H': [ | |
| { time: '00:00', balance: 1245000 }, | |
| { time: '04:00', balance: 1247000 }, | |
| { time: '08:00', balance: 1246000 }, | |
| { time: '12:00', balance: 1248000 }, | |
| { time: '16:00', balance: 1249103 }, | |
| { time: '20:00', balance: 1251200 }, | |
| ], | |
| '7D': [ | |
| { time: 'Mon', balance: 1100000 }, | |
| { time: 'Tue', balance: 1150000 }, | |
| { time: 'Wed', balance: 1240000 }, | |
| { time: 'Thu', balance: 1220000 }, | |
| { time: 'Fri', balance: 1250000 }, | |
| { time: 'Sat', balance: 1260000 }, | |
| { time: 'Sun', balance: 1251200 }, | |
| ] | |
| }; | |
| const StatCard = ({ label, value, trend, icon: Icon, color, subValue, onClick, loading }: any) => { | |
| if (loading) return <CardSkeleton />; | |
| return ( | |
| <div | |
| onClick={onClick} | |
| className="bg-zinc-950 border border-zinc-900 p-8 rounded-[2rem] hover:border-blue-500/30 transition-all group relative overflow-hidden shadow-2xl cursor-pointer" | |
| > | |
| <div className="absolute top-0 right-0 p-4 opacity-[0.03] group-hover:opacity-10 transition-opacity"> | |
| <Icon size={120} /> | |
| </div> | |
| <div className="flex justify-between items-start mb-6 relative z-10"> | |
| <div className={`p-4 rounded-2xl bg-${color}-500/10 text-${color}-500 group-hover:scale-110 transition-transform duration-500`}> | |
| <Icon size={24} /> | |
| </div> | |
| <div className={`flex items-center space-x-1.5 px-3 py-1 rounded-full ${trend >= 0 ? 'bg-emerald-500/10 text-emerald-500' : 'bg-rose-500/10 text-rose-500'} text-[10px] font-black uppercase tracking-widest`}> | |
| {trend >= 0 ? <ArrowUpRight size={12} /> : <ArrowDownRight size={12} />} | |
| <span>{Math.abs(trend)}%</span> | |
| </div> | |
| </div> | |
| <div className="relative z-10"> | |
| <p className="text-zinc-500 text-[10px] font-black uppercase tracking-[0.2em] mb-2">{label}</p> | |
| <h3 className="text-3xl font-bold text-white mono tracking-tighter mb-1">{value}</h3> | |
| {subValue && <p className="text-[10px] text-zinc-600 font-bold uppercase tracking-widest">{subValue}</p>} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const RecommendationCard = ({ suggestion, loading }: any) => { | |
| if (loading) return <Skeleton className="h-40 rounded-[2.5rem]" />; | |
| const icons = { | |
| ALPHA: <TrendingUp size={20} className="text-blue-400" />, | |
| RISK: <Shield size={20} className="text-rose-400" />, | |
| LIQUIDITY: <Zap size={20} className="text-emerald-400" /> | |
| }; | |
| const borderColors = { | |
| ALPHA: 'hover:border-blue-500/40', | |
| RISK: 'hover:border-rose-500/40', | |
| LIQUIDITY: 'hover:border-emerald-500/40' | |
| }; | |
| return ( | |
| <div className={`bg-zinc-950 border border-zinc-900 p-8 rounded-[2.5rem] transition-all group relative overflow-hidden shadow-2xl ${borderColors[suggestion.type as keyof typeof borderColors] || 'hover:border-zinc-700'}`}> | |
| <div className="flex items-center justify-between mb-6"> | |
| <div className="p-3 bg-zinc-900 rounded-xl"> | |
| {icons[suggestion.type as keyof typeof icons]} | |
| </div> | |
| <span className="text-[8px] font-black uppercase tracking-widest text-zinc-600 bg-black px-2 py-1 rounded border border-zinc-800">{suggestion.type}</span> | |
| </div> | |
| <h4 className="text-sm font-black text-white uppercase italic tracking-widest mb-2 truncate pr-4">{suggestion.title}</h4> | |
| <p className="text-[11px] text-zinc-500 leading-relaxed font-medium group-hover:text-zinc-300 transition-colors">"{suggestion.description}"</p> | |
| <div className="mt-6 flex justify-end"> | |
| <button className="text-[9px] font-black text-blue-500 uppercase tracking-widest hover:text-white transition-colors flex items-center gap-1.5"> | |
| Execute Node <ChevronRight size={10} /> | |
| </button> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const Overview: React.FC = () => { | |
| const navigate = useNavigate(); | |
| const [activeRange, setActiveRange] = useState<keyof typeof MOCK_BALANCE_HISTORY>('24H'); | |
| const [intelFeed, setIntelFeed] = useState<AIInsight[]>([]); | |
| const [bankingAccounts, setBankingAccounts] = useState<InternalAccount[]>([]); | |
| const [suggestions, setSuggestions] = useState<any[]>([]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [loadingSuggestions, setLoadingSuggestions] = useState(true); | |
| const [error, setError] = useState<string | null>(null); | |
| const loadData = async () => { | |
| setIsLoading(true); | |
| setError(null); | |
| try { | |
| const [accounts, intel, globalMarket] = await Promise.all([ | |
| apiClient.getRegistryNodes(), | |
| getSystemIntelligenceFeed(), | |
| cryptoService.getGlobal() | |
| ]); | |
| setBankingAccounts(accounts); | |
| setIntelFeed(intel); | |
| // Trigger proactive suggestions based on gathered context | |
| refreshSuggestions(accounts, globalMarket); | |
| } catch (e: any) { | |
| console.error(e); | |
| setError("Synchronous parity check failed. Using fallback node."); | |
| setBankingAccounts([{ | |
| id: 'fallback_01', | |
| productName: 'Emergency Parity Node', | |
| displayAccountNumber: '•••• 0000', | |
| currency: 'USD', | |
| status: 'ACTIVE', | |
| currentBalance: 0, | |
| availableBalance: 0, | |
| institutionName: 'Local Ledger', | |
| connectionId: 'LOC-001' | |
| }]); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const refreshSuggestions = async (accounts: InternalAccount[], globalMarket: any) => { | |
| setLoadingSuggestions(true); | |
| try { | |
| const context = { | |
| totalLiquidity: accounts.reduce((s, a) => s + a.availableBalance, 0), | |
| activeNodes: accounts.length, | |
| globalMarketCap: globalMarket?.total_market_cap?.usd | |
| }; | |
| const strategicRecs = await getPortfolioSuggestions(context); | |
| setSuggestions(strategicRecs); | |
| } catch (err) { | |
| console.error("Neural strategist link failure."); | |
| } finally { | |
| setLoadingSuggestions(false); | |
| } | |
| }; | |
| useEffect(() => { | |
| loadData(); | |
| }, []); | |
| const totalLiquidity = useMemo(() => { | |
| return bankingAccounts.reduce((sum, acc) => sum + acc.availableBalance, 0); | |
| }, [bankingAccounts]); | |
| return ( | |
| <div className="space-y-10 animate-in fade-in duration-700 pb-20"> | |
| {error && ( | |
| <div className="bg-rose-500/10 border border-rose-500/20 p-4 rounded-2xl flex items-center gap-4 text-rose-500 text-xs font-bold uppercase tracking-widest"> | |
| <ShieldAlert size={16} /> | |
| {error} | |
| </div> | |
| )} | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> | |
| <StatCard | |
| loading={isLoading} | |
| label="Total Liquidity" | |
| value={`$${totalLiquidity.toLocaleString()}`} | |
| trend={12.4} | |
| icon={Wallet} | |
| color="blue" | |
| subValue={`${bankingAccounts.length} Registry Nodes Active`} | |
| onClick={() => navigate('/registry')} | |
| /> | |
| <StatCard loading={isLoading} label="Network Health" value="99.98%" trend={0.01} icon={ShieldCheck} color="emerald" subValue="Handshake Integrity: High" /> | |
| <StatCard loading={isLoading} label="Fabric Load" value="1.2 P/s" trend={0.5} icon={Zap} color="blue" subValue="Subspace Polling Active" /> | |
| <StatCard loading={isLoading} label="Deficit Offset" value="1,242 Kg" trend={-4.2} icon={Globe} color="rose" subValue="ESG Neutrality Level: AA" /> | |
| </div> | |
| {/* Neural Strategy Center Section */} | |
| <div className="bg-zinc-950/40 border border-zinc-900 rounded-[3rem] p-10 shadow-2xl relative overflow-hidden"> | |
| <div className="absolute top-0 right-0 p-10 opacity-[0.03]"> | |
| <Sparkles size={200} className="text-blue-500" /> | |
| </div> | |
| <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 gap-6 relative z-10"> | |
| <div> | |
| <h2 className="text-2xl font-black text-white italic tracking-tighter uppercase mb-2">Neural <span className="text-blue-500 not-italic">Strategy Center</span></h2> | |
| <p className="text-zinc-500 text-[10px] font-bold uppercase tracking-widest">Actionable alpha derived from treasury mesh telemetry</p> | |
| </div> | |
| <button | |
| onClick={() => refreshSuggestions(bankingAccounts, null)} | |
| disabled={loadingSuggestions} | |
| className="flex items-center gap-3 px-6 py-3 bg-zinc-900 border border-zinc-800 hover:border-zinc-700 text-white rounded-2xl font-black text-[10px] uppercase tracking-widest transition-all" | |
| > | |
| {loadingSuggestions ? <Loader2 size={14} className="animate-spin" /> : <RefreshCw size={14} />} | |
| <span>Recalculate Alpha</span> | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 relative z-10"> | |
| {loadingSuggestions ? ( | |
| Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-44 rounded-[2.5rem]" />) | |
| ) : ( | |
| suggestions.map((s, idx) => ( | |
| <RecommendationCard key={idx} suggestion={s} loading={loadingSuggestions} /> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-12 gap-8"> | |
| <div className="col-span-12 lg:col-span-8 space-y-8 min-w-0"> | |
| <div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-10 relative overflow-hidden shadow-2xl"> | |
| <div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 relative z-10 gap-6"> | |
| <div> | |
| <h2 className="text-2xl font-black text-white italic tracking-tighter uppercase mb-2">Aggregate <span className="text-blue-500 not-italic">Cash Curve</span></h2> | |
| <div className="flex items-center gap-3"> | |
| <span className={`w-2 h-2 rounded-full ${isLoading ? 'bg-zinc-800' : 'bg-emerald-500'} animate-pulse`}></span> | |
| <p className="text-zinc-500 text-[10px] font-bold uppercase tracking-widest">Live Node Trace</p> | |
| </div> | |
| </div> | |
| <div className="flex bg-black p-1.5 rounded-2xl border border-zinc-900"> | |
| {(Object.keys(MOCK_BALANCE_HISTORY) as Array<keyof typeof MOCK_BALANCE_HISTORY>).map(t => ( | |
| <button | |
| key={t} | |
| onClick={() => setActiveRange(t)} | |
| className={`px-6 py-2 rounded-xl text-[10px] font-black transition-all uppercase tracking-widest ${t === activeRange ? 'bg-zinc-100 text-black shadow-lg' : 'text-zinc-500 hover:text-zinc-300'}`} | |
| > | |
| {t} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="w-full relative z-10 min-h-[400px] h-[400px]"> | |
| {isLoading ? <Skeleton className="w-full h-full rounded-2xl" /> : ( | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <AreaChart data={MOCK_BALANCE_HISTORY[activeRange]}> | |
| <defs> | |
| <linearGradient id="colorBalance" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="5%" stopColor="#3b82f6" stopOpacity={0.2}/> | |
| <stop offset="95%" stopColor="#3b82f6" stopOpacity={0}/> | |
| </linearGradient> | |
| </defs> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#18181b" vertical={false} /> | |
| <XAxis dataKey="time" stroke="#3f3f46" fontSize={10} tickLine={false} axisLine={false} tick={{ fontWeight: 800 }} /> | |
| <YAxis stroke="#3f3f46" fontSize={10} tickLine={false} axisLine={false} tickFormatter={(v) => `$${v/1000}k`} tick={{ fontWeight: 800 }} /> | |
| <Tooltip | |
| contentStyle={{ backgroundColor: '#000', border: '1px solid #27272a', borderRadius: '16px', padding: '12px' }} | |
| itemStyle={{ color: '#3b82f6', fontWeight: 900, fontSize: '12px' }} | |
| /> | |
| <Area type="monotone" dataKey="balance" stroke="#3b82f6" strokeWidth={4} fillOpacity={1} fill="url(#colorBalance)" animationDuration={1000} /> | |
| </AreaChart> | |
| </ResponsiveContainer> | |
| )} | |
| </div> | |
| </div> | |
| <div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-10 shadow-2xl"> | |
| <div className="flex justify-between items-center mb-8"> | |
| <h3 className="text-xl font-black text-white italic tracking-tighter uppercase">Registry <span className="text-blue-500 not-italic">Nodes</span></h3> | |
| <button onClick={loadData} className="p-3 bg-zinc-900 rounded-xl text-zinc-500 hover:text-white transition-all"> | |
| <RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} /> | |
| </button> | |
| </div> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| {isLoading ? ( | |
| <> | |
| <Skeleton className="h-32 rounded-[2rem]" /> | |
| <Skeleton className="h-32 rounded-[2rem]" /> | |
| </> | |
| ) : ( | |
| bankingAccounts.map(acc => ( | |
| <div key={acc.id} className="p-8 bg-black/40 border border-zinc-900 rounded-[2rem] hover:border-blue-500/20 transition-all group"> | |
| <div className="flex justify-between items-start mb-6"> | |
| <div className="p-3 bg-blue-600/10 text-blue-500 rounded-xl group-hover:scale-110 transition-transform"> | |
| <Building2 size={20} /> | |
| </div> | |
| <span className="text-[9px] font-black uppercase text-emerald-500 bg-emerald-500/5 px-2.5 py-1 rounded-full border border-emerald-500/10">Active Node</span> | |
| </div> | |
| <h4 className="text-white font-black text-sm uppercase italic mb-1">{acc.productName}</h4> | |
| <p className="text-[10px] text-zinc-600 font-bold uppercase tracking-widest mb-4">{acc.displayAccountNumber}</p> | |
| <div className="flex justify-between items-baseline"> | |
| <p className="text-[9px] font-black text-zinc-500 uppercase tracking-widest">Liquidity</p> | |
| <p className="text-xl font-black text-white mono tracking-tighter">${acc.availableBalance.toLocaleString()}</p> | |
| </div> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| <div className="col-span-12 lg:col-span-4 space-y-8 min-w-0"> | |
| <div className="bg-zinc-950 border border-zinc-900 rounded-[2.5rem] p-10 flex flex-col h-full shadow-2xl"> | |
| <div className="flex items-center justify-between mb-8"> | |
| <div> | |
| <h2 className="text-xl font-black text-white italic tracking-tighter uppercase">Quantum <span className="text-blue-500 not-italic">Intel</span></h2> | |
| <p className="text-zinc-500 text-[10px] font-bold uppercase tracking-[0.2em]">Neural Feedback Stream</p> | |
| </div> | |
| <Terminal size={18} className="text-zinc-700" /> | |
| </div> | |
| <div className="space-y-6 flex-1 overflow-y-auto pr-2 custom-scrollbar"> | |
| {isLoading ? ( | |
| Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-20 rounded-2xl" />) | |
| ) : ( | |
| intelFeed.map((intel, idx) => ( | |
| <div key={idx} className="p-5 bg-black/40 border border-zinc-900 rounded-2xl hover:border-blue-500/30 transition-all group/item"> | |
| <div className="flex items-start gap-4"> | |
| {intel.severity === 'CRITICAL' ? <ShieldAlert size={16} className="text-rose-500 mt-1" /> : <Activity size={16} className="text-blue-500 mt-1" />} | |
| <div> | |
| <p className="text-[10px] font-black text-white uppercase tracking-widest mb-1">{intel.title}</p> | |
| <p className="text-[11px] text-zinc-500 leading-relaxed group-hover/item:text-zinc-300 transition-colors">{intel.description}</p> | |
| </div> | |
| </div> | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| <button onClick={() => navigate('/advisor')} className="mt-8 w-full py-5 bg-zinc-900 hover:bg-zinc-100 hover:text-black text-white rounded-[1.5rem] text-[10px] font-black uppercase tracking-[0.3em] transition-all border border-zinc-800 flex items-center justify-center gap-4"> | |
| <ChevronRight size={18} /> | |
| <span>Deep Advisory</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Overview; | |