TradingAgents/ui/features/run-detail/components/AnalystReports.tsx

273 lines
10 KiB
TypeScript

import { AGENT_STEPS, AGENT_STEP_LABELS, STEP_PHASE } from '@/lib/types/run'
import type { AgentStep } from '@/lib/types/run'
import type { StepStatus } from '@/lib/types/agents'
function formatTokens(n: number): string {
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n)
}
type Phase = 'analysts' | 'researchers' | 'trader' | 'risk'
const MULTI_TURN_STEPS = new Set<AgentStep>([
'bull_researcher', 'bear_researcher',
'aggressive_analyst', 'conservative_analyst', 'neutral_analyst',
])
const STEP_ACCENT: Record<AgentStep, string> = {
market_analyst: '#00C4E8',
news_analyst: '#A78BFA',
fundamentals_analyst: '#00E078',
social_analyst: '#FFB400',
bull_researcher: '#00E078',
bear_researcher: '#FF1F4C',
research_manager: '#00C4E8',
trader: '#FFB400',
aggressive_analyst: '#FF1F4C',
conservative_analyst: '#00C4E8',
neutral_analyst: '#A78BFA',
risk_judge: '#FFB400',
chief_analyst: '#adc6ff',
}
const STEP_ROLE_DESC: Partial<Record<AgentStep, string>> = {
market_analyst: 'Technical & price action',
news_analyst: 'Sentiment & headlines',
fundamentals_analyst: 'Earnings & financials',
social_analyst: 'Social signals',
bull_researcher: 'Bullish thesis',
bear_researcher: 'Bearish thesis',
research_manager: 'Research synthesis',
trader: 'Trade plan',
aggressive_analyst: 'High-risk perspective',
conservative_analyst: 'Risk-averse perspective',
neutral_analyst: 'Balanced view',
risk_judge: 'Final risk decision',
}
type Props = {
phase: Phase
steps: Record<AgentStep, StepStatus>
reports: Record<AgentStep, string[]>
tokensByStep: Record<AgentStep, { in: number; out: number }>
}
export default function AnalystReports({ phase, steps, reports, tokensByStep }: Props) {
const phaseSteps = AGENT_STEPS.filter((s) => STEP_PHASE[s] === phase)
return (
<div className="space-y-3">
{phaseSteps.map((step, stepIdx) => {
const stepStatus = steps[step] ?? 'pending'
const turns = reports[step] ?? []
const isRunning = stepStatus === 'running'
const isDone = stepStatus === 'done'
const isMulti = MULTI_TURN_STEPS.has(step)
const accent = STEP_ACCENT[step]
const roleDesc = STEP_ROLE_DESC[step]
return (
<div key={step} className="animate-fade-up" style={{ animationDelay: `${stepIdx * 40}ms` }}>
{/* Completed turns */}
{turns.map((report, i) => (
<div
key={`${step}-${i}`}
className="mb-2.5 overflow-hidden"
style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-raised)',
borderRadius: '12px',
}}
>
{/* Colored header bar */}
<div
className="px-4 py-2.5 flex items-center justify-between"
style={{
background: `${accent}0D`,
borderBottom: `1px solid ${accent}20`,
}}
>
<div className="flex items-center gap-2.5">
<div
className="w-2 h-2 rounded-full shrink-0"
style={{ background: accent, boxShadow: `0 0 6px ${accent}80` }}
/>
<span
className="text-[13px] font-semibold"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }}
>
{AGENT_STEP_LABELS[step]}
</span>
{roleDesc && (
<span
className="text-[10px]"
style={{ color: accent, opacity: 0.7, fontFamily: 'var(--font-mono)', letterSpacing: '0.04em' }}
>
· {roleDesc}
</span>
)}
{isMulti && (
<span
className="px-2 py-0.5 rounded text-[9px] font-bold"
style={{
background: `${accent}18`,
color: accent,
fontFamily: 'var(--font-mono)',
letterSpacing: '0.06em',
}}
>
T{i + 1}
</span>
)}
{(() => {
const tok = tokensByStep[step]
return tok && (tok.in > 0 || tok.out > 0) ? (
<span
style={{
fontFamily: 'var(--font-mono)',
fontSize: '9px',
color: 'var(--text-mid)',
letterSpacing: '0.02em',
}}
>
<span style={{ color: 'var(--accent)' }}>{formatTokens(tok.in)}</span>{' '}
{formatTokens(tok.out)}
</span>
) : null
})()}
</div>
<div
className="flex items-center gap-1.5 text-[10px] font-bold"
style={{
fontFamily: 'var(--font-mono)',
letterSpacing: '0.08em',
color: 'var(--buy)',
}}
>
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
<polyline points="2,5 4,7 8,3" stroke="var(--buy)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
DONE
</div>
</div>
{/* Report body */}
<div className="px-4 py-3">
<p
className="text-sm leading-relaxed line-clamp-5"
style={{ color: 'var(--text-mid)', lineHeight: '1.75', fontFamily: 'var(--font-manrope)' }}
>
{report}
</p>
</div>
</div>
))}
{/* Running state */}
{isRunning && (
<div
className="mb-2.5 overflow-hidden"
style={{
background: 'var(--bg-elevated)',
border: '1px solid rgba(255,180,0,0.20)',
borderRadius: '12px',
}}
>
<div
className="px-4 py-2.5 flex items-center justify-between"
style={{
background: 'rgba(255,180,0,0.06)',
borderBottom: '1px solid rgba(255,180,0,0.12)',
}}
>
<div className="flex items-center gap-2.5">
<div
className="w-2 h-2 rounded-full shrink-0"
style={{ background: accent, animation: 'shimmer 0.8s infinite' }}
/>
<span
className="text-[13px] font-semibold"
style={{ color: 'var(--text-high)', fontFamily: 'var(--font-manrope)' }}
>
{AGENT_STEP_LABELS[step]}
</span>
{isMulti && turns.length > 0 && (
<span
className="px-2 py-0.5 rounded text-[9px] font-bold"
style={{
background: `${accent}18`,
color: accent,
fontFamily: 'var(--font-mono)',
letterSpacing: '0.06em',
}}
>
T{turns.length + 1}
</span>
)}
</div>
<div
className="flex items-center gap-1.5 text-[10px] font-bold"
style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.08em', color: 'var(--status-running)' }}
>
<div
className="w-1.5 h-1.5 rounded-full"
style={{ background: 'var(--status-running)', animation: 'shimmer 0.7s infinite' }}
/>
ANALYZING
</div>
</div>
<div className="px-4 py-3 space-y-2">
{[85, 62, 44, 30].map((w, i) => (
<div
key={i}
className="h-2 rounded animate-shimmer"
style={{
background: 'var(--border-raised)',
width: `${w}%`,
animationDelay: `${i * 0.18}s`,
}}
/>
))}
</div>
</div>
)}
{/* Pending state */}
{turns.length === 0 && !isRunning && (
<div
className="mb-2.5 overflow-hidden"
style={{
background: 'var(--bg-elevated)',
border: '1px solid var(--border)',
borderRadius: '12px',
opacity: 0.45,
}}
>
<div className="px-4 py-2.5 flex items-center justify-between" style={{ borderBottom: '1px solid var(--border)' }}>
<div className="flex items-center gap-2.5">
<div className="w-2 h-2 rounded-full shrink-0" style={{ background: 'var(--text-low)' }} />
<span className="text-[13px] font-medium" style={{ color: 'var(--text-mid)', fontFamily: 'var(--font-manrope)' }}>
{AGENT_STEP_LABELS[step]}
</span>
</div>
<span
className="text-[9px] font-bold"
style={{ fontFamily: 'var(--font-mono)', letterSpacing: '0.1em', color: 'var(--text-low)' }}
>
QUEUED
</span>
</div>
<div className="px-4 py-3">
<div className="h-2 rounded" style={{ background: 'var(--border)', width: '45%' }} />
</div>
</div>
)}
</div>
)
})}
</div>
)
}