From 8ac3adbb5d75b69099c98314a802094fdc9e2375 Mon Sep 17 00:00:00 2001 From: MS-01-Codex <3647348c-0c07-48e0-8938-fb9280321c5f@agents.multica.local> Date: Tue, 18 Aug 2026 11:54:57 +0800 Subject: [PATCH] feat: restore mentor page to final day/night spec Rebuild the mentor workspace per the approved final design (day.png/night.png): - mentorView gets a dedicated full-width immersive shell (independent 56px top bar with page title slot + theme mode text, hidden module-nav/market tape/overview/status bar) scoped to body[data-active-view=mentorView]. - Assistant messages become borderless body text with name/time above; only user messages use a blue bubble; keep loading/error/streaming caret states. - 4 quick topics stay visible with history; composer restored to a framed ~94px card with bottom-left shortcut hint and bottom-right send button. - Directory tools merged into one row (search + filter menu + sort); the filter menu still offers all/A/B/C; list selected state is an inset rounded fill; contact rows are borderless 75px items. - Chat header always shows pin/note/profile/clear; pin reuses /api/mentors/ preferences; theme toggle stays the single #themeToggle. - Night tokens match the spec including the two distinct blues (#316FEF link, #5B8DEF quote/selected icon). Mobile (<768) stacks panels with no horizontal overflow; 1024+ follows the desktop spec. - Update stale test baselines (300px sidebar, 94px composer, hint presence, night bubble color, centered disclaimer) and regenerate the architecture inventory. Co-authored-by: multica-agent --- app/config/architecture-inventory.json | 56 +- app/frontend/index.html | 14 +- app/frontend/pages.config.js | 4 +- app/frontend/pages/mentor/foundation.css | 948 ++++++++++++++--------- app/frontend/pages/mentor/page.html | 89 ++- app/frontend/pages/mentor/page.js | 189 ++++- app/frontend/shared/shell.css | 124 +++ app/frontend/shared/shell.js | 5 + app/frontend/shared/theme.js | 2 + app/frontend/shared/tokens.css | 9 +- app/tests/e2e/app-shell.spec.js | 124 ++- app/tests/test_frontend_contract.py | 19 +- 12 files changed, 1118 insertions(+), 465 deletions(-) diff --git a/app/config/architecture-inventory.json b/app/config/architecture-inventory.json index 9cca123..ec10204 100644 --- a/app/config/architecture-inventory.json +++ b/app/config/architecture-inventory.json @@ -370,9 +370,9 @@ } ], "css_layers": [ - "/shared/tokens.css?v=20260806-2", + "/shared/tokens.css?v=20260818-1", "/shared/base.css?v=20260806-1", - "/shared/shell.css?v=20260806-2", + "/shared/shell.css?v=20260818-1", "/shared/auth.css?v=20260806-1", "/shared/components/controls.css?v=20260806-1", "/shared/components/navigation.css?v=20260806-1", @@ -390,7 +390,7 @@ "/pages/popularity/foundation.css?v=20260806-1", "/pages/dragon-tiger/foundation.css?v=20260806-1", "/pages/screener/foundation.css?v=20260806-2", - "/pages/mentor/foundation.css?v=20260806-2", + "/pages/mentor/foundation.css?v=20260818-1", "/pages/heaven/foundation.css?v=20260806-2", "/pages/review/foundation.css?v=20260806-1" ], @@ -451,8 +451,8 @@ }, { "path": "frontend/shared/shell.css", - "bytes": 51978, - "lines": 3224 + "bytes": 54738, + "lines": 3348 }, { "path": "backend/features/heaven/engine.py", @@ -461,8 +461,8 @@ }, { "path": "frontend/index.html", - "bytes": 45846, - "lines": 643 + "bytes": 46345, + "lines": 649 }, { "path": "backend/features/screener/catalog.py", @@ -541,8 +541,8 @@ }, { "path": "frontend/shared/admin.js", - "bytes": 13975, - "lines": 256 + "bytes": 14386, + "lines": 265 }, { "path": "backend/features/heaven/market_context.py", @@ -561,13 +561,13 @@ }, { "path": "backend/features/system/service.py", - "bytes": 12392, - "lines": 254 + "bytes": 12812, + "lines": 262 }, { "path": "backend/features/market/insights_auction.py", - "bytes": 10717, - "lines": 221 + "bytes": 11618, + "lines": 243 }, { "path": "backend/features/heaven/readings.py", @@ -605,14 +605,19 @@ "lines": 194 }, { - "path": "backend/features/screener/formula.py", - "bytes": 6983, - "lines": 146 + "path": "frontend/pages/mentor/page.html", + "bytes": 8199, + "lines": 110 }, { "path": "backend/data/providers/tushare_daily.py", - "bytes": 6837, - "lines": 160 + "bytes": 7121, + "lines": 167 + }, + { + "path": "backend/features/screener/formula.py", + "bytes": 6983, + "lines": 146 }, { "path": "backend/application.py", @@ -644,11 +649,6 @@ "bytes": 6202, "lines": 141 }, - { - "path": "frontend/pages/mentor/page.html", - "bytes": 6190, - "lines": 89 - }, { "path": "backend/features/screener/selection.py", "bytes": 6092, @@ -704,6 +704,11 @@ "bytes": 4276, "lines": 91 }, + { + "path": "frontend/shared/theme.js", + "bytes": 4258, + "lines": 117 + }, { "path": "backend/features/screener/engine.py", "bytes": 4242, @@ -714,11 +719,6 @@ "bytes": 4118, "lines": 115 }, - { - "path": "frontend/shared/theme.js", - "bytes": 4118, - "lines": 115 - }, { "path": "frontend/shared/table.js", "bytes": 3790, diff --git a/app/frontend/index.html b/app/frontend/index.html index f811191..ab6c4da 100644 --- a/app/frontend/index.html +++ b/app/frontend/index.html @@ -17,9 +17,9 @@ document.documentElement.style.colorScheme = theme; })(); - + - + @@ -37,7 +37,7 @@ - + @@ -68,6 +68,10 @@ 行情 情绪周期 +
+ 小白复盘 + +
上涨 -- 下跌 -- @@ -82,7 +86,7 @@
- + @@ -589,6 +593,8 @@

公共行情

待检查
+ +

默认停用。停用时仅保留凭据,不会发出任何 iFinD 请求;续费后开启此开关并保存即可恢复。

所有用户读取同一份后台快照,页面不会随后台任务自动重绘。

