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 ( `
` + `${label}` + - `
${esc(content || "")}
` + + `
${bubbleInner}
` + `
` ); } @@ -3057,7 +3075,7 @@ const j = await r.json(); const latest = j.latest; if (latest && latest.content_md) { - if (body) body.innerHTML = renderAiMarkdown(latest.content_md); + if (body) setAiSummaryMarkdown(body, latest.content_md); renderAiSummaryStats(latest.stats_snapshot); const sm = document.getElementById("ai-summary-meta"); if (sm && latest.generated_at) { @@ -3065,7 +3083,7 @@ } } } catch (e) { - if (body) body.innerHTML = `

${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 @@
- + +