diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css index 28a01787ed..ced97b1564 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css @@ -260,6 +260,7 @@ .research-history-detail .research-finding-exceptions { font-size: var(--text-body); color: var(--pw-muted); margin: 0; } .research-recorded-finding > h3 { max-width: none; font-size: var(--text-heading); line-height: var(--leading-heading); margin: 8px 0; } .research-reader-states { display: flex; align-items: center; flex-wrap: wrap; gap: 4px 12px; } +.research-reader-return { display: flex; align-items: center; gap: 4px; } .research-reader-question { margin-block: 4px 12px; font-size: var(--text-body); } .research-reader-question > summary { display: flex; align-items: center; gap: 8px; min-height: 44px; cursor: pointer; color: var(--pw-muted); } .research-reader-question > summary::before { content: "▸"; flex-shrink: 0; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx index e7b74a9db3..6c764cef2b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx @@ -247,6 +247,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re const selected = value?.nodes.find(node => node.id === selectedId); const selectedFinding = selected?.findings.find(finding => finding.result_id === selection.finding) ?? (!selection.finding ? selected?.findings.at(-1) : undefined); + const hasSeparateQuestion = Boolean(selected?.findings.length && selectedFinding?.title !== selected.latest.title); const selectedGroup = selection.day !== null ? grouped.groups.find(group => group.day === selection.day) : undefined; const selectedMembers = selectedGroup?.nodes ?? (selection.nodes.length > 1 ? value?.nodes.filter(node => selection.nodes.includes(node.id)) : undefined); const requestedCollection = selection.collection; @@ -501,8 +502,9 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re {selected ?
{if (event.key === "Escape") {event.stopPropagation(); closeReader();}}}>
-
-
+
+ {!hasQuestionNavigation && hasSeparateQuestion && !questionExpanded ?

{selected.latest.title}

{selected.latest.summary ?

{selected.latest.summary}

: null}
: null} +
{kindLabel(selected.latest.node_kind)} · {stateLabel(selected.latest.status)}

{findingCopy.meaning}

{selected.findings.length > 1 && findingExceptions ?

{findingExceptions}

: null}
@@ -520,7 +522,11 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re })}
: null} - {!hasQuestionNavigation ? selected.findings.length ? selectedFinding?.title !== selected.latest.title ?
setQuestionExpanded(event.currentTarget.open)}> + {!hasQuestionNavigation ? selected.findings.length ? hasSeparateQuestion && questionExpanded ?
{ + if (event.currentTarget.open) return; + if (event.currentTarget.contains(document.activeElement)) detail.current?.focus({preventScroll: true}); + setQuestionExpanded(false); + }}> {selected.latest.title} {selected.latest.summary ?

{selected.latest.summary}

: null}
: null :

{selected.latest.title}

