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>
  )
}