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 8fa9dae101..909a015134 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 @@ -1,6 +1,10 @@ .research-analysis { margin-block: 16px; padding: 24px; border: 1px solid var(--pw-line); border-radius: 12px; min-width: 0; color: var(--pw-text); background: var(--pw-bg); font-size: var(--text-body); line-height: var(--leading-prose); overflow-wrap: anywhere; } .research-analysis > header, .analysis-source > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; } .analysis-header-actions { display: flex; gap: 8px; flex-shrink: 0; } +.analysis-report-title { display: flex; align-items: center; gap: 4px; } +.analysis-report-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 16px; margin-block-start: 4px; } +.research-analysis .analysis-report-meta > p { margin: 0; font-size: var(--text-body); } +.analysis-report-meta > span { color: var(--pw-muted); font-size: var(--text-body); } .research-analysis-entry { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; } .research-analysis-entry-actions, .analysis-scope { display: flex; align-items: center; gap: 4px; } .research-analysis-entry > small { font-size: var(--text-caption); color: var(--pw-muted); } 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 1ddb58b09c..7c6c492504 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 @@ -1,5 +1,5 @@ import {useCallback, useEffect, useLayoutEffect, useRef, useState} from "react"; -import {ArrowLeft, ArrowUpRight, ChevronDown, CircleHelp, LoaderCircle, RefreshCw, Sparkles, TriangleAlert, X} from "lucide-react"; +import {ArrowLeft, ArrowUpRight, ChevronDown, CircleHelp, History, LoaderCircle, RefreshCw, Sparkles, TriangleAlert, X} from "lucide-react"; import type {AssessmentCitation, TrajectoryAssessment, TrajectoryEpisode, TrajectoryOutputLanguage, TrajectoryContinuation} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; import {ResearchProgress, episodeQuestionGroups, metricCopy, verdictLabel, type ResearchProgressPack} from "./goal-research-progress"; import {requestJson} from "../../data/chat"; @@ -487,18 +487,23 @@ export function ResearchAnalysisPanel({analysis: a, onHistory}: {analysis: Contr : benefit?.unknown ? (zh ? "原目标收益的检验情况仍未知" : "Original-objective utility testing remains unknown") : benefit?.yes ? (zh ? "模型记录了收益检验;这不等于收益为正" : "The model identified a utility test; this does not imply positive utility") : (zh ? "此方法未覆盖原目标收益检验" : "This method does not cover original-objective utility testing"); + const scope = p ? <>{p.episode_count} {zh ? "个研究片段" : "research episodes"} · {p.selection.exhaustive ? (zh ? "固定范围" : "Fixed scope") : (zh ? "定向抽样" : "Purposive sample")} : null; + const scopeHelp = p ? + {result?.synthesis ?

{zh ? "Agent 分析 · 未校准" : "Agent analysis · uncalibrated"}

: null} +

{p.selection.exhaustive ? (zh ? "覆盖此固定分析范围;不等于覆盖全部研究。" : "Covers this fixed analysis scope, not necessarily the whole research history.") : (zh ? "定向抽样,仅分析选定研究片段;范围外或未捕获的工作未纳入,不代表全程。" : "Purposive sample of selected episodes; work outside this scope or not captured is excluded. It does not represent the whole run.")}

+

{zh ? "正向诊断、证据采用与最终任务收益分别判断。没有综合分数,也不据此推断因果或整体研究质量。引用存在不代表模型解释已被独立核验。" : "Diagnostic value, evidence adoption and task utility are separate. There is no total score, causal claim or whole-run quality rating. Citations do not independently verify model interpretations."}

+ {result ? Object.values(metricCopy).map(label =>

{label[zh ? 0 : 1]} · {label[zh ? 2 : 3]}

) : null} +

{zh ? "下方仍显示当前研究 Goal 的已记录历史。" : "The current research Goal’s recorded history remains below."}

+
: null; return
{if (event.key === "Escape") {event.stopPropagation(); a.close();}}}> -

{result?.synthesis ? (zh ? "研究汇报" : "Research report") : (zh ? "轨迹深度分析" : "Trajectory analysis")}

{p?.title ?

{p.title}

: null}
{result?.synthesis && onHistory ? : null}{result ? : null}
+
{result?.synthesis ? <>

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

{scopeHelp}
+
{p?.title ?

{p.title}

: null}{scope}
+ : <>

{zh ? "轨迹深度分析" : "Trajectory analysis"}

{p?.title ?

{p.title}

: null}}
{result?.synthesis && onHistory ? : null}{result ? : null}
{a.error ?
{zh ? "分析请求未完成,已保存结果保留。" : "The analysis request did not complete; saved results are preserved."}

{a.error}

: null} {a.snapshot?.catalog.unreadable_pack_ids?.length && !p?.continuation_unavailable ?

{zh ? "部分历史分析暂不可读" : "Some historical analyses cannot be read"} · {a.snapshot.catalog.unreadable_pack_ids.length}

: null} {p ? <> -
{p.episode_count} {zh ? "个研究片段" : "research episodes"} · {p.selection.exhaustive ? (zh ? "固定范围" : "Fixed scope") : (zh ? "定向抽样" : "Purposive sample")} -

{p.selection.exhaustive ? (zh ? "覆盖此固定分析范围;不等于覆盖全部研究。" : "Covers this fixed analysis scope, not necessarily the whole research history.") : (zh ? "定向抽样,仅分析选定研究片段;范围外或未捕获的工作未纳入,不代表全程。" : "Purposive sample of selected episodes; work outside this scope or not captured is excluded. It does not represent the whole run.")}

-

{zh ? "正向诊断、证据采用与最终任务收益分别判断。没有综合分数,也不据此推断因果或整体研究质量。引用存在不代表模型解释已被独立核验。" : "Diagnostic value, evidence adoption and task utility are separate. There is no total score, causal claim or whole-run quality rating. Citations do not independently verify model interpretations."}

- {result ? Object.values(metricCopy).map(label =>

{label[zh ? 0 : 1]} · {label[zh ? 2 : 3]}

) : null} -

{zh ? "下方仍显示当前研究 Goal 的已记录历史。" : "The current research Goal’s recorded history remains below."}

-
+ {!result?.synthesis ?
{scope}{scopeHelp}
: null} {p.result && !sourceAvailable ?

{reasonCopy(p.reason_code, zh)}

: null} {p.result_attempt && p.result_attempt !== p.job?.attempt ?

{zh ? "当前重试尚未完成,显示上次已保存分析。" : "The current retry is incomplete; showing the previously saved analysis."}

: null} 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 c1ed04e35c..8e06134e73 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 @@ -266,7 +266,7 @@ .research-reading-order button { min-height: 44px; padding: 6px 0; background: none; border: 0; color: var(--pw-text); font: inherit; font-size: var(--text-body); cursor: pointer; } .research-reading-order > span { color: var(--pw-muted); } .research-reading-layer { min-width: 0; scroll-margin-block-start: 16px; } -.research-reading-layer .research-synthesis { margin-block: 24px 32px; } +.research-reading-layer .research-synthesis:not(.research-synthesis-detail) { margin-block: 24px 32px; } .research-reading-layer:focus, .research-history-heading:focus { outline: none; } @media (max-width: 640px) { diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.css index 563ecdfe63..d6cdfc547b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-synthesis.css @@ -1,7 +1,6 @@ .research-synthesis { margin-block: 24px; max-width: 82ch; color: var(--pw-text); font-size: var(--text-lead); line-height: var(--leading-prose); overflow-wrap: anywhere; } -.research-synthesis > header { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 16px; } +.research-synthesis > header { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } .research-synthesis :is(h3,h4,h5,h6) { margin: 0; color: var(--pw-text); font-size: inherit; font-weight: 600; line-height: var(--leading-body); } -.research-synthesis > header > span { color: var(--pw-muted); font-size: var(--text-body); } .research-synthesis .research-synthesis-summary { margin-block: 12px 8px; } .research-synthesis p, .research-progress .research-synthesis p, .research-analysis .research-synthesis p { margin-block: 8px 12px; font-size: var(--text-lead); color: var(--pw-text); } .research-synthesis summary { cursor: pointer; min-height: 44px; padding-block: 10px; } @@ -21,11 +20,12 @@ .research-synthesis button:hover { text-decoration: underline; text-underline-offset: 3px; } .research-synthesis .research-review-episodes { display: inline-flex; align-items: center; gap: 6px; min-height: 44px; padding: 4px 0; border: 0; background: none; color: var(--pw-text); font: inherit; font-size: var(--text-body); cursor: pointer; } .research-synthesis button:focus-visible, .research-synthesis summary:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } -.research-synthesis-detail { max-width: none; } +.research-synthesis-detail { max-width: none; margin-block-start: 16px; } .research-report-tabs { display: flex; gap: 24px; border-block-end: 1px solid var(--pw-line); margin-block: 16px; } -.research-report-tabs > button { min-height: 44px; border: 0; border-block-end: 2px solid transparent; padding: 10px 0; background: none; color: var(--pw-muted); font: inherit; cursor: pointer; } -.research-report-tabs > button[aria-selected="true"] { border-block-end-color: var(--pw-text); color: var(--pw-text); } -.research-report-tabs > button > span { margin-inline-start: 8px; color: var(--pw-muted); font-size: var(--text-body); } +.research-synthesis-detail .research-report-tabs { margin-block-start: 0; } +.research-synthesis .research-report-tabs > button { min-height: 44px; border: 0; border-block-end: 2px solid transparent; border-radius: 0; padding: 10px 0; background: none; color: var(--pw-muted); font: inherit; cursor: pointer; } +.research-synthesis .research-report-tabs > button[aria-selected="true"] { border-block-end-color: var(--pw-text); color: var(--pw-text); } +.research-synthesis .research-report-tabs > button > span { margin-inline-start: 8px; color: var(--pw-muted); font-size: var(--text-body); } .research-report-comparison { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; } .research-report-finding, .research-report-experiment { border-block-start: 1px solid var(--pw-line); padding-block: 16px; } .research-report-finding > h5, .research-report-experiment h5 { margin-block-end: 8px; } @@ -39,12 +39,12 @@ @media (max-width: 640px) { .research-analysis.has-synthesis > header { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; } .research-analysis.has-synthesis > header > div:first-child { display: contents; } - .research-analysis.has-synthesis > header h3 { grid-column: 1; grid-row: 1; align-self: center; } - .research-analysis.has-synthesis > header p { grid-column: 1 / -1; grid-row: 2; } + .research-analysis.has-synthesis > header .analysis-report-title { grid-column: 1; grid-row: 1; } + .research-analysis.has-synthesis > header .analysis-report-meta { grid-column: 1 / -1; grid-row: 2; } .research-analysis.has-synthesis > header .analysis-header-actions { grid-column: 2; grid-row: 1; } .research-report-comparison { grid-template-columns: 1fr; gap: 12px; } .research-synthesis dl { grid-template-columns: 1fr; gap: 4px; } .research-synthesis dd { margin-block-end: 12px; } - .research-synthesis > header { gap: 4px 12px; } + .research-synthesis > header { gap: 4px; } } .research-synthesis-detail { container-type: inline-size; } 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 737b24f99b..cb993e5879 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 @@ -1,6 +1,7 @@ import {useId, useState, type ReactNode} from "react"; -import {ArrowDownRight, ArrowUpRight} from "lucide-react"; +import {ArrowDownRight, ArrowUpRight, Sparkles} from "lucide-react"; import type {AssessmentCitation, TrajectoryResearchSynthesis} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; +import {ContextHelp} from "./context-help"; import "./goal-research-synthesis.css"; type Props = {synthesis: TrajectoryResearchSynthesis; zh: boolean; detail?: boolean; @@ -78,9 +79,12 @@ export function ResearchSynthesis({synthesis: s, zh, detail = false, onExpand, o ; } return
-
{!detail ?

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

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

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

+ +

{zh ? "此汇报是 Agent 对选定证据的分析;引用可读不等于解释已被独立核验。" : "This report is the Agent’s analysis of selected evidence; readable citations do not independently verify its interpretation."}

+
+ {onExpand ? : null} +
: null} {!detail ? overview : null} {body}
; diff --git a/examples/personal-workspace-browser/evidence/research-report-header/after-desktop.png b/examples/personal-workspace-browser/evidence/research-report-header/after-desktop.png new file mode 100644 index 0000000000..b53af12997 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-report-header/after-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/research-report-header/after-phone.png b/examples/personal-workspace-browser/evidence/research-report-header/after-phone.png new file mode 100644 index 0000000000..514fb1c119 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-report-header/after-phone.png differ diff --git a/examples/personal-workspace-browser/evidence/research-report-header/before-desktop.png b/examples/personal-workspace-browser/evidence/research-report-header/before-desktop.png new file mode 100644 index 0000000000..13b14f2030 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-report-header/before-desktop.png differ diff --git a/examples/personal-workspace-browser/evidence/research-report-header/before-phone.png b/examples/personal-workspace-browser/evidence/research-report-header/before-phone.png new file mode 100644 index 0000000000..2080d48ffd Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-report-header/before-phone.png differ diff --git a/examples/personal-workspace-browser/evidence/research-report-header/report-phone-en.png b/examples/personal-workspace-browser/evidence/research-report-header/report-phone-en.png new file mode 100644 index 0000000000..6bfa5d0ba1 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-report-header/report-phone-en.png differ diff --git a/examples/personal-workspace-browser/evidence/research-report-header/scope-help-phone.png b/examples/personal-workspace-browser/evidence/research-report-header/scope-help-phone.png new file mode 100644 index 0000000000..112cb8f945 Binary files /dev/null and b/examples/personal-workspace-browser/evidence/research-report-header/scope-help-phone.png differ diff --git a/examples/personal-workspace-browser/research-history.mjs b/examples/personal-workspace-browser/research-history.mjs index 509dfa48dd..3806404883 100644 --- a/examples/personal-workspace-browser/research-history.mjs +++ b/examples/personal-workspace-browser/research-history.mjs @@ -225,7 +225,7 @@ export const researchHistoryScenario = { // Its fixture is synthetic: these checks qualify reading and provenance, // not the scientific usefulness of a real model's interpretation. const synthesisOwner = await startHistoryOwner(resolve(repoRoot, "apps/presentation/dashboard/src/data/fixtures/research-synthesis-readback.json")); - const synthesisPage = await browser.newPage({viewport: {width: 1280, height: 900}}); + const synthesisPage = await browser.newPage({viewport: {width: 1280, height: 900}, hasTouch: true}); try { await synthesisPage.addInitScript(() => {if (!localStorage.getItem('loopx-pw-locale')) localStorage.setItem('loopx-pw-locale', 'zh-CN');}); const query = new URLSearchParams({goalId: 'synthetic-research-readback', view: 'research', statusUrl: '/status.json?goal_id=synthetic-research-readback'}); @@ -236,6 +236,13 @@ export const researchHistoryScenario = { assert.equal(await synthesis.locator('.research-synthesis-finding').count(), 2); assert(await synthesis.locator('.research-synthesis-summary').evaluate(e => parseFloat(getComputedStyle(e).fontSize) >= 16), 'Main research account must be readable body text'); assert.equal(await synthesis.locator('details[open]').count(), 0, 'Ordinary reading starts with observations; reasoning is optional detail'); + const authorship = synthesis.getByRole('button', {name: 'Agent 分析 · 未校准', exact: true}); + await authorship.focus(); + await synthesisPage.getByRole('tooltip').getByText(/引用可读不等于解释已被独立核验/).waitFor(); + await synthesisPage.keyboard.press('Escape'); + assert(await authorship.evaluate(e => e === document.activeElement), 'Dismissing provenance keeps the ordinary report in focus'); + await authorship.evaluate(e => e.blur()); + await synthesisPage.mouse.move(1, 1); assert.equal(await synthesisPage.locator('.research-effect-metrics:visible').count(), 0, 'Metric counts do not replace the research account'); await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-ordinary-desktop.png'), animations: 'disabled'}); assert(await synthesisPage.evaluate(() => { @@ -280,6 +287,8 @@ export const researchHistoryScenario = { assert(await synthesisPage.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-ordinary-mobile.png'), animations: 'disabled'}); await reading.getByRole('button', {name: '汇报', exact: true}).click(); + await synthesisPage.mouse.move(1, 1); + if (await synthesisPage.locator('.context-help-popover:popover-open').count()) await synthesisPage.keyboard.press('Escape'); await synthesisPage.screenshot({path: resolve(outputDir, 'research-synthesis-report-mobile.png'), animations: 'disabled'}); await synthesisPage.getByRole('button', {name: '深入解读', exact: true}).click(); const analysis = synthesisPage.getByRole('region', {name: '轨迹深度分析', exact: true}); @@ -297,6 +306,31 @@ export const researchHistoryScenario = { await synthesisPage.setViewportSize(viewport); await synthesisPage.reload({waitUntil: 'domcontentloaded'}); await analysis.locator('.research-synthesis-summary').waitFor(); + assert.match(await analysis.locator('.analysis-report-meta').innerText(), /固定窗口.*3 个研究片段.*定向抽样/s, + 'The selected report identity, episode count and partial sampling remain visible'); + const scopeHelp = analysis.getByRole('button', {name: '分析范围', exact: true}); + await scopeHelp.hover(); + await synthesisPage.getByRole('tooltip').waitFor({state: 'visible'}); + await synthesisPage.getByRole('tooltip').hover(); + await synthesisPage.keyboard.press('Escape'); + await scopeHelp.focus(); + const scopeInfo = synthesisPage.getByRole('tooltip'); + await scopeInfo.getByText('Agent 分析 · 未校准', {exact: true}).waitFor(); + assert.match(await scopeInfo.innerText(), /不代表全程.*引用存在不代表模型解释已被独立核验/s); + await synthesisPage.keyboard.press('Escape'); + assert(await scopeHelp.evaluate(e => e === document.activeElement)); + assert(await analysis.isVisible(), 'Closing scope help does not close the report'); + if (viewport.width === 390) { + await scopeHelp.tap(); + await scopeInfo.waitFor({state: 'visible'}); + assert(await scopeInfo.evaluate(e => e.scrollWidth <= e.clientWidth + 1), 'The same full scope explanation is readable by touch'); + await synthesisPage.keyboard.press('Escape'); + } + await analysis.evaluate(e => e.scrollIntoView({block: 'start'})); + assert(await analysis.locator('.research-report-account').first().locator(':scope > div').nth(1).locator('p').evaluate(e => { + const body = e.closest('.personal-channel-scroll').getBoundingClientRect(), paragraph = e.getBoundingClientRect(); + return paragraph.top >= body.top && paragraph.bottom <= body.bottom; + }), 'The first short finding exposes both its observation and interpretation without scrolling past repeated metadata'); await synthesisPage.screenshot({path: resolve(outputDir, `research-deep-link-${viewport.width}.png`), animations: 'disabled'}); assert(await analysis.locator('.research-synthesis-summary').evaluate(e => { const box = e.getBoundingClientRect(); @@ -406,6 +440,11 @@ export const researchHistoryScenario = { await synthesisPage.setViewportSize({width: 390, height: 844}); await synthesisPage.getByRole('button', {name: 'Read deeper', exact: true}).click(); const english = synthesisPage.locator('.research-analysis'); + assert.match(await english.locator('.analysis-report-meta').innerText(), /3 research episodes.*Purposive sample/); + await english.getByRole('button', {name: 'Analysis scope', exact: true}).focus(); + await synthesisPage.getByRole('tooltip').getByText('Agent analysis · uncalibrated', {exact: true}).waitFor(); + await synthesisPage.keyboard.press('Escape'); + assert(await english.getByRole('button', {name: 'Goal research history', exact: true}).isVisible()); const englishAccount = english.locator('.research-report-account').first(); assert(await englishAccount.getByRole('heading', {name: 'Observation and evidence', exact: true}).isVisible()); assert(await englishAccount.getByRole('heading', {name: 'Interpretation', exact: true}).isVisible());