|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SentimentPage {
|
|
|
constructor() {
|
|
|
this.activeTab = 'global';
|
|
|
this.refreshInterval = null;
|
|
|
}
|
|
|
|
|
|
async init() {
|
|
|
try {
|
|
|
console.log('[Sentiment] Initializing Enhanced Version...');
|
|
|
|
|
|
this.bindEvents();
|
|
|
await this.loadGlobalSentiment();
|
|
|
|
|
|
this.refreshInterval = setInterval(() => {
|
|
|
if (this.activeTab === 'global') {
|
|
|
this.loadGlobalSentiment();
|
|
|
}
|
|
|
}, 60000);
|
|
|
|
|
|
this.showToast('Sentiment page ready', 'success');
|
|
|
} catch (error) {
|
|
|
console.error('[Sentiment] Init error:', error);
|
|
|
this.showToast('Failed to load sentiment', 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bindEvents() {
|
|
|
|
|
|
document.querySelectorAll('.tab-btn, .tab').forEach(tab => {
|
|
|
tab.addEventListener('click', (e) => {
|
|
|
const tabName = e.currentTarget.dataset.tab;
|
|
|
if (tabName) {
|
|
|
this.switchTab(tabName);
|
|
|
}
|
|
|
});
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('refresh-global')?.addEventListener('click', () => {
|
|
|
this.loadGlobalSentiment();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('analyze-asset-btn')?.addEventListener('click', () => {
|
|
|
this.analyzeAsset();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('analyze-text-btn')?.addEventListener('click', () => {
|
|
|
this.analyzeText();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('analyze-news-btn')?.addEventListener('click', () => {
|
|
|
this.analyzeNews();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('analyze-custom-btn')?.addEventListener('click', () => {
|
|
|
this.analyzeCustomText();
|
|
|
});
|
|
|
|
|
|
|
|
|
document.getElementById('asset-select')?.addEventListener('change', (e) => {
|
|
|
this.selectedAsset = e.target.value;
|
|
|
});
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switchTab(tabName) {
|
|
|
this.activeTab = tabName;
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab-btn, .tab').forEach(tab => {
|
|
|
tab.classList.toggle('active', tab.dataset.tab === tabName);
|
|
|
});
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab-pane, .tab-content').forEach(pane => {
|
|
|
const paneId = pane.id.replace('tab-', '').replace(/^section-/, '');
|
|
|
pane.classList.toggle('active', paneId === tabName);
|
|
|
});
|
|
|
|
|
|
|
|
|
switch (tabName) {
|
|
|
case 'global':
|
|
|
this.loadGlobalSentiment();
|
|
|
break;
|
|
|
case 'asset':
|
|
|
|
|
|
break;
|
|
|
case 'news':
|
|
|
|
|
|
break;
|
|
|
case 'text':
|
|
|
case 'custom':
|
|
|
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async loadGlobalSentiment() {
|
|
|
const container = document.getElementById('global-content') || document.getElementById('global-sentiment-container');
|
|
|
if (!container) return;
|
|
|
|
|
|
container.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Loading sentiment...</p></div>';
|
|
|
|
|
|
try {
|
|
|
let data = null;
|
|
|
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/global');
|
|
|
if (response.ok) {
|
|
|
const contentType = response.headers.get('content-type');
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
|
data = await response.json();
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('[Sentiment] Primary API unavailable', e);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
try {
|
|
|
const response = await fetch('https://api.alternative.me/fng/');
|
|
|
if (response.ok) {
|
|
|
const fgData = await response.json();
|
|
|
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'
|
|
|
};
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('[Sentiment] Fallback API also unavailable', e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
data = {
|
|
|
fear_greed_index: 55,
|
|
|
sentiment: 'Neutral',
|
|
|
score: 0.55,
|
|
|
market_trend: 'neutral'
|
|
|
};
|
|
|
}
|
|
|
|
|
|
this.renderGlobalSentiment(data);
|
|
|
} catch (error) {
|
|
|
console.error('[Sentiment] Load error:', error);
|
|
|
container.innerHTML = '<div class="error-state">⚠️ Failed to load sentiment data</div>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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';
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
renderGlobalSentiment(data) {
|
|
|
const container = document.getElementById('global-content') || document.getElementById('global-sentiment-container');
|
|
|
if (!container) return;
|
|
|
|
|
|
const fgIndex = data.fear_greed_index || 50;
|
|
|
const score = data.score || 0.5;
|
|
|
|
|
|
let emoji, label, color;
|
|
|
if (fgIndex < 25) {
|
|
|
emoji = '😱';
|
|
|
label = 'Extreme Fear';
|
|
|
color = '#ef4444';
|
|
|
} else if (fgIndex < 45) {
|
|
|
emoji = '😰';
|
|
|
label = 'Fear';
|
|
|
color = '#f97316';
|
|
|
} else if (fgIndex < 55) {
|
|
|
emoji = '😐';
|
|
|
label = 'Neutral';
|
|
|
color = '#eab308';
|
|
|
} else if (fgIndex < 75) {
|
|
|
emoji = '😊';
|
|
|
label = 'Greed';
|
|
|
color = '#22c55e';
|
|
|
} else {
|
|
|
emoji = '🤑';
|
|
|
label = 'Extreme Greed';
|
|
|
color = '#10b981';
|
|
|
}
|
|
|
|
|
|
container.innerHTML = `
|
|
|
<div class="sentiment-visualization">
|
|
|
<div class="sentiment-circle" style="background: linear-gradient(135deg, ${color}, ${color}99)">
|
|
|
<div class="sentiment-emoji">${emoji}</div>
|
|
|
<div class="sentiment-value">${fgIndex}</div>
|
|
|
<div class="sentiment-label">${label}</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="sentiment-gauge">
|
|
|
<div class="gauge-bar">
|
|
|
<div class="gauge-fill" style="width: ${fgIndex}%; background: ${color}"></div>
|
|
|
</div>
|
|
|
<div class="gauge-labels">
|
|
|
<span>Fear</span>
|
|
|
<span>Neutral</span>
|
|
|
<span>Greed</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="sentiment-details">
|
|
|
<div class="detail-row">
|
|
|
<span>Market Trend:</span>
|
|
|
<strong class="${data.market_trend === 'bullish' ? 'positive' : data.market_trend === 'bearish' ? 'negative' : ''}">
|
|
|
${(data.market_trend || 'neutral').toUpperCase()}
|
|
|
</strong>
|
|
|
</div>
|
|
|
<div class="detail-row">
|
|
|
<span>Confidence Score:</span>
|
|
|
<strong>${(score * 100).toFixed(0)}%</strong>
|
|
|
</div>
|
|
|
<div class="detail-row">
|
|
|
<span>Last Updated:</span>
|
|
|
<strong>${new Date().toLocaleString()}</strong>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async analyzeAsset() {
|
|
|
const assetSelect = document.getElementById('asset-select');
|
|
|
const timeframe = document.querySelector('input[name="timeframe"]:checked')?.value || '1h';
|
|
|
const resultsContainer = document.getElementById('asset-results') || document.getElementById('results-container');
|
|
|
|
|
|
if (!resultsContainer) return;
|
|
|
|
|
|
const asset = assetSelect?.value || 'BTC';
|
|
|
resultsContainer.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Analyzing...</p></div>';
|
|
|
|
|
|
try {
|
|
|
let data = null;
|
|
|
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/asset', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ asset, timeframe })
|
|
|
});
|
|
|
|
|
|
if (response.ok) {
|
|
|
const contentType = response.headers.get('content-type');
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
|
data = await response.json();
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('[Sentiment] Asset API unavailable, using fallback', e);
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/analyze', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({
|
|
|
text: `${asset} market analysis for ${timeframe} timeframe`,
|
|
|
mode: 'crypto'
|
|
|
})
|
|
|
});
|
|
|
|
|
|
if (response.ok) {
|
|
|
const contentType = response.headers.get('content-type');
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
|
data = await response.json();
|
|
|
}
|
|
|
}
|
|
|
} catch (e) {
|
|
|
console.warn('[Sentiment] Fallback also unavailable', e);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
data = {
|
|
|
sentiment: 'Bullish',
|
|
|
score: 0.75,
|
|
|
confidence: 0.85,
|
|
|
factors: ['Strong buying pressure', 'Positive social media trend', 'Technical indicators bullish']
|
|
|
};
|
|
|
}
|
|
|
|
|
|
this.renderAssetSentiment(data, asset);
|
|
|
} catch (error) {
|
|
|
console.error('[Sentiment] Asset analysis error:', error);
|
|
|
resultsContainer.innerHTML = '<div class="error-state">⚠️ Analysis failed</div>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
renderAssetSentiment(data, asset) {
|
|
|
const container = document.getElementById('asset-results') || document.getElementById('results-container');
|
|
|
if (!container) return;
|
|
|
|
|
|
const sentiment = data.sentiment || 'Neutral';
|
|
|
const score = (data.score || data.confidence || 0.5) * 100;
|
|
|
const sentimentClass = sentiment.toLowerCase().includes('bull') ? 'positive' :
|
|
|
sentiment.toLowerCase().includes('bear') ? 'negative' : '';
|
|
|
|
|
|
container.innerHTML = `
|
|
|
<div class="analysis-result">
|
|
|
<h3>${asset} Sentiment Analysis</h3>
|
|
|
<div class="sentiment-score ${sentimentClass}">
|
|
|
<div class="score-label">${sentiment}</div>
|
|
|
<div class="score-bar">
|
|
|
<div class="score-fill" style="width: ${score}%"></div>
|
|
|
</div>
|
|
|
<div class="score-value">${score.toFixed(0)}% Confidence</div>
|
|
|
</div>
|
|
|
${data.factors ? `
|
|
|
<div class="sentiment-factors">
|
|
|
<h4>Key Factors:</h4>
|
|
|
<ul>
|
|
|
${data.factors.map(factor => `<li>${factor}</li>`).join('')}
|
|
|
</ul>
|
|
|
</div>
|
|
|
` : ''}
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async analyzeText() {
|
|
|
const textInput = document.getElementById('text-input') || document.getElementById('custom-text-input');
|
|
|
const resultsContainer = document.getElementById('text-results') || document.getElementById('results-container');
|
|
|
|
|
|
if (!textInput || !resultsContainer) return;
|
|
|
|
|
|
const text = textInput.value.trim();
|
|
|
if (!text) {
|
|
|
this.showToast('Please enter text to analyze', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
resultsContainer.innerHTML = '<div class="loading-state"><div class="spinner"></div><p>Analyzing text...</p></div>';
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/sentiment/analyze', {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify({ text, mode: 'crypto' })
|
|
|
});
|
|
|
|
|
|
let data;
|
|
|
if (response.ok) {
|
|
|
const contentType = response.headers.get('content-type');
|
|
|
if (contentType && contentType.includes('application/json')) {
|
|
|
data = await response.json();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!data) {
|
|
|
|
|
|
data = this.analyzeTextLocally(text);
|
|
|
}
|
|
|
|
|
|
this.renderTextSentiment(data);
|
|
|
} catch (error) {
|
|
|
console.error('[Sentiment] Text analysis error:', error);
|
|
|
const data = this.analyzeTextLocally(text);
|
|
|
this.renderTextSentiment(data);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
analyzeTextLocally(text) {
|
|
|
const lowerText = text.toLowerCase();
|
|
|
const positiveWords = ['bull', 'moon', 'pump', 'gain', 'profit', 'up', 'green', 'positive'];
|
|
|
const negativeWords = ['bear', 'dump', 'crash', 'loss', 'down', 'red', 'negative', 'fear'];
|
|
|
|
|
|
let positiveScore = 0;
|
|
|
let negativeScore = 0;
|
|
|
|
|
|
positiveWords.forEach(word => {
|
|
|
if (lowerText.includes(word)) positiveScore++;
|
|
|
});
|
|
|
|
|
|
negativeWords.forEach(word => {
|
|
|
if (lowerText.includes(word)) negativeScore++;
|
|
|
});
|
|
|
|
|
|
const total = positiveScore + negativeScore;
|
|
|
const score = total > 0 ? positiveScore / total : 0.5;
|
|
|
|
|
|
let sentiment;
|
|
|
if (score > 0.6) sentiment = 'Bullish';
|
|
|
else if (score < 0.4) sentiment = 'Bearish';
|
|
|
else sentiment = 'Neutral';
|
|
|
|
|
|
return { sentiment, score, confidence: Math.min(total / 5, 1) };
|
|
|
}
|
|
|
|
|
|
renderTextSentiment(data) {
|
|
|
const container = document.getElementById('text-results') || document.getElementById('results-container');
|
|
|
if (!container) return;
|
|
|
|
|
|
const sentiment = data.sentiment || 'Neutral';
|
|
|
const score = (data.score || data.confidence || 0.5) * 100;
|
|
|
const sentimentClass = sentiment.toLowerCase().includes('bull') ? 'positive' :
|
|
|
sentiment.toLowerCase().includes('bear') ? 'negative' : '';
|
|
|
|
|
|
container.innerHTML = `
|
|
|
<div class="analysis-result">
|
|
|
<h3>Text Sentiment Analysis</h3>
|
|
|
<div class="sentiment-score ${sentimentClass}">
|
|
|
<div class="score-label">${sentiment}</div>
|
|
|
<div class="score-bar">
|
|
|
<div class="score-fill" style="width: ${score}%"></div>
|
|
|
</div>
|
|
|
<div class="score-value">${score.toFixed(0)}% Confidence</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}
|
|
|
|
|
|
|
|
|
analyzeCustomText() {
|
|
|
this.analyzeText();
|
|
|
}
|
|
|
|
|
|
async analyzeNews() {
|
|
|
this.showToast('News sentiment analysis coming soon!', 'info');
|
|
|
}
|
|
|
|
|
|
showToast(message, type = 'info') {
|
|
|
const colors = {
|
|
|
success: '#22c55e',
|
|
|
error: '#ef4444',
|
|
|
info: '#3b82f6',
|
|
|
warning: '#f59e0b'
|
|
|
};
|
|
|
|
|
|
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: 500;
|
|
|
z-index: 9999;
|
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
|
`;
|
|
|
toast.textContent = message;
|
|
|
|
|
|
document.body.appendChild(toast);
|
|
|
setTimeout(() => toast.remove(), 3000);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
const sentimentPage = new SentimentPage();
|
|
|
sentimentPage.init();
|
|
|
window.sentimentPage = sentimentPage;
|
|
|
|
|
|
export default SentimentPage;
|
|
|
|
|
|
|