345 lines
9.0 KiB
HTML
345 lines
9.0 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>{{.Title}}</title>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="news-container">
|
|
<div class="news-header">
|
|
<h1>📰 Live News Feed</h1>
|
|
<div class="news-stats">
|
|
<span id="news-count">0 articles</span>
|
|
<span id="last-update">Last update: never</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="news-filters">
|
|
<button class="filter-btn active" data-filter="all">All</button>
|
|
<button class="filter-btn" data-filter="positive">Positive</button>
|
|
<button class="filter-btn" data-filter="neutral">Neutral</button>
|
|
<button class="filter-btn" data-filter="negative">Negative</button>
|
|
</div>
|
|
|
|
<div id="news-list" class="news-list">
|
|
<p class="loading">Loading news...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script src="/static/app.js"></script>
|
|
|
|
<style>
|
|
.news-container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.news-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 2px solid #333;
|
|
}
|
|
|
|
.news-stats {
|
|
display: flex;
|
|
gap: 20px;
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
}
|
|
|
|
.news-filters {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.filter-btn {
|
|
padding: 8px 16px;
|
|
border: 2px solid #ddd;
|
|
background: white;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.filter-btn:hover {
|
|
background: #f0f0f0;
|
|
}
|
|
|
|
.filter-btn.active {
|
|
background: #007bff;
|
|
color: white;
|
|
border-color: #007bff;
|
|
}
|
|
|
|
.news-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.news-article {
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #ccc;
|
|
background: white;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.news-article:hover {
|
|
transform: translateX(5px);
|
|
}
|
|
|
|
.news-article.positive {
|
|
border-left-color: #28a745;
|
|
background: #f0fff4;
|
|
}
|
|
|
|
.news-article.negative {
|
|
border-left-color: #dc3545;
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.news-article.neutral {
|
|
border-left-color: #6c757d;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.news-article-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: start;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.news-title {
|
|
font-size: 1.1em;
|
|
font-weight: bold;
|
|
margin: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.news-title a {
|
|
color: #333;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.news-title a:hover {
|
|
color: #007bff;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.news-sentiment {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-left: 15px;
|
|
}
|
|
|
|
.sentiment-badge {
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 0.85em;
|
|
font-weight: bold;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.sentiment-badge.positive {
|
|
background: #28a745;
|
|
color: white;
|
|
}
|
|
|
|
.sentiment-badge.negative {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.sentiment-badge.neutral {
|
|
background: #6c757d;
|
|
color: white;
|
|
}
|
|
|
|
.sentiment-score {
|
|
font-size: 0.9em;
|
|
color: #666;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.news-meta {
|
|
display: flex;
|
|
gap: 15px;
|
|
font-size: 0.85em;
|
|
color: #666;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.news-source {
|
|
font-weight: bold;
|
|
color: #007bff;
|
|
}
|
|
|
|
.news-time {
|
|
color: #999;
|
|
}
|
|
|
|
.news-content {
|
|
color: #555;
|
|
line-height: 1.5;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.news-symbols {
|
|
margin-top: 10px;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.symbol-tag {
|
|
padding: 3px 8px;
|
|
background: #e9ecef;
|
|
border-radius: 4px;
|
|
font-size: 0.85em;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
}
|
|
|
|
.news-method {
|
|
font-size: 0.8em;
|
|
color: #999;
|
|
font-style: italic;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #999;
|
|
}
|
|
|
|
.no-news {
|
|
text-align: center;
|
|
padding: 40px;
|
|
color: #999;
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let currentFilter = 'all';
|
|
let allNews = [];
|
|
|
|
function formatTimeAgo(dateString) {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const seconds = Math.floor((now - date) / 1000);
|
|
|
|
if (seconds < 60) return 'just now';
|
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
|
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
|
|
return Math.floor(seconds / 86400) + 'd ago';
|
|
}
|
|
|
|
function formatScore(score) {
|
|
if (score === null || score === undefined) return '';
|
|
return (score > 0 ? '+' : '') + score.toFixed(2);
|
|
}
|
|
|
|
function getSentimentLabel(article) {
|
|
return article.SentimentLabel || 'neutral';
|
|
}
|
|
|
|
function renderNews(articles) {
|
|
const container = document.getElementById('news-list');
|
|
|
|
if (!articles || articles.length === 0) {
|
|
container.innerHTML = '<p class="no-news">No news articles available</p>';
|
|
return;
|
|
}
|
|
|
|
const filtered = currentFilter === 'all'
|
|
? articles
|
|
: articles.filter(a => getSentimentLabel(a) === currentFilter);
|
|
|
|
if (filtered.length === 0) {
|
|
container.innerHTML = '<p class="no-news">No ' + currentFilter + ' articles</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = filtered.map(article => {
|
|
const sentiment = getSentimentLabel(article);
|
|
const score = article.SentimentScore;
|
|
const symbols = article.Symbols ? article.Symbols.split(',').filter(s => s.trim()) : [];
|
|
const method = article.SentimentMethod || 'keyword';
|
|
|
|
return `
|
|
<div class="news-article ${sentiment}">
|
|
<div class="news-article-header">
|
|
<h3 class="news-title">
|
|
<a href="${article.URL}" target="_blank" rel="noopener">${article.Title}</a>
|
|
</h3>
|
|
<div class="news-sentiment">
|
|
${score !== null && score !== undefined ?
|
|
`<span class="sentiment-score">${formatScore(score)}</span>` : ''}
|
|
<span class="sentiment-badge ${sentiment}">${sentiment}</span>
|
|
</div>
|
|
</div>
|
|
<div class="news-meta">
|
|
<span class="news-source">${article.Source}</span>
|
|
<span class="news-time">${formatTimeAgo(article.PublishedAt)}</span>
|
|
</div>
|
|
${article.Content ? `<div class="news-content">${article.Content}</div>` : ''}
|
|
${symbols.length > 0 ? `
|
|
<div class="news-symbols">
|
|
${symbols.map(s => `<span class="symbol-tag">${s.trim()}</span>`).join('')}
|
|
</div>
|
|
` : ''}
|
|
<div class="news-method">Analysis: ${method}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// Update stats
|
|
document.getElementById('news-count').textContent = filtered.length + ' article' + (filtered.length !== 1 ? 's' : '');
|
|
document.getElementById('last-update').textContent = 'Last update: ' + new Date().toLocaleTimeString();
|
|
}
|
|
|
|
function loadNews() {
|
|
fetch('/api/news/recent?limit=50')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
allNews = data || [];
|
|
renderNews(allNews);
|
|
})
|
|
.catch(error => {
|
|
console.error('Failed to load news:', error);
|
|
document.getElementById('news-list').innerHTML =
|
|
'<p class="no-news">Failed to load news. Please try again later.</p>';
|
|
});
|
|
}
|
|
|
|
// Filter buttons
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
btn.addEventListener('click', function() {
|
|
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
|
this.classList.add('active');
|
|
currentFilter = this.dataset.filter;
|
|
renderNews(allNews);
|
|
});
|
|
});
|
|
|
|
// Initial load
|
|
loadNews();
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(loadNews, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|