'use client'; import { useMemo, useState, type ReactNode } from 'react'; import { Plus, MoreVertical, Play, Copy, Trash2, GitBranch, Cpu, Activity, Search, } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Textarea } from '../ui/textarea'; import { Badge } from '../ui/badge'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '../ui/table'; import { Tabs, TabsList, TabsTrigger } from '../ui/tabs'; import { cn } from '../../lib/utils'; /** * Minimal, in-package shape of a workflow list row. Mirrors the subset of the * backend list item this composer reads — kept local so the component carries * no app type coupling. */ export interface WorkflowListItem { uuid: string; name: string; description?: string | null; isBrain?: boolean; isTemplate?: boolean; nodeCount: number; version: number | string; lastExecutionStatus?: string | null; lastExecutionAt?: string | null; lastMod: string; executionCount?: number | null; } /** Payload for creating a new workflow. */ export interface WorkflowCreateInput { name: string; description?: string; isBrain?: boolean; } export interface WorkflowListComposerProps { /** Workflow rows to render. */ workflows: WorkflowListItem[]; /** Whether the list is loading (drives skeleton/empty messaging). */ isLoading?: boolean; /** Error message to surface, if any. */ error?: string | null; /** Navigate to a workflow's detail/canvas view. */ onNavigateToDetail: (uuid: string) => void; /** Navigate to a workflow's executions view. Falls back to detail when omitted. */ onNavigateToExecutions?: (uuid: string) => void; /** Delete a workflow by id. */ onDelete: (uuid: string) => void | Promise; /** Create a workflow from the inline create dialog. */ onCreate: (input: WorkflowCreateInput) => void | Promise; /** Execute a workflow by id (optional — Execute action hidden when omitted). */ onExecute?: (uuid: string) => void | Promise; /** Duplicate a workflow by id (optional — Duplicate action hidden when omitted). */ onClone?: (uuid: string) => void | Promise; /** Re-fetch the list (e.g. after search/filter/tab changes). */ onRefresh?: (params: { tab: 'workflows' | 'brains'; search: string; status: string; }) => void; /** Formats an ISO timestamp for display. Defaults to the raw string. */ formatTimestamp?: (timestamp: string) => string; } const STATUS_OPTIONS = [ { label: 'All', value: 'all' }, { label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }, { label: 'Templates', value: 'template' }, ] as const; /** * Inline status badge — substitute for the app's `StatusBadge` primitive * (not part of @startsimpli/ui). Maps common execution status slugs to a * theme-aware Badge color. */ function StatusBadge({ status }: { status: string }) { const tone: Record = { completed: 'border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950/30 dark:text-green-400', success: 'border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950/30 dark:text-green-400', running: 'border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950/30 dark:text-blue-400', failed: 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400', error: 'border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950/30 dark:text-red-400', pending: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-400', waiting: 'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-400', }; return ( {status} ); } export function WorkflowListComposer({ workflows, isLoading = false, error = null, onNavigateToDetail, onNavigateToExecutions, onDelete, onCreate, onExecute, onClone, onRefresh, formatTimestamp, }: WorkflowListComposerProps) { const [activeTab, setActiveTab] = useState<'workflows' | 'brains'>( 'workflows' ); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); // Create dialog state const [createDialogOpen, setCreateDialogOpen] = useState(false); const [createForm, setCreateForm] = useState({ name: '', }); const [creating, setCreating] = useState(false); const [createNameError, setCreateNameError] = useState(null); // Delete dialog state const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [workflowToDelete, setWorkflowToDelete] = useState(null); const isBrains = activeTab === 'brains'; const formatTime = formatTimestamp ?? ((t: string) => t); // Client-side filtering over the controlled `workflows` prop. The consumer // may also refetch via onRefresh; this keeps the UI responsive in between. const visibleWorkflows = useMemo(() => { const q = searchQuery.trim().toLowerCase(); return workflows.filter((w) => { if (isBrains ? w.isBrain !== true : w.isBrain === true) return false; if (statusFilter === 'template' && !w.isTemplate) return false; if (q) { const haystack = `${w.name} ${w.description ?? ''}`.toLowerCase(); if (!haystack.includes(q)) return false; } return true; }); }, [workflows, isBrains, statusFilter, searchQuery]); const notifyRefresh = ( next: Partial<{ tab: 'workflows' | 'brains'; search: string; status: string; }> ) => { onRefresh?.({ tab: next.tab ?? activeTab, search: next.search ?? searchQuery, status: next.status ?? statusFilter, }); }; const handleTabChange = (v: string) => { const tab = v as 'workflows' | 'brains'; setActiveTab(tab); setSearchQuery(''); setStatusFilter('all'); notifyRefresh({ tab, search: '', status: 'all' }); }; const handleSearch = (value: string) => { setSearchQuery(value); notifyRefresh({ search: value }); }; const handleStatusChange = (value: string) => { setStatusFilter(value); notifyRefresh({ status: value }); }; const handleCreate = async () => { if (!createForm.name.trim()) { setCreateNameError('Name is required'); return; } setCreateNameError(null); try { setCreating(true); await onCreate({ ...createForm, isBrain: isBrains }); setCreateDialogOpen(false); setCreateForm({ name: '' }); } finally { setCreating(false); } }; const handleDeleteConfirm = async () => { if (!workflowToDelete) return; try { await onDelete(workflowToDelete.uuid); } finally { setDeleteDialogOpen(false); setWorkflowToDelete(null); } }; const emptyMessage = searchQuery ? `No ${isBrains ? 'brains' : 'workflows'} match your search` : `No ${isBrains ? 'brains' : 'workflows'} yet. Create your first ${isBrains ? 'brain' : 'workflow'} to get started.`; return (
{/* Page header */}

{isBrains ? ( ) : ( )} {isBrains ? 'Brains' : 'Workflows'}

{isBrains ? 'Self-contained think/decide/act loops that run inside workflows' : 'Top-level execution loops that orchestrate your agents'}

Workflows Brains {/* Search + status filter (inline substitute for SearchFilterBar) */}
handleSearch(e.target.value)} placeholder={ isBrains ? 'Search brains by name or description...' : 'Search workflows by name or description...' } className="pl-9" />
{STATUS_OPTIONS.map((opt) => ( ))}
{/* Data table (inline substitute for DataTable) */}
Workflow Nodes Version Status Last Run Last Edit Executions Actions {isLoading ? ( Loading… ) : error ? ( {error} ) : visibleWorkflows.length === 0 ? ( {emptyMessage} ) : ( visibleWorkflows.map((row) => ( onNavigateToDetail(row.uuid)} >
{row.isBrain && ( )} {row.name}
{row.description && (
{row.description}
)} {row.isTemplate && ( template )}
{row.nodeCount} v{row.version} {row.lastExecutionStatus ? ( ) : ( )} {row.lastExecutionAt ? ( {formatTime(row.lastExecutionAt)} ) : ( )} {formatTime(row.lastMod)} { setWorkflowToDelete(row); setDeleteDialogOpen(true); }} />
)) )}
{/* Create Workflow Dialog */} {isBrains ? 'Create Brain' : 'Create Workflow'} Create a new workflow definition. You can add nodes on the canvas after creation.
{ setCreateForm((prev) => ({ ...prev, name: e.target.value })); if (createNameError) setCreateNameError(null); }} aria-invalid={createNameError ? 'true' : 'false'} aria-describedby={ createNameError ? 'workflow-name-error' : undefined } className={ createNameError ? 'border-red-500 focus-visible:ring-red-500' : '' } /> {createNameError && ( )}