diff --git a/app/frontend/pages.config.js b/app/frontend/pages.config.js index 9a1b1f3..b0e361f 100644 --- a/app/frontend/pages.config.js +++ b/app/frontend/pages.config.js @@ -47,7 +47,7 @@ ["heaven", "/pages/heaven/page.html?v=20260803-1", ["heavenView"]], ["ladder", "/pages/ladder/page.html?v=20260803-1", ["ladderView"]], ["screener", "/pages/screener/page.html?v=20260804-1", ["screenerView", "screenerTrackingView"]], - ["mentor", "/pages/mentor/page.html?v=20260806-2", ["mentorView"]], + ["mentor", "/pages/mentor/page.html?v=20260818-1", ["mentorView"]], ["rotation", "/pages/rotation/page.html?v=20260803-1", ["rotationView"]], ["auction", "/pages/auction/page.html?v=20260803-1", ["auctionView"]], ["themes", "/pages/themes/page.html?v=20260803-1", ["themeLibraryView"]], @@ -81,7 +81,7 @@ "/pages/popularity/page.js?v=20260729-1", "/pages/dragon-tiger/page.js?v=20260806-1", "/pages/screener/page.js?v=20260806-1", - "/pages/mentor/page.js?v=20260806-2", + "/pages/mentor/page.js?v=20260818-1", "/pages/heaven/page.js?v=20260729-1", "/pages/review/page.js?v=20260729-1", "/shared/state.js?v=20260729-1", diff --git a/app/frontend/pages/mentor/foundation.css b/app/frontend/pages/mentor/foundation.css index b8bc780..0936308 100644 --- a/app/frontend/pages/mentor/foundation.css +++ b/app/frontend/pages/mentor/foundation.css @@ -1,40 +1,87 @@ /* Canonical CSS owner: mentor. Historical layers consolidated 2026-08-02. */ #mentorView.workspace-view { - --mentor-blue: var(--action); - --mentor-blue-dark: var(--action-hover); - --mentor-blue-soft: var(--action-soft); - --mentor-blue-line: var(--blue-line); - --mentor-line: var(--border); - --mentor-line-soft: var(--border-subtle); - --mentor-ink: var(--text-primary); - --mentor-sub: var(--text-secondary); - --mentor-faint: var(--text-tertiary); + --qp-bg-header: #F5F6F7; + --qp-bg-sidebar: #FFFFFF; + --qp-bg-chat: #FFFFFF; + --qp-bg-footer: #FDFDFD; + --qp-border: #ECEDED; + --qp-border-input: #DEE0E3; + --qp-bg-control: #F2F3F5; + --qp-bg-selected: #EAF1FE; + --qp-bg-btn-active: #EBF1FE; + --qp-bg-bubble-self: #D8E6FF; + --qp-accent: #3370FF; + --qp-accent-soft: #3370FF; + --qp-link: #3370FF; + --qp-text-1: #1F2329; + --qp-text-1-on-accent-bg: #1F2329; + --qp-text-2: #676B74; + --qp-text-3: #8F959E; + --qp-text-placeholder: #838892; + --qp-text-hint: #969AA8; + --qp-text-faint: #BBBDC2; + --qp-icon-normal: #989EA6; + --qp-icon-active: #3370FF; + --qp-badge-pinned-ink: #3370FF; + --qp-badge-pinned-bg: #E0E9FF; + --qp-badge-a-ink: #F54A45; + --qp-badge-a-bg: #FDECEB; + --qp-badge-b-ink: #646A73; + --qp-badge-b-bg: #F5F3EE; + --qp-badge-c-ink: #2EA44F; + --qp-badge-c-bg: #E6F6EE; + --qp-avatar-bg: #E3F0FF; + --qp-avatar-ink: #3370FF; + --qp-input-shadow: 0 1px 2px rgba(31, 35, 41, .04), 0 4px 12px rgba(31, 35, 41, .05); height: 100%; min-height: 0; flex-direction: column; - padding-bottom: 0; - color: var(--mentor-ink); + padding: 0; + color: var(--qp-text-1); + font-family: "PingFang SC", "Microsoft YaHei", system-ui, sans-serif; +} + +:root[data-theme="dark"] #mentorView.workspace-view { + --qp-bg-header: #141519; + --qp-bg-sidebar: #1C1E23; + --qp-bg-chat: #232529; + --qp-bg-footer: #212327; + --qp-border: #2B2E34; + --qp-border-input: #3A3E47; + --qp-bg-control: #2A2D33; + --qp-bg-selected: #2B3B58; + --qp-bg-btn-active: #2A3955; + --qp-bg-bubble-self: #35598C; + --qp-accent: #3D74E0; + --qp-accent-soft: #5B8DEF; + --qp-link: #316FEF; + --qp-text-1: #E8EAED; + --qp-text-1-on-accent-bg: #EAF1FB; + --qp-text-2: #A9ADB3; + --qp-text-3: #7C828A; + --qp-text-placeholder: #898F95; + --qp-text-hint: #798187; + --qp-text-faint: #5C6063; + --qp-icon-normal: #747981; + --qp-icon-active: #5B8DEF; + --qp-badge-pinned-ink: #5B8DEF; + --qp-badge-pinned-bg: #2B3F66; + --qp-badge-a-ink: #F26762; + --qp-badge-a-bg: #3D2829; + --qp-badge-b-ink: #A9ADB3; + --qp-badge-b-bg: #3A3527; + --qp-badge-c-ink: #5FCE8F; + --qp-badge-c-bg: #22362C; + --qp-avatar-bg: #2B3F66; + --qp-avatar-ink: #7BA5F5; + --qp-input-shadow: 0 1px 2px rgba(0, 0, 0, .30), 0 4px 12px rgba(0, 0, 0, .35); } #mentorView.active-view { display: flex; } -#mentorView .mentor-page-header { - flex: 0 0 auto; - min-height: var(--topbar-height); - margin: 0; -} - -#mentorView .mentor-page-title { - min-width: 0; -} - -#mentorView .mentor-page-title h2 { - letter-spacing: 0; -} - #mentorView #mentorNotice:not([hidden]) { flex: 0 0 auto; margin-bottom: var(--space-8); @@ -45,12 +92,8 @@ min-height: 0; flex: 1 1 auto; display: grid; - grid-template-columns: var(--mentor-directory-width) minmax(0, 1fr) var(--mentor-profile-width); + grid-template-columns: var(--mentor-directory-width) minmax(0, 1fr); overflow: hidden; - border: 1px solid var(--mentor-line); - border-radius: var(--radius-lg); - background: var(--surface); - box-shadow: var(--shadow-xs); } #mentorView .mentor-sidebar { @@ -59,95 +102,46 @@ display: flex; flex-direction: column; overflow: hidden; - border-right: 1px solid var(--mentor-line); - background: var(--surface-muted); -} - -#mentorView .mentor-directory-heading { - height: var(--mentor-pane-header-height); - flex: 0 0 var(--mentor-pane-header-height); - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-8); - padding: 0 var(--space-12); - border-bottom: 1px solid var(--mentor-line); - background: var(--surface); -} - -#mentorView .mentor-directory-title { - min-width: 0; - display: flex; - align-items: baseline; - gap: var(--space-8); -} - -#mentorView .mentor-directory-title strong { - color: var(--mentor-ink); - font-size: var(--font-size-card-title); - font-weight: var(--font-weight-semibold); -} - -#mentorView .mentor-directory-title span { - color: var(--mentor-faint); - font-size: var(--font-size-caption); - font-variant-numeric: tabular-nums; -} - -#mentorView .mentor-sort-toggle { - height: var(--control-height); - display: inline-flex; - align-items: center; - gap: var(--space-4); - padding: 0 var(--space-8); - border: 0; - border-radius: var(--radius-sm); - background: transparent; - color: var(--mentor-sub); - font-size: var(--font-size-label); - cursor: pointer; - transition: color var(--motion-fast), background var(--motion-fast); -} - -#mentorView .mentor-sort-toggle:is(:hover, .active) { - background: var(--action-soft); - color: var(--action); -} - -#mentorView .mentor-sort-toggle .lucide { - width: var(--font-size-body); - height: var(--font-size-body); + border-right: 1px solid var(--qp-border); + background: var(--qp-bg-sidebar); } #mentorView .mentor-directory-tools { flex: 0 0 auto; - display: grid; - gap: var(--space-8); - padding: var(--space-12); - border-bottom: 1px solid var(--mentor-line-soft); -} - -#mentorView .mentor-search-field { - height: var(--control-height); display: flex; align-items: center; gap: var(--space-8); - padding: 0 var(--space-8); - border: 1px solid var(--control-border); - border-radius: var(--radius-md); - background: var(--control-surface); - color: var(--mentor-faint); + padding: 12px; + border-bottom: 1px solid var(--qp-border); +} + +#mentorView .mentor-search-field { + min-width: 0; + height: 32px; + flex: 1 1 auto; + display: flex; + align-items: center; + gap: var(--space-8); + padding: 0 10px; + border: 1px solid var(--qp-border-input); + border-radius: 8px; + background: var(--qp-bg-control); + color: var(--qp-text-placeholder); transition: border-color var(--motion-fast), box-shadow var(--motion-fast); } #mentorView .mentor-search-field:focus-within { - border-color: var(--action); - box-shadow: 0 0 0 3px var(--focus-ring); + border-color: var(--qp-accent); + box-shadow: 0 0 0 2px rgba(51, 112, 255, .15); +} + +:root[data-theme="dark"] #mentorView .mentor-search-field:focus-within { + box-shadow: 0 0 0 2px rgba(51, 112, 255, .25); } #mentorView .mentor-search-field .lucide { - width: var(--font-size-body); - height: var(--font-size-body); + width: 16px; + height: 16px; flex: 0 0 auto; } @@ -157,39 +151,124 @@ border: 0; outline: 0; background: transparent; - color: var(--mentor-ink); - font-size: var(--font-size-label); + color: var(--qp-text-1); + font-size: 14px; } #mentorView .mentor-search-field input::placeholder { - color: var(--mentor-faint); + color: var(--qp-text-placeholder); } -#mentorView .mentor-evidence-filters { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); +#mentorView .mentor-filter-menu { + position: relative; + flex: 0 0 auto; +} + +#mentorView .mentor-filter-toggle { + height: 32px; + display: inline-flex; + align-items: center; gap: var(--space-4); + padding: 0 10px; + border: 1px solid transparent; + border-radius: 8px; + background: var(--qp-bg-control); + color: var(--qp-text-1); + font-size: 14px; + cursor: pointer; + transition: color var(--motion-fast), background var(--motion-fast), border-color var(--motion-fast); } -#mentorView .mentor-evidence-filters button { - height: var(--control-height); +#mentorView .mentor-filter-toggle:is(:hover, .active) { + color: var(--qp-accent); +} + +#mentorView .mentor-filter-toggle .lucide { + width: 14px; + height: 14px; +} + +#mentorView .mentor-filter-options { + position: absolute; + top: calc(100% + 4px); + right: 0; + z-index: 20; + min-width: 128px; + display: flex; + flex-direction: column; + padding: 4px; + border: 1px solid var(--qp-border-input); + border-radius: 8px; + background: var(--qp-bg-sidebar); + box-shadow: 0 4px 14px rgba(16, 24, 40, .08); +} + +:root[data-theme="dark"] #mentorView .mentor-filter-options { + border-color: var(--qp-border); + box-shadow: 0 4px 14px rgba(0, 0, 0, .35); +} + +#mentorView .mentor-filter-options[hidden] { + display: none; +} + +#mentorView .mentor-filter-options button { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-8); + padding: 8px 10px; border: 0; - border-radius: var(--radius-sm); + border-radius: 6px; background: transparent; - color: var(--mentor-sub); - font-size: var(--font-size-label); + color: var(--qp-text-1); + font-size: 14px; + text-align: left; cursor: pointer; transition: color var(--motion-fast), background var(--motion-fast); } -#mentorView .mentor-evidence-filters button:is(:hover, .active) { - background: var(--surface-selected); - color: var(--action); +#mentorView .mentor-filter-options button span { + color: var(--qp-text-3); + font-size: 12px; +} + +#mentorView .mentor-filter-options button:is(:hover, .active) { + background: var(--qp-bg-selected); + color: var(--qp-accent); font-weight: var(--font-weight-semibold); } +#mentorView .mentor-sort-toggle { + height: 32px; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + gap: var(--space-4); + padding: 0 8px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--qp-text-2); + font-size: 14px; + cursor: pointer; + transition: color var(--motion-fast), background var(--motion-fast); +} + +#mentorView .mentor-sort-toggle:is(:hover, .active) { + background: var(--qp-bg-control); + color: var(--qp-accent); +} + +#mentorView .mentor-sort-toggle .lucide { + width: 16px; + height: 16px; +} + #mentorView .mentor-sort-hint { + flex: 0 0 auto; margin: 0; + padding: 8px 12px; color: var(--warning-color); font-size: var(--font-size-caption); line-height: 1.5; @@ -200,24 +279,26 @@ flex: 1 1 auto; overflow: hidden auto; scrollbar-gutter: stable; + padding: 4px 0; } #mentorView .mentor-option { position: relative; - min-height: 68px; + height: 75px; + margin: 0 8px; display: flex; align-items: stretch; - border-bottom: 1px solid var(--mentor-line-soft); + border-radius: 10px; background: transparent; transition: background var(--motion-fast), box-shadow var(--motion-fast); } -#mentorView .mentor-option:is(:hover, .active) { - background: var(--surface-hover); +#mentorView .mentor-option:is(:hover, :focus-within) { + background: color-mix(in srgb, var(--qp-bg-control) 55%, transparent); } #mentorView .mentor-option.active { - box-shadow: inset 3px 0 0 var(--action); + background: var(--qp-bg-selected); } #mentorView .mentor-option.is-dragging { @@ -225,7 +306,7 @@ } #mentorView .mentor-option.is-drag-over { - box-shadow: inset 0 2px 0 var(--action); + box-shadow: inset 0 2px 0 var(--qp-accent); } #mentorView .mentor-option-main { @@ -233,8 +314,8 @@ flex: 1 1 auto; display: flex; align-items: center; - gap: var(--space-8); - padding: var(--space-8) var(--space-4) var(--space-8) var(--space-12); + gap: 12px; + padding: 0 4px 0 8px; border: 0; background: transparent; color: inherit; @@ -249,31 +330,19 @@ display: inline-flex; align-items: center; justify-content: center; - border: 1px solid var(--mentor-blue-line); + border: 0; border-radius: 50%; - background: var(--mentor-blue-soft); - color: var(--mentor-blue-dark); - font-size: var(--font-size-card-title); + background: var(--qp-avatar-bg); + color: var(--qp-avatar-ink); + font-size: 16px; font-weight: var(--font-weight-semibold); } -#mentorView .mentor-avatar[data-grade="a"] { - border-color: var(--market-up); - background: var(--market-up-soft); - color: var(--market-up); -} - -#mentorView .mentor-avatar[data-grade="c"] { - border-color: var(--market-down); - background: var(--market-down-soft); - color: var(--market-down); -} - #mentorView .mentor-option-copy { min-width: 0; display: grid; flex: 1 1 auto; - gap: var(--space-4); + gap: 3px; } #mentorView .mentor-option-heading { @@ -286,8 +355,8 @@ #mentorView .mentor-option-heading > strong { min-width: 0; overflow: hidden; - color: var(--mentor-ink); - font-size: var(--font-size-body); + color: var(--qp-text-1); + font-size: 16px; font-weight: var(--font-weight-semibold); text-overflow: ellipsis; white-space: nowrap; @@ -302,7 +371,7 @@ #mentorView :is(.mentor-option-copy > em, .mentor-option-meta) { min-width: 0; overflow: hidden; - font-size: var(--font-size-caption); + font-size: 14px; font-style: normal; line-height: 1.35; text-overflow: ellipsis; @@ -310,31 +379,32 @@ } #mentorView .mentor-option-copy > em { - color: var(--mentor-sub); + color: var(--qp-text-2); } #mentorView .mentor-option-meta { - color: var(--mentor-faint); + color: var(--qp-text-3); + font-size: 13px; } #mentorView .mentor-option-tools { flex: 0 0 auto; display: flex; align-items: center; - gap: var(--space-4); - padding-right: var(--space-8); + gap: 2px; + padding-right: 4px; } #mentorView :is(.mentor-pin-button, .mentor-order-button) { - width: var(--control-height); - height: var(--control-height); + width: 30px; + height: 30px; display: inline-flex; align-items: center; justify-content: center; border: 0; - border-radius: var(--radius-sm); + border-radius: 8px; background: transparent; - color: var(--mentor-faint); + color: var(--qp-icon-normal); cursor: pointer; opacity: 0; transition: color var(--motion-fast), background var(--motion-fast), opacity var(--motion-fast); @@ -347,73 +417,65 @@ } #mentorView :is(.mentor-pin-button, .mentor-order-button):is(:hover, .active) { - background: var(--action-soft); - color: var(--action); + background: var(--qp-bg-btn-active); + color: var(--qp-icon-active); } #mentorView :is(.mentor-pin-button, .mentor-order-button) .lucide { - width: var(--font-size-body); - height: var(--font-size-body); + width: 16px; + height: 16px; } #mentorView :is(.mentor-list-empty, .mentor-evidence-legend) { margin: 0; - color: var(--mentor-faint); - font-size: var(--font-size-caption); - text-align: center; + color: var(--qp-text-hint); + font-size: 12px; } #mentorView .mentor-list-empty { padding: var(--space-24) var(--space-12); -} - -#mentorView .mentor-evidence-legend { - flex: 0 0 auto; - padding: var(--space-8) var(--space-12); - border-top: 1px solid var(--mentor-line-soft); - background: var(--surface); + text-align: center; } #mentorView .mentor-badge { height: 18px; display: inline-flex; align-items: center; - gap: var(--space-4); - padding: 0 var(--space-4); - border: 1px solid var(--mentor-line); - border-radius: var(--radius-sm); - background: var(--surface-muted); - color: var(--mentor-sub); - font-size: var(--font-size-aux); - font-weight: var(--font-weight-semibold); + gap: 2px; + padding: 0 6px; + border: 0; + border-radius: 4px; + font-size: 12px; + font-weight: var(--font-weight-medium); line-height: 1; } #mentorView .mentor-badge .lucide { - width: var(--font-size-aux); - height: var(--font-size-aux); + width: 12px; + height: 12px; +} + +#mentorView .mentor-badge.pinned { + background: var(--qp-badge-pinned-bg); + color: var(--qp-badge-pinned-ink); } #mentorView .mentor-badge.grade-a { - border-color: var(--market-up); - background: var(--market-up-soft); - color: var(--market-up); + background: var(--qp-badge-a-bg); + color: var(--qp-badge-a-ink); } #mentorView .mentor-badge.grade-b { - border-color: var(--mentor-blue-line); - background: var(--mentor-blue-soft); - color: var(--mentor-blue-dark); + background: var(--qp-badge-b-bg); + color: var(--qp-badge-b-ink); } #mentorView .mentor-badge.grade-c { - border-color: var(--market-down); - background: var(--market-down-soft); - color: var(--market-down); + background: var(--qp-badge-c-bg); + color: var(--qp-badge-c-ink); } #mentorView .mentor-badge.private { - border-color: var(--warning-color); background: var(--warning-soft); color: var(--warning-color); } @@ -424,7 +486,7 @@ display: flex; flex-direction: column; overflow: hidden; - background: var(--surface-subtle); + background: var(--qp-bg-chat); } #mentorView .mentor-chat-header { @@ -434,22 +496,29 @@ align-items: center; justify-content: space-between; gap: var(--space-12); - padding: 0 var(--space-16); - border-bottom: 1px solid var(--mentor-line); - background: var(--surface); + padding: 0 16px; + border-bottom: 1px solid var(--qp-border); + background: var(--qp-bg-chat); } #mentorView .mentor-chat-identity { min-width: 0; display: flex; align-items: center; - gap: var(--space-8); + gap: 12px; } #mentorView .mentor-chat-identity > div { min-width: 0; } +#mentorView .mentor-chat-identity .mentor-avatar { + width: 44px; + height: 44px; + flex: 0 0 44px; + font-size: 17px; +} + #mentorView .mentor-active-title { min-width: 0; display: flex; @@ -461,8 +530,8 @@ min-width: 0; margin: 0; overflow: hidden; - color: var(--mentor-ink); - font-size: var(--font-size-card-title); + color: var(--qp-text-1); + font-size: 16px; font-weight: var(--font-weight-semibold); letter-spacing: 0; text-overflow: ellipsis; @@ -475,21 +544,58 @@ } #mentorView #activeMentorStatus { - margin: var(--space-4) 0 0; - color: var(--mentor-faint); - font-size: var(--font-size-caption); + margin: 2px 0 0; + color: var(--qp-text-3); + font-size: 13px; + line-height: 1.4; +} + +#mentorView .mentor-chat-actions { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 2px; +} + +#mentorView .mentor-header-action { + width: 34px; + height: 34px; + min-height: 34px; + background: transparent; + color: var(--qp-icon-normal); + font-size: 18px; +} + +#mentorView .mentor-header-action:hover { + background: var(--qp-bg-control); + color: var(--qp-icon-active); +} + +#mentorView .mentor-header-action.active { + background: var(--qp-bg-btn-active); + color: var(--qp-icon-active); } #mentorView .mentor-clear-button { flex: 0 0 auto; - color: var(--mentor-sub); + width: 34px; + height: 34px; + min-height: 34px; + background: transparent; + color: var(--qp-icon-normal); + font-size: 18px; +} + +#mentorView .mentor-clear-button:hover { + background: var(--qp-bg-control); + color: var(--qp-accent); } #mentorView .mentor-messages { min-height: 0; flex: 1 1 auto; overflow: hidden auto; - padding: var(--space-24); + padding: 20px 24px 20px 20px; scrollbar-gutter: stable; scroll-behavior: smooth; } @@ -501,7 +607,7 @@ align-items: center; justify-content: center; gap: var(--space-8); - color: var(--mentor-sub); + color: var(--qp-text-2); text-align: center; } @@ -511,10 +617,10 @@ display: inline-flex; align-items: center; justify-content: center; - border: 1px solid var(--mentor-blue-line); + border: 0; border-radius: 50%; - background: var(--mentor-blue-soft); - color: var(--mentor-blue-dark); + background: var(--qp-avatar-bg); + color: var(--qp-avatar-ink); } #mentorView .mentor-empty-mark .lucide { @@ -523,7 +629,7 @@ } #mentorView .mentor-empty-state strong { - color: var(--mentor-ink); + color: var(--qp-text-1); font-size: var(--font-size-card-title); } @@ -538,8 +644,8 @@ width: 100%; display: flex; align-items: flex-start; - gap: var(--space-8); - margin-bottom: var(--space-16); + gap: 20px; + margin-bottom: 22px; } #mentorView .mentor-message.user { @@ -553,67 +659,59 @@ display: inline-flex; align-items: center; justify-content: center; - border: 1px solid var(--mentor-line); + border: 0; border-radius: 50%; - background: var(--surface); - color: var(--mentor-sub); - font-size: var(--font-size-label); + background: var(--qp-avatar-bg); + color: var(--qp-avatar-ink); + font-size: 16px; font-weight: var(--font-weight-semibold); } -#mentorView .mentor-message.user .mentor-message-avatar { - border-color: var(--mentor-blue-line); - background: var(--mentor-blue-soft); - color: var(--mentor-blue-dark); -} - #mentorView .mentor-message-body { - max-width: var(--mentor-message-max-width); + min-width: 0; display: flex; flex-direction: column; align-items: flex-start; - gap: var(--space-4); + gap: 4px; +} + +#mentorView .mentor-message.assistant .mentor-message-body { + flex: 1 1 auto; } #mentorView .mentor-message.user .mentor-message-body { + max-width: var(--mentor-message-max-width); align-items: flex-end; } #mentorView .mentor-message-label { - color: var(--mentor-faint); - font-size: var(--font-size-caption); + color: var(--qp-text-3); + font-size: 12px; line-height: 1.4; } #mentorView .mentor-message-content { - min-width: 48px; - padding: var(--space-12) var(--space-16); - border: 1px solid var(--mentor-line); - border-radius: var(--radius-lg); - background: var(--surface); - color: var(--mentor-ink); - font-size: var(--font-size-body); - line-height: 1.75; + min-width: 0; + color: var(--qp-text-1); + font-size: 16px; + line-height: 1.7; overflow-wrap: anywhere; } -#mentorView .mentor-message.assistant .mentor-message-content { - border-top-left-radius: var(--radius-sm); -} - #mentorView .mentor-message.user .mentor-message-content { - border-color: var(--mentor-blue-line); - border-top-right-radius: var(--radius-sm); - background: var(--mentor-blue-soft); + padding: 8px 14px; + border-radius: 12px; + background: var(--qp-bg-bubble-self); + color: var(--qp-text-1-on-accent-bg); } #mentorView .mentor-message.is-error .mentor-message-content { - border-color: var(--market-up); + color: var(--market-up); } #mentorView .mentor-message small { - color: var(--mentor-faint); - font-size: var(--font-size-caption); + color: var(--qp-text-3); + font-size: 12px; } #mentorView .mentor-answer-paragraph { @@ -627,9 +725,10 @@ #mentorView .mentor-answer-heading { display: block; margin: var(--space-12) 0 var(--space-4); - color: var(--mentor-ink); - font-size: var(--font-size-body); + color: var(--qp-text-1); + font-size: 18px; font-weight: var(--font-weight-semibold); + line-height: 1.4; } #mentorView .mentor-answer-heading:first-child { @@ -649,20 +748,20 @@ display: block; margin: var(--space-8) 0; padding-left: var(--space-12); - border-left: 2px solid var(--action); - color: var(--mentor-sub); + border-left: 2px solid var(--qp-accent-soft); + color: var(--qp-text-2); } #mentorView .mentor-answer-rule { display: block; height: 1px; margin: var(--space-12) 0; - background: var(--mentor-line-soft); + background: var(--qp-border); } #mentorView .mentor-loading-copy { margin: 0; - color: var(--mentor-sub); + color: var(--qp-text-2); } #mentorView .assistant-stream-caret { @@ -670,7 +769,7 @@ height: var(--font-size-body); display: inline-block; margin-left: var(--space-4); - background: var(--action); + background: var(--qp-accent); animation: mentor-caret 900ms steps(1, end) infinite; } @@ -678,14 +777,14 @@ width: 100%; display: grid; gap: var(--space-4); - margin-top: var(--space-4); + margin-top: var(--space-8); padding-top: var(--space-8); - border-top: 1px solid var(--mentor-line-soft); + border-top: 1px solid var(--qp-border); } #mentorView .mentor-follow-ups > span { - color: var(--mentor-faint); - font-size: var(--font-size-caption); + color: var(--qp-text-faint); + font-size: 12px; } #mentorView .mentor-follow-ups button { @@ -696,17 +795,17 @@ padding: var(--space-4) 0; border: 0; background: transparent; - color: var(--action); - font-size: var(--font-size-label); - line-height: 1.5; + color: var(--qp-link); + font-size: 14px; + line-height: 1.8; text-align: left; cursor: pointer; transition: color var(--motion-fast), transform var(--motion-fast); } #mentorView .mentor-follow-ups button:hover { - color: var(--action-hover); - transform: translateX(var(--space-4)); + text-decoration: underline; + color: var(--qp-link); } #mentorView .mentor-follow-ups button .lucide { @@ -720,10 +819,8 @@ flex: 0 0 auto; display: flex; align-items: center; - gap: var(--space-8); - padding: var(--space-8) var(--space-16) 0; - border-top: 1px solid var(--mentor-line); - background: var(--surface); + gap: 10px; + padding: 12px 24px 0; } #mentorView .mentor-quick-prompts[hidden] { @@ -731,101 +828,166 @@ } #mentorView .mentor-prompt-label { - color: var(--mentor-faint); - font-size: var(--font-size-caption); + flex: 0 0 auto; + color: var(--qp-text-3); + font-size: 13px; } #mentorView .mentor-quick-prompts button { - height: var(--control-height); + height: 29px; display: inline-flex; align-items: center; gap: var(--space-4); - padding: 0 var(--space-8); - border: 1px solid var(--mentor-line); - border-radius: var(--radius-md); - background: var(--surface-subtle); - color: var(--mentor-sub); - font-size: var(--font-size-label); + padding: 0 12px; + border: 0; + border-radius: 8px; + background: var(--qp-bg-control); + color: var(--qp-text-2); + font-size: 14px; cursor: pointer; - transition: border-color var(--motion-fast), color var(--motion-fast), background var(--motion-fast); + transition: color var(--motion-fast), background var(--motion-fast); } #mentorView .mentor-quick-prompts button:hover { - border-color: var(--mentor-blue-line); - background: var(--mentor-blue-soft); - color: var(--action); + background: color-mix(in srgb, var(--qp-bg-control) 82%, var(--qp-text-1) 18%); + color: var(--qp-text-1); } #mentorView .mentor-quick-prompts button .lucide { - width: var(--font-size-body); - height: var(--font-size-body); + width: 15px; + height: 15px; } #mentorView .mentor-chat-form { min-height: var(--mentor-composer-min-height); flex: 0 0 auto; display: flex; + align-items: stretch; + gap: var(--space-12); + margin: 12px 24px 16px 21px; + padding: 14px 16px; + border: 1px solid var(--qp-border-input); + border-radius: 12px; + background: var(--qp-bg-footer); + box-shadow: var(--qp-input-shadow); + transition: border-color var(--motion-fast), box-shadow var(--motion-fast); +} + +#mentorView .mentor-chat-form:focus-within { + border-color: var(--qp-accent); + box-shadow: 0 0 0 2px rgba(51, 112, 255, .15), var(--qp-input-shadow); +} + +:root[data-theme="dark"] #mentorView .mentor-chat-form:focus-within { + box-shadow: 0 0 0 2px rgba(51, 112, 255, .25), var(--qp-input-shadow); +} + +#mentorView .mentor-composer-main { + min-width: 0; + flex: 1 1 auto; + display: flex; flex-direction: column; - padding: var(--space-8) var(--space-16); - border-top: 1px solid var(--mentor-line); - background: var(--surface); + justify-content: space-between; + gap: var(--space-8); } #mentorView #mentorQuestion { width: 100%; - min-height: 50px; - flex: 1 1 auto; + min-width: 0; + height: 27px; + min-height: 27px; + flex: 0 1 auto; + align-self: flex-start; resize: none; + overflow-y: hidden; border: 0; outline: 0; background: transparent; - color: var(--mentor-ink); + color: var(--qp-text-1); font-family: inherit; - font-size: var(--font-size-body); - line-height: 1.6; + font-size: 16px; + line-height: 1.7; } #mentorView #mentorQuestion::placeholder { - color: var(--mentor-faint); + color: var(--qp-text-placeholder); +} + +#mentorView .mentor-composer-hint { + flex: 0 0 auto; + color: var(--qp-text-hint); + font-size: 12px; + line-height: 1.4; } #mentorView .mentor-composer-actions { + flex: 0 0 auto; display: flex; - align-items: center; + align-items: flex-end; justify-content: flex-end; gap: var(--space-8); } -#mentorView .mentor-composer-actions > span { - margin-right: auto; - color: var(--mentor-faint); - font-size: var(--font-size-caption); +#mentorView .mentor-send-button { + width: 71px; + height: 31px; + min-height: 31px; + padding: 0; + border: 0; + border-radius: 10px; + background: var(--qp-accent); + color: #FFFFFF; + font-size: 14px; + font-weight: var(--font-weight-medium); } -#mentorView :is(.mentor-send-button, .mentor-stop-button) { - min-width: var(--col-action); +:root[data-theme="dark"] #mentorView .mentor-send-button { + color: #FFFFFF; +} + +#mentorView .mentor-send-button:hover { + background: color-mix(in srgb, var(--qp-accent) 88%, #000 12%); + color: #FFFFFF; +} + +#mentorView .mentor-send-button:active { + transform: scale(.97); +} + +#mentorView .mentor-send-button .lucide { + width: 15px; + height: 15px; } #mentorView .mentor-stop-button { color: var(--market-up); } -#mentorView .mentor-disclaimer { - flex: 0 0 auto; - margin: 0; - padding: var(--space-4) var(--space-16) var(--space-8); - background: var(--surface); - color: var(--mentor-faint); - font-size: var(--font-size-aux); - text-align: center; +#mentorView .mentor-workspace-footer { + flex: 0 0 28px; + height: 28px; + min-height: 28px; + display: flex; + align-items: center; + border-top: 1px solid var(--qp-border); + background: var(--qp-bg-footer); } -#mentorView .mentor-profile-panel { +#mentorView .mentor-workspace-footer .mentor-evidence-legend { + flex: 0 0 var(--mentor-directory-width); + padding-left: 15px; +} + +#mentorView .mentor-disclaimer { + flex: 1 1 auto; min-width: 0; - min-height: 0; - overflow: hidden auto; - border-left: 1px solid var(--mentor-line); - background: var(--surface); + margin: 0; + padding: 0 16px; + color: var(--qp-text-hint); + font-size: 12px; + line-height: 1.4; + text-align: center; + overflow-wrap: anywhere; } #mentorView .mentor-profile-hero { @@ -843,17 +1005,17 @@ display: inline-flex; align-items: center; justify-content: center; - border: 1px solid var(--mentor-blue-line); + border: 0; border-radius: 50%; - background: var(--mentor-blue-soft); - color: var(--mentor-blue-dark); + background: var(--qp-avatar-bg); + color: var(--qp-avatar-ink); font-size: var(--font-size-metric); font-weight: var(--font-weight-semibold); } #mentorView .mentor-profile-hero h3 { margin: 0; - color: var(--mentor-ink); + color: var(--qp-text-1); font-size: var(--font-size-page-title); font-weight: var(--font-weight-semibold); letter-spacing: 0; @@ -861,7 +1023,7 @@ #mentorView .mentor-profile-hero > p { margin: 0; - color: var(--mentor-sub); + color: var(--qp-text-2); font-size: var(--font-size-label); line-height: 1.65; } @@ -877,25 +1039,25 @@ display: grid; gap: var(--space-8); padding: var(--space-16); - border-top: 1px solid var(--mentor-line-soft); + border-top: 1px solid var(--qp-border); } #mentorView .mentor-profile-section h4 { margin: 0; - color: var(--mentor-faint); + color: var(--qp-text-faint); font-size: var(--font-size-caption); font-weight: var(--font-weight-medium); } #mentorView .mentor-profile-section > strong { - color: var(--mentor-ink); + color: var(--qp-text-1); font-size: var(--font-size-label); font-weight: var(--font-weight-semibold); } #mentorView .mentor-profile-section > p { margin: 0; - color: var(--mentor-sub); + color: var(--qp-text-2); font-size: var(--font-size-label); line-height: 1.7; } @@ -909,8 +1071,8 @@ #mentorView .mentor-active-focus span { padding: var(--space-4) var(--space-8); border-radius: var(--radius-sm); - background: var(--surface-muted); - color: var(--mentor-sub); + background: var(--qp-bg-control); + color: var(--qp-text-2); font-size: var(--font-size-caption); } @@ -925,11 +1087,106 @@ height: var(--font-size-body); flex: 0 0 auto; margin-top: var(--space-4); - color: var(--mentor-faint); + color: var(--qp-text-faint); +} + +#mentorView .mentor-floating-dialog:not([open]) { + display: none; +} + +#mentorView .mentor-floating-dialog { + position: fixed; + inset: 0; + margin: auto; + width: 400px; + max-width: calc(100vw - 24px); + max-height: min(600px, calc(100dvh - 24px)); + padding: 0; + border: 1px solid var(--qp-border); + border-radius: var(--radius-lg); + background: var(--qp-bg-chat); + color: var(--qp-text-1); + box-shadow: var(--shadow-float); +} + +#mentorView .mentor-floating-dialog::backdrop { + background: var(--backdrop); + backdrop-filter: blur(2px); +} + +#mentorView .mentor-dialog-head { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-12); + padding: var(--space-12) var(--space-16); + border-bottom: 1px solid var(--qp-border); +} + +#mentorView .mentor-dialog-head h3 { + margin: 0; + color: var(--qp-text-1); + font-size: var(--font-size-page-title); + font-weight: var(--font-weight-semibold); + letter-spacing: 0; +} + +#mentorView .mentor-dialog-close { + color: var(--qp-text-2); +} + +#mentorView .mentor-dialog-body { + min-height: 0; + padding: var(--space-16); +} + +#mentorView .mentor-note-dialog { + display: flex; + flex-direction: column; +} + +#mentorView .mentor-note-dialog .mentor-dialog-body { + flex: 1 1 auto; + display: flex; + align-items: flex-start; + overflow-y: auto; +} + +#mentorView #mentorNoteInput { + width: 100%; + min-height: 120px; + max-height: 320px; + resize: vertical; + border: 1px solid var(--qp-border); + border-radius: var(--radius-md); + background: var(--qp-bg-control); + color: var(--qp-text-1); + font-family: inherit; + font-size: var(--font-size-body); + line-height: 1.6; + padding: var(--space-8) var(--space-12); +} + +#mentorView #mentorNoteInput:focus { + border-color: var(--qp-accent); + outline: 0; +} + +#mentorView .mentor-profile-dialog { + display: flex; + flex-direction: column; +} + +#mentorView .mentor-profile-dialog-body { + min-height: 0; + flex: 1 1 auto; + overflow-y: auto; + padding-bottom: var(--space-8); } #mentorView :is(button, input, textarea):focus-visible { - outline: 2px solid var(--action); + outline: 2px solid var(--qp-accent); outline-offset: 2px; } @@ -940,7 +1197,7 @@ } @media (prefers-reduced-motion: reduce) { - #mentorView :is(.mentor-sort-toggle, .mentor-search-field, .mentor-evidence-filters button, .mentor-option, .mentor-pin-button, .mentor-order-button, .mentor-follow-ups button, .mentor-quick-prompts button) { + #mentorView :is(.mentor-sort-toggle, .mentor-search-field, .mentor-filter-toggle, .mentor-filter-options button, .mentor-option, .mentor-pin-button, .mentor-order-button, .mentor-follow-ups button, .mentor-quick-prompts button, .mentor-send-button) { transition: none; } @@ -953,36 +1210,35 @@ } } -:root[data-theme="dark"] #mentorView { - --mentor-blue: var(--action); - --mentor-blue-dark: var(--action-hover); - --mentor-blue-soft: var(--action-soft); - --mentor-blue-line: var(--blue-line); - --mentor-line: var(--border); - --mentor-line-soft: var(--border-subtle); - --mentor-ink: var(--text-primary); - --mentor-sub: var(--text-secondary); - --mentor-faint: var(--text-tertiary); -} - -:root[data-theme="dark"] #mentorView .mentor-message { - border-color: var(--line-soft); - background: var(--surface-subtle); - box-shadow: none; -} - -:root[data-theme="dark"] #mentorView .mentor-message.user .mentor-message-content { - border-color: var(--blue-line); - background: var(--action-soft); - color: var(--text-primary); -} - -@media (max-width: 1279px) { +@media (max-width: 767px) { #mentorView .mentor-layout { - grid-template-columns: var(--mentor-directory-width) minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); + overflow: visible; } - #mentorView .mentor-profile-panel { - display: none; + #mentorView .mentor-sidebar { + max-height: 44vh; + border-right: 0; + border-bottom: 1px solid var(--qp-border); + } + + #mentorView .mentor-chat-panel { + overflow: visible; + } + + #mentorView .mentor-messages { + overflow: visible; + max-height: none; + } + + #mentorView .mentor-workspace-footer { + flex-wrap: wrap; + height: auto; + min-height: 28px; + } + + #mentorView .mentor-workspace-footer .mentor-evidence-legend { + flex: 0 0 auto; + padding-left: 12px; } } diff --git a/app/frontend/pages/mentor/page.html b/app/frontend/pages/mentor/page.html index 40b6a94..d66ca3c 100644 --- a/app/frontend/pages/mentor/page.html +++ b/app/frontend/pages/mentor/page.html @@ -1,35 +1,28 @@ -
+
-
-
-

