620 lines
24 KiB
JavaScript
620 lines
24 KiB
JavaScript
// 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 = `
|
||
<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 = 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 = `
|
||
<div class="tree-file-header">
|
||
<span class="tree-icon file-icon">${getFileIcon(component.type)}</span>
|
||
<span class="tree-file-name">${component.name}</span>
|
||
<div class="tree-file-actions">
|
||
<button class="tree-action-btn" title="View Details" onclick="showComponentDetails('${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;
|
||
}
|
||
|
||
// 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 = `
|
||
<div class="step-number">${index + 1}</div>
|
||
<div class="step-icon">${step.icon}</div>
|
||
<div class="step-content">
|
||
<div class="step-name">${step.name}</div>
|
||
<div class="step-type">${step.type}</div>
|
||
</div>
|
||
<div class="step-actions">
|
||
<button class="step-action details" onclick="showComponentDetails('${step.type}', '${step.name}', '${step.path}', '${step.category}')">ℹ️</button>
|
||
<button class="step-action remove" onclick="removeStep('${step.id}')">🗑️</button>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="modal-overlay" onclick="closeComponentModal()">
|
||
<div class="modal-content component-modal" onclick="event.stopPropagation()">
|
||
<div class="modal-header">
|
||
<div class="component-modal-title">
|
||
<span class="component-icon" style="color: ${config.color}">${config.icon}</span>
|
||
<h3>${formatComponentName(component.name)}</h3>
|
||
<span class="component-type-badge" style="background: ${config.color}">${config.label}</span>
|
||
</div>
|
||
<button class="modal-close" onclick="closeComponentModal()">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="component-details">
|
||
<div class="component-description">
|
||
${getComponentDescription(component)}
|
||
</div>
|
||
|
||
<div class="installation-section">
|
||
<h4>📦 Installation</h4>
|
||
<div class="command-line">
|
||
<code>${installCommand}</code>
|
||
<button class="copy-btn" onclick="copyToClipboard('${installCommand}')">Copy</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="component-content">
|
||
<h4>📋 Component Details</h4>
|
||
<div class="component-preview">
|
||
<pre><code>${component.content ? component.content.substring(0, 500) + (component.content.length > 500 ? '...' : '') : 'No content available.'}</code></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button class="github-folder-link" onclick="viewOnGitHub('${component.path}')">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.30.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||
</svg>
|
||
View on GitHub
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
// 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; |