v1.1.0: AI分析历史记录 + 数据源详情页(/analysis/<id>),展示大模型参考的RAG新闻/概况/指标/评级/持仓/提示词

This commit is contained in:
2026-08-19 21:00:20 +08:00
parent b0fece63ff
commit 4a9ea9ddd7
8 changed files with 273 additions and 10 deletions
+14
View File
@@ -208,3 +208,17 @@ tr:hover td { background: rgba(59,130,246,.05); }
.score-bar .sb-track { flex: 1; height: 6px; background: #262d3a; border-radius: 3px; overflow: hidden; }
.score-bar .sb-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 3px; }
.score-bar .sb-val { width: 28px; text-align: right; color: var(--text); }
/* ===== 数据源区块(分析详情页) ===== */
.src-block { border: 1px solid var(--border); border-radius: 10px; margin-bottom: 10px; background: var(--bg2); overflow: hidden; }
.src-block summary { cursor: pointer; padding: 11px 14px; font-weight: 600; font-size: 13px; user-select: none; list-style: none; }
.src-block summary::-webkit-details-marker { display: none; }
.src-block summary:hover { background: rgba(59,130,246,.06); }
.src-count { float: right; color: var(--accent); font-weight: 700; }
.src-body { padding: 0 14px 12px; border-top: 1px solid #1f2630; }
.src-news { padding: 10px 0; border-bottom: 1px dashed #1f2630; }
.src-news:last-child { border-bottom: none; }
.src-news-title { font-weight: 600; margin-bottom: 5px; font-size: 13px; }
.src-news-meta { display: flex; gap: 10px; align-items: center; font-size: 12px; color: var(--muted); margin-bottom: 5px; flex-wrap: wrap; }
.src-news-text { font-size: 12px; color: var(--text2); line-height: 1.7; }
.src-pre { white-space: pre-wrap; word-break: break-word; font-size: 12px; color: var(--text2); line-height: 1.7; background: #0d1117; border: 1px solid #1f2630; border-radius: 8px; padding: 10px; overflow-x: auto; }
+89
View File
@@ -0,0 +1,89 @@
/* AI 分析详情页:研报正文 + 数据源 */
const CODE_AID = parseInt(location.pathname.split('/').pop(), 10);
async function load() {
try {
const d = await api('/api/analyses/' + CODE_AID);
const a = d.analysis, stock = d.stock || {};
$('#aStock').textContent = a.stock_name || stock.name || '—';
$('#aCode').textContent = a.code;
$('#aIndustry').textContent = stock ? (stock.name + ' · ' + (stock.industry || '') + ' · ' + (stock.board || '')) : '';
$('#aTime').textContent = a.created_at;
$('#aFocus').textContent = a.focus || '整体投资价值';
$('#reportBox').innerHTML = mdRender(a.report);
renderSources(a.sources || {});
} catch (e) {
$('#reportBox').innerHTML = '<div class="empty">加载失败:' + escapeHtml(e.message) + '</div>';
}
}
function srcBlock(title, icon, inner, count) {
return `<details class="src-block" ${inner ? 'open' : ''}>
<summary>${icon} ${title} ${count !== undefined ? `<span class="src-count">${count}</span>` : ''}</summary>
<div class="src-body">${inner || '<div class="empty">无</div>'}</div>
</details>`;
}
function renderSources(s) {
let html = '';
// 1. RAG 相关资讯
const news = s.news || [];
html += srcBlock('RAG 相关资讯(向量检索命中)', '📰', news.map(n => `
<div class="src-news">
<div class="src-news-title">${escapeHtml(n.title)}</div>
<div class="src-news-meta">
<span>${n.date || '—'}</span>
<span class="tag tag-${n.sentiment > 0 ? '正' : n.sentiment < 0 ? '负' : '平'}">情感 ${Number(n.sentiment).toFixed(2)}</span>
<span style="color:var(--muted)">相似度 ${(1 - (n.distance || 0)).toFixed(3)}</span>
</div>
<div class="src-news-text">${escapeHtml(n.text)}</div>
</div>`).join(''), news.length);
// 2. 公司概况
html += srcBlock('公司概况(RAG 检索)', '🏢', s.profile ? `<p>${escapeHtml(s.profile)}</p>` : '', s.profile ? 1 : 0);
// 3. 技术指标
html += srcBlock('技术指标', '📈', s.indicators ? `<pre class="src-pre">${escapeHtml(s.indicators)}</pre>` : '', s.indicators ? 1 : 0);
// 4. 综合评分
if (s.score) {
const sc = s.score;
html += srcBlock('综合评分', '🎯', `
<div class="mini-stats" style="gap:14px">
<div class="mini-stat"><div class="ms-label">总分</div><div class="ms-value" style="color:${sc.total >= 68 ? '#f59e0b' : '#3b82f6'}">${sc.total}</div></div>
<div class="mini-stat"><div class="ms-label">评级</div><div class="ms-value"><span class="tag tag-${sc.rating}">${sc.rating}</span></div></div>
<div class="mini-stat"><div class="ms-label">趋势</div><div class="ms-value">${sc.trend}</div></div>
<div class="mini-stat"><div class="ms-label">动量</div><div class="ms-value">${sc.momentum}</div></div>
<div class="mini-stat"><div class="ms-label">技术</div><div class="ms-value">${sc.technical}</div></div>
<div class="mini-stat"><div class="ms-label">量能</div><div class="ms-value">${sc.volume}</div></div>
<div class="mini-stat"><div class="ms-label">消息</div><div class="ms-value">${sc.news}</div></div>
<div class="mini-stat"><div class="ms-label">机构</div><div class="ms-value">${sc.institutional}</div></div>
</div>`, 1);
}
// 5. 机构评级
const ratings = s.ratings || [];
html += srcBlock('机构评级', '🏦', ratings.length ? `<table>
<tr><th>机构</th><th>评级</th><th>目标价</th><th>日期</th></tr>
${ratings.map(r => `<tr><td>${escapeHtml(r.inst_name)}</td>
<td><span class="tag tag-${r.rating}">${r.rating}</span></td>
<td class="num">${r.target_price}</td><td>${r.rating_date}</td></tr>`).join('')}
</table>` : '', ratings.length);
// 6. 基金持仓
const holdings = s.holdings || [];
html += srcBlock('基金持仓', '💼', holdings.length ? `<table>
<tr><th>机构</th><th>季度</th><th>持仓市值</th><th>环比</th></tr>
${holdings.map(h => `<tr><td>${escapeHtml(h.inst_name)}</td><td>${h.quarter}</td>
<td class="num">${fmtNum(h.hold_value)}</td>
<td class="num ${pctClass(h.change_pct)}">${fmtPct(h.change_pct)}</td></tr>`).join('')}
</table>` : '', holdings.length);
// 7. 完整提示词
html += srcBlock('完整提示词(Prompt', '🧠', s.prompt ? `<pre class="src-pre">${escapeHtml(s.prompt)}</pre>` : '', s.prompt ? 1 : 0);
$('#sourceBox').innerHTML = html;
}
load();
+28 -1
View File
@@ -232,7 +232,9 @@ async function pollReport() {
clearInterval(timer);
$('#analyzeBtn').disabled = false;
$('#reportBox').innerHTML = `<div class="markdown-body">${mdRender(d.report)}</div>
<div style="color:var(--muted);font-size:12px;margin-top:12px">${d.cached ? '缓存报告 ' + d.created_at + '' : '生成耗时 ' + Math.round((Date.now() - t0) / 1000) + ' 秒'}</div>`;
<div style="color:var(--muted);font-size:12px;margin-top:12px">${d.cached ? '最近一次分析记录,' + d.created_at + '' : '生成耗时 ' + Math.round((Date.now() - t0) / 1000) + ' 秒'}
${d.history_id ? ` · <a href="/analysis/${d.history_id}" target="_blank">查看数据源详情 ↗</a>` : ''}</div>`;
loadHistory();
} else if (d.status === 'error') {
clearInterval(timer);
$('#analyzeBtn').disabled = false;
@@ -246,7 +248,32 @@ async function pollReport() {
}, 4000);
}
/* 历史分析记录 */
async function loadHistory() {
try {
const d = await api(`/api/stock/${CODE}/analyses`);
const items = d.items || [];
if (!items.length) {
$('#historyBox').innerHTML = '<div class="empty">暂无历史分析,点击上方「生成研报」开始</div>';
return;
}
$('#historyBox').innerHTML = `<table>
<tr><th>时间</th><th>关注点</th><th>引用资讯</th><th>报告摘要</th><th>操作</th></tr>
${items.map(h => `<tr>
<td style="color:var(--muted)">${h.created_at}</td>
<td>${escapeHtml(h.focus || '整体投资价值')}</td>
<td class="num">${h.news_count} 条</td>
<td style="max-width:300px;white-space:normal;color:var(--text2)">${escapeHtml(h.excerpt)}…</td>
<td><button class="btn btn-primary" onclick="window.open('/analysis/${h.id}','_blank')">查看详情 ↗</button></td>
</tr>`).join('')}
</table>`;
} catch (e) {
$('#historyBox').innerHTML = '<div class="empty">历史记录加载失败</div>';
}
}
$('#analyzeBtn').onclick = generateReport;
$('#focusInput').addEventListener('keydown', e => { if (e.key === 'Enter') generateReport(); });
load();
loadHistory();