diff --git a/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs b/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs index 5eb933f06f..cbdfab2824 100644 --- a/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs +++ b/apps/presentation/dashboard/smoke/research-analysis-read-smoke.mjs @@ -20,7 +20,7 @@ const study = caseFixture.analyses[0].case_study; const studyNavigation = packId => ({url: new URL('http://127.0.0.1/chat/'), goalId: 'child', packId, namespace: null, follow: () => {throw Error('read navigated');}}); const renderedStudy = renderToStaticMarkup(createElement(ResearchCaseStudy, { - study, zh: false, historical: true, scope: caseFixture.analyses[0].input.scope, + study, zh: false, historical: true, active: true, scope: caseFixture.analyses[0].input.scope, navigation: studyNavigation('saved-pack'), onCitation: () => {throw Error('read executed a citation');}, })); @@ -35,6 +35,7 @@ const casePack = {pack_id: "one", title: "First study", scope: caseFixture.analy case_study: study, case_study_summary: {title: study.title}, applicability: "current"}; const secondCase = {...casePack, pack_id: "two", title: "Second study"}; const renderCase = packs => renderToStaticMarkup(createElement(ResearchCaseReading, { + active: true, analysis: {zh: false, pack: casePack, snapshot: {catalog: {packs}}, location: {open: false}, studyNavigation}, })); assert.ok(!renderCase([casePack]).includes(" new Promise(resolvePaint => requestAnimationFrame(() => requestAnimationFrame(resolvePaint)))); assert.equal(await citation.count(), 0, "Late original response cannot reopen the previous evidence"); - console.log("research evidence version: saved and late original excerpts isolated"); + + // Open a saved chapter through the same workspace router used by the tabs + // and parent links. Browser Back must keep the previous visit's position. + localizedReady = false; + const child = JSON.parse(await readFile(resolve(import.meta.dirname, "../src/data/fixtures/case-study-child.json"), "utf8")); + const parent = {goal_id: child.parent_goal_id, display_name: "Benchmark", events: [], analyses: []}; + const study = await start("study", child, parent); + const studyPack = (await (await fetch(new URL(analysisPath + "?" + new URLSearchParams({goal_id: child.goal_id}), study))).json()).pack; + const chapter = studyPack.case_study.sections[1]; + const address = new URL("/chat/", study); + address.search = new URLSearchParams({goalId: child.goal_id, view: "research", research_goal: child.goal_id, + research_reading: "case", research_analysis_goal: child.goal_id, research_pack: studyPack.pack_id}); + address.hash = "case-section-" + studyPack.pack_id + "-" + chapter.id; + const atChapter = () => page.waitForFunction(id => document.activeElement?.id === id, address.hash.slice(1)); + await page.goto(address.href); + await atChapter(); + const tabs = page.locator(".personal-goal-tabs"); + await tabs.getByRole("button", {name: "任务", exact: true}).click(); + await page.waitForURL(url => url.searchParams.get("view") === "tasks"); + assert.equal(new URL(page.url()).hash, address.hash); + await tabs.getByRole("button", {name: "研究", exact: true}).click(); + await atChapter(); + await page.locator("button.research-parent-goal").click(); + await page.waitForURL(url => url.searchParams.get("goalId") === parent.goal_id); + function assertParentAddress() { + const url = new URL(page.url()); + assert.equal(url.hash, ""); + assert.equal([...url.searchParams.keys()].some(key => key.startsWith("research_")), false, + "The parent's address belongs to its own research"); + } + assertParentAddress(); + await page.goBack(); + await atChapter(); + assert.equal(new URL(page.url()).searchParams.get("research_pack"), studyPack.pack_id); + await page.reload(); + await atChapter(); + await page.goForward(); + await page.waitForURL(url => url.searchParams.get("goalId") === parent.goal_id); + assertParentAddress(); + console.log("research evidence version: saved excerpts isolated and chapter navigation restored"); } finally { releaseOriginal?.(); await browser?.close(); @@ -110,12 +149,15 @@ try { await rm(root, {recursive: true, force: true}); } -async function start(name, capture) { - const fixture = resolve(root, name + ".json"); - await writeFile(fixture, JSON.stringify(capture)); +async function start(name, ...captures) { + const fixtures = await Promise.all(captures.map(async (capture, index) => { + const path = resolve(root, name + "-" + index + ".json"); + await writeFile(path, JSON.stringify(capture)); + return path; + })); const child = spawn(resolveTestPython({repoRoot: repo}), [ "-u", "examples/personal-workspace-browser/research-history-backend.py", - "--fixture", fixture, "--workspace-directory", "--assets-dir", assets, + ...fixtures.flatMap(path => ["--fixture", path]), "--workspace-directory", "--assets-dir", assets, ], {cwd: repo, env: {...process.env, PYTHONPATH: repo}, stdio: ["ignore", "pipe", "pipe"]}); servers.push(child); let error = ""; 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 9fcced13eb..1e55e0f8d5 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 @@ -396,7 +396,7 @@ export function ResearchReportSummary({analysis: a}: {analysis: Controller}) { onExpand={origin => {a.trigger.current = origin; a.show();}} onCitation={a.readCitation}/> : null; } -export function ResearchCaseReading({analysis: a, onHistory}: {analysis: Controller; onHistory: () => void}) { +export function ResearchCaseReading({analysis: a, active, onHistory}: {analysis: Controller; active: boolean; onHistory: () => void}) { const packs = (a.snapshot?.catalog.packs ?? []).filter(p => p.case_study_summary); const p = a.pack; const firstStudy = packs[0]?.pack_id; @@ -410,7 +410,7 @@ export function ResearchCaseReading({analysis: a, onHistory}: {analysis: Control {packs.map(pack => )} : null} - {p?.case_study ? : !a.loading && !a.readError ?

{a.zh ? "选择一份解读,阅读问题背景、策略差异、关键转折与证据。" : "Choose a study to read its background, approaches, turning points and evidence."}

: null} ; 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 5c30414da6..5876a80b50 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 @@ -514,7 +514,7 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist onSelect={strategies.choose} onClose={() => strategies.choose(null)}/>; } if (caseReading && (hasCases || awaitingRequestedCase)) { - return setCaseMode(false)}/>; + return setCaseMode(false)}/>; } return
{hasCases ? : null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx b/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx index 536e5d6982..047f2e7cea 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.tsx @@ -52,8 +52,8 @@ function focusChapter(chapter: HTMLElement) { } /** A document's author controls the composition; the host owns navigation and evidence. */ -export function ResearchCaseStudy({study, zh, historical, scope, navigation, onCitation}: { - study: TrajectoryCaseStudy; zh: boolean; historical: boolean; +export function ResearchCaseStudy({study, zh, historical, active, scope, navigation, onCitation}: { + study: TrajectoryCaseStudy; zh: boolean; historical: boolean; active: boolean; scope: {case_id: string; run_id: string; revision: string; window: string}; navigation: {url: URL; goalId: string; packId: string; namespace: string | null; follow: (href: URL) => void}; onCitation: (c: AssessmentCitation, origin?: HTMLButtonElement) => void; @@ -62,6 +62,7 @@ export function ResearchCaseStudy({study, zh, historical, scope, navigation, onC const {url, goalId, packId, namespace, follow} = navigation; const prefix = `${caseChapterHashPrefix.slice(1)}${namespace ? namespace + '-' : ''}${packId}-`; useEffect(() => { + if (!active) return; // The URL can arrive before the saved study is mounted, or while history // switches to another Goal/version. Only restore a visible matching chapter. let frame = 0; @@ -84,7 +85,7 @@ export function ResearchCaseStudy({study, zh, historical, scope, navigation, onC window.removeEventListener('popstate', restore); window.removeEventListener('hashchange', restore); }; - }, [goalId, packId, prefix]); + }, [active, goalId, packId, prefix]); const citationLinks = (citations: AssessmentCitation[]) =>
    {citations.map((c, i) =>