Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
ImportWorkflowModal.tsx21.7 kB
import { useState, useRef, useId } from 'react'; import { X, Upload, FileJson, AlertCircle, CheckCircle, Copy, FileUp } from 'lucide-react'; import { usePlanStore } from '../store/planStore'; import { Task, AgentTask, TransformerTask, ParallelGroup } from '../types/task'; interface ImportWorkflowModalProps { isOpen: boolean; onClose: () => void; } type ImportTab = 'paste' | 'upload'; interface WorkflowJSON { name?: string; description?: string; tasks: any[]; parallelGroups?: any[]; lambdas?: any[]; agentTemplates?: any[]; projectPlan?: any; } interface ValidationResult { valid: boolean; errors: string[]; warnings: string[]; taskCount: number; lambdaCount: number; parallelGroupCount: number; } export function ImportWorkflowModal({ isOpen, onClose }: ImportWorkflowModalProps) { const [activeTab, setActiveTab] = useState<ImportTab>('paste'); const [jsonInput, setJsonInput] = useState(''); const [fileName, setFileName] = useState<string | null>(null); const [validation, setValidation] = useState<ValidationResult | null>(null); const [isImporting, setIsImporting] = useState(false); const fileInputRef = useRef<HTMLInputElement>(null); const jsonInputId = useId(); const { setTasks, setParallelGroups, addLambda, addAgentTemplate, clearTasks } = usePlanStore(); if (!isOpen) return null; const validateWorkflow = (json: string): ValidationResult => { const errors: string[] = []; const warnings: string[] = []; let taskCount = 0; let lambdaCount = 0; let parallelGroupCount = 0; try { const data = JSON.parse(json) as WorkflowJSON; // Check for tasks array if (!data.tasks || !Array.isArray(data.tasks)) { errors.push('Missing or invalid "tasks" array'); return { valid: false, errors, warnings, taskCount, lambdaCount, parallelGroupCount }; } taskCount = data.tasks.length; if (taskCount === 0) { errors.push('Workflow must contain at least one task'); } // Validate each task const taskIds = new Set<string>(); data.tasks.forEach((task, idx) => { if (!task.id) { errors.push(`Task at index ${idx} missing "id"`); } else if (taskIds.has(task.id)) { errors.push(`Duplicate task ID: "${task.id}"`); } else { taskIds.add(task.id); } if (!task.title) { warnings.push(`Task "${task.id || idx}" missing "title"`); } if (!task.taskType) { warnings.push(`Task "${task.id || idx}" missing "taskType", will default to "agent"`); } // Check dependencies reference valid tasks if (task.dependencies && Array.isArray(task.dependencies)) { task.dependencies.forEach((dep: string) => { // Check if dependency exists (it might be defined later in array) const depExists = data.tasks.some((t: any) => t.id === dep); if (!depExists) { errors.push(`Task "${task.id}" references unknown dependency: "${dep}"`); } }); } // Agent-specific validation if (task.taskType === 'agent' || !task.taskType) { if (!task.agentRoleDescription) { warnings.push(`Agent task "${task.id}" missing "agentRoleDescription"`); } } // Transformer-specific validation if (task.taskType === 'transformer') { if (!task.lambdaScript && !task.lambdaId) { warnings.push(`Transformer "${task.id}" has no Lambda assigned (will be pass-through)`); } } }); // Count lambdas (from explicit array + inline scripts on transformers) if (data.lambdas && Array.isArray(data.lambdas)) { lambdaCount = data.lambdas.length; } // Also count inline lambdas from transformer tasks const inlineLambdaCount = data.tasks.filter( (t: any) => t.taskType === 'transformer' && t.lambdaScript ).length; lambdaCount += inlineLambdaCount; // Count parallel groups if (data.parallelGroups && Array.isArray(data.parallelGroups)) { parallelGroupCount = data.parallelGroups.length; } return { valid: errors.length === 0, errors, warnings, taskCount, lambdaCount, parallelGroupCount, }; } catch (e) { return { valid: false, errors: [`Invalid JSON: ${e instanceof Error ? e.message : 'Parse error'}`], warnings: [], taskCount: 0, lambdaCount: 0, parallelGroupCount: 0, }; } }; const handleValidate = () => { const result = validateWorkflow(jsonInput); setValidation(result); }; const handleImport = async () => { if (!validation?.valid) return; setIsImporting(true); try { const data = JSON.parse(jsonInput) as WorkflowJSON; // Clear existing tasks clearTasks(); // Import lambdas - merge explicit lambdas array with inline scripts from tasks // Build a map of lambdaName -> script from transformer tasks const inlineScripts = new Map<string, { script: string; language: string; title: string; description: string }>(); data.tasks.forEach(task => { if (task.taskType === 'transformer' && task.lambdaScript) { const key = task.lambdaName || task.id; inlineScripts.set(key, { script: task.lambdaScript, language: task.lambdaLanguage || 'javascript', title: task.title || task.lambdaName || task.id, description: task.description || `Lambda for ${task.title || task.id}`, }); } }); // Track which lambdas we've added (to avoid duplicates) const addedLambdaIds = new Set<string>(); // 1. From explicit lambdas array - merge with inline scripts if available // Skip lambdas that have no script and no matching inline script (stale entries) if (data.lambdas && Array.isArray(data.lambdas)) { data.lambdas.forEach(lambda => { const inline = inlineScripts.get(lambda.id); const script = lambda.script || inline?.script || ''; // Skip if no script available (stale lambda entry with no matching task) if (!script) { console.log(`Skipping lambda "${lambda.id}" - no script found`); return; } addLambda({ id: lambda.id, name: lambda.name, description: lambda.description || inline?.description || '', language: lambda.language || inline?.language || 'javascript', script, version: lambda.version || '1.0', created: lambda.created || new Date().toISOString(), }); addedLambdaIds.add(lambda.id); }); } // 2. Add any inline lambdas not already in the lambdas array inlineScripts.forEach((inline, key) => { if (!addedLambdaIds.has(key)) { addLambda({ id: key, name: inline.title, description: inline.description, language: inline.language as 'javascript' | 'typescript' | 'python', script: inline.script, version: '1.0', created: new Date().toISOString(), }); } }); // Import agent templates if provided if (data.agentTemplates && Array.isArray(data.agentTemplates)) { data.agentTemplates.forEach(template => { addAgentTemplate(template); }); } // Import parallel groups if (data.parallelGroups && Array.isArray(data.parallelGroups)) { const groups: ParallelGroup[] = data.parallelGroups.map(g => ({ id: g.id, name: g.name || `Group ${g.id}`, taskIds: g.taskIds || [], color: g.color, })); setParallelGroups(groups); } // Import tasks const tasks: Task[] = data.tasks.map(t => { const baseTask = { id: t.id, title: t.title || 'Untitled Task', description: t.description || '', dependencies: t.dependencies || [], parallelGroup: t.parallelGroup ?? null, position: t.position, executionStatus: undefined, }; if (t.taskType === 'transformer') { return { ...baseTask, taskType: 'transformer' as const, lambdaId: t.lambdaId, lambdaScript: t.lambdaScript, lambdaLanguage: t.lambdaLanguage, lambdaName: t.lambdaName, inputMapping: t.inputMapping, outputMapping: t.outputMapping, } as TransformerTask; } else { return { ...baseTask, taskType: 'agent' as const, agentRoleDescription: t.agentRoleDescription || 'General assistant', recommendedModel: t.recommendedModel || 'gpt-4.1', prompt: t.prompt || '', estimatedDuration: t.estimatedDuration || '5 min', estimatedToolCalls: t.estimatedToolCalls || 0, successCriteria: t.successCriteria || [], workerPreambleId: t.workerPreambleId, qcPreambleId: t.qcPreambleId, qcRole: t.qcRole, verificationCriteria: t.verificationCriteria || [], maxRetries: t.maxRetries || 2, } as AgentTask; } }); setTasks(tasks); // Success - close modal handleClose(); } catch (error) { console.error('Import failed:', error); setValidation({ valid: false, errors: [`Import failed: ${error instanceof Error ? error.message : 'Unknown error'}`], warnings: [], taskCount: 0, lambdaCount: 0, parallelGroupCount: 0, }); } finally { setIsImporting(false); } }; const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; setFileName(file.name); const reader = new FileReader(); reader.onload = (event) => { const content = event.target?.result as string; setJsonInput(content); // Auto-validate after loading const result = validateWorkflow(content); setValidation(result); }; reader.readAsText(file); }; const handlePaste = async () => { try { const text = await navigator.clipboard.readText(); setJsonInput(text); // Auto-validate after paste const result = validateWorkflow(text); setValidation(result); } catch (err) { console.error('Failed to read clipboard:', err); } }; const handleClose = () => { setJsonInput(''); setFileName(null); setValidation(null); setActiveTab('paste'); onClose(); }; const loadExample = () => { const example = { name: "Example Workflow", tasks: [ { id: "task-1", taskType: "agent", title: "Research Task", agentRoleDescription: "Research assistant", prompt: "Research the topic and provide key findings", dependencies: [], parallelGroup: 1 }, { id: "transformer-1", taskType: "transformer", title: "Summarize Results", dependencies: ["task-1"], lambdaName: "Summarizer", lambdaLanguage: "typescript", lambdaScript: "function transform(input: any): string {\n return input.tasks.map((t: any) => t.workerOutput).join('\\n');\n}" } ], parallelGroups: [ { id: 1, name: "Research Phase" } ] }; setJsonInput(JSON.stringify(example, null, 2)); }; return ( <div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 backdrop-blur-sm"> <div className="bg-norse-stone border-2 border-norse-rune rounded-xl w-full max-w-3xl max-h-[90vh] flex flex-col shadow-2xl"> {/* Header */} <div className="flex items-center justify-between p-4 border-b border-norse-rune"> <div className="flex items-center space-x-3"> <FileJson className="w-6 h-6 text-valhalla-gold" /> <h2 className="text-xl font-bold text-valhalla-gold">Import Workflow</h2> </div> <button type="button" onClick={handleClose} className="p-2 text-gray-400 hover:text-white hover:bg-norse-rune rounded-lg transition-colors" > <X className="w-5 h-5" /> </button> </div> {/* Tabs */} <div className="flex border-b border-norse-rune"> <button type="button" onClick={() => setActiveTab('paste')} className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${ activeTab === 'paste' ? 'text-valhalla-gold border-b-2 border-valhalla-gold bg-norse-shadow/30' : 'text-gray-400 hover:text-gray-200 hover:bg-norse-rune/30' }`} > <div className="flex items-center justify-center space-x-2"> <Copy className="w-4 h-4" /> <span>Paste JSON</span> </div> </button> <button type="button" onClick={() => setActiveTab('upload')} className={`flex-1 px-4 py-3 text-sm font-medium transition-colors ${ activeTab === 'upload' ? 'text-valhalla-gold border-b-2 border-valhalla-gold bg-norse-shadow/30' : 'text-gray-400 hover:text-gray-200 hover:bg-norse-rune/30' }`} > <div className="flex items-center justify-center space-x-2"> <FileUp className="w-4 h-4" /> <span>Upload File</span> </div> </button> </div> {/* Content */} <div className="flex-1 overflow-hidden p-4 flex flex-col min-h-0"> {activeTab === 'paste' ? ( <div className="flex flex-col h-full space-y-3"> <div className="flex items-center justify-between"> <label htmlFor={jsonInputId} className="text-sm font-medium text-gray-300"> Workflow JSON </label> <div className="flex items-center space-x-2"> <button type="button" onClick={handlePaste} className="text-xs text-frost-ice hover:text-frost-ice/80 flex items-center space-x-1" > <Copy className="w-3 h-3" /> <span>Paste from clipboard</span> </button> <button type="button" onClick={loadExample} className="text-xs text-violet-400 hover:text-violet-300" > Load example </button> </div> </div> <textarea id={jsonInputId} value={jsonInput} onChange={(e) => { setJsonInput(e.target.value); setValidation(null); }} placeholder='{\n "tasks": [\n {\n "id": "task-1",\n "taskType": "agent",\n "title": "My Task",\n ...\n }\n ]\n}' className="flex-1 min-h-[300px] px-4 py-3 bg-norse-night border-2 border-norse-rune text-gray-100 placeholder-gray-600 rounded-lg focus:ring-2 focus:ring-valhalla-gold focus:border-valhalla-gold font-mono text-sm resize-none" spellCheck={false} /> </div> ) : ( <div className="flex flex-col h-full space-y-4"> <button type="button" onClick={() => fileInputRef.current?.click()} className="flex-1 min-h-[200px] border-2 border-dashed border-norse-rune rounded-lg flex flex-col items-center justify-center cursor-pointer hover:border-valhalla-gold hover:bg-norse-shadow/30 transition-colors" > <Upload className="w-12 h-12 text-gray-500 mb-4" /> <p className="text-gray-300 font-medium"> {fileName ? fileName : 'Click to upload or drag & drop'} </p> <p className="text-sm text-gray-500 mt-1"> JSON files only (.json) </p> </button> <input ref={fileInputRef} type="file" accept=".json,application/json" onChange={handleFileUpload} className="hidden" /> {fileName && jsonInput && ( <div className="bg-norse-shadow/50 rounded-lg p-3"> <div className="flex items-center space-x-2 text-sm text-gray-300"> <FileJson className="w-4 h-4 text-valhalla-gold" /> <span>{fileName}</span> <span className="text-gray-500">({(jsonInput.length / 1024).toFixed(1)} KB)</span> </div> </div> )} </div> )} {/* Validation Results */} {validation && ( <div className={`mt-4 p-3 rounded-lg border ${ validation.valid ? 'bg-green-900/20 border-green-700' : 'bg-red-900/20 border-red-700' }`}> <div className="flex items-start space-x-2"> {validation.valid ? ( <CheckCircle className="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5" /> ) : ( <AlertCircle className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" /> )} <div className="flex-1 min-w-0"> <p className={`font-medium ${validation.valid ? 'text-green-400' : 'text-red-400'}`}> {validation.valid ? 'Validation Passed' : 'Validation Failed'} </p> {validation.valid && ( <p className="text-sm text-gray-400 mt-1"> {validation.taskCount} tasks, {validation.lambdaCount} lambdas, {validation.parallelGroupCount} groups </p> )} {validation.errors.length > 0 && ( <ul className="mt-2 space-y-1"> {validation.errors.map((error) => ( <li key={`err-${error}`} className="text-sm text-red-300">• {error}</li> ))} </ul> )} {validation.warnings.length > 0 && ( <div className="mt-2"> <p className="text-xs text-yellow-500 font-medium">Warnings:</p> <ul className="mt-1 space-y-0.5"> {validation.warnings.slice(0, 5).map((warning) => ( <li key={`warn-${warning}`} className="text-xs text-yellow-400/80">• {warning}</li> ))} {validation.warnings.length > 5 && ( <li className="text-xs text-yellow-400/60"> ... and {validation.warnings.length - 5} more </li> )} </ul> </div> )} </div> </div> </div> )} </div> {/* Footer */} <div className="flex items-center justify-between p-4 border-t border-norse-rune bg-norse-shadow/30"> <p className="text-xs text-gray-500"> Import will replace current workflow </p> <div className="flex items-center space-x-3"> <button type="button" onClick={handleClose} className="px-4 py-2 bg-norse-rune text-gray-200 font-medium rounded-lg hover:bg-norse-mist transition-colors" > Cancel </button> {!validation && ( <button type="button" onClick={handleValidate} disabled={!jsonInput.trim()} className="px-4 py-2 bg-frost-ice text-norse-night font-semibold rounded-lg hover:bg-frost-ice/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" > Validate </button> )} {validation?.valid && ( <button type="button" onClick={handleImport} disabled={isImporting} className="px-4 py-2 bg-valhalla-gold text-norse-night font-semibold rounded-lg hover:bg-valhalla-gold/90 disabled:opacity-50 disabled:cursor-not-allowed flex items-center space-x-2 transition-colors" > {isImporting ? ( <> <div className="animate-spin h-4 w-4 border-2 border-norse-night border-t-transparent rounded-full" /> <span>Importing...</span> </> ) : ( <> <Upload className="w-4 h-4" /> <span>Import Workflow</span> </> )} </button> )} </div> </div> </div> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server