import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/src/components/ui/alert-dialog";
import { Button } from "@/src/components/ui/button";
import { Card, CardContent } from "@/src/components/ui/card";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/src/components/ui/dropdown-menu";
import { Input } from "@/src/components/ui/input";
import { Label } from "@/src/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/src/components/ui/select";
import { Switch } from "@/src/components/ui/switch";
import { Tabs, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { splitUrl } from "@/src/lib/client-utils";
import { composeUrl } from "@/src/lib/general-utils";
import { buildCategorizedSources } from "@/src/lib/templating-utils";
import { ExecutionStep } from "@superglue/shared";
import {
Bug,
Check,
ChevronDown,
ChevronRight,
FileBraces,
FileInput,
FileOutput,
MessagesSquare,
Pencil,
Play,
RotateCw,
Route,
Square,
Trash2,
X,
} from "lucide-react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { JavaScriptCodeEditor } from "../../editors/JavaScriptCodeEditor";
import { TemplateAwareJsonEditor } from "../../editors/TemplateAwareJsonEditor";
import { TemplateAwareTextEditor } from "../../editors/TemplateAwareTextEditor";
import { HelpTooltip } from "../../utils/HelpTooltip";
import { useToolConfig, useExecution } from "../context";
import { CopyButton } from "../shared/CopyButton";
import { StepInputTab } from "./tabs/StepInputTab";
import { StepResultTab } from "./tabs/StepResultTab";
import { useRightSidebar } from "../../sidebar/RightSidebarContext";
export interface StepItem {
type: "step";
data: ExecutionStep;
stepResult: any;
transformError: undefined;
categorizedSources: ReturnType<typeof buildCategorizedSources>;
}
interface SpotlightStepCardProps {
step: ExecutionStep;
stepIndex: number;
onEdit?: (stepId: string, updatedStep: ExecutionStep, isUserInitiated?: boolean) => void;
onRemove?: () => void;
onExecuteStep?: () => Promise<void>;
onExecuteStepWithLimit?: (limit: number) => Promise<void>;
onAbort?: () => void;
isExecuting?: boolean;
showOutputSignal?: number;
onConfigEditingChange?: (editing: boolean) => void;
onDataSelectorChange?: (itemCount: number | null, isInitial: boolean) => void;
isFirstStep?: boolean;
isPayloadValid?: boolean;
}
export const SpotlightStepCard = React.memo(
({
step,
stepIndex,
onEdit,
onRemove,
onExecuteStep,
onExecuteStepWithLimit,
onAbort,
isExecuting,
showOutputSignal,
onConfigEditingChange,
onDataSelectorChange,
isFirstStep = false,
isPayloadValid = true,
}: SpotlightStepCardProps) => {
const {
isExecutingAny,
getStepResult,
getStepError,
isStepFailed,
isStepAborted,
canExecuteStep,
getDataSelectorResult,
} = useExecution();
const { sendMessageToAgent } = useRightSidebar();
const isGlobalExecuting = isExecutingAny;
const stepResult = getStepResult(step.id);
const stepFailed = isStepFailed(step.id);
const stepAborted = isStepAborted(step.id);
const canExecute = canExecuteStep(stepIndex);
const errorResult =
(stepFailed || stepAborted) && (!stepResult || typeof stepResult === "string");
const showFixButton = errorResult && !stepAborted;
const { output: dataSelectorOutput, error: dataSelectorError } = getDataSelectorResult(step.id);
const lastNotifiedStepIdRef = useRef<string | null>(null);
// Notify parent of data selector changes
useEffect(() => {
const isInitial = lastNotifiedStepIdRef.current !== step.id;
const hasValidOutput = !dataSelectorError && dataSelectorOutput != null;
const itemCount =
hasValidOutput && Array.isArray(dataSelectorOutput) ? dataSelectorOutput.length : null;
onDataSelectorChange?.(itemCount, isInitial);
if (isInitial) {
lastNotifiedStepIdRef.current = step.id;
}
}, [dataSelectorOutput, dataSelectorError, step.id, onDataSelectorChange]);
const [activePanel, setActivePanel] = useState<"input" | "config" | "output">("config");
const [showInvalidPayloadDialog, setShowInvalidPayloadDialog] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [pendingAction, setPendingAction] = useState<"execute" | null>(null);
const [isEditingInstruction, setIsEditingInstruction] = useState(false);
const [instructionEditValue, setInstructionEditValue] = useState(
step.apiConfig?.instruction || "",
);
const prevShowOutputSignalRef = useRef<number | undefined>(undefined);
const editingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const instructionTextareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
return () => {
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
};
}, []);
// === CONFIG TAB STATE (from ToolStepConfigurator) ===
const [advancedSettingsOpen, setAdvancedSettingsOpen] = useState(false);
const [paginationOpen, setPaginationOpen] = useState(false);
const [requestOptionsOpen, setRequestOptionsOpen] = useState(false);
const [headersText, setHeadersText] = useState("");
const [queryParamsText, setQueryParamsText] = useState("");
const serializeValue = (val: unknown): string => {
if (typeof val === "string") return val;
if (val !== undefined && val !== null) {
try {
return JSON.stringify(val, null, 2);
} catch {
return "{}";
}
}
return "{}";
};
const isEmptyValue = (val: string): boolean => {
const trimmed = val.trim();
return !trimmed || trimmed === "{}" || trimmed === "[]";
};
useEffect(() => {
const headers = serializeValue(step.apiConfig?.headers);
const queryParams = serializeValue(step.apiConfig?.queryParams);
setHeadersText(headers);
setQueryParamsText(queryParams);
}, [step.id, step.apiConfig?.headers, step.apiConfig?.queryParams, step.apiConfig?.body]);
useEffect(() => {
if (
showOutputSignal &&
showOutputSignal !== prevShowOutputSignalRef.current &&
stepResult != null
) {
setActivePanel("output");
}
prevShowOutputSignalRef.current = showOutputSignal;
}, [showOutputSignal, stepResult]);
const handleImmediateEdit = useCallback(
(updater: (s: any) => any) => {
if (!onEdit) return;
const updated = updater(step);
if (onConfigEditingChange) {
onConfigEditingChange(true);
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
editingTimeoutRef.current = setTimeout(() => onConfigEditingChange(false), 100);
}
onEdit(step.id, updated, true);
},
[onEdit, onConfigEditingChange, step],
);
const DEFAULT_PAGINATION_STOP_CONDITION =
"(response, pageInfo) => !response.data || response.data.length === 0";
const handlePaginationTypeChange = (value: string) => {
if (value === "none") {
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, pagination: undefined },
}));
} else {
handleImmediateEdit((s) => ({
...s,
apiConfig: {
...s.apiConfig,
pagination: {
type: value,
pageSize: s.apiConfig.pagination?.pageSize || "50",
cursorPath: s.apiConfig.pagination?.cursorPath || "",
stopCondition:
s.apiConfig.pagination?.stopCondition || DEFAULT_PAGINATION_STOP_CONDITION,
},
},
}));
}
};
const handleEditStepInstruction = useCallback(() => {
setInstructionEditValue(step.apiConfig?.instruction || "");
setIsEditingInstruction(true);
setTimeout(() => {
if (instructionTextareaRef.current) {
instructionTextareaRef.current.focus();
const len = instructionTextareaRef.current.value.length;
instructionTextareaRef.current.setSelectionRange(len, len);
}
}, 0);
}, [step.apiConfig?.instruction]);
const handleSaveInstruction = useCallback(() => {
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, instruction: instructionEditValue },
}));
setIsEditingInstruction(false);
}, [instructionEditValue, handleImmediateEdit]);
const handleCancelInstructionEdit = useCallback(() => {
setInstructionEditValue(step.apiConfig?.instruction || "");
setIsEditingInstruction(false);
}, [step.apiConfig?.instruction]);
const handleInstructionKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Escape") {
handleCancelInstructionEdit();
} else if (e.key === "Enter" && e.metaKey) {
handleSaveInstruction();
}
},
[handleCancelInstructionEdit, handleSaveInstruction],
);
const handleRunStepClick = () => {
if (isFirstStep && !isPayloadValid) {
setPendingAction("execute");
setShowInvalidPayloadDialog(true);
} else if (onExecuteStep) {
onExecuteStep();
}
};
const handleFixInChat = useCallback(() => {
const stepError = getStepError(step.id);
const errorMsg =
stepError || (typeof stepResult === "string" ? stepResult : "Step execution failed");
const truncatedError = errorMsg.length > 500 ? `${errorMsg.slice(0, 500)}...` : errorMsg;
sendMessageToAgent(
`Step "${step.id}" failed with the following error:\n\n${truncatedError}\n\nPlease fix this step.`,
);
}, [step.id, stepResult, getStepError, sendMessageToAgent]);
return (
<Card className="w-full max-w-6xl mx-auto shadow-md border border-border/50 dark:border-border/70 overflow-hidden bg-gradient-to-br from-muted/30 to-muted/10 dark:from-muted/40 dark:to-muted/20 backdrop-blur-sm">
<div className="p-3">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2 min-w-0 flex-1">
<Route className="h-4 w-4 text-muted-foreground flex-shrink-0" />
<h3 className="text-lg font-semibold truncate">
{step.id || `Step ${stepIndex + 1}`}
</h3>
</div>
</div>
<div className={activePanel === "config" ? "space-y-1" : "space-y-2"}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Tabs
value={activePanel}
onValueChange={(v) => setActivePanel(v as "input" | "config" | "output")}
>
<TabsList className="h-9 p-1 rounded-md">
<TabsTrigger
value="input"
className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"
>
<FileInput className="h-4 w-4" /> Step Input
</TabsTrigger>
<TabsTrigger
value="config"
className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"
>
<FileBraces className="h-4 w-4" /> Step Config
</TabsTrigger>
<TabsTrigger
value="output"
className="h-full px-3 text-xs flex items-center gap-1 rounded-sm data-[state=active]:rounded-sm"
>
<FileOutput className="h-4 w-4" /> Step Result
</TabsTrigger>
</TabsList>
</Tabs>
</div>
<div className="flex items-center gap-1.5">
{showFixButton && (
<Button
variant="glass-primary"
onClick={handleFixInChat}
className="h-8 px-3 gap-2 rounded-xl"
>
<MessagesSquare className="h-3.5 w-3.5" />
<span className="font-medium text-[13px]">Fix in chat</span>
</Button>
)}
{onExecuteStep && (
<div className="flex items-center">
{isExecuting && onAbort ? (
<Button
variant="glass"
onClick={onAbort}
className="h-8 px-3 gap-2 rounded-xl"
>
<Square className="h-3 w-3" />
<span className="font-medium text-[13px]">Stop</span>
</Button>
) : (
<span
title={
!canExecute
? "Execute previous steps first"
: isExecuting
? "Step is executing..."
: "Run this step"
}
>
<div
className={`relative flex rounded-xl border border-input bg-gradient-to-br from-muted/50 to-muted/30 dark:from-muted/50 dark:to-muted/30 backdrop-blur-sm shadow-sm ${dataSelectorOutput && Array.isArray(dataSelectorOutput) && dataSelectorOutput.length > 1 && onExecuteStepWithLimit ? "" : ""}`}
>
<Button
variant="ghost"
onClick={handleRunStepClick}
disabled={!canExecute || isExecuting || isGlobalExecuting}
className={`h-8 pl-3 gap-2 border-0 ${dataSelectorOutput && Array.isArray(dataSelectorOutput) && dataSelectorOutput.length > 1 && onExecuteStepWithLimit ? "pr-2 rounded-r-none" : "pr-3"}`}
>
{dataSelectorOutput &&
Array.isArray(dataSelectorOutput) &&
dataSelectorOutput.length > 1 ? (
<RotateCw className="h-3.5 w-3.5" />
) : (
<Play className="h-3 w-3" />
)}
<span className="font-medium text-[13px]">Run Step</span>
</Button>
{dataSelectorOutput &&
Array.isArray(dataSelectorOutput) &&
dataSelectorOutput.length > 1 &&
onExecuteStepWithLimit && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
disabled={!canExecute || isExecuting || isGlobalExecuting}
className="h-8 px-1.5 rounded-l-none border-0"
>
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => onExecuteStepWithLimit(1)}>
<Bug className="h-3.5 w-3.5 mr-2" />
Run single iteration
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{dataSelectorOutput &&
Array.isArray(dataSelectorOutput) &&
dataSelectorOutput.length > 1 && (
<span className="absolute -top-2 -left-2 min-w-[16px] h-[16px] px-1 text-[10px] font-bold bg-primary text-primary-foreground rounded flex items-center justify-center">
{dataSelectorOutput.length >= 1000
? `${Math.floor(dataSelectorOutput.length / 1000)}k`
: dataSelectorOutput.length}
</span>
)}
</div>
</span>
)}
</div>
)}
{onRemove && (
<Button
variant="ghost"
size="icon"
onClick={() => setShowDeleteConfirm(true)}
className="h-8 w-8"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</div>
</div>
<div>
{activePanel === "input" && (
<StepInputTab step={step} stepIndex={stepIndex} onEdit={onEdit} />
)}
{activePanel === "config" && (
<Card className="w-full border-none shadow-none opacity-100">
<CardContent className="space-y-3 text-sm p-3">
<div>
<Label className="text-xs flex items-center gap-1">Step Instruction</Label>
{isEditingInstruction ? (
<div className="relative mt-1 rounded-lg border shadow-sm bg-muted/30">
<textarea
ref={instructionTextareaRef}
value={instructionEditValue}
onChange={(e) => setInstructionEditValue(e.target.value)}
onKeyDown={handleInstructionKeyDown}
className="w-full min-h-[72px] text-xs font-mono border-0 bg-transparent shadow-none resize-y focus:outline-none focus:ring-0 px-3 py-2 pr-20"
placeholder="Describe what this step should do..."
/>
<div className="absolute top-1 right-1 flex items-center gap-1">
<button
type="button"
onClick={handleSaveInstruction}
className="h-6 w-6 flex items-center justify-center rounded transition-colors hover:bg-green-500/20 text-green-600"
title="Save (⌘+Enter)"
>
<Check className="h-3 w-3" />
</button>
<button
type="button"
onClick={handleCancelInstructionEdit}
className="h-6 w-6 flex items-center justify-center rounded transition-colors hover:bg-red-500/20 text-red-600"
title="Cancel (Esc)"
>
<X className="h-3 w-3" />
</button>
</div>
</div>
) : (
<div className="group relative mt-1 rounded-lg border shadow-sm bg-muted/30">
<div className="absolute top-0 right-0 bottom-0 z-10 flex items-center gap-1 pl-2 pr-1 bg-gradient-to-l from-muted via-muted/90 to-muted/60 opacity-0 group-hover:opacity-100 transition-opacity">
<button
type="button"
onClick={handleEditStepInstruction}
className="h-6 w-6 flex items-center justify-center rounded transition-colors hover:bg-muted/50"
title="Edit instruction"
aria-label="Edit instruction"
>
<Pencil className="h-3 w-3 text-muted-foreground" />
</button>
<CopyButton text={step.apiConfig.instruction || ""} />
</div>
<div className="h-9 flex items-center text-xs text-muted-foreground px-3 pr-16 truncate">
{step.apiConfig.instruction || (
<span className="text-muted-foreground italic">
Describe what this step should do...
</span>
)}
</div>
</div>
)}
</div>
<div>
<Label className="text-xs flex items-center gap-1">
Request & URL
<HelpTooltip text="Configure the HTTP method and URL for this request. Use variables like {variable} to reference previous step outputs." />
</Label>
<div className="space-y-2 mt-1">
<div className="flex gap-2">
<div className="rounded-lg border shadow-sm bg-muted/30">
<Select
value={step.apiConfig.method}
onValueChange={(value) =>
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, method: value },
}))
}
>
<SelectTrigger className="h-9 w-28 border-0 bg-transparent shadow-none">
<SelectValue placeholder="Method" />
</SelectTrigger>
<SelectContent>
{["GET", "POST", "PUT", "DELETE", "PATCH"].map((method) => (
<SelectItem key={method} value={method}>
{method}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<TemplateAwareTextEditor
value={composeUrl(
step.apiConfig.urlHost || "",
step.apiConfig.urlPath || "",
)}
onChange={(newValue) => {
const { urlHost, urlPath } = splitUrl(newValue);
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, urlHost, urlPath },
}));
}}
stepId={step.id}
className="flex-1"
placeholder="https://api.example.com/endpoint"
/>
</div>
</div>
</div>
{(() => {
const showBody = ["POST", "PUT", "PATCH"].includes(step.apiConfig.method);
const hasHeaders = !isEmptyValue(headersText);
const hasQueryParams = !isEmptyValue(queryParamsText);
const hasBody = showBody && !isEmptyValue(step.apiConfig.body || "");
const hasAnyRequestOptions = hasHeaders || hasQueryParams || hasBody;
return (
<div>
<div
onClick={() => setRequestOptionsOpen(!requestOptionsOpen)}
className="w-full flex items-center justify-between text-xs font-medium text-left p-2 rounded-md hover:bg-muted/50 transition-colors cursor-pointer"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setRequestOptionsOpen(!requestOptionsOpen);
}
}}
>
<div className="flex items-center gap-1">
{requestOptionsOpen ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<span>Request Options</span>
{!requestOptionsOpen && hasAnyRequestOptions && (
<span className="text-[10px] text-muted-foreground ml-1">
(configured)
</span>
)}
</div>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${requestOptionsOpen ? "max-h-[1200px] opacity-100" : "max-h-0 opacity-0"}`}
>
<div className="pl-2 space-y-3 pt-2">
<div>
<Label className="text-xs text-muted-foreground mb-1 block">
Headers
</Label>
<TemplateAwareJsonEditor
value={headersText}
onChange={(val) => {
setHeadersText(val || "");
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, headers: val || "" },
}));
}}
stepId={step.id}
minHeight="75px"
maxHeight="300px"
placeholder="{}"
showValidation={true}
/>
</div>
<div>
<Label className="text-xs text-muted-foreground mb-1 block">
Query Parameters
</Label>
<TemplateAwareJsonEditor
value={queryParamsText}
onChange={(val) => {
setQueryParamsText(val || "");
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, queryParams: val || "" },
}));
}}
stepId={step.id}
minHeight="75px"
maxHeight="300px"
placeholder="{}"
showValidation={true}
/>
</div>
{showBody && (
<div>
<Label className="text-xs text-muted-foreground mb-1 block">
Body
</Label>
<TemplateAwareJsonEditor
value={step.apiConfig.body || ""}
onChange={(val) =>
handleImmediateEdit((s) => ({
...s,
apiConfig: { ...s.apiConfig, body: val || "" },
}))
}
stepId={step.id}
minHeight="75px"
maxHeight="300px"
placeholder=""
/>
</div>
)}
</div>
</div>
</div>
);
})()}
<div>
<div
onClick={() => setPaginationOpen(!paginationOpen)}
className="w-full flex items-center justify-between text-xs font-medium text-left p-2 rounded-md hover:bg-muted/50 transition-colors cursor-pointer"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setPaginationOpen(!paginationOpen);
}
}}
>
<div className="flex items-center gap-1">
{paginationOpen ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<span>Pagination</span>
<HelpTooltip text="Configure pagination if the API returns data in pages. Only set this if you're using pagination variables like {'<<offset>>'}, {'<<page>>'}, or {'<<cursor>>'} in your request." />
</div>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${paginationOpen ? "max-h-[1000px] opacity-100" : "max-h-0 opacity-0"}`}
>
<div className="space-y-2 mt-1 border-muted">
<div className="pl-2 mb-1">
<div className="rounded-lg border shadow-sm bg-muted/30">
<Select
value={step.apiConfig.pagination?.type || "none"}
onValueChange={handlePaginationTypeChange}
>
<SelectTrigger className="h-9 border-0 bg-transparent shadow-none">
<SelectValue placeholder="No pagination" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No pagination</SelectItem>
<SelectItem value="OFFSET_BASED">
Offset-based (uses {"<<offset>>"})
</SelectItem>
<SelectItem value="PAGE_BASED">
Page-based (uses {"<<page>>"})
</SelectItem>
<SelectItem value="CURSOR_BASED">
Cursor-based (uses {"<<cursor>>"})
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{step.apiConfig.pagination && (
<div className="mt-2 gap-2 pl-2">
<div className="flex gap-2">
<div className="flex-1">
<Label className="text-xs">Page Size</Label>
<div className="rounded-lg border shadow-sm bg-muted/30 mt-1">
<Input
value={step.apiConfig.pagination.pageSize || "50"}
onChange={(e) =>
handleImmediateEdit((s) => ({
...s,
apiConfig: {
...s.apiConfig,
pagination: {
...(s.apiConfig.pagination || {}),
pageSize: e.target.value,
},
},
}))
}
className="h-9 text-xs border-0 bg-transparent shadow-none focus:ring-0 focus:ring-offset-0"
placeholder="50"
/>
</div>
</div>
{step.apiConfig.pagination.type === "CURSOR_BASED" && (
<div className="flex-1">
<Label className="text-xs">Cursor Path</Label>
<div className="rounded-lg border shadow-sm bg-muted/30 mt-1">
<Input
value={step.apiConfig.pagination.cursorPath || ""}
onChange={(e) =>
handleImmediateEdit((s) => ({
...s,
apiConfig: {
...s.apiConfig,
pagination: {
...(s.apiConfig.pagination || {}),
cursorPath: e.target.value,
},
},
}))
}
className="h-9 text-xs border-0 bg-transparent shadow-none focus:ring-0 focus:ring-offset-0"
placeholder="e.g., response.nextCursor"
/>
</div>
</div>
)}
</div>
<div className="mt-2 gap-2">
<Label className="text-xs flex items-center gap-1">
Stop Condition (JavaScript)
<HelpTooltip text="JavaScript function that returns true when pagination should stop. Receives (response, pageInfo) where pageInfo has: page, offset, cursor, total fetched." />
</Label>
<div className="mt-1">
<JavaScriptCodeEditor
value={
step.apiConfig.pagination.stopCondition ||
DEFAULT_PAGINATION_STOP_CONDITION
}
onChange={(val) =>
handleImmediateEdit((s) => ({
...s,
apiConfig: {
...s.apiConfig,
pagination: {
...(s.apiConfig.pagination || {}),
stopCondition: val,
},
},
}))
}
minHeight="50px"
maxHeight="300px"
resizable={true}
isTransformEditor={false}
autoFormatOnMount={true}
/>
</div>
</div>
</div>
)}
</div>
</div>
</div>
<div>
<div
onClick={() => setAdvancedSettingsOpen(!advancedSettingsOpen)}
className="w-full flex items-center justify-between text-xs font-medium text-left p-2 rounded-md hover:bg-muted/50 transition-colors cursor-pointer"
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setAdvancedSettingsOpen(!advancedSettingsOpen);
}
}}
>
<div className="flex items-center gap-1">
{advancedSettingsOpen ? (
<ChevronDown className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
<span>Advanced Settings</span>
</div>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${advancedSettingsOpen ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0"}`}
>
<div className="space-y-3 mt-2 border-muted">
<div className="flex items-center justify-between space-x-2 pl-2">
<div className="flex flex-col gap-1">
<label
htmlFor={`modify-${step.id}`}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Modifies data
</label>
<p className="text-[10px] text-muted-foreground">
Enable this if the step can modify or delete live data. When
enabled, you'll be prompted to confirm before execution.
</p>
</div>
<Switch
id={`modify-${step.id}`}
checked={step.modify === true}
onCheckedChange={(checked) => {
handleImmediateEdit((s) => ({
...s,
modify: checked === true,
}));
}}
/>
</div>
<div className="flex items-center justify-between space-x-2 pl-2">
<div className="flex flex-col gap-1">
<label
htmlFor={`continue-on-failure-${step.id}`}
className="text-xs font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Continue on failure
</label>
<p className="text-[10px] text-muted-foreground">
When enabled, the workflow continues even if this step fails. Failed
iterations are tracked in the results but don't stop execution.
Error detection is automatically disabled when this is enabled.
</p>
</div>
<Switch
id={`continue-on-failure-${step.id}`}
checked={step.failureBehavior === "CONTINUE"}
onCheckedChange={(checked) => {
handleImmediateEdit((s) => ({
...s,
failureBehavior: checked === true ? "CONTINUE" : "FAIL",
}));
}}
/>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
)}
{activePanel === "output" && (
<StepResultTab
step={step}
stepIndex={stepIndex}
isExecuting={isExecuting}
isActive={true}
/>
)}
</div>
</div>
</div>
<AlertDialog
open={showInvalidPayloadDialog}
onOpenChange={(open) => {
setShowInvalidPayloadDialog(open);
if (!open) {
setPendingAction(null);
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Tool Input Does Not Match Input Schema</AlertDialogTitle>
<AlertDialogDescription>
Your tool input does not match the input schema. This may cause execution to fail.
You can edit the input and schema in the Start (Tool Input) Card.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setShowInvalidPayloadDialog(false);
if (pendingAction === "execute" && onExecuteStep) {
onExecuteStep();
}
setPendingAction(null);
}}
>
Run Anyway
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Step</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete step "{step.id}"? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
setShowDeleteConfirm(false);
onRemove?.();
}}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card>
);
},
);