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 @@ -64,7 +64,7 @@
.research-question-index button:hover strong { text-decoration: underline; text-underline-offset: 3px; }
.research-history-detail code { display: block; font-family: var(--font-mono); font-size: var(--text-body); overflow-wrap: anywhere; }
.research-history-records { padding-left: 18px; font-size: var(--text-lead); line-height: var(--leading-prose); }
.research-history-records li { padding: 6px 0; }
.research-history-records > li { padding: 6px 0; }
.research-history-records time { display: block; font-size: var(--text-caption); color: var(--pw-muted); }
.research-history-loading { min-height: 240px; margin-top: 20px; padding: 24px; border: 1px solid var(--pw-line); border-radius: 12px; color: var(--pw-muted); background: var(--pw-bg); font-size: var(--text-body); }
.research-history-loading-label { display: block; margin-bottom: 20px; }
Expand Down Expand Up @@ -106,7 +106,7 @@
.research-comparison-controls > label { margin-inline-start: auto; color: var(--pw-text); }
.research-record-search .context-help-trigger { min-height: 28px; padding: 4px; border: 0; border-radius: 4px; }
.research-record-search .research-history-records { list-style: none; padding: 0; }
.research-record-search li { border-bottom: 1px solid var(--pw-line); }
.research-record-search > .research-history-records > li { border-bottom: 1px solid var(--pw-line); }
.research-record-search summary { min-height: 44px; overflow-wrap: anywhere; }
.research-record-search summary span { display: block; color: var(--pw-muted); font-size: var(--text-caption); }
.research-record-entry:not([open]) > summary > strong { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
Expand All @@ -118,8 +118,8 @@
.research-record-sources { margin-block: 12px; }
.research-record-search p { white-space: pre-wrap; line-height: var(--leading-prose); overflow-wrap: anywhere; }
.research-record-search code { display: block; overflow-wrap: anywhere; font-size: var(--text-body); }
.research-record-search dd { margin: 0 0 8px; }
.research-record-search dt { color: var(--pw-muted); font-size: var(--text-caption); }
.research-record-sources dd { margin: 0 0 8px; }
.research-record-sources dt { color: var(--pw-muted); font-size: var(--text-caption); }
.research-history-all-records { scroll-margin-top: 16px; }
.research-record-search :is(input, select, summary):focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; }
.research-node-count { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
Expand Down Expand Up @@ -303,6 +303,8 @@
.research-comparison-pair pre { white-space: pre-wrap; }
.research-comparison-identities p { margin-block: 4px 12px; }
.research-comparison-field { border-block-start: 1px solid var(--pw-line); padding-block: 12px; }
.research-comparison-field .research-finding-sections { grid-template-columns: max-content minmax(0, 1fr); gap: 8px 12px; margin-block: 8px; }
.research-comparison-field .research-finding-sections dd { margin: 0; }
.research-comparison-side, .research-comparison-missing { color: var(--pw-muted); }
.research-comparison-side { display: none; }
@media (max-width: 640px) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,21 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const sectionCopy = zh ? {inputRevision: "输入版本", applicability: "适用范围", observation: "观察", interpretation: "解读", changed: "变化", stopped: "停止", retained: "保留"}
: {inputRevision: "Input revision", applicability: "Applicability", observation: "Observation", interpretation: "Interpretation", changed: "Changed", stopped: "Stopped", retained: "Retained"};
const outcomeCopy: Record<string, string> = zh ? {replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定"} : {replan: "Replan", continue: "Continue", stop: "Stop", wait: "Wait", no_change: "No change", ask_human: "Ask human"};
const findingBody = (finding: ExploreHistoryEvent) => {
const findingNarrative = (finding: ExploreHistoryEvent) => {
const sections = findingSections(finding);
const decision = sections?.decision;
const execution = finding.execution_observation;
const measured = (value: unknown) => value === null ? (zh ? "未测量" : "Not measured") : String(value);
return <>{finding.measurements ? <ExploreMeasurements measurements={finding.measurements} zh={zh}/> : null}{sections ? <dl className="research-finding-sections">
return sections ? <dl className="research-finding-sections">
<dt>{sectionCopy.observation}</dt><dd>{sections.observation}</dd>
{decision ? <>
<dt>{zh ? "决策" : "Decision"}</dt><dd><b className={`research-outcome is-${decision.outcome}`}>{outcomeCopy[decision.outcome] ?? decision.outcome}</b></dd>
{(["changed", "stopped", "retained"] as const).map(key => decision[key].length ? <Fragment key={key}><dt>{sectionCopy[key]}</dt><dd key={key}><ul>{decision[key].map(item => <li key={item}>{item}</li>)}</ul></dd></Fragment> : null)}
</> : <><dt>{sectionCopy.interpretation}</dt><dd>{sections.interpretation}</dd></>}
</dl> : <p>{finding.summary}</p>}{execution ? <details><summary>{zh ? "实际执行与资源观测" : "Execution and resource observations"}</summary>
</dl> : <p>{finding.summary}</p>;
};
const findingBody = (finding: ExploreHistoryEvent) => {
const execution = finding.execution_observation;
const measured = (value: unknown) => value === null ? (zh ? "未测量" : "Not measured") : String(value);
return <>{finding.measurements ? <ExploreMeasurements measurements={finding.measurements} zh={zh}/> : null}{findingNarrative(finding)}{execution ? <details><summary>{zh ? "实际执行与资源观测" : "Execution and resource observations"}</summary>
<dl className="research-finding-sections">
<dt>{zh ? "计划方法 → 实际方法" : "Planned → executed method"}</dt><dd>{String(execution.planned_method_id)} → {String(execution.executed_method_id)}</dd>
{execution.change_reason ? <><dt>{zh ? "改向原因" : "Reason for change"}</dt><dd>{String(execution.change_reason)}</dd></> : null}
Expand Down Expand Up @@ -542,6 +545,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
displayEvents={currentDisplay && !originalDisplay ? value.events : undefined}
displaySections={currentDisplay && !originalDisplay ? displayProjection.sections : undefined}
renderBody={currentDisplay && !originalDisplay ? findingBody : undefined}
renderSummary={currentDisplay && !originalDisplay ? findingNarrative : undefined}
complete={read.complete} stale={Boolean(error || history.error)} busy={busy} zh={zh} stateLabel={stateLabel} timeLabel={timeLabel}/> : null}
</details> : null}
</section>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import {exportResearchRecords} from "./research-record-export";
import {downloadReport} from "./report-download";

/** Local read controls over the existing validated history snapshot. */
export function ResearchRecordSearch({goalId, events, displayEvents, displaySections, renderBody, complete, stale, busy, zh, stateLabel, timeLabel}: {
export function ResearchRecordSearch({goalId, events, displayEvents, displaySections, renderBody, renderSummary, complete, stale, busy, zh, stateLabel, timeLabel}: {
goalId: string;
displayEvents?: readonly ExploreHistoryEvent[]; renderBody?: (event: ExploreHistoryEvent) => ReactNode;
displaySections?: ResearchRecordDisplay["sections"];
renderSummary?: (event: ExploreHistoryEvent) => ReactNode;
events: readonly ExploreHistoryEvent[]; complete: boolean; stale: boolean; busy: boolean; zh: boolean;
stateLabel: (status: string) => string; timeLabel: (at: string) => string;
}) {
Expand Down Expand Up @@ -83,7 +84,8 @@ export function ResearchRecordSearch({goalId, events, displayEvents, displaySect
</dl>
{event.evidence_refs.map(ref => <code key={ref}>{ref}</code>)}
</details>
<ResearchRecordComparison events={events} selected={event} zh={zh} stateLabel={stateLabel} timeLabel={timeLabel}/>
<ResearchRecordComparison events={events} selected={event} displayed={displayed} renderSummary={renderSummary}
zh={zh} stateLabel={stateLabel} timeLabel={timeLabel}/>
</details>
</li>;})}</ol>
{limit < matches.length ? <button type="button" onClick={() => setLimit(value => value + 50)}>{zh ? "继续展开记录" : "Show more records"} · {Math.min(limit, matches.length)} / {matches.length}</button> : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {useState} from "react";
import {useState, type ReactNode} from "react";
import type {ExploreHistoryEvent} from "../../../../../../loopx/control_plane/presentation/explore_history.ts";
import {changedResearchFields, researchComparisonFields, researchRecordVersions, type ResearchComparisonField} from "./research-record-versions.ts";
import {ExploreMeasurements} from "./explore-measurements";
Expand All @@ -15,8 +15,9 @@ const labels: Record<ResearchComparisonField, [string, string]> = {
};

/** Requested original-source detail; no analysis, acceptance or write path. */
export function ResearchRecordComparison({events, selected, zh, stateLabel, timeLabel}: {
export function ResearchRecordComparison({events, selected, displayed, renderSummary, zh, stateLabel, timeLabel}: {
events: readonly ExploreHistoryEvent[]; selected: ExploreHistoryEvent; zh: boolean;
displayed?: ReadonlyMap<string, ExploreHistoryEvent>; renderSummary?: (event: ExploreHistoryEvent) => ReactNode;
stateLabel: (status: string) => string; timeLabel: (at: string) => string;
}) {
const versions = researchRecordVersions(events, selected);
Expand All @@ -31,18 +32,24 @@ export function ResearchRecordComparison({events, selected, zh, stateLabel, time
const changed = changedResearchFields(selected, comparison);
const value = (event: ExploreHistoryEvent, field: ResearchComparisonField) => {
const raw = event[field];
const shown = displayed?.get(event.event_id) ?? event;
if (raw === undefined || raw === null || raw === "" || (Array.isArray(raw) && !raw.length)) return <span className="research-comparison-missing">{zh ? "未记录" : "Not recorded"}</span>;
if (field === "measurements") return <ExploreMeasurements measurements={event.measurements ?? []} zh={zh}/>;
if (field === "status") return stateLabel(event.status);
if (Array.isArray(raw)) return <ul>{raw.map((item, i) => <li key={i}><code>{String(item)}</code></li>)}</ul>;
if (typeof raw === "object") return <pre>{JSON.stringify(raw, null, 2)}</pre>;
return String(raw);
const narrative = field === "summary" && renderSummary;
const content = narrative ? renderSummary(shown)
: typeof raw === "object" ? <pre>{JSON.stringify(shown[field], null, 2)}</pre> : String(shown[field]);
return <>{content}{narrative || JSON.stringify(shown[field]) !== JSON.stringify(raw) ? <details>
<summary>{zh ? "完整原文" : "Full original text"}</summary>
{typeof raw === "object" ? <pre>{JSON.stringify(raw, null, 2)}</pre> : <p>{String(raw)}</p>}
</details> : null}</>;
};
return <details className="research-record-comparison" open={open} onToggle={event => setOpen(event.currentTarget.open)}>
<summary>{zh ? "比较记录版本" : "Compare recorded versions"} · {versions.length}</summary>
{open ? <>
<label>{zh ? "比较对象" : "Compare with"}<select value={comparison.event_id} onChange={event => setChosen(event.target.value)}>
{alternatives.map(event => <option key={event.event_id} value={event.event_id}>{timeLabel(event.recorded_at)} · {event.title} · {event.event_id}</option>)}
{alternatives.map(event => <option key={event.event_id} value={event.event_id}>{timeLabel(event.recorded_at)} · {(displayed?.get(event.event_id) ?? event).title} · {event.event_id}</option>)}
</select></label>
<div className="research-comparison-controls">
<span role="status">{zh ? `${changed.length} 个字段不同` : `${changed.length} fields differ`}</span>
Expand All @@ -57,7 +64,7 @@ export function ResearchRecordComparison({events, selected, zh, stateLabel, time
{[selected, comparison].map((event, i) => <div key={event.event_id}><strong>{i === 0 ? (zh ? "所选记录" : "Selected record") : (zh ? "对照记录" : "Comparison record")}</strong><p>{timeLabel(event.recorded_at)}</p></div>)}
</div>
{(showAll ? researchComparisonFields : changed).map(field => <section key={field} className="research-comparison-field">
<h4>{labels[field][zh ? 1 : 0]}</h4>
<h4>{field === "summary" && renderSummary ? (zh ? "记录正文" : "Recorded text") : labels[field][zh ? 1 : 0]}</h4>
<div className="research-comparison-pair">{[selected, comparison].map((event, i) => <div key={event.event_id}>
<span className="research-comparison-side">{i === 0 ? (zh ? "所选记录" : "Selected record") : (zh ? "对照记录" : "Comparison record")}</span>
<div>{value(event, field)}</div>
Expand Down
7 changes: 7 additions & 0 deletions docs/development/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,15 @@ When an explicit analysis supplies a whole-history translation, use it throughou
the matching research snapshot, including routes, decisions and expanded history.
Offer one in-place original/translation switch; keep coverage and source/version
detail in contextual help, with incomplete or inapplicable coverage visible.
Version comparison uses that same qualified field projection for both records,
while lineage and change counts still compare original records. Keep both
originals readable in place; a translation must not create a source change.
Keep original evidence directly readable. A language switch only reads; missing
or failed translations retain original text and never silently launch a model.

版本比较复用两条记录已核对的同一快照译文;版本关系与差异数仍按原始记录计算,
两侧原文可原位展开,不把译文变化算作来源变化。

Review the displayed translation against its original and cited context, including
quantities, their referents, negation and uncertainty. Field coverage and preserved
literals do not establish readable meaning. When the source supports a quantity,
Expand All @@ -251,6 +257,7 @@ existing task and review.
的连写英文;准确原词仍可查,含义不明的计数、单位和标识不猜测。修订沿版本化分析
处理,评审不自动重跑模型或覆盖已存结果;剩余正文问题记入原任务和评审。


刷新和前进后退保留视图、时间分组与证据焦点;标签之间保留阅读位置。选择状态按
Goal 和来源隔离。研究中从侧栏切换 Goal 仍停留在研究页,读取新 Goal 自身证据;
返回恢复原 Goal 的选择。首页及任务、会话、成果的明确链接保留各自目的地。
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Research version language

Synthetic records served by the packaged Chat frontend and the disposable
production SQLite/history/analysis HTTP owners. No model or solver runs.
These captures show renderer behavior, not installed-App adoption or independently
qualified translation meaning.

- Rendered product source: `4a49f62f0eb5ac74c524ac26155f03d3c47c2433`.
- The rendered source includes the current-main translated-search integration;
the evidence refresh changes only this scenario and its captures.
- Before: `fb0303f33327c51da434a215f0c757b5d1a5b16a`.
- Both views use the same 31-event fixed snapshot, including two revisions of
one finding, a refuted observation and an unverified follow-up.
- `first-*` is the complete first viewport; `before-*` and `after-*` show the
requested comparison on desktop and at 390px.
- After: comparison reuses the saved field translations; original records still
determine the version pair and four changed fields. Full originals remain
available in place.
- `partial-*` intentionally omits a field and retains its original text.
The existing translation-coverage indicator stays available in the page header.

The existing `research-display-language` browser scenario recreates these views
and verifies originals, exact comparison selection, unchanged source-difference
counts, invalid/missing/source/language fallback, citation return and read-only
behavior. Its existing explicit execution-refusal case makes one rejected POST;
ordinary reading never executes analysis.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading