From 51517200cd07a20460d414d5863f84904bee93ff Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Thu, 12 Nov 2026 08:35:11 +0800 Subject: [PATCH] fix(research): scope the timeline to loaded history during recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../goal-research-history.tsx | 33 +++++++++++-------- .../goal-research-timeline.tsx | 30 +++++++++-------- .../research-record-search.mjs | 26 +++++++++++++-- 3 files changed, 61 insertions(+), 28 deletions(-) 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 5876a80b50..2ecb9d2dbb 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 @@ -116,7 +116,9 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist const [recordsOpen, setRecordsOpen] = useState(false); const records = useRef(null); const [lens, setLens] = useState(() => viewFromUrl(goalId).lens); - const [phase, setPhase] = useState(() => viewFromUrl(goalId).phase); + const [requestedPhase, setPhase] = useState(() => viewFromUrl(goalId).phase); + const phase = read.complete ? requestedPhase : null; + const historyCoverage = useRef(null); const presentationId = useId().replace(/:/g, ""); const detail = useRef(null); const graph = useRef(null); @@ -127,16 +129,18 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist const copy = zh ? { title: "研究历程", loading: "正在读取完整记录", reading: "读取中", refresh: "刷新记录", all: "回到全图", scope: "Explore 已记录历史", coverage: "其他来源覆盖、尝试发生时序尚未核验", - partial: "历史尚未完整载入", failure: "无法读取此来源的研究记录", - time: "记录时间(本地)", unknown: "时间未记录", record: "条记录", records: "条记录", evidence: "已记录依据", + partialFailure: "只载入了部分研究记录", refreshFailure: "刷新失败,当前记录已保留", + failure: "无法读取此来源的研究记录", + time: "记录时间(本地)", partialTime: "已读记录(本地时间)", unknown: "时间未记录", record: "条记录", records: "条记录", evidence: "已记录依据", interpretation: "独立检查与实际采用尚未核验", versions: "来源与版本引用", updates: "历史记录", changes: "次记录", question: "个问题", questions: "个问题", links: "显式关联", missing: "部分关联的来源节点尚未记录", group: "来源分组", lost: "当前记录中找不到所选节点,其他历史仍可浏览。", } : { title: "Research history", loading: "Reading the full recorded history", reading: "Reading", refresh: "Refresh history", all: "Whole graph", scope: "Recorded Explore history", coverage: "Other-source coverage and attempt-time coverage are unverified", - partial: "History is not fully loaded", failure: "Could not read this source's research history", - time: "Recorded time (local)", unknown: "Time not recorded", record: "record", records: "records", evidence: "Recorded evidence", + partialFailure: "Only part of the history loaded", refreshFailure: "Refresh failed; current records are preserved", + failure: "Could not read this source's research history", + time: "Recorded time (local)", partialTime: "Loaded records (local time)", unknown: "Time not recorded", record: "record", records: "records", evidence: "Recorded evidence", interpretation: "Independent checking and actual adoption are unverified", versions: "Source and version references", updates: "History", changes: "records", question: "question", questions: "questions", links: "Explicit relationships", missing: "Some linked source nodes are not recorded", group: "Source grouping", lost: "The selected node is absent from this snapshot. Other history remains readable.", @@ -276,9 +280,9 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist url.searchParams.set("research_lens", next); if (nextPhase === null) url.searchParams.delete("research_phase"); else url.searchParams.set("research_phase", String(nextPhase)); - if (next !== lens || nextPhase !== phase) window.history.pushState(null, "", url); + if (next !== lens || nextPhase !== requestedPhase) window.history.pushState(null, "", url); }; - const chooseLens = (next: Lens) => chooseView(next, phase); + const chooseLens = (next: Lens) => chooseView(next, requestedPhase); const fullTime = (at: number, precise = false) => new Intl.DateTimeFormat(zh ? "zh-CN" : "en", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", hour12: false, second: precise ? "2-digit" : undefined, fractionalSecondDigits: precise ? 3 : undefined, @@ -444,7 +448,7 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist : null; const historyView = !value ? null : lens === "timeline" && timeline ?
- select({nodes: [], day}, true, button)} phase={phase} onPhase={next => chooseView(lens, next)} onSelect={(id, finding, event) => choose(id, true, finding, undefined, event)} onSelectMany={(nodes, button) => select({nodes, day: null}, true, button)} timeLabel={fullTime} dayLabel={dayTime}/>
:
@@ -553,19 +557,22 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
{!emptySourceHistory ?
-
{emptyWithAnalysis ? <>{emptyHistoryMessage} · {read.events.length} / {read.total} +
{emptyWithAnalysis ? <>{emptyHistoryMessage} · {read.events.length} / {read.total}

{zh ? "已保存的分析、原生测量与 Explore 历程分别读取。这个 Goal 没有已记录的 Explore 历程;已保存的结果仍可在下方阅读。" : "Saved analysis, native measurements and Explore history are read separately. This Goal has no recorded Explore history; its saved results remain readable below."}

-
: <>{copy.time}{value?.startAt ? <> · — : null} · {busy ? `${copy.reading} ${loaded}${read.total ? ` / ${read.total}` : ""}` + : <>{!read.complete && read.events.length ? copy.partialTime : copy.time}{value?.startAt ? <> · — : null} · {busy ? `${copy.reading} ${loaded}${read.total ? ` / ${read.total}` : ""}` : read.total === null ? (zh ? "来源尚未读到" : "Source has not been read") - : `${read.events.length} / ${read.total} ${recordUnit}${read.complete ? "" : ` · ${copy.partial}`}`}}
+ : `${read.events.length} / ${read.total} ${recordUnit}`}}
{!readingSelection ? displayControl : null}
: null} {readFailure ?
-
{read.events.length ? (zh ? "刷新失败,当前记录已保留" : "Refresh failed; current records are preserved") : copy.failure} +
{read.events.length ? (read.complete ? copy.refreshFailure : copy.partialFailure) : copy.failure}

{readFailure}

- +
: null} {emptySourceHistory ? <>
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx index 610115a35e..5dbf027af0 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx @@ -11,7 +11,7 @@ import {ContextHelp} from "./context-help"; * Position means first record time only; it never encodes progress or cause. */ type Copy = Record; type Props = { - timeline: ExploreTimeline; zh: boolean; selectedIds: readonly string[]; selectedEventId?: string; phase: number | null; + timeline: ExploreTimeline; complete: boolean; zh: boolean; selectedIds: readonly string[]; selectedEventId?: string; phase: number | null; selectedDay: string | null; dayGroups: ReturnType["groups"]; onGroupSelect: (day: string, button: HTMLButtonElement) => void; onSelect: (id: string, finding?: string, event?: string) => void; onSelectMany: (ids: string[], button: HTMLButtonElement) => void; onPhase: (index: number | null) => void; timeLabel: (value: number, precise?: boolean) => string; dayLabel: (value: number) => string; @@ -37,7 +37,7 @@ function elapsedLabel(offset: number, zh: boolean) { return parts.join(" ") || (zh ? "0 秒" : "0s"); } -export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId, selectedDay, dayGroups, onGroupSelect, phase, onSelect, onSelectMany, onPhase, timeLabel, dayLabel}: Props) { +export function GoalResearchTimeline({timeline, complete, zh, selectedIds, selectedEventId, selectedDay, dayGroups, onGroupSelect, phase, onSelect, onSelectMany, onPhase, timeLabel, dayLabel}: Props) { const plot = useRef(null); const [plotWidth, setPlotWidth] = useState(0); useLayoutEffect(() => { @@ -54,7 +54,8 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId early: "前段", middle: "中段", late: "后段", item: "个有记录的研究项", items: "个有记录的研究项", question: "个问题", questions: "个问题", decisions: "显式决策", none: "未记录显式决策", changed: "变化", stopped: "停止", retained: "保留", unstructured: "决策未结构化", unstructuredHint: "结论未按 writeback 结构记录,无法拆出依据与决策。", - density: "记录密度", densityHint: "柱高为事件数;浅红为无记录空档", axis: "自首条记录起", + density: "记录密度", densityHint: "柱高为事件数;浅红为无记录空档", axis: "自首条记录起", gap: "无记录", + partialAxis: "自最早的已读记录起", partialDensityHint: "柱高为已读事件数", partialGap: "无已读记录", routes: "研究路线", routeHint: "仅由显式关联或父子分组相连", singles: "未关联研究项", trail: "决策轨迹", more: "显示更早记录", moreQuestions: "显示更多问题", record: "条决策记录", records: "条决策记录", unknownTime: "时间未知", all: "全部时段", span: "记录跨度", start: "起点", end: "末端", replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定", @@ -65,7 +66,8 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId early: "Early", middle: "Middle", late: "Late", item: "research item with records", items: "research items with records", question: "question", questions: "questions", decisions: "Explicit decisions", none: "No explicit decision recorded", changed: "Changed", stopped: "Stopped", retained: "Retained", unstructured: "Unstructured decisions", unstructuredHint: "Findings were not recorded in writeback sections; evidence and decisions cannot be separated.", - density: "Recording density", densityHint: "Bar height is events; pale red is a recording gap", axis: "Since first record", + density: "Recording density", densityHint: "Bar height is events; pale red is a recording gap", axis: "Since first record", gap: "No records", + partialAxis: "Since earliest loaded record", partialDensityHint: "Bar height counts loaded events", partialGap: "No loaded records", routes: "Research routes", routeHint: "Connected only by recorded relationships or parent grouping", singles: "Unlinked research items", trail: "Decision history", more: "Show earlier records", moreQuestions: "Show more questions", record: "decision record", records: "decision records", unknownTime: "Unknown time", all: "Whole history", span: "Recorded span", start: "Start", end: "End", replan: "Replan", continue: "Continue", stop: "Stop", wait: "Wait", no_change: "No change", ask_human: "Ask human", @@ -75,6 +77,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId }; const {start, end} = timeline; if (start === null || end === null) return

{copy.noTime}

; + const axisLabel = complete ? copy.axis : copy.partialAxis; const elapsed = end - start; const span = Math.max(1, elapsed); const pct = (at: number) => `${((at - start) / span) * 100}%`; @@ -131,24 +134,25 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId }; return
-
{timeline.phases.map(phaseCard)}
+ {complete ?
{timeline.phases.map(phaseCard)}
: null}
{LABELS.map(key =>