问师

- 与不同交易思维模型持续对话 · -- -
-
@@ -41,7 +34,12 @@

思维模型已就绪

- +
+ + + + +
@@ -52,38 +50,61 @@
- - +
+ + + Enter 发送 · Shift + Enter 换行 +
- Enter 发送 · Shift + Enter 换行
-

基于公开资料提炼的思维模型模拟,不代表本人观点,不构成投资建议。

- - + +
diff --git a/app/frontend/pages/mentor/page.js b/app/frontend/pages/mentor/page.js index 5c3e03e..b0c8a8d 100644 --- a/app/frontend/pages/mentor/page.js +++ b/app/frontend/pages/mentor/page.js @@ -35,23 +35,63 @@ function renderMentorWorkspace() { const setup = state.mentorSetup; if (!setup) return; const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null; - setText("mentorDataDate", `数据日期 ${displayCompactDate(setup.trade_date)}`); + setText("currentPageSubtitle", `与不同交易思维模型持续对话 · 数据日期 ${displayCompactDate(setup.trade_date)}`); setText("activeMentorName", selected?.name || "--"); - setText("mentorProfileName", selected?.name || "--"); - setText("mentorProfileTagline", selected?.tagline || selected?.description || "--"); - setText("mentorProfileSource", selected?.evidence?.label || "公开资料整理"); - setText("mentorProfileDataDate", `行情数据 ${displayCompactDate(setup.trade_date)}`); setText("activeMentorAvatar", mentorAvatarText(selected)); - setText("mentorProfileAvatar", mentorAvatarText(selected)); document.querySelector("#activeMentorBadges").innerHTML = selected ? renderMentorBadges(selected, true) : ""; - document.querySelector("#mentorProfileBadges").innerHTML = selected ? renderMentorBadges(selected, true) : ""; - setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--"); - document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4) - .map((item) => `${escapeHtml(item)}`).join(""); + const pinButton = document.querySelector("#mentorPinButton"); + if (pinButton) { + pinButton.classList.toggle("active", Boolean(selected?.pinned)); + pinButton.setAttribute("aria-label", selected?.pinned ? "取消置顶当前思维模型" : "置顶当前思维模型"); + pinButton.setAttribute("aria-pressed", String(Boolean(selected?.pinned))); + } + populateMentorDialogs(selected); renderMentorDirectory(); renderMentorMessages(); } +function populateMentorDialogs(selected) { + const noteInput = document.querySelector("#mentorNoteInput"); + if (noteInput) { + noteInput.value = loadMentorNote(selected); + } + setText("mentorProfileDialogName", selected?.name || "--"); + setText("mentorProfileDialogTagline", selected?.tagline || selected?.description || "--"); + setText("mentorProfileDialogSource", selected?.evidence?.label || "公开资料整理"); + setText("mentorProfileDialogEvidence", selected?.evidence?.note || selected?.description || "--"); + setText("mentorProfileDialogDataDate", `行情数据 ${displayCompactDate(state.mentorSetup?.trade_date || elements.tradeDate.value)}`); + setText("mentorProfileDialogAvatar", mentorAvatarText(selected)); + document.querySelector("#mentorProfileDialogBadges").innerHTML = selected ? renderMentorBadges(selected, true) : ""; + document.querySelector("#mentorProfileDialogFocus").innerHTML = (selected?.focus || []).slice(0, 4) + .map((item) => `${escapeHtml(item)}`).join(""); +} + +function mentorNoteStorageKey(selected) { + const accountId = state.user?.id || state.user?.username || "anon"; + return `xiaobai-mentor-note-${accountId}-${String(selected?.id || "")}`; +} + +function loadMentorNote(selected) { + if (!selected) return ""; + try { + return window.localStorage.getItem(mentorNoteStorageKey(selected)) || ""; + } catch (_error) { + return ""; + } +} + +function saveMentorNote() { + const selected = selectedMentor(); + if (!selected) return; + const input = document.querySelector("#mentorNoteInput"); + if (!input) return; + try { + window.localStorage.setItem(mentorNoteStorageKey(selected), input.value); + } catch (_error) { + showToast("备注保存失败"); + } +} + function renderMentorDirectory() { const mentors = state.mentorSetup?.mentors || []; const query = state.mentorQuery; @@ -70,15 +110,22 @@ function renderMentorDirectory() { return haystack.includes(query); }); setText("mentorCount", filtered.length === mentors.length ? `${mentors.length} 位` : `${filtered.length} / ${mentors.length} 位`); + setText("mentorFilterLabel", state.mentorGrade === "all" ? "全部" : `${state.mentorGrade}级`); const sortToggle = document.querySelector("#mentorSortToggle"); sortToggle.classList.toggle("active", state.mentorSortMode); sortToggle.setAttribute("aria-pressed", String(state.mentorSortMode)); sortToggle.querySelector("span").textContent = state.mentorSortMode ? "完成" : "整理"; document.querySelector("#mentorSortHint").hidden = !state.mentorSortMode; document.querySelector("#mentorSearchInput").disabled = state.mentorSortMode; + document.querySelector("#mentorFilterToggle").disabled = state.mentorSortMode; document.querySelectorAll("[data-mentor-grade]").forEach((button) => { button.disabled = state.mentorSortMode; }); + const filterOptions = document.querySelector("#mentorFilterOptions"); + if (state.mentorSortMode && filterOptions && !filterOptions.hidden) { + filterOptions.hidden = true; + document.querySelector("#mentorFilterToggle").setAttribute("aria-expanded", "false"); + } const container = document.querySelector("#mentorList"); container.classList.toggle("is-sorting", state.mentorSortMode); container.innerHTML = filtered.map((mentor) => { @@ -143,6 +190,36 @@ function toggleMentorSortMode() { renderMentorDirectory(); } +function toggleMentorFilterMenu() { + if (state.mentorSortMode || state.mentorLoading) return; + const options = document.querySelector("#mentorFilterOptions"); + const toggle = document.querySelector("#mentorFilterToggle"); + if (!options || !toggle) return; + const open = options.hidden; + options.hidden = !open; + toggle.setAttribute("aria-expanded", String(open)); + if (open) { + const active = options.querySelector("[data-mentor-grade].active"); + (active || options.querySelector("[data-mentor-grade]"))?.focus({ preventScroll: true }); + } +} + +function closeMentorFilterMenu() { + const options = document.querySelector("#mentorFilterOptions"); + const toggle = document.querySelector("#mentorFilterToggle"); + if (!options || options.hidden) return; + options.hidden = true; + toggle?.setAttribute("aria-expanded", "false"); +} + +function selectMentorGrade(grade) { + state.mentorGrade = grade || "all"; + document.querySelectorAll("[data-mentor-grade]").forEach((button) => { + button.classList.toggle("active", button.dataset.mentorGrade === state.mentorGrade); + }); + renderMentorDirectory(); +} + async function toggleMentorPin(mentorId) { if (state.mentorSavingPreferences) return; const mentors = state.mentorSetup?.mentors || []; @@ -262,6 +339,14 @@ function mentorAvatarText(mentor) { return Array.from(String(mentor?.name || "师").trim())[0] || "师"; } +function mentorMessageTime(message) { + const parsed = new Date(String(message?.created_at || "")); + if (Number.isNaN(parsed.getTime())) { + return new Date().toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }); + } + return parsed.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }); +} + async function selectMentor(mentorId) { if (mentorId === state.selectedMentorId) return; state.selectedMentorId = mentorId; @@ -289,7 +374,7 @@ function renderMentorMessages() {
-
${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}
+
${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")} · ${escapeHtml(mentorMessageTime(message))}
${message.role === "assistant" ? (message.content ? formatMentorAnswer(message.content) : '

正在读取复盘数据并推演...

') : escapeHtml(message.content)}
@@ -300,7 +385,7 @@ function renderMentorMessages() {
`).join(""); } - document.querySelector("#mentorQuickPrompts").hidden = state.mentorMessages.length > 0 || state.mentorLoading; + document.querySelector("#mentorQuickPrompts").hidden = false; document.querySelector("#clearMentorChatButton").disabled = !state.mentorMessages.length || state.mentorLoading; document.querySelector("#mentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId; document.querySelector("#sendMentorQuestion").disabled = state.mentorLoading || !state.selectedMentorId; @@ -342,6 +427,7 @@ async function sendMentorQuestion(event) { const responseMessage = { role: "assistant", content: "", streaming: true, meta: "", followUps: [] }; state.mentorMessages.push(responseMessage); input.value = ""; + syncMentorComposerHeight(); state.mentorLoading = true; state.mentorController = new AbortController(); hideMentorNotice(); @@ -431,6 +517,7 @@ async function streamMentorRequest(body, signal, onDelta, onMeta) { function useMentorQuickPrompt(prompt) { const input = document.querySelector("#mentorQuestion"); input.value = prompt || ""; + syncMentorComposerHeight(); input.focus(); } @@ -529,19 +616,21 @@ function bindMentorEvents() { document.querySelector("#stopMentorQuestion").addEventListener("click", stopMentorGeneration); document.querySelector("#clearMentorChatButton").addEventListener("click", clearMentorConversation); document.querySelector("#mentorSortToggle").addEventListener("click", toggleMentorSortMode); + document.querySelector("#mentorFilterToggle").addEventListener("click", toggleMentorFilterMenu); document.querySelector("#mentorSearchInput").addEventListener("input", (event) => { state.mentorQuery = event.target.value.trim().toLocaleLowerCase("zh-CN"); renderMentorDirectory(); }); document.querySelectorAll("[data-mentor-grade]").forEach((button) => { button.addEventListener("click", () => { - state.mentorGrade = button.dataset.mentorGrade || "all"; - document.querySelectorAll("[data-mentor-grade]").forEach((item) => { - item.classList.toggle("active", item === button); - }); - renderMentorDirectory(); + selectMentorGrade(button.dataset.mentorGrade); + closeMentorFilterMenu(); }); }); + document.addEventListener("click", (event) => { + if (event.target.closest(".mentor-filter-menu")) return; + closeMentorFilterMenu(); + }); document.querySelectorAll("[data-mentor-prompt]").forEach((button) => { button.addEventListener("click", () => useMentorQuickPrompt(button.dataset.mentorPrompt)); }); @@ -550,4 +639,70 @@ function bindMentorEvents() { event.preventDefault(); document.querySelector("#mentorChatForm").requestSubmit(); }); + document.querySelector("#mentorQuestion").addEventListener("input", syncMentorComposerHeight); + document.querySelector("#mentorNoteButton").addEventListener("click", openMentorNoteDialog); + document.querySelector("#mentorProfileButton").addEventListener("click", openMentorProfileDialog); + document.querySelector("#mentorPinButton").addEventListener("click", () => toggleMentorPin(state.selectedMentorId)); + document.querySelector("#mentorNoteInput").addEventListener("input", saveMentorNote); + document.querySelectorAll("[data-mentor-dialog-close]").forEach((button) => { + button.addEventListener("click", () => closeMentorDialog(button.dataset.mentorDialogClose)); + }); + window.addEventListener("resize", centerOpenMentorDialogs); +} + +function selectedMentor() { + return (state.mentorSetup?.mentors || []).find((item) => item.id === state.selectedMentorId) || null; +} + +function syncMentorComposerHeight() { + const input = document.querySelector("#mentorQuestion"); + if (!input) return; + input.style.height = "0"; + const nextHeight = Math.min(input.scrollHeight, 168); + input.style.height = `${nextHeight}px`; + input.style.overflowY = nextHeight >= 168 ? "auto" : "hidden"; +} + +function openMentorDialog(dialogId) { + const dialog = document.querySelector(`#${dialogId}`); + if (!(dialog instanceof HTMLDialogElement)) return; + if (!dialog.open) dialog.showModal(); + positionMentorDialog(dialog); +} + +function closeMentorDialog(dialogId) { + const dialog = document.querySelector(`#${dialogId}`); + if (dialog instanceof HTMLDialogElement && dialog.open) dialog.close(); +} + +function openMentorNoteDialog() { + const input = document.querySelector("#mentorNoteInput"); + if (input) input.value = loadMentorNote(selectedMentor()); + openMentorDialog("mentorNoteDialog"); + const noteInput = document.querySelector("#mentorNoteInput"); + if (noteInput) requestAnimationFrame(() => noteInput.focus()); +} + +function openMentorProfileDialog() { + populateMentorDialogs(selectedMentor()); + openMentorDialog("mentorProfileDialog"); +} + +function positionMentorDialog(dialog) { + if (!(dialog instanceof HTMLDialogElement) || !dialog.open) return; + const viewportPadding = 12; + const rect = dialog.getBoundingClientRect(); + const width = Math.min(400, window.innerWidth - viewportPadding * 2); + const height = Math.min(rect.height, window.innerHeight - viewportPadding * 2); + dialog.style.top = `${Math.max(viewportPadding, Math.round((window.innerHeight - height) / 2))}px`; + dialog.style.left = `${Math.max(viewportPadding, Math.round((window.innerWidth - width) / 2))}px`; + dialog.style.right = "auto"; + dialog.style.bottom = "auto"; + dialog.style.margin = "0"; +} + +function centerOpenMentorDialogs() { + document.querySelectorAll("#mentorNoteDialog, #mentorProfileDialog").forEach((dialog) => { + if (dialog instanceof HTMLDialogElement && dialog.open) positionMentorDialog(dialog); + }); } diff --git a/app/frontend/shared/shell.css b/app/frontend/shared/shell.css index fe99b7c..7ef18ff 100644 --- a/app/frontend/shared/shell.css +++ b/app/frontend/shared/shell.css @@ -3222,3 +3222,127 @@ body.sidebar-collapsed .status-bar { display: none; } } + +/* Dedicated mentor workspace (final day/night spec). Only the active mentor + view enables this immersive state; other pages keep the canonical shell. */ +.app-page-context { + min-width: 0; + flex: 1 1 auto; + flex-direction: column; + justify-content: center; + gap: 2px; + display: none; +} + +#themeModeText { + display: none; +} + +.app-page-context strong { + overflow: hidden; + color: var(--text-primary); + font-size: 18px; + font-weight: var(--font-weight-semibold); + line-height: 1.3; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-page-context span { + overflow: hidden; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (min-width: 1024px) { + body[data-active-view="mentorView"] .main { + width: 100%; + min-width: 0; + margin-left: 0; + height: 100dvh; + display: flex; + flex-direction: column; + overflow: hidden; + } + + body[data-active-view="mentorView"] .module-nav { + display: none; + } + + body[data-active-view="mentorView"] .status-bar { + display: none; + } + + body[data-active-view="mentorView"] .market-tape, + body[data-active-view="mentorView"] .mobile-page-context { + display: none; + } + + body[data-active-view="mentorView"] .overview-strip { + display: none; + } + + body[data-active-view="mentorView"] .app-header { + flex: 0 0 56px; + height: 56px; + min-height: 56px; + gap: 24px; + padding: 0 20px; + background: #F5F6F7; + } + + :root[data-theme="dark"] body[data-active-view="mentorView"] .app-header { + background: #141519; + } + + body[data-active-view="mentorView"] .app-page-context { + display: flex; + } + + body[data-active-view="mentorView"] .header-actions > :is(.global-search-button, .alert-button, .assistant-button, .header-menu-button) { + display: none; + } + + body[data-active-view="mentorView"] .theme-toggle { + width: auto; + height: 32px; + min-height: 32px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 12px; + border-radius: var(--radius-md); + background: transparent; + color: var(--text-secondary); + font-size: var(--font-size-label); + font-weight: var(--font-weight-medium); + } + + body[data-active-view="mentorView"] .theme-toggle .lucide { + width: 16px; + height: 16px; + } + + body[data-active-view="mentorView"] #themeModeText { + display: inline-block; + } + + body[data-active-view="mentorView"] .app-main { + flex: 1 1 auto; + height: calc(100dvh - 56px); + min-height: 0; + display: flex; + flex-direction: column; + padding: 0; + overflow: hidden; + } + + body[data-active-view="mentorView"] .workspace-view.active-view { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; + } +} diff --git a/app/frontend/shared/shell.js b/app/frontend/shared/shell.js index 5b2ccb2..0d2cc6c 100644 --- a/app/frontend/shared/shell.js +++ b/app/frontend/shared/shell.js @@ -60,6 +60,7 @@ const page = registry.get(viewId); const navigationId = page?.navigation_alias || viewId; const marketView = page?.group === "market"; + const previousActiveView = document.body.dataset.activeView; document.body.dataset.activeView = viewId; const mobileGroup = document.querySelector("#mobilePageGroup"); const mobileTitle = document.querySelector("#mobilePageTitle"); @@ -69,6 +70,10 @@ : page?.group === "personal" ? "复盘" : "工具"; } if (mobileTitle) mobileTitle.textContent = page?.title || "小白复盘"; + const pageTitle = document.querySelector("#currentPageTitle"); + if (pageTitle) pageTitle.textContent = page?.title || "小白复盘"; + const pageSubtitle = document.querySelector("#currentPageSubtitle"); + if (pageSubtitle && previousActiveView !== viewId) pageSubtitle.textContent = ""; document.querySelectorAll(".module-tab").forEach((button) => { button.classList.toggle("active", button.dataset.view === navigationId); button.classList.toggle( diff --git a/app/frontend/shared/theme.js b/app/frontend/shared/theme.js index 2a73cc5..9e745d0 100644 --- a/app/frontend/shared/theme.js +++ b/app/frontend/shared/theme.js @@ -13,6 +13,8 @@ function syncThemeControl() { button.setAttribute("aria-label", label); button.setAttribute("aria-pressed", String(dark)); button.querySelector("i")?.setAttribute("data-lucide", dark ? "sun" : "moon"); + const modeText = document.querySelector("#themeModeText"); + if (modeText) modeText.textContent = dark ? "夜间模式" : "日间模式"; } function clearThemeTransitionEffects() { diff --git a/app/frontend/shared/tokens.css b/app/frontend/shared/tokens.css index 26eeb14..7174c4a 100644 --- a/app/frontend/shared/tokens.css +++ b/app/frontend/shared/tokens.css @@ -219,13 +219,12 @@ --dragon-profile-weight-strong: 750; --dragon-profile-weight-semibold: 600; - --mentor-directory-width: 280px; - --mentor-profile-width: 272px; - --mentor-pane-header-height: 58px; - --mentor-avatar-size: 38px; + --mentor-directory-width: 300px; + --mentor-pane-header-height: 60px; + --mentor-avatar-size: 42px; --mentor-profile-avatar-size: 68px; --mentor-composer-min-height: 94px; - --mentor-message-max-width: 82%; + --mentor-message-max-width: 60%; --chart-background: #fbfcfd; --chart-grid: #e2e8ec; diff --git a/app/tests/e2e/app-shell.spec.js b/app/tests/e2e/app-shell.spec.js index ab86c81..d85829b 100644 --- a/app/tests/e2e/app-shell.spec.js +++ b/app/tests/e2e/app-shell.spec.js @@ -526,7 +526,7 @@ test("admin shell opens every primary workspace and global search", async ({ pag const views = [ "auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView", "performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView", "screenerView", - "mentorView", "heavenView", "reviewWorkspaceView", + "heavenView", "reviewWorkspaceView", "mentorView", ]; for (const view of views) { await page.locator(`[data-view="${view}"]`).first().click(); @@ -567,7 +567,7 @@ test("every primary workspace shares the canonical desktop shell geometry", asyn const views = [ "auctionView", "sentimentCycleView", "limitPool", "brokenView", "downView", "yesterdayView", "performanceView", "ladderView", "rotationView", "themeLibraryView", "popularityView", "dragonView", - "screenerView", "mentorView", "heavenView", "reviewWorkspaceView", + "screenerView", "heavenView", "reviewWorkspaceView", ]; let reference = null; for (const view of views) { @@ -2805,7 +2805,8 @@ test("mentor directory exposes evidence filters and private owner metadata", asy await page.goto("/index.html"); await page.locator('[data-view="mentorView"]').first().click(); - await expect(page.locator("#mentorView .mentor-sidebar .mentor-evidence-filters")).toBeVisible(); + await expect(page.locator("#mentorView .mentor-sidebar .mentor-directory-tools")).toBeVisible(); + await expect(page.locator("#mentorFilterToggle")).toBeVisible(); await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22); await expect(page.locator('#mentorList [data-mentor-id="private-owner"] .mentor-badge.private')).toContainText("仅自己"); await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A"); @@ -2815,36 +2816,58 @@ test("mentor directory exposes evidence filters and private owner metadata", asy await expect(page.locator("#mentorList .mentor-option")).toHaveCount(1); await expect(page.locator("#mentorCount")).toHaveText("1 / 22 位"); await page.locator("#mentorSearchInput").fill(""); + await page.locator("#mentorFilterToggle").click(); await page.locator('[data-mentor-grade="B"]').click(); + await expect(page.locator("#mentorFilterLabel")).toHaveText("B级"); await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7); await page.locator('#mentorList [data-mentor-id="source-b"]').click(); await expect(page.locator("#activeMentorName")).toHaveText("多源老师"); await expect(page.locator("#activeMentorBadges")).toHaveText("B"); - await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料"); const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox(); const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox(); - const mentorProfile = await page.locator("#mentorView .mentor-profile-panel").boundingBox(); const mentorInput = await page.locator("#mentorQuestion").boundingBox(); - expect(Math.abs(mentorLibrary.width - 280)).toBeLessThanOrEqual(1); - expect(Math.abs(mentorProfile.width - 272)).toBeLessThanOrEqual(1); + const mentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox(); + expect(Math.abs(mentorLibrary.width - 300)).toBeLessThanOrEqual(1); expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1); - expect(Math.abs(mentorProfile.x - (mentorChat.x + mentorChat.width))).toBeLessThanOrEqual(1); - expect(mentorInput.height).toBeGreaterThanOrEqual(48); + expect(Math.abs(mentorChat.x + mentorChat.width - (mentorLayout.x + mentorLayout.width))).toBeLessThanOrEqual(1); + const composerForm = await page.locator("#mentorView .mentor-chat-form").boundingBox(); + expect(composerForm.height).toBeGreaterThanOrEqual(88); + expect(composerForm.height).toBeLessThanOrEqual(104); + expect(mentorInput.height).toBeGreaterThanOrEqual(24); + expect(mentorInput.height).toBeLessThanOrEqual(34); expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1); await page.setViewportSize({ width: 1920, height: 947 }); const expandedMentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox(); - const statusBar = await page.locator(".status-bar").boundingBox(); - const lowerGap = statusBar.y - (expandedMentorLayout.y + expandedMentorLayout.height); - expect(lowerGap).toBeGreaterThanOrEqual(0); - expect(lowerGap).toBeLessThanOrEqual(16); - await page.locator("#overviewToggle").click(); - const openOverviewLayout = await page.locator("#mentorView .mentor-layout").boundingBox(); - const openOverviewGap = statusBar.y - (openOverviewLayout.y + openOverviewLayout.height); - expect(openOverviewGap).toBeGreaterThanOrEqual(0); - expect(openOverviewGap).toBeLessThanOrEqual(16); + const footerBar = await page.locator("#mentorView .mentor-workspace-footer").boundingBox(); + expect(Math.abs(footerBar.y + footerBar.height - 947)).toBeLessThanOrEqual(1); + expect(Math.abs(expandedMentorLayout.y + expandedMentorLayout.height - footerBar.y)).toBeLessThanOrEqual(1); + expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); expect(await page.evaluate(() => document.documentElement.scrollHeight - window.innerHeight)).toBeLessThanOrEqual(1); }); +test("mentor keeps two columns and opens the profile floating dialog on desktop", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await mockApplication(page, session("admin", true)); + await page.goto("/index.html"); + await page.locator('[data-view="mentorView"]').first().click(); + await page.locator('#mentorList [data-mentor-id="source-b"]').click(); + + await expect(page.locator("#mentorView .mentor-profile-panel")).toHaveCount(0); + const mentorLayout = await page.locator("#mentorView .mentor-layout").boundingBox(); + const mentorLibrary = await page.locator("#mentorView .mentor-sidebar").boundingBox(); + const mentorChat = await page.locator("#mentorView .mentor-chat-panel").boundingBox(); + expect(Math.abs(mentorChat.x - (mentorLibrary.x + mentorLibrary.width))).toBeLessThanOrEqual(1); + expect(Math.abs(mentorChat.x + mentorChat.width - (mentorLayout.x + mentorLayout.width))).toBeLessThanOrEqual(1); + expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1); + + await page.locator("#mentorProfileButton").click(); + await expect(page.locator("#mentorProfileDialog")).toBeVisible(); + await expect(page.locator("#mentorProfileDialogName")).toHaveText("多源老师"); + await expect(page.locator("#mentorProfileDialogEvidence")).toHaveText("公开访谈与多源材料"); + await page.locator('[data-mentor-dialog-close="mentorProfileDialog"]').click(); + await expect(page.locator("#mentorProfileDialog")).not.toBeVisible(); +}); + test("mentor pins, custom order and streamed replies work together", async ({ page }) => { await mockApplication(page, session("admin", true)); await page.goto("/index.html"); @@ -2883,8 +2906,8 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa })); expect(darkUserMessageStyle.background).not.toBe("rgb(255, 255, 255)"); expect(darkUserMessageStyle.border).not.toBe("rgb(255, 255, 255)"); - expect(darkUserMessageStyle.contentBackground).toBe("rgb(35, 54, 74)"); - expect(darkUserMessageStyle.contentColor).toBe("rgb(232, 234, 237)"); + expect(darkUserMessageStyle.contentBackground).toBe("rgb(53, 89, 140)"); + expect(darkUserMessageStyle.contentColor).toBe("rgb(234, 241, 251)"); const darkMessageStyle = await answer.evaluate((element) => { const style = getComputedStyle(element); const content = element.querySelector(".mentor-message-content"); @@ -2906,8 +2929,65 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa expect(darkMessageStyle.shadow).toBe("none"); expect(darkMessageStyle.contentColor).toBe("rgb(232, 234, 237)"); expect(darkMessageStyle.headingColor).toBe("rgb(232, 234, 237)"); - expect(darkMessageStyle.labelColor).toBe("rgb(127, 137, 147)"); - expect(darkMessageStyle.metaColor).toBe("rgb(127, 137, 147)"); + expect(darkMessageStyle.labelColor).toBe("rgb(124, 130, 138)"); + expect(darkMessageStyle.metaColor).toBe("rgb(124, 130, 138)"); +}); + +test("mentor floating dialogs open centered, save notes per account, and keep composer one-line", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await mockApplication(page, session("admin", true)); + await page.goto("/index.html"); + await page.locator('[data-view="mentorView"]').first().click(); + await page.locator('#mentorList [data-mentor-id="source-b"]').click(); + + await page.locator("#mentorNoteButton").click(); + await expect(page.locator("#mentorNoteDialog")).toBeVisible(); + const noteDialog = await page.locator("#mentorNoteDialog").boundingBox(); + expect(noteDialog.width).toBeLessThanOrEqual(400); + expect(Math.abs(noteDialog.x + noteDialog.width / 2 - 720)).toBeLessThanOrEqual(3); + expect(Math.abs(noteDialog.y + noteDialog.height / 2 - 450)).toBeLessThanOrEqual(3); + await page.locator("#mentorNoteInput").fill("多源老师:确认后再行动"); + await expect(page.locator("#mentorNoteInput")).toHaveValue("多源老师:确认后再行动"); + await page.locator('[data-mentor-dialog-close="mentorNoteDialog"]').click(); + await expect(page.locator("#mentorNoteDialog")).not.toBeVisible(); + + await page.locator("#mentorNoteButton").click(); + await expect(page.locator("#mentorNoteInput")).toHaveValue("多源老师:确认后再行动"); + await page.locator('[data-mentor-dialog-close="mentorNoteDialog"]').click(); + + await page.locator("#mentorProfileButton").click(); + await expect(page.locator("#mentorProfileDialog")).toBeVisible(); + await expect(page.locator("#mentorProfileDialogName")).toHaveText("多源老师"); + await expect(page.locator("#mentorProfileDialogEvidence")).toHaveText("公开访谈与多源材料"); + const profileDialog = await page.locator("#mentorProfileDialog").boundingBox(); + expect(profileDialog.width).toBeLessThanOrEqual(400); + expect(Math.abs(profileDialog.x + profileDialog.width / 2 - 720)).toBeLessThanOrEqual(3); + await page.locator('[data-mentor-dialog-close="mentorProfileDialog"]').click(); + await expect(page.locator("#mentorProfileDialog")).not.toBeVisible(); + + const disclaimerStyle = await page.locator("#mentorView .mentor-disclaimer").evaluate((el) => ({ + align: getComputedStyle(el).textAlign, + })); + expect(disclaimerStyle.align).toBe("center"); + + const composerHint = page.locator("#mentorView .mentor-composer-hint"); + await expect(composerHint).toHaveCount(1); + await expect(composerHint).toBeVisible(); + + const composerInput = page.locator("#mentorQuestion"); + const oneLineHeight = (await composerInput.boundingBox()).height; + expect(oneLineHeight).toBeGreaterThanOrEqual(24); + expect(oneLineHeight).toBeLessThanOrEqual(34); + await composerInput.fill("第一行\n第二行"); + const grownHeight = (await composerInput.boundingBox()).height; + expect(grownHeight).toBeGreaterThan(oneLineHeight + 8); + await composerInput.press("Shift+Enter"); + await composerInput.type("第三行"); + await composerInput.fill("只发送这一行"); + await page.locator("#sendMentorQuestion").click(); + await expect(page.locator("#mentorMessages .mentor-message.user").last()).toContainText("只发送这一行"); + const resetHeight = (await composerInput.boundingBox()).height; + expect(resetHeight).toBeLessThanOrEqual(34); }); test("global dialogs share the stage 18 geometry without changing account or admin access", async ({ page }) => { diff --git a/app/tests/test_frontend_contract.py b/app/tests/test_frontend_contract.py index e14b9a0..703c889 100644 --- a/app/tests/test_frontend_contract.py +++ b/app/tests/test_frontend_contract.py @@ -280,13 +280,18 @@ class FrontendContractTests(unittest.TestCase): self.assertNotIn('context.fillStyle = "#6c7983";', chart) def test_dark_mentor_tokens_and_sentiment_bottom_clearance_are_defined(self): - self.assertIn(":root[data-theme=\"dark\"] #mentorView {", self.mentor_styles) - self.assertIn("--mentor-ink: var(--text-primary);", self.mentor_styles) - self.assertIn("--mentor-sub: var(--text-secondary);", self.mentor_styles) - self.assertIn(":root[data-theme=\"dark\"] #mentorView .mentor-message {", self.mentor_styles) - self.assertIn("border-color: var(--line-soft);", self.mentor_styles) - self.assertIn("background: var(--surface-subtle);", self.mentor_styles) - self.assertIn("box-shadow: none;", self.mentor_styles) + self.assertIn(":root[data-theme=\"dark\"] #mentorView.workspace-view {", self.mentor_styles) + self.assertIn("--qp-bg-chat: #232529;", self.mentor_styles) + self.assertIn("--qp-bg-bubble-self: #35598C;", self.mentor_styles) + self.assertIn("--qp-bg-selected: #2B3B58;", self.mentor_styles) + self.assertIn("--qp-link: #316FEF;", self.mentor_styles) + self.assertIn("--qp-accent-soft: #5B8DEF;", self.mentor_styles) + self.assertIn("--mentor-directory-width: 300px;", self.tokens) + self.assertIn("--mentor-composer-min-height: 94px;", self.tokens) + self.assertIn("#mentorView .mentor-message.user .mentor-message-content {", self.mentor_styles) + self.assertIn("background: var(--qp-bg-bubble-self);", self.mentor_styles) + self.assertIn("#mentorView .mentor-quick-prompts button {", self.mentor_styles) + self.assertIn("#mentorView .mentor-composer-hint {", self.mentor_styles) self.assertIn("#sentimentCycleView .sentiment-history-frame {", self.sentiment_styles) self.assertIn("margin-bottom: var(--card-gap);", self.sentiment_styles) self.assertIn("padding-bottom: var(--card-gap);", self.sentiment_styles)