Spaces:
Sleeping
Sleeping
| // Crypto Intelligence Hub - Main JavaScript with Sidebar Navigation | |
| // Enhanced Pro Trading Terminal UI | |
| // ============================================================================= | |
| // Console Warning Filter (suppress external server warnings) | |
| // ============================================================================= | |
| (function() { | |
| const originalWarn = console.warn; | |
| const originalError = console.error; | |
| console.warn = function(...args) { | |
| const message = args.join(' '); | |
| // Suppress Permissions-Policy warnings from external servers (e.g., Hugging Face) | |
| if (message.includes('Unrecognized feature:') && | |
| (message.includes('ambient-light-sensor') || | |
| message.includes('battery') || | |
| message.includes('document-domain') || | |
| message.includes('layout-animations') || | |
| message.includes('legacy-image-formats') || | |
| message.includes('oversized-images') || | |
| message.includes('vr') || | |
| message.includes('wake-lock'))) { | |
| return; // Suppress these warnings | |
| } | |
| originalWarn.apply(console, args); | |
| }; | |
| console.error = function(...args) { | |
| const message = args.join(' '); | |
| // Suppress Hugging Face Spaces SSE errors (only relevant for HF deployment) | |
| if (message.includes('/api/spaces/') && message.includes('/events') || | |
| message.includes('Failed to fetch Space status') || | |
| message.includes('SSE Stream ended') || | |
| message.includes('ERR_HTTP2_PROTOCOL_ERROR')) { | |
| return; // Suppress these errors (not relevant for local deployment) | |
| } | |
| originalError.apply(console, args); | |
| }; | |
| })(); | |
| // ============================================================================= | |
| // Toast Notification System (use the one from toast.js) | |
| // ============================================================================= | |
| // Helper function to get toast manager (loaded from toast.js) | |
| function getToast() { | |
| return window.toastManager || window.toast || { | |
| init() {}, | |
| show(msg, type) { console.log(`Toast: ${type} - ${msg}`); }, | |
| success(msg) { this.show(msg, 'success'); }, | |
| error(msg) { this.show(msg, 'error'); }, | |
| warning(msg) { this.show(msg, 'warning'); }, | |
| info(msg) { this.show(msg, 'info'); } | |
| }; | |
| } | |
| // ============================================================================= | |
| // Global State | |
| // ============================================================================= | |
| const AppState = { | |
| currentTab: 'dashboard', | |
| data: {}, | |
| charts: {}, | |
| isLoading: false, | |
| sidebarOpen: false | |
| }; | |
| // ============================================================================= | |
| // Sidebar Navigation | |
| // ============================================================================= | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('sidebar-overlay'); | |
| if (sidebar && overlay) { | |
| sidebar.classList.toggle('active'); | |
| overlay.classList.toggle('active'); | |
| AppState.sidebarOpen = !AppState.sidebarOpen; | |
| } | |
| } | |
| function switchTab(tabId) { | |
| // Update nav items | |
| const navItems = document.querySelectorAll('.nav-item'); | |
| navItems.forEach(item => { | |
| if (item.dataset.tab === tabId) { | |
| item.classList.add('active'); | |
| } else { | |
| item.classList.remove('active'); | |
| } | |
| }); | |
| // Update tab panels | |
| const tabPanels = document.querySelectorAll('.tab-panel'); | |
| tabPanels.forEach(panel => { | |
| if (panel.id === `tab-${tabId}`) { | |
| panel.classList.add('active'); | |
| } else { | |
| panel.classList.remove('active'); | |
| } | |
| }); | |
| // Update page title | |
| const pageTitles = { | |
| 'dashboard': { title: 'Dashboard', subtitle: 'System Overview' }, | |
| 'market': { title: 'Market Data', subtitle: 'Real-time Cryptocurrency Prices' }, | |
| 'models': { title: 'AI Models', subtitle: 'Hugging Face Models' }, | |
| 'sentiment': { title: 'Sentiment Analysis', subtitle: 'AI-Powered Sentiment Detection' }, | |
| 'trading-assistant': { title: 'Trading Signals', subtitle: 'AI Trading Assistant' }, | |
| 'news': { title: 'Crypto News', subtitle: 'Latest News & Updates' }, | |
| 'settings': { title: 'Settings', subtitle: 'System Configuration' }, | |
| 'diagnostics': { title: 'Test & Diagnostics', subtitle: 'System Diagnostics & Model Testing' }, | |
| 'ai-tools': { title: 'AI Design Tools', subtitle: 'AI-Powered Tools & Utilities' }, | |
| 'providers': { title: 'Providers', subtitle: 'Provider Management' }, | |
| 'resources': { title: 'Resources', subtitle: 'Resource Management' }, | |
| 'defi': { title: 'DeFi Analytics', subtitle: 'DeFi Protocol Analytics' }, | |
| 'system-status': { title: 'System Status', subtitle: 'System Health Monitoring' } | |
| }; | |
| const pageTitle = document.getElementById('page-title'); | |
| const pageSubtitle = document.getElementById('page-subtitle'); | |
| if (pageTitle && pageTitles[tabId]) { | |
| pageTitle.textContent = pageTitles[tabId].title; | |
| } | |
| if (pageSubtitle && pageTitles[tabId]) { | |
| pageSubtitle.textContent = pageTitles[tabId].subtitle; | |
| } | |
| // Update state | |
| AppState.currentTab = tabId; | |
| // Load tab data | |
| loadTabData(tabId); | |
| // Close sidebar on mobile after selection | |
| if (window.innerWidth <= 768) { | |
| toggleSidebar(); | |
| } | |
| } | |
| // ============================================================================= | |
| // Initialize App | |
| // ============================================================================= | |
| document.addEventListener('DOMContentLoaded', () => { | |
| console.log('🚀 Initializing Crypto Intelligence Hub...'); | |
| // Initialize toast manager | |
| getToast().init(); | |
| // Check API status | |
| checkAPIStatus(); | |
| // Load initial dashboard immediately | |
| setTimeout(() => { | |
| loadDashboard(); | |
| }, 100); | |
| // Auto-refresh every 30 seconds | |
| setInterval(() => { | |
| if (AppState.currentTab === 'dashboard') { | |
| loadDashboard(); | |
| } | |
| }, 30000); | |
| // Listen for trading pairs loaded event | |
| document.addEventListener('tradingPairsLoaded', function(e) { | |
| console.log('Trading pairs loaded:', e.detail.pairs.length); | |
| initTradingPairSelectors(); | |
| }); | |
| console.log('✅ App initialized successfully'); | |
| }); | |
| // Initialize trading pair selectors after pairs are loaded | |
| function initTradingPairSelectors() { | |
| // Initialize asset symbol selector | |
| const assetSymbolContainer = document.getElementById('asset-symbol-container'); | |
| if (assetSymbolContainer && window.TradingPairsLoader) { | |
| const pairs = window.TradingPairsLoader.getTradingPairs(); | |
| if (pairs && pairs.length > 0) { | |
| assetSymbolContainer.innerHTML = window.TradingPairsLoader.createTradingPairCombobox( | |
| 'asset-symbol', | |
| 'Select or type trading pair', | |
| 'BTCUSDT' | |
| ); | |
| } | |
| } | |
| // Initialize trading symbol selector | |
| const tradingSymbolContainer = document.getElementById('trading-symbol-container'); | |
| if (tradingSymbolContainer && window.TradingPairsLoader) { | |
| const pairs = window.TradingPairsLoader.getTradingPairs(); | |
| if (pairs && pairs.length > 0) { | |
| tradingSymbolContainer.innerHTML = window.TradingPairsLoader.createTradingPairCombobox( | |
| 'trading-symbol', | |
| 'Select or type trading pair', | |
| 'BTCUSDT' | |
| ); | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // Tab Data Loading | |
| // ============================================================================= | |
| function loadTabData(tabId) { | |
| console.log(`Loading data for tab: ${tabId}`); | |
| switch(tabId) { | |
| case 'dashboard': | |
| loadDashboard(); | |
| break; | |
| case 'market': | |
| loadMarketData(); | |
| break; | |
| case 'models': | |
| loadModels(); | |
| break; | |
| case 'sentiment': | |
| // Sentiment tab is interactive, no auto-load needed | |
| break; | |
| case 'trading-assistant': | |
| // Trading assistant tab is interactive, no auto-load needed | |
| break; | |
| case 'news': | |
| loadNews(); | |
| break; | |
| case 'settings': | |
| loadSettings(); | |
| break; | |
| case 'diagnostics': | |
| refreshDiagnosticStatus(); | |
| break; | |
| case 'ai-tools': | |
| loadAITools(); | |
| break; | |
| default: | |
| console.log('No specific loader for tab:', tabId); | |
| } | |
| } | |
| function refreshCurrentTab() { | |
| loadTabData(AppState.currentTab); | |
| getToast().success('Data refreshed successfully'); | |
| } | |
| // ============================================================================= | |
| // API Status Check | |
| // ============================================================================= | |
| async function checkAPIStatus() { | |
| try { | |
| const response = await fetch('/health'); | |
| const data = await response.json(); | |
| const statusIndicator = document.getElementById('sidebar-status'); | |
| if (statusIndicator) { | |
| if (data.status === 'healthy') { | |
| statusIndicator.textContent = 'System Active'; | |
| statusIndicator.parentElement.style.background = 'rgba(16, 185, 129, 0.15)'; | |
| statusIndicator.parentElement.style.borderColor = 'rgba(16, 185, 129, 0.3)'; | |
| } else { | |
| statusIndicator.textContent = 'System Error'; | |
| statusIndicator.parentElement.style.background = 'rgba(239, 68, 68, 0.15)'; | |
| statusIndicator.parentElement.style.borderColor = 'rgba(239, 68, 68, 0.3)'; | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error checking API status:', error); | |
| const statusIndicator = document.getElementById('sidebar-status'); | |
| if (statusIndicator) { | |
| statusIndicator.textContent = 'Connection Failed'; | |
| statusIndicator.parentElement.style.background = 'rgba(239, 68, 68, 0.15)'; | |
| statusIndicator.parentElement.style.borderColor = 'rgba(239, 68, 68, 0.3)'; | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // Dashboard Loading | |
| // ============================================================================= | |
| async function loadDashboard() { | |
| console.log('📊 Loading dashboard...'); | |
| // Show loading state | |
| const statsElements = [ | |
| 'stat-total-resources', 'stat-free-resources', | |
| 'stat-models', 'stat-providers' | |
| ]; | |
| statsElements.forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) el.textContent = '...'; | |
| }); | |
| const systemStatusDiv = document.getElementById('system-status'); | |
| if (systemStatusDiv) { | |
| systemStatusDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading system status...</div>'; | |
| } | |
| try { | |
| // Load resources summary (use the correct endpoint) | |
| const resourcesRes = await fetch('/api/resources/summary'); | |
| if (!resourcesRes.ok) { | |
| throw new Error(`Resources API returned ${resourcesRes.status}`); | |
| } | |
| let resourcesData = await resourcesRes.json(); | |
| console.log('Resources data:', resourcesData); | |
| // Check if data is an array (unexpected format - might be from wrong endpoint) | |
| if (Array.isArray(resourcesData)) { | |
| // Try to extract summary from array if it contains objects with summary property | |
| const summaryObj = resourcesData.find(item => item && typeof item === 'object' && !Array.isArray(item) && item.summary); | |
| if (summaryObj && summaryObj.summary) { | |
| resourcesData = summaryObj; | |
| console.log('Extracted summary from array response'); | |
| } else { | |
| // Fallback: use array length as total resources estimate | |
| const totalResources = resourcesData.length; | |
| console.log(`Using array length (${totalResources}) as resource count estimate`); | |
| document.getElementById('stat-total-resources').textContent = totalResources; | |
| document.getElementById('stat-free-resources').textContent = Math.floor(totalResources * 0.8); // Estimate 80% free | |
| document.getElementById('stat-models').textContent = '0'; | |
| // Update sidebar stats | |
| const sidebarResources = document.getElementById('sidebar-resources'); | |
| const sidebarModels = document.getElementById('sidebar-models'); | |
| if (sidebarResources) sidebarResources.textContent = totalResources; | |
| if (sidebarModels) sidebarModels.textContent = '0'; | |
| return; // Exit early since we can't process array format properly | |
| } | |
| } | |
| // Check if we have the summary object | |
| let summary = null; | |
| if (resourcesData && typeof resourcesData === 'object' && !Array.isArray(resourcesData)) { | |
| summary = resourcesData.summary || resourcesData; | |
| } | |
| // Validate that summary is an object with expected properties | |
| if (summary && typeof summary === 'object' && !Array.isArray(summary)) { | |
| // Check if it has at least one of the expected properties | |
| const hasExpectedProperties = summary.total_resources !== undefined || | |
| summary.free_resources !== undefined || | |
| summary.models_available !== undefined || | |
| (resourcesData.success !== false && resourcesData.success !== undefined); | |
| if (hasExpectedProperties || resourcesData.success === true) { | |
| const totalResources = summary.total_resources || 0; | |
| const freeResources = summary.free_resources || 0; | |
| const modelsAvailable = summary.models_available || 0; | |
| // Update metric cards - ensure elements exist before updating | |
| const totalResourcesEl = document.getElementById('stat-total-resources'); | |
| const freeResourcesEl = document.getElementById('stat-free-resources'); | |
| const modelsEl = document.getElementById('stat-models'); | |
| if (totalResourcesEl) totalResourcesEl.textContent = totalResources; | |
| if (freeResourcesEl) freeResourcesEl.textContent = freeResources; | |
| if (modelsEl) modelsEl.textContent = modelsAvailable; | |
| // Update sidebar stats | |
| const sidebarResources = document.getElementById('sidebar-resources'); | |
| const sidebarModels = document.getElementById('sidebar-models'); | |
| if (sidebarResources) sidebarResources.textContent = totalResources; | |
| if (sidebarModels) sidebarModels.textContent = modelsAvailable; | |
| // Load categories chart - handle both object and simple count format | |
| if (summary.categories && typeof summary.categories === 'object' && !Array.isArray(summary.categories)) { | |
| const categories = summary.categories; | |
| // Convert {category: {count: N}} to {category: N} for chart | |
| const chartData = {}; | |
| for (const [key, value] of Object.entries(categories)) { | |
| chartData[key] = typeof value === 'object' && value !== null ? (value.count || value) : value; | |
| } | |
| createCategoriesChart(chartData); | |
| } | |
| } else { | |
| // Data structure exists but doesn't have expected properties | |
| console.warn('Resources data missing expected properties:', resourcesData); | |
| document.getElementById('stat-total-resources').textContent = '0'; | |
| document.getElementById('stat-free-resources').textContent = '0'; | |
| document.getElementById('stat-models').textContent = '0'; | |
| } | |
| } else { | |
| // Invalid data format - log minimal info to avoid console spam | |
| if (Array.isArray(resourcesData)) { | |
| console.log(`Resources API returned array (${resourcesData.length} items) instead of summary object`); | |
| } else { | |
| console.log('Resources data format unexpected - not a valid object:', typeof resourcesData); | |
| } | |
| document.getElementById('stat-total-resources').textContent = '0'; | |
| document.getElementById('stat-free-resources').textContent = '0'; | |
| document.getElementById('stat-models').textContent = '0'; | |
| } | |
| // Load system status | |
| try { | |
| const statusRes = await fetch('/api/status'); | |
| if (statusRes.ok) { | |
| const statusData = await statusRes.json(); | |
| // Handle different response formats | |
| let providers = 0; | |
| if (statusData.providers && typeof statusData.providers === 'object') { | |
| providers = statusData.providers.total || 0; | |
| } else { | |
| providers = statusData.total_apis || statusData.total_providers || statusData.providers || 0; | |
| } | |
| const providersEl = document.getElementById('stat-providers'); | |
| if (providersEl) { | |
| providersEl.textContent = providers; | |
| } | |
| // Display system status - handle different response formats | |
| const systemStatusDiv = document.getElementById('system-status'); | |
| if (systemStatusDiv) { | |
| // Try to get health status from different possible fields | |
| const healthStatus = statusData.system_health || statusData.status || 'ok'; | |
| const healthClass = healthStatus === 'healthy' || healthStatus === 'ok' ? 'alert-success' : | |
| healthStatus === 'degraded' ? 'alert-warning' : 'alert-error'; | |
| // Get provider counts | |
| const providers = statusData.providers || {}; | |
| const totalProviders = providers.total || statusData.total_apis || 0; | |
| const onlineProviders = statusData.online || 0; | |
| const degradedProviders = statusData.degraded || 0; | |
| const offlineProviders = statusData.offline || 0; | |
| const avgResponseTime = statusData.avg_response_time_ms || 0; | |
| const lastUpdate = statusData.last_update || statusData.timestamp || new Date().toISOString(); | |
| // Format last update time | |
| let formattedTime = 'N/A'; | |
| try { | |
| const updateDate = new Date(lastUpdate); | |
| formattedTime = updateDate.toLocaleString('en-US', { | |
| year: 'numeric', | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }); | |
| } catch (e) { | |
| formattedTime = lastUpdate; | |
| } | |
| // Create a properly formatted system status display | |
| const statusIcon = healthStatus === 'healthy' || healthStatus === 'ok' ? | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>' : | |
| healthStatus === 'degraded' ? | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>' : | |
| '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>'; | |
| const statusText = healthStatus === 'ok' ? 'Healthy' : | |
| healthStatus === 'healthy' ? 'Healthy' : | |
| healthStatus === 'degraded' ? 'Degraded' : | |
| healthStatus === 'error' ? 'Error' : 'Unknown'; | |
| systemStatusDiv.innerHTML = ` | |
| <div class="system-status-container"> | |
| <div class="system-status-header ${healthClass}"> | |
| <div class="status-icon-wrapper"> | |
| ${statusIcon} | |
| </div> | |
| <div> | |
| <div class="status-title">System Status</div> | |
| <div class="status-value">${statusText}</div> | |
| </div> | |
| </div> | |
| <div class="system-status-grid"> | |
| <div class="status-item"> | |
| <div class="status-item-label">Total Providers</div> | |
| <div class="status-item-value">${totalProviders}</div> | |
| </div> | |
| <div class="status-item status-online"> | |
| <div class="status-item-label">Online APIs</div> | |
| <div class="status-item-value">${onlineProviders}</div> | |
| </div> | |
| <div class="status-item status-degraded"> | |
| <div class="status-item-label">Degraded APIs</div> | |
| <div class="status-item-value">${degradedProviders}</div> | |
| </div> | |
| <div class="status-item status-offline"> | |
| <div class="status-item-label">Offline APIs</div> | |
| <div class="status-item-value">${offlineProviders}</div> | |
| </div> | |
| <div class="status-item"> | |
| <div class="status-item-label">Avg Response Time</div> | |
| <div class="status-item-value">${avgResponseTime}ms</div> | |
| </div> | |
| <div class="status-item status-full-width"> | |
| <div class="status-item-label">Last Update</div> | |
| <div class="status-item-value status-time">${formattedTime}</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| } else { | |
| throw new Error('Status endpoint not available'); | |
| } | |
| } catch (statusError) { | |
| console.warn('Status endpoint error:', statusError); | |
| document.getElementById('stat-providers').textContent = '-'; | |
| const systemStatusDiv = document.getElementById('system-status'); | |
| if (systemStatusDiv) { | |
| systemStatusDiv.innerHTML = '<div class="alert alert-warning">System status unavailable. Core features are operational.</div>'; | |
| } | |
| } | |
| console.log('✅ Dashboard loaded successfully'); | |
| } catch (error) { | |
| console.error('❌ Error loading dashboard:', error); | |
| getToast().error('Failed to load dashboard. Please check the backend.'); | |
| // Show error state | |
| const systemStatusDiv = document.getElementById('system-status'); | |
| if (systemStatusDiv) { | |
| systemStatusDiv.innerHTML = `<div class="alert alert-error">Failed to load dashboard data: ${error.message}<br>Please refresh or check backend status.</div>`; | |
| } | |
| // Set default values | |
| statsElements.forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) el.textContent = '0'; | |
| }); | |
| } | |
| } | |
| // Create Categories Chart | |
| function createCategoriesChart(categories) { | |
| const ctx = document.getElementById('categories-chart'); | |
| if (!ctx) return; | |
| // Check if Chart.js is loaded | |
| if (typeof Chart === 'undefined') { | |
| console.error('Chart.js is not loaded'); | |
| ctx.parentElement.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">Chart library not loaded</p>'; | |
| return; | |
| } | |
| if (AppState.charts.categories) { | |
| AppState.charts.categories.destroy(); | |
| } | |
| const labels = Object.keys(categories); | |
| const values = Object.values(categories); | |
| if (labels.length === 0) { | |
| ctx.parentElement.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No category data available</p>'; | |
| return; | |
| } | |
| AppState.charts.categories = new Chart(ctx, { | |
| type: 'bar', | |
| data: { | |
| labels: labels, | |
| datasets: [{ | |
| label: 'Total Resources', | |
| data: values, | |
| backgroundColor: 'rgba(102, 126, 234, 0.6)', | |
| borderColor: 'rgba(102, 126, 234, 1)', | |
| borderWidth: 2 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: true, | |
| plugins: { | |
| legend: { display: false } | |
| }, | |
| scales: { | |
| y: { beginAtZero: true } | |
| } | |
| } | |
| }); | |
| } | |
| // ============================================================================= | |
| // Market Data Loading | |
| // ============================================================================= | |
| async function loadMarketData() { | |
| console.log('💰 Loading market data...'); | |
| const marketDiv = document.getElementById('market-data'); | |
| const trendingDiv = document.getElementById('trending-coins'); | |
| const fgDiv = document.getElementById('fear-greed'); | |
| if (marketDiv) marketDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading market data...</div>'; | |
| if (trendingDiv) trendingDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading trending coins...</div>'; | |
| if (fgDiv) fgDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading Fear & Greed Index...</div>'; | |
| try { | |
| const response = await fetch('/api/market'); | |
| if (!response.ok) { | |
| throw new Error(`Market API returned ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| if (data.cryptocurrencies && data.cryptocurrencies.length > 0) { | |
| marketDiv.innerHTML = ` | |
| <div style="overflow-x: auto;"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>#</th> | |
| <th>Name</th> | |
| <th>Price (USD)</th> | |
| <th>24h Change</th> | |
| <th>24h Volume</th> | |
| <th>Market Cap</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${data.cryptocurrencies.map(coin => ` | |
| <tr> | |
| <td>${coin.rank || '-'}</td> | |
| <td> | |
| ${coin.image ? `<img src="${coin.image}" style="width: 24px; height: 24px; margin-right: 8px; vertical-align: middle;" />` : ''} | |
| <strong>${coin.symbol}</strong> ${coin.name} | |
| </td> | |
| <td>$${formatNumber(coin.price)}</td> | |
| <td style="color: ${coin.change_24h >= 0 ? 'var(--success)' : 'var(--danger)'}; font-weight: 600;"> | |
| ${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h || 0).toFixed(2)}% | |
| </td> | |
| <td>$${formatNumber(coin.volume_24h)}</td> | |
| <td>$${formatNumber(coin.market_cap)}</td> | |
| </tr> | |
| `).join('')} | |
| </tbody> | |
| </table> | |
| </div> | |
| ${data.total_market_cap ? `<div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;"> | |
| <strong>Total Market Cap:</strong> $${formatNumber(data.total_market_cap)} | | |
| <strong>BTC Dominance:</strong> ${(data.btc_dominance || 0).toFixed(2)}% | |
| </div>` : ''} | |
| `; | |
| } else { | |
| marketDiv.innerHTML = '<div class="alert alert-warning">No market data available</div>'; | |
| } | |
| // Load trending coins | |
| try { | |
| const trendingRes = await fetch('/api/trending'); | |
| if (trendingRes.ok) { | |
| const trendingData = await trendingRes.json(); | |
| if (trendingData.trending && trendingData.trending.length > 0) { | |
| trendingDiv.innerHTML = ` | |
| <div class="trending-coins-grid"> | |
| ${trendingData.trending.map((coin, index) => { | |
| const coinSymbol = coin.symbol || coin.id || 'N/A'; | |
| const coinName = coin.name || 'Unknown'; | |
| const marketCapRank = coin.market_cap_rank || null; | |
| const score = coin.score !== undefined && coin.score !== null ? coin.score : null; | |
| const thumb = coin.thumb || null; | |
| return ` | |
| <div class="trending-coin-card"> | |
| <div class="trending-coin-rank">#${index + 1}</div> | |
| <div class="trending-coin-content"> | |
| ${thumb ? `<img src="${thumb}" alt="${coinName}" class="trending-coin-thumb" onerror="this.style.display='none'">` : ''} | |
| <div class="trending-coin-info"> | |
| <div class="trending-coin-name"> | |
| <strong>${coinSymbol}</strong> | |
| <span class="trending-coin-fullname">${coinName}</span> | |
| </div> | |
| ${marketCapRank ? `<div class="trending-coin-meta">Market Cap Rank: #${marketCapRank}</div>` : ''} | |
| </div> | |
| </div> | |
| ${score !== null && score > 0 ? ` | |
| <div class="trending-coin-score"> | |
| <div class="trending-coin-score-value">${score.toFixed(2)}</div> | |
| <div class="trending-coin-score-label">Score</div> | |
| </div> | |
| ` : ''} | |
| </div> | |
| `; | |
| }).join('')} | |
| </div> | |
| `; | |
| } else { | |
| trendingDiv.innerHTML = '<div class="alert alert-warning">No trending data available</div>'; | |
| } | |
| } else { | |
| throw new Error('Trending endpoint not available'); | |
| } | |
| } catch (trendingError) { | |
| console.warn('Trending endpoint error:', trendingError); | |
| trendingDiv.innerHTML = '<div class="alert alert-warning">Trending data unavailable</div>'; | |
| } | |
| // Load Fear & Greed Index with enhanced visualization | |
| try { | |
| const sentimentRes = await fetch('/api/sentiment'); | |
| if (sentimentRes.ok) { | |
| const sentimentData = await sentimentRes.json(); | |
| if (sentimentData.fear_greed_index !== undefined) { | |
| const fgValue = sentimentData.fear_greed_index; | |
| const fgLabel = sentimentData.fear_greed_label || 'Unknown'; | |
| // Determine sentiment classification and colors | |
| let sentimentClass = ''; | |
| let sentimentIcon = ''; | |
| let fgColor = ''; | |
| let bgGradient = ''; | |
| let description = ''; | |
| if (fgValue >= 75) { | |
| sentimentClass = 'Extreme Greed'; | |
| sentimentIcon = '🚀'; | |
| fgColor = '#10b981'; | |
| bgGradient = 'linear-gradient(135deg, rgba(16, 185, 129, 0.2) 0%, rgba(5, 150, 105, 0.1) 100%)'; | |
| description = 'Market shows extreme greed. Consider taking profits.'; | |
| } else if (fgValue >= 50) { | |
| sentimentClass = 'Greed'; | |
| sentimentIcon = '📈'; | |
| fgColor = '#3b82f6'; | |
| bgGradient = 'linear-gradient(135deg, rgba(59, 130, 246, 0.2) 0%, rgba(37, 99, 235, 0.1) 100%)'; | |
| description = 'Market sentiment is bullish. Optimistic outlook.'; | |
| } else if (fgValue >= 25) { | |
| sentimentClass = 'Fear'; | |
| sentimentIcon = '⚠️'; | |
| fgColor = '#f59e0b'; | |
| bgGradient = 'linear-gradient(135deg, rgba(245, 158, 11, 0.2) 0%, rgba(217, 119, 6, 0.1) 100%)'; | |
| description = 'Market shows fear. Caution advised.'; | |
| } else { | |
| sentimentClass = 'Extreme Fear'; | |
| sentimentIcon = '😱'; | |
| fgColor = '#ef4444'; | |
| bgGradient = 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.1) 100%)'; | |
| description = 'Extreme fear in market. Potential buying opportunity.'; | |
| } | |
| // Calculate progress bar percentage (0-100) | |
| const progressPercent = fgValue; | |
| // Create circular gauge SVG | |
| const circumference = 2 * Math.PI * 90; // radius = 90 | |
| const offset = circumference - (progressPercent / 100) * circumference; | |
| fgDiv.innerHTML = ` | |
| <div style="display: grid; gap: 24px;"> | |
| <!-- Main Fear & Greed Index Display with Circular Gauge --> | |
| <div style="background: ${bgGradient}; border: 2px solid ${fgColor}40; border-radius: 20px; padding: 40px; text-align: center; position: relative; overflow: hidden;"> | |
| <!-- Background Pattern --> | |
| <div style="position: absolute; top: -50%; right: -50%; width: 200%; height: 200%; background: radial-gradient(circle, ${fgColor}15 0%, transparent 70%); pointer-events: none; animation: pulse 3s ease-in-out infinite;"></div> | |
| <div style="position: relative; z-index: 1;"> | |
| <!-- Circular Gauge --> | |
| <div style="position: relative; width: 240px; height: 240px; margin: 0 auto 24px;"> | |
| <svg width="240" height="240" style="transform: rotate(-90deg);"> | |
| <!-- Background Circle --> | |
| <circle cx="120" cy="120" r="90" fill="none" stroke="rgba(255, 255, 255, 0.1)" stroke-width="12" /> | |
| <!-- Progress Circle --> | |
| <circle | |
| cx="120" | |
| cy="120" | |
| r="90" | |
| fill="none" | |
| stroke="${fgColor}" | |
| stroke-width="12" | |
| stroke-linecap="round" | |
| stroke-dasharray="${circumference}" | |
| stroke-dashoffset="${offset}" | |
| style="transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1); filter: drop-shadow(0 0 10px ${fgColor}60);" | |
| /> | |
| </svg> | |
| <!-- Center Content --> | |
| <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;"> | |
| <div style="font-size: 48px; margin-bottom: 8px; filter: drop-shadow(0 2px 4px ${fgColor}40);"> | |
| ${sentimentIcon} | |
| </div> | |
| <div style="font-size: 56px; font-weight: 900; line-height: 1; background: linear-gradient(135deg, ${fgColor}, ${fgColor}dd); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;"> | |
| ${fgValue} | |
| </div> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">/ 100</div> | |
| </div> | |
| </div> | |
| <!-- Classification Label --> | |
| <div style="font-size: 28px; font-weight: 700; color: ${fgColor}; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 20px ${fgColor}40;"> | |
| ${sentimentClass} | |
| </div> | |
| <!-- Description --> | |
| <div style="font-size: 14px; color: var(--text-secondary); max-width: 400px; margin: 0 auto; line-height: 1.6;"> | |
| ${description} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Progress Bar Visualization --> | |
| <div> | |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;"> | |
| <span style="font-size: 14px; font-weight: 600; color: var(--text-secondary);">Fear & Greed Index</span> | |
| <span style="font-size: 14px; font-weight: 700; color: ${fgColor};">${fgValue}/100</span> | |
| </div> | |
| <!-- Progress Bar Container --> | |
| <div class="sentiment-progress-bar"> | |
| <!-- Progress Fill with Gradient --> | |
| <div class="sentiment-progress-fill" style="width: ${progressPercent}%; background: linear-gradient(90deg, ${fgColor} 0%, ${fgColor}dd 100%); box-shadow: 0 0 20px ${fgColor}40;"> | |
| <div style="position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: 4px; height: 60%; background: rgba(255, 255, 255, 0.5); border-radius: 2px;"></div> | |
| </div> | |
| <!-- Scale Markers --> | |
| <div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 8px; pointer-events: none;"> | |
| <span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">0</span> | |
| <span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">25</span> | |
| <span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">50</span> | |
| <span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">75</span> | |
| <span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">100</span> | |
| </div> | |
| </div> | |
| <!-- Scale Labels --> | |
| <div style="display: flex; justify-content: space-between; margin-top: 8px; font-size: 11px; color: var(--text-secondary);"> | |
| <span>😱 Extreme Fear</span> | |
| <span>⚠️ Fear</span> | |
| <span>😐 Neutral</span> | |
| <span>📈 Greed</span> | |
| <span>🚀 Extreme Greed</span> | |
| </div> | |
| </div> | |
| <!-- Additional Info Grid --> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-top: 8px;"> | |
| <div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);"> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Current Value</div> | |
| <div style="font-size: 24px; font-weight: 700; color: ${fgColor};">${fgValue}</div> | |
| </div> | |
| <div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);"> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Classification</div> | |
| <div style="font-size: 14px; font-weight: 600; color: var(--text-primary);">${fgLabel}</div> | |
| </div> | |
| ${sentimentData.timestamp ? ` | |
| <div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);"> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Last Update</div> | |
| <div style="font-size: 12px; font-weight: 600; color: var(--text-primary);">${new Date(sentimentData.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}</div> | |
| </div> | |
| ` : ''} | |
| <div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);"> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Source</div> | |
| <div style="font-size: 12px; font-weight: 600; color: var(--text-primary);">Alternative.me</div> | |
| </div> | |
| </div> | |
| <!-- Market Interpretation --> | |
| <div style="padding: 20px; background: rgba(0, 0, 0, 0.3); border-radius: 12px; border-left: 4px solid ${fgColor};"> | |
| <div style="display: flex; align-items: start; gap: 12px;"> | |
| <div style="font-size: 24px;">💡</div> | |
| <div> | |
| <div style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">Market Interpretation</div> | |
| <div style="font-size: 13px; color: var(--text-secondary); line-height: 1.6;"> | |
| ${fgValue >= 75 ? | |
| 'The market is showing extreme greed. Historically, this may indicate a potential market top. Consider taking profits and being cautious with new positions.' : | |
| fgValue >= 50 ? | |
| 'Market sentiment is positive with greed prevailing. This suggests bullish momentum, but monitor for overbought conditions.' : | |
| fgValue >= 25 ? | |
| 'Fear is present in the market. This could indicate a buying opportunity for long-term investors, but exercise caution.' : | |
| 'Extreme fear dominates the market. Historically, this has often been a good time to buy, but ensure you have a solid risk management strategy.'} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } else { | |
| fgDiv.innerHTML = '<div class="alert alert-warning">Fear & Greed Index unavailable</div>'; | |
| } | |
| } else { | |
| throw new Error('Sentiment endpoint not available'); | |
| } | |
| } catch (fgError) { | |
| console.warn('Fear & Greed endpoint error:', fgError); | |
| fgDiv.innerHTML = ` | |
| <div style="padding: 40px; text-align: center;"> | |
| <div style="font-size: 48px; margin-bottom: 16px;">⚠️</div> | |
| <div style="font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">Fear & Greed Index Unavailable</div> | |
| <div style="font-size: 14px; color: var(--text-secondary);">Unable to fetch sentiment data at this time. Please try again later.</div> | |
| </div> | |
| `; | |
| } | |
| console.log('✅ Market data loaded successfully'); | |
| } catch (error) { | |
| console.error('❌ Error loading market data:', error); | |
| getToast().error('Failed to load market data'); | |
| if (marketDiv) marketDiv.innerHTML = `<div class="alert alert-error">Error loading market data: ${error.message}</div>`; | |
| if (trendingDiv) trendingDiv.innerHTML = '<div class="alert alert-error">Error loading trending coins</div>'; | |
| if (fgDiv) fgDiv.innerHTML = '<div class="alert alert-error">Error loading Fear & Greed Index</div>'; | |
| } | |
| } | |
| // ============================================================================= | |
| // News Loading (FIXED) | |
| // ============================================================================= | |
| async function fetchNewsFromAPI() { | |
| console.log('📥 Fetching news from CryptoCompare API...'); | |
| getToast().info('Fetching latest news from CryptoCompare...'); | |
| const newsListDiv = document.getElementById('news-list'); | |
| if (!newsListDiv) return; | |
| newsListDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Fetching news from CryptoCompare API...</div>'; | |
| try { | |
| const response = await fetch('/api/news/fetch?limit=50', { method: 'POST' }); | |
| if (!response.ok) { | |
| throw new Error(`Fetch API returned ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| console.log('Fetch news result:', data); | |
| if (data.success) { | |
| getToast().success(`Successfully fetched and saved ${data.saved} news articles!`); | |
| // Reload news list | |
| loadNews(); | |
| } else { | |
| getToast().error(`Failed to fetch news: ${data.error}`); | |
| newsListDiv.innerHTML = `<div class="alert alert-error">Error: ${data.error}</div>`; | |
| } | |
| } catch (error) { | |
| console.error('❌ Error fetching news:', error); | |
| getToast().error('Failed to fetch news from API'); | |
| newsListDiv.innerHTML = `<div class="alert alert-error">Error fetching news: ${error.message}</div>`; | |
| } | |
| } | |
| // Utility function to format time ago | |
| function formatTimeAgo(dateString) { | |
| if (!dateString) return 'Unknown time'; | |
| const date = new Date(dateString); | |
| const now = new Date(); | |
| const diffMs = now - date; | |
| const diffSecs = Math.floor(diffMs / 1000); | |
| const diffMins = Math.floor(diffSecs / 60); | |
| const diffHours = Math.floor(diffMins / 60); | |
| const diffDays = Math.floor(diffHours / 24); | |
| if (diffSecs < 60) return 'Just now'; | |
| if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`; | |
| if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; | |
| if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined }); | |
| } | |
| async function loadNews() { | |
| console.log('📰 Loading news...'); | |
| const newsListDiv = document.getElementById('news-list'); | |
| if (!newsListDiv) return; | |
| newsListDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading news...</div>'; | |
| try { | |
| const response = await fetch('/api/news?limit=50'); | |
| if (!response.ok) { | |
| throw new Error(`News API returned ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| console.log('News data:', data); | |
| if (data.success && data.news && data.news.length > 0) { | |
| // Sort by latest timestamp (published_date or analyzed_at) | |
| const sortedNews = [...data.news].sort((a, b) => { | |
| const dateA = new Date(a.published_date || a.analyzed_at || 0); | |
| const dateB = new Date(b.published_date || b.analyzed_at || 0); | |
| return dateB - dateA; | |
| }); | |
| newsListDiv.innerHTML = ` | |
| <div style="display: grid; gap: 20px;"> | |
| ${sortedNews.map(article => { | |
| const timeAgo = formatTimeAgo(article.published_date || article.analyzed_at); | |
| const symbols = Array.isArray(article.related_symbols) | |
| ? article.related_symbols | |
| : (typeof article.related_symbols === 'string' | |
| ? (article.related_symbols.startsWith('[') | |
| ? JSON.parse(article.related_symbols) | |
| : article.related_symbols.split(',').map(s => s.trim())) | |
| : []); | |
| return ` | |
| <div class="news-card" onclick="${article.url ? `window.open('${article.url}', '_blank')` : ''}"> | |
| <div class="news-card-image"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path> | |
| <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path> | |
| </svg> | |
| </div> | |
| <div class="news-card-content"> | |
| <h4 class="news-card-title"> | |
| ${article.url ? `<a href="${article.url}" target="_blank">${article.title || 'Untitled'}</a>` : (article.title || 'Untitled')} | |
| </h4> | |
| ${article.content ? `<p class="news-card-excerpt">${article.content.substring(0, 200)}${article.content.length > 200 ? '...' : ''}</p>` : ''} | |
| <div class="news-card-meta"> | |
| <div class="news-card-source"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path> | |
| <polyline points="15 3 21 3 21 9"></polyline> | |
| <line x1="10" y1="14" x2="21" y2="3"></line> | |
| </svg> | |
| ${article.source || 'Unknown'} | |
| </div> | |
| <div class="news-card-time"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <polyline points="12 6 12 12 16 14"></polyline> | |
| </svg> | |
| <span class="time-ago">${timeAgo}</span> | |
| </div> | |
| ${article.sentiment_label ? `<span class="sentiment-badge ${article.sentiment_label.toLowerCase()}">${article.sentiment_label}</span>` : ''} | |
| </div> | |
| ${symbols.length > 0 ? ` | |
| <div class="news-card-symbols" style="margin-top: 10px;"> | |
| ${symbols.slice(0, 5).map(symbol => `<span class="symbol-badge">${symbol}</span>`).join('')} | |
| ${symbols.length > 5 ? `<span class="symbol-badge">+${symbols.length - 5}</span>` : ''} | |
| </div> | |
| ` : ''} | |
| </div> | |
| </div> | |
| `; | |
| }).join('')} | |
| </div> | |
| <div style="margin-top: 20px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px; text-align: center; font-size: 13px; color: var(--text-secondary);"> | |
| Showing ${sortedNews.length} article${sortedNews.length !== 1 ? 's' : ''}${data.source ? ` from ${data.source}` : ''} | |
| </div> | |
| `; | |
| console.log('✅ News loaded successfully'); | |
| getToast().success(`Loaded ${sortedNews.length} news articles`); | |
| } else { | |
| newsListDiv.innerHTML = '<div class="alert alert-warning">No news articles available at the moment. Click "Fetch Latest News" to load articles.</div>'; | |
| console.warn('No news data available'); | |
| } | |
| } catch (error) { | |
| console.error('❌ Error loading news:', error); | |
| getToast().error('Failed to load news'); | |
| newsListDiv.innerHTML = `<div class="alert alert-error">Error loading news: ${error.message}<br>Please check your internet connection and try again.</div>`; | |
| } | |
| } | |
| // ============================================================================= | |
| // Models Loading | |
| // ============================================================================= | |
| async function loadModels() { | |
| console.log('🤖 Loading models...'); | |
| const modelsStatusDiv = document.getElementById('models-status'); | |
| const modelsListDiv = document.getElementById('models-list'); | |
| if (modelsStatusDiv) modelsStatusDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading models status...</div>'; | |
| if (modelsListDiv) modelsListDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Loading models list...</div>'; | |
| try { | |
| // Load models status | |
| const statusRes = await fetch('/api/models/status'); | |
| if (statusRes.ok) { | |
| const statusData = await statusRes.json(); | |
| // Handle different response formats from API | |
| const isOk = statusData.ok || statusData.success || statusData.status === 'ok'; | |
| const pipelinesLoaded = statusData.pipelines_loaded || statusData.models_loaded || 0; | |
| const availableModels = statusData.available_models || statusData.loaded_models || []; | |
| const modelsCount = Array.isArray(availableModels) ? availableModels.length : (availableModels || 0); | |
| const hfMode = statusData.hf_mode || 'unknown'; | |
| const transformersAvailable = statusData.transformers_available !== undefined ? statusData.transformers_available : false; | |
| const statusMessage = statusData.status_message || (isOk ? 'Active' : 'Partial'); | |
| // Determine if lazy loading is active | |
| const isLazyLoading = hfMode === 'public' && pipelinesLoaded === 0 && modelsCount > 0; | |
| const alertClass = isLazyLoading ? 'alert-info' : (isOk ? 'alert-success' : 'alert-warning'); | |
| const statusIcon = isLazyLoading ? 'ℹ️' : (isOk ? '✅' : '⚠️'); | |
| modelsStatusDiv.innerHTML = ` | |
| <div class="alert ${alertClass}"> | |
| <strong>Status:</strong> ${statusIcon} ${statusMessage}<br> | |
| <strong>Models Configured:</strong> ${modelsCount} (Lazy Loading)<br> | |
| <strong>Models Loaded:</strong> ${pipelinesLoaded}<br> | |
| <strong>HF Mode:</strong> ${hfMode}<br> | |
| <strong>Transformers:</strong> ${transformersAvailable ? '✅ Available' : '❌ Not Available'} | |
| ${isLazyLoading ? '<br><br><em>💡 Models will load automatically on first use (sentiment analysis, etc.)</em>' : ''} | |
| </div> | |
| `; | |
| // Update stat-models in dashboard and sidebar with actual loaded count | |
| const modelsStatEl = document.getElementById('stat-models'); | |
| const sidebarModelsEl = document.getElementById('sidebar-models'); | |
| // Use pipelines_loaded if available, otherwise use modelsCount | |
| const displayCount = pipelinesLoaded > 0 ? pipelinesLoaded : modelsCount; | |
| if (modelsStatEl) { | |
| modelsStatEl.textContent = displayCount; | |
| } | |
| if (sidebarModelsEl) { | |
| sidebarModelsEl.textContent = displayCount; | |
| } | |
| } else { | |
| throw new Error('Models status endpoint not available'); | |
| } | |
| // Load models list | |
| const listRes = await fetch('/api/models/list'); | |
| if (listRes.ok) { | |
| const listData = await listRes.json(); | |
| // Update sidebar models count | |
| const sidebarModels = document.getElementById('sidebar-models'); | |
| const modelsStatEl = document.getElementById('stat-models'); | |
| const totalModels = listData.total_models || (listData.models ? listData.models.length : 0); | |
| const loadedModels = listData.models ? listData.models.filter(m => m.loaded).length : 0; | |
| if (sidebarModels) { | |
| sidebarModels.textContent = loadedModels > 0 ? loadedModels : totalModels; | |
| } | |
| if (modelsStatEl) { | |
| modelsStatEl.textContent = loadedModels > 0 ? loadedModels : totalModels; | |
| } | |
| if (listData.models && listData.models.length > 0) { | |
| modelsListDiv.innerHTML = ` | |
| <div style="overflow-x: auto;"> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Model ID</th> | |
| <th>Task</th> | |
| <th>Category</th> | |
| <th>Status</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${listData.models.map(model => { | |
| const modelId = model.model_id || model.id || model.key || 'N/A'; | |
| const task = model.task || 'N/A'; | |
| const category = model.category || 'N/A'; | |
| const isLoaded = model.loaded === true; | |
| const hasError = model.error && model.error.length > 0; | |
| const statusClass = isLoaded ? 'available' : (hasError ? 'error' : 'standby'); | |
| const statusText = isLoaded ? '✅ Loaded' : (hasError ? '❌ Error' : '⏸️ Standby'); | |
| return ` | |
| <tr> | |
| <td><strong>${modelId}</strong></td> | |
| <td>${task}</td> | |
| <td>${category}</td> | |
| <td><span class="model-status ${statusClass}">${statusText}</span></td> | |
| </tr> | |
| `; | |
| }).join('')} | |
| </tbody> | |
| </table> | |
| </div> | |
| `; | |
| } else { | |
| modelsListDiv.innerHTML = '<div class="alert alert-warning">No models available</div>'; | |
| } | |
| } else { | |
| throw new Error('Models list endpoint not available'); | |
| } | |
| console.log('✅ Models loaded successfully'); | |
| } catch (error) { | |
| console.error('❌ Error loading models:', error); | |
| getToast().error('Failed to load models'); | |
| if (modelsStatusDiv) modelsStatusDiv.innerHTML = `<div class="alert alert-error">Error loading models status: ${error.message}</div>`; | |
| if (modelsListDiv) modelsListDiv.innerHTML = '<div class="alert alert-error">Error loading models list</div>'; | |
| } | |
| } | |
| async function initializeModels() { | |
| getToast().info('Initializing models... This may take a moment.'); | |
| const modelsStatusDiv = document.getElementById('models-status'); | |
| if (modelsStatusDiv) { | |
| modelsStatusDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Initializing models...</div>'; | |
| } | |
| try { | |
| const response = await fetch('/api/models/initialize', { method: 'POST' }); | |
| if (!response.ok) { | |
| throw new Error(`Initialize returned ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| // Handle different response formats | |
| const isOk = data.status === 'ok' || data.ok === true || (data.models_loaded && data.models_loaded > 0); | |
| const modelsLoaded = data.models_loaded || data.pipelines_loaded || 0; | |
| const modelsFailed = data.models_failed || data.pipelines_failed || 0; | |
| if (isOk) { | |
| getToast().success(`Models initialized successfully! ${modelsLoaded} model(s) loaded.`); | |
| } else if (modelsLoaded > 0) { | |
| getToast().warning(`Models partially initialized: ${modelsLoaded} loaded, ${modelsFailed} failed`); | |
| } else { | |
| getToast().warning('No models loaded. Using fallback mode.'); | |
| } | |
| // Reload models list and status | |
| await loadModels(); | |
| } catch (error) { | |
| console.error('Error initializing models:', error); | |
| getToast().error('Failed to initialize models: ' + error.message); | |
| if (modelsStatusDiv) { | |
| modelsStatusDiv.innerHTML = `<div class="alert alert-error">Error initializing models: ${error.message}</div>`; | |
| } | |
| } | |
| } | |
| // ============================================================================= | |
| // Settings | |
| // ============================================================================= | |
| function loadSettings() { | |
| const apiInfoDiv = document.getElementById('api-info'); | |
| if (apiInfoDiv) { | |
| apiInfoDiv.innerHTML = ` | |
| <div class="alert alert-info"> | |
| <strong>API Base URL:</strong> ${window.location.origin}<br> | |
| <strong>Documentation:</strong> <a href="/docs" target="_blank" style="color: var(--primary);">/docs</a><br> | |
| <strong>Health Check:</strong> <a href="/health" target="_blank" style="color: var(--primary);">/health</a> | |
| </div> | |
| `; | |
| } | |
| } | |
| function saveSettings() { | |
| getToast().success('Settings saved successfully!'); | |
| } | |
| function toggleTheme() { | |
| document.body.classList.toggle('light-theme'); | |
| const themeSelect = document.getElementById('theme-select'); | |
| if (themeSelect) { | |
| themeSelect.value = document.body.classList.contains('light-theme') ? 'light' : 'dark'; | |
| } | |
| } | |
| function changeTheme(theme) { | |
| if (theme === 'light') { | |
| document.body.classList.add('light-theme'); | |
| } else { | |
| document.body.classList.remove('light-theme'); | |
| } | |
| } | |
| // ============================================================================= | |
| // Sentiment Analysis Functions with Visualizations | |
| // ============================================================================= | |
| // Create sentiment gauge chart | |
| function createSentimentGauge(containerId, sentimentValue, sentimentClass) { | |
| const container = document.getElementById(containerId); | |
| if (!container) return null; | |
| // Clear previous chart | |
| container.innerHTML = ''; | |
| // Create canvas | |
| const canvas = document.createElement('canvas'); | |
| canvas.id = `gauge-${containerId}`; | |
| canvas.width = 300; | |
| canvas.height = 150; | |
| container.appendChild(canvas); | |
| // Calculate gauge value (0-100, where 50 is neutral) | |
| let gaugeValue = 50; // neutral | |
| if (sentimentClass === 'bullish' || sentimentClass === 'positive') { | |
| gaugeValue = 50 + (sentimentValue * 50); // 50-100 | |
| } else if (sentimentClass === 'bearish' || sentimentClass === 'negative') { | |
| gaugeValue = 50 - (sentimentValue * 50); // 0-50 | |
| } | |
| gaugeValue = Math.max(0, Math.min(100, gaugeValue)); | |
| const ctx = canvas.getContext('2d'); | |
| const centerX = canvas.width / 2; | |
| const centerY = canvas.height / 2; | |
| const radius = 60; | |
| // Draw gauge background (semi-circle) | |
| ctx.beginPath(); | |
| ctx.arc(centerX, centerY + 20, radius, Math.PI, 0, false); | |
| ctx.lineWidth = 20; | |
| ctx.strokeStyle = 'rgba(31, 41, 55, 0.6)'; | |
| ctx.stroke(); | |
| // Draw gauge fill | |
| const startAngle = Math.PI; | |
| const endAngle = Math.PI + (Math.PI * (gaugeValue / 100)); | |
| ctx.beginPath(); | |
| ctx.arc(centerX, centerY + 20, radius, startAngle, endAngle, false); | |
| ctx.lineWidth = 20; | |
| ctx.lineCap = 'round'; | |
| let gaugeColor; | |
| if (gaugeValue >= 70) gaugeColor = '#10b981'; // green | |
| else if (gaugeValue >= 50) gaugeColor = '#3b82f6'; // blue | |
| else if (gaugeValue >= 30) gaugeColor = '#f59e0b'; // yellow | |
| else gaugeColor = '#ef4444'; // red | |
| ctx.strokeStyle = gaugeColor; | |
| ctx.stroke(); | |
| // Draw value text | |
| ctx.fillStyle = '#f9fafb'; | |
| ctx.font = 'bold 32px Inter, sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText(Math.round(gaugeValue), centerX, centerY + 15); | |
| // Draw labels | |
| ctx.fillStyle = '#9ca3af'; | |
| ctx.font = '12px Inter, sans-serif'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('Bearish', 20, centerY + 50); | |
| ctx.textAlign = 'right'; | |
| ctx.fillText('Bullish', canvas.width - 20, centerY + 50); | |
| return canvas; | |
| } | |
| // Get trend arrow SVG | |
| function getTrendArrow(sentimentClass) { | |
| const color = sentimentClass === 'bullish' ? 'var(--success)' : | |
| sentimentClass === 'bearish' ? 'var(--danger)' : 'var(--warning)'; | |
| const rotation = sentimentClass === 'bearish' ? 'rotate(180deg)' : | |
| sentimentClass === 'neutral' ? 'rotate(90deg)' : ''; | |
| return ` | |
| <svg class="sentiment-trend-arrow ${sentimentClass}" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="${color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform: ${rotation};"> | |
| <polyline points="18 15 12 9 6 15"></polyline> | |
| </svg> | |
| `; | |
| } | |
| // Create confidence bar | |
| function createConfidenceBar(confidence) { | |
| const confidencePercent = Math.round(confidence * 100); | |
| return ` | |
| <div class="confidence-bar-container"> | |
| <div class="confidence-bar-label"> | |
| <span>Model Confidence</span> | |
| <span>${confidencePercent}%</span> | |
| </div> | |
| <div class="confidence-bar"> | |
| <div class="confidence-bar-fill" style="width: ${confidencePercent}%;"></div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| async function analyzeGlobalSentiment() { | |
| getToast().info('Analyzing global market sentiment...'); | |
| const resultDiv = document.getElementById('global-sentiment-result'); | |
| if (resultDiv) { | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>'; | |
| } | |
| try { | |
| const response = await fetch('/api/sentiment/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| text: 'Overall cryptocurrency market sentiment analysis', | |
| mode: 'crypto' | |
| }) | |
| }); | |
| if (!response.ok) throw new Error(`API returned ${response.status}`); | |
| const data = await response.json(); | |
| if (data.available && data.sentiment) { | |
| const sentiment = data.sentiment.toUpperCase(); | |
| const confidence = data.confidence || 0; | |
| const sentimentClass = sentiment.includes('POSITIVE') || sentiment.includes('BULLISH') ? 'bullish' : | |
| sentiment.includes('NEGATIVE') || sentiment.includes('BEARISH') ? 'bearish' : 'neutral'; | |
| resultDiv.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <div class="ai-result-header"> | |
| <h4>Global Market Sentiment</h4> | |
| <span class="sentiment-badge ${sentimentClass}">${sentiment}</span> | |
| </div> | |
| <div class="sentiment-gauge-container" id="global-sentiment-gauge"></div> | |
| <div style="text-align: center; margin: 20px 0;"> | |
| ${getTrendArrow(sentimentClass)} | |
| <span style="font-size: 18px; font-weight: 700; color: var(--${sentimentClass === 'bullish' ? 'success' : sentimentClass === 'bearish' ? 'danger' : 'warning'}); margin: 0 10px;"> | |
| ${sentiment} | |
| </span> | |
| ${getTrendArrow(sentimentClass)} | |
| </div> | |
| ${createConfidenceBar(confidence)} | |
| <p style="margin-top: 15px; color: var(--text-secondary); text-align: center;"> | |
| <strong>Model:</strong> ${data.model || 'AI Sentiment Analysis'} | | |
| <strong>Engine:</strong> ${data.engine || 'N/A'} | |
| </p> | |
| </div> | |
| `; | |
| // Create gauge chart after DOM update | |
| setTimeout(() => { | |
| createSentimentGauge('global-sentiment-gauge', confidence, sentimentClass); | |
| }, 100); | |
| getToast().success('Sentiment analysis complete!'); | |
| } else { | |
| resultDiv.innerHTML = '<div class="alert alert-warning">Sentiment analysis unavailable</div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error analyzing sentiment:', error); | |
| resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; | |
| getToast().error('Failed to analyze sentiment'); | |
| } | |
| } | |
| async function analyzeAssetSentiment() { | |
| const symbol = document.getElementById('asset-symbol')?.value; | |
| const text = document.getElementById('asset-sentiment-text')?.value; | |
| if (!symbol) { | |
| getToast().warning('Please select a trading pair'); | |
| return; | |
| } | |
| getToast().info('Analyzing asset sentiment...'); | |
| const resultDiv = document.getElementById('asset-sentiment-result'); | |
| if (resultDiv) { | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>'; | |
| } | |
| try { | |
| const response = await fetch('/api/sentiment/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| text: text || `Sentiment analysis for ${symbol}`, | |
| mode: 'crypto', | |
| symbol: symbol | |
| }) | |
| }); | |
| if (!response.ok) throw new Error(`API returned ${response.status}`); | |
| const data = await response.json(); | |
| if (data.available && data.sentiment) { | |
| const sentiment = data.sentiment.toUpperCase(); | |
| const confidence = data.confidence || 0; | |
| const sentimentClass = sentiment.includes('POSITIVE') || sentiment.includes('BULLISH') ? 'bullish' : | |
| sentiment.includes('NEGATIVE') || sentiment.includes('BEARISH') ? 'bearish' : 'neutral'; | |
| resultDiv.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <div class="ai-result-header"> | |
| <h4>${symbol} Sentiment</h4> | |
| <span class="sentiment-badge ${sentimentClass}">${sentiment}</span> | |
| </div> | |
| <div class="sentiment-gauge-container" id="asset-sentiment-gauge"></div> | |
| <div style="text-align: center; margin: 20px 0;"> | |
| ${getTrendArrow(sentimentClass)} | |
| <span style="font-size: 18px; font-weight: 700; color: var(--${sentimentClass === 'bullish' ? 'success' : sentimentClass === 'bearish' ? 'danger' : 'warning'}); margin: 0 10px;"> | |
| ${sentiment} | |
| </span> | |
| ${getTrendArrow(sentimentClass)} | |
| </div> | |
| ${createConfidenceBar(confidence)} | |
| <p style="margin-top: 15px; color: var(--text-secondary); text-align: center;"> | |
| <strong>Model:</strong> ${data.model || 'AI Sentiment Analysis'} | | |
| <strong>Engine:</strong> ${data.engine || 'N/A'} | |
| </p> | |
| </div> | |
| `; | |
| // Create gauge chart after DOM update | |
| setTimeout(() => { | |
| createSentimentGauge('asset-sentiment-gauge', confidence, sentimentClass); | |
| }, 100); | |
| getToast().success('Asset sentiment analysis complete!'); | |
| } else { | |
| resultDiv.innerHTML = '<div class="alert alert-warning">Sentiment analysis unavailable</div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error analyzing asset sentiment:', error); | |
| resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; | |
| getToast().error('Failed to analyze asset sentiment'); | |
| } | |
| } | |
| async function analyzeSentiment() { | |
| const text = document.getElementById('sentiment-text')?.value; | |
| const mode = document.getElementById('sentiment-mode')?.value || 'auto'; | |
| if (!text || text.trim() === '') { | |
| getToast().warning('Please enter text to analyze'); | |
| return; | |
| } | |
| getToast().info('Analyzing sentiment...'); | |
| const resultDiv = document.getElementById('sentiment-result'); | |
| if (resultDiv) { | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>'; | |
| } | |
| try { | |
| const response = await fetch('/api/sentiment/analyze', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text, mode }) | |
| }); | |
| if (!response.ok) throw new Error(`API returned ${response.status}`); | |
| const data = await response.json(); | |
| if (data.available && data.sentiment) { | |
| const sentiment = data.sentiment.toUpperCase(); | |
| const confidence = data.confidence || 0; | |
| const sentimentClass = sentiment.includes('POSITIVE') || sentiment.includes('BULLISH') ? 'bullish' : | |
| sentiment.includes('NEGATIVE') || sentiment.includes('BEARISH') ? 'bearish' : 'neutral'; | |
| resultDiv.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <div class="ai-result-header"> | |
| <h4>Sentiment Analysis Result</h4> | |
| <span class="sentiment-badge ${sentimentClass}">${sentiment}</span> | |
| </div> | |
| <div class="sentiment-gauge-container" id="sentiment-gauge"></div> | |
| <div style="text-align: center; margin: 20px 0;"> | |
| ${getTrendArrow(sentimentClass)} | |
| <span style="font-size: 18px; font-weight: 700; color: var(--${sentimentClass === 'bullish' ? 'success' : sentimentClass === 'bearish' ? 'danger' : 'warning'}); margin: 0 10px;"> | |
| ${sentiment} | |
| </span> | |
| ${getTrendArrow(sentimentClass)} | |
| </div> | |
| ${createConfidenceBar(confidence)} | |
| <p style="margin-top: 15px; color: var(--text-secondary); text-align: center;"> | |
| <strong>Text:</strong> ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}<br> | |
| <strong>Model:</strong> ${data.model || 'AI Sentiment Analysis'} | | |
| <strong>Engine:</strong> ${data.engine || 'N/A'} | |
| </p> | |
| </div> | |
| `; | |
| // Create gauge chart after DOM update | |
| setTimeout(() => { | |
| createSentimentGauge('sentiment-gauge', confidence, sentimentClass); | |
| }, 100); | |
| getToast().success('Sentiment analysis complete!'); | |
| } else { | |
| resultDiv.innerHTML = '<div class="alert alert-warning">Sentiment analysis unavailable</div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error analyzing sentiment:', error); | |
| resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; | |
| getToast().error('Failed to analyze sentiment'); | |
| } | |
| } | |
| // ============================================================================= | |
| // Trading Assistant | |
| // ============================================================================= | |
| async function runTradingAssistant() { | |
| const symbol = document.getElementById('trading-symbol')?.value; | |
| const context = document.getElementById('trading-context')?.value; | |
| if (!symbol) { | |
| getToast().warning('Please select a trading symbol'); | |
| return; | |
| } | |
| getToast().info('Generating trading signal...'); | |
| const resultDiv = document.getElementById('trading-assistant-result'); | |
| if (resultDiv) { | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>'; | |
| } | |
| try { | |
| const response = await fetch('/api/trading/decision', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| symbol: symbol, | |
| context: context || `Trading decision for ${symbol}` | |
| }) | |
| }); | |
| if (!response.ok) throw new Error(`API returned ${response.status}`); | |
| const data = await response.json(); | |
| if (data.decision) { | |
| const decision = data.decision.toUpperCase(); | |
| const confidence = data.confidence ? (data.confidence * 100).toFixed(2) : 'N/A'; | |
| const decisionClass = decision === 'BUY' ? 'bullish' : decision === 'SELL' ? 'bearish' : 'neutral'; | |
| resultDiv.innerHTML = ` | |
| <div class="ai-result-card"> | |
| <div class="ai-result-header"> | |
| <h4>${symbol} Trading Signal</h4> | |
| <span class="sentiment-badge ${decisionClass}">${decision}</span> | |
| </div> | |
| <div style="display: flex; gap: 15px; justify-content: center;"> | |
| <div class="ai-result-metric"> | |
| <div class="ai-result-metric-value" style="color: var(--${decisionClass === 'bullish' ? 'success' : decisionClass === 'bearish' ? 'danger' : 'warning'});">${confidence}%</div> | |
| <div class="ai-result-metric-label">Confidence</div> | |
| </div> | |
| </div> | |
| ${data.reasoning ? `<p style="margin-top: 15px; color: var(--text-secondary);"><strong>Reasoning:</strong> ${data.reasoning}</p>` : ''} | |
| <p style="margin-top: 10px; color: var(--text-muted); font-size: 12px;"> | |
| <strong>Model:</strong> ${data.model || 'AI Trading Assistant'} | |
| </p> | |
| </div> | |
| `; | |
| getToast().success('Trading signal generated!'); | |
| } else { | |
| resultDiv.innerHTML = '<div class="alert alert-warning">Trading signal unavailable</div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error generating trading signal:', error); | |
| resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`; | |
| getToast().error('Failed to generate trading signal'); | |
| } | |
| } | |
| // ============================================================================= | |
| // Utility Functions | |
| // ============================================================================= | |
| function formatNumber(num) { | |
| if (num === null || num === undefined) return '0'; | |
| if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T'; | |
| if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B'; | |
| if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M'; | |
| if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K'; | |
| return num.toFixed(2); | |
| } | |
| // ============================================================================= | |
| // Export for global access | |
| // ============================================================================= | |
| window.AppState = AppState; | |
| // ToastManager is loaded from toast.js as window.toastManager | |
| window.toggleSidebar = toggleSidebar; | |
| window.switchTab = switchTab; | |
| window.refreshCurrentTab = refreshCurrentTab; | |
| window.loadDashboard = loadDashboard; | |
| window.loadMarketData = loadMarketData; | |
| window.loadModels = loadModels; | |
| window.initializeModels = initializeModels; | |
| window.loadNews = loadNews; | |
| window.fetchNewsFromAPI = fetchNewsFromAPI; | |
| window.loadSettings = loadSettings; | |
| window.saveSettings = saveSettings; | |
| window.toggleTheme = toggleTheme; | |
| window.changeTheme = changeTheme; | |
| window.analyzeGlobalSentiment = analyzeGlobalSentiment; | |
| window.analyzeAssetSentiment = analyzeAssetSentiment; | |
| window.analyzeSentiment = analyzeSentiment; | |
| window.runTradingAssistant = runTradingAssistant; | |
| window.formatNumber = formatNumber; | |
| // ===== DIAGNOSTICS FUNCTIONS ===== | |
| // Export diagnostic functions to window for onclick handlers | |
| async function runDiagnostic() { | |
| const runBtn = document.getElementById('run-diagnostics-btn'); | |
| const progressDiv = document.getElementById('test-progress'); | |
| const outputPre = document.getElementById('diagnostic-output'); | |
| const summaryDiv = document.getElementById('diagnostic-summary'); | |
| // Disable button and show progress | |
| runBtn.disabled = true; | |
| runBtn.textContent = 'Running...'; | |
| progressDiv.style.display = 'block'; | |
| summaryDiv.style.display = 'none'; | |
| outputPre.textContent = ''; | |
| try { | |
| const response = await fetch('/api/diagnostics/run-test', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| } | |
| }); | |
| const data = await response.json(); | |
| // Display output with color coding | |
| outputPre.innerHTML = colorCodeOutput(data.output); | |
| // Update summary | |
| updateDiagnosticSummary(data); | |
| // Store last run time | |
| localStorage.setItem('lastDiagnosticRun', data.timestamp); | |
| // Update status cards | |
| updateStatusCards(data.summary); | |
| // Show summary | |
| summaryDiv.style.display = 'block'; | |
| // Auto-scroll to bottom | |
| outputPre.scrollTop = outputPre.scrollHeight; | |
| } catch (error) { | |
| console.error('Diagnostic error:', error); | |
| outputPre.innerHTML = `<span style="color: #ef4444;">❌ Error running diagnostic: ${error.message}</span>`; | |
| showToast('❌ Diagnostic failed: ' + error.message, 'error'); | |
| } finally { | |
| // Re-enable button | |
| runBtn.disabled = false; | |
| runBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 6px;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>▶️ Run Full Diagnostic'; | |
| progressDiv.style.display = 'none'; | |
| } | |
| } | |
| function colorCodeOutput(output) { | |
| if (!output) return ''; | |
| return output | |
| .replace(/✅/g, '<span style="color: #10b981;">✅</span>') | |
| .replace(/❌/g, '<span style="color: #ef4444;">❌</span>') | |
| .replace(/⚠️/g, '<span style="color: #f59e0b;">⚠️</span>') | |
| .replace(/🔍/g, '<span style="color: #3b82f6;">🔍</span>') | |
| .replace(/📦/g, '<span style="color: #8b5cf6;">📦</span>') | |
| .replace(/🌐/g, '<span style="color: #06b6d4;">🌐</span>') | |
| .replace(/🧪/g, '<span style="color: #84cc16;">🧪</span>') | |
| .replace(/📄/g, '<span style="color: #f97316;">📄</span>') | |
| .replace(/💡/g, '<span style="color: #eab308;">💡</span>') | |
| .replace(/⏭️/g, '<span style="color: #6b7280;">⏭️</span>') | |
| .split('\n').join('<br>'); | |
| } | |
| function updateDiagnosticSummary(data) { | |
| document.getElementById('summary-duration').textContent = `${data.duration_seconds}s`; | |
| document.getElementById('summary-passed').textContent = data.summary.transformers_available && data.summary.hf_hub_connected ? '2/2' : '1/2'; | |
| document.getElementById('summary-failed').textContent = (!data.summary.transformers_available || !data.summary.hf_hub_connected) ? '1/2' : '0/2'; | |
| document.getElementById('summary-critical').textContent = data.summary.critical_issues.length; | |
| const fixesDiv = document.getElementById('suggested-fixes'); | |
| if (data.summary.critical_issues.length > 0) { | |
| fixesDiv.innerHTML = '<h4>🔧 Suggested Fixes:</h4><ul>' + | |
| data.summary.critical_issues.map(issue => | |
| `<li>${issue}</li>` | |
| ).join('') + '</ul>'; | |
| } else { | |
| fixesDiv.innerHTML = '<p style="color: #10b981;">✅ No critical issues found</p>'; | |
| } | |
| } | |
| function updateStatusCards(summary) { | |
| const transformersEl = document.getElementById('transformers-status-value'); | |
| if (transformersEl) { | |
| transformersEl.textContent = summary.transformers_available ? 'Available' : 'Not Available'; | |
| transformersEl.style.color = summary.transformers_available ? 'var(--success)' : 'var(--danger)'; | |
| } | |
| const hfEl = document.getElementById('hf-status-value'); | |
| if (hfEl) { | |
| hfEl.textContent = summary.hf_hub_connected ? 'Connected' : 'Disconnected'; | |
| hfEl.style.color = summary.hf_hub_connected ? 'var(--success)' : 'var(--danger)'; | |
| } | |
| const modelsEl = document.getElementById('models-status-value'); | |
| if (modelsEl) { | |
| modelsEl.textContent = summary.models_loaded || 0; | |
| } | |
| const lastRun = localStorage.getItem('lastDiagnosticRun'); | |
| const lastTestEl = document.getElementById('last-test-value'); | |
| if (lastTestEl) { | |
| lastTestEl.textContent = lastRun ? new Date(lastRun).toLocaleString() : 'Never'; | |
| } | |
| } | |
| async function refreshDiagnosticStatus() { | |
| try { | |
| // Get models status to determine transformers and HF hub status | |
| const modelsResponse = await fetch('/api/models/status'); | |
| if (modelsResponse.ok) { | |
| const modelsData = await modelsResponse.json(); | |
| // Update status cards | |
| const transformersStatusEl = document.getElementById('transformers-status-value'); | |
| const hfStatusEl = document.getElementById('hf-status-value'); | |
| const modelsLoadedEl = document.getElementById('models-status-value'); | |
| if (transformersStatusEl) { | |
| const transformersAvailable = modelsData.transformers_available || false; | |
| transformersStatusEl.textContent = transformersAvailable ? '✅ Installed' : '❌ Not Installed'; | |
| transformersStatusEl.style.color = transformersAvailable ? 'var(--success)' : 'var(--danger)'; | |
| } | |
| if (hfStatusEl) { | |
| const hfMode = modelsData.hf_mode || 'off'; | |
| const isConnected = hfMode !== 'off'; | |
| const modeText = hfMode === 'public' ? 'Public' : hfMode === 'auth' ? 'Authenticated' : 'Offline'; | |
| hfStatusEl.textContent = isConnected ? `✅ ${modeText}` : '⚠️ Offline'; | |
| hfStatusEl.style.color = isConnected ? 'var(--success)' : 'var(--warning)'; | |
| } | |
| if (modelsLoadedEl) { | |
| const modelsLoaded = modelsData.models_loaded || 0; | |
| const modelsFailed = modelsData.models_failed || 0; | |
| if (modelsLoaded > 0) { | |
| modelsLoadedEl.textContent = `${modelsLoaded} Ready`; | |
| modelsLoadedEl.style.color = 'var(--success)'; | |
| } else if (modelsFailed > 0) { | |
| modelsLoadedEl.textContent = `${modelsFailed} Failed`; | |
| modelsLoadedEl.style.color = 'var(--danger)'; | |
| } else { | |
| modelsLoadedEl.textContent = '0'; | |
| modelsLoadedEl.style.color = 'var(--text-secondary)'; | |
| } | |
| } | |
| } | |
| // Update the last test time | |
| const lastRun = localStorage.getItem('lastDiagnosticRun'); | |
| const lastTestEl = document.getElementById('last-test-value'); | |
| if (lastTestEl) { | |
| lastTestEl.textContent = lastRun ? new Date(lastRun).toLocaleString() : 'Never'; | |
| } | |
| getToast().success('Status refreshed'); | |
| } catch (error) { | |
| console.error('Error refreshing status:', error); | |
| getToast().error('Failed to refresh status'); | |
| } | |
| } | |
| function downloadDiagnosticLog() { | |
| const output = document.getElementById('diagnostic-output').textContent; | |
| if (!output.trim()) { | |
| showToast('❌ No diagnostic output to download', 'warning'); | |
| return; | |
| } | |
| const blob = new Blob([output], { type: 'text/plain' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `diagnostic-log-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.txt`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| showToast('✅ Log downloaded', 'success'); | |
| } | |
| // Note: Diagnostics initialization is handled in the enhanced section below | |
| // ===== ENHANCED DIAGNOSTIC FUNCTIONS ===== | |
| let autoRefreshInterval = null; | |
| let autoRefreshEnabled = false; | |
| function toggleAutoRefresh() { | |
| autoRefreshEnabled = !autoRefreshEnabled; | |
| const btn = document.getElementById('auto-refresh-btn'); | |
| if (autoRefreshEnabled) { | |
| btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 4px;"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>Auto: ON (30s)'; | |
| btn.style.background = 'rgba(16, 185, 129, 0.2)'; | |
| btn.style.borderColor = 'var(--success)'; | |
| autoRefreshInterval = setInterval(() => { | |
| refreshDiagnosticStatus(); | |
| loadSystemHealth(); | |
| loadProviderHealth(); | |
| }, 30000); | |
| getToast().success('Auto-refresh enabled (30s interval)'); | |
| } else { | |
| btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 4px;"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>Auto: OFF'; | |
| btn.style.background = ''; | |
| btn.style.borderColor = ''; | |
| if (autoRefreshInterval) { | |
| clearInterval(autoRefreshInterval); | |
| autoRefreshInterval = null; | |
| } | |
| getToast().info('Auto-refresh disabled'); | |
| } | |
| } | |
| function switchDiagnosticTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.diagnostic-tab-content').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.diagnostic-tab-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| // Show selected tab | |
| document.getElementById(`diagnostic-tab-${tabName}`).classList.add('active'); | |
| document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); | |
| // Load content if needed | |
| if (tabName === 'health' && document.getElementById('health-details-content').innerHTML.includes('Click')) { | |
| loadSystemHealth(); | |
| } else if (tabName === 'logs') { | |
| loadRecentLogs(); | |
| } | |
| } | |
| async function loadSystemHealth() { | |
| try { | |
| const response = await fetch('/api/diagnostics/health'); | |
| if (!response.ok) throw new Error('Failed to fetch health data'); | |
| const data = await response.json(); | |
| const container = document.getElementById('system-health-overview'); | |
| if (!container) return; | |
| const providers = data.providers?.summary || {}; | |
| const models = data.models?.summary || {}; | |
| const overall = data.overall_health || {}; | |
| container.innerHTML = ` | |
| <div class="stat-card gradient-blue"> | |
| <div class="stat-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path> | |
| <polyline points="22 4 12 14.01 9 11.01"></polyline> | |
| </svg> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-label">Providers</div> | |
| <div class="stat-value">${providers.healthy || 0}/${providers.total || 0}</div> | |
| <div class="stat-trend" style="color: ${overall.providers_ok ? 'var(--success)' : 'var(--warning)'};"> | |
| ${overall.providers_ok ? '✅ Healthy' : '⚠️ Degraded'} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="stat-card gradient-purple"> | |
| <div class="stat-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | |
| </svg> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-label">AI Models</div> | |
| <div class="stat-value">${models.healthy || 0}/${models.total || 0}</div> | |
| <div class="stat-trend" style="color: ${overall.models_ok ? 'var(--success)' : 'var(--warning)'};"> | |
| ${overall.models_ok ? '✅ Healthy' : '⚠️ Degraded'} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="stat-card gradient-orange"> | |
| <div class="stat-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <path d="M12 6v6l4 2"></path> | |
| </svg> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-label">In Cooldown</div> | |
| <div class="stat-value">${(providers.in_cooldown || 0) + (models.in_cooldown || 0)}</div> | |
| <div class="stat-trend">${(providers.in_cooldown || 0) + (models.in_cooldown || 0) > 0 ? '⚠️ Some services cooling' : '✅ All active'}</div> | |
| </div> | |
| </div> | |
| <div class="stat-card gradient-green"> | |
| <div class="stat-icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path> | |
| </svg> | |
| </div> | |
| <div class="stat-content"> | |
| <div class="stat-label">Degraded</div> | |
| <div class="stat-value">${(providers.degraded || 0) + (models.degraded || 0)}</div> | |
| <div class="stat-trend">${(providers.degraded || 0) + (models.degraded || 0) > 0 ? '⚠️ Needs attention' : '✅ All optimal'}</div> | |
| </div> | |
| </div> | |
| `; | |
| // Update health details tab | |
| const healthDetails = document.getElementById('health-details-content'); | |
| if (healthDetails) { | |
| healthDetails.innerHTML = ` | |
| <div style="display: grid; gap: 16px;"> | |
| <div> | |
| <h4 style="margin-bottom: 12px;">Provider Health Summary</h4> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;"> | |
| <div style="padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px;"> | |
| <div style="color: var(--text-secondary); font-size: 12px;">Total</div> | |
| <div style="font-size: 24px; font-weight: 700;">${providers.total || 0}</div> | |
| </div> | |
| <div style="padding: 12px; background: rgba(16,185,129,0.1); border-radius: 8px; border: 1px solid rgba(16,185,129,0.3);"> | |
| <div style="color: var(--success); font-size: 12px;">Healthy</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--success);">${providers.healthy || 0}</div> | |
| </div> | |
| <div style="padding: 12px; background: rgba(245,158,11,0.1); border-radius: 8px; border: 1px solid rgba(245,158,11,0.3);"> | |
| <div style="color: var(--warning); font-size: 12px;">Degraded</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--warning);">${providers.degraded || 0}</div> | |
| </div> | |
| <div style="padding: 12px; background: rgba(239,68,68,0.1); border-radius: 8px; border: 1px solid rgba(239,68,68,0.3);"> | |
| <div style="color: var(--danger); font-size: 12px;">Unavailable</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--danger);">${providers.unavailable || 0}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 style="margin-bottom: 12px;">Model Health Summary</h4> | |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;"> | |
| <div style="padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px;"> | |
| <div style="color: var(--text-secondary); font-size: 12px;">Total</div> | |
| <div style="font-size: 24px; font-weight: 700;">${models.total || 0}</div> | |
| </div> | |
| <div style="padding: 12px; background: rgba(16,185,129,0.1); border-radius: 8px; border: 1px solid rgba(16,185,129,0.3);"> | |
| <div style="color: var(--success); font-size: 12px;">Healthy</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--success);">${models.healthy || 0}</div> | |
| </div> | |
| <div style="padding: 12px; background: rgba(245,158,11,0.1); border-radius: 8px; border: 1px solid rgba(245,158,11,0.3);"> | |
| <div style="color: var(--warning); font-size: 12px;">Degraded</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--warning);">${models.degraded || 0}</div> | |
| </div> | |
| <div style="padding: 12px; background: rgba(239,68,68,0.1); border-radius: 8px; border: 1px solid rgba(239,68,68,0.3);"> | |
| <div style="color: var(--danger); font-size: 12px;">Unavailable</div> | |
| <div style="font-size: 24px; font-weight: 700; color: var(--danger);">${models.unavailable || 0}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| } catch (error) { | |
| console.error('Error loading system health:', error); | |
| getToast().error('Failed to load system health'); | |
| } | |
| } | |
| // Export to window immediately | |
| window.loadSystemHealth = loadSystemHealth; | |
| async function loadProviderHealth() { | |
| try { | |
| const response = await fetch('/api/diagnostics/health'); | |
| if (!response.ok) throw new Error('Failed to fetch provider health'); | |
| const data = await response.json(); | |
| const tbody = document.getElementById('provider-health-table'); | |
| if (!tbody) return; | |
| const providers = data.providers?.entries || []; | |
| const models = data.models?.entries || []; | |
| let html = ''; | |
| // Add providers | |
| providers.slice(0, 10).forEach(entry => { | |
| const statusClass = entry.status === 'healthy' ? 'healthy' : | |
| entry.status === 'degraded' ? 'degraded' : | |
| entry.status === 'unavailable' ? 'unavailable' : 'unknown'; | |
| const lastCheck = entry.last_success ? new Date(entry.last_success * 1000).toLocaleString() : 'Never'; | |
| html += ` | |
| <tr style="border-bottom: 1px solid var(--border);"> | |
| <td style="padding: 12px;">${entry.name || entry.id}</td> | |
| <td style="padding: 12px;"><span style="color: var(--text-secondary);">Provider</span></td> | |
| <td style="padding: 12px;"><span class="health-badge ${statusClass}">${entry.status || 'unknown'}</span></td> | |
| <td style="padding: 12px; color: var(--text-secondary); font-size: 12px;">${lastCheck}</td> | |
| <td style="padding: 12px;"> | |
| ${entry.in_cooldown ? '<span style="color: var(--warning); font-size: 12px;">⏳ Cooldown</span>' : '-'} | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| // Add models | |
| models.slice(0, 10).forEach(entry => { | |
| const statusClass = entry.status === 'healthy' ? 'healthy' : | |
| entry.status === 'degraded' ? 'degraded' : | |
| entry.status === 'unavailable' ? 'unavailable' : 'unknown'; | |
| html += ` | |
| <tr style="border-bottom: 1px solid var(--border);"> | |
| <td style="padding: 12px;">${entry.name || entry.key || 'Unknown'}</td> | |
| <td style="padding: 12px;"><span style="color: var(--text-secondary);">AI Model</span></td> | |
| <td style="padding: 12px;"><span class="health-badge ${statusClass}">${entry.status || 'unknown'}</span></td> | |
| <td style="padding: 12px; color: var(--text-secondary); font-size: 12px;">-</td> | |
| <td style="padding: 12px;"> | |
| ${entry.in_cooldown ? '<span style="color: var(--warning); font-size: 12px;">⏳ Cooldown</span>' : '-'} | |
| </td> | |
| </tr> | |
| `; | |
| }); | |
| if (html === '') { | |
| html = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: var(--text-secondary);">No health data available</td></tr>'; | |
| } | |
| tbody.innerHTML = html; | |
| } catch (error) { | |
| console.error('Error loading provider health:', error); | |
| getToast().error('Failed to load provider health'); | |
| } | |
| } | |
| // Export to window immediately | |
| window.loadProviderHealth = loadProviderHealth; | |
| async function triggerSelfHeal() { | |
| try { | |
| getToast().info('Triggering self-healing...'); | |
| const response = await fetch('/api/diagnostics/self-heal', { method: 'POST' }); | |
| const data = await response.json(); | |
| if (data.status === 'completed') { | |
| getToast().success(`Self-healing completed: ${data.summary.successful} successful, ${data.summary.failed} failed`); | |
| loadProviderHealth(); | |
| loadSystemHealth(); | |
| } else { | |
| getToast().error('Self-healing failed: ' + (data.error || 'Unknown error')); | |
| } | |
| } catch (error) { | |
| console.error('Error triggering self-heal:', error); | |
| getToast().error('Failed to trigger self-healing'); | |
| } | |
| } | |
| // Export immediately after definition | |
| window.triggerSelfHeal = triggerSelfHeal; | |
| // Update the actual implementation (replacing placeholder) | |
| async function testAPIEndpoints() { | |
| const resultsDiv = document.getElementById('api-test-results'); | |
| if (!resultsDiv) return; | |
| resultsDiv.innerHTML = '<div class="spinner"></div> <span>Testing API endpoints...</span>'; | |
| const endpoints = [ | |
| { name: 'Health Check', url: '/api/health' }, | |
| { name: 'System Status', url: '/api/status' }, | |
| { name: 'Market Data', url: '/api/market' }, | |
| { name: 'Models Status', url: '/api/models/status' }, | |
| { name: 'Providers', url: '/api/providers' }, | |
| ]; | |
| let html = '<div style="display: grid; gap: 12px;">'; | |
| let passed = 0; | |
| let failed = 0; | |
| for (const endpoint of endpoints) { | |
| try { | |
| const startTime = performance.now(); | |
| const response = await fetch(endpoint.url); | |
| const duration = (performance.now() - startTime).toFixed(0); | |
| if (response.ok) { | |
| passed++; | |
| html += ` | |
| <div style="padding: 12px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 8px;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <strong style="color: var(--success);">✅ ${endpoint.name}</strong> | |
| <div style="color: var(--text-secondary); font-size: 12px; margin-top: 4px;">${endpoint.url}</div> | |
| </div> | |
| <div style="color: var(--text-secondary); font-size: 12px;">${duration}ms</div> | |
| </div> | |
| </div> | |
| `; | |
| } else { | |
| failed++; | |
| html += ` | |
| <div style="padding: 12px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <strong style="color: var(--danger);">❌ ${endpoint.name}</strong> | |
| <div style="color: var(--text-secondary); font-size: 12px; margin-top: 4px;">${endpoint.url} - HTTP ${response.status}</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| } catch (error) { | |
| failed++; | |
| html += ` | |
| <div style="padding: 12px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px;"> | |
| <div> | |
| <strong style="color: var(--danger);">❌ ${endpoint.name}</strong> | |
| <div style="color: var(--text-secondary); font-size: 12px; margin-top: 4px;">${endpoint.url} - ${error.message}</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| } | |
| html += `</div><div style="margin-top: 16px; padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px;"> | |
| <strong>Summary:</strong> ${passed} passed, ${failed} failed | |
| </div>`; | |
| resultsDiv.innerHTML = html; | |
| getToast().success(`API tests completed: ${passed} passed, ${failed} failed`); | |
| } | |
| // Export to window immediately | |
| window.testAPIEndpoints = testAPIEndpoints; | |
| async function checkDatabaseHealth() { | |
| try { | |
| getToast().info('Checking database health...'); | |
| const response = await fetch('/api/diagnostics/run?auto_fix=false'); | |
| const data = await response.json(); | |
| const output = document.getElementById('diagnostic-output'); | |
| if (output) { | |
| output.textContent = JSON.stringify(data, null, 2); | |
| } | |
| if (data.issues_found === 0) { | |
| getToast().success('Database health check passed'); | |
| } else { | |
| getToast().warning(`Database health check found ${data.issues_found} issues`); | |
| } | |
| } catch (error) { | |
| console.error('Error checking database:', error); | |
| getToast().error('Failed to check database health'); | |
| } | |
| } | |
| // Export to window immediately | |
| window.checkDatabaseHealth = checkDatabaseHealth; | |
| async function testNetworkConnectivity() { | |
| const output = document.getElementById('diagnostic-output'); | |
| if (output) { | |
| output.textContent = 'Testing network connectivity...\n'; | |
| } | |
| const endpoints = [ | |
| { name: 'HuggingFace Hub', url: 'https://huggingface.co' }, | |
| { name: 'CoinGecko API', url: 'https://api.coingecko.com/api/v3/ping' }, | |
| { name: 'Alternative.me', url: 'https://api.alternative.me/fng/' }, | |
| ]; | |
| let results = 'Network Connectivity Test Results:\n' + '='.repeat(50) + '\n\n'; | |
| for (const endpoint of endpoints) { | |
| try { | |
| const startTime = performance.now(); | |
| const response = await fetch(endpoint.url, { method: 'HEAD', mode: 'no-cors' }); | |
| const duration = (performance.now() - startTime).toFixed(0); | |
| results += `✅ ${endpoint.name}: Reachable (${duration}ms)\n`; | |
| } catch (error) { | |
| results += `❌ ${endpoint.name}: ${error.message}\n`; | |
| } | |
| } | |
| if (output) { | |
| output.textContent = results; | |
| } | |
| getToast().success('Network connectivity test completed'); | |
| } | |
| // Export to window immediately | |
| window.testNetworkConnectivity = testNetworkConnectivity; | |
| async function loadRecentLogs() { | |
| try { | |
| const response = await fetch('/api/logs/recent'); | |
| const data = await response.json(); | |
| const container = document.getElementById('recent-logs-content'); | |
| if (!container) return; | |
| if (data.logs && data.logs.length > 0) { | |
| let html = '<div style="display: grid; gap: 8px;">'; | |
| data.logs.slice(0, 20).forEach(log => { | |
| const level = log.level || 'INFO'; | |
| const levelColor = level === 'ERROR' ? 'var(--danger)' : | |
| level === 'WARNING' ? 'var(--warning)' : | |
| level === 'INFO' ? 'var(--info)' : 'var(--text-secondary)'; | |
| html += ` | |
| <div style="padding: 10px; background: rgba(0,0,0,0.2); border-left: 3px solid ${levelColor}; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 12px;"> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 4px;"> | |
| <span style="color: ${levelColor}; font-weight: 600;">[${level}]</span> | |
| <span style="color: var(--text-secondary);">${log.timestamp || ''}</span> | |
| </div> | |
| <div style="color: var(--text-primary);">${log.message || JSON.stringify(log)}</div> | |
| </div> | |
| `; | |
| }); | |
| html += '</div>'; | |
| container.innerHTML = html; | |
| } else { | |
| container.innerHTML = '<p class="text-secondary">No recent logs available</p>'; | |
| } | |
| } catch (error) { | |
| console.error('Error loading logs:', error); | |
| document.getElementById('recent-logs-content').innerHTML = '<p style="color: var(--danger);">Failed to load logs</p>'; | |
| } | |
| } | |
| // Export to window immediately | |
| window.loadRecentLogs = loadRecentLogs; | |
| // Export diagnostic functions to window | |
| // Note: loadSystemHealth, loadProviderHealth, triggerSelfHeal, testAPIEndpoints, | |
| // checkDatabaseHealth, testNetworkConnectivity, loadRecentLogs, and handleAIToolsIframeLoad | |
| // are exported immediately after their definitions above (not here to avoid overwriting) | |
| // Export diagnostic functions to window (actual implementations) | |
| window.runDiagnostic = runDiagnostic; | |
| window.refreshDiagnosticStatus = refreshDiagnosticStatus; | |
| window.downloadDiagnosticLog = downloadDiagnosticLog; | |
| window.toggleAutoRefresh = toggleAutoRefresh; | |
| window.switchDiagnosticTab = switchDiagnosticTab; | |
| // ===== AI TOOLS LOADER ===== | |
| function loadAITools() { | |
| const iframe = document.getElementById('ai-tools-iframe'); | |
| const loading = document.getElementById('ai-tools-loading'); | |
| if (!iframe) return; | |
| // Show loading, hide iframe | |
| if (loading) loading.style.display = 'block'; | |
| iframe.style.display = 'none'; | |
| // Reload iframe if it already has content, or just show it | |
| if (iframe.src && iframe.src.includes('/ai-tools')) { | |
| // Iframe already loaded, just show it | |
| setTimeout(() => { | |
| if (loading) loading.style.display = 'none'; | |
| iframe.style.display = 'block'; | |
| }, 100); | |
| } else { | |
| // Set src to load the page | |
| iframe.src = '/ai-tools'; | |
| } | |
| } | |
| function handleAIToolsIframeLoad() { | |
| const iframe = document.getElementById('ai-tools-iframe'); | |
| const loading = document.getElementById('ai-tools-loading'); | |
| if (loading) loading.style.display = 'none'; | |
| if (iframe) iframe.style.display = 'block'; | |
| console.log('✅ AI Tools iframe loaded successfully'); | |
| } | |
| window.loadAITools = loadAITools; | |
| window.handleAIToolsIframeLoad = handleAIToolsIframeLoad; | |
| // Initialize diagnostics on page load | |
| document.addEventListener('DOMContentLoaded', function() { | |
| refreshDiagnosticStatus(); | |
| loadSystemHealth(); | |
| loadProviderHealth(); | |
| }); | |
| console.log('✅ App.js loaded successfully'); | |