343 lines
9.9 KiB
TypeScript
343 lines
9.9 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import { Workflow, WorkflowNode, WorkflowEdge, MCPServer } from '@/lib/workflow/types';
|
|
import {
|
|
saveWorkflow as saveWorkflowToStorage,
|
|
getWorkflows,
|
|
getWorkflow,
|
|
deleteWorkflow as deleteWorkflowFromStorage,
|
|
setCurrentWorkflow,
|
|
getCurrentWorkflowId,
|
|
saveMCPServer,
|
|
getMCPServers,
|
|
} from '@/lib/workflow/storage';
|
|
import { cleanupInvalidEdges } from '@/lib/workflow/edge-cleanup';
|
|
|
|
export function useWorkflow(workflowId?: string) {
|
|
const [workflow, setWorkflow] = useState<Workflow | null>(null);
|
|
const [workflows, setWorkflows] = useState<Workflow[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [convexId, setConvexId] = useState<string | null>(null); // Track Convex ID
|
|
const saveToConvexTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
// Load workflow from Redis via API
|
|
useEffect(() => {
|
|
const loadWorkflow = async () => {
|
|
setLoading(true);
|
|
|
|
if (workflowId) {
|
|
// Fetch workflow from API (Redis)
|
|
try {
|
|
const response = await fetch('/api/workflows');
|
|
const data = await response.json();
|
|
const workflows = data.workflows || [];
|
|
const loaded = workflows.find((w: any) => w.id === workflowId);
|
|
|
|
if (loaded) {
|
|
// Fetch full workflow details
|
|
const fullWorkflow = await fetch(`/api/workflows/${workflowId}`).then(r => r.json());
|
|
let workflowData = fullWorkflow.workflow || loaded;
|
|
|
|
// Clean up any invalid edges before setting the workflow
|
|
const cleaned = cleanupInvalidEdges(workflowData.nodes, workflowData.edges);
|
|
if (cleaned.removedCount > 0) {
|
|
console.log(`🧹 Cleaned ${cleaned.removedCount} invalid edges from loaded workflow`);
|
|
workflowData = {
|
|
...workflowData,
|
|
nodes: cleaned.nodes,
|
|
edges: cleaned.edges,
|
|
};
|
|
}
|
|
|
|
setWorkflow(workflowData);
|
|
// Store the Convex ID for future saves
|
|
setConvexId(workflowData._convexId || workflowData._id || null);
|
|
} else {
|
|
createNewWorkflow();
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load workflow from Convex:', error);
|
|
createNewWorkflow();
|
|
}
|
|
} else {
|
|
createNewWorkflow();
|
|
}
|
|
|
|
setLoading(false);
|
|
};
|
|
|
|
loadWorkflow();
|
|
}, [workflowId]);
|
|
|
|
// Load all workflows from API
|
|
const loadWorkflows = useCallback(async () => {
|
|
try {
|
|
const response = await fetch('/api/workflows');
|
|
const data = await response.json();
|
|
setWorkflows(data.workflows || []);
|
|
} catch (error) {
|
|
console.error('Failed to load workflows from API:', error);
|
|
setWorkflows([]);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadWorkflows();
|
|
}, [loadWorkflows]);
|
|
|
|
// Create new workflow
|
|
const createNewWorkflow = useCallback(() => {
|
|
const newWorkflow: Workflow = {
|
|
id: `workflow_${Date.now()}`,
|
|
name: 'New Workflow',
|
|
nodes: [
|
|
{
|
|
id: 'node_0',
|
|
type: 'start',
|
|
position: { x: 250, y: 100 },
|
|
data: { label: 'Start' },
|
|
},
|
|
{
|
|
id: 'node_1',
|
|
type: 'agent',
|
|
position: { x: 250, y: 250 },
|
|
data: {
|
|
label: 'Agent',
|
|
name: 'My agent',
|
|
instructions: 'You are a helpful assistant.',
|
|
model: 'gpt-4.1',
|
|
includeChatHistory: true,
|
|
tools: [],
|
|
outputFormat: 'Text',
|
|
},
|
|
},
|
|
],
|
|
edges: [
|
|
{
|
|
id: 'edge_0_1',
|
|
source: 'node_0',
|
|
target: 'node_1',
|
|
},
|
|
],
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
setWorkflow(newWorkflow);
|
|
// No longer save to localStorage
|
|
// Workflow will be saved via API when user makes changes
|
|
|
|
return newWorkflow;
|
|
}, []);
|
|
|
|
// Save workflow with debounce to prevent multiple rapid saves
|
|
// Use useCallback with minimal deps to prevent infinite loops
|
|
const saveWorkflow = useCallback(async (updates?: Partial<Workflow>) => {
|
|
// If no workflow exists yet, create a new one from updates
|
|
if (!workflow) {
|
|
if (!updates || !updates.nodes || !updates.edges) {
|
|
console.warn('⚠️ Cannot save workflow: no workflow state and incomplete updates');
|
|
return;
|
|
}
|
|
|
|
// Create a complete workflow from updates
|
|
const newWorkflow: Workflow = {
|
|
id: updates.id || `workflow_${Date.now()}`,
|
|
name: updates.name || 'New Workflow',
|
|
description: updates.description,
|
|
nodes: updates.nodes,
|
|
edges: updates.edges,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
setWorkflow(newWorkflow);
|
|
|
|
// Save immediately to Convex
|
|
try {
|
|
const response = await fetch('/api/workflows', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(newWorkflow),
|
|
});
|
|
const data = await response.json();
|
|
console.log('💾 New workflow saved to Convex:', data.success ? 'SUCCESS' : 'FAILED');
|
|
|
|
// Store the Convex ID from the response
|
|
if (data.success && data.workflowId) {
|
|
setConvexId(data.workflowId);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to save new workflow to Convex:', error);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const updated: Workflow = {
|
|
...workflow,
|
|
...updates,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
setWorkflow(updated);
|
|
|
|
// Clear any pending save timeout
|
|
if (saveToConvexTimeoutRef.current) {
|
|
clearTimeout(saveToConvexTimeoutRef.current);
|
|
}
|
|
|
|
// Debounce the save to Convex to prevent rapid saves
|
|
saveToConvexTimeoutRef.current = setTimeout(async () => {
|
|
try {
|
|
const response = await fetch('/api/workflows', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updated),
|
|
});
|
|
const data = await response.json();
|
|
console.log('💾 [AUTO-SAVE] Workflow synced to Convex:', data.success ? '✅ SUCCESS' : '❌ FAILED');
|
|
|
|
// Store the Convex ID from the response
|
|
if (data.success && data.workflowId) {
|
|
setConvexId(data.workflowId);
|
|
}
|
|
|
|
// Don't reload workflows on every save - only when explicitly needed
|
|
// This prevents unnecessary re-fetches and duplicate saves
|
|
} catch (error) {
|
|
console.error('❌ Failed to save workflow to Convex:', error);
|
|
}
|
|
}, 1000); // 1000ms debounce to batch rapid saves
|
|
}, [workflow, loadWorkflows]);
|
|
|
|
// Update nodes
|
|
const updateNodes = useCallback((nodes: WorkflowNode[]) => {
|
|
if (!workflow) {
|
|
console.warn('⚠️ updateNodes called but no workflow exists');
|
|
return;
|
|
}
|
|
|
|
console.log('📝 updateNodes called with', nodes.length, 'nodes');
|
|
saveWorkflow({ nodes });
|
|
}, [workflow, saveWorkflow]);
|
|
|
|
// Update edges
|
|
const updateEdges = useCallback((edges: WorkflowEdge[]) => {
|
|
if (!workflow) return;
|
|
|
|
saveWorkflow({ edges });
|
|
}, [workflow, saveWorkflow]);
|
|
|
|
// Update node data
|
|
const updateNodeData = useCallback((nodeId: string, data: any) => {
|
|
if (!workflow) return;
|
|
|
|
const nodes = workflow.nodes.map(node =>
|
|
node.id === nodeId
|
|
? { ...node, data: { ...node.data, ...data } }
|
|
: node
|
|
);
|
|
|
|
updateNodes(nodes);
|
|
}, [workflow, updateNodes]);
|
|
|
|
// Delete workflow
|
|
const deleteWorkflow = useCallback((id: string) => {
|
|
deleteWorkflowFromStorage(id);
|
|
loadWorkflows();
|
|
|
|
if (workflow?.id === id) {
|
|
createNewWorkflow();
|
|
}
|
|
}, [workflow, loadWorkflows, createNewWorkflow]);
|
|
|
|
// Save workflow immediately (non-debounced) - used before execution
|
|
const saveWorkflowImmediate = useCallback(async (updates?: Partial<Workflow>) => {
|
|
if (!workflow) {
|
|
console.warn('⚠️ Cannot save workflow immediately: no workflow state');
|
|
return;
|
|
}
|
|
|
|
const updated: Workflow = {
|
|
...workflow,
|
|
...updates,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
|
|
setWorkflow(updated);
|
|
|
|
// Cancel any pending debounced saves
|
|
if (saveToConvexTimeoutRef.current) {
|
|
clearTimeout(saveToConvexTimeoutRef.current);
|
|
saveToConvexTimeoutRef.current = null;
|
|
}
|
|
|
|
// Save immediately without debounce
|
|
try {
|
|
const response = await fetch('/api/workflows', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updated),
|
|
});
|
|
const data = await response.json();
|
|
console.log('💾 [IMMEDIATE SAVE] Workflow saved to Convex:', data.success ? '✅ SUCCESS' : '❌ FAILED');
|
|
|
|
if (data.success && data.workflowId) {
|
|
setConvexId(data.workflowId);
|
|
}
|
|
|
|
return data.success;
|
|
} catch (error) {
|
|
console.error('❌ Failed to save workflow immediately:', error);
|
|
return false;
|
|
}
|
|
}, [workflow]);
|
|
|
|
return {
|
|
workflow,
|
|
workflows,
|
|
loading,
|
|
convexId, // Expose Convex ID for templates and other features
|
|
saveWorkflow,
|
|
saveWorkflowImmediate, // Non-debounced save for before execution
|
|
updateNodes,
|
|
updateEdges,
|
|
updateNodeData,
|
|
deleteWorkflow,
|
|
createNewWorkflow,
|
|
loadWorkflows,
|
|
};
|
|
}
|
|
|
|
export function useMCPServers() {
|
|
const [servers, setServers] = useState<MCPServer[]>([]);
|
|
|
|
useEffect(() => {
|
|
loadServers();
|
|
}, []);
|
|
|
|
const loadServers = () => {
|
|
const loaded = getMCPServers();
|
|
setServers(loaded);
|
|
};
|
|
|
|
const addServer = useCallback((server: MCPServer) => {
|
|
saveMCPServer(server);
|
|
loadServers();
|
|
}, []);
|
|
|
|
const updateServer = useCallback((id: string, updates: Partial<MCPServer>) => {
|
|
const existing = servers.find(s => s.id === id);
|
|
if (existing) {
|
|
saveMCPServer({ ...existing, ...updates });
|
|
loadServers();
|
|
}
|
|
}, [servers]);
|
|
|
|
return {
|
|
servers,
|
|
addServer,
|
|
updateServer,
|
|
loadServers,
|
|
};
|
|
}
|