import { useState, useCallback, useEffect } from "react"; import { Plus, Trash2, ChevronUp, ChevronDown, Pencil, GripVertical } from "lucide-react"; import type { AutomationStep, AutomationStepType } from "@fusion/core"; import { StepTypeBadge } from "./StepTypeBadge"; import { CustomModelDropdown } from "./CustomModelDropdown"; import { fetchModels } from "../api"; import type { ModelInfo } from "../api"; interface ScheduleStepsEditorProps { steps: AutomationStep[]; onChange: (steps: AutomationStep[]) => void; /** Called when editing state changes. Useful for parent form validation. */ onEditingChange?: (isEditing: boolean) => void; } function generateStepId(): string { // crypto.randomUUID() may be unavailable in non-secure contexts (HTTP), // older browsers, or some test environments. Fall back to a // cryptographically-acceptable alternative when needed. if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return crypto.randomUUID(); } // Deterministic fallback: timestamp + random hex return `step-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`; } function createEmptyStep(type: AutomationStepType): AutomationStep { return { id: generateStepId(), type, name: type === "command" ? "New Command Step" : "New AI Prompt Step", command: type === "command" ? "" : undefined, prompt: type === "ai-prompt" ? "" : undefined, continueOnFailure: false, }; } interface StepEditorProps { step: AutomationStep; onSave: (step: AutomationStep) => void; onCancel: () => void; } function StepEditor({ step, onSave, onCancel }: StepEditorProps) { const [name, setName] = useState(step.name); const [type, setType] = useState(step.type); const [command, setCommand] = useState(step.command ?? ""); const [prompt, setPrompt] = useState(step.prompt ?? ""); const [modelProvider, setModelProvider] = useState(step.modelProvider ?? ""); const [modelId, setModelId] = useState(step.modelId ?? ""); const [timeoutMs, setTimeoutMs] = useState(step.timeoutMs); const [continueOnFailure, setContinueOnFailure] = useState(step.continueOnFailure ?? false); const [errors, setErrors] = useState>({}); const [models, setModels] = useState([]); const [modelsLoading, setModelsLoading] = useState(false); const [modelsError, setModelsError] = useState(null); // Fetch models on mount useEffect(() => { let cancelled = false; setModelsLoading(true); setModelsError(null); fetchModels() .then((response) => { if (!cancelled) { setModels(response.models); } }) .catch((err: unknown) => { if (!cancelled) { setModelsError(err instanceof Error ? err.message : "Failed to load models"); } }) .finally(() => { if (!cancelled) { setModelsLoading(false); } }); return () => { cancelled = true; }; }, []); const validate = useCallback((): boolean => { const e: Record = {}; if (!name.trim()) e.name = "Step name is required"; if (type === "command" && !command.trim()) e.command = "Command is required"; if (type === "ai-prompt" && !prompt.trim()) e.prompt = "Prompt is required"; if (timeoutMs !== undefined && timeoutMs < 1000) { e.timeoutMs = "Timeout must be at least 1 second (1000ms)"; } setErrors(e); return Object.keys(e).length === 0; }, [name, type, command, prompt, timeoutMs]); // Compute combined model value from separate fields const modelValue = (modelProvider && modelId) ? `${modelProvider}/${modelId}` : ""; // Handle model selection from the dropdown const handleModelChange = useCallback((value: string) => { if (!value) { setModelProvider(""); setModelId(""); } else { const slashIdx = value.indexOf("/"); if (slashIdx !== -1) { setModelProvider(value.slice(0, slashIdx)); setModelId(value.slice(slashIdx + 1)); } } }, []); const handleSave = useCallback(() => { if (!validate()) return; onSave({ ...step, name: name.trim(), type, command: type === "command" ? command.trim() : undefined, prompt: type === "ai-prompt" ? prompt.trim() : undefined, modelProvider: type === "ai-prompt" && modelProvider ? modelProvider.trim() : undefined, modelId: type === "ai-prompt" && modelId ? modelId.trim() : undefined, timeoutMs: timeoutMs || undefined, continueOnFailure, }); }, [validate, onSave, step, name, type, command, prompt, modelProvider, modelId, timeoutMs, continueOnFailure]); return (
setName(e.target.value)} aria-invalid={!!errors.name} /> {errors.name && {errors.name}}
{type === "command" && (