// Workflows Events - Handles events specific to workflows.html class WorkflowManager { constructor() { this.workflowState = { agents: [], // New hierarchical structure: [{agent: {...}, subItems: []}] steps: [], // Legacy support properties: { name: '', description: '', tags: [], prompt: '' }, components: { agents: [], commands: [], mcps: [] } }; } async init() { try { await this.loadComponents(); this.setupEventListeners(); this.initializeDragAndDrop(); this.renderComponentsList(); } catch (error) { console.error('Error initializing Workflow Builder:', error); this.showError('Failed to load components. Please refresh the page.'); } } async loadComponents() { this.workflowState.components = await window.dataLoader.loadComponents(); } setupEventListeners() { // Search functionality const searchInput = document.getElementById('componentSearch'); if (searchInput) { searchInput.addEventListener('input', (e) => this.handleComponentSearch(e)); } // Canvas actions const previewBtn = document.getElementById('previewWorkflow'); if (previewBtn) { previewBtn.addEventListener('click', () => this.previewWorkflow()); } const clearBtn = document.getElementById('clearCanvas'); if (clearBtn) { clearBtn.addEventListener('click', () => this.clearWorkflow()); } const generateBtn = document.getElementById('generateWorkflow'); if (generateBtn) { generateBtn.addEventListener('click', () => this.openPropertiesModal()); } // Properties modal const closePropertiesBtn = document.getElementById('closePropertiesModal'); if (closePropertiesBtn) { closePropertiesBtn.addEventListener('click', () => this.closePropertiesModal()); } const savePropertiesBtn = document.getElementById('saveWorkflowProperties'); if (savePropertiesBtn) { savePropertiesBtn.addEventListener('click', () => this.saveAndGenerateWorkflow()); } // Category accordion toggle document.querySelectorAll('.category-card-header').forEach(header => { header.addEventListener('click', () => { header.parentElement.classList.toggle('expanded'); }); }); // Sub-folders toggle document.addEventListener('click', (event) => { const header = event.target.closest('.tree-node-header'); if (header) { const children = header.nextElementSibling; if (children && children.classList.contains('tree-children')) { header.classList.toggle('expanded'); children.classList.toggle('expanded'); } } }); } renderComponentsList() { ['agents', 'commands', 'mcps'].forEach(type => { this.renderComponentsListByType(type, this.workflowState.components[type]); }); } renderComponentsListByType(type, components) { const container = document.getElementById(`${type}-tree`); const countElement = document.getElementById(`${type}-count`); if (!container || !countElement) return; countElement.textContent = components.length; container.innerHTML = ''; const groupedComponents = components.reduce((acc, component) => { const category = component.category === 'root' ? 'general' : component.category; if (!acc[category]) acc[category] = []; acc[category].push(component); return acc; }, {}); Object.entries(groupedComponents).forEach(([category, categoryComponents]) => { const folderElement = this.createTreeFolder(category, categoryComponents, type); container.appendChild(folderElement); }); } createTreeFolder(category, components, type) { const folderElement = document.createElement('div'); folderElement.className = 'tree-node'; const folderId = `${type}-${category.replace(/[^a-z0-9]/gi, '_').toLowerCase()}`; folderElement.innerHTML = `
📁 ${category} ${components.length} â–ļ
`; const childrenContainer = folderElement.querySelector('.tree-children'); components.forEach(component => { const fileElement = this.createTreeFile(component); childrenContainer.appendChild(fileElement); }); return folderElement; } createTreeFile(component) { const element = document.createElement('div'); element.className = 'tree-file'; element.draggable = true; element.dataset.componentType = component.type; element.dataset.componentName = component.name; element.dataset.componentPath = component.path; element.dataset.componentCategory = component.category; const fileIcon = this.getFileIcon(component.type); element.innerHTML = `
${fileIcon} ${component.name}
`; return element; } getFileIcon(type) { const icons = { 'agent': '📄', 'command': '⚡', 'mcp': 'âš™ī¸' }; return icons[type] || '📄'; } initializeDragAndDrop() { const workflowSteps = document.getElementById('workflowSteps'); if (!workflowSteps) return; document.addEventListener('dragstart', (event) => { if (event.target.closest('.tree-file')) { this.handleDragStart(event); } }); // Main drop zone workflowSteps.addEventListener('dragover', this.handleDragOver); workflowSteps.addEventListener('drop', (e) => this.handleDrop(e)); // Enhanced drag enter/leave for better highlighting workflowSteps.addEventListener('dragenter', (e) => { e.preventDefault(); const dropZone = e.target.closest('.drop-zone'); const subDropZone = e.target.closest('.sub-item-drop-zone'); const addItemBtn = e.target.closest('.add-agent-btn'); if (dropZone && !subDropZone) { dropZone.classList.add('drag-over'); } else if (subDropZone) { subDropZone.classList.add('drag-over'); } else if (addItemBtn) { addItemBtn.classList.add('drag-over'); } }); workflowSteps.addEventListener('dragleave', (e) => { const dropZone = e.target.closest('.drop-zone'); const subDropZone = e.target.closest('.sub-item-drop-zone'); const addItemBtn = e.target.closest('.add-agent-btn'); // Only remove if we're actually leaving the zone if (dropZone && !dropZone.contains(e.relatedTarget)) { dropZone.classList.remove('drag-over'); } if (subDropZone && !subDropZone.contains(e.relatedTarget)) { subDropZone.classList.remove('drag-over'); } if (addItemBtn && !addItemBtn.contains(e.relatedTarget)) { addItemBtn.classList.remove('drag-over'); } }); // Clean up drag-over class when drag ends document.addEventListener('dragend', () => { document.querySelectorAll('.drag-over').forEach(el => { el.classList.remove('drag-over'); }); }); } handleDragStart(event) { const treeFile = event.target.closest('.tree-file'); const componentData = { ...treeFile.dataset }; event.dataTransfer.setData('application/json', JSON.stringify(componentData)); event.dataTransfer.effectAllowed = 'copy'; } handleDragOver(event) { event.preventDefault(); event.dataTransfer.dropEffect = 'copy'; } handleDrop(event) { event.preventDefault(); // Don't handle drops on sub-item drop zones - those are handled by dropOnAgent if (event.target.closest('.sub-item-drop-zone')) { return; } event.target.closest('.drop-zone')?.classList.remove('drag-over'); try { const componentData = JSON.parse(event.dataTransfer.getData('application/json')); this.addWorkflowStep(componentData); } catch (error) { console.error('Error handling drop:', error); } } addWorkflowStep(componentData, targetAgentId = null) { // Ensure agents array is initialized if (!this.workflowState.agents) { this.workflowState.agents = []; } const step = { id: `step_${Date.now()}`, type: componentData.componentType, name: componentData.componentName, path: componentData.componentPath, category: componentData.componentCategory, description: `Execute ${componentData.componentName}`, icon: this.getComponentIcon(componentData.componentType) }; if (step.type === 'agent') { // Add as new agent this.workflowState.agents.push({ agent: step, subItems: [], isMainLevel: true }); } else if (targetAgentId && (step.type === 'command' || step.type === 'mcp')) { // Add as sub-item to specific agent - DON'T add to main flow const agentIndex = this.workflowState.agents.findIndex(a => a.agent.id === targetAgentId); if (agentIndex !== -1) { this.workflowState.agents[agentIndex].subItems.push(step); // Don't add to steps array - it's a sub-item this.renderWorkflowSteps(); this.updateWorkflowStats(); return; } } else if (step.type === 'command' || step.type === 'mcp') { // Add command/MCP as main level item (no sub-items) this.workflowState.agents.push({ agent: step, subItems: null, // null indicates no sub-items allowed isMainLevel: true }); } // Maintain legacy steps array for backwards compatibility (only for main level items) this.workflowState.steps.push(step); this.renderWorkflowSteps(); this.updateWorkflowStats(); } getComponentIcon(type) { const icons = { 'agent': '🤖', 'command': '⚡', 'mcp': '🔌' }; return icons[type] || 'đŸ“Ļ'; } renderWorkflowSteps() { const container = document.getElementById('workflowSteps'); if (!container) return; const dropZone = container.querySelector('.drop-zone'); const existingAgents = container.querySelectorAll('.workflow-agent'); const existingSteps = container.querySelectorAll('.workflow-step'); const existingIndicators = container.querySelectorAll('.add-agent-indicator'); existingAgents.forEach(agent => agent.remove()); existingSteps.forEach(step => step.remove()); existingIndicators.forEach(indicator => indicator.remove()); const agents = this.workflowState.agents || []; if (agents.length > 0) { if (dropZone) dropZone.style.display = 'none'; agents.forEach((agentData, index) => { const mainStepNumber = index + 1; const agentElement = document.createElement('div'); agentElement.className = 'workflow-agent'; agentElement.dataset.agentId = agentData.agent.id; agentElement.dataset.stepNumber = mainStepNumber; // Check if this is a command/MCP in main flow (no sub-items) if (agentData.subItems === null) { // Simple command/MCP item (no sub-items allowed) agentElement.innerHTML = `
${mainStepNumber}
${agentData.agent.icon}
${agentData.agent.name}
${agentData.agent.type}
`; } else { // Agent with potential sub-items const subItemsHtml = agentData.subItems.map((subItem, subIndex) => `
${mainStepNumber}.${subIndex + 1}
${subItem.icon}
${subItem.name}
${subItem.type}
`).join(''); agentElement.innerHTML = `
${mainStepNumber}
${agentData.agent.icon}
${agentData.agent.name}
${agentData.agent.type}
${subItemsHtml}
Drop commands or MCPs here
`; } container.appendChild(agentElement); }); // Add one "add item" indicator at the end of all agents const addAgentIndicator = document.createElement('div'); addAgentIndicator.className = 'add-agent-indicator'; addAgentIndicator.innerHTML = `
Add Next Item
`; container.appendChild(addAgentIndicator); } else { if (dropZone) dropZone.style.display = 'flex'; } } updateWorkflowStats() { const stats = { agents: 0, commands: 0, mcps: 0 }; let totalStepsCount = 0; // Count from hierarchical structure const agents = this.workflowState.agents || []; agents.forEach(agentData => { const agentType = agentData.agent.type; if (agentType === 'agent') { stats.agents++; } else if (agentType === 'command') { stats.commands++; } else if (agentType === 'mcp') { stats.mcps++; } totalStepsCount++; // Count main level item // Count sub-items (commands/MCPs inside agents) if (agentData.subItems && agentData.subItems.length > 0) { agentData.subItems.forEach(subItem => { totalStepsCount++; // Count each sub-item for total // Count sub-items in their respective categories if (subItem.type === 'command') { stats.commands++; } else if (subItem.type === 'mcp') { stats.mcps++; } }); } }); const agentCount = document.getElementById('agentCount'); const commandCount = document.getElementById('commandCount'); const mcpCount = document.getElementById('mcpCount'); const totalSteps = document.getElementById('totalSteps'); if (agentCount) agentCount.textContent = stats.agents; if (commandCount) commandCount.textContent = stats.commands; if (mcpCount) mcpCount.textContent = stats.mcps; if (totalSteps) totalSteps.textContent = totalStepsCount; } handleComponentSearch(event) { const searchTerm = event.target.value.toLowerCase(); document.querySelectorAll('.tree-file').forEach(file => { const match = file.dataset.componentName.toLowerCase().includes(searchTerm); file.style.display = match ? '' : 'none'; }); } clearWorkflow() { if (confirm('Are you sure you want to clear the workflow?')) { this.workflowState.steps = []; this.workflowState.agents = []; this.renderWorkflowSteps(); this.updateWorkflowStats(); } } openPropertiesModal() { const agents = this.workflowState.agents || []; if (agents.length === 0) { this.showError('Add at least one agent to the workflow.'); return; } const modal = document.getElementById('propertiesModal'); if (modal) modal.style.display = 'block'; } closePropertiesModal() { const modal = document.getElementById('propertiesModal'); if (modal) modal.style.display = 'none'; } saveAndGenerateWorkflow() { const nameInput = document.getElementById('workflowName'); const descInput = document.getElementById('workflowDescription'); const tagsInput = document.getElementById('workflowTags'); const promptInput = document.getElementById('workflowPrompt'); if (nameInput) this.workflowState.properties.name = nameInput.value; if (descInput) this.workflowState.properties.description = descInput.value; if (tagsInput) { this.workflowState.properties.tags = tagsInput.value.split(',').map(t => t.trim()); } if (promptInput) this.workflowState.properties.prompt = promptInput.value.trim(); if (!this.workflowState.properties.name) { this.showError('Workflow name is required.'); return; } this.closePropertiesModal(); this.generateWorkflow(); } async generateWorkflow() { try { const workflowCommand = await this.generateWorkflowHash(); const yamlContent = this.generateWorkflowYAML(); this.showGenerateModal(workflowCommand, yamlContent); } catch (error) { console.error('Error generating workflow:', error); this.showError('Failed to generate workflow.'); } } async generateWorkflowHash() { // NEW APPROACH: Generate component-based command instead of embedded hash const agents = this.workflowState.steps.filter(step => step.type === 'agent').map(step => step.path.replace(/\.md$/, '')); const commands = this.workflowState.steps.filter(step => step.type === 'command').map(step => step.path.replace(/\.md$/, '')); const mcps = this.workflowState.steps.filter(step => step.type === 'mcp').map(step => step.path.replace(/\.json$/, '')); // Generate YAML and encode it const yamlContent = this.generateWorkflowYAML(); const encodedYaml = btoa(unescape(encodeURIComponent(yamlContent))); // Build command components for Method 1 (full command with embedded YAML) const commandParts = []; if (agents.length > 0) { commandParts.push(`--agent ${agents.join(',')}`); } if (commands.length > 0) { commandParts.push(`--command ${commands.join(',')}`); } if (mcps.length > 0) { commandParts.push(`--mcp ${mcps.join(',')}`); } // Add prompt parameter if provided if (this.workflowState.properties.prompt) { commandParts.push(`--prompt "${this.workflowState.properties.prompt}"`); } // Method 1: Full command with embedded YAML const fullCommand = `npx claude-code-templates@latest ${commandParts.join(' ')} --workflow ${encodedYaml}`; // Method 2: Components only command (without embedded YAML) // Remove prompt from components-only command since YAML isn't included const componentsParts = commandParts.filter(part => !part.startsWith('--prompt')); const componentsOnlyCommand = componentsParts.length > 0 ? `npx claude-code-templates@latest ${componentsParts.join(' ')}` : '# No components to install'; // Create a short hash for reference const shortHash = CryptoJS.SHA256(fullCommand).toString(CryptoJS.enc.Hex).substring(0, 8); // Store locally for reference localStorage.setItem(`workflow_${shortHash}`, JSON.stringify({ fullCommand: fullCommand, componentsCommand: componentsOnlyCommand, yaml: yamlContent, metadata: this.workflowState.properties, timestamp: new Date().toISOString() })); return { fullCommand: fullCommand, componentsCommand: componentsOnlyCommand, yaml: yamlContent }; } // Simple and reliable string compression with Unicode support compressString(str) { try { // Simple approach: remove redundant whitespace and use efficient encoding let compressed = str // Remove unnecessary whitespace .replace(/\s+/g, ' ') // Remove whitespace around common JSON characters .replace(/\s*([{}[\]:,])\s*/g, '$1') // Remove quotes around simple keys (where safe) .replace(/"([a-zA-Z_][a-zA-Z0-9_]*)"\s*:/g, '$1:'); // Use proper Unicode-safe Base64 encoding return btoa(unescape(encodeURIComponent(compressed))); } catch (error) { console.warn('Compression failed, using fallback:', error); // Last resort: just encode the original string try { return btoa(unescape(encodeURIComponent(str))); } catch (finalError) { // If even that fails, return the original string (will cause error later) console.error('All encoding methods failed:', finalError); return str; } } } // Decompress string (for CLI) decompressString(compressed) { try { // Since we're using simple Base64 encoding, just decode it return decodeURIComponent(escape(atob(compressed))); } catch (error) { throw new Error('Failed to decompress data: ' + error.message); } } async getComponentsData() { const componentsData = {}; // Group components by type and get their full content for (const step of this.workflowState.steps) { if (!componentsData[step.type]) { componentsData[step.type] = []; } // Find the component in the loaded data const componentsList = this.workflowState.components[step.type + 's'] || []; const fullComponent = componentsList.find(c => c.name === step.name); if (fullComponent) { componentsData[step.type].push({ name: step.name, path: step.path, category: step.category, content: fullComponent.content, type: step.type }); } } return componentsData; } generateWorkflowYAML() { // Ensure agents array exists and has valid structure const agents = this.workflowState.agents || []; const totalSteps = agents.reduce((total, agent) => total + 1 + (agent.subItems ? agent.subItems.length : 0), 0); const promptComment = this.workflowState.properties.prompt ? `# Initial Prompt: ${this.workflowState.properties.prompt}\n` : ''; return `# Workflow: ${this.workflowState.properties.name} # Description: ${this.workflowState.properties.description} ${promptComment}# Generated: ${new Date().toISOString()} # Agents: ${agents.length} total # Components: ${totalSteps} total name: "${this.workflowState.properties.name}" description: "${this.workflowState.properties.description}" tags: [${this.workflowState.properties.tags.map(tag => `"${tag}"`).join(', ')}] version: "2.0.0"${this.workflowState.properties.prompt ? '\nprompt: "' + this.workflowState.properties.prompt + '"' : ''} # Hierarchical workflow structure: Agents → Commands/MCPs agents: ${agents.map((agentData, i) => ` - agent: ${i + 1} type: ${agentData.agent.type} name: "${agentData.agent.name}" path: "${agentData.agent.type === 'mcp' ? `.mcp.json#${agentData.agent.name}` : `.claude/${agentData.agent.type}s/${agentData.agent.path}`}" category: "${agentData.agent.category}" description: "${agentData.agent.description}" tasks:${(agentData.subItems && agentData.subItems.length > 0) ? agentData.subItems.map((subItem, j) => ` - task: ${j + 1} type: ${subItem.type} name: "${subItem.name}" path: "${subItem.type === 'mcp' ? `.mcp.json#${subItem.name}` : `.claude/${subItem.type}s/${subItem.path}`}" category: "${subItem.category}" description: "${subItem.description}"`).join('') : ` - task: 1 type: "default" name: "No specific tasks assigned" description: "This agent has no specific commands or MCPs assigned"`}`).join('\n')} execution: mode: "hierarchical" on_error: "stop" sequence: "agent-by-agent" components: agents: [${agents.filter(a => a.agent.type === 'agent').map(a => `".claude/agents/${a.agent.path}"`).join(', ')}] commands: [${[...agents.filter(a => a.agent.type === 'command').map(a => `".claude/commands/${a.agent.path}"`), ...agents.flatMap(a => (a.subItems || []).filter(s => s.type === 'command').map(s => `".claude/commands/${s.path}`))].join(', ')}] mcps: [${[...agents.filter(a => a.agent.type === 'mcp').map(a => `".mcp.json#${a.agent.name}"`), ...agents.flatMap(a => (a.subItems || []).filter(s => s.type === 'mcp').map(s => `".mcp.json#${s.name}`))].join(', ')}] # Instructions for Claude Code: # This hierarchical workflow contains ${agents.length} agents with their assigned tasks. # Each agent should be loaded first, then its associated commands and MCPs should be executed in sequence.`; } showGenerateModal(commandData, yaml) { const { fullCommand, componentsCommand } = commandData; const fullCommandEl = document.getElementById('fullCommand'); const componentsOnlyCommandEl = document.getElementById('componentsOnlyCommand'); const yamlEl = document.getElementById('yamlContent'); const workflowFileNameEl = document.getElementById('workflowFileName'); const modal = document.getElementById('generateModal'); if (fullCommandEl) fullCommandEl.textContent = fullCommand; if (componentsOnlyCommandEl) componentsOnlyCommandEl.textContent = componentsCommand; if (yamlEl) yamlEl.textContent = yaml; // Update workflow file name dynamically if (workflowFileNameEl) { const workflowName = this.workflowState.properties.name || 'workflow'; const fileName = `${workflowName.replace(/[^a-z0-9]/gi, '-').toLowerCase()}.yaml`; workflowFileNameEl.textContent = fileName; } // Show prompt indicator if prompt is provided const promptIndicator = document.getElementById('promptIndicator'); const promptText = document.getElementById('promptText'); if (promptIndicator && promptText) { if (this.workflowState.properties.prompt) { promptText.textContent = `"${this.workflowState.properties.prompt}"`; promptIndicator.style.display = 'flex'; } else { promptIndicator.style.display = 'none'; } } if (modal) modal.style.display = 'block'; // Setup close modal functionality const closeModalBtn = document.getElementById('closeModal'); if (closeModalBtn) { closeModalBtn.onclick = () => modal.style.display = 'none'; } // Close modal when clicking outside window.onclick = (event) => { if (event.target === modal) { modal.style.display = 'none'; } }; // Add copy functionality to the buttons in the modal const copyFullCommandBtn = document.getElementById('copyFullCommand'); if (copyFullCommandBtn) { copyFullCommandBtn.onclick = () => copyToClipboard(fullCommand, 'Full command copied!'); } const copyComponentsCommandBtn = document.getElementById('copyComponentsCommand'); if (copyComponentsCommandBtn) { copyComponentsCommandBtn.onclick = () => copyToClipboard(componentsCommand, 'Components command copied!'); } // Original YAML buttons (in accordion) const copyYamlBtn = document.getElementById('copyYaml'); if (copyYamlBtn) { copyYamlBtn.onclick = () => copyToClipboard(yamlEl.textContent, 'YAML content copied!'); } const downloadYamlBtn = document.getElementById('downloadYaml'); if (downloadYamlBtn) { downloadYamlBtn.onclick = () => { const filename = `${this.workflowState.properties.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'workflow'}.yaml`; const blob = new Blob([yaml], { type: 'text/yaml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification('YAML downloaded successfully!', 'success'); }; } // Manual method YAML buttons const copyYamlManualBtn = document.getElementById('copyYamlManual'); if (copyYamlManualBtn) { copyYamlManualBtn.onclick = () => copyToClipboard(yamlEl.textContent, 'YAML content copied!'); } const downloadYamlManualBtn = document.getElementById('downloadYamlManual'); if (downloadYamlManualBtn) { downloadYamlManualBtn.onclick = () => { const filename = `${this.workflowState.properties.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'workflow'}.yaml`; const blob = new Blob([yaml], { type: 'text/yaml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification('YAML downloaded successfully!', 'success'); }; } const shareWorkflowBtn = document.getElementById('shareWorkflow'); if (shareWorkflowBtn) { shareWorkflowBtn.onclick = () => { // For the new approach, we share the command directly navigator.clipboard.writeText(fullCommand).then(() => { showNotification('Installation command copied to clipboard!', 'success'); }).catch(err => { console.error('Failed to copy command: ', err); showNotification('Failed to copy command', 'error'); }); }; } } previewWorkflow() { const agents = this.workflowState.agents || []; if (agents.length === 0) { this.showError('Add at least one component to preview the workflow.'); return; } // Set default properties if not set if (!this.workflowState.properties.name) { this.workflowState.properties.name = 'Untitled Workflow'; } if (!this.workflowState.properties.description) { this.workflowState.properties.description = 'Generated workflow preview'; } if (!this.workflowState.properties.tags.length) { this.workflowState.properties.tags = ['preview']; } // Generate YAML content const yamlContent = this.generateWorkflowYAML(); // Show in modal this.showPreviewModal(yamlContent); } showPreviewModal(yamlContent) { const modal = document.getElementById('previewModal'); const yamlElement = document.getElementById('previewYamlContent'); const lineNumbersElement = document.getElementById('previewLineNumbers'); if (!modal || !yamlElement || !lineNumbersElement) return; // Apply syntax highlighting to YAML content const highlightedYaml = this.highlightYaml(yamlContent); yamlElement.innerHTML = highlightedYaml; yamlElement.className = 'yaml-syntax'; // Generate line numbers const lines = yamlContent.split('\n'); lineNumbersElement.innerHTML = lines.map((_, index) => `${index + 1}` ).join(''); // Show modal modal.style.display = 'block'; // Setup event listeners this.setupPreviewModalListeners(yamlContent); } highlightYaml(yamlContent) { return yamlContent .split('\n') .map(line => this.highlightYamlLine(line)) .join('\n'); } highlightYamlLine(line) { // Handle comments (everything after #) if (line.trim().startsWith('#')) { return `${this.escapeHtml(line)}`; } // Handle lines with comments const commentIndex = line.indexOf('#'); let mainPart = line; let commentPart = ''; if (commentIndex > -1) { mainPart = line.substring(0, commentIndex); commentPart = `${this.escapeHtml(line.substring(commentIndex))}`; } // Handle key-value pairs const keyValueMatch = mainPart.match(/^(\s*)([\w-]+)(\s*:\s*)(.*)/); if (keyValueMatch) { const [, indent, key, colon, value] = keyValueMatch; let highlightedValue = value; // Highlight different value types if (value.trim().match(/^".*"$/)) { // String in quotes highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim().match(/^[\d.]+$/)) { // Numbers highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim().match(/^(true|false|yes|no|on|off)$/i)) { // Booleans highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim().match(/^[\w\/-]+\.(md|json|yaml|yml)$/)) { // File paths highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim().startsWith('[') && value.trim().endsWith(']')) { // Arrays highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim()) { // Regular strings highlightedValue = `${this.escapeHtml(value)}`; } return `${this.escapeHtml(indent)}${this.escapeHtml(key)}${this.escapeHtml(colon)}${highlightedValue}${commentPart}`; } // Handle list items const listMatch = mainPart.match(/^(\s*-\s*)(.*)/); if (listMatch) { const [, listMarker, content] = listMatch; // Check if it's a key-value in list const listKeyValueMatch = content.match(/^([\w-]+)(\s*:\s*)(.*)/); if (listKeyValueMatch) { const [, key, colon, value] = listKeyValueMatch; let highlightedValue = value; if (value.trim().match(/^".*"$/)) { highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim().match(/^[\d.]+$/)) { highlightedValue = `${this.escapeHtml(value)}`; } else if (value.trim()) { highlightedValue = `${this.escapeHtml(value)}`; } return `${this.escapeHtml(listMarker)}${this.escapeHtml(key)}${this.escapeHtml(colon)}${highlightedValue}${commentPart}`; } else { return `${this.escapeHtml(listMarker)}${this.escapeHtml(content)}${commentPart}`; } } // Handle section headers (main keys without indentation) const sectionMatch = mainPart.match(/^([\w-]+)(\s*:\s*)(.*)/); if (sectionMatch && !mainPart.startsWith(' ')) { const [, key, colon, value] = sectionMatch; return `${this.escapeHtml(key)}${this.escapeHtml(colon)}${this.escapeHtml(value)}${commentPart}`; } // Default: return line as is return `${this.escapeHtml(mainPart)}${commentPart}`; } escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } setupPreviewModalListeners(yamlContent) { // Close modal events const closeBtn = document.getElementById('closePreviewModal'); const closeModalBtn = document.getElementById('closePreviewModalBtn'); const modal = document.getElementById('previewModal'); if (closeBtn) { closeBtn.onclick = () => modal.style.display = 'none'; } if (closeModalBtn) { closeModalBtn.onclick = () => modal.style.display = 'none'; } // Copy YAML const copyBtn = document.getElementById('copyPreviewYaml'); if (copyBtn) { copyBtn.onclick = () => { navigator.clipboard.writeText(yamlContent).then(() => { showNotification('YAML copied to clipboard!', 'success'); }).catch(() => { showNotification('Failed to copy YAML', 'error'); }); }; } // Download YAML const downloadBtn = document.getElementById('downloadPreviewYaml'); if (downloadBtn) { downloadBtn.onclick = () => { const filename = `${this.workflowState.properties.name.replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'workflow'}-preview.yaml`; const blob = new Blob([yamlContent], { type: 'text/yaml' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showNotification('YAML downloaded successfully!', 'success'); }; } // Close modal when clicking outside window.onclick = (event) => { if (event.target === modal) { modal.style.display = 'none'; } }; } showError(message) { showNotification(message, 'error'); } } // Global functions for workflow events (called from onclick) function showWorkflowComponentDetails(type, name, path, category) { const component = window.dataLoader.findComponent(name, type); if (component) { showComponentModal(component); } else { console.warn('Component not found:', type, name); } } function addComponentFromButton(event) { const treeFile = event.target.closest('.tree-file'); const componentData = { ...treeFile.dataset }; if (window.workflowManager) { window.workflowManager.addWorkflowStep(componentData); } } function removeWorkflowStep(stepId) { if (window.workflowManager) { window.workflowManager.workflowState.steps = window.workflowManager.workflowState.steps.filter(step => step.id !== stepId); window.workflowManager.renderWorkflowSteps(); window.workflowManager.updateWorkflowStats(); } } function removeAgent(agentId) { if (window.workflowManager && confirm('Remove this agent and all its associated commands/MCPs?')) { // Remove from agents array window.workflowManager.workflowState.agents = window.workflowManager.workflowState.agents.filter(agentData => agentData.agent.id !== agentId); // Remove from legacy steps array window.workflowManager.workflowState.steps = window.workflowManager.workflowState.steps.filter(step => step.id !== agentId); window.workflowManager.renderWorkflowSteps(); window.workflowManager.updateWorkflowStats(); } } function removeSubItem(agentId, subItemId) { if (window.workflowManager) { const agentIndex = window.workflowManager.workflowState.agents.findIndex( agentData => agentData.agent.id === agentId ); if (agentIndex !== -1) { window.workflowManager.workflowState.agents[agentIndex].subItems = window.workflowManager.workflowState.agents[agentIndex].subItems.filter( subItem => subItem.id !== subItemId ); // Remove from legacy steps array window.workflowManager.workflowState.steps = window.workflowManager.workflowState.steps.filter(step => step.id !== subItemId); window.workflowManager.renderWorkflowSteps(); window.workflowManager.updateWorkflowStats(); } } } // Helper function to scroll to drop zone function scrollToDropZone() { const dropZone = document.querySelector('.drop-zone'); if (dropZone) { dropZone.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add a brief highlight effect dropZone.classList.add('drag-over'); setTimeout(() => { dropZone.classList.remove('drag-over'); }, 1000); } } // Helper functions for drag and drop function allowDrop(event) { event.preventDefault(); } function dropOnAgent(event, agentId) { event.preventDefault(); event.stopPropagation(); // Prevent the main drop handler from also firing // Remove drag-over class event.target.classList.remove('drag-over'); try { const componentData = JSON.parse(event.dataTransfer.getData('application/json')); if (componentData.componentType === 'command' || componentData.componentType === 'mcp') { window.workflowManager.addWorkflowStep(componentData, agentId); } } catch (error) { console.error('Error handling agent drop:', error); } } function dropOnAddItem(event) { event.preventDefault(); event.stopPropagation(); // Prevent the main drop handler from also firing // Remove drag-over class event.target.classList.remove('drag-over'); event.target.closest('.add-agent-btn')?.classList.remove('drag-over'); try { const componentData = JSON.parse(event.dataTransfer.getData('application/json')); // Add as main level item (no targetAgentId) window.workflowManager.addWorkflowStep(componentData); } catch (error) { console.error('Error handling add item drop:', error); } } // Initialize when DOM is ready document.addEventListener('DOMContentLoaded', () => { window.workflowManager = new WorkflowManager(); window.workflowManager.init(); });