import React, { useMemo } from "react"
import { cn } from "@/lib/utils"
import {
User,
Database,
Search,
Brain,
MessageSquare,
FileOutput,
Loader2,
Network,
GitBranch,
TrendingUp,
DollarSign,
Activity,
Globe,
Newspaper,
} from "lucide-react"
import type { MCPStatus, LLMStatus } from "@/lib/api"
// === TYPES ===
type NodeStatus = 'idle' | 'executing' | 'completed' | 'failed' | 'skipped'
type CacheState = 'idle' | 'hit' | 'miss' | 'checking'
interface ProcessFlowProps {
currentStep: string
completedSteps: string[]
mcpStatus: MCPStatus
llmStatus?: LLMStatus
llmProvider?: string
cacheHit?: boolean
stockSelected?: boolean
isSearching?: boolean
revisionCount?: number
isAborted?: boolean
}
// === CONSTANTS ===
const NODE_SIZE = 44
const ICON_SIZE = 24
const MCP_SIZE = 36
const MCP_ICON_SIZE = 20
const LLM_WIDTH = 64
const LLM_HEIGHT = 24
const GAP = 72
const CONNECTOR_PAD = 2
const GROUP_PAD = 4
// ADJUSTED VALUES FOR TIGHT FIT
const ROW_GAP = 68 // Slight reduction to tighten vertical flow
const ROW1_Y = 48 // Increased for labels above containers
const ROW2_Y = ROW1_Y + ROW_GAP
const ROW3_Y = ROW2_Y + ROW_GAP
// SVG dimensions
const SVG_HEIGHT = 218 // Exact content height - scales to fill container
const NODE_COUNT = 6 // Reduced: removed Editor node
const FLOW_WIDTH = GAP * (NODE_COUNT - 1) + NODE_SIZE
const SVG_WIDTH = 480 // Narrower now without Editor
const FLOW_START_X = NODE_SIZE / 2 // Left-aligned with half-node margin
const NODES = {
input: { x: FLOW_START_X, y: ROW1_Y },
cache: { x: FLOW_START_X + GAP, y: ROW1_Y },
a2a: { x: FLOW_START_X + GAP * 2, y: ROW1_Y },
analyzer: { x: FLOW_START_X + GAP * 3, y: ROW1_Y },
critic: { x: FLOW_START_X + GAP * 4, y: ROW1_Y },
output: { x: FLOW_START_X + GAP * 5, y: ROW1_Y }, // Moved up (was editor position)
exchange: { x: FLOW_START_X, y: ROW2_Y },
researcher: { x: FLOW_START_X + GAP * 2, y: ROW3_Y },
}
const MCP_START_X = NODES.researcher.x + NODE_SIZE / 2 + 40
const MCP_GAP_LARGE = 48 // Gap between fundamentals and valuation
const MCP_GAP_SMALL = 40 // Reduced gap for other servers
const MCP_SERVERS = [
{ id: 'fundamentals', label: 'Fundamentals', icon: DollarSign, x: MCP_START_X },
{ id: 'valuation', label: 'Valuation', icon: TrendingUp, x: MCP_START_X + MCP_GAP_LARGE },
{ id: 'volatility', label: 'Volatility', icon: Activity, x: MCP_START_X + MCP_GAP_LARGE + MCP_GAP_SMALL },
{ id: 'macro', label: 'Macro (US)', icon: Globe, x: MCP_START_X + MCP_GAP_LARGE + MCP_GAP_SMALL * 2 },
{ id: 'news', label: 'News', icon: Newspaper, x: MCP_START_X + MCP_GAP_LARGE + MCP_GAP_SMALL * 3 },
{ id: 'sentiment', label: 'Sentiment', icon: MessageSquare, x: MCP_START_X + MCP_GAP_LARGE + MCP_GAP_SMALL * 4 },
]
const AGENTS_CENTER_X = (NODES.analyzer.x + NODES.critic.x) / 2 // Now between Analyzer and Critic only
const LLM_GAP = 68 // LLM_WIDTH (64) + 4px spacing
const LLM_PROVIDERS = [
{ id: 'groq', name: 'Groq', x: AGENTS_CENTER_X - LLM_GAP },
{ id: 'gemini', name: 'Gemini', x: AGENTS_CENTER_X },
{ id: 'openrouter', name: 'OpenRouter', x: AGENTS_CENTER_X + LLM_GAP },
]
const AGENTS_GROUP = {
x: NODES.analyzer.x - NODE_SIZE / 2 - GROUP_PAD,
y: ROW1_Y - NODE_SIZE / 2 - GROUP_PAD,
width: NODES.critic.x - NODES.analyzer.x + NODE_SIZE + GROUP_PAD * 2, // Now only Analyzer + Critic
height: NODE_SIZE + GROUP_PAD * 2,
}
const LLM_GROUP = {
x: LLM_PROVIDERS[0].x - LLM_WIDTH / 2 - GROUP_PAD,
y: ROW2_Y - LLM_HEIGHT / 2 - GROUP_PAD,
width: LLM_PROVIDERS[2].x - LLM_PROVIDERS[0].x + LLM_WIDTH + GROUP_PAD * 2,
height: LLM_HEIGHT + GROUP_PAD * 2,
}
const MCP_GROUP = {
x: MCP_SERVERS[0].x - MCP_SIZE / 2 - GROUP_PAD,
y: ROW3_Y - MCP_SIZE / 2 - GROUP_PAD,
width: MCP_SERVERS[5].x - MCP_SERVERS[0].x + MCP_SIZE + GROUP_PAD * 2,
height: MCP_SIZE + GROUP_PAD * 2,
}
// === HELPER FUNCTIONS ===
function normalizeStep(step: string): string {
const lower = step.toLowerCase()
if (lower === 'completed') return 'output'
return lower
}
function getNodeStatus(
stepId: string,
currentStep: string,
completedSteps: string[],
cacheHit?: boolean
): NodeStatus {
const normalizedCurrent = normalizeStep(currentStep)
const normalizedCompleted = completedSteps.map(normalizeStep)
// On cache hit, intermediate steps stay idle (not completed)
if (cacheHit && ['researcher', 'analyzer', 'critic', 'a2a'].includes(stepId)) {
return 'idle'
}
if (normalizedCompleted.includes(stepId)) return 'completed'
if (normalizedCurrent === stepId) return 'executing'
return 'idle'
}
// === SVG SUB-COMPONENTS ===
function ArrowMarkers() {
return (
{['idle', 'executing', 'completed', 'failed'].map((status) => (
{/* Forward arrow (end) */}
{/* Reverse arrow (start) for bidirectional */}
))}
)
}
function SVGNode({
x,
y,
icon: Icon,
label,
label2,
status,
isDiamond = false,
cacheState,
isAgent = false,
hasBorder = true,
labelPosition = 'below',
flipIcon = false,
}: {
x: number
y: number
icon: React.ElementType
label: string
label2?: string
status: NodeStatus
isDiamond?: boolean
cacheState?: CacheState
isAgent?: boolean
hasBorder?: boolean
labelPosition?: 'above' | 'below'
flipIcon?: boolean
}) {
const isExecuting = status === 'executing' || cacheState === 'checking'
const opacity = status === 'idle' && !cacheState ? 0.7 : status === 'skipped' ? 0.7 : 1
const strokeWidth = hasBorder ? 1 : 0
// Label positioning
const labelY = labelPosition === 'above'
? y - NODE_SIZE / 2 - (label2 ? 16 : 8)
: y + NODE_SIZE / 2 + 10
return (
{isExecuting ? (
) : (
)}
{label}
{label2 && (
{label2}
)}
)
}
// === MAIN COMPONENT ===
export function ProcessFlow({
currentStep,
completedSteps,
mcpStatus,
llmStatus,
llmProvider = 'groq',
cacheHit = false,
stockSelected = false,
isSearching = false,
revisionCount = 0,
isAborted = false,
}: ProcessFlowProps) {
// Logic derivations - when aborted, stop all executing states
const inputStatus = stockSelected ? 'completed' : getNodeStatus('input', currentStep, completedSteps, cacheHit)
const exchangeStatus = stockSelected ? 'completed' : isSearching ? 'executing' : 'idle'
// When aborted, freeze agent nodes at their last completed state (no executing)
const analyzerStatus = isAborted
? (completedSteps.includes('analyzer') ? 'completed' : 'idle')
: getNodeStatus('analyzer', currentStep, completedSteps, cacheHit)
const criticStatus = isAborted
? (completedSteps.includes('critic') ? 'completed' : 'idle')
: getNodeStatus('critic', currentStep, completedSteps, cacheHit)
const outputStatus = isAborted
? (completedSteps.includes('output') ? 'completed' : 'idle')
: getNodeStatus('output', currentStep, completedSteps, cacheHit)
const researcherStatus = isAborted
? (completedSteps.includes('researcher') ? 'completed' : 'idle')
: getNodeStatus('researcher', currentStep, completedSteps, cacheHit)
const a2aStatus = isAborted
? (completedSteps.includes('researcher') ? 'completed' : 'idle')
: (researcherStatus === 'executing' ? 'executing' : researcherStatus === 'completed' ? 'completed' : 'idle')
const cacheState: CacheState = useMemo(() => {
if (currentStep === 'cache') return 'checking'
if (completedSteps.includes('cache')) return cacheHit ? 'hit' : 'miss'
return 'idle'
}, [currentStep, completedSteps, cacheHit])
// Completion halo: workflow completed successfully
const allDone = useMemo(() => {
const normalizedCompleted = completedSteps.map(normalizeStep)
const essentialSteps = ['input', 'cache', 'researcher', 'analyzer', 'critic', 'output']
return essentialSteps.every(s => normalizedCompleted.includes(s))
}, [completedSteps])
const conn = (from: NodeStatus | CacheState, to: NodeStatus): NodeStatus => {
if (from === 'completed' || from === 'miss' || from === 'hit') {
return to === 'idle' ? 'idle' : to === 'executing' ? 'executing' : 'completed'
}
return 'idle'
}
// Positioning helpers
const nodeRight = (n: { x: number }) => n.x + NODE_SIZE / 2 + CONNECTOR_PAD
const nodeLeft = (n: { x: number }) => n.x - NODE_SIZE / 2 - CONNECTOR_PAD
const nodeBottom = (n: { y: number }) => n.y + NODE_SIZE / 2 + CONNECTOR_PAD
const nodeTop = (n: { y: number }) => n.y - NODE_SIZE / 2 - CONNECTOR_PAD
// Diamond corners (rotated 45°, half-diagonal = NODE_SIZE * sqrt(2) / 2)
const diamondLeft = (n: { x: number }) => n.x - NODE_SIZE * Math.sqrt(2) / 2 - CONNECTOR_PAD
const diamondRight = (n: { x: number }) => n.x + NODE_SIZE * Math.sqrt(2) / 2 + CONNECTOR_PAD
return (
)
}
export default ProcessFlow