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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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],
|
||||
*,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -90,6 +90,35 @@
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderRemarkLines(ac) {
|
||||
const lines = Array.isArray(ac && ac.remark_lines) ? ac.remark_lines : [];
|
||||
if (!lines.length) {
|
||||
const fallback = esc((ac && ac.remark) || "—");
|
||||
return `<div class="dash-ac-remark"><div class="dash-ac-remark-line">${fallback}</div></div>`;
|
||||
}
|
||||
return `<div class="dash-ac-remark">${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 (
|
||||
`<div class="dash-ac-remark-line dash-ac-remark-pos">` +
|
||||
`${text} 浮<span class="${pnlClass(pnl)}">${pnlSigned(pnl, 2)}</span>` +
|
||||
`</div>`
|
||||
);
|
||||
}
|
||||
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 `<div class="${cls}">${text}</div>`;
|
||||
})
|
||||
.join("")}</div>`;
|
||||
}
|
||||
|
||||
function renderAccounts(accounts, threshold) {
|
||||
if (!elAccounts) return;
|
||||
const rows = Array.isArray(accounts) ? accounts : [];
|
||||
@@ -129,7 +158,7 @@
|
||||
<div class="dash-ac-metric"><span>浮盈亏</span><strong class="${pnlClass(floatPnl)}">${pnlSigned(floatPnl, 2)}</strong></div>
|
||||
</div>
|
||||
${lossBar}
|
||||
<div class="dash-ac-remark">${esc(ac.remark || "—")}</div>
|
||||
${renderRemarkLines(ac)}
|
||||
</article>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260611-hub-ai-mobile" />
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260611-hub-dash-sse" />
|
||||
<link rel="stylesheet" href="/assets/app.css?v=20260611-hub-ai-tabs" />
|
||||
<link rel="stylesheet" href="/assets/dashboard.css?v=20260611-dash-remark-lines" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -444,8 +444,10 @@
|
||||
<p class="page-desc">交易教练 / 普通聊天 · 右侧可回看历史会话</p>
|
||||
</div>
|
||||
<div class="ai-mobile-tabs" role="tablist" aria-label="AI 教练视图">
|
||||
<button type="button" class="ai-mobile-tab is-active" data-ai-tab="chat" role="tab" aria-selected="true">聊天</button>
|
||||
<button type="button" class="ai-mobile-tab is-active" data-ai-tab="trading" role="tab" aria-selected="true">交易教练</button>
|
||||
<button type="button" class="ai-mobile-tab" data-ai-tab="general" role="tab" aria-selected="false">普通聊天</button>
|
||||
<button type="button" class="ai-mobile-tab" data-ai-tab="history" role="tab" aria-selected="false">历史</button>
|
||||
<button type="button" class="ai-mobile-tab ai-mobile-tab-action" data-ai-tab="new" role="tab" aria-selected="false" title="新开对话">新开</button>
|
||||
</div>
|
||||
<div class="ai-layout" data-ai-mobile-tab="chat">
|
||||
<section class="ai-panel ai-chat-panel" data-ai-panel="chat">
|
||||
@@ -553,8 +555,8 @@
|
||||
<script src="/assets/chart.js?v=20260609-market-day-split"></script>
|
||||
<script src="/assets/archive.js?v=20260608-hub-archive-history"></script>
|
||||
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
||||
<script src="/assets/dashboard.js?v=20260611-hub-dash-sse"></script>
|
||||
<script src="/assets/dashboard.js?v=20260611-dash-remark-lines"></script>
|
||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||
<script src="/assets/app.js?v=20260611-hub-ai-mobile"></script>
|
||||
<script src="/assets/app.js?v=20260611-hub-ai-tabs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user