|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const CONFIG = window.DASHBOARD_CONFIG || { |
|
|
BACKEND_URL: window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space', |
|
|
WS_URL: (window.location.origin || 'https://really-amin-datasourceforcryptocurrency.hf.space').replace('http://', 'ws://').replace('https://', 'wss://') + '/ws', |
|
|
UPDATE_INTERVAL: 30000, |
|
|
CACHE_TTL: 60000, |
|
|
ENDPOINTS: {}, |
|
|
WS_EVENTS: {}, |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WebSocketClient { |
|
|
constructor(url) { |
|
|
this.url = url; |
|
|
this.socket = null; |
|
|
this.status = 'disconnected'; |
|
|
this.reconnectAttempts = 0; |
|
|
this.maxReconnectAttempts = CONFIG.MAX_RECONNECT_ATTEMPTS || 5; |
|
|
this.reconnectDelay = CONFIG.RECONNECT_DELAY || 3000; |
|
|
this.listeners = new Map(); |
|
|
this.heartbeatInterval = null; |
|
|
this.clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; |
|
|
this.subscriptions = new Set(); |
|
|
} |
|
|
|
|
|
connect() { |
|
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) { |
|
|
console.log('[WS] Already connected'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
console.log('[WS] Connecting to:', this.url); |
|
|
this.socket = new WebSocket(this.url); |
|
|
|
|
|
this.socket.onopen = this.handleOpen.bind(this); |
|
|
this.socket.onmessage = this.handleMessage.bind(this); |
|
|
this.socket.onerror = this.handleError.bind(this); |
|
|
this.socket.onclose = this.handleClose.bind(this); |
|
|
|
|
|
this.updateStatus('connecting'); |
|
|
} catch (error) { |
|
|
console.error('[WS] Connection error:', error); |
|
|
this.scheduleReconnect(); |
|
|
} |
|
|
} |
|
|
|
|
|
handleOpen() { |
|
|
console.log('[WS] Connected successfully'); |
|
|
this.status = 'connected'; |
|
|
this.reconnectAttempts = 0; |
|
|
this.updateStatus('connected'); |
|
|
this.startHeartbeat(); |
|
|
|
|
|
|
|
|
this.send({ |
|
|
type: 'identify', |
|
|
client_id: this.clientId, |
|
|
metadata: { |
|
|
user_agent: navigator.userAgent, |
|
|
timestamp: new Date().toISOString() |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
this.subscribe('market_data'); |
|
|
this.subscribe('sentiment'); |
|
|
this.subscribe('news'); |
|
|
|
|
|
this.emit('connected', true); |
|
|
} |
|
|
|
|
|
handleMessage(event) { |
|
|
try { |
|
|
const data = JSON.parse(event.data); |
|
|
|
|
|
if (CONFIG.DEBUG?.SHOW_WS_MESSAGES) { |
|
|
console.log('[WS] Message received:', data.type, data); |
|
|
} |
|
|
|
|
|
|
|
|
switch (data.type) { |
|
|
case 'heartbeat': |
|
|
case 'ping': |
|
|
this.send({ type: 'pong' }); |
|
|
return; |
|
|
|
|
|
case 'welcome': |
|
|
if (data.session_id) { |
|
|
this.clientId = data.session_id; |
|
|
} |
|
|
break; |
|
|
|
|
|
case 'api_update': |
|
|
this.emit('api_update', data); |
|
|
this.emit('market_update', data); |
|
|
break; |
|
|
|
|
|
case 'status_update': |
|
|
this.emit('status_update', data); |
|
|
break; |
|
|
|
|
|
case 'schedule_update': |
|
|
this.emit('schedule_update', data); |
|
|
break; |
|
|
|
|
|
case 'subscribed': |
|
|
case 'unsubscribed': |
|
|
console.log(`[WS] ${data.type} to ${data.api_id || data.service}`); |
|
|
break; |
|
|
} |
|
|
|
|
|
|
|
|
this.emit(data.type, data); |
|
|
this.emit('message', data); |
|
|
} catch (error) { |
|
|
console.error('[WS] Message parse error:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
handleError(error) { |
|
|
|
|
|
|
|
|
const socketState = this.socket ? this.socket.readyState : 'null'; |
|
|
const stateNames = { |
|
|
0: 'CONNECTING', |
|
|
1: 'OPEN', |
|
|
2: 'CLOSING', |
|
|
3: 'CLOSED' |
|
|
}; |
|
|
|
|
|
const stateName = stateNames[socketState] || `UNKNOWN(${socketState})`; |
|
|
|
|
|
|
|
|
if (!this._errorLogged) { |
|
|
console.error('[WS] Connection error:', { |
|
|
url: this.url, |
|
|
state: stateName, |
|
|
readyState: socketState, |
|
|
message: 'WebSocket connection failed. Check if server is running and URL is correct.' |
|
|
}); |
|
|
this._errorLogged = true; |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
this._errorLogged = false; |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
this.updateStatus('error'); |
|
|
|
|
|
|
|
|
if (this.socket && this.socket.readyState === WebSocket.CLOSED && |
|
|
this.reconnectAttempts < this.maxReconnectAttempts) { |
|
|
this.scheduleReconnect(); |
|
|
} |
|
|
} |
|
|
|
|
|
handleClose() { |
|
|
console.log('[WS] Connection closed'); |
|
|
this.status = 'disconnected'; |
|
|
this.updateStatus('disconnected'); |
|
|
this.stopHeartbeat(); |
|
|
this.emit('connected', false); |
|
|
this.scheduleReconnect(); |
|
|
} |
|
|
|
|
|
scheduleReconnect() { |
|
|
if (this.reconnectAttempts >= this.maxReconnectAttempts) { |
|
|
console.error('[WS] Max reconnection attempts reached'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.reconnectAttempts++; |
|
|
console.log(`[WS] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts})`); |
|
|
|
|
|
setTimeout(() => this.connect(), this.reconnectDelay); |
|
|
} |
|
|
|
|
|
startHeartbeat() { |
|
|
this.heartbeatInterval = setInterval(() => { |
|
|
if (this.isConnected()) { |
|
|
this.send({ type: 'ping' }); |
|
|
} |
|
|
}, CONFIG.HEARTBEAT_INTERVAL || 30000); |
|
|
} |
|
|
|
|
|
stopHeartbeat() { |
|
|
if (this.heartbeatInterval) { |
|
|
clearInterval(this.heartbeatInterval); |
|
|
this.heartbeatInterval = null; |
|
|
} |
|
|
} |
|
|
|
|
|
send(data) { |
|
|
if (this.isConnected()) { |
|
|
this.socket.send(JSON.stringify(data)); |
|
|
return true; |
|
|
} |
|
|
console.warn('[WS] Cannot send - not connected'); |
|
|
return false; |
|
|
} |
|
|
|
|
|
subscribe(service) { |
|
|
if (!this.subscriptions.has(service)) { |
|
|
this.subscriptions.add(service); |
|
|
this.send({ |
|
|
type: 'subscribe', |
|
|
service: service, |
|
|
api_id: service |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
unsubscribe(service) { |
|
|
if (this.subscriptions.has(service)) { |
|
|
this.subscriptions.delete(service); |
|
|
this.send({ |
|
|
type: 'unsubscribe', |
|
|
service: service, |
|
|
api_id: service |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
on(event, callback) { |
|
|
if (!this.listeners.has(event)) { |
|
|
this.listeners.set(event, []); |
|
|
} |
|
|
this.listeners.get(event).push(callback); |
|
|
} |
|
|
|
|
|
emit(event, data) { |
|
|
if (this.listeners.has(event)) { |
|
|
this.listeners.get(event).forEach(callback => callback(data)); |
|
|
} |
|
|
} |
|
|
|
|
|
updateStatus(status) { |
|
|
this.status = status; |
|
|
|
|
|
const statusBar = document.getElementById('connection-status-bar'); |
|
|
const statusDot = document.getElementById('ws-status-dot'); |
|
|
const statusText = document.getElementById('ws-status-text'); |
|
|
|
|
|
if (statusBar && statusDot && statusText) { |
|
|
if (status === 'connected') { |
|
|
statusBar.classList.remove('disconnected'); |
|
|
statusText.textContent = 'Connected'; |
|
|
} else if (status === 'disconnected' || status === 'error') { |
|
|
statusBar.classList.add('disconnected'); |
|
|
statusText.textContent = status === 'error' ? 'Connection Error' : 'Disconnected'; |
|
|
} else { |
|
|
statusText.textContent = 'Connecting...'; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
isConnected() { |
|
|
return this.socket && this.socket.readyState === WebSocket.OPEN; |
|
|
} |
|
|
|
|
|
disconnect() { |
|
|
if (this.socket) { |
|
|
this.socket.close(); |
|
|
} |
|
|
this.stopHeartbeat(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class APIClient { |
|
|
constructor(baseURL) { |
|
|
this.baseURL = baseURL || CONFIG.BACKEND_URL; |
|
|
this.cache = new Map(); |
|
|
this.endpoints = CONFIG.ENDPOINTS || {}; |
|
|
} |
|
|
|
|
|
async request(endpoint, options = {}) { |
|
|
const url = `${this.baseURL}${endpoint}`; |
|
|
const cacheKey = `${options.method || 'GET'}:${url}`; |
|
|
|
|
|
|
|
|
if (options.cache && this.cache.has(cacheKey)) { |
|
|
const cached = this.cache.get(cacheKey); |
|
|
if (Date.now() - cached.timestamp < CONFIG.CACHE_TTL) { |
|
|
if (CONFIG.DEBUG?.SHOW_API_REQUESTS) { |
|
|
console.log('[API] Cache hit:', endpoint); |
|
|
} |
|
|
return cached.data; |
|
|
} |
|
|
} |
|
|
|
|
|
try { |
|
|
if (CONFIG.DEBUG?.SHOW_API_REQUESTS) { |
|
|
console.log('[API] Request:', endpoint, options); |
|
|
} |
|
|
|
|
|
const response = await fetch(url, { |
|
|
method: options.method || 'GET', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
...options.headers, |
|
|
}, |
|
|
body: options.body ? JSON.stringify(options.body) : undefined, |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
|
|
|
if (!options.method || options.method === 'GET') { |
|
|
this.cache.set(cacheKey, { |
|
|
data, |
|
|
timestamp: Date.now(), |
|
|
}); |
|
|
} |
|
|
|
|
|
return data; |
|
|
} catch (error) { |
|
|
console.error('[API] Error:', endpoint, error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async getHealth() { |
|
|
return this.request(this.endpoints.HEALTH || '/api/health', { cache: true }); |
|
|
} |
|
|
|
|
|
async getSystemStatus() { |
|
|
return this.request(this.endpoints.SYSTEM_STATUS || '/api/system/status', { cache: true }); |
|
|
} |
|
|
|
|
|
|
|
|
async getMarketStats() { |
|
|
return this.request(this.endpoints.MARKET || '/api/market/stats', { cache: true }); |
|
|
} |
|
|
|
|
|
async getMarketPrices(limit = 50) { |
|
|
return this.request(`${this.endpoints.MARKET_PRICES || '/api/market/prices'}?limit=${limit}`, { cache: true }); |
|
|
} |
|
|
|
|
|
async getTopCoins(limit = 20) { |
|
|
return this.request(`${this.endpoints.COINS_TOP || '/api/coins/top'}?limit=${limit}`, { cache: true }); |
|
|
} |
|
|
|
|
|
async getCoinDetails(symbol) { |
|
|
return this.request(`${this.endpoints.COIN_DETAILS || '/api/coins'}/${symbol}`, { cache: true }); |
|
|
} |
|
|
|
|
|
async getOHLCV(symbol, interval = '1h', limit = 100) { |
|
|
const endpoint = this.endpoints.OHLCV || '/api/ohlcv'; |
|
|
return this.request(`${endpoint}?symbol=${symbol}&interval=${interval}&limit=${limit}`, { cache: true }); |
|
|
} |
|
|
|
|
|
|
|
|
async getChartData(symbol, interval = '1h', limit = 100) { |
|
|
const endpoint = this.endpoints.CHART_HISTORY || '/api/charts/price'; |
|
|
return this.request(`${endpoint}/${symbol}?interval=${interval}&limit=${limit}`, { cache: true }); |
|
|
} |
|
|
|
|
|
async analyzeChart(symbol, interval = '1h') { |
|
|
return this.request(this.endpoints.CHART_ANALYZE || '/api/charts/analyze', { |
|
|
method: 'POST', |
|
|
body: { symbol, interval } |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async getSentiment() { |
|
|
return this.request(this.endpoints.SENTIMENT || '/api/sentiment', { cache: true }); |
|
|
} |
|
|
|
|
|
async analyzeSentiment(texts) { |
|
|
return this.request(this.endpoints.SENTIMENT_ANALYZE || '/api/sentiment/analyze', { |
|
|
method: 'POST', |
|
|
body: { texts } |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async getNews(limit = 20) { |
|
|
return this.request(`${this.endpoints.NEWS || '/api/news/latest'}?limit=${limit}`, { cache: true }); |
|
|
} |
|
|
|
|
|
async summarizeNews(articleUrl) { |
|
|
return this.request(this.endpoints.NEWS_SUMMARIZE || '/api/news/summarize', { |
|
|
method: 'POST', |
|
|
body: { url: articleUrl } |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async getProviders() { |
|
|
return this.request(this.endpoints.PROVIDERS || '/api/providers', { cache: true }); |
|
|
} |
|
|
|
|
|
async getProviderStatus() { |
|
|
return this.request(this.endpoints.PROVIDER_STATUS || '/api/providers/status', { cache: true }); |
|
|
} |
|
|
|
|
|
|
|
|
async getHFHealth() { |
|
|
return this.request(this.endpoints.HF_HEALTH || '/api/hf/health', { cache: true }); |
|
|
} |
|
|
|
|
|
async getHFRegistry() { |
|
|
return this.request(this.endpoints.HF_REGISTRY || '/api/hf/registry', { cache: true }); |
|
|
} |
|
|
|
|
|
async runSentimentAnalysis(texts, model = null) { |
|
|
return this.request(this.endpoints.HF_SENTIMENT || '/api/hf/run-sentiment', { |
|
|
method: 'POST', |
|
|
body: { texts, model } |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async getDatasets() { |
|
|
return this.request(this.endpoints.DATASETS || '/api/datasets/list', { cache: true }); |
|
|
} |
|
|
|
|
|
async getModels() { |
|
|
return this.request(this.endpoints.MODELS || '/api/models/list', { cache: true }); |
|
|
} |
|
|
|
|
|
async testModel(modelName, input) { |
|
|
return this.request(this.endpoints.MODELS_TEST || '/api/models/test', { |
|
|
method: 'POST', |
|
|
body: { model: modelName, input } |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async query(text) { |
|
|
return this.request(this.endpoints.QUERY || '/api/query', { |
|
|
method: 'POST', |
|
|
body: { query: text } |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async getCategories() { |
|
|
return this.request(this.endpoints.CATEGORIES || '/api/categories', { cache: true }); |
|
|
} |
|
|
|
|
|
async getRateLimits() { |
|
|
return this.request(this.endpoints.RATE_LIMITS || '/api/rate-limits', { cache: true }); |
|
|
} |
|
|
|
|
|
async getLogs(logType = 'recent') { |
|
|
return this.request(`${this.endpoints.LOGS || '/api/logs'}/${logType}`, { cache: true }); |
|
|
} |
|
|
|
|
|
async getAlerts() { |
|
|
return this.request(this.endpoints.ALERTS || '/api/alerts', { cache: true }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const Utils = { |
|
|
formatCurrency(value) { |
|
|
if (value === null || value === undefined || isNaN(value)) { |
|
|
return 'β'; |
|
|
} |
|
|
const num = Number(value); |
|
|
if (Math.abs(num) >= 1e12) { |
|
|
return `$${(num / 1e12).toFixed(2)}T`; |
|
|
} |
|
|
if (Math.abs(num) >= 1e9) { |
|
|
return `$${(num / 1e9).toFixed(2)}B`; |
|
|
} |
|
|
if (Math.abs(num) >= 1e6) { |
|
|
return `$${(num / 1e6).toFixed(2)}M`; |
|
|
} |
|
|
if (Math.abs(num) >= 1e3) { |
|
|
return `$${(num / 1e3).toFixed(2)}K`; |
|
|
} |
|
|
return `$${num.toLocaleString(undefined, { |
|
|
minimumFractionDigits: 2, |
|
|
maximumFractionDigits: 2 |
|
|
})}`; |
|
|
}, |
|
|
|
|
|
formatPercent(value) { |
|
|
if (value === null || value === undefined || isNaN(value)) { |
|
|
return 'β'; |
|
|
} |
|
|
const num = Number(value); |
|
|
const sign = num >= 0 ? '+' : ''; |
|
|
return `${sign}${num.toFixed(2)}%`; |
|
|
}, |
|
|
|
|
|
formatNumber(value) { |
|
|
if (value === null || value === undefined || isNaN(value)) { |
|
|
return 'β'; |
|
|
} |
|
|
return Number(value).toLocaleString(); |
|
|
}, |
|
|
|
|
|
formatDate(timestamp) { |
|
|
if (!timestamp) return 'β'; |
|
|
const date = new Date(timestamp); |
|
|
const options = CONFIG.FORMATS?.DATE?.OPTIONS || { |
|
|
year: 'numeric', |
|
|
month: 'long', |
|
|
day: 'numeric', |
|
|
hour: '2-digit', |
|
|
minute: '2-digit', |
|
|
}; |
|
|
return date.toLocaleDateString(CONFIG.FORMATS?.DATE?.LOCALE || 'en-US', options); |
|
|
}, |
|
|
|
|
|
getChangeClass(value) { |
|
|
if (value > 0) return 'positive'; |
|
|
if (value < 0) return 'negative'; |
|
|
return 'neutral'; |
|
|
}, |
|
|
|
|
|
showLoader(element) { |
|
|
if (element) { |
|
|
element.innerHTML = ` |
|
|
<div class="loading-cell"> |
|
|
<div class="loader"></div> |
|
|
Loading... |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
}, |
|
|
|
|
|
showError(element, message) { |
|
|
if (element) { |
|
|
element.innerHTML = ` |
|
|
<div class="error-message"> |
|
|
<i class="fas fa-exclamation-circle"></i> |
|
|
${message} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
}, |
|
|
|
|
|
debounce(func, wait) { |
|
|
let timeout; |
|
|
return function executedFunction(...args) { |
|
|
const later = () => { |
|
|
clearTimeout(timeout); |
|
|
func(...args); |
|
|
}; |
|
|
clearTimeout(timeout); |
|
|
timeout = setTimeout(later, wait); |
|
|
}; |
|
|
}, |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ViewManager { |
|
|
constructor() { |
|
|
this.currentView = 'overview'; |
|
|
this.views = new Map(); |
|
|
this.init(); |
|
|
} |
|
|
|
|
|
init() { |
|
|
|
|
|
document.querySelectorAll('.nav-tab-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
const view = btn.dataset.view; |
|
|
this.switchView(view); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.mobile-nav-tab-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
const view = btn.dataset.view; |
|
|
this.switchView(view); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
switchView(viewName) { |
|
|
if (this.currentView === viewName) return; |
|
|
|
|
|
|
|
|
document.querySelectorAll('.view-section').forEach(section => { |
|
|
section.classList.remove('active'); |
|
|
}); |
|
|
|
|
|
|
|
|
const viewSection = document.getElementById(`view-${viewName}`); |
|
|
if (viewSection) { |
|
|
viewSection.classList.add('active'); |
|
|
} |
|
|
|
|
|
|
|
|
document.querySelectorAll('.nav-tab-btn, .mobile-nav-tab-btn').forEach(btn => { |
|
|
btn.classList.remove('active'); |
|
|
if (btn.dataset.view === viewName) { |
|
|
btn.classList.add('active'); |
|
|
} |
|
|
}); |
|
|
|
|
|
this.currentView = viewName; |
|
|
console.log('[View] Switched to:', viewName); |
|
|
|
|
|
|
|
|
this.triggerViewUpdate(viewName); |
|
|
} |
|
|
|
|
|
triggerViewUpdate(viewName) { |
|
|
const event = new CustomEvent('viewChange', { detail: { view: viewName } }); |
|
|
document.dispatchEvent(event); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DashboardApp { |
|
|
constructor() { |
|
|
this.ws = new WebSocketClient(CONFIG.WS_URL); |
|
|
this.api = new APIClient(CONFIG.BACKEND_URL); |
|
|
this.viewManager = new ViewManager(); |
|
|
this.updateInterval = null; |
|
|
this.data = { |
|
|
market: null, |
|
|
sentiment: null, |
|
|
trending: null, |
|
|
news: [], |
|
|
providers: [], |
|
|
}; |
|
|
} |
|
|
|
|
|
async init() { |
|
|
console.log('[App] Initializing dashboard...'); |
|
|
|
|
|
|
|
|
this.ws.connect(); |
|
|
this.setupWebSocketHandlers(); |
|
|
|
|
|
|
|
|
this.setupUIHandlers(); |
|
|
|
|
|
|
|
|
await this.loadInitialData(); |
|
|
|
|
|
|
|
|
this.startPeriodicUpdates(); |
|
|
|
|
|
|
|
|
document.addEventListener('viewChange', (e) => { |
|
|
this.handleViewChange(e.detail.view); |
|
|
}); |
|
|
|
|
|
console.log('[App] Dashboard initialized successfully'); |
|
|
} |
|
|
|
|
|
setupWebSocketHandlers() { |
|
|
this.ws.on('connected', (isConnected) => { |
|
|
console.log('[App] WebSocket connection status:', isConnected); |
|
|
}); |
|
|
|
|
|
this.ws.on('api_update', (data) => { |
|
|
console.log('[App] API update received'); |
|
|
if (data.api_id === 'market_data' || data.service === 'market_data') { |
|
|
this.handleMarketUpdate(data); |
|
|
} |
|
|
}); |
|
|
|
|
|
this.ws.on('market_update', (data) => { |
|
|
console.log('[App] Market update received'); |
|
|
this.handleMarketUpdate(data); |
|
|
}); |
|
|
|
|
|
this.ws.on('sentiment_update', (data) => { |
|
|
console.log('[App] Sentiment update received'); |
|
|
this.handleSentimentUpdate(data); |
|
|
}); |
|
|
|
|
|
this.ws.on('status_update', (data) => { |
|
|
console.log('[App] Status update received'); |
|
|
if (data.status?.active_connections !== undefined) { |
|
|
this.updateOnlineUsers(data.status.active_connections); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
setupUIHandlers() { |
|
|
|
|
|
const themeToggle = document.getElementById('theme-toggle'); |
|
|
if (themeToggle) { |
|
|
themeToggle.addEventListener('click', () => this.toggleTheme()); |
|
|
} |
|
|
|
|
|
|
|
|
const notificationsBtn = document.getElementById('notifications-btn'); |
|
|
const notificationsPanel = document.getElementById('notifications-panel'); |
|
|
const closeNotifications = document.getElementById('close-notifications'); |
|
|
|
|
|
if (notificationsBtn && notificationsPanel) { |
|
|
notificationsBtn.addEventListener('click', () => { |
|
|
notificationsPanel.classList.toggle('active'); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (closeNotifications && notificationsPanel) { |
|
|
closeNotifications.addEventListener('click', () => { |
|
|
notificationsPanel.classList.remove('active'); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const settingsBtn = document.getElementById('settings-btn'); |
|
|
const settingsModal = document.getElementById('settings-modal'); |
|
|
const closeSettings = document.getElementById('close-settings'); |
|
|
|
|
|
if (settingsBtn && settingsModal) { |
|
|
settingsBtn.addEventListener('click', () => { |
|
|
settingsModal.classList.add('active'); |
|
|
}); |
|
|
} |
|
|
|
|
|
if (closeSettings && settingsModal) { |
|
|
closeSettings.addEventListener('click', () => { |
|
|
settingsModal.classList.remove('active'); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const refreshCoins = document.getElementById('refresh-coins'); |
|
|
if (refreshCoins) { |
|
|
refreshCoins.addEventListener('click', () => this.loadMarketData()); |
|
|
} |
|
|
|
|
|
const refreshProviders = document.getElementById('refresh-providers'); |
|
|
if (refreshProviders) { |
|
|
refreshProviders.addEventListener('click', () => this.loadProviders()); |
|
|
} |
|
|
|
|
|
|
|
|
const minimizeStats = document.getElementById('minimize-stats'); |
|
|
const floatingStats = document.getElementById('floating-stats'); |
|
|
if (minimizeStats && floatingStats) { |
|
|
minimizeStats.addEventListener('click', () => { |
|
|
floatingStats.classList.toggle('minimized'); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const globalSearch = document.getElementById('global-search'); |
|
|
if (globalSearch) { |
|
|
globalSearch.addEventListener('input', Utils.debounce((e) => { |
|
|
this.handleSearch(e.target.value); |
|
|
}, CONFIG.RATE_LIMITS?.SEARCH_DEBOUNCE_MS || 300)); |
|
|
} |
|
|
|
|
|
|
|
|
this.setupAIToolHandlers(); |
|
|
|
|
|
|
|
|
const marketFilter = document.getElementById('market-filter'); |
|
|
if (marketFilter) { |
|
|
marketFilter.addEventListener('change', (e) => { |
|
|
this.filterMarket(e.target.value); |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
setupAIToolHandlers() { |
|
|
const sentimentBtn = document.getElementById('sentiment-analysis-btn'); |
|
|
const summaryBtn = document.getElementById('news-summary-btn'); |
|
|
const predictionBtn = document.getElementById('price-prediction-btn'); |
|
|
const patternBtn = document.getElementById('pattern-detection-btn'); |
|
|
|
|
|
if (sentimentBtn) { |
|
|
sentimentBtn.addEventListener('click', () => this.runSentimentAnalysis()); |
|
|
} |
|
|
|
|
|
if (summaryBtn) { |
|
|
summaryBtn.addEventListener('click', () => this.runNewsSummary()); |
|
|
} |
|
|
|
|
|
if (predictionBtn) { |
|
|
predictionBtn.addEventListener('click', () => this.runPricePrediction()); |
|
|
} |
|
|
|
|
|
if (patternBtn) { |
|
|
patternBtn.addEventListener('click', () => this.runPatternDetection()); |
|
|
} |
|
|
|
|
|
const clearResults = document.getElementById('clear-results'); |
|
|
const aiResults = document.getElementById('ai-results'); |
|
|
if (clearResults && aiResults) { |
|
|
clearResults.addEventListener('click', () => { |
|
|
aiResults.style.display = 'none'; |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
async loadInitialData() { |
|
|
this.showLoadingOverlay(true); |
|
|
|
|
|
try { |
|
|
await Promise.all([ |
|
|
this.loadMarketData(), |
|
|
this.loadSentimentData(), |
|
|
this.loadNewsData(), |
|
|
]); |
|
|
} catch (error) { |
|
|
console.error('[App] Error loading initial data:', error); |
|
|
} |
|
|
|
|
|
this.showLoadingOverlay(false); |
|
|
} |
|
|
|
|
|
async loadMarketData() { |
|
|
try { |
|
|
const [stats, coins] = await Promise.all([ |
|
|
this.api.getMarketStats(), |
|
|
this.api.getTopCoins(CONFIG.MAX_COINS_DISPLAY || 20) |
|
|
]); |
|
|
|
|
|
this.data.market = { stats, coins }; |
|
|
const coinsList = coins?.coins || coins || []; |
|
|
|
|
|
this.renderMarketStats(stats?.stats || stats); |
|
|
this.renderCoinsTable(coinsList); |
|
|
this.renderCoinsGrid(coinsList); |
|
|
} catch (error) { |
|
|
console.error('[App] Error loading market data:', error); |
|
|
Utils.showError(document.getElementById('coins-table-body'), 'Failed to load market data'); |
|
|
} |
|
|
} |
|
|
|
|
|
async loadSentimentData() { |
|
|
try { |
|
|
const data = await this.api.getSentiment(); |
|
|
this.data.sentiment = data; |
|
|
this.renderSentiment(data); |
|
|
} catch (error) { |
|
|
console.error('[App] Error loading sentiment data:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
async loadNewsData() { |
|
|
try { |
|
|
const data = await this.api.getNews(CONFIG.MAX_NEWS_DISPLAY || 20); |
|
|
this.data.news = data.news || data || []; |
|
|
this.renderNews(this.data.news); |
|
|
} catch (error) { |
|
|
console.error('[App] Error loading news data:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
async loadProviders() { |
|
|
try { |
|
|
const providers = await this.api.getProviders(); |
|
|
this.data.providers = providers.providers || providers || []; |
|
|
this.renderProviders(this.data.providers); |
|
|
} catch (error) { |
|
|
console.error('[App] Error loading providers:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
renderMarketStats(data) { |
|
|
|
|
|
const totalMarketCap = document.getElementById('total-market-cap'); |
|
|
const volume24h = document.getElementById('volume-24h'); |
|
|
const marketTrend = document.getElementById('market-trend'); |
|
|
const activeCryptos = document.getElementById('active-cryptocurrencies'); |
|
|
const marketsCount = document.getElementById('markets-count'); |
|
|
const fearGreed = document.getElementById('fear-greed-index'); |
|
|
const marketCapChange24h = document.getElementById('market-cap-change-24h'); |
|
|
const top10Share = document.getElementById('top10-share'); |
|
|
const btcPrice = document.getElementById('btc-price'); |
|
|
const ethPrice = document.getElementById('eth-price'); |
|
|
|
|
|
if (totalMarketCap && data?.total_market_cap) { |
|
|
totalMarketCap.textContent = Utils.formatCurrency(data.total_market_cap); |
|
|
const marketCapChange = document.getElementById('market-cap-change'); |
|
|
if (marketCapChange && data.market_cap_change_percentage_24h !== undefined) { |
|
|
const changeEl = marketCapChange.querySelector('span'); |
|
|
if (changeEl) { |
|
|
changeEl.textContent = Utils.formatPercent(data.market_cap_change_percentage_24h); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (volume24h && data?.total_volume_24h) { |
|
|
volume24h.textContent = Utils.formatCurrency(data.total_volume_24h); |
|
|
const volumeChange = document.getElementById('volume-change'); |
|
|
if (volumeChange) { |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
if (marketTrend && data?.market_cap_change_percentage_24h !== undefined) { |
|
|
const change = data.market_cap_change_percentage_24h; |
|
|
marketTrend.textContent = change > 0 ? 'Bullish' : change < 0 ? 'Bearish' : 'Neutral'; |
|
|
const trendChangeEl = document.getElementById('trend-change'); |
|
|
if (trendChangeEl) { |
|
|
const changeSpan = trendChangeEl.querySelector('span'); |
|
|
if (changeSpan) { |
|
|
changeSpan.textContent = Utils.formatPercent(change); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const activeCryptos = document.getElementById('active-cryptocurrencies'); |
|
|
const marketsCount = document.getElementById('markets-count'); |
|
|
const fearGreed = document.getElementById('fear-greed-index'); |
|
|
const marketCapChange24h = document.getElementById('market-cap-change-24h'); |
|
|
const top10Share = document.getElementById('top10-share'); |
|
|
const btcPrice = document.getElementById('btc-price'); |
|
|
const ethPrice = document.getElementById('eth-price'); |
|
|
const btcDominance = document.getElementById('btc-dominance'); |
|
|
const ethDominance = document.getElementById('eth-dominance'); |
|
|
|
|
|
if (activeCryptos && data?.active_cryptocurrencies) { |
|
|
activeCryptos.textContent = Utils.formatNumber(data.active_cryptocurrencies); |
|
|
} |
|
|
|
|
|
if (marketsCount && data?.markets) { |
|
|
marketsCount.textContent = Utils.formatNumber(data.markets); |
|
|
} |
|
|
|
|
|
if (fearGreed && data?.fear_greed_index !== undefined) { |
|
|
fearGreed.textContent = data.fear_greed_index || 'N/A'; |
|
|
const fearGreedChange = document.getElementById('fear-greed-change'); |
|
|
if (fearGreedChange) { |
|
|
const index = data.fear_greed_index || 50; |
|
|
if (index >= 75) fearGreedChange.textContent = 'Extreme Greed'; |
|
|
else if (index >= 55) fearGreedChange.textContent = 'Greed'; |
|
|
else if (index >= 45) fearGreedChange.textContent = 'Neutral'; |
|
|
else if (index >= 25) fearGreedChange.textContent = 'Fear'; |
|
|
else fearGreedChange.textContent = 'Extreme Fear'; |
|
|
} |
|
|
} |
|
|
|
|
|
if (btcDominance && data?.btc_dominance) { |
|
|
document.getElementById('btc-dominance').textContent = `${data.btc_dominance.toFixed(1)}%`; |
|
|
} |
|
|
|
|
|
if (ethDominance && data?.eth_dominance) { |
|
|
ethDominance.textContent = `${data.eth_dominance.toFixed(1)}%`; |
|
|
} |
|
|
} |
|
|
|
|
|
renderCoinsTable(coins) { |
|
|
const tbody = document.getElementById('coins-table-body'); |
|
|
if (!tbody) return; |
|
|
|
|
|
if (!coins || coins.length === 0) { |
|
|
tbody.innerHTML = '<tr><td colspan="7">No data available</td></tr>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
tbody.innerHTML = coins.slice(0, CONFIG.MAX_COINS_DISPLAY || 20).map((coin, index) => ` |
|
|
<tr> |
|
|
<td>${coin.rank || index + 1}</td> |
|
|
<td> |
|
|
<div style="display: flex; align-items: center; gap: 8px;"> |
|
|
<strong>${coin.symbol || coin.name}</strong> |
|
|
<span style="color: var(--text-muted); font-size: 0.875rem;">${coin.name || ''}</span> |
|
|
</div> |
|
|
</td> |
|
|
<td style="font-family: var(--font-mono);">${Utils.formatCurrency(coin.price || coin.current_price)}</td> |
|
|
<td> |
|
|
<span class="stat-change ${Utils.getChangeClass(coin.change_24h || coin.price_change_percentage_24h)}"> |
|
|
${Utils.formatPercent(coin.change_24h || coin.price_change_percentage_24h)} |
|
|
</span> |
|
|
</td> |
|
|
<td>${Utils.formatCurrency(coin.volume_24h || coin.total_volume)}</td> |
|
|
<td>${Utils.formatCurrency(coin.market_cap)}</td> |
|
|
<td> |
|
|
<button class="btn-ghost" onclick="app.viewCoinDetails('${coin.symbol || coin.name}')"> |
|
|
<i class="fas fa-chart-line"></i> |
|
|
</button> |
|
|
</td> |
|
|
</tr> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
renderCoinsGrid(coins) { |
|
|
const coinsGrid = document.getElementById('coins-grid-compact'); |
|
|
if (!coinsGrid) return; |
|
|
|
|
|
if (!coins || coins.length === 0) { |
|
|
coinsGrid.innerHTML = '<div class="coin-card-compact"><p>No data available</p></div>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const topCoins = coins.slice(0, 12); |
|
|
|
|
|
|
|
|
const coinIcons = { |
|
|
'BTC': 'βΏ', |
|
|
'ETH': 'Ξ', |
|
|
'BNB': 'BNB', |
|
|
'SOL': 'β', |
|
|
'ADA': 'β³', |
|
|
'XRP': 'β', |
|
|
'DOT': 'β', |
|
|
'DOGE': 'Γ', |
|
|
'MATIC': 'β¬', |
|
|
'AVAX': 'β²', |
|
|
'LINK': '⬑', |
|
|
'UNI': 'π¦' |
|
|
}; |
|
|
|
|
|
coinsGrid.innerHTML = topCoins.map((coin) => { |
|
|
const symbol = (coin.symbol || '').toUpperCase(); |
|
|
const change = coin.change_24h || coin.price_change_percentage_24h || 0; |
|
|
const changeClass = Utils.getChangeClass(change); |
|
|
const icon = coinIcons[symbol] || symbol.charAt(0); |
|
|
|
|
|
return ` |
|
|
<div class="coin-card-compact" onclick="app.viewCoinDetails('${symbol}')"> |
|
|
<div class="coin-icon-compact">${icon}</div> |
|
|
<div class="coin-symbol-compact">${symbol}</div> |
|
|
<div class="coin-price-compact">${Utils.formatCurrency(coin.price || coin.current_price)}</div> |
|
|
<div class="coin-change-compact ${changeClass}"> |
|
|
${change >= 0 ? ` |
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<polyline points="18 15 12 9 6 15"></polyline> |
|
|
</svg> |
|
|
` : ` |
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> |
|
|
<polyline points="6 9 12 15 18 9"></polyline> |
|
|
</svg> |
|
|
`} |
|
|
<span>${Utils.formatPercent(change)}</span> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
} |
|
|
|
|
|
renderSentiment(data) { |
|
|
if (!data) return; |
|
|
|
|
|
const bullish = data.bullish || 0; |
|
|
const neutral = data.neutral || 0; |
|
|
const bearish = data.bearish || 0; |
|
|
|
|
|
const bullishPercent = document.getElementById('bullish-percent'); |
|
|
const neutralPercent = document.getElementById('neutral-percent'); |
|
|
const bearishPercent = document.getElementById('bearish-percent'); |
|
|
|
|
|
if (bullishPercent) bullishPercent.textContent = `${bullish}%`; |
|
|
if (neutralPercent) neutralPercent.textContent = `${neutral}%`; |
|
|
if (bearishPercent) bearishPercent.textContent = `${bearish}%`; |
|
|
|
|
|
|
|
|
const progressBars = document.querySelectorAll('.sentiment-progress-bar'); |
|
|
progressBars.forEach(bar => { |
|
|
if (bar.classList.contains('bullish')) { |
|
|
bar.style.width = `${bullish}%`; |
|
|
} else if (bar.classList.contains('neutral')) { |
|
|
bar.style.width = `${neutral}%`; |
|
|
} else if (bar.classList.contains('bearish')) { |
|
|
bar.style.width = `${bearish}%`; |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
renderNews(news) { |
|
|
const newsGrid = document.getElementById('news-grid'); |
|
|
if (!newsGrid) return; |
|
|
|
|
|
if (!news || news.length === 0) { |
|
|
newsGrid.innerHTML = '<p>No news available</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
newsGrid.innerHTML = news.map(item => ` |
|
|
<div class="news-card"> |
|
|
${item.image ? `<img src="${item.image}" alt="${item.title}" class="news-card-image">` : ''} |
|
|
<div class="news-card-content"> |
|
|
<h3 class="news-card-title">${item.title}</h3> |
|
|
<div class="news-card-meta"> |
|
|
<span><i class="fas fa-clock"></i> ${Utils.formatDate(item.published_at || item.published_on)}</span> |
|
|
<span><i class="fas fa-newspaper"></i> ${item.source || 'Unknown'}</span> |
|
|
</div> |
|
|
<p class="news-card-excerpt">${item.description || item.body || item.summary || ''}</p> |
|
|
${item.url ? `<a href="${item.url}" target="_blank" class="btn-ghost">Read More</a>` : ''} |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
renderProviders(providers) { |
|
|
const providersGrid = document.getElementById('providers-grid'); |
|
|
if (!providersGrid) return; |
|
|
|
|
|
if (!providers || providers.length === 0) { |
|
|
providersGrid.innerHTML = '<p>No providers available</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
providersGrid.innerHTML = providers.map(provider => ` |
|
|
<div class="provider-card"> |
|
|
<div class="provider-header"> |
|
|
<h3>${provider.name || provider.provider_id}</h3> |
|
|
<span class="status-badge ${provider.status || 'unknown'}">${provider.status || 'Unknown'}</span> |
|
|
</div> |
|
|
<div class="provider-info"> |
|
|
<p><strong>Category:</strong> ${provider.category || 'N/A'}</p> |
|
|
${provider.latency_ms ? `<p><strong>Latency:</strong> ${provider.latency_ms}ms</p>` : ''} |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
} |
|
|
|
|
|
handleMarketUpdate(data) { |
|
|
if (data.data) { |
|
|
this.renderMarketStats(data.data); |
|
|
if (data.data.cryptocurrencies || data.data.coins) { |
|
|
this.renderCoinsTable(data.data.cryptocurrencies || data.data.coins); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
handleSentimentUpdate(data) { |
|
|
if (data.data) { |
|
|
this.renderSentiment(data.data); |
|
|
} |
|
|
} |
|
|
|
|
|
updateOnlineUsers(count) { |
|
|
const activeUsersCount = document.getElementById('active-users-count'); |
|
|
if (activeUsersCount) { |
|
|
activeUsersCount.textContent = count; |
|
|
} |
|
|
} |
|
|
|
|
|
handleViewChange(view) { |
|
|
console.log('[App] View changed to:', view); |
|
|
|
|
|
|
|
|
switch (view) { |
|
|
case 'providers': |
|
|
this.loadProviders(); |
|
|
break; |
|
|
case 'news': |
|
|
this.loadNewsData(); |
|
|
break; |
|
|
case 'market': |
|
|
this.loadMarketData(); |
|
|
break; |
|
|
} |
|
|
} |
|
|
|
|
|
startPeriodicUpdates() { |
|
|
this.updateInterval = setInterval(() => { |
|
|
if (CONFIG.DEBUG?.ENABLE_CONSOLE_LOGS) { |
|
|
console.log('[App] Periodic update triggered'); |
|
|
} |
|
|
this.loadMarketData(); |
|
|
this.loadSentimentData(); |
|
|
}, CONFIG.UPDATE_INTERVAL || 30000); |
|
|
} |
|
|
|
|
|
stopPeriodicUpdates() { |
|
|
if (this.updateInterval) { |
|
|
clearInterval(this.updateInterval); |
|
|
this.updateInterval = null; |
|
|
} |
|
|
} |
|
|
|
|
|
toggleTheme() { |
|
|
document.body.classList.toggle('light-theme'); |
|
|
const icon = document.querySelector('#theme-toggle i'); |
|
|
if (icon) { |
|
|
icon.classList.toggle('fa-moon'); |
|
|
icon.classList.toggle('fa-sun'); |
|
|
} |
|
|
} |
|
|
|
|
|
handleSearch(query) { |
|
|
console.log('[App] Search query:', query); |
|
|
|
|
|
} |
|
|
|
|
|
filterMarket(filter) { |
|
|
console.log('[App] Filter market:', filter); |
|
|
|
|
|
} |
|
|
|
|
|
viewCoinDetails(symbol) { |
|
|
console.log('[App] View coin details:', symbol); |
|
|
|
|
|
this.viewManager.switchView('charts'); |
|
|
} |
|
|
|
|
|
showLoadingOverlay(show) { |
|
|
const overlay = document.getElementById('loading-overlay'); |
|
|
if (overlay) { |
|
|
if (show) { |
|
|
overlay.classList.add('active'); |
|
|
} else { |
|
|
overlay.classList.remove('active'); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async runSentimentAnalysis() { |
|
|
const aiResults = document.getElementById('ai-results'); |
|
|
const aiResultsContent = document.getElementById('ai-results-content'); |
|
|
|
|
|
if (!aiResults || !aiResultsContent) return; |
|
|
|
|
|
aiResults.style.display = 'block'; |
|
|
aiResultsContent.innerHTML = '<div class="loader"></div> Analyzing...'; |
|
|
|
|
|
try { |
|
|
const data = await this.api.getSentiment(); |
|
|
|
|
|
aiResultsContent.innerHTML = ` |
|
|
<div class="ai-result-card"> |
|
|
<h4>Sentiment Analysis Results</h4> |
|
|
<div class="sentiment-summary"> |
|
|
<div class="sentiment-summary-item"> |
|
|
<div class="summary-label">Bullish</div> |
|
|
<div class="summary-value bullish">${data.bullish || 0}%</div> |
|
|
</div> |
|
|
<div class="sentiment-summary-item"> |
|
|
<div class="summary-label">Neutral</div> |
|
|
<div class="summary-value neutral">${data.neutral || 0}%</div> |
|
|
</div> |
|
|
<div class="sentiment-summary-item"> |
|
|
<div class="summary-label">Bearish</div> |
|
|
<div class="summary-value bearish">${data.bearish || 0}%</div> |
|
|
</div> |
|
|
</div> |
|
|
<p style="margin-top: 1rem; color: var(--text-muted);"> |
|
|
${data.summary || 'Market sentiment analysis based on aggregated data from multiple sources'} |
|
|
</p> |
|
|
</div> |
|
|
`; |
|
|
} catch (error) { |
|
|
aiResultsContent.innerHTML = ` |
|
|
<div class="error-message"> |
|
|
<i class="fas fa-exclamation-circle"></i> |
|
|
Error in analysis: ${error.message} |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
async runNewsSummary() { |
|
|
const aiResults = document.getElementById('ai-results'); |
|
|
const aiResultsContent = document.getElementById('ai-results-content'); |
|
|
|
|
|
if (!aiResults || !aiResultsContent) return; |
|
|
|
|
|
aiResults.style.display = 'block'; |
|
|
aiResultsContent.innerHTML = '<div class="loader"></div> Summarizing...'; |
|
|
|
|
|
setTimeout(() => { |
|
|
aiResultsContent.innerHTML = ` |
|
|
<div class="ai-result-card"> |
|
|
<h4>News Summary</h4> |
|
|
<p>News summarization feature will be available soon.</p> |
|
|
<p style="color: var(--text-muted); font-size: 0.875rem;"> |
|
|
This feature uses Hugging Face models for text summarization. |
|
|
</p> |
|
|
</div> |
|
|
`; |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
async runPricePrediction() { |
|
|
const aiResults = document.getElementById('ai-results'); |
|
|
const aiResultsContent = document.getElementById('ai-results-content'); |
|
|
|
|
|
if (!aiResults || !aiResultsContent) return; |
|
|
|
|
|
aiResults.style.display = 'block'; |
|
|
aiResultsContent.innerHTML = '<div class="loader"></div> Predicting...'; |
|
|
|
|
|
setTimeout(() => { |
|
|
aiResultsContent.innerHTML = ` |
|
|
<div class="ai-result-card"> |
|
|
<h4>Price Prediction</h4> |
|
|
<p>Price prediction feature will be available soon.</p> |
|
|
<p style="color: var(--text-muted); font-size: 0.875rem;"> |
|
|
This feature uses machine learning models to predict price trends. |
|
|
</p> |
|
|
</div> |
|
|
`; |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
async runPatternDetection() { |
|
|
const aiResults = document.getElementById('ai-results'); |
|
|
const aiResultsContent = document.getElementById('ai-results-content'); |
|
|
|
|
|
if (!aiResults || !aiResultsContent) return; |
|
|
|
|
|
aiResults.style.display = 'block'; |
|
|
aiResultsContent.innerHTML = '<div class="loader"></div> Detecting patterns...'; |
|
|
|
|
|
setTimeout(() => { |
|
|
aiResultsContent.innerHTML = ` |
|
|
<div class="ai-result-card"> |
|
|
<h4>Pattern Detection</h4> |
|
|
<p>Pattern detection feature will be available soon.</p> |
|
|
<p style="color: var(--text-muted); font-size: 0.875rem;"> |
|
|
This feature detects candlestick patterns and technical analysis indicators. |
|
|
</p> |
|
|
</div> |
|
|
`; |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
destroy() { |
|
|
this.stopPeriodicUpdates(); |
|
|
this.ws.disconnect(); |
|
|
console.log('[App] Dashboard destroyed'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let app; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
console.log('[Main] DOM loaded, initializing application...'); |
|
|
|
|
|
app = new DashboardApp(); |
|
|
app.init(); |
|
|
|
|
|
|
|
|
window.app = app; |
|
|
|
|
|
console.log('[Main] Application ready'); |
|
|
}); |
|
|
|
|
|
|
|
|
window.addEventListener('beforeunload', () => { |
|
|
if (app) { |
|
|
app.destroy(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.addEventListener('visibilitychange', () => { |
|
|
if (document.hidden) { |
|
|
console.log('[Main] Page hidden, pausing updates'); |
|
|
if (app) app.stopPeriodicUpdates(); |
|
|
} else { |
|
|
console.log('[Main] Page visible, resuming updates'); |
|
|
if (app) { |
|
|
app.startPeriodicUpdates(); |
|
|
app.loadMarketData(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
export { DashboardApp, APIClient, WebSocketClient, Utils }; |
|
|
|