"use client";
import { useTools } from "@/src/app/tools-context";
import { Button } from "@/src/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/src/components/ui/dialog";
import { Input } from "@/src/components/ui/input";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/src/components/ui/tabs";
import { Textarea } from "@/src/components/ui/textarea";
import { cn } from "@/src/lib/general-utils";
import { ExecutionStep } from "@superglue/shared";
import { AlertTriangle, Loader2, WandSparkles } from "lucide-react";
import { useEffect, useState } from "react";
import { useGenerateStepConfig } from "../hooks/use-generate-step-config";
import { IntegrationSelector } from "../shared/IntegrationSelector";
interface AddStepDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfirm: (stepId: string, instruction: string, integrationId?: string) => void;
onConfirmTool?: (steps: ExecutionStep[]) => void;
onConfirmGenerate?: (step: ExecutionStep) => void;
existingStepIds: string[];
stepInput?: Record<string, any>;
currentToolId?: string;
}
export function AddStepDialog({
open,
onOpenChange,
onConfirm,
onConfirmTool,
onConfirmGenerate,
existingStepIds,
stepInput,
currentToolId,
}: AddStepDialogProps) {
const [stepId, setStepId] = useState("");
const [instruction, setInstruction] = useState("");
const [selectedIntegrationId, setSelectedIntegrationId] = useState<string>("");
const [error, setError] = useState("");
const [activeTab, setActiveTab] = useState<"scratch" | "tool">("scratch");
const [selectedToolId, setSelectedToolId] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const { tools, isInitiallyLoading, isRefreshing, refreshTools } = useTools();
const { generateConfig, isGenerating, error: generateError } = useGenerateStepConfig();
useEffect(() => {
refreshTools();
}, []);
useEffect(() => {
if (open) {
setStepId("");
setInstruction("");
setSelectedIntegrationId("");
setError("");
}
}, [open]);
const handleOpenChange = (newOpen: boolean) => {
if (!newOpen) {
setError("");
setInstruction("");
setSelectedIntegrationId("");
setSelectedToolId(null);
setActiveTab("scratch");
setSearchQuery("");
}
onOpenChange(newOpen);
};
const filteredTools = tools.filter((tool) => {
// Exclude tools with no steps
if (!tool.steps || tool.steps.length === 0) return false;
// Exclude the current tool
if (currentToolId && tool.id === currentToolId) return false;
if (!searchQuery.trim()) return true;
const query = searchQuery.toLowerCase();
return (
tool.id?.toLowerCase().includes(query) || tool.instruction?.toLowerCase().includes(query)
);
});
const handleConfirmScratch = () => {
const trimmedId = stepId.trim();
if (!trimmedId) {
setError("Step ID is required");
return;
}
if (existingStepIds.includes(trimmedId)) {
setError(`Step with ID "${trimmedId}" already exists`);
return;
}
if (!selectedIntegrationId) {
setError("Please select an integration");
return;
}
onConfirm(trimmedId, instruction.trim(), selectedIntegrationId);
setError("");
};
const handleConfirmTool = () => {
if (!selectedToolId) {
setError("Please select a tool");
return;
}
const selectedTool = tools.find((t) => t.id === selectedToolId);
if (!selectedTool || !selectedTool.steps) {
setError("Selected tool has no steps");
return;
}
// Rename imported steps if they collide with existing step IDs
const usedIds = new Set(existingStepIds);
const renamedSteps = selectedTool.steps.map((step) => {
let newId = step.id;
if (usedIds.has(newId)) {
let suffix = 2;
while (usedIds.has(`${step.id}${suffix}`)) {
suffix++;
}
newId = `${step.id}${suffix}`;
}
usedIds.add(newId);
if (newId !== step.id) {
return {
...step,
id: newId,
apiConfig: step.apiConfig ? { ...step.apiConfig, id: newId } : undefined,
};
}
return step;
});
if (onConfirmTool) {
onConfirmTool(renamedSteps);
}
setError("");
setSelectedToolId(null);
};
const handleConfirmGenerate = async () => {
const trimmedId = stepId.trim();
const trimmedInstruction = instruction.trim();
if (!trimmedId) {
setError("Step ID is required");
return;
}
if (existingStepIds.includes(trimmedId)) {
setError(`Step with ID "${trimmedId}" already exists`);
return;
}
if (!selectedIntegrationId) {
setError("Please select an integration");
return;
}
if (!trimmedInstruction) {
setError("Instruction is required to automatically generate step");
return;
}
try {
setError("");
const result = await generateConfig({
currentStepConfig: {
id: trimmedId,
instruction: trimmedInstruction,
},
stepInput: stepInput,
integrationId: selectedIntegrationId,
});
const newStep: ExecutionStep = {
id: trimmedId,
integrationId: selectedIntegrationId,
apiConfig: {
...result.config,
id: trimmedId,
instruction: trimmedInstruction,
},
loopSelector: result.dataSelector,
};
if (onConfirmGenerate) {
onConfirmGenerate(newStep);
}
handleOpenChange(false);
} catch (err: any) {
setError(err.message || "Failed to generate step configuration");
}
};
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Add</DialogTitle>
<DialogDescription>
Create a new step from scratch or import steps from an existing tool
</DialogDescription>
</DialogHeader>
<Tabs
value={activeTab}
onValueChange={(v) => setActiveTab(v as "scratch" | "tool")}
className="overflow-hidden w-full"
>
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="scratch">Add new step</TabsTrigger>
<TabsTrigger value="tool">Import tool</TabsTrigger>
</TabsList>
<TabsContent value="scratch" className="space-y-4 py-4">
<div className="space-y-2">
<label htmlFor="step-id" className="text-sm font-medium">
Step ID
</label>
<Input
id="step-id"
value={stepId}
onChange={(e) => {
setStepId(e.target.value);
setError("");
}}
placeholder="e.g., fetch_users"
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Integration</label>
<IntegrationSelector
value={selectedIntegrationId}
onValueChange={(value) => {
setSelectedIntegrationId(value);
setError("");
}}
/>
</div>
<div className="space-y-2">
<label htmlFor="step-instruction" className="text-sm font-medium">
Instruction
</label>
<Textarea
id="step-instruction"
value={instruction}
onChange={(e) => {
setInstruction(e.target.value);
setError("");
}}
placeholder="e.g., Fetch all users from the API"
rows={4}
className="resize-none focus:ring-inset"
/>
</div>
{error && (
<div className="flex items-center gap-2 rounded-md border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-sm text-amber-700 dark:text-amber-300">
<AlertTriangle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
</TabsContent>
<TabsContent value="tool" className="space-y-4 py-4">
{isInitiallyLoading || isRefreshing ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : tools.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">No tools found</div>
) : (
<div className="space-y-3">
<div className="space-y-2">
<Input
placeholder="Search by ID or instruction..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full"
/>
</div>
<div className="border rounded-lg max-h-[400px] overflow-y-auto overflow-x-hidden">
{filteredTools.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No tools match your search
</div>
) : (
filteredTools.map((tool) => (
<button
key={tool.id}
onClick={() => {
setSelectedToolId(tool.id);
setError("");
}}
className={cn(
"w-full text-left px-4 py-3 border-b last:border-b-0 hover:bg-accent transition-colors overflow-hidden",
selectedToolId === tool.id && "bg-accent",
)}
>
<div className="font-medium truncate">{tool.id}</div>
{tool.instruction && (
<div className="text-sm text-muted-foreground truncate mt-1">
{tool.instruction}
</div>
)}
<div className="text-xs text-muted-foreground mt-1">
{tool.steps?.length || 0} step{tool.steps?.length !== 1 ? "s" : ""}
</div>
</button>
))
)}
</div>
{error && (
<div className="flex items-center gap-2 rounded-md border border-amber-500/50 bg-amber-500/10 px-3 py-2 text-sm text-amber-700 dark:text-amber-300">
<AlertTriangle className="h-4 w-4 flex-shrink-0" />
<span>{error}</span>
</div>
)}
</div>
)}
</TabsContent>
</Tabs>
<DialogFooter>
<Button variant="outline" onClick={() => handleOpenChange(false)}>
Cancel
</Button>
{activeTab === "scratch" ? (
<>
<Button variant="outline" onClick={handleConfirmScratch} disabled={isGenerating}>
Add Step
</Button>
<Button onClick={handleConfirmGenerate} disabled={isGenerating}>
{isGenerating ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Generating...
</>
) : (
<>
<WandSparkles className="h-4 w-4" />
Generate Step
</>
)}
</Button>
</>
) : (
<Button onClick={handleConfirmTool} disabled={isInitiallyLoading}>
Import Steps
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}