: null}{displayControl} diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-dense-finding-en-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-dense-finding-en-390.png new file mode 100644 index 0000000000..75812b068f Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-dense-finding-en-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-en-1440.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-en-1440.png new file mode 100644 index 0000000000..d6fc563883 Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-en-1440.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-en-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-en-390.png new file mode 100644 index 0000000000..fa6bebdd53 Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-en-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-question-help-en-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-question-help-en-390.png new file mode 100644 index 0000000000..a86645f8e5 Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-question-help-en-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-question-route-en-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-question-route-en-390.png new file mode 100644 index 0000000000..75eff1d88e Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-question-route-en-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-unavailable-en-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-unavailable-en-390.png new file mode 100644 index 0000000000..49fb85c702 Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-unavailable-en-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-zh-CN-1440.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-zh-CN-1440.png new file mode 100644 index 0000000000..c77dd1aa4f Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-zh-CN-1440.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/after-zh-CN-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/after-zh-CN-390.png new file mode 100644 index 0000000000..eeb1cf5bca Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/after-zh-CN-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/before-en-1440.png b/docs/development/assets/runtime-source-analysis/reader-question-context/before-en-1440.png new file mode 100644 index 0000000000..be0974ec21 Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/before-en-1440.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/before-en-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/before-en-390.png new file mode 100644 index 0000000000..25d0da59ca Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/before-en-390.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/before-zh-CN-1440.png b/docs/development/assets/runtime-source-analysis/reader-question-context/before-zh-CN-1440.png new file mode 100644 index 0000000000..8d73a5fb51 Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/before-zh-CN-1440.png differ diff --git a/docs/development/assets/runtime-source-analysis/reader-question-context/before-zh-CN-390.png b/docs/development/assets/runtime-source-analysis/reader-question-context/before-zh-CN-390.png new file mode 100644 index 0000000000..e1301a5fdd Binary files /dev/null and b/docs/development/assets/runtime-source-analysis/reader-question-context/before-zh-CN-390.png differ diff --git a/docs/development/design.md b/docs/development/design.md index 90b71b6f68..38cf1fcf95 100644 --- a/docs/development/design.md +++ b/docs/development/design.md @@ -192,6 +192,12 @@ the chart. Show one finding at a time with its declared status and full text; keep all findings selectable and failed or tentative counts visible. Preserve the exact finding on reload and Back. Offer the whole recorded history in place and through the existing return action, without letting it bury the chosen text. +When opening a specific finding, put its parent question in accessible contextual +help rather than another collapsed title row. Opening a route's question still +reveals that complete question directly; these two reading intents are distinct. + +打开具体结论时,所属研究问题放进可悬浮、聚焦和点按的上下文帮助,避免再堆一行 +折叠标题;从路线打开问题时仍直接显示完整问题,不增加必经操作。 Decision history groups records by their recorded question identity, with the question title once above its successive decisions. Matching titles alone do diff --git a/examples/personal-workspace-browser/research-route-reading.mjs b/examples/personal-workspace-browser/research-route-reading.mjs index 5b93c20918..6df8e6229d 100644 --- a/examples/personal-workspace-browser/research-route-reading.mjs +++ b/examples/personal-workspace-browser/research-route-reading.mjs @@ -136,6 +136,17 @@ export const researchRouteReadingScenario = { const question = reader.locator(".research-reader-content > .research-reader-question > summary > span"); assert.equal(await question.innerText(), fullTitle, "One route activation reveals the complete original question"); assert(await question.evaluate(element => element.scrollHeight <= element.clientHeight + 1 && element.scrollWidth <= element.clientWidth + 1), "Requested question text is not clipped"); + if (!endpoint) { + const summary = reader.locator(".research-reader-question > summary"); + await summary.focus(); + await page.keyboard.press("Enter"); + await page.waitForFunction(() => document.activeElement?.classList.contains("research-history-detail")); + assert.equal(await reader.locator(".research-reader-question").count(), 0, "Closing the question preserves reader focus without a redundant title row"); + const context = reader.getByRole("button", {name: language === "zh-CN" ? "完整研究问题" : "Full research question", exact: true}); + await context.tap(); + assert.equal(await reader.locator(".research-question-text p").first().innerText(), fullTitle, "Touch can recover the complete question after collapse"); + await page.keyboard.press("Escape"); + } if (endpoint) { await reader.locator("select").selectOption("finding-route-0-end"); const body = reader.getByText("完整观测:局部质量下降,延迟改善;完整成本和独立验证仍未知。", {exact: true}).first(); @@ -145,10 +156,14 @@ export const researchRouteReadingScenario = { assert(box.y >= reading.top - 1 && box.y + Math.min(box.height, lineHeight * 2) <= reading.bottom + 1, "The selected finding begins with readable content inside the reading viewport"); assert(await body.evaluate(element => parseFloat(getComputedStyle(element).fontSize) >= 16), "Density retains readable finding text"); assert.match(await reader.locator(".research-finding-exceptions").innerText(), /1/); - const context = reader.locator(".research-reader-content > .research-reader-question > summary"); - await context.click(); - assert(await context.evaluate(element => element.scrollHeight <= element.clientHeight + 1 && element.scrollWidth <= element.clientWidth + 1), "Full parent context remains one optional activation away"); - await context.click(); + assert.equal(await reader.locator(".research-reader-question").count(), 0, "Finding reading does not repeat the parent question in a collapsed title row"); + const context = reader.getByRole("button", {name: language === "zh-CN" ? "完整研究问题" : "Full research question", exact: true}); + await context.focus(); + const fullQuestion = reader.locator(".research-question-text p").first(); + assert.equal(await fullQuestion.innerText(), fullTitle, "Full parent context remains available through keyboard focus"); + assert(await fullQuestion.evaluate(element => parseFloat(getComputedStyle(element).fontSize) >= 16 && element.scrollWidth <= element.clientWidth + 1), "Original question remains readable without horizontal scrolling"); + await page.keyboard.press("Escape"); + assert(await context.evaluate(element => element === document.activeElement), "Dismissing question help keeps focus at its trigger"); } if (endpoint && language === "zh-CN" && width !== 768) await page.screenshot({path: resolve(outputDir, `research-route-reading-detail-${width}.png`), animations: "disabled"}); await page.keyboard.press("Escape"); @@ -181,7 +196,9 @@ export const researchRouteReadingScenario = { await reader.getByRole("button", {name: "Whole graph", exact: true}).click(); await page.goBack(); await reader.waitFor(); - assert.match(await reader.locator(".research-reader-question").innerText(), /末端研究/); + await reader.getByRole("button", {name: "Full research question", exact: true}).click(); + assert.match(await reader.locator(".research-question-text").innerText(), /末端研究/); + await page.keyboard.press("Escape"); // A grouped reading context survives the document/URL boundary, rather // than living only in an in-memory opener ref. Navigation is a read. for (const locale of ["zh-CN", "en"]) for (const width of [1440, 390]) {