// 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' },
'help': { title: 'Help / API Reference', subtitle: 'Live route catalog and integration guide' },
'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'
);
}
}
}
function setTextContent(id, value) {
const el = document.getElementById(id);
if (el) {
el.textContent = value;
}
}
function firstPresent(...values) {
for (const value of values) {
if (value !== undefined && value !== null && value !== '' && !Number.isNaN(value)) {
return value;
}
}
return null;
}
function normalizeHealthTone(value) {
const token = String(value || '').toLowerCase();
if (token.includes('healthy') || token === 'ok' || token === 'live' || token === 'real' || token === 'complete') {
return 'success';
}
if (token.includes('degraded') || token.includes('partial') || token.includes('warning') || token === 'cached') {
return 'warning';
}
if (token.includes('error') || token.includes('unavailable') || token.includes('offline')) {
return 'danger';
}
return 'info';
}
function formatSidebarCount(value) {
return value === null || value === undefined || value === '' || Number.isNaN(value) ? 'β' : String(value);
}
function getUniqueApiCatalogCount(loader) {
if (!loader) return null;
const seen = new Set();
const add = (items) => {
if (!Array.isArray(items)) return;
for (const item of items) {
const key = item?.id || item?.base_url || item?.url || item?.name;
if (key) {
seen.add(String(key).toLowerCase());
}
}
};
try {
add(loader.getMarketDataAPIs?.());
add(loader.getNewsAPIs?.());
add(loader.getSentimentAPIs?.());
add(loader.getRPCNodes?.());
add(loader.getBlockExplorers?.());
} catch (error) {
console.warn('Unable to derive API catalog count from loader:', error);
}
return seen.size || null;
}
async function getSidebarCatalogSnapshot(resourceSummary = {}, statusData = {}) {
const snapshot = {
resources: null,
apis: null,
sources: null,
};
const loader = window.apiResourceLoader;
if (loader) {
try {
if (!loader.initialized && !loader.initPromise) {
await loader.init();
} else if (loader.initPromise) {
await loader.initPromise;
}
} catch (error) {
console.warn('API resource loader init failed:', error);
}
try {
const stats = loader.getStats ? loader.getStats() : {};
snapshot.resources = firstPresent(stats?.unified?.count, stats?.ultimate?.loaded, null);
snapshot.sources = firstPresent(stats?.ultimate?.count, null);
snapshot.apis = getUniqueApiCatalogCount(loader);
} catch (error) {
console.warn('API resource loader stats failed:', error);
}
}
snapshot.resources = firstPresent(snapshot.resources, resourceSummary?.total_resources, statusData?.resources?.total, null);
snapshot.apis = firstPresent(snapshot.apis, resourceSummary?.categories ? Object.values(resourceSummary.categories).reduce((sum, entry) => {
const count = typeof entry === 'object' && entry !== null ? entry.count : entry;
return sum + (Number(count) || 0);
}, 0) : null, null);
snapshot.sources = firstPresent(snapshot.sources, resourceSummary?.local_routes_count, null);
return snapshot;
}
async function refreshSidebarCatalogCounts(resourceSummary = {}, statusData = {}, label = 'Gateway live', tone = 'success') {
const counts = await getSidebarCatalogSnapshot(resourceSummary, statusData);
applySidebarSummary({
statusText: label,
tone,
resources: formatSidebarCount(counts.resources),
apis: formatSidebarCount(counts.apis),
sources: formatSidebarCount(counts.sources),
});
return counts;
}
function applySidebarSummary({ statusText, tone, resources, apis, sources }) {
const statusIndicator = document.getElementById('sidebar-status');
if (statusIndicator) {
statusIndicator.textContent = statusText;
const container = statusIndicator.parentElement;
if (container) {
const toneStyles = {
success: ['rgba(16, 185, 129, 0.15)', 'rgba(16, 185, 129, 0.3)'],
warning: ['rgba(245, 158, 11, 0.15)', 'rgba(245, 158, 11, 0.3)'],
danger: ['rgba(239, 68, 68, 0.15)', 'rgba(239, 68, 68, 0.3)'],
info: ['rgba(59, 130, 246, 0.15)', 'rgba(59, 130, 246, 0.3)'],
};
const [background, border] = toneStyles[tone] || toneStyles.info;
container.style.background = background;
container.style.borderColor = border;
}
}
setTextContent('sidebar-resources', resources);
setTextContent('sidebar-apis', apis);
setTextContent('sidebar-sources', sources);
}
// =============================================================================
// 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 'help':
loadHelpReference();
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 [healthRes, gatewayRes] = await Promise.allSettled([
fetch('/api/health'),
fetch('/api/short-hunter/health'),
]);
const healthData = healthRes.status === 'fulfilled' && healthRes.value.ok ? await healthRes.value.json() : {};
const gatewayData = gatewayRes.status === 'fulfilled' && gatewayRes.value.ok ? await gatewayRes.value.json() : {};
const gatewayTone = normalizeHealthTone(gatewayData.status || gatewayData.providerRouter?.health || healthData.status);
const statusText = gatewayTone === 'success'
? 'Gateway live'
: gatewayTone === 'warning'
? 'Gateway degraded'
: gatewayTone === 'danger'
? 'Gateway unavailable'
: 'Gateway status unknown';
await refreshSidebarCatalogCounts({}, {}, statusText, gatewayTone);
} catch (error) {
console.error('Error checking API status:', error);
applySidebarSummary({
statusText: 'Connection failed',
tone: 'danger',
resources: 'β',
apis: 'β',
sources: 'β',
});
}
}
// =============================================================================
// 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 = '
';
}
try {
const [resourcesResult, statusResult, apiHealthResult, gatewayHealthResult, providerStatusResult, modelsResult] = await Promise.allSettled([
fetch('/api/resources/summary', { cache: 'no-store' }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`Resources API returned ${res.status}`))),
fetch('/api/status', { cache: 'no-store' }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`Status API returned ${res.status}`))),
fetch('/api/health', { cache: 'no-store' }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`Health API returned ${res.status}`))),
fetch('/api/short-hunter/health', { cache: 'no-store' }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`Short Hunter health returned ${res.status}`))),
fetch('/api/short-hunter/providers/status', { cache: 'no-store' }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`Provider status returned ${res.status}`))),
fetch('/api/models/status', { cache: 'no-store' }).then((res) => res.ok ? res.json() : Promise.reject(new Error(`Models status returned ${res.status}`))),
]);
const resourcesData = resourcesResult.status === 'fulfilled' ? resourcesResult.value : {};
const statusData = statusResult.status === 'fulfilled' ? statusResult.value : {};
const apiHealth = apiHealthResult.status === 'fulfilled' ? apiHealthResult.value : {};
const gatewayHealth = gatewayHealthResult.status === 'fulfilled' ? gatewayHealthResult.value : {};
const providerStatus = providerStatusResult.status === 'fulfilled' ? providerStatusResult.value : {};
const modelsData = modelsResult.status === 'fulfilled' ? modelsResult.value : {};
const resourceSummary = resourcesData?.summary || {};
const resourceCategories = resourceSummary.categories || statusData?.resources?.categories || {};
const catalogCounts = await getSidebarCatalogSnapshot(resourceSummary, statusData);
const totalResources = firstPresent(catalogCounts.resources, resourceSummary.total_resources, statusData?.resources?.total, 0);
const freeResources = firstPresent(resourceSummary.free_resources, 0);
const modelsAvailable = firstPresent(
resourceSummary.models_available,
modelsData?.models_loaded,
modelsData?.pipelines_loaded,
statusData?.models?.total,
0
);
const totalProviders = firstPresent(
providerStatus?.summary?.total,
statusData?.providers?.configuredTotal,
gatewayHealth?.providers_loaded,
0
);
const enabledProviders = firstPresent(providerStatus?.summary?.enabled, providerStatus?.summary?.total, totalProviders);
const disabledProviders = firstPresent(providerStatus?.summary?.disabled, 0);
const dataState = statusData?.dataState || gatewayHealth?.providerRouter?.health || apiHealth?.status || 'unknown';
const gatewayStatus = gatewayHealth?.status || dataState;
const providerRouterHealth = gatewayHealth?.providerRouter?.health || statusData?.providers?.health?.status || 'unknown';
const databaseExists = gatewayHealth?.database?.exists;
const missingCapabilities = Array.isArray(statusData?.missingCapabilities) ? statusData.missingCapabilities : [];
const gatewayTone = normalizeHealthTone(gatewayStatus);
const dataStateTone = normalizeHealthTone(dataState);
const providerRouterTone = normalizeHealthTone(providerRouterHealth);
const databaseTone = databaseExists === false ? 'danger' : 'success';
const missingTone = missingCapabilities.length ? 'warning' : 'success';
const gatewayLabel = gatewayTone === 'success'
? 'Healthy'
: gatewayTone === 'warning'
? 'Degraded'
: gatewayTone === 'danger'
? 'Unavailable'
: 'Unknown';
const databaseText = databaseExists === true ? 'Present' : databaseExists === false ? 'Missing' : 'Unknown';
const lastUpdate = statusData?.last_update || statusData?.timestamp || apiHealth?.timestamp || gatewayHealth?.timestamp || new Date().toISOString();
const totalResourcesEl = document.getElementById('stat-total-resources');
const freeResourcesEl = document.getElementById('stat-free-resources');
const modelsEl = document.getElementById('stat-models');
const providersEl = document.getElementById('stat-providers');
if (totalResourcesEl) totalResourcesEl.textContent = String(totalResources);
if (freeResourcesEl) freeResourcesEl.textContent = String(freeResources);
if (modelsEl) modelsEl.textContent = String(modelsAvailable);
if (providersEl) providersEl.textContent = totalProviders ? `${enabledProviders}/${totalProviders}` : '0';
applySidebarSummary({
statusText: `Gateway ${gatewayLabel.toLowerCase()}`,
tone: gatewayTone,
resources: formatSidebarCount(catalogCounts.resources),
apis: formatSidebarCount(catalogCounts.apis),
sources: formatSidebarCount(catalogCounts.sources),
});
if (resourceCategories && typeof resourceCategories === 'object' && !Array.isArray(resourceCategories)) {
const chartData = {};
for (const [key, value] of Object.entries(resourceCategories)) {
chartData[key] = typeof value === 'object' && value !== null ? (value.count || value) : value;
}
createCategoriesChart(chartData);
}
const apiStatusBadge = apiHealth?.status || 'unknown';
const providerCountText = totalProviders ? `${enabledProviders}/${totalProviders} enabled` : 'No providers reported';
const missingText = missingCapabilities.length ? missingCapabilities.join(', ') : 'None';
const apiHealthTone = normalizeHealthTone(apiStatusBadge);
const apiHealthClass = apiHealthTone === 'success' ? 'status-online' : apiHealthTone === 'warning' ? 'status-degraded' : 'status-offline';
const providerCountClass = totalProviders > 0 ? 'status-online' : 'status-offline';
const databaseClass = databaseTone === 'danger' ? 'status-offline' : databaseTone === 'warning' ? 'status-degraded' : 'status-online';
const missingClass = missingTone === 'warning' ? 'status-degraded' : missingTone === 'success' ? 'status-online' : 'status-offline';
let formattedTime = 'N/A';
try {
formattedTime = new Date(lastUpdate).toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch (e) {
formattedTime = String(lastUpdate);
}
if (systemStatusDiv) {
systemStatusDiv.innerHTML = `
API Health
${apiStatusBadge}
Provider Router
${providerRouterHealth}
Configured Providers
${providerCountText}
Missing Capabilities
${missingText}
Last Update
${formattedTime}
`;
}
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 = `Failed to load dashboard data: ${error.message} Please refresh or check backend status.
`;
}
// Set default values
statsElements.forEach(id => {
const el = document.getElementById(id);
if (el) el.textContent = '0';
});
applySidebarSummary({
statusText: 'Gateway unavailable',
tone: 'danger',
resources: 'β',
apis: 'β',
sources: 'β',
});
}
}
// 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 = 'Chart library not loaded
';
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 = 'No category data available
';
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 = '';
if (trendingDiv) trendingDiv.innerHTML = '
Loading trending coins...
';
if (fgDiv) fgDiv.innerHTML = '
Loading Fear & Greed Index...
';
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 = `
#
Name
Price (USD)
24h Change
24h Volume
Market Cap
${data.cryptocurrencies.map(coin => `
${coin.rank || '-'}
${coin.image ? ` ` : ''}
${coin.symbol} ${coin.name}
$${formatNumber(coin.price)}
${coin.change_24h >= 0 ? 'β' : 'β'} ${Math.abs(coin.change_24h || 0).toFixed(2)}%
$${formatNumber(coin.volume_24h)}
$${formatNumber(coin.market_cap)}
`).join('')}
${data.total_market_cap ? `
Total Market Cap: $${formatNumber(data.total_market_cap)} |
BTC Dominance: ${(data.btc_dominance || 0).toFixed(2)}%
` : ''}
`;
} else {
marketDiv.innerHTML = 'No market data available
';
}
// 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 = `
${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 `
#${index + 1}
${thumb ? `
` : ''}
${coinSymbol}
${coinName}
${marketCapRank ? `
Market Cap Rank: #${marketCapRank}
` : ''}
${score !== null && score > 0 ? `
${score.toFixed(2)}
Score
` : ''}
`;
}).join('')}
`;
} else {
trendingDiv.innerHTML = 'No trending data available
';
}
} else {
throw new Error('Trending endpoint not available');
}
} catch (trendingError) {
console.warn('Trending endpoint error:', trendingError);
trendingDiv.innerHTML = 'Trending data unavailable
';
}
// 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 = `
${sentimentIcon}
${fgValue}
/ 100
${sentimentClass}
${description}
Fear & Greed Index
${fgValue}/100
π± Extreme Fear
β οΈ Fear
π Neutral
π Greed
π Extreme Greed
Classification
${fgLabel}
${sentimentData.timestamp ? `
Last Update
${new Date(sentimentData.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}
` : ''}
π‘
Market Interpretation
${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.'}
`;
} else {
fgDiv.innerHTML = 'Fear & Greed Index unavailable
';
}
} else {
throw new Error('Sentiment endpoint not available');
}
} catch (fgError) {
console.warn('Fear & Greed endpoint error:', fgError);
fgDiv.innerHTML = `
β οΈ
Fear & Greed Index Unavailable
Unable to fetch sentiment data at this time. Please try again later.
`;
}
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 = `Error loading market data: ${error.message}
`;
if (trendingDiv) trendingDiv.innerHTML = 'Error loading trending coins
';
if (fgDiv) fgDiv.innerHTML = 'Error loading Fear & Greed Index
';
}
}
// =============================================================================
// 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 = '
Fetching news from CryptoCompare API...
';
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 = `Error: ${data.error}
`;
}
} catch (error) {
console.error('β Error fetching news:', error);
getToast().error('Failed to fetch news from API');
newsListDiv.innerHTML = `Error fetching news: ${error.message}
`;
}
}
// 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 = '';
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 = `
${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 `
${article.content ? `
${article.content.substring(0, 200)}${article.content.length > 200 ? '...' : ''}
` : ''}
${symbols.length > 0 ? `
${symbols.slice(0, 5).map(symbol => `${symbol} `).join('')}
${symbols.length > 5 ? `+${symbols.length - 5} ` : ''}
` : ''}
`;
}).join('')}
Showing ${sortedNews.length} article${sortedNews.length !== 1 ? 's' : ''}${data.source ? ` from ${data.source}` : ''}
`;
console.log('β
News loaded successfully');
getToast().success(`Loaded ${sortedNews.length} news articles`);
} else {
newsListDiv.innerHTML = 'No news articles available at the moment. Click "Fetch Latest News" to load articles.
';
console.warn('No news data available');
}
} catch (error) {
console.error('β Error loading news:', error);
getToast().error('Failed to load news');
newsListDiv.innerHTML = `Error loading news: ${error.message} Please check your internet connection and try again.
`;
}
}
// =============================================================================
// 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 = '';
if (modelsListDiv) modelsListDiv.innerHTML = '';
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 = `
Status: ${statusIcon} ${statusMessage}
Models Configured: ${modelsCount} (Lazy Loading)
Models Loaded: ${pipelinesLoaded}
HF Mode: ${hfMode}
Transformers: ${transformersAvailable ? 'β
Available' : 'β Not Available'}
${isLazyLoading ? 'π‘ Models will load automatically on first use (sentiment analysis, etc.) ' : ''}
`;
// Update stat-models in dashboard with actual loaded count
const modelsStatEl = document.getElementById('stat-models');
// Use pipelines_loaded if available, otherwise use modelsCount
const displayCount = pipelinesLoaded > 0 ? pipelinesLoaded : modelsCount;
if (modelsStatEl) {
modelsStatEl.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();
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 (modelsStatEl) {
modelsStatEl.textContent = loadedModels > 0 ? loadedModels : totalModels;
}
if (listData.models && listData.models.length > 0) {
modelsListDiv.innerHTML = `
Model ID
Task
Category
Status
${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 `
${modelId}
${task}
${category}
${statusText}
`;
}).join('')}
`;
} else {
modelsListDiv.innerHTML = 'No models available
';
}
} 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 = `Error loading models status: ${error.message}
`;
if (modelsListDiv) modelsListDiv.innerHTML = 'Error loading models list
';
}
}
async function initializeModels() {
getToast().info('Initializing models... This may take a moment.');
const modelsStatusDiv = document.getElementById('models-status');
if (modelsStatusDiv) {
modelsStatusDiv.innerHTML = '';
}
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 = `Error initializing models: ${error.message}
`;
}
}
}
// =============================================================================
// Settings
// =============================================================================
function loadSettings() {
const apiInfoDiv = document.getElementById('api-info');
if (apiInfoDiv) {
apiInfoDiv.innerHTML = `
API Base URL: ${window.location.origin}
Documentation: /docs
Health Check: /health
`;
}
}
function saveSettings() {
getToast().success('Settings saved successfully!');
}
function ensureHelpReferenceModule() {
if (window.HelpReferenceView) {
return Promise.resolve(window.HelpReferenceView);
}
if (window.__helpReferenceLoader) {
return window.__helpReferenceLoader;
}
window.__helpReferenceLoader = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = '/static/js/helpView.js?v=7';
script.async = true;
script.onload = () => resolve(window.HelpReferenceView);
script.onerror = () => {
window.__helpReferenceLoader = null;
reject(new Error('Failed to load help reference module'));
};
document.head.appendChild(script);
});
return window.__helpReferenceLoader;
}
async function loadHelpReference() {
const section = document.getElementById('tab-help');
if (!section) return;
try {
const module = await ensureHelpReferenceModule();
if (module && typeof module.mount === 'function') {
await module.mount(section);
}
} catch (error) {
console.error('Error loading help reference:', error);
section.innerHTML = `
Failed to load the help reference: ${error.message}
`;
}
}
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 `
`;
}
// Create confidence bar
function createConfidenceBar(confidence) {
const confidencePercent = Math.round(confidence * 100);
return `
Model Confidence
${confidencePercent}%
`;
}
async function analyzeGlobalSentiment() {
getToast().info('Analyzing global market sentiment...');
const resultDiv = document.getElementById('global-sentiment-result');
if (resultDiv) {
resultDiv.innerHTML = '';
}
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 = `
${getTrendArrow(sentimentClass)}
${sentiment}
${getTrendArrow(sentimentClass)}
${createConfidenceBar(confidence)}
Model: ${data.model || 'AI Sentiment Analysis'} |
Engine: ${data.engine || 'N/A'}
`;
// Create gauge chart after DOM update
setTimeout(() => {
createSentimentGauge('global-sentiment-gauge', confidence, sentimentClass);
}, 100);
getToast().success('Sentiment analysis complete!');
} else {
resultDiv.innerHTML = 'Sentiment analysis unavailable
';
}
} catch (error) {
console.error('Error analyzing sentiment:', error);
resultDiv.innerHTML = `Error: ${error.message}
`;
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 = '';
}
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 = `
${getTrendArrow(sentimentClass)}
${sentiment}
${getTrendArrow(sentimentClass)}
${createConfidenceBar(confidence)}
Model: ${data.model || 'AI Sentiment Analysis'} |
Engine: ${data.engine || 'N/A'}
`;
// Create gauge chart after DOM update
setTimeout(() => {
createSentimentGauge('asset-sentiment-gauge', confidence, sentimentClass);
}, 100);
getToast().success('Asset sentiment analysis complete!');
} else {
resultDiv.innerHTML = 'Sentiment analysis unavailable
';
}
} catch (error) {
console.error('Error analyzing asset sentiment:', error);
resultDiv.innerHTML = `Error: ${error.message}
`;
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 = '';
}
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 = `
${getTrendArrow(sentimentClass)}
${sentiment}
${getTrendArrow(sentimentClass)}
${createConfidenceBar(confidence)}
Text: ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}
Model: ${data.model || 'AI Sentiment Analysis'} |
Engine: ${data.engine || 'N/A'}
`;
// Create gauge chart after DOM update
setTimeout(() => {
createSentimentGauge('sentiment-gauge', confidence, sentimentClass);
}, 100);
getToast().success('Sentiment analysis complete!');
} else {
resultDiv.innerHTML = 'Sentiment analysis unavailable
';
}
} catch (error) {
console.error('Error analyzing sentiment:', error);
resultDiv.innerHTML = `Error: ${error.message}
`;
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 = '';
}
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 = `
${confidence}%
Confidence
${data.reasoning ? `
Reasoning: ${data.reasoning}
` : ''}
Model: ${data.model || 'AI Trading Assistant'}
`;
getToast().success('Trading signal generated!');
} else {
resultDiv.innerHTML = 'Trading signal unavailable
';
}
} catch (error) {
console.error('Error generating trading signal:', error);
resultDiv.innerHTML = `Error: ${error.message}
`;
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 = `β Error running diagnostic: ${error.message} `;
showToast('β Diagnostic failed: ' + error.message, 'error');
} finally {
// Re-enable button
runBtn.disabled = false;
runBtn.innerHTML = ' βΆοΈ Run Full Diagnostic';
progressDiv.style.display = 'none';
}
}
function colorCodeOutput(output) {
if (!output) return '';
return output
.replace(/β
/g, 'β
')
.replace(/β/g, 'β ')
.replace(/β οΈ/g, 'β οΈ ')
.replace(/π/g, 'π ')
.replace(/π¦/g, 'π¦ ')
.replace(/π/g, 'π ')
.replace(/π§ͺ/g, 'π§ͺ ')
.replace(/π/g, 'π ')
.replace(/π‘/g, 'π‘ ')
.replace(/βοΈ/g, 'βοΈ ')
.split('\n').join(' ');
}
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 ? '2/2' : '1/2';
document.getElementById('summary-failed').textContent = (!data.summary.transformers_available || !data.summary.hf_hub) ? '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 = 'π§ Suggested Fixes: ' +
data.summary.critical_issues.map(issue =>
`${issue} `
).join('') + ' ';
} else {
fixesDiv.innerHTML = 'β
No critical issues found
';
}
}
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' : 'Disconnected';
hfEl.style.color = summary.hf_hub ? '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 = ' 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 = ' 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 = `
Providers
${providers.healthy || 0}/${providers.total || 0}
${overall.providers_ok ? 'β
Healthy' : 'β οΈ Degraded'}
AI Models
${models.healthy || 0}/${models.total || 0}
${overall.models_ok ? 'β
Healthy' : 'β οΈ Degraded'}
In Cooldown
${(providers.in_cooldown || 0) + (models.in_cooldown || 0)}
${(providers.in_cooldown || 0) + (models.in_cooldown || 0) > 0 ? 'β οΈ Some services cooling' : 'β
All active'}
Degraded
${(providers.degraded || 0) + (models.degraded || 0)}
${(providers.degraded || 0) + (models.degraded || 0) > 0 ? 'β οΈ Needs attention' : 'β
All optimal'}
`;
// Update health details tab
const healthDetails = document.getElementById('health-details-content');
if (healthDetails) {
healthDetails.innerHTML = `
Provider Health Summary
Total
${providers.total || 0}
Healthy
${providers.healthy || 0}
Degraded
${providers.degraded || 0}
Unavailable
${providers.unavailable || 0}
Model Health Summary
Total
${models.total || 0}
Healthy
${models.healthy || 0}
Degraded
${models.degraded || 0}
Unavailable
${models.unavailable || 0}
`;
}
} 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 += `
${entry.name || entry.id}
Provider
${entry.status || 'unknown'}
${lastCheck}
${entry.in_cooldown ? 'β³ Cooldown ' : '-'}
`;
});
// Add models
models.slice(0, 10).forEach(entry => {
const statusClass = entry.status === 'healthy' ? 'healthy' :
entry.status === 'degraded' ? 'degraded' :
entry.status === 'unavailable' ? 'unavailable' : 'unknown';
html += `
${entry.name || entry.key || 'Unknown'}
AI Model
${entry.status || 'unknown'}
-
${entry.in_cooldown ? 'β³ Cooldown ' : '-'}
`;
});
if (html === '') {
html = 'No health data available ';
}
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 = '
Testing API endpoints... ';
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 = '';
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 += `
β
${endpoint.name}
${endpoint.url}
${duration}ms
`;
} else {
failed++;
html += `
β ${endpoint.name}
${endpoint.url} - HTTP ${response.status}
`;
}
} catch (error) {
failed++;
html += `
β ${endpoint.name}
${endpoint.url} - ${error.message}
`;
}
}
html += `
Summary: ${passed} passed, ${failed} failed
`;
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 = '';
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 += `
[${level}]
${log.timestamp || ''}
${log.message || JSON.stringify(log)}
`;
});
html += '
';
container.innerHTML = html;
} else {
container.innerHTML = 'No recent logs available
';
}
} catch (error) {
console.error('Error loading logs:', error);
document.getElementById('recent-logs-content').innerHTML = 'Failed to load logs
';
}
}
// 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');