VyaparFlow / frontend /src /components /center /AgentGraph.tsx
Dipan04's picture
Add application file
32d42b3
import { useEffect, useRef } from 'react'
import { useSimulationStore } from '../../lib/simulationStore'
import { motion } from 'framer-motion'
import { clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import { Microscope, Database, ShieldCheck, Receipt, CreditCard, BookText, BrainCircuit } from 'lucide-react'
function cn(...inputs: (string | undefined | null | false)[]) {
return twMerge(clsx(inputs))
}
const AGENT_ICONS: Record<string, React.ElementType> = {
intent: Microscope,
extraction: Database,
validation: ShieldCheck,
invoice: Receipt,
payment: CreditCard,
ledger: BookText,
autonomy: BrainCircuit,
}
export function AgentGraph() {
const { agentSteps, state } = useSimulationStore()
const scrollContainerRef = useRef<HTMLDivElement>(null)
// Auto-scroll to active node
useEffect(() => {
if (state === 'simulating' && scrollContainerRef.current) {
const activeElement = scrollContainerRef.current.querySelector('[data-active="true"]')
if (activeElement) {
activeElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
}
}
}, [agentSteps, state])
return (
<div className="flex flex-col bg-[#0d0d12] rounded-md border border-divider/40 p-3 shrink-0 relative overflow-hidden h-auto">
<div className="flex items-center justify-between mb-4 border-b border-divider/20 pb-2">
<h2 className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em] flex items-center gap-1.5 px-1">
<BrainCircuit size={11} className="text-blue-500" />
Pipeline Execution
</h2>
<div className="flex gap-4 items-center px-1">
<div className="flex gap-1.5 items-center">
<div className="w-1.5 h-1.5 rounded-full bg-divider/40"></div>
<span className="text-[8px] font-bold uppercase tracking-widest text-text-muted/60">Ready</span>
</div>
<div className="flex gap-1.5 items-center">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-[pulse_1s_infinite]"></div>
<span className="text-[8px] font-bold uppercase tracking-widest text-blue-500">Active</span>
</div>
<div className="flex gap-1.5 items-center">
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
<span className="text-[8px] font-bold uppercase tracking-widest text-emerald-500">Done</span>
</div>
</div>
</div>
<div
ref={scrollContainerRef}
className="flex-1 flex items-center overflow-x-auto overflow-y-hidden pb-4 pt-2 -mx-2 px-2 custom-scrollbar snap-x snap-mandatory"
>
<div className="flex flex-nowrap items-center min-w-max">
{agentSteps.map((step, index) => {
const Icon = AGENT_ICONS[step.key] || Microscope
const isLast = index === agentSteps.length - 1
const isRunning = step.status === 'running'
const isDone = step.status === 'done'
const isFailed = step.status === 'failed'
let stateColor = 'bg-[#121218] border-divider/40 text-text-muted/40 opacity-40'
let iconColor = 'text-text-muted/30'
if (isRunning) {
stateColor = 'bg-blue-500/10 border-blue-500/60 text-blue-100 opacity-100 z-10 scale-105 shadow-lg shadow-blue-500/5'
iconColor = 'text-blue-400'
} else if (isDone) {
stateColor = 'bg-emerald-500/5 border-emerald-500/40 text-emerald-100 opacity-100'
iconColor = 'text-emerald-500'
} else if (isFailed) {
stateColor = 'bg-red-500/10 border-red-500/60 text-red-100 opacity-100'
iconColor = 'text-red-500'
}
return (
<div
key={step.id}
className="flex items-center relative z-10 snap-center shrink-0"
data-active={isRunning}
>
<div className="flex flex-col items-center gap-1.5">
<motion.div
className={cn(
"flex items-center justify-center p-0 rounded-sm border transition-all duration-300",
stateColor,
"w-12 h-12 shrink-0 border-dashed"
)}
>
<Icon size={16} strokeWidth={2.5} className={cn("transition-colors duration-300", iconColor)} />
</motion.div>
<span className={cn(
"text-[8px] uppercase tracking-[1px] font-bold text-center",
isRunning ? "text-blue-400" : isDone ? "text-emerald-500" : isFailed ? "text-red-500" : "text-text-muted/40"
)}>
{step.key}
</span>
</div>
{!isLast && (
<div className="w-8 h-[1px] relative mx-2 shrink-0 self-center -mt-3.5">
<div className="absolute inset-0 bg-divider/30"></div>
{isDone && (
<div className="absolute top-0 left-0 bottom-0 bg-emerald-500/60" style={{ width: '100%' }} />
)}
{isRunning && (
<div className="absolute top-0 left-0 bottom-0 bg-blue-500 animate-pulse" style={{ width: '50%' }} />
)}
</div>
)}
</div>
)
})}
</div>
</div>
</div>
)
}