@nordsym/apiclaw 1.3.13 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRD-API-CHAINING.md +483 -0
- package/PRD-HARDEN-SHELL.md +18 -12
- package/convex/_generated/api.d.ts +2 -0
- package/convex/chains.ts +1095 -0
- package/convex/schema.ts +101 -0
- package/dist/chain-types.d.ts +187 -0
- package/dist/chain-types.d.ts.map +1 -0
- package/dist/chain-types.js +33 -0
- package/dist/chain-types.js.map +1 -0
- package/dist/chainExecutor.d.ts +122 -0
- package/dist/chainExecutor.d.ts.map +1 -0
- package/dist/chainExecutor.js +454 -0
- package/dist/chainExecutor.js.map +1 -0
- package/dist/chainResolver.d.ts +100 -0
- package/dist/chainResolver.d.ts.map +1 -0
- package/dist/chainResolver.js +519 -0
- package/dist/chainResolver.js.map +1 -0
- package/dist/chainResolver.test.d.ts +5 -0
- package/dist/chainResolver.test.d.ts.map +1 -0
- package/dist/chainResolver.test.js +201 -0
- package/dist/chainResolver.test.js.map +1 -0
- package/dist/execute.d.ts +4 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +3 -0
- package/dist/execute.js.map +1 -1
- package/dist/index.js +382 -2
- package/dist/index.js.map +1 -1
- package/landing/public/logos/chattgpt.svg +1 -0
- package/landing/public/logos/claude.svg +1 -0
- package/landing/public/logos/gemini.svg +1 -0
- package/landing/public/logos/grok.svg +1 -0
- package/landing/src/app/page.tsx +12 -21
- package/landing/src/app/workspace/chains/page.tsx +520 -0
- package/landing/src/components/AITestimonials.tsx +15 -9
- package/landing/src/components/ChainStepDetail.tsx +310 -0
- package/landing/src/components/ChainTrace.tsx +261 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +1 -1
- package/src/chain-types.ts +270 -0
- package/src/chainExecutor.ts +730 -0
- package/src/chainResolver.test.ts +246 -0
- package/src/chainResolver.ts +658 -0
- package/src/execute.ts +23 -0
- package/src/index.ts +423 -2
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ChevronDown,
|
|
6
|
+
ChevronRight,
|
|
7
|
+
CheckCircle2,
|
|
8
|
+
XCircle,
|
|
9
|
+
Loader2,
|
|
10
|
+
PauseCircle,
|
|
11
|
+
SkipForward,
|
|
12
|
+
Clock,
|
|
13
|
+
RotateCcw,
|
|
14
|
+
Zap,
|
|
15
|
+
ArrowRight,
|
|
16
|
+
AlertTriangle,
|
|
17
|
+
} from "lucide-react";
|
|
18
|
+
|
|
19
|
+
interface StepExecution {
|
|
20
|
+
_id: string;
|
|
21
|
+
stepId: string;
|
|
22
|
+
stepIndex: number;
|
|
23
|
+
status: "pending" | "running" | "completed" | "failed" | "skipped";
|
|
24
|
+
input?: any;
|
|
25
|
+
output?: any;
|
|
26
|
+
latencyMs?: number;
|
|
27
|
+
costCents?: number;
|
|
28
|
+
error?: {
|
|
29
|
+
code: string;
|
|
30
|
+
message: string;
|
|
31
|
+
retryCount?: number;
|
|
32
|
+
};
|
|
33
|
+
parallelGroup?: string;
|
|
34
|
+
createdAt: number;
|
|
35
|
+
startedAt?: number;
|
|
36
|
+
completedAt?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ChainStepDetailProps {
|
|
40
|
+
step: StepExecution;
|
|
41
|
+
stepDef?: {
|
|
42
|
+
id: string;
|
|
43
|
+
provider: string;
|
|
44
|
+
action?: string;
|
|
45
|
+
params?: Record<string, any>;
|
|
46
|
+
};
|
|
47
|
+
isExpanded: boolean;
|
|
48
|
+
onToggle: () => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function ChainStepDetail({ step, stepDef, isExpanded, onToggle }: ChainStepDetailProps) {
|
|
52
|
+
const [activeTab, setActiveTab] = useState<"input" | "output" | "error">("input");
|
|
53
|
+
|
|
54
|
+
const getStatusIcon = (status: string) => {
|
|
55
|
+
switch (status) {
|
|
56
|
+
case "completed":
|
|
57
|
+
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
|
58
|
+
case "running":
|
|
59
|
+
return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
|
|
60
|
+
case "failed":
|
|
61
|
+
return <XCircle className="w-4 h-4 text-red-500" />;
|
|
62
|
+
case "skipped":
|
|
63
|
+
return <SkipForward className="w-4 h-4 text-gray-500" />;
|
|
64
|
+
case "paused":
|
|
65
|
+
return <PauseCircle className="w-4 h-4 text-yellow-500" />;
|
|
66
|
+
default:
|
|
67
|
+
return <Clock className="w-4 h-4 text-gray-500" />;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const getStatusBg = (status: string) => {
|
|
72
|
+
switch (status) {
|
|
73
|
+
case "completed":
|
|
74
|
+
return "bg-green-500/10 border-green-500/20";
|
|
75
|
+
case "running":
|
|
76
|
+
return "bg-blue-500/10 border-blue-500/20";
|
|
77
|
+
case "failed":
|
|
78
|
+
return "bg-red-500/10 border-red-500/20";
|
|
79
|
+
case "skipped":
|
|
80
|
+
return "bg-gray-500/10 border-gray-500/20";
|
|
81
|
+
case "paused":
|
|
82
|
+
return "bg-yellow-500/10 border-yellow-500/20";
|
|
83
|
+
default:
|
|
84
|
+
return "bg-white/5 border-white/10";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const formatDuration = (ms: number) => {
|
|
89
|
+
if (ms < 1000) return `${ms}ms`;
|
|
90
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const formatCost = (cents: number) => {
|
|
94
|
+
if (cents === 0) return "$0.00";
|
|
95
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Highlight references in input (e.g., $generate.url)
|
|
99
|
+
const highlightReferences = (obj: any): any => {
|
|
100
|
+
if (typeof obj === "string") {
|
|
101
|
+
// Check if string contains references
|
|
102
|
+
if (obj.includes("$")) {
|
|
103
|
+
return obj;
|
|
104
|
+
}
|
|
105
|
+
return obj;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(obj)) {
|
|
108
|
+
return obj.map(highlightReferences);
|
|
109
|
+
}
|
|
110
|
+
if (typeof obj === "object" && obj !== null) {
|
|
111
|
+
return Object.fromEntries(
|
|
112
|
+
Object.entries(obj).map(([k, v]) => [k, highlightReferences(v)])
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return obj;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Render JSON with syntax highlighting
|
|
119
|
+
const renderJson = (data: any, highlightRefs = false) => {
|
|
120
|
+
if (!data) return <span className="text-white/40">null</span>;
|
|
121
|
+
|
|
122
|
+
const jsonString = JSON.stringify(data, null, 2);
|
|
123
|
+
|
|
124
|
+
if (highlightRefs) {
|
|
125
|
+
// Highlight $references
|
|
126
|
+
const parts = jsonString.split(/(\$[a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z0-9_\[\]]+)*)/g);
|
|
127
|
+
return (
|
|
128
|
+
<pre className="text-xs font-mono text-white/80 whitespace-pre-wrap break-all">
|
|
129
|
+
{parts.map((part, i) =>
|
|
130
|
+
part.startsWith("$") ? (
|
|
131
|
+
<span key={i} className="bg-[#ef4444]/20 text-[#ef4444] px-1 rounded">
|
|
132
|
+
{part}
|
|
133
|
+
</span>
|
|
134
|
+
) : (
|
|
135
|
+
<span key={i}>{part}</span>
|
|
136
|
+
)
|
|
137
|
+
)}
|
|
138
|
+
</pre>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<pre className="text-xs font-mono text-white/80 whitespace-pre-wrap break-all">
|
|
144
|
+
{jsonString}
|
|
145
|
+
</pre>
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className={`rounded-lg border transition-all ${getStatusBg(step.status)}`}>
|
|
151
|
+
{/* Header */}
|
|
152
|
+
<button
|
|
153
|
+
onClick={onToggle}
|
|
154
|
+
className="w-full px-4 py-3 flex items-center justify-between text-left"
|
|
155
|
+
>
|
|
156
|
+
<div className="flex items-center gap-3">
|
|
157
|
+
{isExpanded ? (
|
|
158
|
+
<ChevronDown className="w-4 h-4 text-white/40" />
|
|
159
|
+
) : (
|
|
160
|
+
<ChevronRight className="w-4 h-4 text-white/40" />
|
|
161
|
+
)}
|
|
162
|
+
{getStatusIcon(step.status)}
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<span className="font-medium font-mono">{step.stepId}</span>
|
|
165
|
+
{stepDef && (
|
|
166
|
+
<span className="text-white/40 flex items-center gap-1 text-sm">
|
|
167
|
+
<ArrowRight className="w-3 h-3" />
|
|
168
|
+
<span className="text-white/60">{stepDef.provider}</span>
|
|
169
|
+
{stepDef.action && (
|
|
170
|
+
<>
|
|
171
|
+
<span className="text-white/20">/</span>
|
|
172
|
+
<span>{stepDef.action}</span>
|
|
173
|
+
</>
|
|
174
|
+
)}
|
|
175
|
+
</span>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div className="flex items-center gap-4 text-sm text-white/60">
|
|
180
|
+
{step.error?.retryCount && step.error.retryCount > 0 && (
|
|
181
|
+
<span className="flex items-center gap-1 text-yellow-500">
|
|
182
|
+
<RotateCcw className="w-3.5 h-3.5" />
|
|
183
|
+
{step.error.retryCount} retries
|
|
184
|
+
</span>
|
|
185
|
+
)}
|
|
186
|
+
<span className="flex items-center gap-1">
|
|
187
|
+
<Clock className="w-3.5 h-3.5" />
|
|
188
|
+
{formatDuration(step.latencyMs || 0)}
|
|
189
|
+
</span>
|
|
190
|
+
<span className="flex items-center gap-1">
|
|
191
|
+
<Zap className="w-3.5 h-3.5" />
|
|
192
|
+
{formatCost(step.costCents || 0)}
|
|
193
|
+
</span>
|
|
194
|
+
</div>
|
|
195
|
+
</button>
|
|
196
|
+
|
|
197
|
+
{/* Expanded Content */}
|
|
198
|
+
{isExpanded && (
|
|
199
|
+
<div className="px-4 pb-4 pt-0">
|
|
200
|
+
{/* Tabs */}
|
|
201
|
+
<div className="flex items-center gap-1 mb-3 border-b border-white/10 pb-2">
|
|
202
|
+
<button
|
|
203
|
+
onClick={() => setActiveTab("input")}
|
|
204
|
+
className={`px-3 py-1.5 rounded-t text-sm transition-colors ${
|
|
205
|
+
activeTab === "input"
|
|
206
|
+
? "bg-white/10 text-white"
|
|
207
|
+
: "text-white/40 hover:text-white/60"
|
|
208
|
+
}`}
|
|
209
|
+
>
|
|
210
|
+
Input
|
|
211
|
+
</button>
|
|
212
|
+
<button
|
|
213
|
+
onClick={() => setActiveTab("output")}
|
|
214
|
+
className={`px-3 py-1.5 rounded-t text-sm transition-colors ${
|
|
215
|
+
activeTab === "output"
|
|
216
|
+
? "bg-white/10 text-white"
|
|
217
|
+
: "text-white/40 hover:text-white/60"
|
|
218
|
+
}`}
|
|
219
|
+
>
|
|
220
|
+
Output
|
|
221
|
+
</button>
|
|
222
|
+
{step.error && (
|
|
223
|
+
<button
|
|
224
|
+
onClick={() => setActiveTab("error")}
|
|
225
|
+
className={`px-3 py-1.5 rounded-t text-sm transition-colors flex items-center gap-1 ${
|
|
226
|
+
activeTab === "error"
|
|
227
|
+
? "bg-red-500/20 text-red-500"
|
|
228
|
+
: "text-red-400 hover:text-red-300"
|
|
229
|
+
}`}
|
|
230
|
+
>
|
|
231
|
+
<AlertTriangle className="w-3.5 h-3.5" />
|
|
232
|
+
Error
|
|
233
|
+
</button>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Tab Content */}
|
|
238
|
+
<div className="bg-black/30 rounded-lg p-3 max-h-64 overflow-auto">
|
|
239
|
+
{activeTab === "input" && (
|
|
240
|
+
<div>
|
|
241
|
+
{step.input ? (
|
|
242
|
+
renderJson(step.input, true)
|
|
243
|
+
) : stepDef?.params ? (
|
|
244
|
+
renderJson(stepDef.params, true)
|
|
245
|
+
) : (
|
|
246
|
+
<span className="text-white/40 text-sm">No input data</span>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
)}
|
|
250
|
+
|
|
251
|
+
{activeTab === "output" && (
|
|
252
|
+
<div>
|
|
253
|
+
{step.output ? (
|
|
254
|
+
renderJson(step.output)
|
|
255
|
+
) : (
|
|
256
|
+
<span className="text-white/40 text-sm">
|
|
257
|
+
{step.status === "running"
|
|
258
|
+
? "Waiting for output..."
|
|
259
|
+
: step.status === "pending"
|
|
260
|
+
? "Step not yet executed"
|
|
261
|
+
: "No output data"}
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{activeTab === "error" && step.error && (
|
|
268
|
+
<div className="space-y-2">
|
|
269
|
+
<div className="flex items-center gap-2">
|
|
270
|
+
<span className="text-xs text-white/40">Code:</span>
|
|
271
|
+
<code className="text-xs bg-red-500/20 text-red-400 px-2 py-0.5 rounded">
|
|
272
|
+
{step.error.code}
|
|
273
|
+
</code>
|
|
274
|
+
</div>
|
|
275
|
+
<div>
|
|
276
|
+
<span className="text-xs text-white/40">Message:</span>
|
|
277
|
+
<p className="text-sm text-red-400 mt-1">{step.error.message}</p>
|
|
278
|
+
</div>
|
|
279
|
+
{step.error.retryCount && step.error.retryCount > 0 && (
|
|
280
|
+
<div className="flex items-center gap-2 pt-2 border-t border-white/10">
|
|
281
|
+
<RotateCcw className="w-3.5 h-3.5 text-yellow-500" />
|
|
282
|
+
<span className="text-xs text-white/60">
|
|
283
|
+
Retried {step.error.retryCount} time(s) before failing
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
289
|
+
</div>
|
|
290
|
+
|
|
291
|
+
{/* Step Metadata */}
|
|
292
|
+
<div className="flex items-center gap-4 mt-3 pt-3 border-t border-white/10 text-xs text-white/40">
|
|
293
|
+
<span>Index: {step.stepIndex}</span>
|
|
294
|
+
{step.parallelGroup && (
|
|
295
|
+
<span className="px-2 py-0.5 bg-white/10 rounded">
|
|
296
|
+
Parallel: {step.parallelGroup}
|
|
297
|
+
</span>
|
|
298
|
+
)}
|
|
299
|
+
{step.startedAt && (
|
|
300
|
+
<span>Started: {new Date(step.startedAt).toLocaleTimeString()}</span>
|
|
301
|
+
)}
|
|
302
|
+
{step.completedAt && (
|
|
303
|
+
<span>Completed: {new Date(step.completedAt).toLocaleTimeString()}</span>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { CheckCircle2, XCircle, Loader2, PauseCircle, SkipForward, Clock, DollarSign, Sparkles } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface StepExecution {
|
|
7
|
+
_id: string;
|
|
8
|
+
stepId: string;
|
|
9
|
+
stepIndex: number;
|
|
10
|
+
status: "pending" | "running" | "completed" | "failed" | "skipped";
|
|
11
|
+
input?: any;
|
|
12
|
+
output?: any;
|
|
13
|
+
latencyMs?: number;
|
|
14
|
+
costCents?: number;
|
|
15
|
+
error?: {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
retryCount?: number;
|
|
19
|
+
};
|
|
20
|
+
parallelGroup?: string;
|
|
21
|
+
createdAt: number;
|
|
22
|
+
startedAt?: number;
|
|
23
|
+
completedAt?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ChainTraceProps {
|
|
27
|
+
chain: {
|
|
28
|
+
_id: string;
|
|
29
|
+
status: string;
|
|
30
|
+
totalCostCents: number;
|
|
31
|
+
totalLatencyMs: number;
|
|
32
|
+
startedAt?: number;
|
|
33
|
+
completedAt?: number;
|
|
34
|
+
steps?: any[];
|
|
35
|
+
};
|
|
36
|
+
executions: StepExecution[];
|
|
37
|
+
tokensSaved: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function ChainTrace({ chain, executions, tokensSaved }: ChainTraceProps) {
|
|
41
|
+
// Calculate timeline boundaries
|
|
42
|
+
const timelineBounds = useMemo(() => {
|
|
43
|
+
if (executions.length === 0) return { start: 0, end: 1000, duration: 1000 };
|
|
44
|
+
|
|
45
|
+
const startTimes = executions
|
|
46
|
+
.filter((e) => e.startedAt)
|
|
47
|
+
.map((e) => e.startedAt!);
|
|
48
|
+
const endTimes = executions
|
|
49
|
+
.filter((e) => e.completedAt)
|
|
50
|
+
.map((e) => e.completedAt!);
|
|
51
|
+
|
|
52
|
+
const start = startTimes.length > 0 ? Math.min(...startTimes) : chain.startedAt || Date.now();
|
|
53
|
+
const end = endTimes.length > 0 ? Math.max(...endTimes) : chain.completedAt || Date.now();
|
|
54
|
+
const duration = Math.max(end - start, 1);
|
|
55
|
+
|
|
56
|
+
return { start, end, duration };
|
|
57
|
+
}, [executions, chain]);
|
|
58
|
+
|
|
59
|
+
// Group parallel executions
|
|
60
|
+
const groupedExecutions = useMemo(() => {
|
|
61
|
+
const groups: { group: string | null; steps: StepExecution[] }[] = [];
|
|
62
|
+
let currentGroup: string | null = null;
|
|
63
|
+
let currentSteps: StepExecution[] = [];
|
|
64
|
+
|
|
65
|
+
executions.forEach((exec) => {
|
|
66
|
+
if (exec.parallelGroup !== currentGroup) {
|
|
67
|
+
if (currentSteps.length > 0) {
|
|
68
|
+
groups.push({ group: currentGroup, steps: currentSteps });
|
|
69
|
+
}
|
|
70
|
+
currentGroup = exec.parallelGroup || null;
|
|
71
|
+
currentSteps = [exec];
|
|
72
|
+
} else {
|
|
73
|
+
currentSteps.push(exec);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (currentSteps.length > 0) {
|
|
78
|
+
groups.push({ group: currentGroup, steps: currentSteps });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return groups;
|
|
82
|
+
}, [executions]);
|
|
83
|
+
|
|
84
|
+
const getStatusIcon = (status: string) => {
|
|
85
|
+
switch (status) {
|
|
86
|
+
case "completed":
|
|
87
|
+
return <CheckCircle2 className="w-3.5 h-3.5 text-green-500" />;
|
|
88
|
+
case "running":
|
|
89
|
+
return <Loader2 className="w-3.5 h-3.5 text-blue-500 animate-spin" />;
|
|
90
|
+
case "failed":
|
|
91
|
+
return <XCircle className="w-3.5 h-3.5 text-red-500" />;
|
|
92
|
+
case "skipped":
|
|
93
|
+
return <SkipForward className="w-3.5 h-3.5 text-gray-500" />;
|
|
94
|
+
case "paused":
|
|
95
|
+
return <PauseCircle className="w-3.5 h-3.5 text-yellow-500" />;
|
|
96
|
+
default:
|
|
97
|
+
return <Clock className="w-3.5 h-3.5 text-gray-500" />;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const getBarColor = (status: string) => {
|
|
102
|
+
switch (status) {
|
|
103
|
+
case "completed":
|
|
104
|
+
return "bg-green-500";
|
|
105
|
+
case "running":
|
|
106
|
+
return "bg-blue-500 animate-pulse";
|
|
107
|
+
case "failed":
|
|
108
|
+
return "bg-red-500";
|
|
109
|
+
case "skipped":
|
|
110
|
+
return "bg-gray-500";
|
|
111
|
+
case "paused":
|
|
112
|
+
return "bg-yellow-500";
|
|
113
|
+
default:
|
|
114
|
+
return "bg-gray-600";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const formatDuration = (ms: number) => {
|
|
119
|
+
if (ms < 1000) return `${ms}ms`;
|
|
120
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const formatCost = (cents: number) => {
|
|
124
|
+
if (cents === 0) return "$0.00";
|
|
125
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const calculateBarPosition = (step: StepExecution) => {
|
|
129
|
+
const start = step.startedAt || timelineBounds.start;
|
|
130
|
+
const end = step.completedAt || (step.startedAt ? step.startedAt + (step.latencyMs || 0) : timelineBounds.end);
|
|
131
|
+
|
|
132
|
+
const left = ((start - timelineBounds.start) / timelineBounds.duration) * 100;
|
|
133
|
+
const width = ((end - start) / timelineBounds.duration) * 100;
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
left: `${Math.max(0, left)}%`,
|
|
137
|
+
width: `${Math.max(2, Math.min(100 - left, width))}%`,
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Get provider info from step definition
|
|
142
|
+
const getStepProvider = (stepId: string) => {
|
|
143
|
+
const stepDef = chain.steps?.find((s: any) => s.id === stepId);
|
|
144
|
+
return stepDef?.provider || "unknown";
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="bg-black/30 rounded-xl border border-white/10 overflow-hidden">
|
|
149
|
+
{/* Header */}
|
|
150
|
+
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
|
|
151
|
+
<div className="flex items-center gap-2">
|
|
152
|
+
<span className="text-sm font-medium">Chain:</span>
|
|
153
|
+
<code className="text-xs text-white/60 font-mono">{chain._id.slice(0, 16)}...</code>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="flex items-center gap-4 text-sm text-white/60">
|
|
156
|
+
<span className="flex items-center gap-1">
|
|
157
|
+
<Clock className="w-3.5 h-3.5" />
|
|
158
|
+
Total: {formatDuration(chain.totalLatencyMs)}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Timeline */}
|
|
164
|
+
<div className="p-4 space-y-2">
|
|
165
|
+
{groupedExecutions.map(({ group, steps }, groupIndex) => (
|
|
166
|
+
<div key={group || groupIndex}>
|
|
167
|
+
{/* Parallel group indicator */}
|
|
168
|
+
{group && steps.length > 1 && (
|
|
169
|
+
<div className="text-xs text-white/40 mb-1 flex items-center gap-1">
|
|
170
|
+
<span className="px-1.5 py-0.5 bg-white/10 rounded">Parallel</span>
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Steps in group */}
|
|
175
|
+
<div className={group && steps.length > 1 ? "pl-4 border-l-2 border-white/10 space-y-2" : "space-y-2"}>
|
|
176
|
+
{steps.map((step) => {
|
|
177
|
+
const position = calculateBarPosition(step);
|
|
178
|
+
return (
|
|
179
|
+
<div key={step._id} className="flex items-center gap-3">
|
|
180
|
+
{/* Step name */}
|
|
181
|
+
<div className="w-24 flex-shrink-0 flex items-center gap-2">
|
|
182
|
+
{getStatusIcon(step.status)}
|
|
183
|
+
<span className="text-sm font-mono truncate" title={step.stepId}>
|
|
184
|
+
{step.stepId}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Gantt bar */}
|
|
189
|
+
<div className="flex-1 h-6 bg-white/5 rounded relative overflow-hidden">
|
|
190
|
+
<div
|
|
191
|
+
className={`absolute top-0 h-full rounded ${getBarColor(step.status)}`}
|
|
192
|
+
style={{
|
|
193
|
+
left: position.left,
|
|
194
|
+
width: position.width,
|
|
195
|
+
}}
|
|
196
|
+
/>
|
|
197
|
+
{/* Time markers - simplified */}
|
|
198
|
+
<div className="absolute inset-0 flex items-center px-2">
|
|
199
|
+
<span
|
|
200
|
+
className="text-xs font-mono text-white/80 drop-shadow-sm"
|
|
201
|
+
style={{
|
|
202
|
+
marginLeft: position.left,
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
{formatDuration(step.latencyMs || 0)}
|
|
206
|
+
</span>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Cost */}
|
|
211
|
+
<div className="w-16 text-right text-xs text-white/60">
|
|
212
|
+
{formatCost(step.costCents || 0)}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
{/* Footer */}
|
|
223
|
+
<div className="px-4 py-3 border-t border-white/10 flex items-center justify-between text-sm">
|
|
224
|
+
<div className="flex items-center gap-4">
|
|
225
|
+
<span className="flex items-center gap-1 text-white/60">
|
|
226
|
+
<DollarSign className="w-3.5 h-3.5" />
|
|
227
|
+
Total Cost: <span className="text-white font-medium">{formatCost(chain.totalCostCents)}</span>
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="flex items-center gap-1 text-white/60">
|
|
231
|
+
<Sparkles className="w-3.5 h-3.5 text-yellow-500" />
|
|
232
|
+
<span>Tokens Saved: <span className="text-yellow-500 font-medium">~{tokensSaved.toLocaleString()}</span></span>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
{/* Legend */}
|
|
237
|
+
<div className="px-4 py-2 border-t border-white/5 flex items-center gap-4 text-xs text-white/40">
|
|
238
|
+
<div className="flex items-center gap-1">
|
|
239
|
+
<div className="w-3 h-3 rounded bg-green-500" />
|
|
240
|
+
<span>Completed</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div className="flex items-center gap-1">
|
|
243
|
+
<div className="w-3 h-3 rounded bg-blue-500" />
|
|
244
|
+
<span>Running</span>
|
|
245
|
+
</div>
|
|
246
|
+
<div className="flex items-center gap-1">
|
|
247
|
+
<div className="w-3 h-3 rounded bg-red-500" />
|
|
248
|
+
<span>Failed</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex items-center gap-1">
|
|
251
|
+
<div className="w-3 h-3 rounded bg-yellow-500" />
|
|
252
|
+
<span>Paused</span>
|
|
253
|
+
</div>
|
|
254
|
+
<div className="flex items-center gap-1">
|
|
255
|
+
<div className="w-3 h-3 rounded bg-gray-500" />
|
|
256
|
+
<span>Skipped</span>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
);
|
|
261
|
+
}
|