muthuk1's picture
feat: full Torque integration β€” live events, toast, bulk rescue, auto-scan
c6b6c96
'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>
)
}