From 1042fdeef3c703b974c217bb1a5592b84f5adff1 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 11 Jun 2026 11:16:28 +0800 Subject: [PATCH] feat(hub): mobile AI tabs and dashboard position lines Mobile AI coach uses four top tabs (trading, general, history, new) with single-panel view and wider desktop history. Dashboard account cards show key levels and positions one per line with colored float PnL. Co-authored-by: Cursor --- manual_trading_hub/hub_ai/context.py | 37 +++++++++++++++ manual_trading_hub/hub_dashboard.py | 2 + manual_trading_hub/static/app.css | 51 ++++++++++++--------- manual_trading_hub/static/app.js | 60 ++++++++++++++++++++----- manual_trading_hub/static/dashboard.css | 31 +++++++++++++ manual_trading_hub/static/dashboard.js | 31 ++++++++++++- manual_trading_hub/static/index.html | 12 ++--- 7 files changed, 185 insertions(+), 39 deletions(-) diff --git a/manual_trading_hub/hub_ai/context.py b/manual_trading_hub/hub_ai/context.py index 3877b07..1ddd343 100644 --- a/manual_trading_hub/hub_ai/context.py +++ b/manual_trading_hub/hub_ai/context.py @@ -652,6 +652,43 @@ def format_account_remark(ac: dict) -> str: return ";".join(parts) +def format_dashboard_account_lines(ac: dict) -> list[dict[str, Any]]: + """数据看板分户卡片:监控与持仓逐行展示(含浮盈亏数值供前端着色)。""" + lines: list[dict[str, Any]] = [] + mon = ac.get("monitor_lines") or {} + for row in (mon.get("keys") or [])[:3]: + if row: + lines.append({"kind": "monitor", "text": str(row)}) + for row in (mon.get("orders") or [])[:3]: + if row: + lines.append({"kind": "monitor", "text": str(row)}) + for row in (mon.get("trends") or [])[:2]: + if row: + lines.append({"kind": "monitor", "text": str(row)}) + for row in (mon.get("rolls") or [])[:2]: + if row: + lines.append({"kind": "monitor", "text": str(row)}) + for p in _filter_open_positions(ac.get("positions") or []): + sym = p.get("symbol") or "?" + side = p.get("side") or "?" + upnl = _position_float_pnl(p) + lines.append( + { + "kind": "position", + "text": f"{sym} {side}", + "pnl": round(upnl, 4), + } + ) + if not lines: + issues = ac.get("issues") or [] + if issues: + for iss in issues[:3]: + lines.append({"kind": "issue", "text": str(iss)}) + else: + lines.append({"kind": "empty", "text": "无"}) + return lines + + def collect_closed_trades_snapshot( accounts: list[dict], *, diff --git a/manual_trading_hub/hub_dashboard.py b/manual_trading_hub/hub_dashboard.py index 220c775..638dba7 100644 --- a/manual_trading_hub/hub_dashboard.py +++ b/manual_trading_hub/hub_dashboard.py @@ -8,6 +8,7 @@ from hub_ai.context import ( build_daily_context, collect_closed_trades_snapshot, format_account_remark, + format_dashboard_account_lines, ) from hub_ai.config import trading_day_reset_hour from hub_trades_lib import current_trading_day @@ -63,6 +64,7 @@ def _enrich_account_row(ac: dict) -> dict: "float_pnl_u": ac.get("float_pnl_u"), "open_position_count": ac.get("open_position_count"), "remark": format_account_remark(ac), + "remark_lines": format_dashboard_account_lines(ac), "issues": ac.get("issues") or [], "daily_loss_pct": loss_pct, "loss_alert": loss_alert, diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 8a7734e..e892744 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -3914,25 +3914,32 @@ body.hub-page-ai #page-ai { } body.hub-page-ai .ai-mobile-tabs { - display: flex; - gap: 8px; - margin-bottom: 8px; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 6px; + margin-bottom: 6px; flex-shrink: 0; width: 100%; } body.hub-page-ai .ai-mobile-tab { - flex: 1; - min-height: 40px; - padding: 8px 12px; + min-height: 38px; + padding: 6px 4px; border-radius: 8px; border: 1px solid var(--border-soft); background: var(--inset-surface); color: var(--muted); font-family: var(--font); - font-size: 0.82rem; + font-size: 0.7rem; font-weight: 600; cursor: pointer; + line-height: 1.2; + text-align: center; + } + + body.hub-page-ai .ai-mobile-tab-action { + color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 35%, var(--border-soft)); } body.hub-page-ai .ai-mobile-tab.is-active { @@ -3942,7 +3949,7 @@ body.hub-page-ai #page-ai { box-shadow: none; } - body.hub-page-ai #page-ai .page-desc { + body.hub-page-ai #page-ai .page-head { display: none; } @@ -3957,7 +3964,8 @@ body.hub-page-ai #page-ai { overflow: hidden; } - body.hub-page-ai .ai-layout[data-ai-mobile-tab="chat"] .ai-chat-panel, + body.hub-page-ai .ai-layout[data-ai-mobile-tab="trading"] .ai-chat-panel, + body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-panel, body.hub-page-ai .ai-layout[data-ai-mobile-tab="history"] .ai-chat-panel { display: flex; flex: 1 1 auto; @@ -3967,10 +3975,19 @@ body.hub-page-ai #page-ai { min-width: 0; } - body.hub-page-ai .ai-layout[data-ai-mobile-tab="chat"] .ai-chat-history-panel { + body.hub-page-ai .ai-layout[data-ai-mobile-tab="trading"] .ai-chat-history-panel, + body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-history-panel { display: none !important; } + body.hub-page-ai .ai-layout[data-ai-mobile-tab="trading"] .ai-chat-main, + body.hub-page-ai .ai-layout[data-ai-mobile-tab="general"] .ai-chat-main { + display: flex; + flex: 1 1 auto; + min-height: 0; + flex-direction: column; + } + body.hub-page-ai .ai-layout[data-ai-mobile-tab="history"] .ai-chat-main, body.hub-page-ai .ai-layout[data-ai-mobile-tab="history"] .ai-chat-topbar { display: none !important; @@ -4003,7 +4020,7 @@ body.hub-page-ai #page-ai { } body.hub-page-ai .ai-chat-topbar { - gap: 6px; + display: none; } body.hub-page-ai .ai-bot-tab { @@ -4039,15 +4056,7 @@ body.hub-page-ai #page-ai { } body.hub-page-ai .ai-chat-session-head { - margin: 0; - padding: 0 2px 4px; - } - - body.hub-page-ai .ai-chat-session-head h2 { - font-size: 0.78rem; - font-weight: 600; - color: var(--muted); - letter-spacing: 0.02em; + display: none; } body.hub-page-ai .ai-chat-messages { @@ -4470,7 +4479,7 @@ body.hub-page-ai #page-ai { flex: 1 1 auto; min-height: 0; display: grid; - grid-template-columns: minmax(0, 1fr) minmax(200px, 260px); + grid-template-columns: minmax(0, 1fr) minmax(300px, 380px); gap: 0; overflow: hidden; border: 1px solid var(--border-soft); diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js index 8665316..0ef0471 100644 --- a/manual_trading_hub/static/app.js +++ b/manual_trading_hub/static/app.js @@ -1038,22 +1038,43 @@ } const AI_MOBILE_TAB_KEY = "hub_ai_mobile_tab"; + const AI_MOBILE_CHAT_TABS = new Set(["trading", "general"]); + + function normalizeAiMobileTab(tab) { + const raw = (tab || "").trim().toLowerCase(); + if (raw === "chat") return "trading"; + if (AI_MOBILE_CHAT_TABS.has(raw) || raw === "history") return raw; + return "trading"; + } function applyAiMobileTab(tab) { const layout = document.querySelector(".ai-layout"); const tabs = document.querySelectorAll(".ai-mobile-tab"); if (!layout) return; const mobile = isMobileLayout(); - const active = mobile ? tab || localStorage.getItem(AI_MOBILE_TAB_KEY) || "chat" : "both"; - if (mobile) layout.dataset.aiMobileTab = active; - else delete layout.dataset.aiMobileTab; + if (!mobile) { + delete layout.dataset.aiMobileTab; + tabs.forEach((btn) => { + btn.classList.remove("is-active"); + btn.setAttribute("aria-selected", "false"); + }); + return; + } + const active = normalizeAiMobileTab( + tab || localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading" + ); + layout.dataset.aiMobileTab = active; tabs.forEach((btn) => { - const on = mobile && btn.dataset.aiTab === active; + const t = btn.dataset.aiTab || ""; + const on = t === active; btn.classList.toggle("is-active", on); btn.setAttribute("aria-selected", on ? "true" : "false"); }); - if (mobile && active === "chat") scrollAiChatToEnd(); - if (mobile && active === "history") { + if (AI_MOBILE_CHAT_TABS.has(active)) { + updateAiBotTabs(active); + scrollAiChatToEnd(); + } + if (active === "history") { const hist = document.getElementById("ai-chat-history-list"); if (hist) hist.scrollTop = 0; } @@ -1064,10 +1085,16 @@ if (!tabs.length) return; tabs.forEach((btn) => { btn.addEventListener("click", () => { - const tab = btn.dataset.aiTab || "chat"; + const tab = btn.dataset.aiTab || "trading"; + if (tab === "new") { + const prev = normalizeAiMobileTab(localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading"); + const botMode = prev === "general" ? "general" : "trading"; + void newAiChat(botMode); + return; + } localStorage.setItem(AI_MOBILE_TAB_KEY, tab); applyAiMobileTab(tab); - if (tab === "chat") { + if (AI_MOBILE_CHAT_TABS.has(tab)) { const input = document.getElementById("ai-chat-input"); if (input && isMobileLayout()) input.focus(); } @@ -3388,8 +3415,13 @@ aiChatSessionsCache = j.sessions || []; renderAiChatMessages(aiChatSessionCache); renderAiChatHistory(aiChatSessionsCache); - updateAiBotTabs((aiChatSessionCache && aiChatSessionCache.bot_mode) || "trading"); - applyAiMobileTab("chat"); + const mode = + (aiChatSessionCache && aiChatSessionCache.bot_mode) === "general" ? "general" : "trading"; + updateAiBotTabs(mode); + if (isMobileLayout()) { + localStorage.setItem(AI_MOBILE_TAB_KEY, mode); + applyAiMobileTab(mode); + } scrollAiChatToEnd(); } catch (e) { showToast(String(e), true); @@ -3421,7 +3453,8 @@ async function loadAiPage() { applyAiMobileTab(); await loadAiChatSession(); - if (isMobileLayout() && (localStorage.getItem(AI_MOBILE_TAB_KEY) || "chat") === "chat") { + const mobTab = normalizeAiMobileTab(localStorage.getItem(AI_MOBILE_TAB_KEY) || "trading"); + if (isMobileLayout() && AI_MOBILE_CHAT_TABS.has(mobTab)) { const input = document.getElementById("ai-chat-input"); if (input && !aiChatLoading) { setTimeout(() => input.focus(), 80); @@ -3443,7 +3476,10 @@ renderAiChatMessages(aiChatSessionCache); renderAiChatHistory(aiChatSessionsCache); updateAiBotTabs(mode); - applyAiMobileTab("chat"); + if (isMobileLayout()) { + localStorage.setItem(AI_MOBILE_TAB_KEY, mode); + applyAiMobileTab(mode); + } showToast(mode === "general" ? "已开始普通聊天" : "已开始交易教练对话"); } catch (e) { showToast(String(e), true); diff --git a/manual_trading_hub/static/dashboard.css b/manual_trading_hub/static/dashboard.css index 07c107d..dd1736a 100644 --- a/manual_trading_hub/static/dashboard.css +++ b/manual_trading_hub/static/dashboard.css @@ -317,6 +317,37 @@ body.hub-page-dashboard .page#page-dashboard { color: var(--dash-muted); line-height: 1.4; word-break: break-word; + display: flex; + flex-direction: column; + gap: 3px; +} + +.dash-ac-remark-line { + margin: 0; + padding: 3px 0; + border-top: 1px solid color-mix(in srgb, var(--dash-card-border) 65%, transparent); +} + +.dash-ac-remark-line:first-child { + border-top: none; + padding-top: 0; +} + +.dash-ac-remark-mon { + color: var(--dash-muted); +} + +.dash-ac-remark-pos { + color: var(--dash-text); +} + +.dash-ac-remark-pos .pos, +.dash-ac-remark-pos .neg { + font-weight: 600; +} + +.dash-ac-remark-issue { + color: var(--dash-warn); } .dash-table-wrap { diff --git a/manual_trading_hub/static/dashboard.js b/manual_trading_hub/static/dashboard.js index 8e6c897..f8001e6 100644 --- a/manual_trading_hub/static/dashboard.js +++ b/manual_trading_hub/static/dashboard.js @@ -90,6 +90,35 @@ `; } + function renderRemarkLines(ac) { + const lines = Array.isArray(ac && ac.remark_lines) ? ac.remark_lines : []; + if (!lines.length) { + const fallback = esc((ac && ac.remark) || "—"); + return `
${fallback}
`; + } + return `
${lines + .map((ln) => { + const kind = ln && ln.kind; + const text = esc((ln && ln.text) || ""); + if (kind === "position" && ln.pnl != null && Number.isFinite(Number(ln.pnl))) { + const pnl = Number(ln.pnl); + return ( + `
` + + `${text} 浮${pnlSigned(pnl, 2)}` + + `
` + ); + } + const cls = + kind === "monitor" + ? "dash-ac-remark-line dash-ac-remark-mon" + : kind === "issue" + ? "dash-ac-remark-line dash-ac-remark-issue" + : "dash-ac-remark-line"; + return `
${text}
`; + }) + .join("")}
`; + } + function renderAccounts(accounts, threshold) { if (!elAccounts) return; const rows = Array.isArray(accounts) ? accounts : []; @@ -129,7 +158,7 @@
浮盈亏${pnlSigned(floatPnl, 2)}
${lossBar} -
${esc(ac.remark || "—")}
+ ${renderRemarkLines(ac)} `; }) .join(""); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 3ca08fd..1e5741a 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -15,8 +15,8 @@ - - + + @@ -444,8 +444,10 @@

交易教练 / 普通聊天 · 右侧可回看历史会话

- + + +
@@ -553,8 +555,8 @@ - + - +