| <!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> |
| |
| |
| <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"> |
| |
| <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" /> |
| |
| |
| <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" /> |
| |
| |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js" defer></script> |
|
|
| |
| <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> |
| |
| if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') { |
| window.BACKEND_URL = `http://${window.location.hostname}:7860`; |
| } else { |
| |
| window.BACKEND_URL = 'https://really-amin-datasourceforcryptocurrency.hf.space'; |
| } |
| </script> |
|
|
| <div class="app-shell"> |
| |
| <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 class="main-area"> |
| |
| <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"> |
| |
| <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> |
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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>×</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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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;">×</button> |
| </div> |
| </section> |
|
|
| |
| <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> |
|
|
| |
| <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>×</button> |
| </div> |
| </section> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <script> |
| let chartInstances = {}; |
| let allCoins = []; |
| let selectedCoin = null; |
| let selectedTimeframe = 7; |
| |
| |
| function initNavigation() { |
| const navButtons = document.querySelectorAll('.nav-button'); |
| const pages = document.querySelectorAll('.page'); |
| const topbarIcon = document.querySelector('.topbar-icon'); |
| |
| |
| 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) { |
| |
| 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': |
| |
| break; |
| case 'page-chart': |
| |
| break; |
| } |
| } |
| |
| navButtons.forEach(button => { |
| button.addEventListener('click', () => { |
| const targetPage = button.dataset.nav; |
| |
| |
| navButtons.forEach(btn => btn.classList.remove('active')); |
| button.classList.add('active'); |
| |
| |
| pages.forEach(page => { |
| page.classList.toggle('active', page.id === targetPage); |
| }); |
| |
| |
| updateHeaderIcon(targetPage); |
| |
| |
| loadPageData(targetPage); |
| |
| |
| window.scrollTo({ top: 0, behavior: 'smooth' }); |
| }); |
| }); |
| |
| |
| const activeButton = document.querySelector('.nav-button.active'); |
| if (activeButton) { |
| updateHeaderIcon(activeButton.dataset.nav); |
| } |
| } |
| |
| |
| function initThemeToggle() { |
| const themeToggle = document.querySelector('[data-theme-toggle]'); |
| const body = document.getElementById('main-body'); |
| |
| if (!themeToggle || !body) return; |
| |
| |
| const savedTheme = localStorage.getItem('theme') || 'dark'; |
| body.setAttribute('data-theme', savedTheme); |
| themeToggle.checked = savedTheme === 'light'; |
| |
| |
| themeToggle.addEventListener('change', (e) => { |
| const newTheme = e.target.checked ? 'light' : 'dark'; |
| body.setAttribute('data-theme', newTheme); |
| localStorage.setItem('theme', newTheme); |
| }); |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| initNavigation(); |
| initThemeToggle(); |
| |
| |
| |
| 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(); |
| }); |
| |
| |
| 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 { |
| |
| 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'); |
| |
| |
| if (window.toast) { |
| window.toast.show('Data refreshed successfully', 'success', { duration: 2000 }); |
| } |
| } catch (error) { |
| console.error('[Refresh] Error refreshing data:', error); |
| |
| |
| 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'; |
| } |
| } |
| } |
| |
| |
| async function loadOverviewStats() { |
| try { |
| |
| 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 (!res.ok && res.status === 404) { |
| |
| 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) { |
| |
| 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>'; |
| } |
| } |
| } |
| |
| |
| 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 (!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) => { |
| |
| 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(); |
| |
| |
| 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>`; |
| } |
| } |
| } |
| |
| |
| 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 (!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>`; |
| } |
| } |
| } |
| |
| |
| 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 } } |
| } |
| }); |
| } |
| |
| |
| 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 (!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'; |
| } |
| }); |
| |
| |
| 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(); |
| |
| |
| 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 { |
| |
| 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'}`; |
| |
| |
| const priceCtx = document.getElementById('price-chart'); |
| if (priceCtx) { |
| if (chartInstances.price) chartInstances.price.destroy(); |
| |
| const chartType = document.getElementById('chartType').value; |
| |
| |
| 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' |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| 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 |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| 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 |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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; |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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 (!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(''); |
| |
| |
| window.filteredNews = filtered; |
| |
| |
| 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'; |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| 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>`; |
| } |
| }); |
| } |
| |
| |
| 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>`; |
| } |
| }); |
| } |
| }); |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const newsPage = document.getElementById('page-news'); |
| if (newsPage) { |
| |
| const observer = new MutationObserver((mutations) => { |
| if (newsPage.classList.contains('active') && newsData.length === 0) { |
| loadNews(); |
| } |
| }); |
| observer.observe(newsPage, { attributes: true, attributeFilter: ['class'] }); |
| |
| |
| document.getElementById('refresh-news')?.addEventListener('click', loadNews); |
| |
| |
| document.querySelector('[data-news-search]')?.addEventListener('input', (e) => { |
| newsFilter.search = e.target.value; |
| renderNews(newsData); |
| }); |
| |
| |
| document.querySelector('[data-news-range]')?.addEventListener('change', (e) => { |
| newsFilter.range = e.target.value; |
| loadNews(); |
| }); |
| |
| |
| document.querySelector('[data-news-symbol]')?.addEventListener('input', (e) => { |
| newsFilter.symbol = e.target.value; |
| renderNews(newsData); |
| }); |
| |
| |
| document.querySelector('[data-close-news-modal]')?.addEventListener('click', () => { |
| document.querySelector('[data-news-modal]').style.display = 'none'; |
| }); |
| |
| |
| document.querySelector('[data-news-modal]')?.addEventListener('click', (e) => { |
| if (e.target === e.currentTarget) { |
| e.currentTarget.style.display = 'none'; |
| } |
| }); |
| } |
| }); |
| </script> |
| |
| |
| <script src="config.js?v=20250119"></script> |
| |
| |
| <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> |
| |
| |
| <script> |
| |
| 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> |
| |
| |
| <script> |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| if (typeof ToastManager !== 'undefined') { |
| window.toast = new ToastManager(); |
| console.log('[Init] Toast manager initialized'); |
| } |
| |
| |
| if (typeof ThemeManager !== 'undefined') { |
| window.themeManager = new ThemeManager(); |
| window.themeManager.init(); |
| console.log('[Init] Theme manager initialized'); |
| } |
| |
| |
| if (typeof AnimationController !== 'undefined') { |
| window.animations = new AnimationController(); |
| console.log('[Init] Animation controller initialized'); |
| } |
| |
| |
| if (typeof ProviderDiscoveryEngine !== 'undefined') { |
| window.providerDiscovery = new ProviderDiscoveryEngine(); |
| window.providerDiscovery.init().catch(e => { |
| console.warn('[Init] Provider discovery failed:', e); |
| }); |
| } |
| |
| |
| if (typeof AccessibilityManager !== 'undefined') { |
| window.accessibility = new AccessibilityManager(); |
| window.accessibility.init(); |
| console.log('[Init] Accessibility manager initialized'); |
| } |
| |
| |
| if (window.apiResourceLoader && !window.apiResourceLoader.initialized) { |
| window.apiResourceLoader.init().then(() => { |
| const stats = window.apiResourceLoader.getStats(); |
| |
| if (stats.unified.count > 0 || stats.ultimate.count > 0) { |
| console.log('[Init] API Resource Loader ready:', stats); |
| |
| |
| if (window.toast) { |
| window.toast.show( |
| `Loaded ${stats.unified.count + stats.ultimate.count} API resources`, |
| 'success', |
| { title: 'Resources Loaded', duration: 3000 } |
| ); |
| } |
| } |
| |
| }).catch(() => { |
| |
| }); |
| } |
| |
| |
| if (window.animations) { |
| |
| 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> |
| |
| |
| <script type="module" src="static/js/app.js?v=20250119"></script> |
| </body> |
| </html> |
|
|