: !a.loading && !a.readError ? {a.zh ? "选择一份解读,阅读问题背景、策略差异、关键转折与证据。" : "Choose a study to read its background, approaches, turning points and evidence."}
: null}
+ scope={p.scope} navigation={a.studyNavigation(p.pack_id)} onCitation={a.readCitation}/> : !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/research-case-study.css b/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.css
index 719e020888..bd216c436b 100644
--- a/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.css
+++ b/apps/presentation/dashboard/src/features/personal-workspace/research-case-study.css
@@ -30,8 +30,8 @@
.case-provenance dt { color: var(--pw-muted); }.case-provenance dd { margin: 0; }
.case-reading-layout { display: grid; grid-template-columns: 185px minmax(0, 1fr); gap: 40px; align-items: start; }
.case-outline { position: sticky; top: 16px; display: grid; gap: 2px; border-top: 1px solid var(--pw-line); padding-top: 12px; }
-.case-outline button { display: flex; gap: 12px; text-align: start; padding: 9px 0; min-height: 44px; color: var(--pw-muted); background: none; border: 0; cursor: pointer; font: inherit; font-size: var(--text-body); line-height: 1.6; }
-.case-outline button span { flex: 0 0 20px; white-space: nowrap; font-variant-numeric: tabular-nums; opacity: .65; }.case-outline button:hover { color: var(--pw-text); }
+.case-outline a { display: flex; gap: 12px; text-align: start; padding: 9px 0; min-height: 44px; color: var(--pw-muted); text-decoration: none; font: inherit; font-size: var(--text-body); line-height: 1.6; }
+.case-outline a span { flex: 0 0 20px; white-space: nowrap; font-variant-numeric: tabular-nums; opacity: .65; }.case-outline a:hover { color: var(--pw-text); }
.case-sections { min-width: 0; }.case-section { border-top: 1px solid var(--pw-line); padding-block: 24px 40px; scroll-margin-top: 24px; }
.case-section-number { color: var(--pw-muted); font-size: var(--text-body); margin-bottom: 6px; }
.research-case-study .case-section h3 { font-size: 23px; font-weight: 600; line-height: 1.5; margin: 0 0 18px; }
@@ -73,6 +73,6 @@
}
}
.case-limitations { color: var(--pw-muted); }.case-limitations li { margin-block: 10px; }.case-read-warning { border-left: 3px solid currentColor; padding-left: 14px; }
-.research-case-reader :is(button,select,summary,[tabindex]):focus-visible, .case-return-reader:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 3px; }
+.research-case-reader :is(a,button,select,summary,[tabindex]):focus-visible, .case-return-reader:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 3px; }
@container (max-width: 760px) { .case-reading-layout { grid-template-columns: 1fr; gap: 18px; }.case-outline { position: static; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 4px 20px; }.case-opening { margin-block: 24px; } }
@media (max-width: 640px) { .case-reader-toolbar { align-items: start; }.case-reader-toolbar label { min-width: 0; width: 100%; }.case-reader-toolbar select { width: 100%; }.case-timeline li { grid-template-columns: 1fr; gap: 8px; }.case-timeline li > div { padding-left: 14px; }.research-case-study .case-opening h2 { font-size: 28px; }.case-provenance dl { grid-template-columns: 1fr; gap: 4px; }.case-provenance dd { margin-bottom: 10px; } }
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 d9d03d5b0c..536e5d6982 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
@@ -1,4 +1,4 @@
-import {useId} from 'react';
+import {useEffect, useRef} from 'react';
import type {TrajectoryCaseStudy} from '../../../../../../loopx/control_plane/capabilities/trajectory_case_study.js';
import type {AssessmentCitation} from '../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js';
import {MarkdownText} from './markdown';
@@ -44,19 +44,53 @@ function CaseSeries({series, zh}: {series: Series; zh: boolean}) {
;
}
+export const caseChapterHashPrefix = '#case-section-';
+
+function focusChapter(chapter: HTMLElement) {
+ chapter.focus({preventScroll: true});
+ chapter.scrollIntoView({block: 'start'});
+}
+
/** A document's author controls the composition; the host owns navigation and evidence. */
-export function ResearchCaseStudy({study, zh, historical, scope, onCitation}: {
+export function ResearchCaseStudy({study, zh, historical, scope, navigation, onCitation}: {
study: TrajectoryCaseStudy; zh: boolean; historical: 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;
}) {
- const id = useId().replace(/:/g, '');
+ const article = useRef(null);
+ const {url, goalId, packId, namespace, follow} = navigation;
+ const prefix = `${caseChapterHashPrefix.slice(1)}${namespace ? namespace + '-' : ''}${packId}-`;
+ useEffect(() => {
+ // 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;
+ const restore = () => {
+ cancelAnimationFrame(frame);
+ frame = requestAnimationFrame(() => {
+ const {searchParams, hash} = new URL(window.location.href);
+ if (searchParams.get('view') !== 'research' || searchParams.get('research_reading') !== 'case'
+ || searchParams.get('research_goal') !== goalId || searchParams.get('research_analysis_goal') !== goalId
+ || searchParams.get('research_pack') !== packId || !hash.startsWith(`#${prefix}`)) return;
+ const chapter = document.getElementById(hash.slice(1));
+ if (chapter && article.current?.contains(chapter) && chapter.getClientRects().length) focusChapter(chapter);
+ });
+ };
+ restore();
+ window.addEventListener('popstate', restore);
+ window.addEventListener('hashchange', restore);
+ return () => {
+ cancelAnimationFrame(frame);
+ window.removeEventListener('popstate', restore);
+ window.removeEventListener('hashchange', restore);
+ };
+ }, [goalId, packId, prefix]);
const citationLinks = (citations: AssessmentCitation[]) =>
{citations.map((c, i) => )}
;
- return
+ return
-
-
{study.sections.map((s, n) =>