Really-amin's picture
Upload 196 files
250b128 verified
raw
history blame
59.6 kB
// Crypto Intelligence Hub - Main JavaScript with Sidebar Navigation
// Enhanced Pro Trading Terminal UI
// =============================================================================
// Toast Notification System
// =============================================================================
const ToastManager = {
container: null,
init() {
this.container = document.getElementById('toast-container');
if (!this.container) {
this.container = document.createElement('div');
this.container.id = 'toast-container';
this.container.className = 'toast-container';
document.body.appendChild(this.container);
}
},
show(message, type = 'info', duration = 5000) {
if (!this.container) this.init();
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: 'fa-check-circle',
error: 'fa-times-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
const titles = {
success: 'Success',
error: 'Error',
warning: 'Warning',
info: 'Info'
};
toast.innerHTML = `
<div class="toast-icon">
<i class="fas ${icons[type] || icons.info}"></i>
</div>
<div class="toast-content">
<div class="toast-title">${titles[type] || titles.info}</div>
<div class="toast-message">${message}</div>
</div>
<button class="toast-close" onclick="ToastManager.remove(this.parentElement)">
<i class="fas fa-times"></i>
</button>
<div class="toast-progress"></div>
`;
this.container.appendChild(toast);
// Auto remove after duration
if (duration > 0) {
setTimeout(() => this.remove(toast), duration);
}
return toast;
},
remove(toast) {
if (!toast) return;
toast.classList.add('removing');
setTimeout(() => {
if (toast.parentElement) {
toast.parentElement.removeChild(toast);
}
}, 300);
},
success(message, duration) {
return this.show(message, 'success', duration);
},
error(message, duration) {
return this.show(message, 'error', duration);
},
warning(message, duration) {
return this.show(message, 'warning', duration);
},
info(message, duration) {
return this.show(message, 'info', duration);
}
};
// =============================================================================
// 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' }
};
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
ToastManager.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;
default:
console.log('No specific loader for tab:', tabId);
}
}
function refreshCurrentTab() {
loadTabData(AppState.currentTab);
ToastManager.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
const resourcesRes = await fetch('/api/resources');
if (!resourcesRes.ok) {
throw new Error(`Resources API returned ${resourcesRes.status}`);
}
const resourcesData = await resourcesRes.json();
console.log('Resources data:', resourcesData);
if (resourcesData.success && resourcesData.summary) {
const totalResources = resourcesData.summary.total_resources || 0;
const freeResources = resourcesData.summary.free_resources || 0;
const modelsAvailable = resourcesData.summary.models_available || 0;
document.getElementById('stat-total-resources').textContent = totalResources;
document.getElementById('stat-free-resources').textContent = freeResources;
document.getElementById('stat-models').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
if (resourcesData.summary.categories) {
createCategoriesChart(resourcesData.summary.categories);
}
} else {
console.warn('Resources data format unexpected:', 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();
const providers = statusData.total_apis || statusData.total_providers || 0;
document.getElementById('stat-providers').textContent = providers;
// Display system status
const systemStatusDiv = document.getElementById('system-status');
const healthStatus = statusData.system_health || 'unknown';
const healthClass = healthStatus === 'healthy' ? 'alert-success' :
healthStatus === 'degraded' ? 'alert-warning' : 'alert-error';
systemStatusDiv.innerHTML = `
<div class="alert ${healthClass}">
<strong>System Status:</strong> ${healthStatus}<br>
<strong>Online APIs:</strong> ${statusData.online || 0}<br>
<strong>Degraded APIs:</strong> ${statusData.degraded || 0}<br>
<strong>Offline APIs:</strong> ${statusData.offline || 0}<br>
<strong>Avg Response Time:</strong> ${statusData.avg_response_time_ms || 0}ms<br>
<strong>Last Update:</strong> ${new Date(statusData.last_update || Date.now()).toLocaleString('en-US')}
</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);
ToastManager.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 style="display: grid; gap: 10px;">
${trendingData.trending.map((coin, index) => `
<div style="padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 10px; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid var(--primary);">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 18px; font-weight: 800; color: var(--primary);">#${index + 1}</span>
<div>
<strong>${coin.symbol || coin.id}</strong> - ${coin.name || 'Unknown'}
${coin.market_cap_rank ? `<div style="font-size: 12px; color: var(--text-secondary);">Market Cap Rank: ${coin.market_cap_rank}</div>` : ''}
</div>
</div>
<div style="font-size: 20px; font-weight: 700; color: var(--success);">${coin.score ? coin.score.toFixed(2) : 'N/A'}</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
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';
let fgColor = 'var(--warning)';
if (fgValue >= 75) fgColor = 'var(--success)';
else if (fgValue >= 50) fgColor = 'var(--info)';
else if (fgValue >= 25) fgColor = 'var(--warning)';
else fgColor = 'var(--danger)';
fgDiv.innerHTML = `
<div style="text-align: center; padding: 30px;">
<div style="font-size: 72px; font-weight: 800; margin-bottom: 10px; color: ${fgColor};">
${fgValue}
</div>
<div style="font-size: 24px; font-weight: 600; color: var(--text-primary); margin-bottom: 10px;">
${fgLabel}
</div>
<div style="font-size: 14px; color: var(--text-secondary);">
Market Fear & Greed Index
</div>
${sentimentData.timestamp ? `<div style="font-size: 12px; color: var(--text-secondary); margin-top: 10px;">
Last Update: ${new Date(sentimentData.timestamp).toLocaleString('en-US')}
</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 class="alert alert-warning">Fear & Greed Index unavailable</div>';
}
console.log('✅ Market data loaded successfully');
} catch (error) {
console.error('❌ Error loading market data:', error);
ToastManager.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...');
ToastManager.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) {
ToastManager.success(`Successfully fetched and saved ${data.saved} news articles!`);
// Reload news list
loadNews();
} else {
ToastManager.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);
ToastManager.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');
ToastManager.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);
ToastManager.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();
modelsStatusDiv.innerHTML = `
<div class="alert ${statusData.ok ? 'alert-success' : 'alert-warning'}">
<strong>Status:</strong> ${statusData.ok ? '✅ Active' : '⚠️ Partial'}<br>
<strong>Pipelines Loaded:</strong> ${statusData.pipelines_loaded || 0}<br>
<strong>Available Models:</strong> ${statusData.available_models?.length || 0}<br>
<strong>HF Mode:</strong> ${statusData.hf_mode || 'unknown'}<br>
<strong>Transformers Available:</strong> ${statusData.transformers_available ? '✅' : '❌'}
</div>
`;
} 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();
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 => `
<tr>
<td><strong>${model.model_id || model.key}</strong></td>
<td>${model.task || 'N/A'}</td>
<td>${model.category || 'N/A'}</td>
<td><span class="model-status ${model.loaded ? 'available' : 'unavailable'}">${model.loaded ? '✅ Loaded' : '❌ Not Loaded'}</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);
ToastManager.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() {
ToastManager.info('Initializing models... This may take a moment.');
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();
if (data.status === 'ok') {
ToastManager.success('Models initialized successfully!');
loadModels(); // Reload models list
} else {
ToastManager.warning('Models partially initialized');
}
} catch (error) {
console.error('Error initializing models:', error);
ToastManager.error('Failed to initialize models');
}
}
// =============================================================================
// 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() {
ToastManager.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() {
ToastManager.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);
ToastManager.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>`;
ToastManager.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) {
ToastManager.warning('Please select a trading pair');
return;
}
ToastManager.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);
ToastManager.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>`;
ToastManager.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() === '') {
ToastManager.warning('Please enter text to analyze');
return;
}
ToastManager.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);
ToastManager.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>`;
ToastManager.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) {
ToastManager.warning('Please select a trading symbol');
return;
}
ToastManager.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>
`;
ToastManager.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>`;
ToastManager.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;
window.ToastManager = 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;
console.log('✅ App.js loaded successfully');