From d0d8150a020ac9b804e094c7b60567eb42c8005f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=BD=E5=B7=A5=E5=91=98?= <4846a27a-ba87-49c9-939d-7275f63efcc7@agents.multica.local> Date: Mon, 24 Aug 2026 23:54:19 +0800 Subject: [PATCH] =?UTF-8?q?fix(HEL-104):=20=E4=BF=AE=E5=A4=8D=E7=AC=AC?= =?UTF-8?q?=E5=9B=9B=E6=89=B9P3a=E5=85=AB=E5=A4=84=E8=BF=94=E5=B7=A5?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=BC=95=E5=AF=BC=E8=83=B6=E5=9B=8A=E5=85=A5?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E5=8C=BA/=E5=AF=BC=E5=B8=88=E8=83=B6?= =?UTF-8?q?=E5=9B=8A=E7=BD=AE=E9=A1=B6/=E9=97=AE=E5=B8=88=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E4=B8=8E=E7=A9=BA=E6=80=81/=E9=80=89=E8=82=A1?= =?UTF-8?q?=E4=B8=89=E8=A7=86=E5=9B=BE=E6=8C=89=E7=AD=96=E7=95=A5=E8=BF=87?= =?UTF-8?q?=E6=BB=A4/=E5=85=A5=E9=80=89=E6=97=A5=E5=88=97/=E7=A9=BA?= =?UTF-8?q?=E6=80=81=E5=88=87=E7=AD=96=E7=95=A5=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: multica-agent --- frontend/m/css/features.css | 28 +++-- frontend/m/js/pages.js | 198 +++++++++++++++++++++++++----------- 2 files changed, 161 insertions(+), 65 deletions(-) diff --git a/frontend/m/css/features.css b/frontend/m/css/features.css index 9c979d0..921cd59 100644 --- a/frontend/m/css/features.css +++ b/frontend/m/css/features.css @@ -2172,17 +2172,19 @@ padding: 12px 12px 4px; display: flex; flex-direction: column; - gap: 4px; + gap: 0; } .m-chat-msg { display: flex; max-width: 82%; - margin-top: 8px; + margin-top: 12px; word-break: break-word; white-space: pre-wrap; } .m-chat-msg:first-child { margin-top: 12px; } +.m-chat-msg--user + .m-chat-msg--user, +.m-chat-msg--ai + .m-chat-msg--ai { margin-top: 4px; } .m-chat-msg--user { align-self: flex-end; } .m-chat-msg--ai { align-self: flex-start; } @@ -2304,10 +2306,11 @@ 30% { transform: translateY(-3px); opacity: 1; } } -/* 引导问题 / 继续追问胶囊 */ +/* 引导问题胶囊(空态时渲染在消息区内,横滑) */ .m-chat-presets { flex: 0 0 auto; - padding: 4px 12px 0; + width: 100%; + margin-top: 8px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; @@ -2630,17 +2633,30 @@ color: var(--text-primary); } .m-screener-drawer-row:active { background: var(--surface-hover); } +.m-screener-drawer-main { + flex: 1 1 auto; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; +} .m-screener-drawer-name { font-size: 14px; font-weight: 600; - flex: 1 1 auto; - display: inline-flex; + display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.m-screener-drawer-sub { + font-size: 11.5px; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} .m-strategy-status { font-size: 12px; color: var(--text-primary); } .m-strategy-status--warn { color: var(--warning); } .m-strategy-status--muted { color: var(--text-secondary); } diff --git a/frontend/m/js/pages.js b/frontend/m/js/pages.js index 102b380..cc1d53e 100644 --- a/frontend/m/js/pages.js +++ b/frontend/m/js/pages.js @@ -1620,16 +1620,8 @@ history: "入选历史" }; - function screenerViewKey(payload, view) { - if (!payload) return []; - if (view === "active") return payload.active_signals || []; - if (view === "history") return payload.candidate_history || []; - const latest = payload.latest_results || {}; - const strategyLibrary = screenerCurrentLibrary(payload); - const mode = strategyLibrary || "smart"; - const run = latest[mode] || latest.smart || latest.curated || null; - if (!run) return []; - return run.candidates || []; + function screenerCurrentStrategy(payload) { + return findScreenerStrategy(payload, state.screener.strategyId); } function screenerCurrentLibrary(payload) { @@ -1639,13 +1631,71 @@ return (s.formula && s.formula.meta && s.formula.meta.library) || "smart"; } + function screenerLibraryMode(library) { + if (library === "curated") return "curated"; + if (library === "quant" || library === "custom") return "quant"; + return "smart"; + } + + // 按当前策略名在 recent_results / latest_results 中匹配最新一次运行(对齐电脑端 activeScreenerResultEntry) + function screenerRunForCurrentStrategy(payload) { + if (!payload) return null; + const strategy = screenerCurrentStrategy(payload); + if (!strategy || !strategy.name) return null; + const name = strategy.name; + const recent = (payload.recent_results || []).slice().reverse(); + for (let i = 0; i < recent.length; i += 1) { + const run = recent[i]; + if (run && run.meta && run.meta.strategy_name === name) return run; + } + const latest = (payload.latest_results || {})[screenerLibraryMode(screenerCurrentLibrary(payload))]; + if (latest && latest.meta && latest.meta.strategy_name === name) return latest; + return null; + } + + // 持续有效 / 入选历史:对齐电脑端 selectedScreenerArchiveRows,按当前策略过滤 active_signals/candidate_history 组装行 + function screenerArchiveRows(payload, view) { + if (!payload) return []; + const source = view === "active" ? (payload.active_signals || []) : (payload.candidate_history || []); + const strategy = screenerCurrentStrategy(payload); + const strategyName = strategy ? strategy.name : ""; + const mode = screenerLibraryMode(screenerCurrentLibrary(payload)); + return source.filter(function (row) { + if ((row.mode || "smart") !== mode) return false; + return (row.hits || []).some(function (hit) { return hit.strategy_name === strategyName; }); + }).map(function (row) { + let hits = (row.hits || []).filter(function (hit) { return hit.strategy_name === strategyName; }); + if (view === "active") hits = hits.filter(function (hit) { return hit.active; }); + const latestHit = hits.slice().sort(function (a, b) { + return String(b.selection_date || "").localeCompare(String(a.selection_date || "")); + })[0] || {}; + return { + code: row.code, + name: row.name, + sector: row.sector, + pct_chg: row.pct_chg, + return_5d: row.return_5d, + score_display: latestHit.score_display != null ? latestHit.score_display : row.score_display, + selection_date: displayCompactDate(latestHit.selection_date || row.selection_date || ""), + status: view === "active" ? "持续有效" : "已入选" + }; + }); + } + + function screenerViewKey(payload, view) { + if (!payload) return []; + if (view === "active") return screenerArchiveRows(payload, "active"); + if (view === "history") return screenerArchiveRows(payload, "history"); + const run = screenerRunForCurrentStrategy(payload); + if (!run) return []; + return run.candidates || []; + } + function screenerCurrentRun(payload) { if (!payload) return null; if (state.screener.view === "active") return null; if (state.screener.view === "history") return null; - const latest = payload.latest_results || {}; - const mode = screenerCurrentLibrary(payload); - return latest[mode] || latest.smart || latest.curated || null; + return screenerRunForCurrentStrategy(payload); } function screenerCurrentRunId() { @@ -1810,6 +1860,16 @@ return "暂无信号"; } + function screenerEmptyText(payload) { + if (state.screener.view === "active") return "该策略暂无持续有效信号"; + if (state.screener.view === "history") return "该策略暂无入选历史"; + const status = screenerStrategyStatusText(payload); + if (status === "数据不足") return "该策略数据不足,暂无法出候选"; + if (status === "等待盘后") return "该策略等待盘后更新"; + if (status === "选择策略") return "请先选择一个策略查看结果"; + return "该策略当日暂无信号"; + } + function formatScreenerUpdatedAt(value) { if (!value) return "盘后已更新"; const text = String(value); @@ -1829,8 +1889,9 @@ if (!rows.length) { wrap.innerHTML = '
' + '' + icon("inbox", 26) + '' + - '

当前策略当日暂无信号

' + - '可点上方策略行切换其他策略' + + '

' + escapeHtml(screenerEmptyText(payload)) + '

' + + '可切换其他策略查看结果' + + '' + '
'; return; } @@ -1838,15 +1899,20 @@ const followed = screenerFollowedCodes(); rows.forEach(function (r) { r.tracked = followed[String(r.code)] ? true : false; }); + const primaryColumns = []; + if (state.screener.view === "history") { + primaryColumns.push({ key: "selection_date", label: "入选日", type: "text", width: 76 }); + } + primaryColumns.push( + { key: "score_display", label: "综合分", type: "score" }, + { key: "pct_chg", label: "涨幅%", type: "change" }, + { key: "sector", label: "板块", type: "text" } + ); const cols = { frozenColumns: [ { key: "stock", label: "股票", type: "stock", width: 96 } ], - primaryColumns: [ - { key: "score_display", label: "综合分", type: "score" }, - { key: "pct_chg", label: "涨幅%", type: "change" }, - { key: "sector", label: "板块", type: "text" } - ], + primaryColumns: primaryColumns, scrollColumns: [ { key: "historical_probability", label: "历史胜率%", type: "rate" }, { key: "return_5d", label: "5日%", type: "change" }, @@ -1941,6 +2007,20 @@ }); } + function screenerStrategyRegimeText(s) { + const regimes = (s && s.regimes) || []; + if (!regimes.length) return ""; + const all = (state.screener.data && state.screener.data.regimes) || []; + const labels = []; + regimes.forEach(function (id) { + const r = all.find(function (x) { return x.id === id; }); + labels.push(r ? r.label : id); + }); + if (!labels.length) return ""; + if (all.length > 0 && labels.length >= all.length) return "全阶段"; + return labels.join(" / "); + } + function screenerStrategyStatusTextFor(s, payload) { if (s.published_run) { const pub = s.published_run; @@ -1972,8 +2052,12 @@ const isCurrent = String(s.id) === String(state.screener.strategyId); const status = screenerStrategyStatusTextFor(s, state.screener.data); const statusCls = status === "数据不足" ? 'm-strategy-status--warn' : (status === "暂无信号" || status === "等待盘后" ? 'm-strategy-status--muted' : ''); + const regimeText = screenerStrategyRegimeText(s); return ''; }).join("") || '

无匹配策略

'; @@ -2151,6 +2235,23 @@ const runId = screenerCurrentRunId(); const followed = screenerFollowedCodes()[String(code)]; const isTracked = Boolean(followed); + const contributions = Array.isArray(row.contributions) ? row.contributions : []; + const riskFlags = Array.isArray(row.risk_flags) ? row.risk_flags : []; + const contributionGroup = (row.reason || contributions.length) + ? appendDetailGroup("主要贡献", contributions.length + ? contributions.map(function (c) { + return { + label: c.label || "", + value: (c.value != null && c.value !== "" ? escapeHtml(String(c.value)) : "--") + (c.points != null ? "(+" + formatNumber(Number(c.points), 1) + "分)" : "") + }; + }) + : [{ label: "", value: '' + escapeHtml(row.reason || "") + '' }]) + : ""; + const riskGroup = riskFlags.length + ? appendDetailGroup("风险标记", riskFlags.map(function (f) { + return { label: "", value: '' + escapeHtml(f) + '' }; + })) + : ""; const html = appendDetailGroup("选股信息", [ { label: "综合分", value: formatNumber(Number(row.score_display || 0), 1), cls: Number(row.score_display || 0) >= 80 ? "up" : "" }, @@ -2158,7 +2259,8 @@ { label: "历史胜率", value: row.historical_probability != null ? formatNumber(Number(row.historical_probability), 1) + "%" : "--" }, { label: "板块", value: escapeHtml(row.sector || "--") } ]) + - (row.reason ? appendDetailGroup("主要贡献", [{ label: "", value: '' + escapeHtml(row.reason) + '' }]) : '') + + contributionGroup + + riskGroup + '
' + (isTracked ? '' @@ -2328,8 +2430,8 @@ aborter: null }; const isAssistant = key === "assistant/chat"; - // 聊天页是底栏直达:不要 back 按钮;m-actions 由本函数自己控制 - global.MobileRouter.updateHeader({ title: isAssistant ? "复盘助手" : "问师", back: false, actions: "" }); + // 复盘助手是底栏直达(无 back);问师从智能工具图标页进入(有 back);m-actions 由本函数自己控制 + global.MobileRouter.updateHeader({ title: isAssistant ? "复盘助手" : "问师", back: !isAssistant, actions: "" }); document.getElementById("m-view").innerHTML = buildChatShell(key); const actions = []; if (!isAssistant) { @@ -2343,9 +2445,8 @@ function buildChatShell(key) { const isAssistant = key === "assistant/chat"; return '
' + - '
' + chatSkeletonHtml() + '
' + (isAssistant ? '' : '
') + - '
' + + '
' + chatSkeletonHtml() + '
' + '
' + '
' + '' + @@ -2429,7 +2530,6 @@ }); state.chat.historyLoaded = true; renderChatStream(); - renderChatPresets(); }).catch(function (error) { if (seq !== state.seq) return; renderChatStreamError(error && error.message ? error.message : "对话历史加载失败"); @@ -2444,12 +2544,9 @@ global.MobileAPI.request("/api/mentors/setup?trade_date=" + encodeURIComponent(todayString())).then(function (payload) { if (seq !== state.seq) return; state.chat.mentorSetup = payload; - // 没有选定导师时,从偏好或首位开始 + // 无已选导师时保持空态(spec §1.5):不自动选中导师,等用户点「选择导师」或顶栏 bot 开抽屉 if (!state.chat.mentorId) { - const mentors = (payload && payload.mentors) || []; - if (mentors.length) { - selectMentor(mentors[0].id, { silent: true }); - } + // 空态,等待选择 } else { // 已选导师,更新名称/标签 const cur = ((payload && payload.mentors) || []).find(function (m) { return m.id === state.chat.mentorId; }); @@ -2463,7 +2560,6 @@ } renderChatMentorBar(); renderChatStream(); - renderChatPresets(); }).catch(function (error) { if (seq !== state.seq) return; renderChatStreamError(error && error.message ? error.message : "导师目录加载失败"); @@ -2484,7 +2580,6 @@ }); state.chat.historyLoaded = true; renderChatStream(); - renderChatPresets(); }).catch(function (error) { if (seq !== state.seq) return; renderChatStreamError(error && error.message ? error.message : "对话历史加载失败"); @@ -2510,7 +2605,6 @@ if (typeof closeSheet === "function") closeSheet(); renderChatMentorBar(); renderChatStream(); - renderChatPresets(); loadMentorHistory(); } @@ -2519,7 +2613,6 @@ state.chat.messages = []; state.chat.followUps = []; renderChatStream(); - renderChatPresets(); showToast("已清空对话"); }).catch(function (err) { showToast((err && err.message) || "清空失败"); @@ -2532,7 +2625,6 @@ state.chat.messages = []; state.chat.followUps = []; renderChatStream(); - renderChatPresets(); showToast("已清空当日对话"); }).catch(function (err) { showToast((err && err.message) || "清空失败"); @@ -2558,7 +2650,8 @@ if (isAssistant) { html = chatBubbleHtml("ai", "我是你的复盘助手,可以问我市场位置、主线、交易复盘、明日清单。") + - '
从下面挑一个开始,或直接输入你的问题
'; + '
从下面挑一个开始,或直接输入你的问题
' + + chatPresetPillsHtml(); } else if (state.chat.mentorId) { // 显示导师 tagline 原文(spec §1.5) const tagline = state.chat.mentorTagline || "今天想问点什么?"; @@ -2664,8 +2757,9 @@ function renderChatMentorBar() { const bar = document.getElementById("m-chat-mentor-bar"); if (!bar) return; - if (state.chat.page === "assistant/chat") { bar.innerHTML = ""; return; } - if (!state.chat.mentorId) { bar.innerHTML = ""; return; } + if (state.chat.page === "assistant/chat") { bar.innerHTML = ""; bar.style.display = "none"; return; } + if (!state.chat.mentorId) { bar.innerHTML = ""; bar.style.display = "none"; return; } + bar.style.display = "flex"; const grade = state.chat.mentorGrade || ""; const gradeBadge = grade ? '' + escapeHtml(grade) + '' : ""; bar.innerHTML = ''; } - function renderChatPresets() { - const wrap = document.getElementById("m-chat-presets"); - if (!wrap) return; - const isAssistant = state.chat.page === "assistant/chat"; - const hasHistory = (state.chat.messages || []).length > 0; - if (isAssistant) { - // 复盘助手:仅在空会话时显示引导问题 - if (hasHistory || state.chat.streaming) { wrap.innerHTML = ""; return; } - wrap.innerHTML = '
' + - ASSISTANT_PRESET_QUESTIONS.map(function (q) { - return ''; - }).join("") + '
'; - } else { - // 问师:无引导问题 - wrap.innerHTML = ""; - } + function chatPresetPillsHtml() { + return '
' + + '
' + + ASSISTANT_PRESET_QUESTIONS.map(function (q) { + return ''; + }).join("") + '
'; } function renderChatFollowUps() { @@ -2734,7 +2818,6 @@ const input = document.getElementById("m-chat-input"); if (input) { input.value = ""; autoSizeChatInput(); } renderChatStream(); - renderChatPresets(); startChatStream(text); } @@ -2805,7 +2888,6 @@ state.chat.streamBubble = null; syncChatSendEnabled(); renderChatStream(); - renderChatPresets(); } function handleChatStreamError(err, aborted) { @@ -2820,7 +2902,6 @@ state.chat.streamBuffer = ""; syncChatSendEnabled(); renderChatStream(); - renderChatPresets(); return; } state.chat.streaming = false; @@ -3660,7 +3741,6 @@ state.chat.messages = []; renderChatStream(); renderChatMentorBar(); - renderChatPresets(); return; } });