| 'use client' |
| import { cn, fmtNum } from '@/lib/utils' |
| import { stats, agentMsgs } from '@/lib/mock-data' |
| import { Brain, Zap, Shield, Eye, Play, Pause, Bot, ArrowRight, Cpu, RefreshCw, Target, Activity, AlertTriangle, Scan } from 'lucide-react' |
| import { useState, useEffect, useCallback } from 'react' |
|
|
| type FeedMsg = { text: string; time: string; real?: boolean } |
|
|
| type ScanDetection = { |
| wallet: string; risk: string; score: number; eventType: string; |
| eventSent: boolean; eventId?: string; error?: string |
| } |
|
|
| type ScanResult = { |
| detections: ScanDetection[]; count: number; configured: boolean; timestamp: string |
| } |
|
|
| const RISK_ICON: Record<string, string> = { |
| critical: 'π¨', high: 'β οΈ', medium: 'π', |
| } |
|
|
| export default function AgentPage() { |
| const [on, setOn] = useState(true) |
| const [tab, setTab] = useState<'feed' | 'config'>('feed') |
| const [feed, setFeed] = useState<FeedMsg[]>([]) |
| const [scanning, setScanning] = useState(false) |
| const [configured, setConfigured] = useState<boolean | null>(null) |
|
|
| useEffect(() => { |
| fetch('/api/agent/scan').then(r => r.json()).then(d => setConfigured(d.configured)).catch(() => setConfigured(false)) |
| }, []) |
|
|
| useEffect(() => { |
| const init = agentMsgs.slice(0, 8).map((t, i) => ({ text: t, time: new Date(Date.now() - i * 120000).toLocaleTimeString('en-US', { hour12: false }) })) |
| setFeed(init) |
| if (!on) return |
| let c = 8 |
| const iv = setInterval(() => { |
| const t = agentMsgs[c++ % agentMsgs.length] |
| setFeed(p => [{ text: t, time: new Date().toLocaleTimeString('en-US', { hour12: false }) }, ...p].slice(0, 50)) |
| }, 3000) |
| return () => clearInterval(iv) |
| }, [on]) |
|
|
| const triggerScan = useCallback(async () => { |
| setScanning(true) |
| const now = () => new Date().toLocaleTimeString('en-US', { hour12: false }) |
| try { |
| setFeed(p => [{ text: 'π Triggering real wallet scan via Torque API...', time: now(), real: true }, ...p].slice(0, 50)) |
| const res = await fetch('/api/agent/scan', { method: 'POST' }) |
| const data: ScanResult = await res.json() |
|
|
| const msgs: FeedMsg[] = [] |
|
|
| if (!data.configured) { |
| msgs.push({ text: 'β οΈ TORQUE_API_KEY not set β add it to .env to fire live events', time: now(), real: true }) |
| msgs.push({ text: `π Scan ran locally: ${data.count} at-risk wallets scored (no events sent)`, time: now(), real: true }) |
| } else { |
| msgs.push({ text: `β
Scan complete β ${data.count} at-risk wallets detected`, time: now(), real: true }) |
| } |
|
|
| for (const d of data.detections.slice(0, 6)) { |
| const icon = RISK_ICON[d.risk] || 'π' |
| if (d.eventSent) { |
| msgs.push({ text: `${icon} ${d.risk.toUpperCase()}: ${d.wallet.slice(0, 8)}... β ${d.eventType} sent [${d.eventId}]`, time: now(), real: true }) |
| } else if (data.configured) { |
| msgs.push({ text: `β ${d.risk.toUpperCase()}: ${d.wallet.slice(0, 8)}... β ${d.error}`, time: now(), real: true }) |
| } else { |
| msgs.push({ text: `${icon} ${d.risk.toUpperCase()}: ${d.wallet.slice(0, 8)}... score=${d.score} β ${d.eventType}`, time: now(), real: true }) |
| } |
| } |
|
|
| setFeed(p => [...msgs.reverse(), ...p].slice(0, 50)) |
| setConfigured(data.configured) |
| } catch (e) { |
| setFeed(p => [{ text: 'β Scan failed: ' + String(e), time: now(), real: true }, ...p].slice(0, 50)) |
| } finally { |
| setScanning(false) |
| } |
| }, []) |
|
|
| return ( |
| <div className="p-6 space-y-6"> |
| <div className="flex items-center justify-between"> |
| <div className="flex items-center gap-4"> |
| <div className={cn('w-12 h-12 rounded-xl flex items-center justify-center', on ? 'bg-trading-up/10 animate-pulse-glow' : 'bg-surface-elevated')}><Brain className={cn('w-6 h-6', on ? 'text-trading-up' : 'text-muted')} /></div> |
| <div><h1 className="text-display-sm text-[#eaecef]">AI Agent</h1><p className="text-body-md text-muted mt-0.5">Autonomous churn detection & retention engine</p></div> |
| </div> |
| <div className="flex items-center gap-3"> |
| {configured !== null && ( |
| <div className={cn('px-3 py-1.5 rounded-lg border text-caption font-semibold', configured ? 'bg-trading-up/10 border-trading-up/30 text-trading-up' : 'bg-trading-down/10 border-trading-down/30 text-trading-down')}> |
| {configured ? 'β‘ Torque Connected' : 'β οΈ API Key Missing'} |
| </div> |
| )} |
| <button |
| onClick={triggerScan} |
| disabled={scanning} |
| className="flex items-center gap-2 px-4 py-2.5 rounded-md text-button font-semibold bg-brand-yellow/10 text-brand-yellow border border-brand-yellow/30 hover:bg-brand-yellow/20 transition disabled:opacity-50" |
| > |
| <Scan className={cn('w-4 h-4', scanning && 'animate-spin')} /> |
| {scanning ? 'Scanning...' : 'Scan Now'} |
| </button> |
| <div className={cn('px-4 py-2 rounded-lg border', on ? 'bg-trading-up/10 border-trading-up/30 text-trading-up' : 'bg-surface-card border-hairline-dark text-muted')}> |
| <div className="flex items-center gap-2"><div className={cn('w-2.5 h-2.5 rounded-full', on ? 'bg-trading-up animate-pulse' : 'bg-muted')} /><span className="text-button font-semibold">{on ? 'RUNNING' : 'PAUSED'}</span></div> |
| </div> |
| <button onClick={() => setOn(!on)} className={cn('flex items-center gap-2 px-5 py-2.5 rounded-md text-button font-semibold transition', on ? 'bg-trading-down/10 text-trading-down border border-trading-down/30 hover:bg-trading-down/20' : 'bg-brand-yellow text-ink hover:bg-brand-yellow-active')}> |
| {on ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />}{on ? 'Pause' : 'Start'} |
| </button> |
| </div> |
| </div> |
| |
| <div className="grid grid-cols-2 lg:grid-cols-4 gap-4"> |
| {[{ i: Activity, l: 'Actions Today', v: fmtNum(stats.agentActionsToday), c: 'text-brand-yellow' }, { i: Eye, l: 'Wallets Scanned', v: fmtNum(stats.activeWallets), c: 'text-info' }, { i: Shield, l: 'Wallets Saved', v: fmtNum(stats.walletsSaved), c: 'text-trading-up' }, { i: Target, l: 'Churn Prevented', v: fmtNum(stats.walletsAtRisk), c: 'text-brand-yellow' }].map(s => { const I = s.i; return ( |
| <div key={s.l} className="rounded-xl bg-surface-card border border-hairline-dark p-4"><div className="flex items-center gap-2 mb-2"><I className={cn('w-4 h-4', s.c)} /><span className="text-caption text-muted">{s.l}</span></div><p className={cn('font-mono text-title-lg tabular-nums', s.c === 'text-info' ? 'text-[#eaecef]' : s.c)}>{s.v}</p></div> |
| )})} |
| </div> |
| |
| <div className="flex items-center gap-1 p-1 rounded-lg bg-surface-card border border-hairline-dark w-fit"> |
| {(['feed', 'config'] as const).map(t => <button key={t} onClick={() => setTab(t)} className={cn('px-5 py-2 rounded-md text-button transition capitalize', tab === t ? 'bg-brand-yellow text-ink font-semibold' : 'text-muted hover:text-[#eaecef]')}>{t === 'feed' ? 'Live Feed' : 'Configuration'}</button>)} |
| </div> |
| |
| {tab === 'feed' && ( |
| <div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden"> |
| <div className="p-4 border-b border-hairline-dark flex items-center justify-between"> |
| <div className="flex items-center gap-2"><Bot className="w-4 h-4 text-brand-yellow" /><span className="text-title-sm">Real-Time Agent Feed</span>{on && <div className="w-2 h-2 rounded-full bg-trading-up animate-pulse" />}</div> |
| <div className="flex items-center gap-3"> |
| <span className="text-caption text-muted">{feed.length} messages</span> |
| {!configured && configured !== null && ( |
| <span className="text-caption text-trading-down">Set TORQUE_API_KEY to fire live events</span> |
| )} |
| </div> |
| </div> |
| <div className="max-h-[500px] overflow-y-auto divide-y divide-hairline-dark/50">{feed.map((m, i) => ( |
| <div key={`${m.time}-${i}`} className={cn('flex items-start gap-4 px-5 py-3 transition-colors', i === 0 && on && 'animate-slide-up bg-brand-yellow/5', m.real && 'bg-trading-up/5 border-l-2 border-trading-up/40')}> |
| <span className="font-mono text-caption text-muted tabular-nums whitespace-nowrap mt-0.5">{m.time}</span> |
| <span className={cn('text-body-sm', m.real ? 'text-[#eaecef] font-medium' : 'text-[#eaecef]')}>{m.text}</span> |
| {m.real && <span className="ml-auto text-[10px] text-trading-up font-semibold uppercase tracking-wider whitespace-nowrap">LIVE</span>} |
| </div> |
| ))}</div> |
| </div> |
| )} |
| |
| {tab === 'config' && ( |
| <div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> |
| <div className="rounded-xl bg-surface-card border border-hairline-dark p-5"> |
| <h3 className="text-title-sm mb-4 flex items-center gap-2"><AlertTriangle className="w-4 h-4 text-brand-yellow" />Detection Thresholds</h3> |
| <div className="space-y-3">{[{ l: 'critical', d: 10, v: 90 }, { l: 'high', d: 7, v: 60 }, { l: 'medium', d: 5, v: 30 }].map(t => ( |
| <div key={t.l} className="p-3 rounded-lg bg-surface-elevated border border-hairline-dark/50"> |
| <span className={cn('text-caption font-semibold uppercase', t.l === 'critical' ? 'text-trading-down' : t.l === 'high' ? 'text-[#ff9500]' : 'text-brand-yellow')}>{t.l}</span> |
| <div className="grid grid-cols-2 gap-3 mt-2"><div><span className="text-caption text-muted">Inactive Days</span><p className="font-mono text-num-md text-[#eaecef]">{'>='} {t.d}</p></div><div><span className="text-caption text-muted">Volume Drop</span><p className="font-mono text-num-md text-[#eaecef]">{'>='} {t.v}%</p></div></div> |
| </div> |
| ))}</div> |
| </div> |
| <div className="rounded-xl bg-surface-card border border-hairline-dark p-5"> |
| <h3 className="text-title-sm mb-4 flex items-center gap-2"><Cpu className="w-4 h-4 text-brand-yellow" />Agent Configuration</h3> |
| <div className="space-y-0">{[ |
| ['Scan Interval', '30s'], |
| ['Monitored Protocols', '6'], |
| ['Torque API', configured === null ? 'Checking...' : configured ? 'Connected' : 'Not Configured'], |
| ['Sybil Filter', 'Enabled'], |
| ].map(([k, v]) => ( |
| <div key={k} className="flex items-center justify-between py-2 border-b border-hairline-dark/50"> |
| <span className="text-body-sm text-muted">{k}</span> |
| <span className={cn('font-mono text-num-sm', v === 'Connected' || v === 'Enabled' ? 'text-trading-up font-semibold' : v === 'Not Configured' ? 'text-trading-down font-semibold' : 'text-[#eaecef]')}>{v}</span> |
| </div> |
| ))}</div> |
| <div className="mt-4 p-3 rounded-lg bg-surface-elevated border border-brand-yellow/20"> |
| <p className="text-caption text-muted mb-1">To enable live events:</p> |
| <p className="text-caption text-muted mb-0.5">Get token: platform.torque.so/connect-mcp</p> |
| <code className="text-caption text-brand-yellow font-mono">TORQUE_API_TOKEN=your-token</code> |
| </div> |
| </div> |
| </div> |
| )} |
| |
| <div className="rounded-xl bg-brand-yellow/5 border border-brand-yellow/20 p-6"> |
| <h3 className="text-title-sm text-brand-yellow mb-3">How FlowState AI Agent Works</h3> |
| <div className="grid grid-cols-5 gap-3 items-center"> |
| {[{ i: Eye, l: 'Monitor', d: 'Helius webhooks scan Solana txns' }, { i: Brain, l: 'Detect', d: 'AI scores churn risk per wallet' }, { i: Zap, l: 'Decide', d: 'Select optimal incentive type' }, { i: Bot, l: 'Execute', d: 'Fire events via Torque API' }, { i: RefreshCw, l: 'Learn', d: 'Track outcomes, improve model' }].map((s, i) => ( |
| <div key={s.l} className="text-center"> |
| <div className="w-10 h-10 rounded-lg bg-surface-card border border-brand-yellow/30 flex items-center justify-center mx-auto mb-2"><s.i className="w-5 h-5 text-brand-yellow" /></div> |
| <p className="text-caption text-brand-yellow font-semibold">{s.l}</p> |
| <p className="text-[10px] text-muted mt-0.5">{s.d}</p> |
| {i < 4 && <ArrowRight className="w-4 h-4 text-brand-yellow/30 mx-auto mt-2 hidden lg:block" />} |
| </div> |
| ))} |
| </div> |
| </div> |
| </div> |
| ) |
| } |
|
|