项目文件夹

文件
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 行
48 KiB
JavaScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// 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();
});