diff --git a/app/frontend/pages/mentor/foundation.css b/app/frontend/pages/mentor/foundation.css index 5a1e3d4..0cb2e7c 100644 --- a/app/frontend/pages/mentor/foundation.css +++ b/app/frontend/pages/mentor/foundation.css @@ -250,20 +250,20 @@ align-items: center; justify-content: center; border: 1px solid var(--mentor-blue-line); - border-radius: 50%; + border-radius: var(--radius-md); background: var(--mentor-blue-soft); color: var(--mentor-blue-dark); font-size: var(--font-size-card-title); font-weight: var(--font-weight-semibold); } -#mentorView .mentor-avatar[data-grade="a"] { +#mentorView :is(.mentor-avatar, .mentor-message-avatar, .mentor-profile-avatar)[data-grade="a"] { border-color: var(--market-up); background: var(--market-up-soft); color: var(--market-up); } -#mentorView .mentor-avatar[data-grade="c"] { +#mentorView :is(.mentor-avatar, .mentor-message-avatar, .mentor-profile-avatar)[data-grade="c"] { border-color: var(--market-down); background: var(--market-down-soft); color: var(--market-down); @@ -427,61 +427,10 @@ background: var(--surface-subtle); } -#mentorView .mentor-chat-header { - 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-12); - padding: 0 var(--space-16); - border-bottom: 1px solid var(--mentor-line); - background: var(--surface); -} - -#mentorView .mentor-chat-identity { - min-width: 0; - display: flex; - align-items: center; - gap: var(--space-8); -} - -#mentorView .mentor-chat-identity > div { - min-width: 0; -} - -#mentorView .mentor-active-title { - min-width: 0; - display: flex; - align-items: center; - gap: var(--space-8); -} - -#mentorView .mentor-active-title h3 { - min-width: 0; - margin: 0; - overflow: hidden; - color: var(--mentor-ink); - font-size: var(--font-size-card-title); - font-weight: var(--font-weight-semibold); - letter-spacing: 0; - text-overflow: ellipsis; - white-space: nowrap; -} - -#mentorView .mentor-active-badges { - display: inline-flex; - gap: var(--space-4); -} - -#mentorView #activeMentorStatus { - margin: var(--space-4) 0 0; - color: var(--mentor-faint); - font-size: var(--font-size-caption); -} - #mentorView .mentor-clear-button { - flex: 0 0 auto; + width: 40px; + height: 40px; + flex: 0 0 40px; color: var(--mentor-sub); } @@ -554,7 +503,7 @@ align-items: center; justify-content: center; border: 1px solid var(--mentor-line); - border-radius: 50%; + border-radius: var(--radius-md); background: var(--surface); color: var(--mentor-sub); font-size: var(--font-size-label); @@ -568,6 +517,7 @@ } #mentorView .mentor-message-body { + min-width: 0; max-width: var(--mentor-message-max-width); display: flex; flex-direction: column; @@ -579,6 +529,20 @@ align-items: flex-end; } +#mentorView .mentor-message-stack { + min-width: 0; + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--space-8); +} + +#mentorView .mentor-message-stack > .mentor-message-content { + width: fit-content; + max-width: 100%; +} + #mentorView .mentor-message-label { color: var(--mentor-faint); font-size: var(--font-size-caption); @@ -618,6 +582,7 @@ #mentorView .mentor-answer-paragraph { margin: 0 0 var(--space-8); + text-wrap: pretty; } #mentorView .mentor-answer-paragraph:last-child { @@ -630,6 +595,7 @@ color: var(--mentor-ink); font-size: var(--font-size-body); font-weight: var(--font-weight-semibold); + text-wrap: balance; } #mentorView .mentor-answer-heading:first-child { @@ -844,7 +810,7 @@ align-items: center; justify-content: center; border: 1px solid var(--mentor-blue-line); - border-radius: 50%; + border-radius: var(--radius-md); background: var(--mentor-blue-soft); color: var(--mentor-blue-dark); font-size: var(--font-size-metric); diff --git a/app/frontend/pages/mentor/page.html b/app/frontend/pages/mentor/page.html index 40b6a94..ee40da3 100644 --- a/app/frontend/pages/mentor/page.html +++ b/app/frontend/pages/mentor/page.html @@ -33,16 +33,7 @@
-
-
- -
-

