Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,16 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
const recordUnit = read.complete && history.canonical && read.events.length > history.canonical.events.length
? (zh ? "条原始记录" : "source rows") : read.total === 1 ? copy.record : copy.records;
const emptySourceHistory = hasNativeSource && emptyHistory;
const emptyWithAnalysis = emptyHistory && Boolean(analysis.pack?.result || analysis.pack?.native_outcomes);
const hasSavedResult = Boolean(analysis.pack?.result || analysis.pack?.native_outcomes);
const emptyWithAnalysis = emptyHistory && hasSavedResult;
const historyBeforeAnalysisSummary = hasNativeSource && Boolean(value?.nodes.length) && !hasSavedResult;
// Reorder the existing reader in the DOM so visual and keyboard order agree.
// Keeping its identity also preserves a focused scope selector while loading.
const progressSummary = !progressiveOverview && !readingSelection
? <ResearchProgressSummary key="progress" analysis={analysis}/> : null;
const analysisDetails = !progressiveOverview ? <Fragment key="analysis">
<ResearchAnalysisPanel analysis={analysis} onHistory={showHistory}/><ResearchCitationPanel analysis={analysis}/>
</Fragment> : null;
const emptyHistoryMessage = zh ? "这个 Goal 尚无研究历程" : "No research history for this Goal yet";
const hasCases = (analysis.snapshot?.catalog.packs ?? []).some(p => p.case_study_summary);
// Keep an explicitly opened study in place during a read failure. A complete
Expand Down Expand Up @@ -586,15 +595,16 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
<div role="status"><strong>{emptyHistoryMessage}</strong><ContextHelp icon={FlaskConical} label={zh ? "研究历程来源" : "Research history source"}><p>{zh ? "开启研究模式后,后续研究问题、尝试与证据会在这里形成可回读的历程。" : "Enable research mode to build a readable history of questions, attempts and evidence from later work."}</p></ContextHelp></div>
<button type="button" onClick={focusResearchMode}>{zh ? "查看研究模式" : "Review research mode"}</button>
</div> : null}
{!progressiveOverview ? <><ResearchAnalysisStatus analysis={analysis}/>
{!readingSelection ? <ResearchProgressSummary analysis={analysis}/> : null}
<ResearchAnalysisPanel analysis={analysis} onHistory={showHistory}/><ResearchCitationPanel analysis={analysis}/></> : null}
{busy && !read.events.length ? <div className="research-history-loading" role="status" aria-label={copy.loading}>
{!progressiveOverview ? <ResearchAnalysisStatus analysis={analysis}/> : null}
{[
historyBeforeAnalysisSummary ? null : progressSummary,
analysisDetails,
busy && !read.events.length ? <div key="loading" className="research-history-loading" role="status" aria-label={copy.loading}>
<span className="research-history-loading-label">{copy.loading}…</span>
<div className="research-history-loading-phases" aria-hidden>{Array.from({length: 3}, (_, index) => <span key={index}/>)}</div>
<div className="research-history-loading-chart" aria-hidden>{Array.from({length: 18}, (_, index) => <span key={index}/>)}</div>
</div> : null}
{value?.nodes.length ? <div className={`research-history-layout${selected || selectedMembers?.length ? " has-selection" : ""}`}>
</div> : null,
value?.nodes.length ? <div key="history" className={`research-history-layout${selected || selectedMembers?.length ? " has-selection" : ""}`}>

{selected ? <div className="research-history-detail" ref={detail} role="region" aria-label={selected.latest.title} tabIndex={-1} onKeyDown={event => {if (event.key === "Escape") {event.stopPropagation(); closeReader();}}}>
<div className="research-reader-content">
Expand Down Expand Up @@ -686,7 +696,9 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
<summary hidden={!readingSelection}>{zh ? "研究全景" : "Research overview"}</summary>
{readingSelection ? <ResearchProgressSummary analysis={analysis}/> : null}{historyView}
</details>
</div> : null}
</div> : null,
historyBeforeAnalysisSummary ? progressSummary : null,
]}
{(selection.nodes.some(id => !value?.nodes.some(node => node.id === id)) || (selection.day !== null && !selectedGroup)) && read.complete && !busy && !error && !history.error ? <p role="status">{copy.lost}</p> : null}
{value?.events.length && !value.nodes.length ? <ContextHelp label={zh ? "节点结构未记录" : "Node structure not recorded"} icon={CircleHelp}><p className="research-history-note">{zh ? "尚未记录节点结构;可展开下面的已读记录。" : "Node structure has not been recorded; loaded records remain available below."}</p></ContextHelp> : null}
{value && (value.missingLinks.length || value.unknownTimeCount || value.unknownKindCount) ? <ContextHelp icon={CircleHelp} label={zh ? "研究记录覆盖缺口" : "Research record coverage gaps"}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ function ResearchGoalNavigation({goalId, goals, zh, onSelect, directoryUrl, acti
const children = tree.children.get(goalId) ?? [];
if (!parent && !children.length) return null;
return <nav className="research-goal-navigation" aria-label={zh ? '研究 Goal 层级' : 'Research Goal hierarchy'}>
{parent ? <button type="button" className="research-parent-goal" onClick={() => onSelect(parent.goalId)}>← {parent.title}</button> : null}
{parent ? <button type="button" className="research-parent-goal" title={parent.title} onClick={() => onSelect(parent.goalId)}>
<span aria-hidden="true">←</span><span className="research-parent-title">{parent.title}</span>
</button> : null}
{children.length ? <ResearchChildGoals goals={children} zh={zh} onSelect={onSelect}
directoryUrl={directoryUrl} active={active} showTiming refreshRevision={refreshRevision}/> : null}
</nav>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
.research-goal-navigation { padding-block: 0 12px; }
.research-goal-navigation h2 { font-size: 22px; font-weight: 600; margin-block: 16px; }
.research-goal-navigation button { font: inherit; color: var(--pw-text); background: none; text-align: start; cursor: pointer; min-height: 44px; }
.research-parent-goal { border: 0; padding: 8px 0; }
.research-parent-goal { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; border: 0; padding: 8px 0; }
.research-parent-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.research-child-heading { display: flex; align-items: baseline; flex-wrap: wrap; column-gap: 12px; }
.research-child-heading > span, .research-child-timing { color: var(--pw-muted); font-size: var(--text-caption); }
.research-child-goals { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px,100%), 1fr)); gap: 16px; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {pathToFileURL} from "node:url";
import type {Browser} from "playwright";
import {launchBrowser, loadPlaywright} from "../dashboard-browser-smoke-support.mjs";
import {resolveTestPython} from "../../scripts/test-python.mjs";
import {analysisReadback} from "./research-history.mjs";

const root = resolve(import.meta.dirname, "../..");
const output = resolve(root, "output/playwright/research-source-recovery");
Expand Down Expand Up @@ -130,12 +131,50 @@ threading.Event().wait()
await coverage.filter({hasText:"33 / 33"}).waitFor();
await page.goForward();
await coverage.filter({hasText:"11 / 11"}).waitFor();

const empty = analysisReadback("c");
empty.pack.result = null;
empty.pack.job = null;
const saved = analysisReadback("a");
const namespace = new URL(catalog[1].status_url).searchParams.get("source_namespace");
await page.route("**/api/chat/research-analysis?*", async route => {
const query = new URL(route.request().url()).searchParams;
if (query.get("source_namespace") !== namespace) return route.continue();
const response = query.get("pack_id") === empty.pack.pack_id ? empty : saved;
const language = query.get("output_language");
const config = {...response.pack.config, ...(language ? {output_language:language} : {})};
const result = response.pack.result ? {...response.pack.result, output_language:language} : null;
await route.fulfill({json:{...response, execution_enabled:false, pack:{...response.pack, config, result}}});
});
await page.reload();
const progress = page.locator(".research-progress");
const scope = progress.locator(":scope > header select");
const savedScope = progress.locator(`.research-progress-scope[data-pack-id="${saved.pack.pack_id}"]`);
const emptyScope = progress.locator(`.research-progress-scope[data-pack-id="${empty.pack.pack_id}"]`);
await savedScope.waitFor();
const originalSelect = await scope.elementHandle();
await scope.focus();
await scope.selectOption(empty.pack.pack_id);
await emptyScope.waitFor();
assert.equal(await scope.evaluate((select, original) => select === original && select === document.activeElement, originalSelect), true,
"Moving the empty analysis below history keeps the focused selector");
assert.equal(await progress.evaluate(element =>
Boolean(document.querySelector(".research-axis")!.compareDocumentPosition(element) & Node.DOCUMENT_POSITION_FOLLOWING)), true);
await scope.selectOption(saved.pack.pack_id);
await savedScope.waitFor();
assert.equal(await scope.evaluate((select, original) => select === original && select === document.activeElement, originalSelect), true,
"Moving the saved analysis above history keeps the focused selector");
assert.equal(await progress.evaluate(element =>
Boolean(element.compareDocumentPosition(document.querySelector(".research-axis")!) & Node.DOCUMENT_POSITION_FOLLOWING)), true);
await page.keyboard.press("Tab");
assert.equal(await progress.evaluate(element => element.contains(document.activeElement)), true,
"The next Tab continues within the saved analysis");
assert.deepEqual(writes, [], "Browsing and retry never dispatch work, analysis or source mutations");
assert.deepEqual(errors, []);
await page.close();
}
for (let i=0;i<fixturePaths.length;i++) assert.equal(await readFile(fixturePaths[i], "utf8"), originals[i]);
return {coverageEntries:[], note:"Original R7 on/off; same-source retention, cross-source failure/retry/export, Back/Forward, bilingual/narrow/keyboard, zero writes."};
return {coverageEntries:[], note:"Original R7 on/off; same-source retention, cross-source failure/retry/export, Back/Forward, bilingual/narrow/keyboard, analysis scope focus, zero writes."};
} finally {
if (backend.exitCode === null && backend.signalCode === null) {
const stopped = once(backend, "exit"); backend.kill("SIGTERM"); await stopped;
Expand Down
Loading