sentiment-analysis / templates /dashboard.html
xtinkarpiu's picture
Upload folder using huggingface_hub
e18a159 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Real-Time Sentiment Analysis Dashboard</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
color: white;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 15px;
padding: 25px;
text-align: center;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-number {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 10px;
}
.stat-label {
font-size: 1rem;
opacity: 0.8;
text-transform: uppercase;
letter-spacing: 1px;
}
.positive { color: #4ade80; }
.negative { color: #f87171; }
.neutral { color: #60a5fa; }
.total { color: #fbbf24; }
.charts-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 30px;
}
.chart-container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 15px;
padding: 25px;
}
.chart-title {
font-size: 1.3rem;
margin-bottom: 20px;
text-align: center;
}
.tweets-section {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 15px;
padding: 25px;
}
.section-title {
font-size: 1.5rem;
margin-bottom: 20px;
text-align: center;
}
.tweets-container {
max-height: 400px;
overflow-y: auto;
}
.tweet-item {
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
padding: 15px;
margin-bottom: 10px;
border-left: 4px solid;
transition: all 0.3s ease;
}
.tweet-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.tweet-item.positive { border-left-color: #4ade80; }
.tweet-item.negative { border-left-color: #f87171; }
.tweet-item.neutral { border-left-color: #60a5fa; }
.tweet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.tweet-sentiment {
font-size: 0.8rem;
padding: 4px 8px;
border-radius: 12px;
font-weight: bold;
text-transform: uppercase;
}
.tweet-sentiment.positive { background: #4ade80; color: #000; }
.tweet-sentiment.negative { background: #f87171; color: #000; }
.tweet-sentiment.neutral { background: #60a5fa; color: #000; }
.tweet-time {
font-size: 0.8rem;
opacity: 0.7;
}
.tweet-text {
font-size: 0.9rem;
line-height: 1.4;
}
.status-indicator {
position: fixed;
top: 20px;
right: 20px;
padding: 10px 15px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
}
.status-connected {
background: #4ade80;
color: #000;
}
.status-disconnected {
background: #f87171;
color: #000;
}
@media (max-width: 768px) {
.charts-section {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.header h1 {
font-size: 2rem;
}
}
/* Custom scrollbar */
.tweets-container::-webkit-scrollbar {
width: 8px;
}
.tweets-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
}
.tweets-container::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
.tweets-container::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body>
<div class="status-indicator" id="status">Connecting...</div>
<div class="container">
<div class="header">
<h1>🚀 Real-Time Sentiment Analysis</h1>
<p>Live Tweet Processing with Apache Kafka & Apache Spark</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<div class="stat-number positive" id="positive-count">0</div>
<div class="stat-label">Positive Tweets</div>
</div>
<div class="stat-card">
<div class="stat-number negative" id="negative-count">0</div>
<div class="stat-label">Negative Tweets</div>
</div>
<div class="stat-card">
<div class="stat-number neutral" id="neutral-count">0</div>
<div class="stat-label">Neutral Tweets</div>
</div>
<div class="stat-card">
<div class="stat-number total" id="total-count">0</div>
<div class="stat-label">Total Processed</div>
</div>
</div>
<div class="charts-section">
<div class="chart-container">
<h3 class="chart-title">Sentiment Distribution</h3>
<canvas id="sentiment-pie-chart"></canvas>
</div>
<div class="chart-container">
<h3 class="chart-title">Hourly Sentiment Trend</h3>
<canvas id="hourly-chart"></canvas>
</div>
</div>
<div class="tweets-section">
<h3 class="section-title">📱 Recent Tweets</h3>
<div class="tweets-container" id="tweets-container">
<div style="text-align: center; opacity: 0.7; padding: 20px;">
Waiting for tweets...
</div>
</div>
</div>
</div>
<script>
// Initialize Socket.IO
const socket = io();
// Status indicator
const statusElement = document.getElementById('status');
// Charts
let pieChart, hourlyChart;
// Initialize charts
function initCharts() {
// Pie Chart
const pieCtx = document.getElementById('sentiment-pie-chart').getContext('2d');
pieChart = new Chart(pieCtx, {
type: 'doughnut',
data: {
labels: ['Positive', 'Negative', 'Neutral'],
datasets: [{
data: [0, 0, 0],
backgroundColor: ['#4ade80', '#f87171', '#60a5fa'],
borderWidth: 0
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#fff' }
}
}
}
});
// Hourly Chart
const hourlyCtx = document.getElementById('hourly-chart').getContext('2d');
hourlyChart = new Chart(hourlyCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: 'Positive',
data: [],
borderColor: '#4ade80',
backgroundColor: 'rgba(74, 222, 128, 0.1)',
tension: 0.4
},
{
label: 'Negative',
data: [],
borderColor: '#f87171',
backgroundColor: 'rgba(248, 113, 113, 0.1)',
tension: 0.4
},
{
label: 'Neutral',
data: [],
borderColor: '#60a5fa',
backgroundColor: 'rgba(96, 165, 250, 0.1)',
tension: 0.4
}
]
},
options: {
responsive: true,
plugins: {
legend: {
labels: { color: '#fff' }
}
},
scales: {
y: {
ticks: { color: '#fff' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }
},
x: {
ticks: { color: '#fff' },
grid: { color: 'rgba(255, 255, 255, 0.1)' }
}
}
}
});
}
// Update dashboard with new data
function updateDashboard(data) {
// Update counters
document.getElementById('positive-count').textContent = data.sentiment_counts.positive || 0;
document.getElementById('negative-count').textContent = data.sentiment_counts.negative || 0;
document.getElementById('neutral-count').textContent = data.sentiment_counts.neutral || 0;
const total = (data.sentiment_counts.positive || 0) +
(data.sentiment_counts.negative || 0) +
(data.sentiment_counts.neutral || 0);
document.getElementById('total-count').textContent = total;
// Update pie chart
pieChart.data.datasets[0].data = [
data.sentiment_counts.positive || 0,
data.sentiment_counts.negative || 0,
data.sentiment_counts.neutral || 0
];
pieChart.update();
// Update hourly chart
if (data.hourly_data) {
const hours = Object.keys(data.hourly_data).sort();
hourlyChart.data.labels = hours;
hourlyChart.data.datasets[0].data = hours.map(h => data.hourly_data[h].positive || 0);
hourlyChart.data.datasets[1].data = hours.map(h => data.hourly_data[h].negative || 0);
hourlyChart.data.datasets[2].data = hours.map(h => data.hourly_data[h].neutral || 0);
hourlyChart.update();
}
// Update recent tweets
if (data.recent_tweets && data.recent_tweets.length > 0) {
const container = document.getElementById('tweets-container');
container.innerHTML = data.recent_tweets.map(tweet => `
<div class="tweet-item ${tweet.sentiment}">
<div class="tweet-header">
<span class="tweet-sentiment ${tweet.sentiment}">${tweet.sentiment}</span>
<span class="tweet-time">${tweet.timestamp}</span>
</div>
<div class="tweet-text">${tweet.text}</div>
</div>
`).join('');
}
}
// Socket event handlers
socket.on('connect', function() {
statusElement.textContent = '🟢 Connected';
statusElement.className = 'status-indicator status-connected';
});
socket.on('disconnect', function() {
statusElement.textContent = '🔴 Disconnected';
statusElement.className = 'status-indicator status-disconnected';
});
socket.on('sentiment_update', function(data) {
updateDashboard(data);
});
// Initialize everything when page loads
document.addEventListener('DOMContentLoaded', function() {
initCharts();
// Fetch initial data
fetch('/api/data')
.then(response => response.json())
.then(data => updateDashboard(data))
.catch(error => console.error('Error fetching initial data:', error));
});
</script>
</body>
</html>