diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css index 5851785b3a..8fa9dae101 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css @@ -69,13 +69,16 @@ .analysis-method > summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; color: var(--pw-text); } .analysis-method dl { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 8px 16px; } .analysis-method dd { margin: 0; color: var(--pw-text); } -.analysis-continuation { margin-block: 16px; border-block: 1px solid var(--pw-line); padding-block: 12px; } +.analysis-continuation { margin-block: 8px; } +.analysis-continuation-controls { display: flow-root; } +.analysis-continuation-controls > details { min-width: 0; } +.analysis-continuation-controls > button { float: inline-end; margin-inline-start: 16px; } +.analysis-continuation-controls > details[open] { padding-block-end: 12px; } .analysis-continuation-summary { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; } .analysis-continuation-summary strong { font-weight: 500; } .analysis-continuation-summary > span { color: var(--pw-muted); } -.analysis-continuation-summary button { margin-inline-start: auto; } .analysis-continuation .analysis-method { border: 0; margin: 0; padding: 0; } -.analysis-continuation .analysis-method > summary { display: list-item; list-style-position: inside; min-height: 36px; } +.analysis-continuation .analysis-method > summary { display: list-item; list-style-position: inside; min-height: 44px; } .analysis-spin { animation: analysis-request-spin 1s linear infinite; } @keyframes analysis-request-spin { to { transform: rotate(360deg); } } @media (prefers-reduced-motion: reduce) { .analysis-spin { animation: none; } } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx index 5c6e34c7c8..f05a3026db 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx @@ -229,32 +229,28 @@ const analysisActionLabel = (p: Pack | null, zh: boolean) => p?.continuation?.ca function AnalysisContinuation({pack: p, analysis: a}: {pack: Pack; analysis: Controller}) { const c = p.continuation, zh = a.zh; if (!c) return p.continuation_unavailable ?

{zh ? "基础分析暂不可读;本版仍可独立分析完整范围。" : "The base analysis cannot be read; this version can still assess its complete scope independently."}

: null; - const changes = [ - c.sources.added.length ? `${zh ? "新增来源" : "New sources"} ${c.sources.added.length}` : "", - c.sources.revised.length ? `${zh ? "更新来源" : "Updated sources"} ${c.sources.revised.length}` : "", - c.sources.removed.length ? `${zh ? "移除来源" : "Removed sources"} ${c.sources.removed.length}` : "", - c.episodes.added.length ? `${zh ? "新增片段" : "New episodes"} ${c.episodes.added.length}` : "", - c.episodes.revised.length ? `${zh ? "更新片段" : "Updated episodes"} ${c.episodes.revised.length}` : "", - c.episodes.removed.length ? `${zh ? "移除片段" : "Removed episodes"} ${c.episodes.removed.length}` : "", - c.can_reuse ? (zh ? "复用原判读" : "Reuse judgments") : `${zh ? (p.result ? "已复核全部" : "完整复核") : "Reassess all"} ${p.episode_count} ${zh ? "片段" : "episodes"}`, - ].filter(Boolean).join(" · "); + const reassessment = c.can_reuse ? (zh ? "复用原判读" : "Reuse judgments") + : `${zh ? (p.result ? "已复核全部" : "完整复核") : "Reassess all"} ${p.episode_count} ${zh ? "片段" : "episodes"}`; return
-
{c.can_reuse ? (zh ? "证据内容未变" : "Evidence content unchanged") : (zh ? "接续上一版" : "Continue from previous version")} - {changes} +
-
- {!c.base_available ?

{zh ? "上一版尚无适用分析,本次将独立分析完整范围。" : "No applicable previous analysis is available. This version will assess the full scope independently."}

: null} - {c.configuration_changed || c.objective_changed ?

{zh ? "分析设置或目标已变更,两版判断不作为同条件比较。" : "The analysis configuration or objective changed. Judgments are not a matched comparison."}

: null} -
{p.result && c.base_available ? `${c.verdict_changes.length} ${zh ? "项判断变化" : "changed judgments"}` : (zh ? "查看变化" : "Inspect changes")} -

{c.base_title} · {c.base_scope.revision} → {c.current_scope.revision}

-

{zh ? "上次覆盖" : "Previous coverage"}: {c.base_episode_count} · {c.base_scope.window}
{zh ? "本次覆盖" : "Current coverage"}: {p.episode_count} · {c.current_scope.window}

-

{zh ? "来源(新增 / 更正 / 删除)" : "Sources (added / revised / removed)"}: {c.sources.added.length} / {c.sources.revised.length} / {c.sources.removed.length}
{zh ? "片段(新增 / 更正 / 删除)" : "Episodes (added / revised / removed)"}: {c.episodes.added.length} / {c.episodes.revised.length} / {c.episodes.removed.length}
{zh ? "适用判断项(新增 / 移除)" : "Eligible judgments (added / removed)"}: {c.new_pair_count} / {c.removed_pair_count}

+
{zh ? "版本与变化" : "Version and changes"} +
{c.can_reuse ? (zh ? "证据内容未变" : "Evidence content unchanged") : (zh ? "接续上一版" : "Continue from previous version")}{reassessment}
+

{p.result && c.base_available ? `${c.verdict_changes.length} ${zh ? "项判断变化" : "changed judgments"}` : (zh ? "查看变化" : "Inspect changes")}

+
{zh ? "上一版" : "Previous"}
{c.base_title} · {c.base_scope.revision}
{c.base_episode_count} · {c.base_scope.window}
+
{zh ? "本版" : "Current"}
{c.current_scope.revision}
{p.episode_count} · {c.current_scope.window}
+
{zh ? "来源变动" : "Source changes"}
{zh ? "新增 / 更正 / 删除" : "Added / revised / removed"}: {c.sources.added.length} / {c.sources.revised.length} / {c.sources.removed.length}
+
{zh ? "片段变动" : "Episode changes"}
{zh ? "新增 / 更正 / 删除" : "Added / revised / removed"}: {c.episodes.added.length} / {c.episodes.revised.length} / {c.episodes.removed.length}
+
{zh ? "适用判断项" : "Eligible judgments"}
{zh ? "新增 / 移除" : "Added / removed"}: {c.new_pair_count} / {c.removed_pair_count}
{c.episodes.added.length ?

{zh ? "新增片段" : "New episodes"}: {c.episodes.added.join(" · ")}

: null} {c.episodes.revised.length ?

{zh ? "更正片段" : "Revised episodes"}: {c.episodes.revised.join(" · ")}

: null} {c.episodes.removed.length ?

{zh ? "移除片段" : "Removed episodes"}: {c.episodes.removed.join(" · ")}

: null}

{zh ? "无法仅凭局部变化证明旧结论不受影响,因此实质变化均复核完整新版证据;指标按本版适用项重新计算,旧版不覆盖、不相加。" : "Local changes cannot prove earlier conclusions unaffected. Substantive changes therefore reassess the complete new evidence. Metrics are recalculated from this version’s eligible items; previous results are neither overwritten nor added."}

{c.verdict_changes.length ?
    {c.verdict_changes.map(row =>
  • {row.episode_id} · {metricCopy[row.metric][zh ? 0 : 1]}: {verdictLabel(row.metric, row.before, zh)} → {verdictLabel(row.metric, row.after, zh)}
  • )}
: null} -
+
+
+ {!c.base_available ?

{zh ? "上一版尚无适用分析,本次将独立分析完整范围。" : "No applicable previous analysis is available. This version will assess the full scope independently."}

: null} + {c.configuration_changed || c.objective_changed ?

{zh ? "分析设置或目标已变更,两版判断不作为同条件比较。" : "The analysis configuration or objective changed. Judgments are not a matched comparison."}

: null}
; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.tsx index be2a43e580..18cb2d9c56 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.tsx @@ -27,6 +27,10 @@ export function ResearchSynthesis({synthesis: s, zh, detail = false, onExpand, o
{zh ? "预期区分" : "Distinguishing prediction"}
{experiment.prediction}
{zh ? "推翻解释的结果" : "Disconfirming result"}
{experiment.disconfirming_result}
; + const overview = <> +

{s.summary.text}

+
{zh ? "概览来源" : "Summary sources"} · {s.summary.citations.length}{citations(s.summary.citations)}
+ ; let body: ReactNode; if (!detail) { body = <> @@ -48,6 +52,7 @@ export function ResearchSynthesis({synthesis: s, zh, detail = false, onExpand, o }}>{next ? (zh ? "下一步实验" : "Next experiments") : (zh ? "结论与解释" : "Findings and explanations")} {next ? s.next_experiments.length : s.findings.length})}
+ {!experiments ? overview : null} {experiments ?
{!s.next_experiments.length ?

{zh ? "现有证据不足以提出有区分力的下一步实验。" : "The evidence does not justify a discriminating next experiment."}

: null} {s.next_experiments.map((experiment, i) =>
@@ -73,8 +78,7 @@ export function ResearchSynthesis({synthesis: s, zh, detail = false, onExpand, o
{!detail ?

{zh ? "研究汇报" : "Research report"}

: null}{zh ? "Agent 分析" : "Agent analysis"} {!detail && onExpand ? : null}
-

{s.summary.text}

-
{zh ? "概览来源" : "Summary sources"} · {s.summary.citations.length}{citations(s.summary.citations)}
+ {!detail ? overview : null} {body} ; } diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/report-next-experiment-mobile.png b/examples/personal-workspace-browser/evidence/research-finding-reader/report-next-experiment-mobile.png new file mode 100644 index 0000000000..e7aa8b9b2d Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/report-next-experiment-mobile.png differ diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-after-desktop.png b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-after-desktop.png new file mode 100644 index 0000000000..79746e2435 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-after-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-after-mobile.png b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-after-mobile.png new file mode 100644 index 0000000000..876ce8a659 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-after-mobile.png differ diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-before-desktop.png b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-before-desktop.png new file mode 100644 index 0000000000..55fa6da53a Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-before-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-before-mobile.png b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-before-mobile.png new file mode 100644 index 0000000000..7a55819183 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-finding-reader/report-reading-before-mobile.png differ diff --git a/examples/personal-workspace-browser/research-history.mjs b/examples/personal-workspace-browser/research-history.mjs index 55fe13ac43..d68696d797 100644 --- a/examples/personal-workspace-browser/research-history.mjs +++ b/examples/personal-workspace-browser/research-history.mjs @@ -218,9 +218,15 @@ export const researchHistoryScenario = { await analysis.waitFor(); assert.equal(await synthesisPage.getByRole('region', {name: '研究汇报', exact: true}).count(), 1, 'One reading focus when expanded'); assert.match(await analysis.innerText(), /竞争解释.*随机波动/s); + const savedOverview = await analysis.locator('.research-synthesis-summary').innerText(); + assert(await analysis.locator('.research-report-tabs').evaluate(tabs => { + const overview = tabs.parentElement.querySelector('.research-synthesis-summary'); + return Boolean(overview && (tabs.compareDocumentPosition(overview) & Node.DOCUMENT_POSITION_FOLLOWING)); + }), 'Choose findings or experiments before reading a potentially long overview'); await analysis.getByRole('tab', {name: /结论与解释/}).focus(); await synthesisPage.keyboard.press('ArrowRight'); assert.equal(await analysis.getByRole('tab', {name: /下一步实验/}).getAttribute('aria-selected'), 'true'); + assert.equal(await analysis.locator('.research-synthesis-summary').count(), 0, 'Experiment reading starts with the experiment instead of repeating the report overview'); assert.match(await analysis.innerText(), /固定条件.*随机序列.*推翻解释的结果/s); assert.equal(await analysis.locator('.analysis-method').first().evaluate(e => e.open), false, 'Episode judgments remain available as secondary verification'); await synthesisPage.mouse.move(1, 1); @@ -228,6 +234,7 @@ export const researchHistoryScenario = { await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-deep-mobile.png'), animations: 'disabled'}); await synthesisPage.setViewportSize({width: 1280, height: 900}); await analysis.getByRole('tab', {name: /结论与解释/}).click(); + assert.equal(await analysis.locator('.research-synthesis-summary').innerText(), savedOverview, 'Returning to findings restores the complete saved overview'); await analysis.evaluate(e => e.scrollIntoView({block: 'start'})); await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-deep-desktop.png'), animations: 'disabled'}); // The report already declares its episode lineage. Follow those exact @@ -1371,13 +1378,15 @@ print(json.dumps([ assert.equal(await chineseAnalysis.locator('.analysis-episode[open]').count(), 0); continuationFixture = true; await page.reload({waitUntil: 'networkidle'}); - // The inline version summary is a semantic section, not another page. + // Version bookkeeping is optional detail; the saved analysis leads. const version = chineseAnalysis.locator('.analysis-continuation'); - await version.getByText('接续上一版', {exact: true}).waitFor(); - await version.getByText(/已复核全部 4 片段/).waitFor(); + await version.getByText('版本与变化', {exact: true}).waitFor(); assert.equal(await version.locator('details').getAttribute('open'), null); - await version.locator('summary').click(); - assert.match(await version.innerText(), /original-capture → later-evidence/); + assert.doesNotMatch(await version.innerText(), /新增来源|已复核全部|original-capture/); + await version.locator('summary').focus(); + await page.keyboard.press('Enter'); + await version.getByText(/已复核全部 4 片段/).waitFor(); + assert.match(await version.innerText(), /original-capture.*later-evidence/s); assert.match(await version.innerText(), /variant-alpha.*已做收益检验.*证据不足/); await version.locator('summary').click(); await page.setViewportSize({width: 1440, height: 1000}); @@ -1388,9 +1397,9 @@ print(json.dumps([ await chineseAnalysis.getByText('Synthetic original analysis', {exact: true}).waitFor(); assert.equal(await chineseAnalysis.locator('.analysis-continuation').count(), 0); await page.goBack({waitUntil: 'networkidle'}); - await version.getByText('接续上一版', {exact: true}).waitFor(); + await version.getByText('版本与变化', {exact: true}).waitFor(); await page.reload({waitUntil: 'networkidle'}); - await version.getByText('接续上一版', {exact: true}).waitFor(); + await version.getByText('版本与变化', {exact: true}).waitFor(); await page.setViewportSize({width: 390, height: 844}); await chineseAnalysis.evaluate(element => element.scrollIntoView({block: 'start'})); assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));