diff --git a/app/api/workflows/[key]/route.ts b/app/api/workflows/[key]/route.ts new file mode 100644 index 0000000..141483e --- /dev/null +++ b/app/api/workflows/[key]/route.ts @@ -0,0 +1,132 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { saveWorkflow, deleteWorkflow, userWorkflowExists } from '@/services/workflows/workflowFileService'; +import { WorkflowRegistry, BUNDLED_WORKFLOW_KEYS } from '@/services/workflows/registry'; + +// PUT /api/workflows/[key] - Create or update a workflow +export async function PUT( + request: NextRequest, + { params }: { params: Promise<{ key: string }> } +) { + try { + const { key } = await params; + const body = await request.json(); + + // Validate required fields + if (!body.displayName || typeof body.displayName !== 'string') { + return NextResponse.json( + { success: false, error: 'displayName is required' }, + { status: 400 } + ); + } + + if (!body.instructions || typeof body.instructions !== 'string') { + return NextResponse.json( + { success: false, error: 'instructions is required' }, + { status: 400 } + ); + } + + // Save the workflow + saveWorkflow({ + key, + displayName: body.displayName.trim(), + description: body.description?.trim() || '', + instructions: body.instructions, + enabled: body.enabled !== false, + requiresFocusedNode: body.requiresFocusedNode !== false, + }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error('Error saving workflow:', error); + return NextResponse.json( + { success: false, error: 'Failed to save workflow' }, + { status: 500 } + ); + } +} + +// DELETE /api/workflows/[key] - Delete a workflow (resets to default if bundled) +export async function DELETE( + request: NextRequest, + { params }: { params: Promise<{ key: string }> } +) { + try { + const { key } = await params; + + // Check if user file exists + if (!userWorkflowExists(key)) { + // If it's a bundled workflow, nothing to delete + if (BUNDLED_WORKFLOW_KEYS.has(key)) { + return NextResponse.json( + { success: false, error: 'Cannot delete bundled workflow (no user override exists)' }, + { status: 400 } + ); + } + return NextResponse.json( + { success: false, error: 'Workflow not found' }, + { status: 404 } + ); + } + + const deleted = deleteWorkflow(key); + + if (!deleted) { + return NextResponse.json( + { success: false, error: 'Failed to delete workflow' }, + { status: 500 } + ); + } + + // If this was a bundled workflow, it will now fall back to default + const isBundled = BUNDLED_WORKFLOW_KEYS.has(key); + + return NextResponse.json({ + success: true, + resetToDefault: isBundled, + }); + } catch (error) { + console.error('Error deleting workflow:', error); + return NextResponse.json( + { success: false, error: 'Failed to delete workflow' }, + { status: 500 } + ); + } +} + +// GET /api/workflows/[key] - Get a single workflow +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ key: string }> } +) { + try { + const { key } = await params; + const workflow = await WorkflowRegistry.getWorkflowByKey(key); + + if (!workflow) { + return NextResponse.json( + { success: false, error: 'Workflow not found' }, + { status: 404 } + ); + } + + // Include metadata about whether it's user-modified + const hasUserOverride = userWorkflowExists(key); + const isBundled = BUNDLED_WORKFLOW_KEYS.has(key); + + return NextResponse.json({ + success: true, + data: { + ...workflow, + isBundled, + hasUserOverride, + }, + }); + } catch (error) { + console.error('Error fetching workflow:', error); + return NextResponse.json( + { success: false, error: 'Failed to fetch workflow' }, + { status: 500 } + ); + } +} diff --git a/src/components/settings/WorkflowsViewer.tsx b/src/components/settings/WorkflowsViewer.tsx index 346b20a..2e7dc1c 100644 --- a/src/components/settings/WorkflowsViewer.tsx +++ b/src/components/settings/WorkflowsViewer.tsx @@ -12,89 +12,319 @@ interface WorkflowDefinition { requiresFocusedNode: boolean; primaryActor: 'oracle' | 'main'; expectedOutcome?: string; + isBundled?: boolean; + hasUserOverride?: boolean; +} + +interface EditingWorkflow { + key: string; + displayName: string; + description: string; + instructions: string; + enabled: boolean; + requiresFocusedNode: boolean; + isNew: boolean; } export default function WorkflowsViewer() { const [workflows, setWorkflows] = useState([]); - const [expandedId, setExpandedId] = useState(null); const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(null); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const loadWorkflows = async () => { + try { + const res = await fetch('/api/workflows'); + const result = await res.json(); + if (result.success) { + // Fetch additional metadata for each workflow + const enriched = await Promise.all( + result.data.map(async (w: WorkflowDefinition) => { + try { + const detailRes = await fetch(`/api/workflows/${w.key}`); + const detail = await detailRes.json(); + return detail.success ? detail.data : w; + } catch { + return w; + } + }) + ); + setWorkflows(enriched); + } + } catch (e) { + console.error('Failed to load workflows:', e); + } finally { + setLoading(false); + } + }; useEffect(() => { - const load = async () => { - try { - const res = await fetch('/api/workflows'); - const result = await res.json(); - if (result.success) setWorkflows(result.data); - } catch (e) { - console.error('Failed to load workflows:', e); - } finally { - setLoading(false); - } - }; - load(); + loadWorkflows(); }, []); + const handleEdit = (workflow: WorkflowDefinition) => { + setEditing({ + key: workflow.key, + displayName: workflow.displayName, + description: workflow.description, + instructions: workflow.instructions, + enabled: workflow.enabled, + requiresFocusedNode: workflow.requiresFocusedNode, + isNew: false, + }); + setError(null); + }; + + const handleNewWorkflow = () => { + setEditing({ + key: '', + displayName: '', + description: '', + instructions: '', + enabled: true, + requiresFocusedNode: true, + isNew: true, + }); + setError(null); + }; + + const handleCancel = () => { + setEditing(null); + setError(null); + }; + + const generateKey = (name: string): string => { + return name + .toLowerCase() + .trim() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, ''); + }; + + const handleSave = async () => { + if (!editing) return; + + const key = editing.isNew ? generateKey(editing.displayName) : editing.key; + + if (!key) { + setError('Please enter a workflow name'); + return; + } + + if (!editing.instructions.trim()) { + setError('Please enter instructions'); + return; + } + + setSaving(true); + setError(null); + + try { + const res = await fetch(`/api/workflows/${key}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + displayName: editing.displayName, + description: editing.description, + instructions: editing.instructions, + enabled: editing.enabled, + requiresFocusedNode: editing.requiresFocusedNode, + }), + }); + + const result = await res.json(); + + if (result.success) { + setEditing(null); + await loadWorkflows(); + } else { + setError(result.error || 'Failed to save workflow'); + } + } catch (e) { + setError('Failed to save workflow'); + console.error('Save error:', e); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (workflow: WorkflowDefinition) => { + const action = workflow.isBundled ? 'reset to default' : 'delete'; + if (!confirm(`Are you sure you want to ${action} "${workflow.displayName}"?`)) { + return; + } + + try { + const res = await fetch(`/api/workflows/${workflow.key}`, { + method: 'DELETE', + }); + + const result = await res.json(); + + if (result.success) { + await loadWorkflows(); + } else { + alert(result.error || 'Failed to delete workflow'); + } + } catch (e) { + alert('Failed to delete workflow'); + console.error('Delete error:', e); + } + }; + if (loading) { return
Loading...
; } + // Editing view + if (editing) { + return ( +
+
+ + {editing.isNew ? 'New Workflow' : `Edit: ${editing.displayName}`} + +
+ + +
+
+ + {error &&
{error}
} + +
+
+ + setEditing({ ...editing, displayName: e.target.value })} + placeholder="My Workflow" + style={inputStyle} + disabled={saving} + /> +
+ +
+ + setEditing({ ...editing, description: e.target.value })} + placeholder="Brief description of what this workflow does" + style={inputStyle} + disabled={saving} + /> + Keep this short — it tells the agent when to use this workflow +
+ +
+ +