Skip to content
Merged
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 @@ -116,7 +116,9 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
const [recordsOpen, setRecordsOpen] = useState(false);
const records = useRef<HTMLDetailsElement>(null);
const [lens, setLens] = useState<Lens>(() => viewFromUrl(goalId).lens);
const [phase, setPhase] = useState<number | null>(() => viewFromUrl(goalId).phase);
const [requestedPhase, setPhase] = useState<number | null>(() => viewFromUrl(goalId).phase);
const phase = read.complete ? requestedPhase : null;
const historyCoverage = useRef<HTMLDivElement>(null);
const presentationId = useId().replace(/:/g, "");
const detail = useRef<HTMLDivElement>(null);
const graph = useRef<HTMLDivElement>(null);
Expand All @@ -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.",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -444,7 +448,7 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
</ContextHelp>
</div> : null;
const historyView = !value ? null : lens === "timeline" && timeline ? <div className="research-history-canvas is-timeline" ref={graph} tabIndex={-1}>
<GoalResearchTimeline timeline={timeline} zh={zh} selectedIds={selection.nodes} selectedEventId={selectedFinding?.event_id} selectedDay={selection.day} dayGroups={grouped.groups}
<GoalResearchTimeline timeline={timeline} complete={read.complete} zh={zh} selectedIds={selection.nodes} selectedEventId={selectedFinding?.event_id} selectedDay={selection.day} dayGroups={grouped.groups}
onGroupSelect={(day, button) => 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}/>
</div> : <div className="research-history-canvas" ref={graph} tabIndex={0} aria-label={zh ? "完整已载入研究关系图;可滚动" : "Full loaded research graph; scrollable"}>
Expand Down Expand Up @@ -553,19 +557,22 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
<button type="button" aria-label={copy.refresh} disabled={busy} onClick={refresh}><RefreshCw size={15} aria-hidden/></button>
</div></header>
{!emptySourceHistory ? <div className="research-history-meta">
<div className="research-history-coverage" role="status">{emptyWithAnalysis ? <>{emptyHistoryMessage} · {read.events.length} / {read.total}<ContextHelp icon={FlaskConical} label={zh ? "研究历程来源" : "Research history source"}>
<div className="research-history-coverage" ref={historyCoverage} role="status" tabIndex={-1}>{emptyWithAnalysis ? <>{emptyHistoryMessage} · {read.events.length} / {read.total}<ContextHelp icon={FlaskConical} label={zh ? "研究历程来源" : "Research history source"}>
<p>{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."}</p>
</ContextHelp></> : <>{copy.time}{value?.startAt ? <> · <time>{timeLabel(value.startAt)}</time> — <time>{timeLabel(value.endAt)}</time></> : null} · {busy ? `${copy.reading} ${loaded}${read.total ? ` / ${read.total}` : ""}`
</ContextHelp></> : <>{!read.complete && read.events.length ? copy.partialTime : copy.time}{value?.startAt ? <> · <time>{timeLabel(value.startAt)}</time> — <time>{timeLabel(value.endAt)}</time></> : 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}`}`}</>}</div>
: `${read.events.length} / ${read.total} ${recordUnit}`}</>}</div>
{!readingSelection ? displayControl : null}
</div> : null}
{readFailure ? <div className="research-history-read-error" role="alert">
<AlertCircle size={18} aria-hidden/>
<div><strong>{read.events.length ? (zh ? "刷新失败,当前记录已保留" : "Refresh failed; current records are preserved") : copy.failure}</strong>
<div><strong>{read.events.length ? (read.complete ? copy.refreshFailure : copy.partialFailure) : copy.failure}</strong>
<ContextHelp label={zh ? "研究记录读取详情" : "History read details"}><p>{readFailure}</p></ContextHelp>
</div>
<button type="button" disabled={busy} onClick={refresh}><RefreshCw size={14} aria-hidden/>{zh ? "重试" : "Retry"}</button>
<button type="button" disabled={busy} onClick={event => {
if (document.activeElement === event.currentTarget) historyCoverage.current?.focus({preventScroll: true});
refresh();
}}><RefreshCw size={14} aria-hidden/>{zh ? "重试" : "Retry"}</button>
</div> : null}
{emptySourceHistory ? <>
<div className="research-history-empty research-history-source-empty"><div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {ContextHelp} from "./context-help";
* Position means first record time only; it never encodes progress or cause. */
type Copy = Record<string, string>;
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<typeof groupUnlinkedHistoryQuestions>["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;
Expand All @@ -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<HTMLDivElement>(null);
const [plotWidth, setPlotWidth] = useState(0);
useLayoutEffect(() => {
Expand All @@ -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: "请人决定",
Expand All @@ -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",
Expand All @@ -75,6 +77,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
};
const {start, end} = timeline;
if (start === null || end === null) return <ContextHelp label={zh ? "记录时间未知" : "Record time unknown"} icon={CircleHelp}><p className="research-history-note">{copy.noTime}</p></ContextHelp>;
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}%`;
Expand Down Expand Up @@ -131,24 +134,25 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
};
return <div className="research-timeline">
<div className="research-timeline-summary">
<div className={`research-phases${timeline.phases.length === 1 ? " is-single" : ""}`} role="group" aria-label={phaseNote}>{timeline.phases.map(phaseCard)}</div>
{complete ? <div className={`research-phases${timeline.phases.length === 1 ? " is-single" : ""}`} role="group" aria-label={phaseNote}>{timeline.phases.map(phaseCard)}</div> : null}
<div className="research-timeline-key">
<div className="research-phase-legend">{LABELS.map(key => <span key={key}><i className={tone(key)} aria-hidden="true"/>{label(key)}</span>)}</div>
<ContextHelp label={zh ? "时间线读法" : "Reading the timeline"}>
<p>{phaseNote}</p><p className="research-density-help"><i className="research-bar-key" aria-hidden/>{copy.densityHint}{zh ? "。" : ". "}{zh ? "无记录空档不代表研究停止。" : "A recording gap does not establish that research stopped."}</p>
<p>{zh ? "横轴从首条记录算起。悬停刻度可查看对应的本地时间。" : "The axis starts at the first record. Hover over a tick for its local time."}</p>
{complete ? <p>{phaseNote}</p> : null}
<p className="research-density-help"><i className="research-bar-key" aria-hidden/>{complete ? copy.densityHint : copy.partialDensityHint}{zh ? "。" : ". "}{zh ? "无记录空档不代表研究停止。" : "A recording gap does not establish that research stopped."}</p>
<p>{zh ? "悬停刻度可查看对应的本地时间。" : "Hover over a tick for its local time."}</p>
<p>{findingCopy.meaning}</p>
{!timeline.structuredCount ? <p>{copy.unstructuredHint}</p> : null}
<p>{copy.routeHint}{zh ? "。" : ". "}{zh ? "相邻点合拢,数字为研究项数,一次展开原始时间与依据。" : "Nearby points group; the number counts research items. Select once for original times and sources."}</p>
<p>{zh ? "选中时段后,高亮其中有记录的研究项。各项位置仍按首次记录时间。" : "Selecting a period highlights items with records in it. Each item stays at its first recorded time."}</p>
{complete ? <p>{zh ? "选中时段后,高亮其中有记录的研究项。各项位置仍按首次记录时间。" : "Selecting a period highlights items with records in it. Each item stays at its first recorded time."}</p> : null}
<p>{zh ? "未关联问题按首次记录 UTC 日期收纳,不推断路线或因果。决策轨迹按研究问题归组,组内保留各轮显式决策,最近记录在前;同名问题不会自动合并。" : "Unlinked questions group by first-record UTC date, without inferring a route or cause. Decision history groups explicit decisions by question, newest record first; matching titles do not merge questions."}</p>
<dl className="research-phase-details">{timeline.phases.map(item => <div key={item.index}><dt>{phaseName(item)}</dt><dd>{phaseDescription(item)}</dd></div>)}</dl>
{complete ? <dl className="research-phase-details">{timeline.phases.map(item => <div key={item.index}><dt>{phaseName(item)}</dt><dd>{phaseDescription(item)}</dd></div>)}</dl> : null}
</ContextHelp>{phase !== null ? <button type="button" className="research-link" onClick={() => onPhase(null)}>{copy.all}</button> : null}
</div>
</div>
<div className="research-timeline-grid">
<div className="research-row-label"><strong>{copy.density}</strong><small>{copy.axis}</small><small>
{hasRecordingGaps ? <span><i className="research-gap-key" aria-hidden="true"/> {zh ? "无记录" : "No records"}</span> : null}
<div className="research-row-label"><strong>{copy.density}</strong><small>{axisLabel}</small><small>
{hasRecordingGaps ? <span><i className="research-gap-key" aria-hidden="true"/> {complete ? copy.gap : copy.partialGap}</span> : null}
{decisionOutcomes.map(outcome => <span key={outcome}><i className={`research-tick-key is-${outcome}`}/> {label(outcome)}</span>)}
</small></div>
<div className="research-row-plot">
Expand All @@ -157,8 +161,8 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
{timeline.decisions.length ? <div className="research-decision-ticks">{timeline.decisions.filter(item => item.at !== null).map(item => <span key={item.event.event_id}
title={`${label(item.sections!.decision!.outcome)} · ${item.at === null ? "—" : timeLabel(item.at)}`}
className={`is-${item.sections!.decision!.outcome}${inPhase(item.at) ? "" : " is-dimmed"}`} style={{left: pct(item.at!)}}/>)}</div> : null}
{timeline.phases.slice(1).map(item => <span key={item.index} className="research-phase-divider" style={{left: pct(item.start)}}/>)}
<div className="research-axis" role="group" aria-label={copy.axis}>{ticks.map(offset => <time
{complete ? timeline.phases.slice(1).map(item => <span key={item.index} className="research-phase-divider" style={{left: pct(item.start)}}/>) : null}
<div className="research-axis" role="group" aria-label={axisLabel}>{ticks.map(offset => <time
key={offset} dateTime={`PT${offset / 1_000}S`} title={timeLabel(start + offset, true)}
className={offset === 0 ? "is-start" : offset === elapsed ? "is-end" : undefined}
style={{left: pct(start + offset)}}>{elapsedLabel(offset, zh)}</time>)}</div>
Expand Down
Loading
Loading