--

-

思维模型已就绪

-
-
- -
+

思维模型已就绪

开始一个话题 @@ -56,6 +47,7 @@
Enter 发送 · Shift + Enter 换行 +
diff --git a/app/frontend/pages/mentor/page.js b/app/frontend/pages/mentor/page.js index 5c3e03e..22a59bd 100644 --- a/app/frontend/pages/mentor/page.js +++ b/app/frontend/pages/mentor/page.js @@ -36,14 +36,12 @@ function renderMentorWorkspace() { if (!setup) return; const selected = setup.mentors.find((item) => item.id === state.selectedMentorId) || null; setText("mentorDataDate", `数据日期 ${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("#mentorProfileAvatar").dataset.grade = String(selected?.evidence?.grade || "").toLowerCase(); document.querySelector("#mentorProfileBadges").innerHTML = selected ? renderMentorBadges(selected, true) : ""; setText("activeMentorEvidence", selected?.evidence?.note || selected?.description || "--"); document.querySelector("#activeMentorFocus").innerHTML = (selected?.focus || []).slice(0, 4) @@ -287,12 +285,14 @@ function renderMentorMessages() { } else { container.innerHTML = state.mentorMessages.map((message) => `
- +
${message.role === "user" ? "我" : escapeHtml(selected?.name || "问师")}
-
${message.role === "assistant" - ? (message.content ? formatMentorAnswer(message.content) : '

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

') - : escapeHtml(message.content)}
+ ${message.role === "assistant" + ? `
${message.content + ? formatMentorAnswer(message.content) + : '

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

'}
` + : `
${escapeHtml(message.content)}
`} ${message.streaming ? '' : ""} ${renderMentorFollowUps(message)} ${message.meta && !message.streaming ? `${escapeHtml(message.meta)}` : ""} @@ -478,12 +478,14 @@ function hideMentorNotice() { } function formatMentorAnswer(content) { - const blocks = []; + const sections = [[]]; + let currentSection = sections[0]; + let headingCount = 0; let listType = ""; let listItems = []; const flushList = () => { if (!listItems.length) return; - blocks.push(`<${listType} class="mentor-answer-list">${listItems.map((item) => `
  • ${item}
  • `).join("")}`); + currentSection.push(`<${listType} class="mentor-answer-list">${listItems.map((item) => `
  • ${item}
  • `).join("")}`); listItems = []; listType = ""; }; @@ -493,18 +495,23 @@ function formatMentorAnswer(content) { flushList(); return; } - const heading = line.match(/^#{1,3}\s+(.+)$/); + const heading = mentorAnswerHeading(line); const bullet = line.match(/^[-*]\s+(.+)$/); const ordered = line.match(/^\d+[.、]\s*(.+)$/); if (heading) { flushList(); - blocks.push(`${formatMentorInline(escapeHtml(heading[1]))}`); + if (currentSection.length) { + currentSection = []; + sections.push(currentSection); + } + headingCount += 1; + currentSection.push(`${formatMentorInline(escapeHtml(heading))}`); } else if (/^-{3,}$/.test(line)) { flushList(); - blocks.push(''); + currentSection.push(''); } else if (line.startsWith("> ")) { flushList(); - blocks.push(`${formatMentorInline(escapeHtml(line.slice(2)))}`); + currentSection.push(`${formatMentorInline(escapeHtml(line.slice(2)))}`); } else if (bullet || ordered) { const nextType = bullet ? "ul" : "ol"; if (listType && listType !== nextType) flushList(); @@ -512,11 +519,27 @@ function formatMentorAnswer(content) { listItems.push(formatMentorInline(escapeHtml((bullet || ordered)[1]))); } else { flushList(); - blocks.push(`

    ${formatMentorInline(escapeHtml(line))}

    `); + currentSection.push(`

    ${formatMentorInline(escapeHtml(line))}

    `); } }); flushList(); - return blocks.join(""); + const populatedSections = sections.filter((section) => section.length); + if (headingCount < 2) { + return `
    ${populatedSections.flat().join("")}
    `; + } + return populatedSections.map((section) => ( + `
    ${section.join("")}
    ` + )).join(""); +} + +function mentorAnswerHeading(line) { + const markdownHeading = line.match(/^#{1,3}\s+(.+)$/); + if (markdownHeading) return markdownHeading[1].trim(); + const boldHeading = line.match(/^\*\*([^*]+)\*\*$/); + if (!boldHeading) return ""; + const title = boldHeading[1].trim(); + if (!title || title.length > 32 || /[。!?!?;;::]$/.test(title)) return ""; + return title; } function formatMentorInline(content) { diff --git a/app/frontend/shared/tokens.css b/app/frontend/shared/tokens.css index 26eeb14..676683b 100644 --- a/app/frontend/shared/tokens.css +++ b/app/frontend/shared/tokens.css @@ -222,8 +222,8 @@ --mentor-directory-width: 280px; --mentor-profile-width: 272px; --mentor-pane-header-height: 58px; - --mentor-avatar-size: 38px; - --mentor-profile-avatar-size: 68px; + --mentor-avatar-size: 46px; + --mentor-profile-avatar-size: 80px; --mentor-composer-min-height: 94px; --mentor-message-max-width: 82%; diff --git a/app/tests/e2e/app-shell.spec.js b/app/tests/e2e/app-shell.spec.js index b55e081..ca6b0fe 100644 --- a/app/tests/e2e/app-shell.spec.js +++ b/app/tests/e2e/app-shell.spec.js @@ -489,8 +489,8 @@ async function mockApplication(page, authSession = session(), options = {}) { status: 200, contentType: "application/x-ndjson; charset=utf-8", body: [ - JSON.stringify({ type: "delta", content: "## 判断\n先看市场结构。\n\n" }), - JSON.stringify({ type: "delta", content: "- 等待确认\n- 控制仓位" }), + JSON.stringify({ type: "delta", content: "**判断**\n先看市场结构。\n\n" }), + JSON.stringify({ type: "delta", content: "**操作**\n- 等待确认\n- 控制仓位" }), JSON.stringify({ type: "meta", data_trade_date: "20260722", @@ -2818,9 +2818,24 @@ test("mentor directory exposes evidence filters and private owner metadata", asy await page.locator('[data-mentor-grade="B"]').click(); 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("#mentorProfileName")).toHaveText("多源老师"); + await expect(page.locator("#mentorProfileBadges")).toHaveText("B"); await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料"); + await expect(page.locator("#mentorView .mentor-chat-header")).toHaveCount(0); + await expect(page.locator("#mentorChatForm #clearMentorChatButton")).toBeVisible(); + const avatarGeometry = await page.evaluate(() => { + const directoryAvatar = document.querySelector("#mentorList .mentor-option.active .mentor-avatar"); + const profileAvatar = document.querySelector("#mentorProfileAvatar"); + return { + directoryWidth: directoryAvatar.getBoundingClientRect().width, + directoryRadius: getComputedStyle(directoryAvatar).borderRadius, + profileWidth: profileAvatar.getBoundingClientRect().width, + profileRadius: getComputedStyle(profileAvatar).borderRadius, + }; + }); + expect(avatarGeometry.directoryWidth).toBeGreaterThanOrEqual(46); + expect(avatarGeometry.profileWidth).toBeGreaterThan(avatarGeometry.directoryWidth); + expect(avatarGeometry.profileRadius).toBe(avatarGeometry.directoryRadius); 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(); @@ -2864,6 +2879,8 @@ test("mentor pins, custom order and streamed replies work together", async ({ pa await page.locator("#sendMentorQuestion").click(); const answer = page.locator("#mentorMessages .mentor-message.assistant").last(); await expect(answer).toContainText("先看市场结构。"); + await expect(answer.locator(".mentor-message-content")).toHaveCount(2); + await expect(answer.locator(".mentor-answer-heading")).toHaveCount(2); await expect(answer.locator(".mentor-answer-list li")).toHaveCount(2); await expect(answer.locator("br")).toHaveCount(0); await expect(page.locator("#mentorMessages .assistant-stream-caret")).toHaveCount(0);