diff --git a/manual_trading_hub/hub.py b/manual_trading_hub/hub.py
index 6254982..3bba1fc 100644
--- a/manual_trading_hub/hub.py
+++ b/manual_trading_hub/hub.py
@@ -234,6 +234,21 @@ async def _hub_lifespan(_app: FastAPI):
app = FastAPI(title="复盘系统中控", docs_url=None, redoc_url=None, lifespan=_hub_lifespan)
STATIC_DIR = DIR / "static"
+_REPO_STATIC = _REPO_ROOT / "static"
+_AI_REVIEW_RENDER_JS = _REPO_STATIC / "ai_review_render.js"
+
+
+@app.get("/assets/ai_review_render.js")
+def hub_ai_review_render_js():
+ """与四所实例共用仓库根 static/ai_review_render.js(须在 /assets mount 之前注册)。"""
+ if not _AI_REVIEW_RENDER_JS.is_file():
+ raise HTTPException(status_code=404, detail="ai_review_render.js not found")
+ return FileResponse(
+ str(_AI_REVIEW_RENDER_JS),
+ media_type="application/javascript; charset=utf-8",
+ )
+
+
if STATIC_DIR.is_dir():
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR)), name="assets")
diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css
index 44cd60d..d3f3caa 100644
--- a/manual_trading_hub/static/app.css
+++ b/manual_trading_hub/static/app.css
@@ -3513,6 +3513,68 @@ body.hub-page-ai #page-ai {
overflow-wrap: anywhere;
word-break: break-word;
}
+.ai-md-body.ai-result-md,
+.ai-bubble-assistant.ai-result-md {
+ white-space: normal;
+}
+.ai-result-md p {
+ margin: 6px 0;
+ color: var(--text);
+}
+.ai-result-md ul,
+.ai-result-md ol {
+ margin: 6px 0 8px 1.25em;
+ padding: 0;
+}
+.ai-result-md li {
+ margin: 5px 0;
+ line-height: 1.5;
+}
+.ai-result-md strong {
+ color: var(--text);
+ font-weight: 600;
+}
+.ai-result-md h2 {
+ font-size: 1.02rem;
+ color: var(--accent-2, var(--accent));
+ margin: 14px 0 8px;
+ padding-bottom: 4px;
+ border-bottom: 1px solid var(--border-soft);
+}
+.ai-result-md h3,
+.ai-result-md h4 {
+ font-size: 0.92rem;
+ color: var(--accent-2, var(--accent));
+ margin: 10px 0 6px;
+}
+.ai-result-md code {
+ background: color-mix(in srgb, var(--inset-surface) 70%, var(--border-soft));
+ padding: 1px 4px;
+ border-radius: 4px;
+ font-size: 0.82em;
+}
+.ai-result-md .md-raw-block-title {
+ margin-top: 14px;
+ padding-top: 10px;
+ border-top: 1px dashed var(--border-soft);
+ color: var(--muted);
+ font-weight: 600;
+}
+.ai-bubble-assistant.ai-result-md p {
+ margin: 4px 0;
+}
+.ai-bubble-assistant.ai-result-md h2,
+.ai-bubble-assistant.ai-result-md h3,
+.ai-bubble-assistant.ai-result-md h4 {
+ margin: 8px 0 4px;
+ font-size: 0.92rem;
+ border-bottom: none;
+ padding-bottom: 0;
+}
+.ai-bubble-assistant.ai-result-md ul,
+.ai-bubble-assistant.ai-result-md ol {
+ margin: 4px 0 6px 1.15em;
+}
.ai-placeholder {
color: var(--muted);
margin: 0;
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index 17a5b21..afbc21a 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -2958,17 +2958,32 @@
return `${n > 0 ? "+" : "-"}${abs}U`;
}
- function renderAiMarkdown(text) {
- const escLocal = (s) =>
- String(s || "")
- .replace(/&/g, "&")
- .replace(//g, ">");
- return escLocal(text)
+ function renderHubMarkdown(text) {
+ const raw = String(text || "");
+ if (typeof window !== "undefined" && window.AiReviewRender && window.AiReviewRender.renderMarkdown) {
+ return window.AiReviewRender.renderMarkdown(raw);
+ }
+ return esc(raw)
.replace(/\*\*(.+?)\*\*/g, "$1")
.replace(/\n/g, "
");
}
+ function renderAiMarkdown(text) {
+ return renderHubMarkdown(text);
+ }
+
+ function setAiSummaryMarkdown(body, contentMd) {
+ if (!body) return;
+ body.classList.add("ai-result-md");
+ body.innerHTML = renderHubMarkdown(contentMd);
+ }
+
+ function setAiSummaryPlaceholder(body, html) {
+ if (!body) return;
+ body.classList.remove("ai-result-md");
+ body.innerHTML = html;
+ }
+
function renderAiSummaryStats(snapshot) {
const el = document.getElementById("ai-summary-stats");
if (!el) return;
@@ -2994,10 +3009,13 @@
const label = isUser ? "主人" : "AI教练";
const rowCls = isUser ? "ai-msg-row-user" : "ai-msg-row-coach";
const bubbleCls = isUser ? "ai-bubble-user" : "ai-bubble-assistant";
+ const isThinking = extraClass && String(extraClass).includes("ai-bubble-thinking");
+ const bubbleInner = isUser || isThinking ? esc(content || "") : renderHubMarkdown(content || "");
+ const mdCls = !isUser && !isThinking ? " ai-result-md" : "";
return (
`
${esc(String(e))}
`; + if (body) setAiSummaryPlaceholder(body, `${esc(String(e))}
`); } } @@ -3087,7 +3105,7 @@ const btn = document.getElementById("btn-ai-summary"); const body = document.getElementById("ai-summary-body"); if (btn) btn.disabled = true; - if (body) body.innerHTML = '正在聚合四户数据并生成总结…
'; + if (body) setAiSummaryPlaceholder(body, '正在聚合四户数据并生成总结…
'); try { const r = await apiFetch("/api/ai/summary/generate", { method: "POST", @@ -3099,14 +3117,14 @@ if (!j.ok && j.detail) throw new Error(j.detail); const sum = j.summary; if (sum && sum.content_md && body) { - body.innerHTML = renderAiMarkdown(sum.content_md); + setAiSummaryMarkdown(body, sum.content_md); renderAiSummaryStats(sum.stats_snapshot); } showToast(j.cached ? "已是最新上下文,返回缓存总结" : "今日总结已生成"); await loadAiSummary(); } catch (e) { showToast(String(e), true); - if (body) body.innerHTML = `${esc(String(e))}
`; + if (body) setAiSummaryPlaceholder(body, `${esc(String(e))}
`); } finally { aiSummaryLoading = false; if (btn) btn.disabled = false; diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index d93a4da..7181c91 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -287,6 +287,7 @@ - + +