// Data Loader - Handles loading component data from various sources class DataLoader { constructor() { this.componentsData = null; this.fullComponentsData = null; // Store full data for accurate counts this.templatesData = null; // Deprecated - templates now in components.json this.metadataData = null; // External metadata for tags, companies, technologies this.loadingStates = { components: false }; this.cache = new Map(); this.TIMEOUT_MS = 8000; // 8 seconds timeout this.ITEMS_PER_PAGE = 50; // Lazy loading batch size } // Get data file paths (always absolute for production) getDataPath(filename) { return '/' + filename; } // Load all components at once (simplified approach) async loadAllComponents() { try { this.loadingStates.components = true; this.showLoadingState('components', true); const cacheKey = 'all_components'; if (this.cache.has(cacheKey)) { this.showLoadingState('components', false); this.loadingStates.components = false; return this.cache.get(cacheKey); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS); const response = await fetch(this.getDataPath('components.json'), { signal: controller.signal, headers: { 'Cache-Control': 'max-age=300' // 5 minutes cache } }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const allData = await response.json(); // Store both full and component data this.fullComponentsData = allData; this.componentsData = allData; // Load metadata await this.loadMetadata(); this.cache.set(cacheKey, allData); this.showLoadingState('components', false); this.loadingStates.components = false; return this.componentsData; } catch (error) { this.showLoadingState('components', false); this.loadingStates.components = false; if (error.name === 'AbortError') { console.error('Components loading timed out after', this.TIMEOUT_MS + 'ms'); this.showError('Loading timed out. Using fallback data.'); } else { console.error('Error loading components:', error); this.showError('Failed to load components. Using fallback data.'); } return this.getFallbackComponentData(); } } // Load components with lazy loading and timeout (kept for backward compatibility) async loadComponents(page = 1, itemsPerPage = this.ITEMS_PER_PAGE) { try { this.loadingStates.components = true; this.showLoadingState('components', true); const cacheKey = `components_${page}_${itemsPerPage}`; if (this.cache.has(cacheKey)) { this.showLoadingState('components', false); this.loadingStates.components = false; return this.cache.get(cacheKey); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.TIMEOUT_MS); const response = await fetch(this.getDataPath('components.json'), { signal: controller.signal, headers: { 'Cache-Control': 'max-age=300' // 5 minutes cache } }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const fullData = await response.json(); // Store full data for accurate counts (only on first load) if (page === 1) { this.fullComponentsData = fullData; } // Apply pagination to reduce memory usage const paginatedData = this.paginateComponents(fullData, page, itemsPerPage); // Store full data for templates if it exists if (fullData.templates && page === 1) { paginatedData.templates = fullData.templates; // Templates don't need pagination } this.cache.set(cacheKey, paginatedData); this.componentsData = paginatedData; this.showLoadingState('components', false); this.loadingStates.components = false; return this.componentsData; } catch (error) { this.showLoadingState('components', false); this.loadingStates.components = false; if (error.name === 'AbortError') { console.error('Components loading timed out after', this.TIMEOUT_MS + 'ms'); this.showError('Loading timed out. Using fallback data.'); } else { console.error('Error loading components:', error); this.showError('Failed to load components. Using fallback data.'); } return this.getFallbackComponentData(); } } // Get fallback component data when components.json is unavailable getFallbackComponentData() { return { agents: [ { name: 'code-reviewer', path: 'development/code-reviewer', category: 'development', type: 'agent', content: 'AI-powered code reviewer that analyzes your code for best practices, security issues, and optimization opportunities.' }, { name: 'documentation-writer', path: 'development/documentation-writer', category: 'development', type: 'agent', content: 'Generates comprehensive documentation for your codebase, including API docs, README files, and inline comments.' }, { name: 'bug-hunter', path: 'development/bug-hunter', category: 'development', type: 'agent', content: 'Specialized in finding and fixing bugs through systematic code analysis and testing.' }, { name: 'security-auditor', path: 'security/security-auditor', category: 'security', type: 'agent', content: 'Performs security audits on your code to identify vulnerabilities and security best practices.' }, { name: 'performance-optimizer', path: 'optimization/performance-optimizer', category: 'optimization', type: 'agent', content: 'Analyzes and optimizes code performance, identifying bottlenecks and suggesting improvements.' } ], commands: [ { name: 'git-setup', path: 'development/git-setup', category: 'development', type: 'command', content: 'Sets up Git repository with best practices, including .gitignore, hooks, and workflow configurations.' }, { name: 'project-init', path: 'development/project-init', category: 'development', type: 'command', content: 'Initializes new projects with proper structure, dependencies, and configuration files.' }, { name: 'docker-setup', path: 'devops/docker-setup', category: 'devops', type: 'command', content: 'Creates Docker configurations including Dockerfile, docker-compose, and container optimization.' }, { name: 'test-runner', path: 'testing/test-runner', category: 'testing', type: 'command', content: 'Sets up comprehensive testing frameworks and runs automated test suites.' }, { name: 'build-pipeline', path: 'devops/build-pipeline', category: 'devops', type: 'command', content: 'Configures CI/CD pipelines for automated building, testing, and deployment.' } ], mcps: [ { name: 'database-connector', path: 'database/database-connector', category: 'database', type: 'mcp', content: 'Connects to various databases and provides query and management capabilities.' }, { name: 'api-client', path: 'api/api-client', category: 'api', type: 'mcp', content: 'HTTP client for interacting with REST APIs and web services.' }, { name: 'file-manager', path: 'system/file-manager', category: 'system', type: 'mcp', content: 'File system operations including reading, writing, and organizing files.' }, { name: 'redis-cache', path: 'database/redis-cache', category: 'database', type: 'mcp', content: 'Redis caching implementation for improved application performance.' }, { name: 'email-service', path: 'communication/email-service', category: 'communication', type: 'mcp', content: 'Email sending and management service with template support.' } ] }; } // Load templates - now using components.json data (templates deprecated) async loadTemplates() { try { // Templates are now included in components.json as 'templates' section // This method is kept for backward compatibility if (this.componentsData && this.componentsData.templates) { this.templatesData = { templates: this.componentsData.templates }; return this.templatesData; } // If components aren't loaded yet, try to load them if (!this.componentsData) { await this.loadComponents(); if (this.componentsData && this.componentsData.templates) { this.templatesData = { templates: this.componentsData.templates }; return this.templatesData; } } // Return empty if no templates in components.json console.warn('No templates found in components.json'); this.templatesData = {}; return this.templatesData; } catch (error) { console.error('Error loading templates from components:', error); this.templatesData = {}; return this.templatesData; } } // Parse the templates.js file content to extract TEMPLATES_CONFIG parseTemplatesConfig(fileContent) { try { const configMatch = fileContent.match(/const TEMPLATES_CONFIG = ({[\s\S]*?});/); if (!configMatch) { throw new Error('TEMPLATES_CONFIG not found in file'); } let configString = configMatch[1]; configString = configString.replace(/'/g, '"'); configString = configString.replace(/(\w+):/g, '"$1":'); configString = configString.replace(/,(\s*[}\]])/g, '$1'); const config = JSON.parse(configString); return this.transformTemplatesData(config); } catch (error) { console.error('Error parsing templates config:', error); throw error; } } // Transform templates data to a unified format transformTemplatesData(config) { const unified = {}; Object.entries(config).forEach(([category, items]) => { unified[category] = Object.entries(items).map(([key, template]) => ({ id: key, name: template.name, description: template.description || '', type: 'template', category: category, folderPath: template.folderPath || '', content: template.description || '', files: template.files || [], ...template })); }); return unified; } // Get all components as a flat array getAllComponents() { if (!this.componentsData) return []; const allComponents = []; ['agents', 'commands', 'mcps', 'skills'].forEach(type => { if (this.componentsData[type]) { allComponents.push(...this.componentsData[type]); } }); return allComponents; } // Find component by name and type findComponent(name, type) { if (!this.componentsData) return null; const typeKey = type + 's'; if (!this.componentsData[typeKey]) return null; return this.componentsData[typeKey].find(component => component.name === name); } // Get components by type getComponentsByType(type) { if (!this.componentsData) return []; const typeKey = type + 's'; return this.componentsData[typeKey] || []; } // Paginate components data to reduce memory usage paginateComponents(fullData, page, itemsPerPage) { const paginatedData = { agents: [], commands: [], mcps: [], skills: [], templates: [] // Include templates in pagination structure }; ['agents', 'commands', 'mcps', 'skills'].forEach(type => { if (fullData[type]) { const startIndex = (page - 1) * itemsPerPage; const endIndex = startIndex + itemsPerPage; paginatedData[type] = fullData[type].slice(startIndex, endIndex); } }); // Templates don't need pagination - include all if available if (fullData.templates) { paginatedData.templates = fullData.templates; } return paginatedData; } // Show loading states showLoadingState(type, isLoading) { const loadingElement = document.getElementById(`${type}-loading`); const contentElement = document.getElementById(`${type}-content`); if (loadingElement && contentElement) { if (isLoading) { loadingElement.style.display = 'flex'; contentElement.style.opacity = '0.5'; } else { loadingElement.style.display = 'none'; contentElement.style.opacity = '1'; } } // Also update any loading spinners in the UI const spinners = document.querySelectorAll('.loading-spinner'); spinners.forEach(spinner => { spinner.style.display = isLoading ? 'block' : 'none'; }); } // Show error messages showError(message) { // Try to use existing notification system if (window.showNotification) { window.showNotification(message, 'error', 5000); } else { console.warn(message); // Create simple toast notification const toast = document.createElement('div'); toast.className = 'error-toast'; toast.textContent = message; toast.style.cssText = ` position: fixed; top: 20px; right: 20px; background: #f85149; color: white; padding: 12px 16px; border-radius: 6px; z-index: 1000; font-size: 14px; max-width: 300px; `; document.body.appendChild(toast); setTimeout(() => toast.remove(), 5000); } } // Load more components (no longer needed but kept for compatibility) async loadMoreComponents(page) { // All components are now loaded at once, so this method returns null return null; } // Get the total counts from full data for accurate filter counts getTotalCounts() { if (this.fullComponentsData) { return { agents: this.fullComponentsData.agents ? this.fullComponentsData.agents.length : 0, commands: this.fullComponentsData.commands ? this.fullComponentsData.commands.length : 0, mcps: this.fullComponentsData.mcps ? this.fullComponentsData.mcps.length : 0, settings: this.fullComponentsData.settings ? this.fullComponentsData.settings.length : 0, hooks: this.fullComponentsData.hooks ? this.fullComponentsData.hooks.length : 0, skills: this.fullComponentsData.skills ? this.fullComponentsData.skills.length : 0, templates: this.fullComponentsData.templates ? this.fullComponentsData.templates.length : 0 }; } // Fallback to current loaded data return { agents: this.componentsData?.agents?.length || 0, commands: this.componentsData?.commands?.length || 0, mcps: this.componentsData?.mcps?.length || 0, settings: this.componentsData?.settings?.length || 0, hooks: this.componentsData?.hooks?.length || 0, skills: this.componentsData?.skills?.length || 0, templates: this.componentsData?.templates?.length || 0 }; } // Load external metadata async loadMetadata() { try { const cacheKey = 'metadata'; if (this.cache.has(cacheKey)) { this.metadataData = this.cache.get(cacheKey); return this.metadataData; } const response = await fetch(this.getDataPath('components-metadata.json'), { headers: { 'Cache-Control': 'max-age=300' // 5 minutes cache } }); if (!response.ok) { console.warn('Could not load metadata file, using empty metadata'); this.metadataData = { metadata: {}, companies: {}, technologies: {} }; return this.metadataData; } this.metadataData = await response.json(); this.cache.set(cacheKey, this.metadataData); return this.metadataData; } catch (error) { console.warn('Error loading metadata:', error); this.metadataData = { metadata: {}, companies: {}, technologies: {} }; return this.metadataData; } } // Get metadata for component (external metadata takes priority over frontmatter) getComponentMetadata(componentName) { if (!this.metadataData) { return { tags: [], companies: [], technologies: [] }; } const metadata = this.metadataData.metadata[componentName]; if (metadata) { return { tags: metadata.tags || [], companies: metadata.companies || [], technologies: metadata.technologies || [] }; } // Fallback to empty metadata return { tags: [], companies: [], technologies: [] }; } // Extract tags from component frontmatter (kept as fallback) extractTagsFromFrontmatter(content) { if (!content) return { tags: [], companies: [], technologies: [] }; const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!frontmatterMatch) return { tags: [], companies: [], technologies: [] }; const frontmatter = frontmatterMatch[1]; // Extract tags array const tagsMatch = frontmatter.match(/tags:\s*\[(.*?)\]/s); const tags = tagsMatch ? tagsMatch[1].split(',').map(tag => tag.trim().replace(/['"]/g, '')) : []; // Extract companies array const companiesMatch = frontmatter.match(/companies:\s*\[(.*?)\]/s); const companies = companiesMatch ? companiesMatch[1].split(',').map(company => company.trim().replace(/['"]/g, '')) : []; // Extract technologies array const technologiesMatch = frontmatter.match(/technologies:\s*\[(.*?)\]/s); const technologies = technologiesMatch ? technologiesMatch[1].split(',').map(tech => tech.trim().replace(/['"]/g, '')) : []; return { tags, companies, technologies }; } // Get all unique tags from components getAllTags() { const allTags = new Set(); this.getAllComponents().forEach(component => { const { tags } = this.getComponentMetadata(component.name); tags.forEach(tag => allTags.add(tag)); }); return Array.from(allTags).sort(); } // Get all unique companies from components getAllCompanies() { const allCompanies = new Set(); this.getAllComponents().forEach(component => { const { companies } = this.getComponentMetadata(component.name); companies.forEach(company => allCompanies.add(company)); }); return Array.from(allCompanies).sort(); } // Get all unique technologies from components getAllTechnologies() { const allTechnologies = new Set(); this.getAllComponents().forEach(component => { const { technologies } = this.getComponentMetadata(component.name); technologies.forEach(tech => allTechnologies.add(tech)); }); return Array.from(allTechnologies).sort(); } // Get company info from metadata getCompanyInfo(companySlug) { if (!this.metadataData || !this.metadataData.companies) { return null; } return this.metadataData.companies[companySlug] || null; } // Get technology info from metadata getTechnologyInfo(techSlug) { if (!this.metadataData || !this.metadataData.technologies) { return null; } return this.metadataData.technologies[techSlug] || null; } // Filter components by tags, companies, or technologies filterComponentsByTags(filterOptions = {}) { const { tags = [], companies = [], technologies = [], type = null } = filterOptions; let filteredComponents = this.getAllComponents(); // Filter by type if specified if (type) { filteredComponents = filteredComponents.filter(component => component.type === type); } // Filter by tags, companies, or technologies if (tags.length > 0 || companies.length > 0 || technologies.length > 0) { filteredComponents = filteredComponents.filter(component => { const componentMeta = this.getComponentMetadata(component.name); const hasMatchingTag = tags.length === 0 || tags.some(tag => componentMeta.tags.includes(tag) ); const hasMatchingCompany = companies.length === 0 || companies.some(company => componentMeta.companies.includes(company) ); const hasMatchingTechnology = technologies.length === 0 || technologies.some(tech => componentMeta.technologies.includes(tech) ); return hasMatchingTag || hasMatchingCompany || hasMatchingTechnology; }); } return filteredComponents; } // Get components for a specific company stack getCompanyStack(companySlug) { const companyComponents = this.filterComponentsByTags({ companies: [companySlug] }); // Group by type const groupedComponents = { agents: companyComponents.filter(c => c.type === 'agent'), commands: companyComponents.filter(c => c.type === 'command'), mcps: companyComponents.filter(c => c.type === 'mcp'), settings: companyComponents.filter(c => c.type === 'setting'), hooks: companyComponents.filter(c => c.type === 'hook'), templates: companyComponents.filter(c => c.type === 'template') }; return groupedComponents; } // Get components for a specific technology stack getTechnologyStack(techSlug) { const techComponents = this.filterComponentsByTags({ technologies: [techSlug] }); // Group by type const groupedComponents = { agents: techComponents.filter(c => c.type === 'agent'), commands: techComponents.filter(c => c.type === 'command'), mcps: techComponents.filter(c => c.type === 'mcp'), settings: techComponents.filter(c => c.type === 'setting'), hooks: techComponents.filter(c => c.type === 'hook'), templates: techComponents.filter(c => c.type === 'template') }; return groupedComponents; } // Get all settings components getSettings() { if (!this.componentsData) return []; return this.componentsData.settings || []; } // Get all hooks components getHooks() { if (!this.componentsData) return []; return this.componentsData.hooks || []; } // Get settings by category getSettingsByCategory(category) { const settings = this.getSettings(); return settings.filter(setting => setting.category === category); } // Get hooks by category getHooksByCategory(category) { const hooks = this.getHooks(); return hooks.filter(hook => hook.category === category); } // Get all setting categories getSettingCategories() { const settings = this.getSettings(); const categories = new Set(); settings.forEach(setting => { if (setting.category) { categories.add(setting.category); } }); return Array.from(categories).sort(); } // Get all hook categories getHookCategories() { const hooks = this.getHooks(); const categories = new Set(); hooks.forEach(hook => { if (hook.category) { categories.add(hook.category); } }); return Array.from(categories).sort(); } } // Global instance window.dataLoader = new DataLoader();