Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a9ea9ddd7 |
@@ -61,6 +61,12 @@ def page_stock(code):
|
|||||||
service=SERVICE_NAME, is_mock=IS_MOCK)
|
service=SERVICE_NAME, is_mock=IS_MOCK)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/analysis/<int:aid>")
|
||||||
|
def page_analysis(aid):
|
||||||
|
return render_template("analysis_detail.html", aid=aid,
|
||||||
|
service=SERVICE_NAME, is_mock=IS_MOCK)
|
||||||
|
|
||||||
|
|
||||||
# ===================================================================== 公共
|
# ===================================================================== 公共
|
||||||
def _indicators(code):
|
def _indicators(code):
|
||||||
rows = query("SELECT date,open,high,low,close,volume FROM stock_daily "
|
rows = query("SELECT date,open,high,low,close,volume FROM stock_daily "
|
||||||
@@ -365,13 +371,32 @@ def api_analyze(code):
|
|||||||
def api_analyze_status(code):
|
def api_analyze_status(code):
|
||||||
from engine import analyst
|
from engine import analyst
|
||||||
st = analyst.report_status(code)
|
st = analyst.report_status(code)
|
||||||
if not st:
|
if st:
|
||||||
cached = analyst.get_cached_report(code)
|
return jsonify(st)
|
||||||
if cached:
|
# 无运行中任务:返回最近一次历史分析
|
||||||
return jsonify({"status": "done", "report": cached["report"],
|
hist = analyst.list_history(code, limit=1)
|
||||||
"cached": True, "created_at": cached["created_at"]})
|
if hist:
|
||||||
return jsonify({"status": "idle"})
|
detail = analyst.get_history(hist[0]["id"])
|
||||||
return jsonify(st)
|
return jsonify({"status": "done", "report": detail["report"],
|
||||||
|
"history_id": detail["id"], "created_at": detail["created_at"],
|
||||||
|
"cached": True})
|
||||||
|
return jsonify({"status": "idle"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/stock/<code>/analyses")
|
||||||
|
def api_stock_analyses(code):
|
||||||
|
from engine import analyst
|
||||||
|
return jsonify({"items": analyst.list_history(code)})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/analyses/<int:aid>")
|
||||||
|
def api_analysis_detail(aid):
|
||||||
|
from engine import analyst
|
||||||
|
d = analyst.get_history(aid)
|
||||||
|
if not d:
|
||||||
|
return jsonify({"error": "记录不存在"}), 404
|
||||||
|
stock = query_one("SELECT code, name, industry, board FROM stocks WHERE code=?", (d["code"],))
|
||||||
|
return jsonify({"analysis": d, "stock": stock})
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ 自选
|
# ------------------------------------------------------------------ 自选
|
||||||
|
|||||||
+13
-1
@@ -100,6 +100,17 @@ CREATE TABLE IF NOT EXISTS analysis_cache (
|
|||||||
created_at TEXT DEFAULT (datetime('now','localtime'))
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS analysis_history (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
code TEXT NOT NULL,
|
||||||
|
stock_name TEXT DEFAULT '',
|
||||||
|
focus TEXT DEFAULT '',
|
||||||
|
report TEXT,
|
||||||
|
sources TEXT DEFAULT '{}', -- JSON:大模型参考的数据源(RAG新闻/概况/指标/评级/持仓/提示词)
|
||||||
|
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_history_code ON analysis_history(code);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS market_index (
|
CREATE TABLE IF NOT EXISTS market_index (
|
||||||
date TEXT PRIMARY KEY,
|
date TEXT PRIMARY KEY,
|
||||||
sh REAL DEFAULT 0, -- 上证指数(点)
|
sh REAL DEFAULT 0, -- 上证指数(点)
|
||||||
@@ -165,7 +176,8 @@ def table_count(name):
|
|||||||
def wipe_all():
|
def wipe_all():
|
||||||
"""清空业务表(保留结构)+ 重置自增序列,用于重灌数据"""
|
"""清空业务表(保留结构)+ 重置自增序列,用于重灌数据"""
|
||||||
for t in ("stock_daily", "inst_ratings", "fund_holdings", "news",
|
for t in ("stock_daily", "inst_ratings", "fund_holdings", "news",
|
||||||
"institutions", "stocks", "watchlist", "analysis_cache", "market_index"):
|
"institutions", "stocks", "watchlist", "analysis_cache", "analysis_history",
|
||||||
|
"market_index"):
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
conn.execute(f'DELETE FROM "{t}"')
|
conn.execute(f'DELETE FROM "{t}"')
|
||||||
with db() as conn:
|
with db() as conn:
|
||||||
|
|||||||
+58
-1
@@ -59,6 +59,7 @@ def _rag_news(code, stock_name, query_text, top_k=6):
|
|||||||
"date": m.get("date", ""),
|
"date": m.get("date", ""),
|
||||||
"sentiment": m.get("sentiment", 0),
|
"sentiment": m.get("sentiment", 0),
|
||||||
"text": h.get("document", "")[:400],
|
"text": h.get("document", "")[:400],
|
||||||
|
"distance": round(h.get("distance", 0), 3),
|
||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -140,7 +141,8 @@ def _build_prompt(stock, ind, news_hits, profile, ratings, holdings, score, focu
|
|||||||
|
|
||||||
|
|
||||||
def generate_report_sync(code, focus=""):
|
def generate_report_sync(code, focus=""):
|
||||||
"""同步生成报告(后台线程调用)"""
|
"""同步生成报告(后台线程调用),并记录历史 + 数据源"""
|
||||||
|
import json
|
||||||
stock = query_one("SELECT * FROM stocks WHERE code=?", (code,))
|
stock = query_one("SELECT * FROM stocks WHERE code=?", (code,))
|
||||||
if not stock:
|
if not stock:
|
||||||
return {"error": "股票不存在"}
|
return {"error": "股票不存在"}
|
||||||
@@ -150,6 +152,17 @@ def generate_report_sync(code, focus=""):
|
|||||||
profile = _rag_profile(code)
|
profile = _rag_profile(code)
|
||||||
ratings, holdings = _inst_summary(code)
|
ratings, holdings = _inst_summary(code)
|
||||||
prompt = _build_prompt(stock, ind, hits, profile, ratings, holdings, score, focus)
|
prompt = _build_prompt(stock, ind, hits, profile, ratings, holdings, score, focus)
|
||||||
|
# 记录大模型参考的数据源(供详情页展示)
|
||||||
|
sources = {
|
||||||
|
"focus": focus,
|
||||||
|
"score": score,
|
||||||
|
"indicators": _fmt_indicators(ind),
|
||||||
|
"profile": profile or stock.get("description", ""),
|
||||||
|
"news": hits,
|
||||||
|
"ratings": ratings,
|
||||||
|
"holdings": holdings,
|
||||||
|
"prompt": prompt,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
report = llm_chat([
|
report = llm_chat([
|
||||||
{"role": "system", "content": "你是一名严谨专业的A股投资顾问,输出结构化、简洁、可执行的研报。"},
|
{"role": "system", "content": "你是一名严谨专业的A股投资顾问,输出结构化、简洁、可执行的研报。"},
|
||||||
@@ -160,6 +173,10 @@ def generate_report_sync(code, focus=""):
|
|||||||
raise RuntimeError("LLM 返回为空")
|
raise RuntimeError("LLM 返回为空")
|
||||||
execute("INSERT OR REPLACE INTO analysis_cache(code, report, created_at) VALUES(?,?,datetime('now','localtime'))",
|
execute("INSERT OR REPLACE INTO analysis_cache(code, report, created_at) VALUES(?,?,datetime('now','localtime'))",
|
||||||
(code, report))
|
(code, report))
|
||||||
|
execute(
|
||||||
|
"INSERT INTO analysis_history(code, stock_name, focus, report, sources, created_at) "
|
||||||
|
"VALUES(?,?,?,?,?,datetime('now','localtime'))",
|
||||||
|
(code, stock["name"], focus, report, json.dumps(sources, ensure_ascii=False)))
|
||||||
return {"report": report, "ts": time.time()}
|
return {"report": report, "ts": time.time()}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception("gen report fail")
|
log.exception("gen report fail")
|
||||||
@@ -221,3 +238,43 @@ def report_status(code):
|
|||||||
|
|
||||||
def get_cached_report(code):
|
def get_cached_report(code):
|
||||||
return query_one("SELECT report, created_at FROM analysis_cache WHERE code=?", (code,))
|
return query_one("SELECT report, created_at FROM analysis_cache WHERE code=?", (code,))
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ 历史记录
|
||||||
|
def list_history(code, limit=20):
|
||||||
|
"""某股票的历史 AI 分析记录(不含正文,只返回摘要)"""
|
||||||
|
rows = query(
|
||||||
|
"SELECT id, code, stock_name, focus, created_at, sources, LENGTH(report) AS len, "
|
||||||
|
"SUBSTR(report, 1, 60) AS excerpt FROM analysis_history "
|
||||||
|
"WHERE code=? ORDER BY id DESC LIMIT ?", (code, limit))
|
||||||
|
out = []
|
||||||
|
for r in rows:
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
src = json.loads(r.get("sources") or "{}")
|
||||||
|
except Exception:
|
||||||
|
src = {}
|
||||||
|
out.append({
|
||||||
|
"id": r["id"], "code": r["code"], "stock_name": r["stock_name"],
|
||||||
|
"focus": r["focus"], "created_at": r["created_at"],
|
||||||
|
"chars": r["len"], "excerpt": (r["excerpt"] or "").strip(),
|
||||||
|
"news_count": len(src.get("news") or []),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_history(aid):
|
||||||
|
"""单条分析详情:正文 + 数据源 JSON"""
|
||||||
|
import json
|
||||||
|
row = query_one("SELECT * FROM analysis_history WHERE id=?", (aid,))
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
src = json.loads(row.get("sources") or "{}")
|
||||||
|
except Exception:
|
||||||
|
src = {}
|
||||||
|
return {
|
||||||
|
"id": row["id"], "code": row["code"], "stock_name": row["stock_name"],
|
||||||
|
"focus": row["focus"], "report": row["report"], "created_at": row["created_at"],
|
||||||
|
"sources": src,
|
||||||
|
}
|
||||||
@@ -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-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-fill { height: 100%; background: linear-gradient(90deg, #3b82f6, #8b5cf6); border-radius: 3px; }
|
||||||
.score-bar .sb-val { width: 28px; text-align: right; color: var(--text); }
|
.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; }
|
||||||
@@ -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();
|
||||||
@@ -232,7 +232,9 @@ async function pollReport() {
|
|||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
$('#analyzeBtn').disabled = false;
|
$('#analyzeBtn').disabled = false;
|
||||||
$('#reportBox').innerHTML = `<div class="markdown-body">${mdRender(d.report)}</div>
|
$('#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') {
|
} else if (d.status === 'error') {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
$('#analyzeBtn').disabled = false;
|
$('#analyzeBtn').disabled = false;
|
||||||
@@ -246,7 +248,32 @@ async function pollReport() {
|
|||||||
}, 4000);
|
}, 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;
|
$('#analyzeBtn').onclick = generateReport;
|
||||||
$('#focusInput').addEventListener('keydown', e => { if (e.key === 'Enter') generateReport(); });
|
$('#focusInput').addEventListener('keydown', e => { if (e.key === 'Enter') generateReport(); });
|
||||||
|
|
||||||
load();
|
load();
|
||||||
|
loadHistory();
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}AI 分析详情{% endblock %}
|
||||||
|
{% block page_title %}AI 深度分析详情{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div id="topCard" class="card">
|
||||||
|
<div class="flex between wrap">
|
||||||
|
<div class="flex">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:20px;font-weight:800"><span id="aStock">—</span> <span id="aCode" style="color:var(--muted);font-size:14px"></span></div>
|
||||||
|
<div class="mt8 flex" style="gap:14px;color:var(--muted);font-size:13px">
|
||||||
|
<span id="aIndustry">—</span>
|
||||||
|
<span>生成时间 <b id="aTime" style="color:var(--text)"></b></span>
|
||||||
|
<span>关注点 <b id="aFocus" style="color:var(--text)"></b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<button class="btn" onclick="location.href='/stock/' + CODE">← 返回股票页</button>
|
||||||
|
<button class="btn btn-primary" onclick="location.href='/stock/' + CODE + '?analyze=1'">🤖 生成新分析</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-2-1 mt16">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="bar"></span>研报正文</div>
|
||||||
|
<div id="reportBox" class="markdown-body"><div class="loading">加载中…</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-title"><span class="bar" style="background:var(--gold)"></span>数据源(大模型参考内容)</div>
|
||||||
|
<div id="sourceBox" style="max-height:70vh;overflow:auto;padding-right:6px"><div class="loading">加载中…</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/analysis_detail.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -71,6 +71,8 @@
|
|||||||
<div id="reportBox" class="mt16">
|
<div id="reportBox" class="mt16">
|
||||||
<div class="empty">点击「生成研报」,系统将基于 新闻RAG检索 + 技术指标 + 机构数据 调用 DeepSeek 生成结构化研报(首次生成约需 30-90 秒)</div>
|
<div class="empty">点击「生成研报」,系统将基于 新闻RAG检索 + 技术指标 + 机构数据 调用 DeepSeek 生成结构化研报(首次生成约需 30-90 秒)</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-title mt16" style="font-size:14px"><span class="bar" style="background:var(--cyan)"></span>历史分析记录</div>
|
||||||
|
<div id="historyBox"><div class="loading">加载中…</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card mt16">
|
<div class="card mt16">
|
||||||
|
|||||||
Reference in New Issue
Block a user