|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
(function() {
|
|
|
const originalWarn = console.warn;
|
|
|
const originalError = console.error;
|
|
|
|
|
|
console.warn = function(...args) {
|
|
|
const message = args.join(' ');
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
originalWarn.apply(console, args);
|
|
|
};
|
|
|
|
|
|
console.error = function(...args) {
|
|
|
const message = args.join(' ');
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
originalError.apply(console, args);
|
|
|
};
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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'); }
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const AppState = {
|
|
|
currentTab: 'dashboard',
|
|
|
data: {},
|
|
|
charts: {},
|
|
|
isLoading: false,
|
|
|
sidebarOpen: false
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
const navItems = document.querySelectorAll('.nav-item');
|
|
|
navItems.forEach(item => {
|
|
|
if (item.dataset.tab === tabId) {
|
|
|
item.classList.add('active');
|
|
|
} else {
|
|
|
item.classList.remove('active');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
const tabPanels = document.querySelectorAll('.tab-panel');
|
|
|
tabPanels.forEach(panel => {
|
|
|
if (panel.id === `tab-${tabId}`) {
|
|
|
panel.classList.add('active');
|
|
|
} else {
|
|
|
panel.classList.remove('active');
|
|
|
}
|
|
|
});
|
|
|
|
|
|
|
|
|
const pageTitles = {
|
|
|
'dashboard': { title: 'Dashboard', subtitle: 'System Overview' },
|
|
|
'market': { title: 'Market Data', subtitle: 'Real-time Cryptocurrency Prices' },
|
|
|
'models': { title: 'AI Models', subtitle: 'Hugging Face Models' },
|
|
|
'sentiment': { title: 'Sentiment Analysis', subtitle: 'AI-Powered Sentiment Detection' },
|
|
|
'trading-assistant': { title: 'Trading Signals', subtitle: 'AI Trading Assistant' },
|
|
|
'news': { title: 'Crypto News', subtitle: 'Latest News & Updates' },
|
|
|
'settings': { title: 'Settings', subtitle: 'System Configuration' },
|
|
|
'diagnostics': { title: 'Test & Diagnostics', subtitle: 'System Diagnostics & Model Testing' },
|
|
|
'ai-tools': { title: 'AI Design Tools', subtitle: 'AI-Powered Tools & Utilities' },
|
|
|
'providers': { title: 'Providers', subtitle: 'Provider Management' },
|
|
|
'resources': { title: 'Resources', subtitle: 'Resource Management' },
|
|
|
'defi': { title: 'DeFi Analytics', subtitle: 'DeFi Protocol Analytics' },
|
|
|
'system-status': { title: 'System Status', subtitle: 'System Health Monitoring' }
|
|
|
};
|
|
|
|
|
|
const pageTitle = document.getElementById('page-title');
|
|
|
const pageSubtitle = document.getElementById('page-subtitle');
|
|
|
|
|
|
if (pageTitle && pageTitles[tabId]) {
|
|
|
pageTitle.textContent = pageTitles[tabId].title;
|
|
|
}
|
|
|
if (pageSubtitle && pageTitles[tabId]) {
|
|
|
pageSubtitle.textContent = pageTitles[tabId].subtitle;
|
|
|
}
|
|
|
|
|
|
|
|
|
AppState.currentTab = tabId;
|
|
|
|
|
|
|
|
|
loadTabData(tabId);
|
|
|
|
|
|
|
|
|
if (window.innerWidth <= 768) {
|
|
|
toggleSidebar();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
console.log('π Initializing Crypto Intelligence Hub...');
|
|
|
|
|
|
|
|
|
getToast().init();
|
|
|
|
|
|
|
|
|
checkAPIStatus();
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
loadDashboard();
|
|
|
}, 100);
|
|
|
|
|
|
|
|
|
setInterval(() => {
|
|
|
if (AppState.currentTab === 'dashboard') {
|
|
|
loadDashboard();
|
|
|
}
|
|
|
}, 30000);
|
|
|
|
|
|
|
|
|
document.addEventListener('tradingPairsLoaded', function(e) {
|
|
|
console.log('Trading pairs loaded:', e.detail.pairs.length);
|
|
|
initTradingPairSelectors();
|
|
|
});
|
|
|
|
|
|
console.log('β
App initialized successfully');
|
|
|
});
|
|
|
|
|
|
|
|
|
function initTradingPairSelectors() {
|
|
|
|
|
|
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'
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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 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':
|
|
|
|
|
|
break;
|
|
|
case 'trading-assistant':
|
|
|
|
|
|
break;
|
|
|
case 'news':
|
|
|
loadNews();
|
|
|
break;
|
|
|
case 'settings':
|
|
|
loadSettings();
|
|
|
break;
|
|
|
case 'diagnostics':
|
|
|
refreshDiagnosticStatus();
|
|
|
break;
|
|
|
case 'ai-tools':
|
|
|
loadAITools();
|
|
|
break;
|
|
|
default:
|
|
|
console.log('No specific loader for tab:', tabId);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function refreshCurrentTab() {
|
|
|
loadTabData(AppState.currentTab);
|
|
|
getToast().success('Data refreshed successfully');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)';
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function loadDashboard() {
|
|
|
console.log('π Loading dashboard...');
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
const resourcesRes = await fetch('/api/resources/summary');
|
|
|
if (!resourcesRes.ok) {
|
|
|
throw new Error(`Resources API returned ${resourcesRes.status}`);
|
|
|
}
|
|
|
let resourcesData = await resourcesRes.json();
|
|
|
|
|
|
console.log('Resources data:', resourcesData);
|
|
|
|
|
|
|
|
|
if (Array.isArray(resourcesData)) {
|
|
|
|
|
|
const summaryObj = resourcesData.find(item => item && typeof item === 'object' && !Array.isArray(item) && item.summary);
|
|
|
if (summaryObj && summaryObj.summary) {
|
|
|
resourcesData = summaryObj;
|
|
|
console.log('Extracted summary from array response');
|
|
|
} else {
|
|
|
|
|
|
const totalResources = resourcesData.length;
|
|
|
console.log(`Using array length (${totalResources}) as resource count estimate`);
|
|
|
document.getElementById('stat-total-resources').textContent = totalResources;
|
|
|
document.getElementById('stat-free-resources').textContent = Math.floor(totalResources * 0.8);
|
|
|
document.getElementById('stat-models').textContent = '0';
|
|
|
|
|
|
|
|
|
const sidebarResources = document.getElementById('sidebar-resources');
|
|
|
const sidebarModels = document.getElementById('sidebar-models');
|
|
|
if (sidebarResources) sidebarResources.textContent = totalResources;
|
|
|
if (sidebarModels) sidebarModels.textContent = '0';
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
let summary = null;
|
|
|
if (resourcesData && typeof resourcesData === 'object' && !Array.isArray(resourcesData)) {
|
|
|
summary = resourcesData.summary || resourcesData;
|
|
|
}
|
|
|
|
|
|
|
|
|
if (summary && typeof summary === 'object' && !Array.isArray(summary)) {
|
|
|
|
|
|
const hasExpectedProperties = summary.total_resources !== undefined ||
|
|
|
summary.free_resources !== undefined ||
|
|
|
summary.models_available !== undefined ||
|
|
|
(resourcesData.success !== false && resourcesData.success !== undefined);
|
|
|
|
|
|
if (hasExpectedProperties || resourcesData.success === true) {
|
|
|
const totalResources = summary.total_resources || 0;
|
|
|
const freeResources = summary.free_resources || 0;
|
|
|
const modelsAvailable = summary.models_available || 0;
|
|
|
|
|
|
|
|
|
const totalResourcesEl = document.getElementById('stat-total-resources');
|
|
|
const freeResourcesEl = document.getElementById('stat-free-resources');
|
|
|
const modelsEl = document.getElementById('stat-models');
|
|
|
|
|
|
if (totalResourcesEl) totalResourcesEl.textContent = totalResources;
|
|
|
if (freeResourcesEl) freeResourcesEl.textContent = freeResources;
|
|
|
if (modelsEl) modelsEl.textContent = modelsAvailable;
|
|
|
|
|
|
|
|
|
const sidebarResources = document.getElementById('sidebar-resources');
|
|
|
const sidebarModels = document.getElementById('sidebar-models');
|
|
|
if (sidebarResources) sidebarResources.textContent = totalResources;
|
|
|
if (sidebarModels) sidebarModels.textContent = modelsAvailable;
|
|
|
|
|
|
|
|
|
if (summary.categories && typeof summary.categories === 'object' && !Array.isArray(summary.categories)) {
|
|
|
const categories = summary.categories;
|
|
|
|
|
|
const chartData = {};
|
|
|
for (const [key, value] of Object.entries(categories)) {
|
|
|
chartData[key] = typeof value === 'object' && value !== null ? (value.count || value) : value;
|
|
|
}
|
|
|
createCategoriesChart(chartData);
|
|
|
}
|
|
|
} else {
|
|
|
|
|
|
console.warn('Resources data missing expected properties:', resourcesData);
|
|
|
document.getElementById('stat-total-resources').textContent = '0';
|
|
|
document.getElementById('stat-free-resources').textContent = '0';
|
|
|
document.getElementById('stat-models').textContent = '0';
|
|
|
}
|
|
|
} else {
|
|
|
|
|
|
if (Array.isArray(resourcesData)) {
|
|
|
console.log(`Resources API returned array (${resourcesData.length} items) instead of summary object`);
|
|
|
} else {
|
|
|
console.log('Resources data format unexpected - not a valid object:', typeof resourcesData);
|
|
|
}
|
|
|
document.getElementById('stat-total-resources').textContent = '0';
|
|
|
document.getElementById('stat-free-resources').textContent = '0';
|
|
|
document.getElementById('stat-models').textContent = '0';
|
|
|
}
|
|
|
|
|
|
|
|
|
try {
|
|
|
const statusRes = await fetch('/api/status');
|
|
|
if (statusRes.ok) {
|
|
|
const statusData = await statusRes.json();
|
|
|
|
|
|
|
|
|
let providers = 0;
|
|
|
if (statusData.providers && typeof statusData.providers === 'object') {
|
|
|
providers = statusData.providers.total || 0;
|
|
|
} else {
|
|
|
providers = statusData.total_apis || statusData.total_providers || statusData.providers || 0;
|
|
|
}
|
|
|
|
|
|
const providersEl = document.getElementById('stat-providers');
|
|
|
if (providersEl) {
|
|
|
providersEl.textContent = providers;
|
|
|
}
|
|
|
|
|
|
|
|
|
const systemStatusDiv = document.getElementById('system-status');
|
|
|
if (systemStatusDiv) {
|
|
|
|
|
|
const healthStatus = statusData.system_health || statusData.status || 'ok';
|
|
|
const healthClass = healthStatus === 'healthy' || healthStatus === 'ok' ? 'alert-success' :
|
|
|
healthStatus === 'degraded' ? 'alert-warning' : 'alert-error';
|
|
|
|
|
|
|
|
|
const providers = statusData.providers || {};
|
|
|
const totalProviders = providers.total || statusData.total_apis || 0;
|
|
|
const onlineProviders = statusData.online || 0;
|
|
|
const degradedProviders = statusData.degraded || 0;
|
|
|
const offlineProviders = statusData.offline || 0;
|
|
|
const avgResponseTime = statusData.avg_response_time_ms || 0;
|
|
|
const lastUpdate = statusData.last_update || statusData.timestamp || new Date().toISOString();
|
|
|
|
|
|
|
|
|
let formattedTime = 'N/A';
|
|
|
try {
|
|
|
const updateDate = new Date(lastUpdate);
|
|
|
formattedTime = updateDate.toLocaleString('en-US', {
|
|
|
year: 'numeric',
|
|
|
month: '2-digit',
|
|
|
day: '2-digit',
|
|
|
hour: '2-digit',
|
|
|
minute: '2-digit',
|
|
|
second: '2-digit'
|
|
|
});
|
|
|
} catch (e) {
|
|
|
formattedTime = lastUpdate;
|
|
|
}
|
|
|
|
|
|
|
|
|
const statusIcon = healthStatus === 'healthy' || healthStatus === 'ok' ?
|
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><polyline points="22 4 12 14.01 9 11.01"></polyline></svg>' :
|
|
|
healthStatus === 'degraded' ?
|
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>' :
|
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>';
|
|
|
|
|
|
const statusText = healthStatus === 'ok' ? 'Healthy' :
|
|
|
healthStatus === 'healthy' ? 'Healthy' :
|
|
|
healthStatus === 'degraded' ? 'Degraded' :
|
|
|
healthStatus === 'error' ? 'Error' : 'Unknown';
|
|
|
|
|
|
systemStatusDiv.innerHTML = `
|
|
|
<div class="system-status-container">
|
|
|
<div class="system-status-header ${healthClass}">
|
|
|
<div class="status-icon-wrapper">
|
|
|
${statusIcon}
|
|
|
</div>
|
|
|
<div>
|
|
|
<div class="status-title">System Status</div>
|
|
|
<div class="status-value">${statusText}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="system-status-grid">
|
|
|
<div class="status-item">
|
|
|
<div class="status-item-label">Total Providers</div>
|
|
|
<div class="status-item-value">${totalProviders}</div>
|
|
|
</div>
|
|
|
<div class="status-item status-online">
|
|
|
<div class="status-item-label">Online APIs</div>
|
|
|
<div class="status-item-value">${onlineProviders}</div>
|
|
|
</div>
|
|
|
<div class="status-item status-degraded">
|
|
|
<div class="status-item-label">Degraded APIs</div>
|
|
|
<div class="status-item-value">${degradedProviders}</div>
|
|
|
</div>
|
|
|
<div class="status-item status-offline">
|
|
|
<div class="status-item-label">Offline APIs</div>
|
|
|
<div class="status-item-value">${offlineProviders}</div>
|
|
|
</div>
|
|
|
<div class="status-item">
|
|
|
<div class="status-item-label">Avg Response Time</div>
|
|
|
<div class="status-item-value">${avgResponseTime}ms</div>
|
|
|
</div>
|
|
|
<div class="status-item status-full-width">
|
|
|
<div class="status-item-label">Last Update</div>
|
|
|
<div class="status-item-value status-time">${formattedTime}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
} else {
|
|
|
throw new Error('Status endpoint not available');
|
|
|
}
|
|
|
} catch (statusError) {
|
|
|
console.warn('Status endpoint error:', statusError);
|
|
|
document.getElementById('stat-providers').textContent = '-';
|
|
|
|
|
|
const systemStatusDiv = document.getElementById('system-status');
|
|
|
if (systemStatusDiv) {
|
|
|
systemStatusDiv.innerHTML = '<div class="alert alert-warning">System status unavailable. Core features are operational.</div>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
console.log('β
Dashboard loaded successfully');
|
|
|
} catch (error) {
|
|
|
console.error('β Error loading dashboard:', error);
|
|
|
getToast().error('Failed to load dashboard. Please check the backend.');
|
|
|
|
|
|
|
|
|
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>`;
|
|
|
}
|
|
|
|
|
|
|
|
|
statsElements.forEach(id => {
|
|
|
const el = document.getElementById(id);
|
|
|
if (el) el.textContent = '0';
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
function createCategoriesChart(categories) {
|
|
|
const ctx = document.getElementById('categories-chart');
|
|
|
if (!ctx) return;
|
|
|
|
|
|
|
|
|
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 }
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>';
|
|
|
}
|
|
|
|
|
|
|
|
|
try {
|
|
|
const trendingRes = await fetch('/api/trending');
|
|
|
if (trendingRes.ok) {
|
|
|
const trendingData = await trendingRes.json();
|
|
|
|
|
|
if (trendingData.trending && trendingData.trending.length > 0) {
|
|
|
trendingDiv.innerHTML = `
|
|
|
<div class="trending-coins-grid">
|
|
|
${trendingData.trending.map((coin, index) => {
|
|
|
const coinSymbol = coin.symbol || coin.id || 'N/A';
|
|
|
const coinName = coin.name || 'Unknown';
|
|
|
const marketCapRank = coin.market_cap_rank || null;
|
|
|
const score = coin.score !== undefined && coin.score !== null ? coin.score : null;
|
|
|
const thumb = coin.thumb || null;
|
|
|
|
|
|
return `
|
|
|
<div class="trending-coin-card">
|
|
|
<div class="trending-coin-rank">#${index + 1}</div>
|
|
|
<div class="trending-coin-content">
|
|
|
${thumb ? `<img src="${thumb}" alt="${coinName}" class="trending-coin-thumb" onerror="this.style.display='none'">` : ''}
|
|
|
<div class="trending-coin-info">
|
|
|
<div class="trending-coin-name">
|
|
|
<strong>${coinSymbol}</strong>
|
|
|
<span class="trending-coin-fullname">${coinName}</span>
|
|
|
</div>
|
|
|
${marketCapRank ? `<div class="trending-coin-meta">Market Cap Rank: #${marketCapRank}</div>` : ''}
|
|
|
</div>
|
|
|
</div>
|
|
|
${score !== null && score > 0 ? `
|
|
|
<div class="trending-coin-score">
|
|
|
<div class="trending-coin-score-value">${score.toFixed(2)}</div>
|
|
|
<div class="trending-coin-score-label">Score</div>
|
|
|
</div>
|
|
|
` : ''}
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('')}
|
|
|
</div>
|
|
|
`;
|
|
|
} else {
|
|
|
trendingDiv.innerHTML = '<div class="alert alert-warning">No trending data available</div>';
|
|
|
}
|
|
|
} else {
|
|
|
throw new Error('Trending endpoint not available');
|
|
|
}
|
|
|
} catch (trendingError) {
|
|
|
console.warn('Trending endpoint error:', trendingError);
|
|
|
trendingDiv.innerHTML = '<div class="alert alert-warning">Trending data unavailable</div>';
|
|
|
}
|
|
|
|
|
|
|
|
|
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 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.';
|
|
|
}
|
|
|
|
|
|
|
|
|
const progressPercent = fgValue;
|
|
|
|
|
|
|
|
|
const circumference = 2 * Math.PI * 90;
|
|
|
const offset = circumference - (progressPercent / 100) * circumference;
|
|
|
|
|
|
fgDiv.innerHTML = `
|
|
|
<div style="display: grid; gap: 24px;">
|
|
|
<!-- Main Fear & Greed Index Display with Circular Gauge -->
|
|
|
<div style="background: ${bgGradient}; border: 2px solid ${fgColor}40; border-radius: 20px; padding: 40px; text-align: center; position: relative; overflow: hidden;">
|
|
|
<!-- Background Pattern -->
|
|
|
<div style="position: absolute; top: -50%; right: -50%; width: 200%; height: 200%; background: radial-gradient(circle, ${fgColor}15 0%, transparent 70%); pointer-events: none; animation: pulse 3s ease-in-out infinite;"></div>
|
|
|
|
|
|
<div style="position: relative; z-index: 1;">
|
|
|
<!-- Circular Gauge -->
|
|
|
<div style="position: relative; width: 240px; height: 240px; margin: 0 auto 24px;">
|
|
|
<svg width="240" height="240" style="transform: rotate(-90deg);">
|
|
|
<!-- Background Circle -->
|
|
|
<circle cx="120" cy="120" r="90" fill="none" stroke="rgba(255, 255, 255, 0.1)" stroke-width="12" />
|
|
|
<!-- Progress Circle -->
|
|
|
<circle
|
|
|
cx="120"
|
|
|
cy="120"
|
|
|
r="90"
|
|
|
fill="none"
|
|
|
stroke="${fgColor}"
|
|
|
stroke-width="12"
|
|
|
stroke-linecap="round"
|
|
|
stroke-dasharray="${circumference}"
|
|
|
stroke-dashoffset="${offset}"
|
|
|
style="transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1); filter: drop-shadow(0 0 10px ${fgColor}60);"
|
|
|
/>
|
|
|
</svg>
|
|
|
<!-- Center Content -->
|
|
|
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
|
|
|
<div style="font-size: 48px; margin-bottom: 8px; filter: drop-shadow(0 2px 4px ${fgColor}40);">
|
|
|
${sentimentIcon}
|
|
|
</div>
|
|
|
<div style="font-size: 56px; font-weight: 900; line-height: 1; background: linear-gradient(135deg, ${fgColor}, ${fgColor}dd); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;">
|
|
|
${fgValue}
|
|
|
</div>
|
|
|
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">/ 100</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Classification Label -->
|
|
|
<div style="font-size: 28px; font-weight: 700; color: ${fgColor}; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 2px; text-shadow: 0 0 20px ${fgColor}40;">
|
|
|
${sentimentClass}
|
|
|
</div>
|
|
|
|
|
|
<!-- Description -->
|
|
|
<div style="font-size: 14px; color: var(--text-secondary); max-width: 400px; margin: 0 auto; line-height: 1.6;">
|
|
|
${description}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Progress Bar Visualization -->
|
|
|
<div>
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
|
|
<span style="font-size: 14px; font-weight: 600; color: var(--text-secondary);">Fear & Greed Index</span>
|
|
|
<span style="font-size: 14px; font-weight: 700; color: ${fgColor};">${fgValue}/100</span>
|
|
|
</div>
|
|
|
|
|
|
<!-- Progress Bar Container -->
|
|
|
<div class="sentiment-progress-bar">
|
|
|
<!-- Progress Fill with Gradient -->
|
|
|
<div class="sentiment-progress-fill" style="width: ${progressPercent}%; background: linear-gradient(90deg, ${fgColor} 0%, ${fgColor}dd 100%); box-shadow: 0 0 20px ${fgColor}40;">
|
|
|
<div style="position: absolute; right: 0; top: 50%; transform: translateY(-50%); width: 4px; height: 60%; background: rgba(255, 255, 255, 0.5); border-radius: 2px;"></div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Scale Markers -->
|
|
|
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; justify-content: space-between; align-items: center; padding: 0 8px; pointer-events: none;">
|
|
|
<span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">0</span>
|
|
|
<span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">25</span>
|
|
|
<span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">50</span>
|
|
|
<span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">75</span>
|
|
|
<span style="font-size: 10px; color: var(--text-secondary); font-weight: 600;">100</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Scale Labels -->
|
|
|
<div style="display: flex; justify-content: space-between; margin-top: 8px; font-size: 11px; color: var(--text-secondary);">
|
|
|
<span>π± Extreme Fear</span>
|
|
|
<span>β οΈ Fear</span>
|
|
|
<span>π Neutral</span>
|
|
|
<span>π Greed</span>
|
|
|
<span>π Extreme Greed</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Additional Info Grid -->
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px; margin-top: 8px;">
|
|
|
<div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);">
|
|
|
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Current Value</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: ${fgColor};">${fgValue}</div>
|
|
|
</div>
|
|
|
<div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);">
|
|
|
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Classification</div>
|
|
|
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary);">${fgLabel}</div>
|
|
|
</div>
|
|
|
${sentimentData.timestamp ? `
|
|
|
<div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);">
|
|
|
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Last Update</div>
|
|
|
<div style="font-size: 12px; font-weight: 600; color: var(--text-primary);">${new Date(sentimentData.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })}</div>
|
|
|
</div>
|
|
|
` : ''}
|
|
|
<div style="padding: 16px; background: rgba(0, 0, 0, 0.2); border-radius: 12px; text-align: center; border: 1px solid var(--border);">
|
|
|
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">Source</div>
|
|
|
<div style="font-size: 12px; font-weight: 600; color: var(--text-primary);">Alternative.me</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Market Interpretation -->
|
|
|
<div style="padding: 20px; background: rgba(0, 0, 0, 0.3); border-radius: 12px; border-left: 4px solid ${fgColor};">
|
|
|
<div style="display: flex; align-items: start; gap: 12px;">
|
|
|
<div style="font-size: 24px;">π‘</div>
|
|
|
<div>
|
|
|
<div style="font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">Market Interpretation</div>
|
|
|
<div style="font-size: 13px; color: var(--text-secondary); line-height: 1.6;">
|
|
|
${fgValue >= 75 ?
|
|
|
'The market is showing extreme greed. Historically, this may indicate a potential market top. Consider taking profits and being cautious with new positions.' :
|
|
|
fgValue >= 50 ?
|
|
|
'Market sentiment is positive with greed prevailing. This suggests bullish momentum, but monitor for overbought conditions.' :
|
|
|
fgValue >= 25 ?
|
|
|
'Fear is present in the market. This could indicate a buying opportunity for long-term investors, but exercise caution.' :
|
|
|
'Extreme fear dominates the market. Historically, this has often been a good time to buy, but ensure you have a solid risk management strategy.'}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
} else {
|
|
|
fgDiv.innerHTML = '<div class="alert alert-warning">Fear & Greed Index unavailable</div>';
|
|
|
}
|
|
|
} else {
|
|
|
throw new Error('Sentiment endpoint not available');
|
|
|
}
|
|
|
} catch (fgError) {
|
|
|
console.warn('Fear & Greed endpoint error:', fgError);
|
|
|
fgDiv.innerHTML = `
|
|
|
<div style="padding: 40px; text-align: center;">
|
|
|
<div style="font-size: 48px; margin-bottom: 16px;">β οΈ</div>
|
|
|
<div style="font-size: 18px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px;">Fear & Greed Index Unavailable</div>
|
|
|
<div style="font-size: 14px; color: var(--text-secondary);">Unable to fetch sentiment data at this time. Please try again later.</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
console.log('β
Market data loaded successfully');
|
|
|
} catch (error) {
|
|
|
console.error('β Error loading market data:', error);
|
|
|
getToast().error('Failed to load market data');
|
|
|
|
|
|
if (marketDiv) marketDiv.innerHTML = `<div class="alert alert-error">Error loading market data: ${error.message}</div>`;
|
|
|
if (trendingDiv) trendingDiv.innerHTML = '<div class="alert alert-error">Error loading trending coins</div>';
|
|
|
if (fgDiv) fgDiv.innerHTML = '<div class="alert alert-error">Error loading Fear & Greed Index</div>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchNewsFromAPI() {
|
|
|
console.log('π₯ Fetching news from CryptoCompare API...');
|
|
|
getToast().info('Fetching latest news from CryptoCompare...');
|
|
|
|
|
|
const newsListDiv = document.getElementById('news-list');
|
|
|
if (!newsListDiv) return;
|
|
|
|
|
|
newsListDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Fetching news from CryptoCompare API...</div>';
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/news/fetch?limit=50', { method: 'POST' });
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`Fetch API returned ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
console.log('Fetch news result:', data);
|
|
|
|
|
|
if (data.success) {
|
|
|
getToast().success(`Successfully fetched and saved ${data.saved} news articles!`);
|
|
|
|
|
|
loadNews();
|
|
|
} else {
|
|
|
getToast().error(`Failed to fetch news: ${data.error}`);
|
|
|
newsListDiv.innerHTML = `<div class="alert alert-error">Error: ${data.error}</div>`;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('β Error fetching news:', error);
|
|
|
getToast().error('Failed to fetch news from API');
|
|
|
newsListDiv.innerHTML = `<div class="alert alert-error">Error fetching news: ${error.message}</div>`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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) {
|
|
|
|
|
|
const sortedNews = [...data.news].sort((a, b) => {
|
|
|
const dateA = new Date(a.published_date || a.analyzed_at || 0);
|
|
|
const dateB = new Date(b.published_date || b.analyzed_at || 0);
|
|
|
return dateB - dateA;
|
|
|
});
|
|
|
|
|
|
newsListDiv.innerHTML = `
|
|
|
<div style="display: grid; gap: 20px;">
|
|
|
${sortedNews.map(article => {
|
|
|
const timeAgo = formatTimeAgo(article.published_date || article.analyzed_at);
|
|
|
const symbols = Array.isArray(article.related_symbols)
|
|
|
? article.related_symbols
|
|
|
: (typeof article.related_symbols === 'string'
|
|
|
? (article.related_symbols.startsWith('[')
|
|
|
? JSON.parse(article.related_symbols)
|
|
|
: article.related_symbols.split(',').map(s => s.trim()))
|
|
|
: []);
|
|
|
|
|
|
return `
|
|
|
<div class="news-card" onclick="${article.url ? `window.open('${article.url}', '_blank')` : ''}">
|
|
|
<div class="news-card-image">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
|
|
|
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="news-card-content">
|
|
|
<h4 class="news-card-title">
|
|
|
${article.url ? `<a href="${article.url}" target="_blank">${article.title || 'Untitled'}</a>` : (article.title || 'Untitled')}
|
|
|
</h4>
|
|
|
${article.content ? `<p class="news-card-excerpt">${article.content.substring(0, 200)}${article.content.length > 200 ? '...' : ''}</p>` : ''}
|
|
|
<div class="news-card-meta">
|
|
|
<div class="news-card-source">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
|
|
|
<polyline points="15 3 21 3 21 9"></polyline>
|
|
|
<line x1="10" y1="14" x2="21" y2="3"></line>
|
|
|
</svg>
|
|
|
${article.source || 'Unknown'}
|
|
|
</div>
|
|
|
<div class="news-card-time">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
<polyline points="12 6 12 12 16 14"></polyline>
|
|
|
</svg>
|
|
|
<span class="time-ago">${timeAgo}</span>
|
|
|
</div>
|
|
|
${article.sentiment_label ? `<span class="sentiment-badge ${article.sentiment_label.toLowerCase()}">${article.sentiment_label}</span>` : ''}
|
|
|
</div>
|
|
|
${symbols.length > 0 ? `
|
|
|
<div class="news-card-symbols" style="margin-top: 10px;">
|
|
|
${symbols.slice(0, 5).map(symbol => `<span class="symbol-badge">${symbol}</span>`).join('')}
|
|
|
${symbols.length > 5 ? `<span class="symbol-badge">+${symbols.length - 5}</span>` : ''}
|
|
|
</div>
|
|
|
` : ''}
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('')}
|
|
|
</div>
|
|
|
<div style="margin-top: 20px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px; text-align: center; font-size: 13px; color: var(--text-secondary);">
|
|
|
Showing ${sortedNews.length} article${sortedNews.length !== 1 ? 's' : ''}${data.source ? ` from ${data.source}` : ''}
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
console.log('β
News loaded successfully');
|
|
|
getToast().success(`Loaded ${sortedNews.length} news articles`);
|
|
|
} else {
|
|
|
newsListDiv.innerHTML = '<div class="alert alert-warning">No news articles available at the moment. Click "Fetch Latest News" to load articles.</div>';
|
|
|
console.warn('No news data available');
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('β Error loading news:', error);
|
|
|
getToast().error('Failed to load news');
|
|
|
newsListDiv.innerHTML = `<div class="alert alert-error">Error loading news: ${error.message}<br>Please check your internet connection and try again.</div>`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
const statusRes = await fetch('/api/models/status');
|
|
|
if (statusRes.ok) {
|
|
|
const statusData = await statusRes.json();
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
const isLazyLoading = hfMode === 'public' && pipelinesLoaded === 0 && modelsCount > 0;
|
|
|
const alertClass = isLazyLoading ? 'alert-info' : (isOk ? 'alert-success' : 'alert-warning');
|
|
|
const statusIcon = isLazyLoading ? 'βΉοΈ' : (isOk ? 'β
' : 'β οΈ');
|
|
|
|
|
|
modelsStatusDiv.innerHTML = `
|
|
|
<div class="alert ${alertClass}">
|
|
|
<strong>Status:</strong> ${statusIcon} ${statusMessage}<br>
|
|
|
<strong>Models Configured:</strong> ${modelsCount} (Lazy Loading)<br>
|
|
|
<strong>Models Loaded:</strong> ${pipelinesLoaded}<br>
|
|
|
<strong>HF Mode:</strong> ${hfMode}<br>
|
|
|
<strong>Transformers:</strong> ${transformersAvailable ? 'β
Available' : 'β Not Available'}
|
|
|
${isLazyLoading ? '<br><br><em>π‘ Models will load automatically on first use (sentiment analysis, etc.)</em>' : ''}
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
const modelsStatEl = document.getElementById('stat-models');
|
|
|
const sidebarModelsEl = document.getElementById('sidebar-models');
|
|
|
|
|
|
const displayCount = pipelinesLoaded > 0 ? pipelinesLoaded : modelsCount;
|
|
|
if (modelsStatEl) {
|
|
|
modelsStatEl.textContent = displayCount;
|
|
|
}
|
|
|
if (sidebarModelsEl) {
|
|
|
sidebarModelsEl.textContent = displayCount;
|
|
|
}
|
|
|
} else {
|
|
|
throw new Error('Models status endpoint not available');
|
|
|
}
|
|
|
|
|
|
|
|
|
const listRes = await fetch('/api/models/list');
|
|
|
if (listRes.ok) {
|
|
|
const listData = await listRes.json();
|
|
|
|
|
|
|
|
|
const sidebarModels = document.getElementById('sidebar-models');
|
|
|
const modelsStatEl = document.getElementById('stat-models');
|
|
|
const totalModels = listData.total_models || (listData.models ? listData.models.length : 0);
|
|
|
const loadedModels = listData.models ? listData.models.filter(m => m.loaded).length : 0;
|
|
|
|
|
|
if (sidebarModels) {
|
|
|
sidebarModels.textContent = loadedModels > 0 ? loadedModels : totalModels;
|
|
|
}
|
|
|
if (modelsStatEl) {
|
|
|
modelsStatEl.textContent = loadedModels > 0 ? loadedModels : totalModels;
|
|
|
}
|
|
|
|
|
|
if (listData.models && listData.models.length > 0) {
|
|
|
modelsListDiv.innerHTML = `
|
|
|
<div style="overflow-x: auto;">
|
|
|
<table>
|
|
|
<thead>
|
|
|
<tr>
|
|
|
<th>Model ID</th>
|
|
|
<th>Task</th>
|
|
|
<th>Category</th>
|
|
|
<th>Status</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
${listData.models.map(model => {
|
|
|
const modelId = model.model_id || model.id || model.key || 'N/A';
|
|
|
const task = model.task || 'N/A';
|
|
|
const category = model.category || 'N/A';
|
|
|
const isLoaded = model.loaded === true;
|
|
|
const hasError = model.error && model.error.length > 0;
|
|
|
const statusClass = isLoaded ? 'available' : (hasError ? 'error' : 'standby');
|
|
|
const statusText = isLoaded ? 'β
Loaded' : (hasError ? 'β Error' : 'βΈοΈ Standby');
|
|
|
return `
|
|
|
<tr>
|
|
|
<td><strong>${modelId}</strong></td>
|
|
|
<td>${task}</td>
|
|
|
<td>${category}</td>
|
|
|
<td><span class="model-status ${statusClass}">${statusText}</span></td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('')}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
`;
|
|
|
} else {
|
|
|
modelsListDiv.innerHTML = '<div class="alert alert-warning">No models available</div>';
|
|
|
}
|
|
|
} else {
|
|
|
throw new Error('Models list endpoint not available');
|
|
|
}
|
|
|
|
|
|
console.log('β
Models loaded successfully');
|
|
|
} catch (error) {
|
|
|
console.error('β Error loading models:', error);
|
|
|
getToast().error('Failed to load models');
|
|
|
|
|
|
if (modelsStatusDiv) modelsStatusDiv.innerHTML = `<div class="alert alert-error">Error loading models status: ${error.message}</div>`;
|
|
|
if (modelsListDiv) modelsListDiv.innerHTML = '<div class="alert alert-error">Error loading models list</div>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function initializeModels() {
|
|
|
getToast().info('Initializing models... This may take a moment.');
|
|
|
|
|
|
const modelsStatusDiv = document.getElementById('models-status');
|
|
|
if (modelsStatusDiv) {
|
|
|
modelsStatusDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Initializing models...</div>';
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/models/initialize', { method: 'POST' });
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`Initialize returned ${response.status}`);
|
|
|
}
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
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.');
|
|
|
}
|
|
|
|
|
|
|
|
|
await loadModels();
|
|
|
} catch (error) {
|
|
|
console.error('Error initializing models:', error);
|
|
|
getToast().error('Failed to initialize models: ' + error.message);
|
|
|
|
|
|
if (modelsStatusDiv) {
|
|
|
modelsStatusDiv.innerHTML = `<div class="alert alert-error">Error initializing models: ${error.message}</div>`;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function loadSettings() {
|
|
|
const apiInfoDiv = document.getElementById('api-info');
|
|
|
if (apiInfoDiv) {
|
|
|
apiInfoDiv.innerHTML = `
|
|
|
<div class="alert alert-info">
|
|
|
<strong>API Base URL:</strong> ${window.location.origin}<br>
|
|
|
<strong>Documentation:</strong> <a href="/docs" target="_blank" style="color: var(--primary);">/docs</a><br>
|
|
|
<strong>Health Check:</strong> <a href="/health" target="_blank" style="color: var(--primary);">/health</a>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function saveSettings() {
|
|
|
getToast().success('Settings saved successfully!');
|
|
|
}
|
|
|
|
|
|
function toggleTheme() {
|
|
|
document.body.classList.toggle('light-theme');
|
|
|
const themeSelect = document.getElementById('theme-select');
|
|
|
if (themeSelect) {
|
|
|
themeSelect.value = document.body.classList.contains('light-theme') ? 'light' : 'dark';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function changeTheme(theme) {
|
|
|
if (theme === 'light') {
|
|
|
document.body.classList.add('light-theme');
|
|
|
} else {
|
|
|
document.body.classList.remove('light-theme');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function createSentimentGauge(containerId, sentimentValue, sentimentClass) {
|
|
|
const container = document.getElementById(containerId);
|
|
|
if (!container) return null;
|
|
|
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
canvas.id = `gauge-${containerId}`;
|
|
|
canvas.width = 300;
|
|
|
canvas.height = 150;
|
|
|
container.appendChild(canvas);
|
|
|
|
|
|
|
|
|
let gaugeValue = 50;
|
|
|
if (sentimentClass === 'bullish' || sentimentClass === 'positive') {
|
|
|
gaugeValue = 50 + (sentimentValue * 50);
|
|
|
} else if (sentimentClass === 'bearish' || sentimentClass === 'negative') {
|
|
|
gaugeValue = 50 - (sentimentValue * 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;
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
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';
|
|
|
else if (gaugeValue >= 50) gaugeColor = '#3b82f6';
|
|
|
else if (gaugeValue >= 30) gaugeColor = '#f59e0b';
|
|
|
else gaugeColor = '#ef4444';
|
|
|
|
|
|
ctx.strokeStyle = gaugeColor;
|
|
|
ctx.stroke();
|
|
|
|
|
|
|
|
|
ctx.fillStyle = '#f9fafb';
|
|
|
ctx.font = 'bold 32px Inter, sans-serif';
|
|
|
ctx.textAlign = 'center';
|
|
|
ctx.textBaseline = 'middle';
|
|
|
ctx.fillText(Math.round(gaugeValue), centerX, centerY + 15);
|
|
|
|
|
|
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
|
|
|
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>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
function createConfidenceBar(confidence) {
|
|
|
const confidencePercent = Math.round(confidence * 100);
|
|
|
return `
|
|
|
<div class="confidence-bar-container">
|
|
|
<div class="confidence-bar-label">
|
|
|
<span>Model Confidence</span>
|
|
|
<span>${confidencePercent}%</span>
|
|
|
</div>
|
|
|
<div class="confidence-bar">
|
|
|
<div class="confidence-bar-fill" style="width: ${confidencePercent}%;"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
async function analyzeGlobalSentiment() {
|
|
|
getToast().info('Analyzing global market sentiment...');
|
|
|
const resultDiv = document.getElementById('global-sentiment-result');
|
|
|
if (resultDiv) {
|
|
|
resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/analyze', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
text: 'Overall cryptocurrency market sentiment analysis',
|
|
|
mode: 'crypto'
|
|
|
})
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error(`API returned ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.available && data.sentiment) {
|
|
|
const sentiment = data.sentiment.toUpperCase();
|
|
|
const confidence = data.confidence || 0;
|
|
|
const sentimentClass = sentiment.includes('POSITIVE') || sentiment.includes('BULLISH') ? 'bullish' :
|
|
|
sentiment.includes('NEGATIVE') || sentiment.includes('BEARISH') ? 'bearish' : 'neutral';
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
<div class="ai-result-card">
|
|
|
<div class="ai-result-header">
|
|
|
<h4>Global Market Sentiment</h4>
|
|
|
<span class="sentiment-badge ${sentimentClass}">${sentiment}</span>
|
|
|
</div>
|
|
|
<div class="sentiment-gauge-container" id="global-sentiment-gauge"></div>
|
|
|
<div style="text-align: center; margin: 20px 0;">
|
|
|
${getTrendArrow(sentimentClass)}
|
|
|
<span style="font-size: 18px; font-weight: 700; color: var(--${sentimentClass === 'bullish' ? 'success' : sentimentClass === 'bearish' ? 'danger' : 'warning'}); margin: 0 10px;">
|
|
|
${sentiment}
|
|
|
</span>
|
|
|
${getTrendArrow(sentimentClass)}
|
|
|
</div>
|
|
|
${createConfidenceBar(confidence)}
|
|
|
<p style="margin-top: 15px; color: var(--text-secondary); text-align: center;">
|
|
|
<strong>Model:</strong> ${data.model || 'AI Sentiment Analysis'} |
|
|
|
<strong>Engine:</strong> ${data.engine || 'N/A'}
|
|
|
</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
createSentimentGauge('global-sentiment-gauge', confidence, sentimentClass);
|
|
|
}, 100);
|
|
|
|
|
|
getToast().success('Sentiment analysis complete!');
|
|
|
} else {
|
|
|
resultDiv.innerHTML = '<div class="alert alert-warning">Sentiment analysis unavailable</div>';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error analyzing sentiment:', error);
|
|
|
resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`;
|
|
|
getToast().error('Failed to analyze sentiment');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function analyzeAssetSentiment() {
|
|
|
const symbol = document.getElementById('asset-symbol')?.value;
|
|
|
const text = document.getElementById('asset-sentiment-text')?.value;
|
|
|
|
|
|
if (!symbol) {
|
|
|
getToast().warning('Please select a trading pair');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
getToast().info('Analyzing asset sentiment...');
|
|
|
const resultDiv = document.getElementById('asset-sentiment-result');
|
|
|
if (resultDiv) {
|
|
|
resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/analyze', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
text: text || `Sentiment analysis for ${symbol}`,
|
|
|
mode: 'crypto',
|
|
|
symbol: symbol
|
|
|
})
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error(`API returned ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.available && data.sentiment) {
|
|
|
const sentiment = data.sentiment.toUpperCase();
|
|
|
const confidence = data.confidence || 0;
|
|
|
const sentimentClass = sentiment.includes('POSITIVE') || sentiment.includes('BULLISH') ? 'bullish' :
|
|
|
sentiment.includes('NEGATIVE') || sentiment.includes('BEARISH') ? 'bearish' : 'neutral';
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
<div class="ai-result-card">
|
|
|
<div class="ai-result-header">
|
|
|
<h4>${symbol} Sentiment</h4>
|
|
|
<span class="sentiment-badge ${sentimentClass}">${sentiment}</span>
|
|
|
</div>
|
|
|
<div class="sentiment-gauge-container" id="asset-sentiment-gauge"></div>
|
|
|
<div style="text-align: center; margin: 20px 0;">
|
|
|
${getTrendArrow(sentimentClass)}
|
|
|
<span style="font-size: 18px; font-weight: 700; color: var(--${sentimentClass === 'bullish' ? 'success' : sentimentClass === 'bearish' ? 'danger' : 'warning'}); margin: 0 10px;">
|
|
|
${sentiment}
|
|
|
</span>
|
|
|
${getTrendArrow(sentimentClass)}
|
|
|
</div>
|
|
|
${createConfidenceBar(confidence)}
|
|
|
<p style="margin-top: 15px; color: var(--text-secondary); text-align: center;">
|
|
|
<strong>Model:</strong> ${data.model || 'AI Sentiment Analysis'} |
|
|
|
<strong>Engine:</strong> ${data.engine || 'N/A'}
|
|
|
</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
createSentimentGauge('asset-sentiment-gauge', confidence, sentimentClass);
|
|
|
}, 100);
|
|
|
|
|
|
getToast().success('Asset sentiment analysis complete!');
|
|
|
} else {
|
|
|
resultDiv.innerHTML = '<div class="alert alert-warning">Sentiment analysis unavailable</div>';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error analyzing asset sentiment:', error);
|
|
|
resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`;
|
|
|
getToast().error('Failed to analyze asset sentiment');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function analyzeSentiment() {
|
|
|
const text = document.getElementById('sentiment-text')?.value;
|
|
|
const mode = document.getElementById('sentiment-mode')?.value || 'auto';
|
|
|
|
|
|
if (!text || text.trim() === '') {
|
|
|
getToast().warning('Please enter text to analyze');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
getToast().info('Analyzing sentiment...');
|
|
|
const resultDiv = document.getElementById('sentiment-result');
|
|
|
if (resultDiv) {
|
|
|
resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/analyze', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ text, mode })
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error(`API returned ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.available && data.sentiment) {
|
|
|
const sentiment = data.sentiment.toUpperCase();
|
|
|
const confidence = data.confidence || 0;
|
|
|
const sentimentClass = sentiment.includes('POSITIVE') || sentiment.includes('BULLISH') ? 'bullish' :
|
|
|
sentiment.includes('NEGATIVE') || sentiment.includes('BEARISH') ? 'bearish' : 'neutral';
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
<div class="ai-result-card">
|
|
|
<div class="ai-result-header">
|
|
|
<h4>Sentiment Analysis Result</h4>
|
|
|
<span class="sentiment-badge ${sentimentClass}">${sentiment}</span>
|
|
|
</div>
|
|
|
<div class="sentiment-gauge-container" id="sentiment-gauge"></div>
|
|
|
<div style="text-align: center; margin: 20px 0;">
|
|
|
${getTrendArrow(sentimentClass)}
|
|
|
<span style="font-size: 18px; font-weight: 700; color: var(--${sentimentClass === 'bullish' ? 'success' : sentimentClass === 'bearish' ? 'danger' : 'warning'}); margin: 0 10px;">
|
|
|
${sentiment}
|
|
|
</span>
|
|
|
${getTrendArrow(sentimentClass)}
|
|
|
</div>
|
|
|
${createConfidenceBar(confidence)}
|
|
|
<p style="margin-top: 15px; color: var(--text-secondary); text-align: center;">
|
|
|
<strong>Text:</strong> ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}<br>
|
|
|
<strong>Model:</strong> ${data.model || 'AI Sentiment Analysis'} |
|
|
|
<strong>Engine:</strong> ${data.engine || 'N/A'}
|
|
|
</p>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
createSentimentGauge('sentiment-gauge', confidence, sentimentClass);
|
|
|
}, 100);
|
|
|
|
|
|
getToast().success('Sentiment analysis complete!');
|
|
|
} else {
|
|
|
resultDiv.innerHTML = '<div class="alert alert-warning">Sentiment analysis unavailable</div>';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error analyzing sentiment:', error);
|
|
|
resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`;
|
|
|
getToast().error('Failed to analyze sentiment');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function runTradingAssistant() {
|
|
|
const symbol = document.getElementById('trading-symbol')?.value;
|
|
|
const context = document.getElementById('trading-context')?.value;
|
|
|
|
|
|
if (!symbol) {
|
|
|
getToast().warning('Please select a trading symbol');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
getToast().info('Generating trading signal...');
|
|
|
const resultDiv = document.getElementById('trading-assistant-result');
|
|
|
if (resultDiv) {
|
|
|
resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> Analyzing...</div>';
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/trading/decision', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
symbol: symbol,
|
|
|
context: context || `Trading decision for ${symbol}`
|
|
|
})
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error(`API returned ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.decision) {
|
|
|
const decision = data.decision.toUpperCase();
|
|
|
const confidence = data.confidence ? (data.confidence * 100).toFixed(2) : 'N/A';
|
|
|
const decisionClass = decision === 'BUY' ? 'bullish' : decision === 'SELL' ? 'bearish' : 'neutral';
|
|
|
|
|
|
resultDiv.innerHTML = `
|
|
|
<div class="ai-result-card">
|
|
|
<div class="ai-result-header">
|
|
|
<h4>${symbol} Trading Signal</h4>
|
|
|
<span class="sentiment-badge ${decisionClass}">${decision}</span>
|
|
|
</div>
|
|
|
<div style="display: flex; gap: 15px; justify-content: center;">
|
|
|
<div class="ai-result-metric">
|
|
|
<div class="ai-result-metric-value" style="color: var(--${decisionClass === 'bullish' ? 'success' : decisionClass === 'bearish' ? 'danger' : 'warning'});">${confidence}%</div>
|
|
|
<div class="ai-result-metric-label">Confidence</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
${data.reasoning ? `<p style="margin-top: 15px; color: var(--text-secondary);"><strong>Reasoning:</strong> ${data.reasoning}</p>` : ''}
|
|
|
<p style="margin-top: 10px; color: var(--text-muted); font-size: 12px;">
|
|
|
<strong>Model:</strong> ${data.model || 'AI Trading Assistant'}
|
|
|
</p>
|
|
|
</div>
|
|
|
`;
|
|
|
getToast().success('Trading signal generated!');
|
|
|
} else {
|
|
|
resultDiv.innerHTML = '<div class="alert alert-warning">Trading signal unavailable</div>';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error generating trading signal:', error);
|
|
|
resultDiv.innerHTML = `<div class="alert alert-error">Error: ${error.message}</div>`;
|
|
|
getToast().error('Failed to generate trading signal');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.AppState = AppState;
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
outputPre.innerHTML = colorCodeOutput(data.output);
|
|
|
|
|
|
|
|
|
updateDiagnosticSummary(data);
|
|
|
|
|
|
|
|
|
localStorage.setItem('lastDiagnosticRun', data.timestamp);
|
|
|
|
|
|
|
|
|
updateStatusCards(data.summary);
|
|
|
|
|
|
|
|
|
summaryDiv.style.display = 'block';
|
|
|
|
|
|
|
|
|
outputPre.scrollTop = outputPre.scrollHeight;
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('Diagnostic error:', error);
|
|
|
outputPre.innerHTML = `<span style="color: #ef4444;">β Error running diagnostic: ${error.message}</span>`;
|
|
|
showToast('β Diagnostic failed: ' + error.message, 'error');
|
|
|
} finally {
|
|
|
|
|
|
runBtn.disabled = false;
|
|
|
runBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 6px;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg>βΆοΈ Run Full Diagnostic';
|
|
|
progressDiv.style.display = 'none';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function colorCodeOutput(output) {
|
|
|
if (!output) return '';
|
|
|
|
|
|
return output
|
|
|
.replace(/β
/g, '<span style="color: #10b981;">β
</span>')
|
|
|
.replace(/β/g, '<span style="color: #ef4444;">β</span>')
|
|
|
.replace(/β οΈ/g, '<span style="color: #f59e0b;">β οΈ</span>')
|
|
|
.replace(/π/g, '<span style="color: #3b82f6;">π</span>')
|
|
|
.replace(/π¦/g, '<span style="color: #8b5cf6;">π¦</span>')
|
|
|
.replace(/π/g, '<span style="color: #06b6d4;">π</span>')
|
|
|
.replace(/π§ͺ/g, '<span style="color: #84cc16;">π§ͺ</span>')
|
|
|
.replace(/π/g, '<span style="color: #f97316;">π</span>')
|
|
|
.replace(/π‘/g, '<span style="color: #eab308;">π‘</span>')
|
|
|
.replace(/βοΈ/g, '<span style="color: #6b7280;">βοΈ</span>')
|
|
|
.split('\n').join('<br>');
|
|
|
}
|
|
|
|
|
|
function updateDiagnosticSummary(data) {
|
|
|
document.getElementById('summary-duration').textContent = `${data.duration_seconds}s`;
|
|
|
document.getElementById('summary-passed').textContent = data.summary.transformers_available && data.summary.hf_hub_connected ? '2/2' : '1/2';
|
|
|
document.getElementById('summary-failed').textContent = (!data.summary.transformers_available || !data.summary.hf_hub_connected) ? '1/2' : '0/2';
|
|
|
document.getElementById('summary-critical').textContent = data.summary.critical_issues.length;
|
|
|
|
|
|
const fixesDiv = document.getElementById('suggested-fixes');
|
|
|
if (data.summary.critical_issues.length > 0) {
|
|
|
fixesDiv.innerHTML = '<h4>π§ Suggested Fixes:</h4><ul>' +
|
|
|
data.summary.critical_issues.map(issue =>
|
|
|
`<li>${issue}</li>`
|
|
|
).join('') + '</ul>';
|
|
|
} else {
|
|
|
fixesDiv.innerHTML = '<p style="color: #10b981;">β
No critical issues found</p>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function updateStatusCards(summary) {
|
|
|
const transformersEl = document.getElementById('transformers-status-value');
|
|
|
if (transformersEl) {
|
|
|
transformersEl.textContent = summary.transformers_available ? 'Available' : 'Not Available';
|
|
|
transformersEl.style.color = summary.transformers_available ? 'var(--success)' : 'var(--danger)';
|
|
|
}
|
|
|
|
|
|
const hfEl = document.getElementById('hf-status-value');
|
|
|
if (hfEl) {
|
|
|
hfEl.textContent = summary.hf_hub_connected ? 'Connected' : 'Disconnected';
|
|
|
hfEl.style.color = summary.hf_hub_connected ? 'var(--success)' : 'var(--danger)';
|
|
|
}
|
|
|
|
|
|
const modelsEl = document.getElementById('models-status-value');
|
|
|
if (modelsEl) {
|
|
|
modelsEl.textContent = summary.models_loaded || 0;
|
|
|
}
|
|
|
|
|
|
const lastRun = localStorage.getItem('lastDiagnosticRun');
|
|
|
const lastTestEl = document.getElementById('last-test-value');
|
|
|
if (lastTestEl) {
|
|
|
lastTestEl.textContent = lastRun ? new Date(lastRun).toLocaleString() : 'Never';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function refreshDiagnosticStatus() {
|
|
|
try {
|
|
|
|
|
|
const modelsResponse = await fetch('/api/models/status');
|
|
|
if (modelsResponse.ok) {
|
|
|
const modelsData = await modelsResponse.json();
|
|
|
|
|
|
|
|
|
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)';
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
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');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let autoRefreshInterval = null;
|
|
|
let autoRefreshEnabled = false;
|
|
|
|
|
|
function toggleAutoRefresh() {
|
|
|
autoRefreshEnabled = !autoRefreshEnabled;
|
|
|
const btn = document.getElementById('auto-refresh-btn');
|
|
|
|
|
|
if (autoRefreshEnabled) {
|
|
|
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 4px;"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>Auto: ON (30s)';
|
|
|
btn.style.background = 'rgba(16, 185, 129, 0.2)';
|
|
|
btn.style.borderColor = 'var(--success)';
|
|
|
autoRefreshInterval = setInterval(() => {
|
|
|
refreshDiagnosticStatus();
|
|
|
loadSystemHealth();
|
|
|
loadProviderHealth();
|
|
|
}, 30000);
|
|
|
getToast().success('Auto-refresh enabled (30s interval)');
|
|
|
} else {
|
|
|
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: inline-block; vertical-align: middle; margin-right: 4px;"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>Auto: OFF';
|
|
|
btn.style.background = '';
|
|
|
btn.style.borderColor = '';
|
|
|
if (autoRefreshInterval) {
|
|
|
clearInterval(autoRefreshInterval);
|
|
|
autoRefreshInterval = null;
|
|
|
}
|
|
|
getToast().info('Auto-refresh disabled');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function switchDiagnosticTab(tabName) {
|
|
|
|
|
|
document.querySelectorAll('.diagnostic-tab-content').forEach(tab => {
|
|
|
tab.classList.remove('active');
|
|
|
});
|
|
|
document.querySelectorAll('.diagnostic-tab-btn').forEach(btn => {
|
|
|
btn.classList.remove('active');
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById(`diagnostic-tab-${tabName}`).classList.add('active');
|
|
|
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
|
|
|
|
|
|
|
|
if (tabName === 'health' && document.getElementById('health-details-content').innerHTML.includes('Click')) {
|
|
|
loadSystemHealth();
|
|
|
} else if (tabName === 'logs') {
|
|
|
loadRecentLogs();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadSystemHealth() {
|
|
|
try {
|
|
|
const response = await fetch('/api/diagnostics/health');
|
|
|
if (!response.ok) throw new Error('Failed to fetch health data');
|
|
|
|
|
|
const data = await response.json();
|
|
|
const container = document.getElementById('system-health-overview');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
const providers = data.providers?.summary || {};
|
|
|
const models = data.models?.summary || {};
|
|
|
const overall = data.overall_health || {};
|
|
|
|
|
|
container.innerHTML = `
|
|
|
<div class="stat-card gradient-blue">
|
|
|
<div class="stat-icon">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
|
|
|
<polyline points="22 4 12 14.01 9 11.01"></polyline>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-label">Providers</div>
|
|
|
<div class="stat-value">${providers.healthy || 0}/${providers.total || 0}</div>
|
|
|
<div class="stat-trend" style="color: ${overall.providers_ok ? 'var(--success)' : 'var(--warning)'};">
|
|
|
${overall.providers_ok ? 'β
Healthy' : 'β οΈ Degraded'}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-card gradient-purple">
|
|
|
<div class="stat-icon">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-label">AI Models</div>
|
|
|
<div class="stat-value">${models.healthy || 0}/${models.total || 0}</div>
|
|
|
<div class="stat-trend" style="color: ${overall.models_ok ? 'var(--success)' : 'var(--warning)'};">
|
|
|
${overall.models_ok ? 'β
Healthy' : 'β οΈ Degraded'}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-card gradient-orange">
|
|
|
<div class="stat-icon">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
|
<path d="M12 6v6l4 2"></path>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-label">In Cooldown</div>
|
|
|
<div class="stat-value">${(providers.in_cooldown || 0) + (models.in_cooldown || 0)}</div>
|
|
|
<div class="stat-trend">${(providers.in_cooldown || 0) + (models.in_cooldown || 0) > 0 ? 'β οΈ Some services cooling' : 'β
All active'}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="stat-card gradient-green">
|
|
|
<div class="stat-icon">
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
|
|
</svg>
|
|
|
</div>
|
|
|
<div class="stat-content">
|
|
|
<div class="stat-label">Degraded</div>
|
|
|
<div class="stat-value">${(providers.degraded || 0) + (models.degraded || 0)}</div>
|
|
|
<div class="stat-trend">${(providers.degraded || 0) + (models.degraded || 0) > 0 ? 'β οΈ Needs attention' : 'β
All optimal'}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
|
|
|
const healthDetails = document.getElementById('health-details-content');
|
|
|
if (healthDetails) {
|
|
|
healthDetails.innerHTML = `
|
|
|
<div style="display: grid; gap: 16px;">
|
|
|
<div>
|
|
|
<h4 style="margin-bottom: 12px;">Provider Health Summary</h4>
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;">
|
|
|
<div style="padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px;">
|
|
|
<div style="color: var(--text-secondary); font-size: 12px;">Total</div>
|
|
|
<div style="font-size: 24px; font-weight: 700;">${providers.total || 0}</div>
|
|
|
</div>
|
|
|
<div style="padding: 12px; background: rgba(16,185,129,0.1); border-radius: 8px; border: 1px solid rgba(16,185,129,0.3);">
|
|
|
<div style="color: var(--success); font-size: 12px;">Healthy</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: var(--success);">${providers.healthy || 0}</div>
|
|
|
</div>
|
|
|
<div style="padding: 12px; background: rgba(245,158,11,0.1); border-radius: 8px; border: 1px solid rgba(245,158,11,0.3);">
|
|
|
<div style="color: var(--warning); font-size: 12px;">Degraded</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: var(--warning);">${providers.degraded || 0}</div>
|
|
|
</div>
|
|
|
<div style="padding: 12px; background: rgba(239,68,68,0.1); border-radius: 8px; border: 1px solid rgba(239,68,68,0.3);">
|
|
|
<div style="color: var(--danger); font-size: 12px;">Unavailable</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: var(--danger);">${providers.unavailable || 0}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div>
|
|
|
<h4 style="margin-bottom: 12px;">Model Health Summary</h4>
|
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px;">
|
|
|
<div style="padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px;">
|
|
|
<div style="color: var(--text-secondary); font-size: 12px;">Total</div>
|
|
|
<div style="font-size: 24px; font-weight: 700;">${models.total || 0}</div>
|
|
|
</div>
|
|
|
<div style="padding: 12px; background: rgba(16,185,129,0.1); border-radius: 8px; border: 1px solid rgba(16,185,129,0.3);">
|
|
|
<div style="color: var(--success); font-size: 12px;">Healthy</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: var(--success);">${models.healthy || 0}</div>
|
|
|
</div>
|
|
|
<div style="padding: 12px; background: rgba(245,158,11,0.1); border-radius: 8px; border: 1px solid rgba(245,158,11,0.3);">
|
|
|
<div style="color: var(--warning); font-size: 12px;">Degraded</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: var(--warning);">${models.degraded || 0}</div>
|
|
|
</div>
|
|
|
<div style="padding: 12px; background: rgba(239,68,68,0.1); border-radius: 8px; border: 1px solid rgba(239,68,68,0.3);">
|
|
|
<div style="color: var(--danger); font-size: 12px;">Unavailable</div>
|
|
|
<div style="font-size: 24px; font-weight: 700; color: var(--danger);">${models.unavailable || 0}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading system health:', error);
|
|
|
getToast().error('Failed to load system health');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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 = '';
|
|
|
|
|
|
|
|
|
providers.slice(0, 10).forEach(entry => {
|
|
|
const statusClass = entry.status === 'healthy' ? 'healthy' :
|
|
|
entry.status === 'degraded' ? 'degraded' :
|
|
|
entry.status === 'unavailable' ? 'unavailable' : 'unknown';
|
|
|
const lastCheck = entry.last_success ? new Date(entry.last_success * 1000).toLocaleString() : 'Never';
|
|
|
html += `
|
|
|
<tr style="border-bottom: 1px solid var(--border);">
|
|
|
<td style="padding: 12px;">${entry.name || entry.id}</td>
|
|
|
<td style="padding: 12px;"><span style="color: var(--text-secondary);">Provider</span></td>
|
|
|
<td style="padding: 12px;"><span class="health-badge ${statusClass}">${entry.status || 'unknown'}</span></td>
|
|
|
<td style="padding: 12px; color: var(--text-secondary); font-size: 12px;">${lastCheck}</td>
|
|
|
<td style="padding: 12px;">
|
|
|
${entry.in_cooldown ? '<span style="color: var(--warning); font-size: 12px;">β³ Cooldown</span>' : '-'}
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
|
|
|
models.slice(0, 10).forEach(entry => {
|
|
|
const statusClass = entry.status === 'healthy' ? 'healthy' :
|
|
|
entry.status === 'degraded' ? 'degraded' :
|
|
|
entry.status === 'unavailable' ? 'unavailable' : 'unknown';
|
|
|
html += `
|
|
|
<tr style="border-bottom: 1px solid var(--border);">
|
|
|
<td style="padding: 12px;">${entry.name || entry.key || 'Unknown'}</td>
|
|
|
<td style="padding: 12px;"><span style="color: var(--text-secondary);">AI Model</span></td>
|
|
|
<td style="padding: 12px;"><span class="health-badge ${statusClass}">${entry.status || 'unknown'}</span></td>
|
|
|
<td style="padding: 12px; color: var(--text-secondary); font-size: 12px;">-</td>
|
|
|
<td style="padding: 12px;">
|
|
|
${entry.in_cooldown ? '<span style="color: var(--warning); font-size: 12px;">β³ Cooldown</span>' : '-'}
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
});
|
|
|
|
|
|
if (html === '') {
|
|
|
html = '<tr><td colspan="5" style="padding: 20px; text-align: center; color: var(--text-secondary);">No health data available</td></tr>';
|
|
|
}
|
|
|
|
|
|
tbody.innerHTML = html;
|
|
|
} catch (error) {
|
|
|
console.error('Error loading provider health:', error);
|
|
|
getToast().error('Failed to load provider health');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
window.triggerSelfHeal = triggerSelfHeal;
|
|
|
|
|
|
|
|
|
async function testAPIEndpoints() {
|
|
|
const resultsDiv = document.getElementById('api-test-results');
|
|
|
if (!resultsDiv) return;
|
|
|
|
|
|
resultsDiv.innerHTML = '<div class="spinner"></div> <span>Testing API endpoints...</span>';
|
|
|
|
|
|
const endpoints = [
|
|
|
{ name: 'Health Check', url: '/api/health' },
|
|
|
{ name: 'System Status', url: '/api/status' },
|
|
|
{ name: 'Market Data', url: '/api/market' },
|
|
|
{ name: 'Models Status', url: '/api/models/status' },
|
|
|
{ name: 'Providers', url: '/api/providers' },
|
|
|
];
|
|
|
|
|
|
let html = '<div style="display: grid; gap: 12px;">';
|
|
|
let passed = 0;
|
|
|
let failed = 0;
|
|
|
|
|
|
for (const endpoint of endpoints) {
|
|
|
try {
|
|
|
const startTime = performance.now();
|
|
|
const response = await fetch(endpoint.url);
|
|
|
const duration = (performance.now() - startTime).toFixed(0);
|
|
|
|
|
|
if (response.ok) {
|
|
|
passed++;
|
|
|
html += `
|
|
|
<div style="padding: 12px; background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 8px;">
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
<div>
|
|
|
<strong style="color: var(--success);">β
${endpoint.name}</strong>
|
|
|
<div style="color: var(--text-secondary); font-size: 12px; margin-top: 4px;">${endpoint.url}</div>
|
|
|
</div>
|
|
|
<div style="color: var(--text-secondary); font-size: 12px;">${duration}ms</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
} else {
|
|
|
failed++;
|
|
|
html += `
|
|
|
<div style="padding: 12px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px;">
|
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
<div>
|
|
|
<strong style="color: var(--danger);">β ${endpoint.name}</strong>
|
|
|
<div style="color: var(--text-secondary); font-size: 12px; margin-top: 4px;">${endpoint.url} - HTTP ${response.status}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
failed++;
|
|
|
html += `
|
|
|
<div style="padding: 12px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 8px;">
|
|
|
<div>
|
|
|
<strong style="color: var(--danger);">β ${endpoint.name}</strong>
|
|
|
<div style="color: var(--text-secondary); font-size: 12px; margin-top: 4px;">${endpoint.url} - ${error.message}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
html += `</div><div style="margin-top: 16px; padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px;">
|
|
|
<strong>Summary:</strong> ${passed} passed, ${failed} failed
|
|
|
</div>`;
|
|
|
|
|
|
resultsDiv.innerHTML = html;
|
|
|
getToast().success(`API tests completed: ${passed} passed, ${failed} failed`);
|
|
|
}
|
|
|
|
|
|
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');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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');
|
|
|
}
|
|
|
|
|
|
window.testNetworkConnectivity = testNetworkConnectivity;
|
|
|
|
|
|
async function loadRecentLogs() {
|
|
|
try {
|
|
|
const response = await fetch('/api/logs/recent');
|
|
|
const data = await response.json();
|
|
|
const container = document.getElementById('recent-logs-content');
|
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
if (data.logs && data.logs.length > 0) {
|
|
|
let html = '<div style="display: grid; gap: 8px;">';
|
|
|
data.logs.slice(0, 20).forEach(log => {
|
|
|
const level = log.level || 'INFO';
|
|
|
const levelColor = level === 'ERROR' ? 'var(--danger)' :
|
|
|
level === 'WARNING' ? 'var(--warning)' :
|
|
|
level === 'INFO' ? 'var(--info)' : 'var(--text-secondary)';
|
|
|
html += `
|
|
|
<div style="padding: 10px; background: rgba(0,0,0,0.2); border-left: 3px solid ${levelColor}; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 12px;">
|
|
|
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
|
|
|
<span style="color: ${levelColor}; font-weight: 600;">[${level}]</span>
|
|
|
<span style="color: var(--text-secondary);">${log.timestamp || ''}</span>
|
|
|
</div>
|
|
|
<div style="color: var(--text-primary);">${log.message || JSON.stringify(log)}</div>
|
|
|
</div>
|
|
|
`;
|
|
|
});
|
|
|
html += '</div>';
|
|
|
container.innerHTML = html;
|
|
|
} else {
|
|
|
container.innerHTML = '<p class="text-secondary">No recent logs available</p>';
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading logs:', error);
|
|
|
document.getElementById('recent-logs-content').innerHTML = '<p style="color: var(--danger);">Failed to load logs</p>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
window.loadRecentLogs = loadRecentLogs;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.runDiagnostic = runDiagnostic;
|
|
|
window.refreshDiagnosticStatus = refreshDiagnosticStatus;
|
|
|
window.downloadDiagnosticLog = downloadDiagnosticLog;
|
|
|
window.toggleAutoRefresh = toggleAutoRefresh;
|
|
|
window.switchDiagnosticTab = switchDiagnosticTab;
|
|
|
|
|
|
|
|
|
function loadAITools() {
|
|
|
const iframe = document.getElementById('ai-tools-iframe');
|
|
|
const loading = document.getElementById('ai-tools-loading');
|
|
|
|
|
|
if (!iframe) return;
|
|
|
|
|
|
|
|
|
if (loading) loading.style.display = 'block';
|
|
|
iframe.style.display = 'none';
|
|
|
|
|
|
|
|
|
if (iframe.src && iframe.src.includes('/ai-tools')) {
|
|
|
|
|
|
setTimeout(() => {
|
|
|
if (loading) loading.style.display = 'none';
|
|
|
iframe.style.display = 'block';
|
|
|
}, 100);
|
|
|
} else {
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
refreshDiagnosticStatus();
|
|
|
loadSystemHealth();
|
|
|
loadProviderHealth();
|
|
|
});
|
|
|
|
|
|
console.log('β
App.js loaded successfully');
|
|
|
|