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 <github@multica.ai>
This commit is contained in:
MS-01-Codex
2026-08-18 11:54:57 +08:00
co-authored by multica-agent
parent d95bcb523a
commit d239519299
12 changed files with 1118 additions and 465 deletions
+102 -22
View File
@@ -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 }) => {
+12 -7
View File
@@ -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)