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
|
import type { DirectiveStep, StepStatus } from "../../lib/api";
const STATUS_COLORS: Record<StepStatus, { bg: string; border: string; text: string }> = {
pending: { bg: "bg-[#1a2540]", border: "border-[#2a3a5a]", text: "text-[#7788aa]" },
ready: { bg: "bg-[#2a2a10]", border: "border-[#4a4a20]", text: "text-yellow-400" },
running: { bg: "bg-[#0a2a1a]", border: "border-[#1a5a3a]", text: "text-green-400" },
completed: { bg: "bg-[#0a2a2a]", border: "border-[#1a5a5a]", text: "text-emerald-400" },
failed: { bg: "bg-[#2a1a1a]", border: "border-[#5a2a2a]", text: "text-red-400" },
skipped: { bg: "bg-[#1a1a2a]", border: "border-[#2a2a4a]", text: "text-[#7788aa]" },
};
const STATUS_LABELS: Record<StepStatus, string> = {
pending: "PENDING",
ready: "READY",
running: "RUNNING",
completed: "DONE",
failed: "FAILED",
skipped: "SKIP",
};
interface StepNodeProps {
step: DirectiveStep;
onComplete?: () => void;
onFail?: () => void;
onSkip?: () => void;
onViewTask?: (taskId: string) => void;
}
export function StepNode({ step, onComplete, onFail, onSkip, onViewTask }: StepNodeProps) {
const colors = STATUS_COLORS[step.status] || STATUS_COLORS.pending;
const label = STATUS_LABELS[step.status] || step.status.toUpperCase();
const isContractBacked = !!step.contractType;
return (
<div
className={`${colors.bg} ${isContractBacked ? "border-2 border-dashed" : "border"} ${colors.border} rounded px-3 py-2 min-w-[160px] max-w-[220px]`}
>
<div className="flex items-center justify-between gap-2 mb-1">
<span className="text-[11px] font-mono text-white truncate font-medium">
{step.name}
</span>
<span className={`text-[9px] font-mono ${colors.text} uppercase shrink-0`}>
{label}
</span>
</div>
{isContractBacked && (
<div className="flex items-center gap-1 mb-1">
<span className="text-[8px] font-mono text-violet-400 bg-violet-400/10 border border-violet-400/20 rounded px-1 py-0.5 uppercase tracking-wide">
CONTRACT
</span>
<span className="text-[8px] font-mono text-violet-300/60">
{step.contractType}
</span>
</div>
)}
{step.description && (
<p className="text-[10px] text-[#7788aa] font-mono truncate mb-1">
{step.description}
</p>
)}
{step.contractId && (
<a
href={`/contracts/${step.contractId}`}
className="text-[9px] font-mono text-violet-400/60 hover:text-violet-300 underline block mb-1"
>
{step.status === "running" ? "Contract running..." : "View contract"}
</a>
)}
{step.taskId && !step.contractId && (
<button
type="button"
onClick={() => onViewTask?.(step.taskId!)}
className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline block mb-1 bg-transparent border-none p-0 cursor-pointer text-left"
>
{step.status === "running" ? "Auto-executing..." : "View task"}
</button>
)}
{(step.status === "running" || step.status === "ready") && (
<div className="flex gap-1 mt-1">
{onComplete && (
<button
type="button"
onClick={onComplete}
className="text-[9px] font-mono text-emerald-400 hover:text-emerald-300 bg-transparent border border-emerald-800 rounded px-1.5 py-0.5"
>
Done
</button>
)}
{onFail && (
<button
type="button"
onClick={onFail}
className="text-[9px] font-mono text-red-400 hover:text-red-300 bg-transparent border border-red-800 rounded px-1.5 py-0.5"
>
Fail
</button>
)}
{onSkip && (
<button
type="button"
onClick={onSkip}
className="text-[9px] font-mono text-[#7788aa] hover:text-white bg-transparent border border-[#2a3a5a] rounded px-1.5 py-0.5"
>
Skip
</button>
)}
</div>
)}
</div>
);
}
|