Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

1145 lines
48 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<div class="tree-node-header" data-folder="${folderId}">
<span class="tree-icon folder-icon">📁</span>
<span class="tree-label">${category}</span>
<span class="tree-count">${components.length}</span>
<span class="tree-arrow">▶</span>
</div>
<div class="tree-children" id="${folderId}"></div>
`;
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 = `
<div class="tree-file-header">
<span class="tree-icon file-icon">${fileIcon}</span>
<span class="tree-file-name">${component.name}</span>
<div class="tree-file-actions">
<button class="tree-action-btn" title="View Details" onclick="showWorkflowComponentDetails('${component.type}', '${component.name}', '${component.path}', '${component.category}')">️</button>
<button class="tree-action-btn" title="Add to Workflow" onclick="addComponentFromButton(event)"></button>
</div>
</div>
`;
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 = `
<div class="step-number">${mainStepNumber}</div>
<div class="agent-header">
<div class="step-icon">${agentData.agent.icon}</div>
<div class="step-content">
<div class="step-name">${agentData.agent.name}</div>
<div class="step-type">${agentData.agent.type}</div>
</div>
<div class="agent-actions">
<button class="agent-action" onclick="showWorkflowComponentDetails('${agentData.agent.type}', '${agentData.agent.name.replace(/'/g, "\\'")}')" title="Details">️</button>
<button class="agent-action" onclick="removeAgent('${agentData.agent.id}')" title="Remove">🗑️</button>
</div>
</div>
`;
} else {
// Agent with potential sub-items
const subItemsHtml = agentData.subItems.map((subItem, subIndex) => `
<div class="sub-item" data-sub-item-id="${subItem.id}">
<div class="step-number">${mainStepNumber}.${subIndex + 1}</div>
<div class="step-icon">${subItem.icon}</div>
<div class="step-content">
<div class="step-name">${subItem.name}</div>
<div class="step-type">${subItem.type}</div>
</div>
<div class="agent-actions">
<button class="sub-item-action" onclick="showWorkflowComponentDetails('${subItem.type}', '${subItem.name.replace(/'/g, "\\'")}')" title="Details">️</button>
<button class="sub-item-action" onclick="removeSubItem('${agentData.agent.id}', '${subItem.id}')" title="Remove">🗑️</button>
</div>
</div>
`).join('');
agentElement.innerHTML = `
<div class="step-number">${mainStepNumber}</div>
<div class="agent-header">
<div class="step-icon">${agentData.agent.icon}</div>
<div class="step-content">
<div class="step-name">${agentData.agent.name}</div>
<div class="step-type">${agentData.agent.type}</div>
</div>
<div class="agent-actions">
<button class="agent-action" onclick="showWorkflowComponentDetails('${agentData.agent.type}', '${agentData.agent.name.replace(/'/g, "\\'")}')" title="Details">️</button>
<button class="agent-action" onclick="removeAgent('${agentData.agent.id}')" title="Remove">🗑️</button>
</div>
</div>
<div class="agent-content">
<div class="agent-sub-items">
${subItemsHtml}
<div class="sub-item-drop-zone" data-agent-id="${agentData.agent.id}"
ondrop="dropOnAgent(event, '${agentData.agent.id}')"
ondragover="allowDrop(event)"
ondragenter="event.preventDefault(); event.target.classList.add('drag-over')"
ondragleave="if (!event.target.contains(event.relatedTarget)) event.target.classList.remove('drag-over')">
Drop commands or MCPs here
</div>
</div>
</div>
`;
}
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 = `
<div class="add-agent-btn"
onclick="scrollToDropZone()"
ondrop="dropOnAddItem(event)"
ondragover="allowDrop(event)">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M11,7H13V11H17V13H13V17H11V13H7V11H11V7Z"/>
</svg>
Add Next Item
</div>
`;
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) =>
`<span>${index + 1}</span>`
).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 `<span class="yaml-comment">${this.escapeHtml(line)}</span>`;
}
// Handle lines with comments
const commentIndex = line.indexOf('#');
let mainPart = line;
let commentPart = '';
if (commentIndex > -1) {
mainPart = line.substring(0, commentIndex);
commentPart = `<span class="yaml-comment">${this.escapeHtml(line.substring(commentIndex))}</span>`;
}
// 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 = `<span class="yaml-string">${this.escapeHtml(value)}</span>`;
} else if (value.trim().match(/^[\d.]+$/)) {
// Numbers
highlightedValue = `<span class="yaml-number">${this.escapeHtml(value)}</span>`;
} else if (value.trim().match(/^(true|false|yes|no|on|off)$/i)) {
// Booleans
highlightedValue = `<span class="yaml-boolean">${this.escapeHtml(value)}</span>`;
} else if (value.trim().match(/^[\w\/-]+\.(md|json|yaml|yml)$/)) {
// File paths
highlightedValue = `<span class="yaml-path">${this.escapeHtml(value)}</span>`;
} else if (value.trim().startsWith('[') && value.trim().endsWith(']')) {
// Arrays
highlightedValue = `<span class="yaml-array">${this.escapeHtml(value)}</span>`;
} else if (value.trim()) {
// Regular strings
highlightedValue = `<span class="yaml-string">${this.escapeHtml(value)}</span>`;
}
return `${this.escapeHtml(indent)}<span class="yaml-key">${this.escapeHtml(key)}</span><span class="yaml-punctuation">${this.escapeHtml(colon)}</span>${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 = `<span class="yaml-string">${this.escapeHtml(value)}</span>`;
} else if (value.trim().match(/^[\d.]+$/)) {
highlightedValue = `<span class="yaml-number">${this.escapeHtml(value)}</span>`;
} else if (value.trim()) {
highlightedValue = `<span class="yaml-string">${this.escapeHtml(value)}</span>`;
}
return `<span class="yaml-list-item">${this.escapeHtml(listMarker)}</span><span class="yaml-key">${this.escapeHtml(key)}</span><span class="yaml-punctuation">${this.escapeHtml(colon)}</span>${highlightedValue}${commentPart}`;
} else {
return `<span class="yaml-list-item">${this.escapeHtml(listMarker)}</span><span class="yaml-string">${this.escapeHtml(content)}</span>${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 `<span class="yaml-section">${this.escapeHtml(key)}</span><span class="yaml-punctuation">${this.escapeHtml(colon)}</span><span class="yaml-string">${this.escapeHtml(value)}</span>${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();
});