import { useState, useEffect, useCallback } from "react"; import type { WorkflowStep, WorkflowStepInput, WorkflowStepMode } from "@fusion/core"; import { fetchWorkflowSteps, createWorkflowStep, updateWorkflowStep, deleteWorkflowStep, refineWorkflowStepPrompt, fetchWorkflowStepTemplates, createWorkflowStepFromTemplate, fetchScripts, type WorkflowStepTemplate, } from "../api"; import type { ToastType } from "../hooks/useToast"; import { X, Plus, Pencil, Trash2, Sparkles, Check, Loader2, FileText, CheckCircle, Shield, Zap, Eye, LayoutGrid, BookOpen, Terminal, MessageSquare, } from "lucide-react"; interface WorkflowStepManagerProps { isOpen: boolean; onClose: () => void; addToast: (message: string, type?: ToastType) => void; projectId?: string; } interface StepFormData { name: string; description: string; mode: WorkflowStepMode; prompt: string; scriptName: string; enabled: boolean; } type TabId = "my-steps" | "templates"; const EMPTY_FORM: StepFormData = { name: "", description: "", mode: "prompt", prompt: "", scriptName: "", enabled: true, }; /** Map template icon names to Lucide components */ function getTemplateIcon(iconName: string | undefined) { switch (iconName) { case "file-text": return FileText; case "check-circle": return CheckCircle; case "shield": return Shield; case "zap": return Zap; case "eye": return Eye; default: return CheckCircle; } } /** Get category badge colors */ function getCategoryColors(category: string): { bg: string; text: string } { switch (category.toLowerCase()) { case "quality": return { bg: "rgba(59, 130, 246, 0.15)", text: "#3b82f6" }; case "security": return { bg: "rgba(239, 68, 68, 0.15)", text: "#ef4444" }; default: return { bg: "var(--bg-tertiary)", text: "var(--text-secondary)" }; } } export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: WorkflowStepManagerProps) { const [steps, setSteps] = useState([]); const [templates, setTemplates] = useState([]); const [loading, setLoading] = useState(true); const [templatesLoading, setTemplatesLoading] = useState(true); const [activeTab, setActiveTab] = useState("my-steps"); const [editingId, setEditingId] = useState(null); const [isCreating, setIsCreating] = useState(false); const [form, setForm] = useState(EMPTY_FORM); const [saving, setSaving] = useState(false); const [refining, setRefining] = useState(false); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [addingTemplateId, setAddingTemplateId] = useState(null); const [availableScripts, setAvailableScripts] = useState>({}); const loadSteps = useCallback(async () => { try { setLoading(true); const data = await fetchWorkflowSteps(projectId); setSteps(data); } catch (err: any) { addToast(err.message || "Failed to load workflow steps", "error"); } finally { setLoading(false); } }, [addToast, projectId]); const loadScripts = useCallback(async () => { try { const scripts = await fetchScripts(projectId); setAvailableScripts(scripts || {}); } catch { // Silently ignore — scripts are optional } }, [projectId]); const loadTemplates = useCallback(async () => { try { setTemplatesLoading(true); const response = await fetchWorkflowStepTemplates(); setTemplates(response.templates); } catch (err: any) { addToast(err.message || "Failed to load templates", "error"); } finally { setTemplatesLoading(false); } }, [addToast]); useEffect(() => { if (isOpen) { loadSteps(); loadTemplates(); loadScripts(); } }, [isOpen, loadSteps, loadTemplates, loadScripts]); const handleCreate = useCallback(() => { setIsCreating(true); setEditingId(null); setForm(EMPTY_FORM); }, []); const handleEdit = useCallback((step: WorkflowStep) => { setEditingId(step.id); setIsCreating(false); setForm({ name: step.name, description: step.description, mode: step.mode || "prompt", prompt: step.prompt, scriptName: step.scriptName || "", enabled: step.enabled, }); }, []); const handleCancel = useCallback(() => { setEditingId(null); setIsCreating(false); setForm(EMPTY_FORM); }, []); const handleSave = useCallback(async () => { if (!form.name.trim() || !form.description.trim()) { addToast("Name and description are required", "error"); return; } setSaving(true); try { if (isCreating) { const input: WorkflowStepInput = { name: form.name.trim(), description: form.description.trim(), mode: form.mode, prompt: form.mode === "prompt" ? (form.prompt.trim() || undefined) : undefined, scriptName: form.mode === "script" ? form.scriptName.trim() : undefined, enabled: form.enabled, }; await createWorkflowStep(input, projectId); addToast("Workflow step created", "success"); } else if (editingId) { await updateWorkflowStep(editingId, { name: form.name.trim(), description: form.description.trim(), mode: form.mode, prompt: form.mode === "prompt" ? form.prompt : "", scriptName: form.mode === "script" ? form.scriptName.trim() : undefined, enabled: form.enabled, }, projectId); addToast("Workflow step updated", "success"); } setIsCreating(false); setEditingId(null); setForm(EMPTY_FORM); await loadSteps(); } catch (err: any) { addToast(err.message || "Failed to save workflow step", "error"); } finally { setSaving(false); } }, [form, isCreating, editingId, addToast, loadSteps]); const handleDelete = useCallback(async (id: string) => { try { await deleteWorkflowStep(id, projectId); addToast("Workflow step deleted", "success"); setDeleteConfirmId(null); if (editingId === id) { setEditingId(null); setForm(EMPTY_FORM); } await loadSteps(); } catch (err: any) { addToast(err.message || "Failed to delete workflow step", "error"); } }, [editingId, addToast, loadSteps]); const handleRefine = useCallback(async () => { if (!editingId && !isCreating) return; // Refine only works for prompt mode if (form.mode !== "prompt") return; // For new steps being created, we need to save first then refine if (isCreating) { if (!form.name.trim() || !form.description.trim()) { addToast("Name and description are required before refining", "error"); return; } setSaving(true); try { const input: WorkflowStepInput = { name: form.name.trim(), description: form.description.trim(), mode: "prompt", prompt: form.prompt.trim() || undefined, enabled: form.enabled, }; const created = await createWorkflowStep(input, projectId); setIsCreating(false); setEditingId(created.id); // Now refine setRefining(true); const result = await refineWorkflowStepPrompt(created.id, projectId); setForm((prev) => ({ ...prev, prompt: result.prompt })); addToast("Prompt refined with AI", "success"); await loadSteps(); } catch (err: any) { addToast(err.message || "Failed to refine prompt", "error"); } finally { setSaving(false); setRefining(false); } return; } if (!editingId) return; setRefining(true); try { const result = await refineWorkflowStepPrompt(editingId, projectId); setForm((prev) => ({ ...prev, prompt: result.prompt })); addToast("Prompt refined with AI", "success"); await loadSteps(); } catch (err: any) { addToast(err.message || "Failed to refine prompt", "error"); } finally { setRefining(false); } }, [editingId, isCreating, form, addToast, loadSteps]); const handleAddTemplate = useCallback(async (template: WorkflowStepTemplate) => { setAddingTemplateId(template.id); try { await createWorkflowStepFromTemplate(template.id, projectId); addToast(`Added ${template.name} workflow step`, "success"); await loadSteps(); // Switch to "My Workflow Steps" tab to show the newly added step setActiveTab("my-steps"); } catch (err: any) { if (err.message?.includes("already exists")) { addToast(`A workflow step named '${template.name}' already exists`, "error"); } else { addToast(err.message || "Failed to add workflow step from template", "error"); } } finally { setAddingTemplateId(null); } }, [addToast, loadSteps]); if (!isOpen) return null; const isEditing = isCreating || editingId !== null; return (
e.stopPropagation()} role="dialog" aria-label="Workflow Steps" > {/* Header */}

Workflow Steps

{loading ? (
Loading...
) : ( <> {/* Tab Navigation */} {!isEditing && (
)} {/* My Workflow Steps Tab */} {activeTab === "my-steps" && !isEditing && ( <> {steps.length === 0 && (
No workflow steps defined. Create one to get started, or add one from the Templates tab.
)} {steps.length > 0 && (
{steps.map((step) => (
{step.name} {step.enabled ? "Enabled" : "Disabled"} {(step.mode || "prompt") === "script" ? "Script" : "AI Prompt"}
{step.description}
{deleteConfirmId === step.id ? (
) : ( )}
))}
)} )} {/* Templates Tab */} {activeTab === "templates" && !isEditing && ( <> {templatesLoading ? (
Loading templates...
) : templates.length === 0 ? (
No templates available.
) : (
{templates.map((template) => { const IconComponent = getTemplateIcon(template.icon); const categoryColors = getCategoryColors(template.category); const isAdding = addingTemplateId === template.id; return (
{/* Icon */}
{/* Content */}
{template.name} {template.category}
{template.description}
); })}
)} )} {/* Edit / Create form */} {isEditing && (

{isCreating ? "New Workflow Step" : "Edit Workflow Step"}

{/* Name */}
setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="e.g. Documentation Review" data-testid="workflow-step-name" />
{/* Description */}