import { useState, useEffect } from 'react' const ACTION_META = { fetch_user_history: { tag: 'INV', color: '#60a5fa', border: 'rgba(96,165,250,0.3)', bg: 'rgba(59,130,246,0.06)' }, fetch_thread_context: { tag: 'INV', color: '#60a5fa', border: 'rgba(96,165,250,0.3)', bg: 'rgba(59,130,246,0.06)' }, check_policy_clause: { tag: 'INV', color: '#60a5fa', border: 'rgba(96,165,250,0.3)', bg: 'rgba(59,130,246,0.06)' }, mark_violation_type: { tag: 'CLS', color: '#fbbf24', border: 'rgba(251,191,36,0.3)', bg: 'rgba(251,191,36,0.06)' }, allow: { tag: 'END', color: '#4ade80', border: 'rgba(74,222,128,0.4)', bg: 'rgba(74,222,128,0.07)' }, flag: { tag: 'END', color: '#fb923c', border: 'rgba(251,146,60,0.4)', bg: 'rgba(251,146,60,0.07)' }, remove: { tag: 'END', color: '#f87171', border: 'rgba(248,113,113,0.4)', bg: 'rgba(248,113,113,0.07)' }, escalate: { tag: 'END', color: '#c084fc', border: 'rgba(192,132,252,0.4)', bg: 'rgba(192,132,252,0.07)' }, } const INV_LABELS = { fetch_user_history: 'Fetched user violation history', fetch_thread_context: 'Retrieved conversation thread context', check_policy_clause: 'Loaded applicable policy clause', mark_violation_type: 'Classified violation type', allow: 'Decision: Allow content', flag: 'Decision: Flag for review', remove: 'Decision: Remove content', escalate: 'Decision: Escalate to senior moderator', } function RewardSign({ value }) { const isPos = value >= 0 return ( {isPos ? '+' : ''}{value.toFixed(2)} ) } export default function StepTimeline({ trajectory, animKey }) { const [visibleCount, setVisibleCount] = useState(0) // Reveal steps one by one whenever trajectory/animKey changes useEffect(() => { setVisibleCount(0) if (!trajectory?.length) return let count = 0 const id = setInterval(() => { count += 1 setVisibleCount(count) if (count >= trajectory.length) clearInterval(id) }, 380) return () => clearInterval(id) }, [animKey, trajectory]) if (!trajectory?.length) return null const visible = trajectory.slice(0, visibleCount) return (
{visible.map((item, i) => { const at = item.action.action_type const meta = ACTION_META[at] || { tag: '?', color: '#5a5f7a', border: 'rgba(90,95,122,0.3)', bg: 'transparent' } const isTerminal = meta.tag === 'END' const params = item.action.parameters return (
{/* Step index + type tag */}
{String(item.step).padStart(2, '0')} {meta.tag}
{/* Content */}
{at}
{/* Human-readable label */}
{INV_LABELS[at] || at}
{/* Parameters for mark_violation_type */} {at === 'mark_violation_type' && params?.violation_type && (
violation_type = "{params.violation_type}"
)} {/* Reward reason (dimmed, compact) */}
{item.reward_reason}
{/* Terminal bottom accent */} {isTerminal && (
)}
) })} {/* Pending steps placeholder — shows how many are still to animate */} {visibleCount < trajectory.length && (
{trajectory.length - visibleCount} more step{trajectory.length - visibleCount !== 1 ? 's' : ''}...
)}
) }