alphax/static/sentiment.html
2026-05-18 08:39:38 +08:00

320 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}AlphaX Agent — 舆情雷达{% endblock %}
{% block extra_head_css %}
<style>
/* SHELL */
.shell { position: relative; z-index: 1; width: min(100% - 40px, 1180px); margin: 0 auto; padding: 24px 0 44px; }
/* Page title */
.page-title { font-size: 24px; font-weight: 800; color: var(--ink); margin-bottom: 4px; }
.page-sub { font-size: 13px; color: var(--stone); margin-bottom: 16px; }
/* === SECTION: DASHBOARD === */
.market-context { display: grid; grid-template-columns: minmax(200px, 250px) 1fr; gap: 10px; margin-bottom: 14px; }
@media(max-width:760px) { .market-context { grid-template-columns: 1fr; } }
/* Fear & Greed */
.fg-card {
background: var(--canvas); border: 1px solid var(--hairline-soft);
border-radius: var(--radius-lg); padding: 10px 12px;
display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 8px 10px;
}
.fg-card .fg-label { grid-column: 1 / -1; font-size: 11px; color: var(--stone); font-weight: 800; }
.fg-card .fg-value { font-size: 28px; font-weight: 900; line-height: 1; transition: color .3s; }
.fg-card .fg-class { justify-self: start; font-size: 12px; font-weight: 800; padding: 3px 9px; border-radius: var(--radius-full); }
.fg-gauge { grid-column: 1 / -1; width: 100%; height: 5px; border-radius: 999px; background: linear-gradient(to right, #e53e3e, #f59e0b, #84cc16); position: relative; }
.fg-gauge::after {
content: ""; position: absolute; top: -3px;
width: 11px; height: 11px; border-radius: 50%; background: var(--canvas);
border: 2px solid var(--ink); transition: left .5s;
left: calc(var(--pos, 0%) - 5px);
}
/* Trending card */
.trend-card {
background: var(--canvas); border: 1px solid var(--hairline-soft);
border-radius: var(--radius-lg); padding: 10px 12px; min-width: 0;
}
.trend-card .section-label { font-size: 11px; color: var(--stone); font-weight: 800; margin-bottom: 8px; }
.trend-list { display: flex; flex-wrap: wrap; gap: 6px; min-height: 29px; align-items: center; }
.trend-pill { display: inline-flex; align-items: center; gap: 6px; max-width: 160px; padding: 4px 8px; border: 1px solid var(--hairline-soft); border-radius: 999px; background: var(--surface); color: var(--slate); font-size: 12px; font-weight: 800; }
.trend-pill img { width: 16px; height: 16px; border-radius: 50%; flex-shrink: 0; }
.trend-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ink); }
.trend-symbol { font-size: 11px; color: var(--stone); margin-left: 4px; }
.trend-rank { font-size: 10px; color: var(--muted); }
.sentiment-grid { display:grid; grid-template-columns:minmax(0, 1.05fr) minmax(360px, .95fr); gap:14px; align-items:start; }
.panel { background: var(--canvas); border: 1px solid var(--hairline-soft); border-radius: var(--radius-lg); overflow:hidden; min-width:0; }
.panel-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; padding:14px 16px; border-bottom:1px solid var(--hairline-soft); }
.panel-title { font-size: 16px; font-weight: 900; color: var(--ink); }
.panel-sub { margin-top:3px; color:var(--stone); font-size:12px; line-height:1.45; }
.feed-count { flex-shrink:0; font-size: 12px; color: var(--muted); background: var(--surface); padding: 3px 10px; border-radius: var(--radius-full); font-weight:800; }
.source-chips { display:flex; flex-wrap:wrap; gap:6px; padding:10px 16px 0; }
.source-chip { display:inline-flex; align-items:center; gap:5px; border:1px solid var(--hairline-soft); border-radius:999px; background:var(--surface); padding:4px 8px; color:var(--slate); font-size:11px; font-weight:850; }
.source-chip b { color:var(--ink); }
.news-feed { display: flex; flex-direction: column; gap: 8px; }
.news-card { background: var(--canvas); border-top: 1px solid var(--hairline-soft); padding: 14px 16px; transition: .15s; cursor: pointer; display: flex; gap: 12px; align-items: flex-start; }
.news-card:hover { border-color: var(--hairline); box-shadow: 0 2px 8px rgba(5,0,56,.04); }
.news-card:active { transform: scale(.995); }
.news-source {
flex-shrink: 0; min-width: 56px; font-size: 10px; font-weight: 700;
color: var(--stone); padding: 4px 8px; background: var(--surface);
border-radius: var(--radius-md); text-align: center; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
.news-source.cn { color: var(--blue); background: rgba(66,98,255,.06); }
.news-body { flex: 1; min-width: 0; }
.news-title { font-size: 14px; font-weight: 750; color: var(--ink); line-height: 1.5; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 6px; }
.news-meta { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--muted); }
.news-meta .dot { width: 3px; height: 3px; border-radius: 50%; background: var(--hairline); }
.news-card.important .news-source { color:var(--blue); background:rgba(66,98,255,.06); }
.news-card.risk .news-source { color:var(--red); background:var(--red-light); }
.analysis-card { padding: 16px; }
.analysis-head { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:12px; }
.analysis-title { font-size: 16px; font-weight: 900; color: var(--ink); }
.analysis-meta { color: var(--stone); font-size: 11px; font-weight: 800; text-align:right; line-height:1.5; }
.mood { display:inline-flex; border-radius:999px; padding:4px 9px; font-size:11px; font-weight:900; background:var(--surface); color:var(--slate); border:1px solid var(--hairline-soft); }
.mood.risk_on { color: var(--green); background: var(--green-light); border-color: rgba(0,180,115,.18); }
.mood.risk_off { color: var(--red); background: var(--red-light); border-color: rgba(229,62,62,.18); }
.analysis-summary { color: var(--slate); font-size: 14px; line-height: 1.75; margin-bottom: 14px; }
.analysis-grid { display:grid; grid-template-columns: 1fr; gap: 10px; }
.analysis-section { border:1px solid var(--hairline-soft); border-radius: var(--radius-lg); background: var(--surface); padding: 12px; min-width:0; }
.analysis-section h3 { font-size: 12px; font-weight: 900; color: var(--ink); margin-bottom: 9px; }
.analysis-item { border-top:1px solid var(--hairline-soft); padding:8px 0; color:var(--slate); font-size:12px; line-height:1.55; }
.analysis-item:first-of-type { border-top:0; padding-top:0; }
.analysis-item b { color:var(--ink); font-size:13px; }
.analysis-item .sub { display:block; margin-top:3px; color:var(--stone); }
.symbol-tags { display:flex; flex-wrap:wrap; gap:4px; margin-top:5px; }
.symbol-tag { display:inline-flex; padding:3px 7px; border-radius:999px; background:var(--canvas); border:1px solid var(--hairline-soft); color:var(--blue); font-size:10px; font-weight:900; }
@media(max-width:1040px){ .sentiment-grid{grid-template-columns:1fr;} .analysis-grid{grid-template-columns:1fr 1fr;} }
@media(max-width:760px){ .analysis-grid{grid-template-columns:1fr;} .analysis-head{display:block;} .analysis-meta{text-align:left;margin-top:8px;} }
/* Empty */
.empty-state { text-align:center; padding:48px 20px; color:var(--stone); }
.empty-state p { font-size:14px; }
/* Loading */
.loading-pulse { animation: pulse 1.5s ease-in-out infinite; }
@keyframes pulse { 0%,100%{ opacity:1 } 50%{ opacity:.3 } }
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to{ transform:rotate(360deg) } }
@media(max-width:640px) {
.shell { width: min(100% - 24px, 1180px); }
.news-card { padding: 14px 14px; gap: 10px; }
.news-source { min-width: 48px; font-size: 9px; padding: 3px 6px; }
}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<h1 class="page-title">实时舆情</h1>
<p class="page-sub">聚合快讯展示系统正在监控的原始新闻AI 分析展示基于最新新闻源的结构化研判。</p>
<!-- Dashboard -->
<div class="market-context">
<div class="fg-card" id="fgCard">
<div class="fg-label">恐惧 & 贪婪指数</div>
<div class="fg-value loading-pulse" id="fgValue">--</div>
<div class="fg-class" id="fgClass">加载中</div>
<div class="fg-gauge" id="fgGauge"></div>
</div>
<div class="trend-card">
<div class="section-label">CoinGecko 热门币种</div>
<div class="trend-list loading-pulse" id="trendList">
<span class="trend-pill">加载中...</span>
</div>
</div>
</div>
<div class="sentiment-grid">
<section class="panel">
<div class="panel-head">
<div>
<div class="panel-title">新闻源快讯</div>
<div class="panel-sub">来自 Binance、吴说、PANews 等入库新闻源的原始信息。</div>
</div>
<span class="feed-count" id="feedCount">--</span>
</div>
<div class="source-chips" id="sourceChips"></div>
<div class="news-feed" id="newsFeed">
<div class="empty-state"><p>加载中...</p></div>
</div>
</section>
<section class="panel">
<div class="panel-head">
<div>
<div class="panel-title">新闻舆情 AI 分析</div>
<div class="panel-sub">基于最近新闻源批次生成,只读解释,不直接改变推荐状态。</div>
</div>
</div>
<div id="aiAnalysis" class="analysis-card">
<div class="empty-state"><p>等待 AI 舆情分析结果...</p></div>
</div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
// ====== FEED ======
function ageStr(h) {
if (h == null) return '';
if (h < 1) return Math.round(h * 60) + '分钟前';
if (h < 24) return Math.floor(h) + '小时前';
return Math.floor(h / 24) + '天前';
}
function esc(v){ return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];}); }
function fgColor(v) {
if (v <= 25) return 'var(--red)';
if (v <= 45) return 'var(--orange)';
if (v <= 55) return 'var(--yellow)';
if (v <= 75) return 'var(--green)';
return 'var(--green)';
}
function fmtTime(t) {
if (!t) return '--';
var d = new Date(t);
if (isNaN(d.getTime())) return t;
return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0');
}
function renderAnalysis(resp) {
var box = document.getElementById('aiAnalysis');
var a = resp && resp.analysis ? resp.analysis : null;
if (!a) {
var msg = resp && resp.error ? ('AI 舆情分析未完成:' + resp.error) : '暂无 AI 舆情分析。调度器会定时生成,或运行 llm-insights --scope sentiment。';
box.innerHTML = '<div class="empty-state"><p>'+esc(msg)+'</p></div>';
return;
}
var mood = a.market_mood || 'neutral';
function symbolsHtml(items) {
return (items || []).slice(0, 8).map(function(s){ return '<span class="symbol-tag">'+esc(s)+'</span>'; }).join('');
}
var themes = (a.hot_themes || []).slice(0, 6).map(function(x){
return '<div class="analysis-item"><b>'+esc(x.theme || '主题')+'</b><span class="sub">'+esc(x.impact || x.reason || '--')+'</span><div class="symbol-tags">'+symbolsHtml(x.symbols || [])+'</div></div>';
}).join('') || '<div class="analysis-item">暂无明确主题</div>';
var impacts = (a.coin_impacts || []).slice(0, 8).map(function(x){
var check = x.need_technical_check ? ' · 需要技术检查' : '';
return '<div class="analysis-item"><b>'+esc(x.symbol || '--')+'</b><span class="sub">'+esc((x.direction || 'neutral') + check)+'</span><span class="sub">'+esc(x.reason || '--')+'</span></div>';
}).join('') || '<div class="analysis-item">暂无币种影响</div>';
var risks = (a.risk_events || []).slice(0, 6).map(function(x){
return '<div class="analysis-item"><b>'+esc(x.risk_type || x.title || '风险事件')+'</b><span class="sub">'+esc(x.title || x.reason || '--')+'</span><div class="symbol-tags">'+symbolsHtml(x.symbols || [])+'</div></div>';
}).join('') || '<div class="analysis-item">暂无集中风险</div>';
var watch = (a.watchlist || []).slice(0, 8).map(function(x){
return '<div class="analysis-item"><b>'+esc(x.symbol || '--')+'</b><span class="sub">'+esc(x.why || '--')+'</span><span class="sub">触发条件:'+esc(x.trigger || '--')+'</span></div>';
}).join('') || '<div class="analysis-item">暂无重点观察</div>';
box.innerHTML =
'<div class="analysis-head"><div><div class="analysis-title">AI 舆情研判 <span class="mood '+esc(mood)+'">'+esc(mood)+'</span></div></div><div class="analysis-meta">模型 '+esc(resp.model || '--')+'<br>更新 '+fmtTime(resp.updated_at)+' · 来源 '+esc(resp.event_count || 0)+' 条</div></div>'+
'<div class="analysis-summary">'+esc(a.summary || '暂无摘要')+'</div>'+
'<div class="analysis-grid">'+
'<div class="analysis-section"><h3>主线主题</h3>'+themes+'</div>'+
'<div class="analysis-section"><h3>币种影响</h3>'+impacts+'</div>'+
'<div class="analysis-section"><h3>风险事件</h3>'+risks+'</div>'+
'<div class="analysis-section"><h3>重点观察</h3>'+watch+'</div>'+
'</div>';
}
function renderSourceChips(items) {
var box = document.getElementById('sourceChips');
var counts = {};
(items || []).forEach(function(n){
var source = n.source_label || n.source || '新闻源';
counts[source] = (counts[source] || 0) + 1;
});
var chips = Object.keys(counts).sort(function(a,b){ return counts[b]-counts[a]; }).slice(0, 8).map(function(k){
return '<span class="source-chip">'+esc(k)+' <b>'+counts[k]+'</b></span>';
}).join('');
box.innerHTML = chips || '<span class="source-chip">暂无源数据</span>';
}
function renderNewsFeed(items) {
var news = (items || []).slice(0, 80);
document.getElementById('feedCount').textContent = news.length + ' 条';
renderSourceChips(news);
if (!news.length) {
document.getElementById('newsFeed').innerHTML = '<div class="empty-state"><p>暂无新闻源快讯</p></div>';
return;
}
document.getElementById('newsFeed').innerHTML = news.map(function(n) {
var importance = String(n.importance || 'B').toUpperCase();
var cls = importance === 'RISK' ? 'risk' : (importance === 'S' || importance === 'A') ? 'important' : '';
var meta = [
n.related_symbol || n.related_base || '',
importance ? ('重要性 ' + importance) : '',
n.decision ? ('技术检查 ' + n.decision) : '',
ageStr(n.age_hours)
].filter(Boolean).join(' · ');
return '<a class="news-card '+cls+'" href="' + esc(n.url || '#') + '" target="_blank" rel="noopener">' +
'<span class="news-source">' + esc(n.source_label || n.source || '新闻源') + '</span>' +
'<div class="news-body">' +
'<div class="news-title">' + esc(n.title) + '</div>' +
'<div class="news-meta"><span>' + esc(meta || '刚刚更新') + '</span></div>' +
'</div>' +
'</a>';
}).join('');
}
async function loadFeed() {
try {
var resp = await fetch(API + '/api/newsfeed');
var data = await resp.json();
var analysisResp = {};
try {
var ar = await fetch(API + '/api/sentiment/analysis');
if (ar.ok) analysisResp = await ar.json();
} catch(e) {}
renderAnalysis(analysisResp);
// Fear & Greed
var fg = data.fear_greed;
if (fg) {
var v = fg.value, cls = fg.classification, clr = fgColor(v);
document.getElementById('fgValue').textContent = v;
document.getElementById('fgValue').style.color = clr;
document.getElementById('fgValue').classList.remove('loading-pulse');
document.getElementById('fgClass').textContent = cls;
document.getElementById('fgClass').style.color = clr;
document.getElementById('fgClass').style.background = clr + '15';
document.getElementById('fgGauge').style.setProperty('--pos', v + '%');
}
// Trending
var trends = data.trending || [];
document.getElementById('trendList').classList.remove('loading-pulse');
if (trends.length) {
document.getElementById('trendList').innerHTML = trends.slice(0, 8).map(function(t) {
var symbol = String(t.symbol || '').toUpperCase();
var icon = t.thumb ? '<img src="' + esc(t.thumb) + '" alt="' + esc(symbol) + '" onerror="this.remove()">' : '';
return '<span class="trend-pill">' +
icon +
'<span class="trend-name">' + esc(t.name || symbol || '--') + '</span>' +
'<span class="trend-symbol">' + esc(symbol) + '</span>' +
'<span class="trend-rank">#' + esc(t.market_cap_rank || '--') + '</span>' +
'</span>';
}).join('');
} else {
document.getElementById('trendList').innerHTML = '<span style="color:var(--muted);font-size:12px">暂无数据</span>';
}
renderNewsFeed(data.news || []);
} catch(e) {
document.getElementById('newsFeed').innerHTML = '<div class="empty-state"><p>加载失败,请稍后重试</p></div>';
}
}
loadFeed();
// Auto-refresh every 5 minutes
setInterval(loadFeed, 300000);
</script>
{% endblock %}