File size: 5,729 Bytes
c492c3f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | 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 (
<span className="tabular-nums" style={{ color: isPos ? '#4ade80' : '#f87171' }}>
{isPos ? '+' : ''}{value.toFixed(2)}
</span>
)
}
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 (
<div className="space-y-2">
{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 (
<div
key={`${animKey}-${i}`}
className="rounded overflow-hidden"
style={{
border: `1px solid ${meta.border}`,
background: meta.bg,
animation: 'stepReveal 0.32s ease forwards',
}}
>
<div className="flex items-start gap-3 px-3 py-2.5">
{/* Step index + type tag */}
<div className="flex-shrink-0 flex items-center gap-2 pt-px">
<span className="text-[#3a3f5a] text-[10px] tabular-nums w-4">
{String(item.step).padStart(2, '0')}
</span>
<span
className="text-[9px] font-semibold px-1.5 py-px rounded-sm tracking-widest"
style={{
color: meta.color,
background: `${meta.color}18`,
border: `1px solid ${meta.border}`,
}}
>
{meta.tag}
</span>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-baseline justify-between gap-2">
<span
className="font-medium tracking-tight"
style={{ color: meta.color, fontSize: '12px' }}
>
{at}
</span>
<span className="text-xs flex-shrink-0">
<RewardSign value={item.reward} />
</span>
</div>
{/* Human-readable label */}
<div className="mt-0.5 text-[10px] text-[#5a5f7a]">
{INV_LABELS[at] || at}
</div>
{/* Parameters for mark_violation_type */}
{at === 'mark_violation_type' && params?.violation_type && (
<div className="mt-1 text-[10px] text-[#3a3f5a]">
violation_type = <span style={{ color: '#fbbf24' }}>"{params.violation_type}"</span>
</div>
)}
{/* Reward reason (dimmed, compact) */}
<div className="mt-0.5 text-[10px] text-[#2a2a45] truncate">
{item.reward_reason}
</div>
</div>
</div>
{/* Terminal bottom accent */}
{isTerminal && (
<div
className="h-px w-full"
style={{ background: `linear-gradient(90deg, ${meta.color}70, transparent)` }}
/>
)}
</div>
)
})}
{/* Pending steps placeholder — shows how many are still to animate */}
{visibleCount < trajectory.length && (
<div className="flex items-center gap-2 px-3 py-1.5">
<span className="animate-scanPulse text-[#3a3f5a] text-xs">◎</span>
<span className="text-[10px] text-[#3a3f5a]">
{trajectory.length - visibleCount} more step{trajectory.length - visibleCount !== 1 ? 's' : ''}...
</span>
</div>
)}
</div>
)
}
|