Really-amin's picture
Upload 303 files
b068b76 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<title>🚀 Crypto Intelligence Hub - Advanced Dashboard</title>
<!-- Google Fonts - Modern & Professional -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Manrope:wght@400;500;600;700;800&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<!-- Core Design System -->
<link rel="stylesheet" href="static/css/design-tokens.css" />
<link rel="stylesheet" href="static/css/glassmorphism.css" />
<link rel="stylesheet" href="static/css/design-system.css" />
<link rel="stylesheet" href="static/css/dashboard.css" />
<link rel="stylesheet" href="static/css/pro-dashboard.css" />
<link rel="stylesheet" href="static/css/sentiment-modern.css" />
<!-- Optional Enhanced Features (Loaded conditionally) -->
<link rel="stylesheet" href="static/css/mobile-responsive.css" media="screen" />
<link rel="stylesheet" href="static/css/toast.css" />
<link rel="stylesheet" href="static/css/accessibility.css" />
<link rel="stylesheet" href="static/css/navigation.css" />
<link rel="stylesheet" href="static/css/connection-status.css" />
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js" defer></script>
<!-- SVG status icon tweaks -->
<style>
.status-pill .status-icon {
margin-inline: 0.35rem;
flex-shrink: 0;
}
.status-pill .status-label {
white-space: nowrap;
}
</style>
</head>
<body data-theme="dark" id="main-body">
<!-- ===== تنظیم بک‌اند (اسکریپت) ===== -->
<script>
// تنظیم خودکار: اگر روی localhost هستیم از localhost استفاده کن، وگرنه از HF Space
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
window.BACKEND_URL = `http://${window.location.hostname}:7860`;
} else {
// برای HuggingFace Spaces
window.BACKEND_URL = 'https://really-amin-datasourceforcryptocurrency.hf.space';
}
</script>
<div class="app-shell">
<!-- Sidebar Navigation -->
<aside class="sidebar">
<div class="brand">
<div class="brand-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(96, 165, 250, 0.15)"/>
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="12" r="3" fill="currentColor" opacity="0.8"/>
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
</svg>
</div>
<div class="brand-text">
<strong>Crypto Intelligence Hub</strong>
<span class="env-pill">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" fill="rgba(129, 140, 248, 0.2)"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2"/>
</svg>
AI Powered
</span>
</div>
</div>
<nav class="nav">
<button class="nav-button active" data-nav="page-overview">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/><rect x="14" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/>
<rect x="3" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/><rect x="14" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.2"/>
</svg>
Overview
</button>
<button class="nav-button" data-nav="page-market">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="22" stroke-width="3"/>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" stroke-width="2.5"/>
</svg>
Market
</button>
<button class="nav-button" data-nav="page-chart">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 3v18h18" stroke-width="3"/>
<path d="M7 12l4-4 4 4 6-6v8H7z" stroke-width="2.5"/>
</svg>
Chart Lab
</button>
<button class="nav-button" data-nav="page-ai">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke-width="2.5"/>
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.3"/>
</svg>
AI Advisor
</button>
<button class="nav-button" data-nav="page-news">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" stroke-width="2.5"/>
<path d="M18 14h-8M15 10h-5M19 18h-3" stroke-width="2.5"/>
</svg>
News
</button>
<button class="nav-button" data-nav="page-providers">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="currentColor" opacity="0.2"/><path d="M2 17l10 5 10-5" fill="currentColor" opacity="0.2"/><path d="M2 12l10 5 10-5" stroke-width="2.5"/>
</svg>
Providers
</button>
<button class="nav-button" data-nav="page-datasets">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="currentColor" opacity="0.1"/>
<line x1="9" y1="3" x2="9" y2="21" stroke-width="3"/>
<line x1="3" y1="9" x2="21" y2="9" stroke-width="3"/>
</svg>
Datasets & Models
</button>
<button class="nav-button" data-nav="page-api">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 9h16M4 15h16" stroke-width="3"/>
<path d="M12 3v18" stroke-width="3"/>
<circle cx="12" cy="12" r="2.5" fill="currentColor" opacity="0.3"/>
</svg>
API Explorer
</button>
<button class="nav-button" data-nav="page-debug">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.1"/>
<line x1="12" y1="8" x2="12" y2="12" stroke-width="3"/>
<line x1="12" y1="16" x2="12.01" y2="16" stroke-width="3"/>
</svg>
Diagnostics
</button>
<button class="nav-button" data-nav="page-settings">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.2"/>
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke-width="2.5"/>
</svg>
Settings
</button>
</nav>
<div class="sidebar-footer">
<div class="footer-badge">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>AI Powered</span>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="main-area">
<!-- Top Bar with Status -->
<header class="topbar">
<div class="topbar-content">
<div class="topbar-icon">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(96, 165, 250, 0.1)"/>
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="12" r="3" fill="currentColor" opacity="0.8"/>
<path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
</svg>
</div>
<div class="topbar-text">
<h1>
<span class="title-gradient">Crypto Intelligence</span>
<span class="title-accent">Dashboard</span>
</h1>
<p class="text-muted">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="display: inline-block; vertical-align: middle; margin-right: 6px;">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M12 8v4l3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Live market data, AI-powered sentiment analysis, and comprehensive crypto intelligence
</p>
</div>
</div>
<div class="status-group">
<!-- API Health with SVG icon -->
<div class="status-pill" data-api-health data-state="warn">
<span class="status-dot"></span>
<svg class="status-icon" width="14" height="14" viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h16M4 12h10M4 17h7"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<span class="status-label">checking</span>
</div>
<!-- WebSocket Status with SVG icon -->
<div class="status-pill" data-ws-status data-state="warn">
<span class="status-dot"></span>
<svg class="status-icon" width="14" height="14" viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 5h16v6H4z"
stroke="currentColor"
stroke-width="2"
fill="none"
stroke-linejoin="round" />
<path d="M8 15h8M10 19h4"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round" />
</svg>
<span class="status-label">connecting</span>
</div>
</div>
</header>
<div class="page-container">
<!-- ========== OVERVIEW PAGE ========== -->
<section id="page-overview" class="page active">
<div class="section-header">
<h2 class="section-title">📊 Market Overview</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<span class="chip" style="background: var(--success-glow); color: var(--success-light);">Live Data</span>
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">Real-time</span>
</div>
</div>
<div class="grid-four" data-overview-stats></div>
<div class="glass-card" style="margin-top:1.5rem;">
<div class="card-header">
<h4>Market Overview - 24H</h4>
<button class="btn-secondary btn-sm" id="refresh-market-btn" onclick="refreshAllData()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none">
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2"/>
<path d="M20.49 9A9 9 0 005.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 013.51 15" stroke="currentColor" stroke-width="2"/>
</svg>
<span id="refresh-text">Refresh</span>
</button>
</div>
<div style="height: 400px; padding: 20px;">
<canvas id="market-overview-chart"></canvas>
</div>
</div>
<div class="glass-card" style="margin-top:1.5rem;">
<div class="card-header">
<h4>Top Cryptocurrencies</h4>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th>#</th>
<th>Coin</th>
<th>Price</th>
<th>24h %</th>
<th>7d %</th>
<th>Market Cap</th>
<th>Volume</th>
<th>Chart</th>
</tr>
</thead>
<tbody data-top-coins-body></tbody>
</table>
</div>
</div>
<div class="glass-card" style="margin-top:1.5rem;">
<h4>Global Sentiment</h4>
<canvas id="sentiment-chart" height="200"></canvas>
</div>
</section>
<!-- ========== MARKET PAGE ========== -->
<section id="page-market" class="page">
<div class="section-header">
<h2 class="section-title">💹 Market Explorer</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">50+ Coins</span>
<span class="chip" style="background: var(--secondary-glow); color: var(--secondary-light);">24/7 Updates</span>
</div>
</div>
<div class="search-bar">
<input type="text" placeholder="Search coins..." data-market-search id="market-search" name="market-search" />
<div class="button-group">
<button class="secondary active" data-timeframe="24h">24h</button>
<button class="secondary" data-timeframe="7d">7d</button>
<button class="secondary" data-timeframe="30d">30d</button>
</div>
<label class="input-chip">Live Updates
<div class="toggle">
<input type="checkbox" data-live-toggle id="live-toggle" name="live-toggle" />
<span></span>
</div>
</label>
</div>
<div class="table-container">
<table>
<thead>
<tr>
<th>#</th>
<th>Symbol</th>
<th>Name</th>
<th>Price</th>
<th>24h %</th>
<th>Volume</th>
<th>Market Cap</th>
</tr>
</thead>
<tbody data-market-body></tbody>
</table>
</div>
<aside class="drawer" data-market-drawer>
<header>
<h3 data-drawer-symbol></h3>
<button data-close-drawer>&times;</button>
</header>
<div class="drawer-body">
<div data-drawer-stats></div>
<div data-chart-wrapper style="margin:1rem 0;">
<canvas id="market-detail-chart" height="180"></canvas>
</div>
<h4>Related Headlines</h4>
<div data-drawer-news></div>
</div>
</aside>
</section>
<!-- ========== CHART LAB PAGE ========== -->
<section id="page-chart" class="page">
<div class="section-header">
<h2 class="section-title">📈 Chart Lab</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">TradingView Style</span>
<span class="chip" style="background: var(--secondary-glow); color: var(--secondary-light);">Professional</span>
</div>
</div>
<div class="glass-card">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; margin-bottom: 20px;">
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-normal);">Select Cryptocurrency</label>
<div style="position: relative;">
<input
type="text"
id="chartCoinSearch"
placeholder="Search Bitcoin, Ethereum..."
style="width: 100%; padding: 12px 40px 12px 16px; background: rgba(15, 23, 42, 0.6); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 10px; color: white; font-size: 14px;"
autocomplete="off"
/>
<svg style="position: absolute; right: 12px; top: 50%; transform: translateY(-50%); pointer-events: none; color: #94A3B8;" width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2"/>
</svg>
<div id="chartCoinDropdown" style="display: none; position: absolute; top: calc(100% + 8px); left: 0; right: 0; max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.95); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 12px; backdrop-filter: blur(20px); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6); z-index: 1000;"></div>
</div>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-normal);">Timeframe</label>
<div style="display: flex; gap: 8px;">
<button class="secondary" data-chart-timeframe="1">1D</button>
<button class="secondary active" data-chart-timeframe="7">7D</button>
<button class="secondary" data-chart-timeframe="30">30D</button>
<button class="secondary" data-chart-timeframe="90">90D</button>
<button class="secondary" data-chart-timeframe="365">1Y</button>
</div>
</div>
<div>
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: var(--text-normal);">Chart Type</label>
<select id="chartType" style="width: 100%; padding: 12px 16px; background: rgba(15, 23, 42, 0.6); border: 1px solid rgba(255, 255, 255, 0.2); border-radius: 10px; color: white; font-size: 14px;">
<option value="line">Line Chart</option>
<option value="area">Area Chart</option>
<option value="bar">Bar Chart</option>
</select>
</div>
</div>
<button class="primary" onclick="loadSelectedChart()" style="width: 100%;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2"/>
<path d="M7 10l4-4 4 4 6-6" stroke="currentColor" stroke-width="2"/>
</svg>
Load Chart
</button>
</div>
<div class="glass-card" style="margin-top: 20px;">
<div class="card-header">
<h4 id="selectedCoinTitle">Select a coin to view chart</h4>
<div style="display: flex; gap: 8px;">
<span class="badge badge-cyan" id="selectedCoinPrice">$0</span>
<span class="badge badge-success" id="selectedCoinChange">0%</span>
</div>
</div>
<div style="height: 500px; padding: 20px;">
<canvas id="price-chart"></canvas>
</div>
</div>
<div class="glass-card" style="margin-top: 20px;">
<div class="card-header">
<h4>Volume Analysis</h4>
</div>
<div style="height: 300px; padding: 20px;">
<canvas id="volume-chart"></canvas>
</div>
</div>
<div class="grid-two" style="margin-top: 20px;">
<div class="glass-card">
<div class="card-header">
<h4>RSI Indicator</h4>
</div>
<div style="height: 250px; padding: 20px;">
<canvas id="rsi-chart"></canvas>
</div>
</div>
<div class="glass-card">
<div class="card-header">
<h4>Moving Averages</h4>
</div>
<div style="height: 250px; padding: 20px;">
<canvas id="ma-chart"></canvas>
</div>
</div>
</div>
</section>
<!-- ========== AI ADVISOR PAGE ========== -->
<section id="page-ai" class="page">
<div class="section-header">
<h2 class="section-title">🤖 AI Advisor & Sentiment Analysis</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<span class="chip" style="background: var(--primary-glow); color: var(--primary-light);">HF Models</span>
<span class="chip" style="background: var(--success-glow); color: var(--success-light);">Ensemble AI</span>
</div>
</div>
<div class="grid-two" style="margin-bottom: 24px;">
<div class="glass-card">
<div class="card-header">
<h4>💬 Natural Language Query</h4>
</div>
<form data-query-form style="display: flex; flex-direction: column; gap: 16px;">
<label style="display: flex; flex-direction: column; gap: 8px;">
<span style="font-weight: 600; color: var(--text-secondary);">Ask anything about crypto markets</span>
<input type="text" placeholder="e.g., What is the current Bitcoin price? What are the top 5 coins by market cap?" name="query" style="padding: 14px 18px; border-radius: 12px; background: var(--glass-bg-light); border: 1px solid var(--glass-border); color: var(--text-primary); font-size: 0.9375rem;" />
</label>
<button class="btn-secondary" type="submit" style="padding: 14px 24px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/>
</svg>
Ask AI
</button>
</form>
<div data-query-output style="margin-top: 20px; padding: 20px; background: var(--glass-bg-light); border-radius: 12px; border: 1px solid var(--glass-border); min-height: 60px; color: var(--text-secondary);"></div>
</div>
<div class="glass-card">
<div class="card-header">
<h4>📊 Sentiment Analyzer</h4>
<span class="chip" style="font-size: 0.75rem;">Ensemble AI Models</span>
</div>
<form data-sentiment-form style="display: flex; flex-direction: column; gap: 16px;">
<label style="display: flex; flex-direction: column; gap: 8px;">
<span style="font-weight: 600; color: var(--text-secondary);">Enter text for sentiment analysis</span>
<textarea name="text" rows="5" placeholder="e.g., Bitcoin is showing strong bullish momentum with increasing adoption..." style="padding: 14px 18px; border-radius: 12px; background: var(--glass-bg-light); border: 1px solid var(--glass-border); color: var(--text-primary); font-size: 0.9375rem; resize: vertical; font-family: inherit;"></textarea>
</label>
<button class="btn-secondary" type="submit" style="padding: 14px 24px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Analyze Sentiment
</button>
</form>
</div>
</div>
<!-- Sentiment Results Display -->
<div id="sentiment-results" class="glass-card" style="display: none; margin-top: 24px;">
<div class="card-header">
<h4>📈 Analysis Results</h4>
</div>
<div data-sentiment-output style="padding: 24px;"></div>
</div>
</section>
<!-- ========== NEWS PAGE ========== -->
<section id="page-news" class="page">
<div class="section-header">
<h2 class="section-title">📰 News Feed</h2>
<div style="display: flex; gap: 12px; align-items: center;">
<span class="chip" id="news-count">Loading...</span>
<button class="btn-secondary" id="refresh-news" style="padding: 8px 16px; font-size: 0.875rem;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" style="margin-right: 6px;">
<path d="M1 4v6h6M23 20v-6h-6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Refresh
</button>
</div>
</div>
<div class="glass-card" style="margin-bottom: 24px;">
<div class="search-bar" style="display: flex; gap: 12px; flex-wrap: wrap;">
<input type="text" placeholder="🔍 Search headlines..." data-news-search id="news-search" name="news-search" style="flex: 1; min-width: 200px;" />
<select data-news-range style="padding: 10px 14px; border-radius: 10px; background: var(--glass-bg); border: 1px solid var(--glass-border); color: var(--text-secondary);">
<option value="24h">Last 24h</option>
<option value="7d">Last 7d</option>
<option value="30d">Last 30d</option>
<option value="all">All Time</option>
</select>
<input type="text" placeholder="Symbol (BTC, ETH...)" data-news-symbol id="news-symbol" name="news-symbol" style="width: 150px;" />
</div>
</div>
<div id="news-loading" style="text-align: center; padding: 60px; color: var(--text-muted); display: none;">
<div style="font-size: 3rem; margin-bottom: 16px;">📰</div>
<div>Loading news...</div>
</div>
<div id="news-error" style="display: none; padding: 40px; text-align: center; color: var(--danger);"></div>
<div id="news-grid" data-news-container style="display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 20px;"></div>
<div class="modal-backdrop" data-news-modal style="display: none;">
<div class="modal glass-card" data-news-modal-content style="max-width: 800px; max-height: 90vh; overflow-y: auto;"></div>
<button data-close-news-modal style="position: absolute; top: 20px; right: 20px; background: var(--glass-bg-strong); border: 1px solid var(--glass-border); color: var(--text-primary); width: 40px; height: 40px; border-radius: 50%; font-size: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center;">&times;</button>
</div>
</section>
<!-- ========== PROVIDERS PAGE ========== -->
<section id="page-providers" class="page">
<div class="section-header">
<h2 class="section-title">API Providers</h2>
<span class="chip">Multi-source</span>
</div>
<div data-providers-grid class="grid-three"></div>
</section>
<!-- ========== DATASETS & MODELS PAGE ========== -->
<section id="page-datasets" class="page">
<div class="section-header">
<h2 class="section-title">Datasets & Models</h2>
<span class="chip">14+ datasets</span>
</div>
<div class="glass-card">
<h4>Datasets</h4>
<div class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody data-datasets-body></tbody>
</table>
</div>
</div>
<div class="glass-card" style="margin-top:1.5rem;">
<h4>HF Models</h4>
<div class="table-container">
<table>
<thead>
<tr>
<th>Name</th>
<th>Task</th>
<th>Status</th>
<th>Description</th>
</tr>
</thead>
<tbody data-models-body></tbody>
</table>
</div>
</div>
<div class="glass-card" style="margin-top:1.5rem;">
<h4>Test Model</h4>
<form data-model-test-form>
<div class="grid-two">
<label>Model
<select name="model" data-model-select></select>
</label>
<label>Input Text
<textarea name="input" rows="3" placeholder="Enter text to test the model..."></textarea>
</label>
</div>
<button class="primary" type="submit">Run Test</button>
</form>
<div data-model-test-output style="margin-top:1rem;"></div>
</div>
<div class="modal-backdrop" data-dataset-modal>
<div class="modal" data-dataset-modal-content></div>
<button data-close-dataset-modal>&times;</button>
</div>
</section>
<!-- ========== API EXPLORER PAGE ========== -->
<section id="page-api" class="page">
<div class="section-header">
<h2 class="section-title">API Explorer</h2>
<span class="chip">15+ endpoints</span>
</div>
<div class="glass-card">
<h4>Test Endpoint</h4>
<form data-api-form>
<div class="grid-two">
<label>Endpoint
<select data-endpoint-select>
<option value="0">/api/health</option>
</select>
</label>
<label>Method
<select data-method-select>
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
</label>
</div>
<div data-api-description style="margin:0.5rem 0;font-size:0.875rem;color:var(--text-secondary);"></div>
<div data-api-path style="margin:0.5rem 0;font-family:monospace;font-size:0.875rem;"></div>
<label>Body (JSON)
<textarea data-body-input rows="4"></textarea>
</label>
<button class="primary" type="submit">Send Request</button>
</form>
<div data-api-response style="margin-top:1rem;"></div>
</div>
</section>
<!-- ========== DIAGNOSTICS PAGE ========== -->
<section id="page-debug" class="page">
<div class="section-header">
<h2 class="section-title">System Diagnostics</h2>
</div>
<div class="grid-two">
<div class="glass-card">
<h4>Health Status</h4>
<div data-health-info>Checking...</div>
</div>
<div class="glass-card">
<h4>WebSocket Status</h4>
<div data-ws-info>Checking...</div>
</div>
</div>
<div class="grid-two" style="margin-top:1.5rem;">
<div class="glass-card">
<h4>Request Logs</h4>
<div class="table-container">
<table>
<thead>
<tr>
<th>Time</th>
<th>Method</th>
<th>Endpoint</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody data-request-log></tbody>
</table>
</div>
</div>
<div class="glass-card">
<h4>Error Logs</h4>
<div class="table-container">
<table>
<thead>
<tr>
<th>Time</th>
<th>Endpoint</th>
<th>Message</th>
</tr>
</thead>
<tbody data-error-log></tbody>
</table>
</div>
</div>
</div>
<div class="glass-card" style="margin-top:1.5rem;">
<h4>WebSocket Events</h4>
<div class="table-container">
<table>
<thead>
<tr>
<th>Time</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody data-ws-log></tbody>
</table>
</div>
</div>
<button class="secondary" data-refresh-health style="margin-top:1.5rem;">Refresh</button>
</section>
<!-- ========== SETTINGS PAGE ========== -->
<section id="page-settings" class="page">
<div class="section-header">
<h2 class="section-title">Settings</h2>
</div>
<div class="glass-card">
<h4>Display Settings</h4>
<div class="grid-two">
<label class="input-chip">Theme
<div class="toggle">
<input type="checkbox" data-theme-toggle id="theme-toggle" name="theme-toggle" />
<span></span>
</div>
<span style="font-size: 0.875rem; color: var(--text-muted); margin-left: 8px;">Light / Dark</span>
</label>
<label class="input-chip">Compact Layout
<div class="toggle">
<input type="checkbox" data-layout-toggle id="layout-toggle" name="layout-toggle" />
<span></span>
</div>
</label>
</div>
</div>
<div class="glass-card" style="margin-top:1.5rem;">
<h4>Refresh Intervals</h4>
<div class="grid-two">
<label>Market Data (seconds)
<input type="number" min="10" step="5" value="30" data-market-interval id="market-interval" name="market-interval" />
</label>
<label>News Feed (seconds)
<input type="number" min="30" step="10" value="60" data-news-interval id="news-interval" name="news-interval" />
</label>
</div>
</div>
<div class="inline-message inline-info" style="margin-top:1.5rem;">
Settings are stored locally in your browser.
</div>
</section>
</div>
</main>
</div>
<!-- Enhanced Chart Functionality -->
<script>
let chartInstances = {};
let allCoins = [];
let selectedCoin = null;
let selectedTimeframe = 7;
// Initialize Navigation
function initNavigation() {
const navButtons = document.querySelectorAll('.nav-button');
const pages = document.querySelectorAll('.page');
const topbarIcon = document.querySelector('.topbar-icon');
// Page icon mapping
const pageIcons = {
'page-overview': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(96, 165, 250, 0.1)"/>
<rect x="3" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/><rect x="14" y="3" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/>
<rect x="3" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/><rect x="14" y="14" width="7" height="7" rx="1.5" fill="currentColor" opacity="0.3"/>
</svg>`,
'page-market': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(167, 139, 250, 0.1)"/>
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>`,
'page-chart': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(244, 114, 182, 0.1)"/>
<path d="M3 3v18h18" stroke="currentColor" stroke-width="2.5"/><path d="M7 12l4-4 4 4 6-6v8H7z" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>`,
'page-ai': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(52, 211, 153, 0.1)"/>
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.4"/>
</svg>`,
'page-news': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(251, 191, 36, 0.1)"/>
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
<path d="M18 14h-8M15 10h-5M19 18h-3" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>`,
'page-providers': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(34, 211, 238, 0.1)"/>
<path d="M12 2L2 7l10 5 10-5-10-5z" fill="currentColor" opacity="0.3"/><path d="M2 17l10 5 10-5" fill="currentColor" opacity="0.3"/><path d="M2 12l10 5 10-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>`,
'page-datasets': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(192, 132, 252, 0.1)"/>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" fill="currentColor" opacity="0.15"/>
<line x1="9" y1="3" x2="9" y2="21" stroke="currentColor" stroke-width="2.5"/><line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2.5"/>
</svg>`,
'page-api': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(129, 140, 248, 0.1)"/>
<path d="M4 9h16M4 15h16" stroke="currentColor" stroke-width="2.5"/><path d="M12 3v18" stroke="currentColor" stroke-width="2.5"/><circle cx="12" cy="12" r="2.5" fill="currentColor" opacity="0.4"/>
</svg>`,
'page-debug': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(248, 113, 113, 0.1)"/>
<circle cx="12" cy="12" r="10" fill="currentColor" opacity="0.15"/>
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/><line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
</svg>`,
'page-settings': `<svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="rgba(148, 163, 184, 0.1)"/>
<circle cx="12" cy="12" r="3.5" fill="currentColor" opacity="0.25"/>
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>`
};
function updateHeaderIcon(pageId) {
if (topbarIcon && pageIcons[pageId]) {
topbarIcon.innerHTML = pageIcons[pageId];
}
}
function loadPageData(pageId) {
// Load data based on page
switch(pageId) {
case 'page-overview':
loadMarketOverviewChart().catch(e => console.error('[Chart]', e));
loadTopCoinsWithSparklines().catch(e => console.error('[Top Coins]', e));
loadOverviewStats().catch(e => console.error('[Stats]', e));
break;
case 'page-news':
loadNewsData().catch(e => console.error('[News]', e));
break;
case 'page-market':
// Load market data if needed
break;
case 'page-chart':
// Load chart lab data if needed
break;
}
}
navButtons.forEach(button => {
button.addEventListener('click', () => {
const targetPage = button.dataset.nav;
// Update active states
navButtons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
// Show target page
pages.forEach(page => {
page.classList.toggle('active', page.id === targetPage);
});
// Update header icon
updateHeaderIcon(targetPage);
// Load page data
loadPageData(targetPage);
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
});
});
// Set initial header icon
const activeButton = document.querySelector('.nav-button.active');
if (activeButton) {
updateHeaderIcon(activeButton.dataset.nav);
}
}
// Theme Toggle
function initThemeToggle() {
const themeToggle = document.querySelector('[data-theme-toggle]');
const body = document.getElementById('main-body');
if (!themeToggle || !body) return;
// Load saved theme
const savedTheme = localStorage.getItem('theme') || 'dark';
body.setAttribute('data-theme', savedTheme);
themeToggle.checked = savedTheme === 'light';
// Toggle theme
themeToggle.addEventListener('change', (e) => {
const newTheme = e.target.checked ? 'light' : 'dark';
body.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
});
}
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
initThemeToggle();
// Load data with error handling
// Load all data with better error handling
Promise.all([
loadMarketOverviewChart().catch(e => {
console.error('[Overview Chart]', e);
const ctx = document.getElementById('market-overview-chart');
if (ctx && ctx.parentElement) {
ctx.parentElement.innerHTML = `<div style="padding: 40px; text-align: center; color: var(--text-muted);">Failed to load chart: ${e.message}</div>`;
}
}),
loadTopCoinsWithSparklines().catch(e => {
console.error('[Top Coins]', e);
const tbody = document.querySelector('[data-top-coins-body]');
if (tbody) {
tbody.innerHTML = `<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-muted);">Failed to load: ${e.message}</td></tr>`;
}
}),
loadOverviewStats().catch(e => {
console.error('[Stats]', e);
const statsContainer = document.querySelector('[data-overview-stats]');
if (statsContainer) {
statsContainer.innerHTML = '<div style="grid-column: 1/-1; padding: 20px; text-align: center; color: var(--text-muted);">Failed to load stats. Please refresh the page.</div>';
}
}),
loadNewsData().catch(e => {
console.error('[News]', e);
const newsGrid = document.getElementById('news-grid');
if (newsGrid) {
newsGrid.innerHTML = '<div style="grid-column: 1/-1; padding: 40px; text-align: center; color: var(--text-muted);">Failed to load news. Please refresh the page.</div>';
}
})
]).then(() => {
console.log('[Init] All initial data loaded');
});
initChartLabControls();
});
// Refresh all data without page reload
async function refreshAllData() {
const refreshBtn = document.getElementById('refresh-market-btn');
const refreshText = document.getElementById('refresh-text');
if (refreshBtn) {
refreshBtn.disabled = true;
refreshBtn.style.opacity = '0.6';
refreshBtn.style.cursor = 'wait';
}
if (refreshText) {
refreshText.textContent = 'Refreshing...';
}
try {
// Refresh all data sources in parallel
await Promise.all([
loadOverviewStats().catch(e => console.error('[Stats]', e)),
loadMarketOverviewChart().catch(e => console.error('[Chart]', e)),
loadTopCoinsWithSparklines().catch(e => console.error('[Top Coins]', e))
]);
console.log('[Refresh] All data refreshed successfully');
// Show success toast if available
if (window.toast) {
window.toast.show('Data refreshed successfully', 'success', { duration: 2000 });
}
} catch (error) {
console.error('[Refresh] Error refreshing data:', error);
// Show error toast if available
if (window.toast) {
window.toast.show(`Refresh failed: ${error.message}`, 'error', { duration: 4000 });
}
} finally {
if (refreshBtn) {
refreshBtn.disabled = false;
refreshBtn.style.opacity = '1';
refreshBtn.style.cursor = 'pointer';
}
if (refreshText) {
refreshText.textContent = 'Refresh';
}
}
}
// Load Overview Stats
async function loadOverviewStats() {
try {
// Try primary backend, fallback to alternative ports/paths
let backendUrl = window.BACKEND_URL || window.location.origin;
const statsContainer = document.querySelector('[data-overview-stats]');
if (!statsContainer) return;
let res = await fetch(`${backendUrl}/api/market/stats`);
// If 404, try alternative endpoints or ports
if (!res.ok && res.status === 404) {
// Try port 7861 (FastAPI might be on different port)
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`;
try {
res = await fetch(`${altBackend}/api/market/stats`);
if (res.ok) backendUrl = altBackend;
} catch (e) {
// Try same origin without port
res = await fetch(`${window.location.origin}/api/market/stats`);
if (res.ok) backendUrl = window.location.origin;
}
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
const stats = data.stats || data.data || data || {};
statsContainer.innerHTML = `
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(34, 211, 238, 0.15));">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
</div>
<div class="stat-label">Total Market Cap</div>
<div class="stat-value">$${formatNum(stats.total_market_cap || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(52, 211, 153, 0.2), rgba(34, 211, 238, 0.15));">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M3 3v18h18"/><path d="M7 12l4-4 4 4 6-6v8H7z"/>
</svg>
</div>
<div class="stat-label">24h Volume</div>
<div class="stat-value">$${formatNum(stats.total_volume_24h || 0)}</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(251, 191, 36, 0.2), rgba(244, 114, 182, 0.15));">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
</svg>
</div>
<div class="stat-label">BTC Dominance</div>
<div class="stat-value">${(stats.btc_dominance || 0).toFixed(2)}%</div>
</div>
<div class="stat-card">
<div class="stat-icon" style="background: linear-gradient(135deg, rgba(167, 139, 250, 0.2), rgba(129, 140, 248, 0.15));">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
</div>
<div class="stat-label">Active Coins</div>
<div class="stat-value">${stats.active_cryptocurrencies || 0}</div>
</div>
`;
} catch (e) {
console.error('[Stats] Error:', e);
const statsContainer = document.querySelector('[data-overview-stats]');
if (statsContainer) {
statsContainer.innerHTML = '<div style="grid-column: 1/-1; padding: 20px; text-align: center; color: var(--text-muted);">Failed to load stats</div>';
}
}
}
// Market Overview Chart
async function loadMarketOverviewChart() {
try {
let backendUrl = window.BACKEND_URL || window.location.origin;
console.log('[Chart] Loading from:', backendUrl);
let res = await fetch(`${backendUrl}/api/coins/top?limit=10`);
// If 404, try alternative endpoints
if (!res.ok && res.status === 404) {
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`;
try {
res = await fetch(`${altBackend}/api/coins/top?limit=10`);
if (res.ok) backendUrl = altBackend;
} catch (e) {
res = await fetch(`${window.location.origin}/api/coins/top?limit=10`);
if (res.ok) backendUrl = window.location.origin;
}
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
const coins = data.coins || data.data || data || [];
if (!coins || coins.length === 0) {
console.warn('[Chart] No coins data received');
const ctx = document.getElementById('market-overview-chart');
if (ctx && ctx.parentElement) {
ctx.parentElement.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">No data available</div>';
}
return;
}
console.log('[Chart] Loaded', coins.length, 'coins');
const ctx = document.getElementById('market-overview-chart');
if (!ctx) return;
const colors = ['#60A5FA', '#22D3EE', '#34D399', '#FBBF24', '#F87171', '#A78BFA', '#F472B6', '#FB923C', '#2DD4BF', '#818CF8'];
const datasets = coins.slice(0, 10).map((coin, i) => {
// Use sparkline data if available, otherwise generate sample data
const priceData = coin.sparkline_in_7d?.price ||
coin.sparkline_data ||
Array.from({length: 168}, () => coin.price || coin.current_price || 0);
return {
label: coin.name || coin.symbol,
data: priceData,
borderColor: colors[i],
backgroundColor: colors[i] + '15',
borderWidth: 2.5,
fill: false,
tension: 0.3,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBorderWidth: 2
};
});
if (chartInstances.overview) chartInstances.overview.destroy();
// TradingView-style overview chart
chartInstances.overview = new Chart(ctx, {
type: 'line',
data: { labels: Array.from({length: 168}, (_, i) => i), datasets },
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {
display: true,
position: 'top',
align: 'start',
labels: {
usePointStyle: true,
pointStyle: 'circle',
padding: 16,
font: {
size: 12,
weight: '600',
family: 'Manrope'
},
color: '#E2E8F0',
boxWidth: 14,
boxHeight: 14,
generateLabels: function(chart) {
const original = Chart.defaults.plugins.legend.labels.generateLabels;
const labels = original.call(this, chart);
labels.forEach(label => {
label.fillStyle = label.strokeStyle;
label.strokeStyle = label.strokeStyle;
label.lineWidth = 2;
});
return labels;
}
},
title: {
display: false
}
},
tooltip: {
backgroundColor: 'rgba(10, 15, 30, 0.98)',
padding: 12,
borderColor: 'rgba(96, 165, 250, 0.3)',
borderWidth: 1.5,
titleColor: '#F8FAFC',
bodyColor: '#E2E8F0',
titleFont: { size: 12, weight: '700', family: 'Manrope' },
bodyFont: { size: 11, family: 'Manrope' },
cornerRadius: 8,
displayColors: true,
boxPadding: 6
}
},
scales: {
x: {
grid: {
display: true,
color: 'rgba(255, 255, 255, 0.03)',
lineWidth: 1
},
ticks: {
display: false,
color: '#64748B',
font: { size: 10, family: 'Manrope' }
},
border: {
display: false
}
},
y: {
position: 'right',
grid: {
color: 'rgba(255, 255, 255, 0.05)',
lineWidth: 1,
drawBorder: false,
drawTicks: true
},
ticks: {
color: '#94A3B8',
font: { size: 11, weight: '600', family: 'Manrope' },
padding: 8,
stepSize: null,
precision: 2,
callback: function(value) {
if (value >= 1e12) return '$' + (value / 1e12).toFixed(2) + 'T';
if (value >= 1e9) return '$' + (value / 1e9).toFixed(2) + 'B';
if (value >= 1e6) return '$' + (value / 1e6).toFixed(2) + 'M';
if (value >= 1e3) return '$' + (value / 1e3).toFixed(2) + 'K';
return '$' + value.toFixed(2);
}
},
border: {
display: false
},
beginAtZero: false
}
},
elements: {
line: {
tension: 0,
borderWidth: 2
},
point: {
radius: 0,
hoverRadius: 4
}
}
}
});
} catch (e) {
console.error('[Chart] Error loading market overview:', e);
const ctx = document.getElementById('market-overview-chart');
if (ctx) {
ctx.parentElement.innerHTML = `<div style="padding: 40px; text-align: center; color: var(--text-muted);">
<p>Failed to load chart data</p>
<p style="font-size: 0.875rem; margin-top: 8px;">${e.message}</p>
</div>`;
}
}
}
// Top Coins with Sparklines
async function loadTopCoinsWithSparklines() {
try {
let backendUrl = window.BACKEND_URL || window.location.origin;
console.log('[Top Coins] Loading from:', backendUrl);
let res = await fetch(`${backendUrl}/api/coins/top?limit=20`);
// If 404, try alternative endpoints
if (!res.ok && res.status === 404) {
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`;
try {
res = await fetch(`${altBackend}/api/coins/top?limit=20`);
if (res.ok) backendUrl = altBackend;
} catch (e) {
res = await fetch(`${window.location.origin}/api/coins/top?limit=20`);
if (res.ok) backendUrl = window.location.origin;
}
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
const coins = data.coins || data || [];
if (!coins || coins.length === 0) {
console.warn('No coins data received');
const tbody = document.querySelector('[data-top-coins-body]');
if (tbody) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align: center; padding: 40px; color: var(--text-muted);">No data available</td></tr>';
}
return;
}
const tbody = document.querySelector('[data-top-coins-body]');
if (!tbody) return;
tbody.innerHTML = coins.map((coin, i) => {
const change24h = coin.price_change_percentage_24h || 0;
const change7d = coin.price_change_percentage_7d_in_currency || 0;
const coinName = coin.name || 'Unknown';
const coinSymbol = (coin.symbol || 'N/A').toUpperCase();
const coinImage = coin.image || 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIgZmlsbD0iIzM0NDc1NiIvPjwvc3ZnPg==';
const coinId = coin.id || `coin-${i}`;
return `
<tr>
<td>${i + 1}</td>
<td>
<div style="display: flex; align-items: center; gap: 12px;">
<img src="${coinImage}" style="width: 32px; height: 32px; border-radius: 50%;" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48Y2lyY2xlIGN4PSIxNiIgY3k9IjE2IiByPSIxNiIgZmlsbD0iIzM0NDc1NiIvPjwvc3ZnPg=='">
<div>
<div style="font-weight: 600;">${coinName}</div>
<div style="font-size: 11px; color: #94A3B8;">${coinSymbol}</div>
</div>
</div>
</td>
<td style="font-weight: 600;">$${formatNum(coin.current_price)}</td>
<td>
<span style="color: ${change24h >= 0 ? '#34D399' : '#F87171'}; font-weight: 600;">
${change24h >= 0 ? '↑' : '↓'} ${Math.abs(Number(change24h) || 0).toFixed(2)}%
</span>
</td>
<td>
<span style="color: ${change7d >= 0 ? '#34D399' : '#F87171'}; font-weight: 600;">
${change7d >= 0 ? '↑' : '↓'} ${Math.abs(Number(change7d) || 0).toFixed(2)}%
</span>
</td>
<td>$${formatNum(coin.market_cap)}</td>
<td>$${formatNum(coin.total_volume)}</td>
<td><canvas id="spark-${coinId}" width="100" height="30"></canvas></td>
</tr>
`;
}).join('');
setTimeout(() => {
coins.forEach((coin, i) => {
const coinId = coin.id || `coin-${i}`;
if (coin.sparkline_in_7d?.price && Array.isArray(coin.sparkline_in_7d.price)) {
const coinChange24h = coin.price_change_percentage_24h || 0;
createSparkline(`spark-${coinId}`, coin.sparkline_in_7d.price, coinChange24h >= 0);
}
});
}, 100);
} catch (e) {
console.error('[Table] Error loading top coins:', e);
const tbody = document.querySelector('[data-top-coins-body]');
if (tbody) {
tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; padding: 40px; color: var(--text-muted);">
<p>Failed to load data</p>
<p style="font-size: 0.875rem; margin-top: 8px;">${e.message}</p>
</td></tr>`;
}
}
}
// Sparkline
function createSparkline(id, data, isPositive) {
const canvas = document.getElementById(id);
if (!canvas) return;
const color = isPositive ? '#10B981' : '#EF4444';
new Chart(canvas, {
type: 'line',
data: {
labels: data.map((_, i) => i),
datasets: [{
data: data,
borderColor: color,
backgroundColor: color + '30',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0
}]
},
options: {
responsive: false,
plugins: { legend: { display: false }, tooltip: { enabled: false } },
scales: { x: { display: false }, y: { display: false } }
}
});
}
// Chart Lab Controls
function initChartLabControls() {
const input = document.getElementById('chartCoinSearch');
const dropdown = document.getElementById('chartCoinDropdown');
if (!input || !dropdown) return;
input.addEventListener('focus', async () => {
if (allCoins.length === 0) {
let backendUrl = window.BACKEND_URL || window.location.origin;
let res = await fetch(`${backendUrl}/api/coins/top?limit=100`);
// If 404, try alternative endpoints
if (!res.ok && res.status === 404) {
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`;
try {
res = await fetch(`${altBackend}/api/coins/top?limit=100`);
if (res.ok) backendUrl = altBackend;
} catch (e) {
res = await fetch(`${window.location.origin}/api/coins/top?limit=100`);
if (res.ok) backendUrl = window.location.origin;
}
}
if (res.ok) {
const data = await res.json();
allCoins = data.coins || data || [];
}
}
renderCoinDropdown(allCoins);
dropdown.style.display = 'block';
});
input.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
const filtered = allCoins.filter(c => c.name.toLowerCase().includes(term) || c.symbol.toLowerCase().includes(term));
renderCoinDropdown(filtered);
});
document.addEventListener('click', (e) => {
if (!input.contains(e.target) && !dropdown.contains(e.target)) {
dropdown.style.display = 'none';
}
});
// Timeframe buttons
document.querySelectorAll('[data-chart-timeframe]').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('[data-chart-timeframe]').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
selectedTimeframe = parseInt(btn.dataset.chartTimeframe);
if (selectedCoin) loadCoinDetailChart(selectedCoin.id);
});
});
}
function renderCoinDropdown(coins) {
const dropdown = document.getElementById('chartCoinDropdown');
if (!dropdown) return;
dropdown.innerHTML = coins.slice(0, 50).map(coin => `
<div onclick="selectChartCoin('${coin.id}')" style="padding: 12px 16px; display: flex; align-items: center; gap: 12px; cursor: pointer; border-bottom: 1px solid rgba(255, 255, 255, 0.05); transition: all 0.2s;">
<img src="${coin.image}" style="width: 32px; height: 32px; border-radius: 50%;">
<div style="flex: 1;">
<div style="font-weight: 600;">${coin.name}</div>
<div style="font-size: 11px; color: #94A3B8;">${coin.symbol.toUpperCase()}</div>
</div>
<div style="font-weight: 600;">$${formatNum(coin.current_price)}</div>
</div>
`).join('');
dropdown.querySelectorAll('div[onclick]').forEach(el => {
el.addEventListener('mouseenter', () => {
el.style.background = 'rgba(34, 211, 238, 0.15)';
el.style.borderLeft = '3px solid #22D3EE';
});
el.addEventListener('mouseleave', () => {
el.style.background = 'transparent';
el.style.borderLeft = 'none';
});
});
}
window.selectChartCoin = function(coinId) {
selectedCoin = allCoins.find(c => c.id === coinId);
if (!selectedCoin) return;
document.getElementById('chartCoinSearch').value = `${selectedCoin.name} (${selectedCoin.symbol.toUpperCase()})`;
document.getElementById('chartCoinDropdown').style.display = 'none';
loadCoinDetailChart(coinId);
};
window.loadSelectedChart = function() {
if (selectedCoin) {
loadCoinDetailChart(selectedCoin.id);
}
};
async function loadCoinDetailChart(coinId) {
try {
const backendUrl = window.BACKEND_URL || window.location.origin;
const interval = selectedTimeframe === 1 ? '1h' : selectedTimeframe === 7 ? '1d' : selectedTimeframe === 30 ? '1d' : '1d';
const res = await fetch(`${backendUrl}/api/charts/price/${coinId}?interval=${interval}&limit=100`);
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const chartData = await res.json();
// Transform data to match expected format
let data;
if (chartData.prices && Array.isArray(chartData.prices)) {
data = {
prices: chartData.prices.map((p, i) => {
const timestamp = typeof p === 'number' ? Date.now() - (chartData.prices.length - i) * 3600000 : p[0];
const price = typeof p === 'number' ? p : p[1];
return [timestamp, price];
}),
total_volumes: chartData.volumes?.map((v, i) => {
const timestamp = typeof v === 'number' ? Date.now() - (chartData.volumes.length - i) * 3600000 : v[0];
const volume = typeof v === 'number' ? v : v[1];
return [timestamp, volume];
}) || []
};
} else {
// Fallback: use OHLCV endpoint
const ohlcvRes = await fetch(`${backendUrl}/api/ohlcv?symbol=${coinId}&interval=${interval}&limit=100`);
if (ohlcvRes.ok) {
const ohlcvData = await ohlcvRes.json();
data = {
prices: ohlcvData.data?.map((d, i) => [Date.now() - (ohlcvData.data.length - i) * 3600000, d.close || d.price || 0]) || [],
total_volumes: ohlcvData.data?.map((d, i) => [Date.now() - (ohlcvData.data.length - i) * 3600000, d.volume || 0]) || []
};
} else {
throw new Error('No chart data available');
}
}
const coin = allCoins.find(c => c.id === coinId) || selectedCoin;
document.getElementById('selectedCoinTitle').textContent = `${coin.name} (${coin.symbol.toUpperCase()})`;
document.getElementById('selectedCoinPrice').textContent = `$${formatNum(coin.current_price)}`;
const change = coin.price_change_percentage_24h || 0;
const changeEl = document.getElementById('selectedCoinChange');
changeEl.textContent = `${change >= 0 ? '+' : ''}${change.toFixed(2)}%`;
changeEl.className = `badge ${change >= 0 ? 'badge-success' : 'badge-danger'}`;
// Price Chart
const priceCtx = document.getElementById('price-chart');
if (priceCtx) {
if (chartInstances.price) chartInstances.price.destroy();
const chartType = document.getElementById('chartType').value;
// TradingView-style chart configuration
chartInstances.price = new Chart(priceCtx, {
type: chartType === 'bar' ? 'bar' : 'line',
data: {
labels: data.prices.map(p => new Date(p[0])),
datasets: [{
label: 'Price',
data: data.prices.map(p => p[1]),
borderColor: '#60A5FA',
backgroundColor: chartType === 'area' ? 'rgba(96, 165, 250, 0.1)' : chartType === 'bar' ? 'rgba(96, 165, 250, 0.6)' : 'transparent',
borderWidth: chartType === 'line' ? 2 : 0,
fill: chartType === 'area',
tension: 0,
pointRadius: 0,
pointHoverRadius: 4,
pointHoverBorderWidth: 2,
pointHoverBackgroundColor: '#60A5FA'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index'
},
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(10, 15, 30, 0.98)',
padding: 14,
borderColor: 'rgba(96, 165, 250, 0.3)',
borderWidth: 1.5,
titleColor: '#F8FAFC',
bodyColor: '#E2E8F0',
titleFont: { size: 13, weight: '700', family: 'Manrope' },
bodyFont: { size: 12, family: 'Manrope' },
cornerRadius: 8,
displayColors: false,
callbacks: {
label: function(context) {
return `Price: $${formatNum(context.parsed.y)}`;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
day: 'MMM dd'
}
},
grid: {
display: true,
color: 'rgba(255, 255, 255, 0.05)',
lineWidth: 1
},
ticks: {
color: '#64748B',
font: { size: 11, family: 'Manrope' },
maxRotation: 0
},
border: {
display: true,
color: 'rgba(255, 255, 255, 0.1)'
}
},
y: {
position: 'right',
grid: {
color: 'rgba(255, 255, 255, 0.05)',
lineWidth: 1,
drawBorder: false
},
ticks: {
color: '#64748B',
font: { size: 11, family: 'Manrope' },
callback: v => '$' + formatNum(v),
padding: 8
},
border: {
display: true,
color: 'rgba(255, 255, 255, 0.1)'
}
}
},
elements: {
line: {
cubicInterpolationMode: 'monotone'
}
}
}
});
}
// Volume Chart
const volumeCtx = document.getElementById('volume-chart');
if (volumeCtx) {
if (chartInstances.volume) chartInstances.volume.destroy();
chartInstances.volume = new Chart(volumeCtx, {
type: 'bar',
data: {
labels: data.total_volumes.map(v => new Date(v[0])),
datasets: [{
label: 'Volume',
data: data.total_volumes.map(v => v[1]),
backgroundColor: (ctx) => {
const idx = ctx.dataIndex;
if (idx === 0) return 'rgba(52, 211, 153, 0.4)';
const prev = data.total_volumes[idx - 1]?.[1] || 0;
const curr = data.total_volumes[idx][1];
return curr >= prev ? 'rgba(52, 211, 153, 0.5)' : 'rgba(248, 113, 113, 0.5)';
},
borderColor: (ctx) => {
const idx = ctx.dataIndex;
if (idx === 0) return '#34D399';
const prev = data.total_volumes[idx - 1]?.[1] || 0;
const curr = data.total_volumes[idx][1];
return curr >= prev ? '#34D399' : '#F87171';
},
borderWidth: 1,
borderRadius: 2,
borderSkipped: false
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: 'rgba(10, 15, 30, 0.98)',
padding: 12,
borderColor: 'rgba(52, 211, 153, 0.3)',
borderWidth: 1.5,
titleColor: '#F8FAFC',
bodyColor: '#E2E8F0',
titleFont: { size: 12, weight: '700', family: 'Manrope' },
bodyFont: { size: 11, family: 'Manrope' },
cornerRadius: 8,
displayColors: false,
callbacks: {
label: function(context) {
return `Volume: $${formatNum(context.parsed.y)}`;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
displayFormats: {
hour: 'HH:mm',
day: 'MMM dd'
}
},
grid: {
display: true,
color: 'rgba(255, 255, 255, 0.03)',
lineWidth: 1
},
ticks: {
color: '#64748B',
font: { size: 10, family: 'Manrope' },
maxRotation: 0
},
border: {
display: false
}
},
y: {
position: 'right',
grid: {
color: 'rgba(255, 255, 255, 0.05)',
lineWidth: 1,
drawBorder: false
},
ticks: {
color: '#64748B',
font: { size: 10, family: 'Manrope' },
callback: v => '$' + formatNum(v),
padding: 6
},
border: {
display: false
}
}
}
}
});
}
// RSI Chart (simulated)
const rsiCtx = document.getElementById('rsi-chart');
if (rsiCtx) {
if (chartInstances.rsi) chartInstances.rsi.destroy();
const rsiData = calculateRSI(data.prices.map(p => p[1]));
chartInstances.rsi = new Chart(rsiCtx, {
type: 'line',
data: {
labels: rsiData.map((_, i) => i),
datasets: [{
label: 'RSI (14)',
data: rsiData,
borderColor: '#A78BFA',
backgroundColor: 'rgba(167, 139, 250, 0.12)',
borderWidth: 2.5,
fill: true,
tension: 0,
pointRadius: 0,
pointHoverRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
align: 'start',
labels: {
usePointStyle: true,
pointStyle: 'circle',
padding: 14,
font: {
size: 12,
weight: '600',
family: 'Manrope'
},
color: '#E2E8F0',
boxWidth: 14,
boxHeight: 14
}
},
tooltip: {
backgroundColor: 'rgba(10, 15, 30, 0.98)',
padding: 12,
borderColor: 'rgba(167, 139, 250, 0.3)',
borderWidth: 1.5,
titleColor: '#F8FAFC',
bodyColor: '#E2E8F0',
titleFont: { size: 12, weight: '700', family: 'Manrope' },
bodyFont: { size: 11, family: 'Manrope' },
cornerRadius: 8,
displayColors: false,
callbacks: {
label: function(context) {
return `RSI: ${context.parsed.y.toFixed(2)}`;
}
}
}
},
scales: {
y: {
min: 0,
max: 100,
grid: {
color: 'rgba(255, 255, 255, 0.05)',
lineWidth: 1
},
ticks: {
color: '#64748B',
font: { size: 11, family: 'Manrope' },
padding: 6
},
border: {
display: false
}
},
x: {
grid: {
display: true,
color: 'rgba(255, 255, 255, 0.03)',
lineWidth: 1
},
ticks: {
display: false,
color: '#64748B',
font: { size: 10, family: 'Manrope' }
},
border: {
display: false
}
}
}
}
});
}
// MA Chart
const maCtx = document.getElementById('ma-chart');
if (maCtx) {
if (chartInstances.ma) chartInstances.ma.destroy();
const prices = data.prices.map(p => p[1]);
const ma7 = calculateMA(prices, 7);
const ma25 = calculateMA(prices, 25);
const ma99 = calculateMA(prices, 99);
chartInstances.ma = new Chart(maCtx, {
type: 'line',
data: {
labels: prices.map((_, i) => i),
datasets: [
{
label: 'MA 7',
data: ma7,
borderColor: '#60A5FA',
backgroundColor: 'rgba(96, 165, 250, 0.1)',
borderWidth: 2.5,
fill: false,
tension: 0,
pointRadius: 0,
pointHoverRadius: 4
},
{
label: 'MA 25',
data: ma25,
borderColor: '#34D399',
backgroundColor: 'rgba(52, 211, 153, 0.1)',
borderWidth: 2.5,
fill: false,
tension: 0,
pointRadius: 0,
pointHoverRadius: 4
},
{
label: 'MA 99',
data: ma99,
borderColor: '#FBBF24',
backgroundColor: 'rgba(251, 191, 36, 0.1)',
borderWidth: 2.5,
fill: false,
tension: 0,
pointRadius: 0,
pointHoverRadius: 4
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
align: 'start',
labels: {
usePointStyle: true,
pointStyle: 'circle',
padding: 14,
font: {
size: 12,
weight: '600',
family: 'Manrope'
},
color: '#E2E8F0',
boxWidth: 14,
boxHeight: 14
}
},
tooltip: {
backgroundColor: 'rgba(10, 15, 30, 0.98)',
padding: 12,
borderColor: 'rgba(96, 165, 250, 0.3)',
borderWidth: 1.5,
titleColor: '#F8FAFC',
bodyColor: '#E2E8F0',
titleFont: { size: 12, weight: '700', family: 'Manrope' },
bodyFont: { size: 11, family: 'Manrope' },
cornerRadius: 8,
displayColors: true,
boxPadding: 6
}
},
scales: {
y: {
grid: {
color: 'rgba(255, 255, 255, 0.05)',
lineWidth: 1
},
ticks: {
color: '#64748B',
font: { size: 11, family: 'Manrope' },
callback: v => '$' + formatNum(v),
padding: 6
},
border: {
display: false
}
},
x: {
grid: {
display: true,
color: 'rgba(255, 255, 255, 0.03)',
lineWidth: 1
},
ticks: {
display: false,
color: '#64748B',
font: { size: 10, family: 'Manrope' }
},
border: {
display: false
}
}
}
}
});
}
} catch (e) {
console.error('Detail chart error:', e);
}
}
// Calculate RSI
function calculateRSI(prices, period = 14) {
const rsi = [];
for (let i = period; i < prices.length; i++) {
let gains = 0, losses = 0;
for (let j = i - period; j < i; j++) {
const change = prices[j + 1] - prices[j];
if (change > 0) gains += change;
else losses -= change;
}
const avgGain = gains / period;
const avgLoss = losses / period;
const rs = avgGain / avgLoss;
rsi.push(100 - (100 / (1 + rs)));
}
return rsi;
}
// Calculate Moving Average
function calculateMA(prices, period) {
const ma = [];
for (let i = 0; i < prices.length; i++) {
if (i < period - 1) {
ma.push(null);
} else {
const sum = prices.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0);
ma.push(sum / period);
}
}
return ma;
}
function formatNum(num) {
if (num === null || num === undefined || isNaN(num)) {
return '0.00';
}
num = Number(num);
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);
}
// ========== NEWS SECTION ==========
let newsData = [];
let newsFilter = { search: '', range: 'all', symbol: '' };
async function loadNewsData() {
return loadNews();
}
async function loadNews() {
const loadingEl = document.getElementById('news-loading');
const errorEl = document.getElementById('news-error');
const gridEl = document.getElementById('news-grid');
const countEl = document.getElementById('news-count');
try {
loadingEl.style.display = 'block';
errorEl.style.display = 'none';
gridEl.innerHTML = '';
let backendUrl = window.BACKEND_URL || window.location.origin;
let res = await fetch(`${backendUrl}/api/news/latest?limit=40`);
// If 404, try alternative endpoints
if (!res.ok && res.status === 404) {
const altBackend = backendUrl.includes(':7860') ? backendUrl.replace(':7860', ':7861') : `${backendUrl}:7861`;
try {
res = await fetch(`${altBackend}/api/news/latest?limit=40`);
if (res.ok) backendUrl = altBackend;
} catch (e) {
res = await fetch(`${window.location.origin}/api/news/latest?limit=40`);
if (res.ok) backendUrl = window.location.origin;
}
}
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}
const data = await res.json();
newsData = data.news || data.data || data || [];
if (!Array.isArray(newsData) || newsData.length === 0) {
gridEl.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 60px; color: var(--text-muted);">
<div style="font-size: 3rem; margin-bottom: 16px;">📰</div>
<div>No news available at the moment</div>
</div>
`;
countEl.textContent = '0 articles';
return;
}
countEl.textContent = `${newsData.length} articles`;
renderNews(newsData);
} catch (e) {
console.error('[News] Error loading news:', e);
errorEl.style.display = 'block';
const errorMessage = e.message || 'Unknown error';
const is404 = errorMessage.includes('404');
errorEl.innerHTML = `
<div style="font-size: 3rem; margin-bottom: 16px;">⚠️</div>
<div style="font-size: 1.125rem; margin-bottom: 8px;">Failed to load news</div>
<div style="font-size: 0.875rem; color: var(--text-muted);">
${is404 ? 'News endpoint not available. This feature may not be configured on the backend.' : errorMessage}
</div>
`;
gridEl.innerHTML = '';
countEl.textContent = '0 articles';
} finally {
loadingEl.style.display = 'none';
}
}
function renderNews(news) {
const gridEl = document.getElementById('news-grid');
const filtered = filterNews(news);
if (filtered.length === 0) {
gridEl.innerHTML = `
<div style="grid-column: 1 / -1; text-align: center; padding: 60px; color: var(--text-muted);">
<div style="font-size: 3rem; margin-bottom: 16px;">🔍</div>
<div>No news found matching your filters</div>
</div>
`;
return;
}
gridEl.innerHTML = filtered.map((item, idx) => {
const date = item.published_at || item.date || item.timestamp || new Date().toISOString();
const timeAgo = getTimeAgo(date);
const sentiment = item.sentiment || 'neutral';
const sentimentColor = sentiment === 'positive' ? 'var(--success)' :
sentiment === 'negative' ? 'var(--danger)' : 'var(--text-muted)';
const sentimentIcon = sentiment === 'positive' ? '📈' :
sentiment === 'negative' ? '📉' : '➡️';
const symbols = Array.isArray(item.symbols) ? item.symbols :
Array.isArray(item.coins) ? item.coins :
Array.isArray(item.tags) ? item.tags : [];
const source = item.source || 'Unknown';
return `
<div class="glass-card news-card" style="cursor: pointer; transition: all 0.3s;" onclick="openNewsModal(${idx})">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;">
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<span class="chip" style="font-size: 0.75rem; padding: 4px 10px;">${source}</span>
<span style="color: var(--text-muted); font-size: 0.75rem;">${timeAgo}</span>
</div>
<span style="font-size: 1.25rem;">${sentimentIcon}</span>
</div>
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; font-weight: 700; color: var(--text-primary); line-height: 1.4;">
${(item.title || 'No title').substring(0, 100)}${(item.title || '').length > 100 ? '...' : ''}
</h3>
${item.summary ? `<p style="margin: 0 0 12px 0; color: var(--text-secondary); font-size: 0.875rem; line-height: 1.5;">${item.summary.substring(0, 120)}...</p>` : ''}
<div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;">
${symbols.slice(0, 3).map(s => `<span class="chip" style="font-size: 0.75rem; padding: 4px 10px; background: var(--primary-glow); color: var(--primary-light);">${s}</span>`).join('')}
${symbols.length > 3 ? `<span style="color: var(--text-muted); font-size: 0.75rem;">+${symbols.length - 3} more</span>` : ''}
<span style="margin-left: auto; color: ${sentimentColor}; font-size: 0.75rem; font-weight: 600; text-transform: capitalize;">
${sentiment}
</span>
</div>
</div>
`;
}).join('');
// Store filtered news for modal
window.filteredNews = filtered;
// Add hover effects
document.querySelectorAll('.news-card').forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.boxShadow = '0 12px 40px rgba(0, 0, 0, 0.4), var(--shadow-glow-primary)';
this.style.borderColor = 'var(--primary)';
});
card.addEventListener('mouseleave', function() {
this.style.boxShadow = '';
this.style.borderColor = '';
});
});
}
function filterNews(news) {
return news.filter(item => {
const title = (item.title || '').toLowerCase();
const searchMatch = !newsFilter.search || title.includes(newsFilter.search.toLowerCase());
const symbols = Array.isArray(item.symbols) ? item.symbols :
Array.isArray(item.coins) ? item.coins : [];
const symbolMatch = !newsFilter.symbol ||
symbols.some(s => s.toLowerCase().includes(newsFilter.symbol.toLowerCase()));
return searchMatch && symbolMatch;
});
}
function getTimeAgo(dateStr) {
try {
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
const minutes = Math.floor(diff / 60000);
const hours = Math.floor(diff / 3600000);
const days = Math.floor(diff / 86400000);
if (minutes < 1) return 'Just now';
if (minutes < 60) return `${minutes}m ago`;
if (hours < 24) return `${hours}h ago`;
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString();
} catch {
return 'Recently';
}
}
function openNewsModal(index) {
const item = window.filteredNews[index];
if (!item) return;
const modal = document.querySelector('[data-news-modal]');
const content = document.querySelector('[data-news-modal-content]');
const sentiment = item.sentiment || 'neutral';
const sentimentColor = sentiment === 'positive' ? 'var(--success)' :
sentiment === 'negative' ? 'var(--danger)' : 'var(--text-muted)';
content.innerHTML = `
<div style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 16px;">
<div>
<span class="chip" style="margin-bottom: 8px; display: inline-block;">${item.source || 'Unknown'}</span>
<div style="color: var(--text-muted); font-size: 0.875rem;">${getTimeAgo(item.published_at || item.date)}</div>
</div>
<span style="color: ${sentimentColor}; font-weight: 600; text-transform: capitalize;">${sentiment}</span>
</div>
<h2 style="font-size: 1.75rem; font-weight: 800; margin: 0 0 16px 0; color: var(--text-primary); line-height: 1.3;">
${item.title || 'No title'}
</h2>
</div>
<div style="margin-bottom: 20px; color: var(--text-secondary); line-height: 1.6;">
${item.content || item.summary || item.description || 'No content available.'}
</div>
${item.url ? `
<div style="margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--glass-border);">
<a href="${item.url}" target="_blank" class="btn-secondary" style="display: inline-flex; align-items: center; gap: 8px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6M15 3h6v6M10 14L21 3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
Read Full Article
</a>
</div>
` : ''}
`;
modal.style.display = 'flex';
}
// ========== AI ADVISOR & SENTIMENT ANALYSIS ==========
document.addEventListener('DOMContentLoaded', () => {
// Query Form Handler
const queryForm = document.querySelector('[data-query-form]');
if (queryForm) {
queryForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(queryForm);
const query = formData.get('query');
const output = document.querySelector('[data-query-output]');
if (!query || !query.trim()) {
output.innerHTML = '<div style="color: var(--text-muted);">Please enter a query</div>';
return;
}
output.innerHTML = '<div style="color: var(--text-secondary); display: flex; align-items: center; gap: 8px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/></svg> Processing...</div>';
try {
const backendUrl = window.BACKEND_URL || window.location.origin;
const res = await fetch(`${backendUrl}/api/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
output.innerHTML = `
<div style="color: var(--text-primary); line-height: 1.6;">
<div style="font-weight: 600; margin-bottom: 8px; color: var(--primary-light);">Response:</div>
<div>${data.response || data.answer || data.message || JSON.stringify(data, null, 2)}</div>
</div>
`;
} catch (e) {
output.innerHTML = `<div style="color: var(--danger);">Error: ${e.message}</div>`;
}
});
}
// Sentiment Form Handler
const sentimentForm = document.querySelector('[data-sentiment-form]');
if (sentimentForm) {
sentimentForm.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(sentimentForm);
const text = formData.get('text');
const output = document.getElementById('sentiment-results');
const outputContent = document.querySelector('[data-sentiment-output]');
if (!text || !text.trim()) {
outputContent.innerHTML = '<div style="color: var(--text-muted);">Please enter text for analysis</div>';
return;
}
output.style.display = 'block';
outputContent.innerHTML = '<div style="color: var(--text-secondary); display: flex; align-items: center; gap: 8px;"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> Analyzing sentiment...</div>';
try {
const backendUrl = window.BACKEND_URL || window.location.origin;
const res = await fetch(`${backendUrl}/api/sentiment/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
const sentiment = data.sentiment || data.label || 'neutral';
const confidence = data.confidence || data.score || 0;
const isPositive = sentiment.toLowerCase().includes('positive') || sentiment.toLowerCase().includes('bullish');
const isNegative = sentiment.toLowerCase().includes('negative') || sentiment.toLowerCase().includes('bearish');
const sentimentColor = isPositive ? 'var(--success)' : isNegative ? 'var(--danger)' : 'var(--text-muted)';
const sentimentIcon = isPositive ? '📈' : isNegative ? '📉' : '➡️';
const sentimentBg = isPositive ? 'rgba(52, 211, 153, 0.15)' : isNegative ? 'rgba(248, 113, 113, 0.15)' : 'var(--glass-bg-light)';
outputContent.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 24px;">
<div style="padding: 20px; background: ${sentimentBg}; border-radius: 16px; border: 2px solid ${sentimentColor}; text-align: center;">
<div style="font-size: 3rem; margin-bottom: 12px;">${sentimentIcon}</div>
<div style="font-size: 1.5rem; font-weight: 800; color: ${sentimentColor}; margin-bottom: 8px; text-transform: capitalize;">${sentiment}</div>
<div style="font-size: 0.875rem; color: var(--text-muted);">Confidence: ${(confidence * 100).toFixed(1)}%</div>
</div>
<div style="padding: 20px; background: var(--glass-bg-light); border-radius: 16px; border: 1px solid var(--glass-border);">
<div style="font-weight: 600; color: var(--text-secondary); margin-bottom: 12px;">Details</div>
<div style="color: var(--text-primary); line-height: 1.6; font-size: 0.9375rem;">
${data.explanation || data.reasoning || 'Analysis completed using ensemble AI models.'}
</div>
</div>
</div>
${data.models_used ? `
<div style="padding: 16px; background: var(--glass-bg-light); border-radius: 12px; border: 1px solid var(--glass-border);">
<div style="font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; font-size: 0.875rem;">Models Used:</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${data.models_used.map(m => `<span class="chip" style="font-size: 0.75rem;">${m}</span>`).join('')}
</div>
</div>
` : ''}
`;
} catch (e) {
outputContent.innerHTML = `<div style="color: var(--danger);">Error: ${e.message}</div>`;
}
});
}
});
// Initialize news section
document.addEventListener('DOMContentLoaded', () => {
const newsPage = document.getElementById('page-news');
if (newsPage) {
// Load news when page becomes active
const observer = new MutationObserver((mutations) => {
if (newsPage.classList.contains('active') && newsData.length === 0) {
loadNews();
}
});
observer.observe(newsPage, { attributes: true, attributeFilter: ['class'] });
// Refresh button
document.getElementById('refresh-news')?.addEventListener('click', loadNews);
// Search input
document.querySelector('[data-news-search]')?.addEventListener('input', (e) => {
newsFilter.search = e.target.value;
renderNews(newsData);
});
// Range select
document.querySelector('[data-news-range]')?.addEventListener('change', (e) => {
newsFilter.range = e.target.value;
loadNews();
});
// Symbol input
document.querySelector('[data-news-symbol]')?.addEventListener('input', (e) => {
newsFilter.symbol = e.target.value;
renderNews(newsData);
});
// Close modal
document.querySelector('[data-close-news-modal]')?.addEventListener('click', () => {
document.querySelector('[data-news-modal]').style.display = 'none';
});
// Close modal on backdrop click
document.querySelector('[data-news-modal]')?.addEventListener('click', (e) => {
if (e.target === e.currentTarget) {
e.currentTarget.style.display = 'none';
}
});
}
});
</script>
<!-- Load Config JS -->
<script src="config.js?v=20250119"></script>
<!-- Optional Enhanced JS Utilities (Loaded conditionally) -->
<script src="static/js/icons.js" defer></script>
<script src="static/js/toast.js" defer></script>
<script src="static/js/theme-manager.js" defer></script>
<script src="static/js/animations.js" defer></script>
<script src="static/js/uiUtils.js?v=20250119" defer></script>
<script src="static/js/accessibility.js" defer></script>
<script src="static/js/provider-discovery.js" defer></script>
<script src="static/js/api-resource-loader.js?v=20250120" defer></script>
<!-- Force localhost override script -->
<script>
// Immediate override for localhost - runs before any other scripts
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
if (window.DASHBOARD_CONFIG) {
window.DASHBOARD_CONFIG.BACKEND_URL = `http://${window.location.hostname}:7860`;
window.DASHBOARD_CONFIG.WS_URL = `ws://${window.location.hostname}:7860/ws`;
}
window.BACKEND_URL = `http://${window.location.hostname}:7860`;
}
console.log('[Init] Hostname:', window.location.hostname);
console.log('[Init] Backend URL:', window.BACKEND_URL || window.DASHBOARD_CONFIG?.BACKEND_URL || 'not set');
</script>
<!-- Initialize Enhanced Features -->
<script>
// Initialize enhanced features when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
// Initialize Toast Manager (if available)
if (typeof ToastManager !== 'undefined') {
window.toast = new ToastManager();
console.log('[Init] Toast manager initialized');
}
// Initialize Theme Manager (if available)
if (typeof ThemeManager !== 'undefined') {
window.themeManager = new ThemeManager();
window.themeManager.init();
console.log('[Init] Theme manager initialized');
}
// Initialize Animation Controller (if available)
if (typeof AnimationController !== 'undefined') {
window.animations = new AnimationController();
console.log('[Init] Animation controller initialized');
}
// Initialize Provider Discovery (if available)
if (typeof ProviderDiscoveryEngine !== 'undefined') {
window.providerDiscovery = new ProviderDiscoveryEngine();
window.providerDiscovery.init().catch(e => {
console.warn('[Init] Provider discovery failed:', e);
});
}
// Initialize Accessibility (if available)
if (typeof AccessibilityManager !== 'undefined') {
window.accessibility = new AccessibilityManager();
window.accessibility.init();
console.log('[Init] Accessibility manager initialized');
}
// Initialize API Resource Loader (if available)
if (window.apiResourceLoader && !window.apiResourceLoader.initialized) {
window.apiResourceLoader.init().then(() => {
const stats = window.apiResourceLoader.getStats();
// Only log/show toast if resources were actually loaded
if (stats.unified.count > 0 || stats.ultimate.count > 0) {
console.log('[Init] API Resource Loader ready:', stats);
// Show toast notification if available
if (window.toast) {
window.toast.show(
`Loaded ${stats.unified.count + stats.ultimate.count} API resources`,
'success',
{ title: 'Resources Loaded', duration: 3000 }
);
}
}
// Silently skip if no resources loaded - they're optional
}).catch(() => {
// Completely silent - resources are optional
});
}
// Enhance UI with smooth animations
if (window.animations) {
// Add fade-in animation to cards
document.querySelectorAll('.glass-card, .stat-card').forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
setTimeout(() => {
card.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 50);
});
}
});
</script>
<!-- Load App JS as ES6 Module -->
<script type="module" src="static/js/app.js?v=20250119"></script>
</body>
</html>