File size: 12,534 Bytes
7200823 de40b1a c6b6c96 7200823 de40b1a c6b6c96 7200823 c6b6c96 7200823 de40b1a c6b6c96 de40b1a 7200823 de40b1a 7200823 c6b6c96 7200823 c6b6c96 7200823 c6b6c96 de40b1a c6b6c96 de40b1a 7200823 de40b1a c6b6c96 7200823 de40b1a 7200823 c6b6c96 7200823 de40b1a 7200823 de40b1a c6b6c96 de40b1a c6b6c96 de40b1a c6b6c96 de40b1a 7200823 de40b1a 7200823 c6b6c96 de40b1a c6b6c96 7200823 de40b1a 7200823 c6b6c96 de40b1a c6b6c96 7200823 de40b1a 7200823 c6b6c96 de40b1a c6b6c96 de40b1a c6b6c96 de40b1a 7200823 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 | '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>
)
}
|