From ebadcb111923afc363f0910574d020a02bfcb367 Mon Sep 17 00:00:00 2001 From: dekun Date: Sun, 7 Jun 2026 00:10:50 +0800 Subject: [PATCH] fix(hub): polish AI coach UI with PnL colors and chat roles Equal-height summary/chat panels, colored closed/float PnL, owner/coach labels, and optimistic thinking state. Co-authored-by: Cursor --- manual_trading_hub/static/app.css | 115 +++++++++++++++++++++++---- manual_trading_hub/static/app.js | 94 ++++++++++++++++------ manual_trading_hub/static/index.html | 12 +-- 3 files changed, 174 insertions(+), 47 deletions(-) diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index db97ac6..e272d62 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -3344,15 +3344,20 @@ html[data-theme="light"] button.danger { } /* --- Hub AI 教练 --- */ +#page-ai .page-head { + margin-bottom: 12px; +} .ai-layout { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; - align-items: start; + align-items: stretch; + min-height: calc(100vh - 168px); } @media (max-width: 960px) { .ai-layout { grid-template-columns: 1fr; + min-height: 0; } } .ai-panel { @@ -3360,17 +3365,20 @@ html[data-theme="light"] button.danger { border: 1px solid var(--border-soft); border-radius: var(--radius); padding: 14px 16px; - min-height: 420px; + min-height: 0; + height: 100%; display: flex; flex-direction: column; gap: 12px; + overflow: hidden; } .ai-panel-head { display: flex; flex-wrap: wrap; - align-items: center; + align-items: flex-start; justify-content: space-between; gap: 8px; + flex-shrink: 0; } .ai-panel-head h2 { margin: 0; @@ -3382,7 +3390,22 @@ html[data-theme="light"] button.danger { display: flex; flex-wrap: wrap; align-items: center; + justify-content: flex-end; gap: 8px; + max-width: 100%; +} +.ai-meta-line { + max-width: min(420px, 100%); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.72rem; +} +.ai-panel-scroll { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + overscroll-behavior: contain; } .ai-stats-row { display: flex; @@ -3390,6 +3413,7 @@ html[data-theme="light"] button.danger { gap: 8px 14px; font-size: 0.82rem; color: var(--muted); + flex-shrink: 0; } .ai-stat-chip { padding: 4px 8px; @@ -3401,10 +3425,22 @@ html[data-theme="light"] button.danger { color: var(--text); margin-right: 4px; } +.ai-stat-chip.pos, +.ai-stat-val.pos { + color: var(--green); + border-color: color-mix(in srgb, var(--green) 35%, transparent); +} +.ai-stat-chip.neg, +.ai-stat-val.neg { + color: var(--red); + border-color: color-mix(in srgb, var(--red) 35%, transparent); +} +.ai-stat-chip.pos strong, +.ai-stat-chip.neg strong { + color: inherit; + opacity: 0.85; +} .ai-md-body { - flex: 1; - overflow: auto; - max-height: min(62vh, 640px); padding: 12px; border-radius: 8px; background: var(--inset-surface); @@ -3412,52 +3448,94 @@ html[data-theme="light"] button.danger { font-size: 0.86rem; line-height: 1.55; color: var(--text); + overflow-wrap: anywhere; + word-break: break-word; } .ai-placeholder { color: var(--muted); margin: 0; } -.ai-chat-panel { - min-height: 520px; -} .ai-chat-messages { - flex: 1; - overflow: auto; - max-height: min(52vh, 520px); display: flex; flex-direction: column; - gap: 10px; - padding: 8px 4px; + gap: 12px; + padding: 8px 4px 4px; +} +.ai-msg-row { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 100%; +} +.ai-msg-row-user { + align-self: flex-end; + align-items: flex-end; + max-width: 88%; +} +.ai-msg-row-coach { + align-self: flex-start; + align-items: flex-start; + max-width: 92%; +} +.ai-msg-role { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; + color: var(--muted); + padding: 0 4px; +} +.ai-msg-row-user .ai-msg-role { + color: var(--accent); +} +.ai-msg-row-coach .ai-msg-role { + color: var(--accent-2); } .ai-bubble { - max-width: 92%; + width: 100%; padding: 10px 12px; border-radius: 10px; font-size: 0.88rem; line-height: 1.5; white-space: pre-wrap; + overflow-wrap: anywhere; word-break: break-word; } .ai-bubble-user { - align-self: flex-end; background: var(--accent-dim); border: 1px solid var(--border); } .ai-bubble-assistant { - align-self: flex-start; background: var(--inset-surface); border: 1px solid var(--border-soft); } +.ai-bubble-thinking { + color: var(--muted); + font-style: italic; + animation: ai-think-pulse 1.2s ease-in-out infinite; +} +@keyframes ai-think-pulse { + 0%, + 100% { + opacity: 0.55; + } + 50% { + opacity: 1; + } +} .ai-chat-form { display: grid; grid-template-columns: 1fr auto; gap: 8px; align-items: end; + flex-shrink: 0; + padding-top: 4px; + border-top: 1px solid var(--border-soft); } .ai-chat-form textarea { width: 100%; resize: vertical; min-height: 72px; + max-height: 160px; padding: 10px 12px; border-radius: 8px; border: 1px solid var(--border-soft); @@ -3470,4 +3548,7 @@ html[data-theme="light"] button.danger { outline: none; border-color: var(--accent); } +.ai-chat-form textarea:disabled { + opacity: 0.65; +} diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index a7164e8..72e58d8 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -2941,14 +2941,29 @@ let aiMeta = null; let aiSummaryLoading = false; let aiChatLoading = false; + let aiChatSessionCache = null; + + function aiPnlClass(v) { + const n = Number(v); + if (!Number.isFinite(n) || Math.abs(n) < 1e-9) return ""; + return n > 0 ? "pos" : "neg"; + } + + function aiPnlSigned(v, digits) { + const n = Number(v); + if (!Number.isFinite(n)) return "—"; + const abs = fmt(Math.abs(n), digits); + if (Math.abs(n) < 1e-9) return `${abs}U`; + return `${n > 0 ? "+" : "-"}${abs}U`; + } function renderAiMarkdown(text) { - const esc = (s) => + const escLocal = (s) => String(s || "") .replace(/&/g, "&") .replace(//g, ">"); - return esc(text) + return escLocal(text) .replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\n/g, "
"); } @@ -2961,17 +2976,33 @@ return; } const t = snapshot.totals; - const pnl = Number(t.total_pnl_u); - const pnlCls = pnl > 0 ? "pos" : pnl < 0 ? "neg" : ""; + const closedPnl = Number(t.total_pnl_u); + const floatPnl = Number(t.float_pnl_u); + const closedCls = aiPnlClass(closedPnl); + const floatCls = aiPnlClass(floatPnl); el.innerHTML = [ `交易日${esc(t.trading_day || "—")}`, - `平仓盈亏${fmt(pnl, 2)}U`, + `平仓盈亏${aiPnlSigned(closedPnl, 2)}`, `笔数${t.closed_count || 0}(胜${t.win_count || 0}/负${t.loss_count || 0})`, - `浮盈亏${fmt(Number(t.float_pnl_u), 2)}U`, + `浮盈亏${aiPnlSigned(floatPnl, 2)}`, ].join(""); } - function renderAiChatMessages(session) { + function renderAiChatRow(role, content, extraClass) { + const isUser = role === "user"; + const label = isUser ? "主人" : "AI教练"; + const rowCls = isUser ? "ai-msg-row-user" : "ai-msg-row-coach"; + const bubbleCls = isUser ? "ai-bubble-user" : "ai-bubble-assistant"; + return ( + `
` + + `${label}` + + `
${esc(content || "")}
` + + `
` + ); + } + + function renderAiChatMessages(session, opts) { + const options = opts || {}; const box = document.getElementById("ai-chat-messages"); const title = document.getElementById("ai-chat-title"); if (!box) return; @@ -2979,20 +3010,34 @@ if (title) { title.textContent = session && session.title ? `聊天 · ${session.title}` : "聊天"; } - if (!msgs.length) { + const showPlaceholder = + !msgs.length && !options.pendingUser && !options.thinking; + if (showPlaceholder) { box.innerHTML = - '

随便聊:行情、心态、纪律、执行都行。我会先看四户监控数据,用搭档口吻回你。

'; + '

主人发消息会立刻出现在右侧;AI教练 会先显示「正在思考…」再回复。

'; return; } - box.innerHTML = msgs - .map((m) => { - const role = m.role === "user" ? "user" : "assistant"; - return `
${esc(m.content || "")}
`; - }) + let html = msgs + .map((m) => renderAiChatRow(m.role === "user" ? "user" : "assistant", m.content || "")) .join(""); + if (options.pendingUser) { + html += renderAiChatRow("user", options.pendingUser); + } + if (options.thinking) { + html += renderAiChatRow("assistant", "正在思考…", "ai-bubble-thinking"); + } + box.innerHTML = html; box.scrollTop = box.scrollHeight; } + function setAiChatBusy(busy) { + aiChatLoading = !!busy; + const btn = document.getElementById("btn-ai-chat-send"); + const input = document.getElementById("ai-chat-input"); + if (btn) btn.disabled = busy; + if (input) input.disabled = busy; + } + async function loadAiMeta() { const r = await apiFetch("/api/ai/meta"); aiMeta = await r.json(); @@ -3026,7 +3071,8 @@ async function loadAiChatSession() { const r = await apiFetch("/api/ai/chat/session"); const j = await r.json(); - renderAiChatMessages(j.session); + aiChatSessionCache = j.session || null; + renderAiChatMessages(aiChatSessionCache); } async function loadAiPage() { @@ -3074,7 +3120,8 @@ body: JSON.stringify({}), }); const j = await r.json(); - renderAiChatMessages(j.session); + aiChatSessionCache = j.session || null; + renderAiChatMessages(aiChatSessionCache); showToast("已开始新对话"); } catch (e) { showToast(String(e), true); @@ -3087,9 +3134,9 @@ const input = document.getElementById("ai-chat-input"); const text = (input && input.value || "").trim(); if (!text) return; - aiChatLoading = true; - const btn = document.getElementById("btn-ai-chat-send"); - if (btn) btn.disabled = true; + if (input) input.value = ""; + setAiChatBusy(true); + renderAiChatMessages(aiChatSessionCache, { pendingUser: text, thinking: true }); try { const r = await apiFetch("/api/ai/chat/send", { method: "POST", @@ -3098,14 +3145,13 @@ }); const j = await r.json(); if (!r.ok) throw new Error(j.detail || j.msg || "发送失败"); - if (j.detail) throw new Error(j.detail); - if (input) input.value = ""; - renderAiChatMessages(j.session); + aiChatSessionCache = j.session || null; + renderAiChatMessages(aiChatSessionCache); } catch (e) { showToast(String(e), true); + renderAiChatMessages(aiChatSessionCache); } finally { - aiChatLoading = false; - if (btn) btn.disabled = false; + setAiChatBusy(false); } } diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 8cb6c31..cd70695 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -15,7 +15,7 @@ - + @@ -214,12 +214,12 @@

今日总结

- +
-
+

点击「生成今日总结」聚合四户平仓与持仓数据(未启用账户显示「未监控」)。

@@ -227,11 +227,11 @@

聊天

- +
-
+
@@ -287,6 +287,6 @@
- +