File size: 4,512 Bytes
de40b1a
c6b6c96
de40b1a
 
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
 
c6b6c96
de40b1a
c6b6c96
 
de40b1a
c6b6c96
de40b1a
 
c6b6c96
 
de40b1a
 
 
 
 
c6b6c96
de40b1a
 
 
 
c6b6c96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
de40b1a
 
 
 
 
 
 
c6b6c96
 
 
 
 
 
de40b1a
 
 
 
 
 
c6b6c96
 
 
 
 
de40b1a
c6b6c96
 
de40b1a
 
 
 
 
 
 
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
'use client'
import { useState, useEffect, useRef } from 'react'
import { cn } from '@/lib/utils'
import { agentMsgs } from '@/lib/mock-data'
import { Bot, ChevronDown, ChevronUp, Zap } from 'lucide-react'

interface LiveEvent {
  ingestionId: string; wallet: string; eventName: string
  risk?: string; score?: number; firedAt: string; source: string
}

const EVENT_EMOJI: Record<string, string> = {
  churn_risk_high: '🚨',
  churn_risk_medium: '⚠️',
  comeback_detected: 'πŸ”₯',
  streak_maintained: '⚑',
  volume_milestone: 'πŸ†',
  inactivity_detected: 'πŸ’€',
  referral_from_saved: '🀝',
}

function liveEventToMsg(e: LiveEvent): string {
  const emoji = EVENT_EMOJI[e.eventName] || 'πŸ“‘'
  const short = `${e.wallet.slice(0, 6)}...${e.wallet.slice(-4)}`
  const label = e.eventName.replace(/_/g, ' ')
  const score = e.score !== undefined ? ` (score=${e.score})` : ''
  return `${emoji} LIVE: ${label} β†’ ${short}${score} [${e.ingestionId.slice(0, 8)}]`
}

export function AgentFeed() {
  const [msgs, setMsgs] = useState<{ text: string; time: string; live?: boolean }[]>([])
  const [open, setOpen] = useState(true)
  const [liveCount, setLiveCount] = useState(0)
  const seenIds = useRef(new Set<string>())

  // Seed mock messages
  useEffect(() => {
    const init = agentMsgs.slice(0, 5).map((t, i) => ({
      text: t,
      time: new Date(Date.now() - i * 120000).toLocaleTimeString('en-US', { hour12: false }),
    }))
    setMsgs(init)
    let c = 5
    const iv = setInterval(() => {
      const t = agentMsgs[c++ % agentMsgs.length]
      setMsgs(p => [{ text: t, time: new Date().toLocaleTimeString('en-US', { hour12: false }) }, ...p].slice(0, 30))
    }, 4000)
    return () => clearInterval(iv)
  }, [])

  // Poll real events and inject at top
  useEffect(() => {
    const poll = async () => {
      try {
        const res = await fetch('/api/torque/events/recent?limit=10')
        if (!res.ok) return
        const data = await res.json()
        const events: LiveEvent[] = data.events || []
        const newEvents = events.filter(e => !seenIds.current.has(e.ingestionId))
        if (newEvents.length === 0) return
        newEvents.forEach(e => seenIds.current.add(e.ingestionId))
        const now = new Date().toLocaleTimeString('en-US', { hour12: false })
        const liveLines = newEvents.map(e => ({ text: liveEventToMsg(e), time: now, live: true }))
        setMsgs(p => [...liveLines, ...p].slice(0, 30))
        setLiveCount(data.total || 0)
      } catch {}
    }
    poll()
    const iv = setInterval(poll, 4000)
    return () => clearInterval(iv)
  }, [])

  return (
    <div className="rounded-xl bg-surface-card border border-hairline-dark overflow-hidden">
      <button onClick={() => setOpen(!open)} className="w-full flex items-center justify-between px-5 py-3 border-b border-hairline-dark hover:bg-surface-elevated transition">
        <div className="flex items-center gap-2">
          <Bot className="w-4 h-4 text-brand-yellow" />
          <span className="text-title-sm">AI Agent Feed</span>
          <div className="w-2 h-2 rounded-full bg-trading-up animate-pulse" />
          {liveCount > 0 && (
            <div className="flex items-center gap-1 px-2 py-0.5 rounded-pill bg-trading-up/10 border border-trading-up/20">
              <Zap className="w-2.5 h-2.5 text-trading-up" />
              <span className="text-[10px] text-trading-up font-semibold">{liveCount} live</span>
            </div>
          )}
        </div>
        {open ? <ChevronUp className="w-4 h-4 text-muted" /> : <ChevronDown className="w-4 h-4 text-muted" />}
      </button>
      {open && (
        <div className="max-h-[300px] overflow-y-auto">
          {msgs.map((m, i) => (
            <div key={i} className={cn(
              'flex items-start gap-3 px-5 py-2.5 border-b border-hairline-dark/50 transition-colors',
              i === 0 && 'animate-slide-up',
              m.live ? 'bg-trading-up/5 border-l-2 border-trading-up/50' : i === 0 && 'bg-brand-yellow/5'
            )}>
              <span className="font-mono text-caption text-muted tabular-nums whitespace-nowrap mt-0.5">{m.time}</span>
              <span className={cn('text-body-sm flex-1', m.live ? 'text-[#eaecef] font-medium' : 'text-[#eaecef]')}>{m.text}</span>
              {m.live && <span className="text-[10px] text-trading-up font-semibold uppercase tracking-wider whitespace-nowrap">LIVE</span>}
            </div>
          ))}
        </div>
      )}
    </div>
  )
}