flowstate / src /components /ui /AgentFeed.tsx
muthuk1's picture
feat: full Torque integration β€” live events, toast, bulk rescue, auto-scan
c6b6c96
'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>
)
}