'use client'
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
import { Users, ShieldAlert, HeartPulse, TrendingUp, Bot, Flame, ArrowUpRight, ArrowDownRight, Scan, Zap, ExternalLink, Timer, ToggleLeft, ToggleRight } from 'lucide-react'
import { StatCard } from '@/components/ui/StatCard'
import { CampaignBadge } from '@/components/ui/CampaignBadge'
import { AgentFeed } from '@/components/ui/AgentFeed'
import { useToast } from '@/components/ui/Toast'
import { cn, fmtNum, fmtUsd } from '@/lib/utils'
import { stats, events, campaigns, retentionData, churnData, protocols } from '@/lib/mock-data'
import type { CampaignType } from '@/lib/types'
import { useState, useEffect, useCallback, useRef } from 'react'
const Tip = ({ active, payload, label }: any) => {
if (!active || !payload?.length) return null
return (
{label}
{payload[0].value}
)
}
const riskDist = [
{ name: 'Safe', value: 45, color: '#2dbdb6' }, { name: 'Low', value: 25, color: '#0ecb81' },
{ name: 'Medium', value: 15, color: '#FCD535' }, { name: 'High', value: 10, color: '#ff9500' },
{ name: 'Critical', value: 5, color: '#f6465d' },
]
interface LiveEvent {
ingestionId: string; wallet: string; eventName: string
risk?: string; score?: number; firedAt: string; source: string
}
const RISK_COLOR: Record = {
critical: '#f6465d', high: '#ff9500', medium: '#FCD535', low: '#0ecb81', safe: '#2dbdb6'
}
const AUTO_SCAN_INTERVAL = 30
export default function DashboardPage() {
const active = campaigns.filter(c => c.status === 'active')
const { fire: toast } = useToast()
const [scanning, setScanning] = useState(false)
const [autoScan, setAutoScan] = useState(false)
const [countdown, setCountdown] = useState(AUTO_SCAN_INTERVAL)
const [liveEvents, setLiveEvents] = useState([])
const [sessionCount, setSessionCount] = useState(0)
const [torqueStatus, setTorqueStatus] = useState<'connected' | 'unconfigured' | null>(null)
const prevCountRef = useRef(0)
useEffect(() => {
fetch('/api/torque/status').then(r => r.json()).then(d => {
setTorqueStatus(d.configured ? 'connected' : 'unconfigured')
setSessionCount(d.sessionEvents || 0)
}).catch(() => setTorqueStatus('unconfigured'))
}, [])
useEffect(() => {
const poll = () => {
fetch('/api/torque/events/recent?limit=8').then(r => r.json()).then(d => {
const newEvents: LiveEvent[] = d.events || []
const newCount: number = d.total || 0
if (newCount > prevCountRef.current && prevCountRef.current > 0) {
const latest = newEvents[0]
if (latest) {
toast({
type: 'event',
title: latest.eventName.replace(/_/g, ' ').toUpperCase(),
body: `${latest.wallet.slice(0, 8)}...${latest.wallet.slice(-4)} · ${latest.ingestionId.slice(0, 8)}`,
})
}
}
prevCountRef.current = newCount
setLiveEvents(newEvents)
setSessionCount(newCount)
}).catch(() => {})
}
poll()
const iv = setInterval(poll, 5000)
return () => clearInterval(iv)
}, [toast])
const triggerScan = useCallback(async () => {
if (scanning) return
setScanning(true)
try {
const res = await fetch('/api/agent/scan', { method: 'POST' })
const data = await res.json()
if (data.configured) {
toast({ type: 'success', title: `Scan complete — ${data.count} at-risk wallets`, body: `${data.detections?.filter((d: any) => d.eventSent).length || 0} events fired to Torque` })
} else {
toast({ type: 'error', title: 'Torque not configured', body: 'Set TORQUE_INGEST_KEY in .env.local' })
}
const d = await fetch('/api/torque/events/recent?limit=8').then(r => r.json())
setLiveEvents(d.events || [])
setSessionCount(d.total || 0)
} catch {
toast({ type: 'error', title: 'Scan failed', body: 'Check server logs' })
} finally {
setScanning(false)
setCountdown(AUTO_SCAN_INTERVAL)
}
}, [scanning, toast])
// Auto-scan countdown + trigger
useEffect(() => {
if (!autoScan) { setCountdown(AUTO_SCAN_INTERVAL); return }
const tick = setInterval(() => {
setCountdown(c => {
if (c <= 1) { triggerScan(); return AUTO_SCAN_INTERVAL }
return c - 1
})
}, 1000)
return () => clearInterval(tick)
}, [autoScan, triggerScan])
// Keyboard shortcut: S = scan
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === 's' && !e.metaKey && !e.ctrlKey && e.target === document.body) triggerScan()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [triggerScan])
return (
Dashboard
Real-time churn detection and autonomous retention
{torqueStatus !== null && (
{torqueStatus === 'connected' ? 'Torque Connected' : 'Torque Unconfigured'}
)}
{sessionCount > 0 && (
{sessionCount} live events
)}
{/* Auto-scan toggle */}
{/* Live Torque Events Strip */}
{liveEvents.length > 0 && (
{liveEvents.slice(0, 5).map((e, i) => (
{e.eventName}
{e.wallet.slice(0, 8)}...{e.wallet.slice(-4)}
{e.score !== undefined &&
score={e.score}}
{e.ingestionId.slice(0, 12)}...
LIVE
))}
)}
{/* Auto-scan active banner */}
{autoScan && (
Auto-scan active
Next scan in {countdown}s — firing real Torque events for at-risk wallets
)}
Retention Rate
30-day trailing average
} />
Churn Rate
Daily churn percentage
} />
Risk Distribution
{riskDist.map((e, i) => | )}
Recent Events
{events.slice(0, 5).map(e => (
{e.eventType.replace(/_/g, ' ').toUpperCase()}
{e.wallet}
{e.timestamp}
))}
Active Campaigns
{active.slice(0, 4).map(c => (
{c.name}
{fmtNum(c.participantCount)} users
{c.createdBy === 'ai-agent' &&
}
))}
Protocol Performance
| Protocol |
Volume |
Users |
Churn |
Retention |
Avg Streak |
{protocols.map(p => (
|
{fmtUsd(p.volume)} |
{fmtNum(p.users)} |
{p.churnRate}% |
= 70 ? 'text-trading-up' : 'text-brand-yellow')}>{p.retentionRate}% |
{p.avgStreak}d |
))}
)
}