/**
* 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
Retry
`;
}
}
/**
* 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}
${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 = `
`;
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
Retry
`;
}
}
/**
* 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 = `
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
Retry
`;
}
}
/**
* 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;