import { useState, useEffect, useCallback } from "react"; import type { WorkflowStep, WorkflowStepInput } from "@kb/core"; import { fetchWorkflowSteps, createWorkflowStep, updateWorkflowStep, deleteWorkflowStep, refineWorkflowStepPrompt } from "../api"; import type { ToastType } from "../hooks/useToast"; import { X, Plus, Pencil, Trash2, Sparkles, Check, Loader2 } from "lucide-react"; interface WorkflowStepManagerProps { isOpen: boolean; onClose: () => void; addToast: (message: string, type?: ToastType) => void; } interface StepFormData { name: string; description: string; prompt: string; enabled: boolean; } const EMPTY_FORM: StepFormData = { name: "", description: "", prompt: "", enabled: true, }; export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepManagerProps) { const [steps, setSteps] = useState([]); const [loading, setLoading] = useState(true); 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 loadSteps = useCallback(async () => { try { setLoading(true); const data = await fetchWorkflowSteps(); setSteps(data); } catch (err: any) { addToast(err.message || "Failed to load workflow steps", "error"); } finally { setLoading(false); } }, [addToast]); useEffect(() => { if (isOpen) { loadSteps(); } }, [isOpen, loadSteps]); 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, prompt: step.prompt, 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(), prompt: form.prompt.trim() || undefined, enabled: form.enabled, }; await createWorkflowStep(input); addToast("Workflow step created", "success"); } else if (editingId) { await updateWorkflowStep(editingId, { name: form.name.trim(), description: form.description.trim(), prompt: form.prompt, enabled: form.enabled, }); 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); 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; // 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(), prompt: form.prompt.trim() || undefined, enabled: form.enabled, }; const created = await createWorkflowStep(input); setIsCreating(false); setEditingId(created.id); // Now refine setRefining(true); const result = await refineWorkflowStepPrompt(created.id); 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); 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]); if (!isOpen) return null; const isEditing = isCreating || editingId !== null; return (
e.stopPropagation()} role="dialog" aria-label="Workflow Steps" > {/* Header */}

Workflow Steps

{loading ? (
Loading...
) : ( <> {/* Step list */} {steps.length === 0 && !isEditing && (
No workflow steps defined. Create one to get started.
)} {steps.length > 0 && !isEditing && (
{steps.map((step) => (
{step.name} {step.enabled ? "Enabled" : "Disabled"}
{step.description}
{deleteConfirmId === step.id ? (
) : ( )}
))}
)} {/* Edit / Create form */} {isEditing && (

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

{/* Name */}
setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="e.g. Documentation Review" style={{ width: "100%", padding: "8px 12px", borderRadius: "6px", border: "1px solid var(--border-primary)", background: "var(--bg-primary)", color: "var(--text-primary)", fontSize: "13px", }} data-testid="workflow-step-name" />
{/* Description */}