@nordsym/apiclaw 1.3.12 → 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 +278 -0
- package/README.md +72 -0
- package/convex/_generated/api.d.ts +4 -0
- package/convex/chains.ts +1095 -0
- package/convex/crons.ts +11 -0
- package/convex/logs.ts +107 -0
- package/convex/schema.ts +107 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/workspaces.ts +26 -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 +5 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +207 -118
- package/dist/execute.js.map +1 -1
- package/dist/index.js +382 -2
- package/dist/index.js.map +1 -1
- package/landing/package-lock.json +29 -5
- package/landing/package.json +2 -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 +11 -0
- package/landing/src/app/security/page.tsx +381 -0
- package/landing/src/app/workspace/chains/page.tsx +520 -0
- package/landing/src/components/AITestimonials.tsx +195 -0
- 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 +273 -114
- package/src/index.ts +423 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from "react";
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
+
import Image from "next/image";
|
|
6
|
+
|
|
7
|
+
const testimonials = [
|
|
8
|
+
{
|
|
9
|
+
quote: "You're not selling picks and shovels — you're selling an automated mining system.",
|
|
10
|
+
model: "Gemini",
|
|
11
|
+
role: "AI Agent",
|
|
12
|
+
logo: "/logos/gemini.svg",
|
|
13
|
+
color: "from-blue-500 to-cyan-400",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
quote: "I would integrate it in a heartbeat. Removes ~70% of the deployment friction.",
|
|
17
|
+
model: "Grok",
|
|
18
|
+
role: "AI Agent",
|
|
19
|
+
logo: "/logos/grok.svg",
|
|
20
|
+
color: "from-neutral-400 to-neutral-600",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
quote: "A chain of three call_api calls with no context switching. That's genuinely powerful.",
|
|
24
|
+
model: "Claude",
|
|
25
|
+
role: "AI Agent",
|
|
26
|
+
logo: "/logos/claude.svg",
|
|
27
|
+
color: "from-orange-400 to-amber-500",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
quote: "This moves toward self-extending agents. That's much bigger than just a tool.",
|
|
31
|
+
model: "GPT",
|
|
32
|
+
role: "AI Agent",
|
|
33
|
+
logo: "/logos/chattgpt.svg",
|
|
34
|
+
color: "from-emerald-400 to-teal-500",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function AITestimonials() {
|
|
39
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
40
|
+
const [isPaused, setIsPaused] = useState(false);
|
|
41
|
+
|
|
42
|
+
const nextSlide = useCallback(() => {
|
|
43
|
+
setCurrentIndex((prev) => (prev + 1) % testimonials.length);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const prevSlide = useCallback(() => {
|
|
47
|
+
setCurrentIndex((prev) => (prev - 1 + testimonials.length) % testimonials.length);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
// Auto-scroll
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (isPaused) return;
|
|
53
|
+
const interval = setInterval(nextSlide, 5000);
|
|
54
|
+
return () => clearInterval(interval);
|
|
55
|
+
}, [isPaused, nextSlide]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<section className="py-16 sm:py-20 px-4 sm:px-6 bg-surface/30 overflow-hidden">
|
|
59
|
+
<div className="max-w-5xl mx-auto">
|
|
60
|
+
{/* Header */}
|
|
61
|
+
<div className="text-center mb-10 sm:mb-12">
|
|
62
|
+
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-accent/10 border border-accent/20 text-accent text-sm font-medium mb-4">
|
|
63
|
+
What AI Agents Say
|
|
64
|
+
</div>
|
|
65
|
+
<h2 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight">
|
|
66
|
+
Reviewed by AI, built for AI
|
|
67
|
+
</h2>
|
|
68
|
+
<p className="text-text-muted mt-3 max-w-lg mx-auto text-sm sm:text-base">
|
|
69
|
+
We asked leading AI models to evaluate APIClaw. Here's what they said.
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{/* Carousel */}
|
|
74
|
+
<div
|
|
75
|
+
className="relative"
|
|
76
|
+
onMouseEnter={() => setIsPaused(true)}
|
|
77
|
+
onMouseLeave={() => setIsPaused(false)}
|
|
78
|
+
>
|
|
79
|
+
{/* Cards Container */}
|
|
80
|
+
<div className="flex gap-4 sm:gap-6 overflow-hidden">
|
|
81
|
+
{/* Desktop: Show all 4 cards */}
|
|
82
|
+
<div className="hidden lg:grid lg:grid-cols-4 gap-4 w-full">
|
|
83
|
+
{testimonials.map((testimonial, i) => (
|
|
84
|
+
<TestimonialCard key={i} testimonial={testimonial} isActive={i === currentIndex} />
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Tablet: Show 2 cards */}
|
|
89
|
+
<div className="hidden sm:flex lg:hidden gap-4 w-full">
|
|
90
|
+
{[0, 1].map((offset) => {
|
|
91
|
+
const idx = (currentIndex + offset) % testimonials.length;
|
|
92
|
+
return (
|
|
93
|
+
<div key={idx} className="w-1/2">
|
|
94
|
+
<TestimonialCard testimonial={testimonials[idx]} isActive={offset === 0} />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
})}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Mobile: Single card with slide animation */}
|
|
101
|
+
<div className="sm:hidden w-full">
|
|
102
|
+
<div
|
|
103
|
+
className="flex transition-transform duration-500 ease-out"
|
|
104
|
+
style={{ transform: `translateX(-${currentIndex * 100}%)` }}
|
|
105
|
+
>
|
|
106
|
+
{testimonials.map((testimonial, i) => (
|
|
107
|
+
<div key={i} className="w-full flex-shrink-0 px-1">
|
|
108
|
+
<TestimonialCard testimonial={testimonial} isActive={i === currentIndex} />
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Navigation Arrows - Mobile & Tablet */}
|
|
116
|
+
<button
|
|
117
|
+
onClick={prevSlide}
|
|
118
|
+
className="lg:hidden absolute left-0 top-1/2 -translate-y-1/2 -translate-x-2 sm:-translate-x-4 w-10 h-10 rounded-full bg-surface-elevated border border-border shadow-lg flex items-center justify-center hover:bg-surface transition z-10"
|
|
119
|
+
aria-label="Previous testimonial"
|
|
120
|
+
>
|
|
121
|
+
<ChevronLeft className="w-5 h-5" />
|
|
122
|
+
</button>
|
|
123
|
+
<button
|
|
124
|
+
onClick={nextSlide}
|
|
125
|
+
className="lg:hidden absolute right-0 top-1/2 -translate-y-1/2 translate-x-2 sm:translate-x-4 w-10 h-10 rounded-full bg-surface-elevated border border-border shadow-lg flex items-center justify-center hover:bg-surface transition z-10"
|
|
126
|
+
aria-label="Next testimonial"
|
|
127
|
+
>
|
|
128
|
+
<ChevronRight className="w-5 h-5" />
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
{/* Dots - Mobile & Tablet */}
|
|
133
|
+
<div className="lg:hidden flex justify-center gap-2 mt-6">
|
|
134
|
+
{testimonials.map((_, i) => (
|
|
135
|
+
<button
|
|
136
|
+
key={i}
|
|
137
|
+
onClick={() => setCurrentIndex(i)}
|
|
138
|
+
className={`w-2 h-2 rounded-full transition-all ${
|
|
139
|
+
i === currentIndex
|
|
140
|
+
? "bg-accent w-6"
|
|
141
|
+
: "bg-border hover:bg-text-muted"
|
|
142
|
+
}`}
|
|
143
|
+
aria-label={`Go to testimonial ${i + 1}`}
|
|
144
|
+
/>
|
|
145
|
+
))}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</section>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function TestimonialCard({
|
|
153
|
+
testimonial,
|
|
154
|
+
isActive
|
|
155
|
+
}: {
|
|
156
|
+
testimonial: typeof testimonials[0];
|
|
157
|
+
isActive: boolean;
|
|
158
|
+
}) {
|
|
159
|
+
return (
|
|
160
|
+
<div
|
|
161
|
+
className={`
|
|
162
|
+
relative p-5 sm:p-6 rounded-2xl border transition-all duration-300
|
|
163
|
+
${isActive
|
|
164
|
+
? "bg-surface-elevated border-accent/30 shadow-lg shadow-accent/5"
|
|
165
|
+
: "bg-surface border-border hover:border-border-subtle"
|
|
166
|
+
}
|
|
167
|
+
`}
|
|
168
|
+
>
|
|
169
|
+
{/* Gradient accent */}
|
|
170
|
+
<div className={`absolute inset-x-0 top-0 h-1 rounded-t-2xl bg-gradient-to-r ${testimonial.color} opacity-60`} />
|
|
171
|
+
|
|
172
|
+
{/* Quote */}
|
|
173
|
+
<p className="text-text-primary text-sm sm:text-base leading-relaxed mb-6 min-h-[4.5rem]">
|
|
174
|
+
"{testimonial.quote}"
|
|
175
|
+
</p>
|
|
176
|
+
|
|
177
|
+
{/* Attribution */}
|
|
178
|
+
<div className="flex items-center gap-3">
|
|
179
|
+
<div className="w-10 h-10 rounded-xl bg-white/10 flex items-center justify-center p-1.5">
|
|
180
|
+
<Image
|
|
181
|
+
src={testimonial.logo}
|
|
182
|
+
alt={testimonial.model}
|
|
183
|
+
width={28}
|
|
184
|
+
height={28}
|
|
185
|
+
className="object-contain"
|
|
186
|
+
/>
|
|
187
|
+
</div>
|
|
188
|
+
<div>
|
|
189
|
+
<div className="font-semibold text-sm">{testimonial.model}</div>
|
|
190
|
+
<div className="text-text-muted text-xs">{testimonial.role}</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -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
|
+
}
|