// Workflow Builder JavaScript
// Global state
let workflowState = {
steps: [],
properties: {
name: '',
description: '',
tags: []
},
components: {
agents: [],
commands: [],
mcps: []
}
};
// GitHub API configuration
const GITHUB_CONFIG = {
owner: 'davila7',
repo: 'claude-code-templates',
branch: 'main'
};
// Initialize the workflow builder
document.addEventListener('DOMContentLoaded', async function() {
showLoadingSpinner();
try {
await loadAllComponents();
initializeDragAndDrop();
initializeEventListeners();
initializeModalEventListeners();
initializeSortableSteps();
} catch (error) {
console.error('Error initializing Workflow Builder:', error);
showError('Failed to load components. Please refresh the page.');
} finally {
hideLoadingSpinner();
}
});
// Load all components from GitHub using the Git Trees API
async function loadAllComponents() {
try {
const response = await fetch('components.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const allComponents = await response.json();
workflowState.components = {
agents: allComponents.agents.sort((a, b) => a.path.localeCompare(b.path)),
commands: allComponents.commands.sort((a, b) => a.path.localeCompare(b.path)),
mcps: allComponents.mcps.sort((a, b) => a.path.localeCompare(b.path))
};
renderComponentsList('agents', workflowState.components.agents);
renderComponentsList('commands', workflowState.components.commands);
renderComponentsList('mcps', workflowState.components.mcps);
} catch (error) {
console.error('Error loading components:', error);
loadComponentsWithFallback();
}
}
async function loadComponentsWithFallback() {
const [agents, commands, mcps] = [
getFallbackComponentData('agents'),
getFallbackComponentData('commands'),
getFallbackComponentData('mcps')
];
workflowState.components = { agents, commands, mcps };
renderComponentsList('agents', agents);
renderComponentsList('commands', commands);
renderComponentsList('mcps', mcps);
}
// Get fallback component data
function getFallbackComponentData(type) {
const fallbackData = {
agents: [
{ name: 'code-reviewer', path: 'development/code-reviewer', category: 'development', type: 'agent', icon: '🤖' },
{ name: 'documentation-writer', path: 'development/documentation-writer', category: 'development', type: 'agent', icon: '🤖' },
{ name: 'bug-hunter', path: 'development/bug-hunter', category: 'development', type: 'agent', icon: '🤖' },
{ name: 'security-auditor', path: 'security/security-auditor', category: 'security', type: 'agent', icon: '🤖' },
{ name: 'performance-optimizer', path: 'optimization/performance-optimizer', category: 'optimization', type: 'agent', icon: '🤖' }
],
commands: [
{ name: 'git-setup', path: 'development/git-setup', category: 'development', type: 'command', icon: '⚡' },
{ name: 'project-init', path: 'development/project-init', category: 'development', type: 'command', icon: '⚡' },
{ name: 'docker-setup', path: 'devops/docker-setup', category: 'devops', type: 'command', icon: '⚡' },
{ name: 'test-runner', path: 'testing/test-runner', category: 'testing', type: 'command', icon: '⚡' },
{ name: 'build-pipeline', path: 'devops/build-pipeline', category: 'devops', type: 'command', icon: '⚡' }
],
mcps: [
{ name: 'database-connector', path: 'database/database-connector', category: 'database', type: 'mcp', icon: '🔌' },
{ name: 'api-client', path: 'api/api-client', category: 'api', type: 'mcp', icon: '🔌' },
{ name: 'file-manager', path: 'system/file-manager', category: 'system', type: 'mcp', icon: '🔌' },
{ name: 'redis-cache', path: 'database/redis-cache', category: 'database', type: 'mcp', icon: '🔌' },
{ name: 'email-service', path: 'communication/email-service', category: 'communication', type: 'mcp', icon: '🔌' }
]
};
const typeKey = type;
const data = fallbackData[typeKey] || [];
return data;
}
// Get icon for component type
function getComponentIcon(type) {
const icons = { 'agent': '🤖', 'command': '⚡', 'mcp': '🔌' };
return icons[type] || '📦';
}
// Render components list in the UI
function renderComponentsList(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 = createTreeFolder(category, categoryComponents, type);
container.appendChild(folderElement);
});
}
// Create tree folder element
function 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 = `
`;
const childrenContainer = folderElement.querySelector('.tree-children');
components.forEach(component => {
const fileElement = createTreeFile(component);
childrenContainer.appendChild(fileElement);
});
return folderElement;
}
// Create tree file element
function 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;
element.innerHTML = `
`;
return element;
}
// Get file icon based on type
function getFileIcon(type) {
const icons = { 'agent': '📄', 'command': '⚡', 'mcp': '⚙️' };
return icons[type] || '📄';
}
// Initialize drag and drop
function initializeDragAndDrop() {
const workflowSteps = document.getElementById('workflowSteps');
document.addEventListener('dragstart', (event) => {
if (event.target.closest('.tree-file')) {
handleDragStart(event);
}
});
workflowSteps.addEventListener('dragover', handleDragOver);
workflowSteps.addEventListener('drop', handleDrop);
workflowSteps.addEventListener('dragenter', (e) => e.target.closest('.drop-zone')?.classList.add('drag-over'));
workflowSteps.addEventListener('dragleave', (e) => e.target.closest('.drop-zone')?.classList.remove('drag-over'));
}
function 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';
}
function handleDragOver(event) {
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
}
function handleDrop(event) {
event.preventDefault();
event.target.closest('.drop-zone')?.classList.remove('drag-over');
try {
const componentData = JSON.parse(event.dataTransfer.getData('application/json'));
addWorkflowStep(componentData);
} catch (error) {
console.error('Error handling drop:', error);
}
}
function addComponentFromButton(event) {
const treeFile = event.target.closest('.tree-file');
const componentData = { ...treeFile.dataset };
addWorkflowStep(componentData);
}
function addWorkflowStep(componentData) {
const step = {
id: `step_${Date.now()}`,
type: componentData.componentType,
name: componentData.componentName,
path: componentData.componentPath,
category: componentData.componentCategory,
description: `Execute ${componentData.componentName}`,
icon: getComponentIcon(componentData.componentType)
};
workflowState.steps.push(step);
renderWorkflowSteps();
updateWorkflowStats();
}
function renderWorkflowSteps() {
const container = document.getElementById('workflowSteps');
const dropZone = container.querySelector('.drop-zone');
const existingSteps = container.querySelectorAll('.workflow-step');
existingSteps.forEach(step => step.remove());
if (workflowState.steps.length > 0) {
dropZone.style.display = 'none';
workflowState.steps.forEach((step, index) => {
const stepElement = document.createElement('div');
stepElement.className = 'workflow-step';
stepElement.dataset.stepId = step.id;
stepElement.innerHTML = `
${index + 1}
${step.icon}
${step.name}
${step.type}
`;
container.appendChild(stepElement);
});
} else {
dropZone.style.display = 'flex';
}
initializeSortableSteps();
}
function initializeSortableSteps() {
const workflowSteps = document.getElementById('workflowSteps');
if (workflowState.steps.length > 0) {
new Sortable(workflowSteps, {
animation: 150,
onEnd: (evt) => {
const movedStep = workflowState.steps.splice(evt.oldIndex, 1)[0];
workflowState.steps.splice(evt.newIndex, 0, movedStep);
renderWorkflowSteps();
}
});
}
}
function removeStep(stepId) {
workflowState.steps = workflowState.steps.filter(step => step.id !== stepId);
renderWorkflowSteps();
updateWorkflowStats();
}
function updateWorkflowStats() {
const stats = { agents: 0, commands: 0, mcps: 0 };
workflowState.steps.forEach(step => stats[step.type + 's']++);
document.getElementById('agentCount').textContent = stats.agents;
document.getElementById('commandCount').textContent = stats.commands;
document.getElementById('mcpCount').textContent = stats.mcps;
document.getElementById('totalSteps').textContent = workflowState.steps.length;
}
function initializeEventListeners() {
document.getElementById('componentSearch').addEventListener('input', handleComponentSearch);
document.getElementById('clearCanvas').addEventListener('click', clearWorkflow);
document.getElementById('generateWorkflow').addEventListener('click', openPropertiesModal);
document.getElementById('closePropertiesModal').addEventListener('click', closePropertiesModal);
document.getElementById('saveWorkflowProperties').addEventListener('click', saveAndGenerateWorkflow);
// Category accordion toggle
document.querySelectorAll('.category-card-header').forEach(header => {
header.addEventListener('click', () => {
header.parentElement.classList.toggle('expanded');
});
});
// Add event listener for sub-folders
document.addEventListener('click', function(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');
}
}
});
}
function 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';
});
}
function clearWorkflow() {
if (confirm('Are you sure you want to clear the workflow?')) {
workflowState.steps = [];
renderWorkflowSteps();
updateWorkflowStats();
}
}
function openPropertiesModal() {
if (workflowState.steps.length === 0) {
showError('Add at least one component to the workflow.');
return;
}
document.getElementById('propertiesModal').style.display = 'block';
}
function closePropertiesModal() {
document.getElementById('propertiesModal').style.display = 'none';
}
function saveAndGenerateWorkflow() {
workflowState.properties.name = document.getElementById('workflowName').value;
workflowState.properties.description = document.getElementById('workflowDescription').value;
workflowState.properties.tags = document.getElementById('workflowTags').value.split(',').map(t => t.trim());
if (!workflowState.properties.name) {
showError('Workflow name is required.');
return;
}
closePropertiesModal();
generateWorkflow();
}
async function generateWorkflow() {
showLoadingSpinner();
try {
const workflowHash = await generateWorkflowHash();
const yamlContent = generateWorkflowYAML();
showGenerateModal(workflowHash, yamlContent);
} catch (error) {
console.error('Error generating workflow:', error);
} finally {
hideLoadingSpinner();
}
}
async function generateWorkflowHash() {
const dataString = JSON.stringify(workflowState);
const hash = CryptoJS.SHA256(dataString).toString(CryptoJS.enc.Hex).substring(0, 12);
localStorage.setItem(`workflow_${hash}`, dataString);
return hash;
}
function generateWorkflowYAML() {
return `# Workflow: ${workflowState.properties.name}\n` +
`steps:\n` +
workflowState.steps.map((step, i) => ` - step: ${i+1}\n type: ${step.type}\n name: "${step.name}"`).join('\n');
}
function showGenerateModal(hash, yaml) {
document.getElementById('workflowCommand').textContent = `npx claude-code-templates@latest --workflow:#${hash}`;
document.getElementById('yamlContent').textContent = yaml;
document.getElementById('generateModal').style.display = 'block';
}
function showLoadingSpinner() { document.getElementById('loadingSpinner').style.display = 'flex'; }
function hideLoadingSpinner() { document.getElementById('loadingSpinner').style.display = 'none'; }
function showError(msg) { alert(msg); }
async function showComponentDetails(type, name, path, category) {
const component = workflowState.components[type + 's'].find(c => c.name === name);
if (!component) {
console.warn('Component not found:', type, name);
return;
}
showComponentModal(component);
}
// Show detailed component modal (recreated from original version)
function showComponentModal(component) {
const typeConfig = {
agent: { icon: '🤖', color: '#ff6b6b', label: 'AGENT' },
command: { icon: '⚡', color: '#4ecdc4', label: 'COMMAND' },
mcp: { icon: '🔌', color: '#45b7d1', label: 'MCP' }
};
const config = typeConfig[component.type];
const installCommand = generateInstallCommand(component);
const modalHTML = `
${getComponentDescription(component)}
📦 Installation
${installCommand}
📋 Component Details
${component.content ? component.content.substring(0, 500) + (component.content.length > 500 ? '...' : '') : 'No content available.'}
`;
// Remove existing modal if present
const existingModal = document.querySelector('.modal-overlay');
if (existingModal) {
existingModal.remove();
}
// Add modal to body
document.body.insertAdjacentHTML('beforeend', modalHTML);
}
// Helper functions for the modal
function formatComponentName(name) {
return name.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
}
function generateInstallCommand(component) {
return `npx claude-code-templates@latest --${component.type} "${component.path}"`;
}
function getComponentDescription(component) {
if (!component.content) {
return 'No description available.';
}
// Try to extract description from frontmatter
const descMatch = component.content.match(/description:\s*(.+?)(?:\n|$)/);
if (descMatch) {
return descMatch[1].trim().replace(/^["']|["']$/g, '');
}
// Use first paragraph if no frontmatter description
const lines = component.content.split('\n');
const firstParagraph = lines.find(line => line.trim() && !line.startsWith('---') && !line.startsWith('#'));
if (firstParagraph) {
return firstParagraph.trim();
}
return 'No description available.';
}
function viewOnGitHub(path) {
const url = `https://github.com/davila7/claude-code-templates/tree/main/${path}`;
window.open(url, '_blank');
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showSuccess('Command copied to clipboard!');
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
function closeComponentModal() {
const modalOverlay = document.querySelector('.modal-overlay');
if (modalOverlay) {
modalOverlay.remove();
}
}
function initializeModalEventListeners() {
// Component modal event listeners
document.getElementById('closeComponentModal').addEventListener('click', closeComponentModal);
document.getElementById('closeComponentModalBtn').addEventListener('click', closeComponentModal);
document.getElementById('addComponentToWorkflow').addEventListener('click', () => {
const componentData = JSON.parse(document.getElementById('componentModal').dataset.currentComponent);
addWorkflowStep({
componentType: componentData.type,
componentName: componentData.name,
componentPath: componentData.path,
componentCategory: componentData.category
});
closeComponentModal();
});
document.getElementById('copyUsageCommand').addEventListener('click', () => {
const command = document.getElementById('componentModalUsage').textContent;
navigator.clipboard.writeText(command).then(() => showSuccess('Command copied!'));
});
// Generate modal event listeners
const closeModal = document.getElementById('closeModal');
if (closeModal) {
closeModal.addEventListener('click', () => {
document.getElementById('generateModal').style.display = 'none';
});
}
const copyCommand = document.getElementById('copyCommand');
if (copyCommand) {
copyCommand.addEventListener('click', () => {
const command = document.getElementById('workflowCommand').textContent;
navigator.clipboard.writeText(command).then(() => showSuccess('Command copied!'));
});
}
const copyYaml = document.getElementById('copyYaml');
if (copyYaml) {
copyYaml.addEventListener('click', () => {
const yaml = document.getElementById('yamlContent').textContent;
navigator.clipboard.writeText(yaml).then(() => showSuccess('YAML copied!'));
});
}
// Close modals when clicking outside
window.addEventListener('click', (event) => {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
});
}
function showSuccess(message) {
// Simple success notification
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #10b981;
color: white;
padding: 12px 24px;
border-radius: 6px;
z-index: 10000;
font-family: system-ui, -apple-system, sans-serif;
`;
document.body.appendChild(notification);
setTimeout(() => {
document.body.removeChild(notification);
}, 2000);
}
// Add functions to window object for inline event handlers
window.removeStep = removeStep;
window.showComponentDetails = showComponentDetails;
window.addComponentFromButton = addComponentFromButton;