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 @@ -9,6 +9,33 @@ import {createElement} from "react";
import {renderToStaticMarkup} from "react-dom/server";
import {resolveTestPython} from "../../../../scripts/test-python.mjs";
import {readAnalysisSnapshot, ResearchAnalysisPanel} from "../src/features/personal-workspace/goal-research-analysis.tsx";
import {ResearchCaseStudy} from "../src/features/personal-workspace/research-case-study.tsx";
import {ResearchGoalNavigation} from "../src/features/personal-workspace/goal-research-workbench.tsx";

// A case belongs to a source-scoped child Goal. Parent navigation must never
// combine private-source siblings or turn organization into execution.
const caseFixture = JSON.parse(await readFile(resolve('src/data/fixtures/case-study-child.json'), 'utf8'));
const study = caseFixture.analyses[0].case_study;
const renderedStudy = renderToStaticMarkup(createElement(ResearchCaseStudy, {
study, zh: false, historical: true, scope: caseFixture.analyses[0].input.scope, onCitation: () => {throw Error('read executed a citation');},
}));
assert.ok(renderedStudy.includes(study.title));
assert.ok(renderedStudy.includes('Showing the saved Chinese original.'));
assert.ok(renderedStudy.includes('not current results'));
assert.ok(renderedStudy.includes('unknown'), 'Missing values do not become zero');
for (const section of study.sections) assert.ok(renderedStudy.includes(section.title));
const goals = [
{goalId:'parent',title:'Benchmark'},
{goalId:'child',parentGoalId:'parent',title:'Local case'},
{goalId:'foreign',parentGoalId:'parent',title:'Foreign case',sourceAccess:{namespace:'other',label:'Other',readOnly:true}},
];
const navigate = goalId => renderToStaticMarkup(createElement(ResearchGoalNavigation, {
goalId, goals, zh:false, onSelect: () => {throw Error('read navigated');},
}));
assert.ok(navigate('parent').includes('Local case'));
assert.ok(!navigate('parent').includes('Foreign case'));
assert.ok(navigate('child').includes('Benchmark'));
assert.equal(navigate('foreign'), '', 'An absent same-source parent is not a navigation grant');

// A language-specific cache miss must not hide the same registered pack's
// older, unspecified-language result or turn a read into model execution.
Expand Down
165 changes: 165 additions & 0 deletions apps/presentation/dashboard/src/data/fixtures/case-study-child.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
{
"goal_id": "synthetic-case-search",
"parent_goal_id": "synthetic-benchmark",
"display_name": "Search · Case 研究",
"events": [],
"analyses": [
{
"title": "初版解读",
"input": {
"schema_version": "trajectory_assessment_input_v1",
"scope": {
"goal_id": "synthetic-solver",
"case_id": "Search",
"run_id": "synthetic-run",
"revision": "fixed-synthetic-evidence",
"window": "Three synthetic observations"
},
"objective": "Compare valid error measurements without inventing retention or causality.",
"selection": {
"rule": "Complete synthetic illustration",
"population_episodes": 1,
"exhaustive": true
},
"sources": [
{
"id": "facts",
"artifact": "facts.txt",
"artifact_sha256": "2dec56af0cdca4e603227b1a42b56d15c2ccec3ad4973bd2b34d2d94122aeb91",
"first_line": 1,
"text": "Synthetic candidate A measured error 7 points.\nSynthetic candidate B was invalid and has no score.\nSynthetic candidate C measured error 3 points.\nRetention and matched treatment controls were not captured."
}
],
"episodes": [
{
"id": "search",
"question": "What changed and what is still unknown?",
"source_ids": [
"facts"
],
"eligible_metrics": [
"benefit_test"
]
}
]
},
"artifacts": {
"facts.txt": "Synthetic candidate A measured error 7 points.\nSynthetic candidate B was invalid and has no score.\nSynthetic candidate C measured error 3 points.\nRetention and matched treatment controls were not captured."
},
"case_study": {
"title": "搜索示例:误差下降了,是否真正采用仍未知",
"language": "zh",
"summary": "合成观测中,后续候选把误差从 **7 分降至 3 分**,中间还有一次无效候选。这个变化值得研究,但不能直接称作最终交付,更不能当成 LoopX 的因果收益。\n\n理解本案需要分开三个问题:怎样算更好,测到了什么,以及改善是否真的留在最终成果里。下面按这个顺序解释,原始事实可在每一节查阅。",
"citations": [
{
"source_id": "facts",
"first_line": 1,
"last_line": 4
}
],
"sections": [
{
"id": "problem",
"title": "先理解题目:什么才算一次有效改善",
"text": "本例用误差衡量候选结果与目标的距离,因此数值越低越好。有效性先于数值比较:一次未通过有效性检查的尝试没有可比较的分数,不能把它补成零。\n\n**本页全部数据都是合成的展示样本。** 它用于验证负例、未知状态和证据阅读,不代表真实 benchmark 的结果。后面的最佳值也只描述这个固定窗口,不代表窗口外的历史。",
"citations": [
{
"source_id": "facts",
"first_line": 1,
"last_line": 4
}
],
"table": {
"columns": [
"概念",
"怎样理解",
"本例中可说什么"
],
"rows": [
[
"误差",
"偏离目标的程度;更低更好",
"候选 C 的 3 分低于 A 的 7 分"
],
[
"无效结果",
"没有通过资格检查",
"候选 B 没有分数,不能当作零"
],
[
"最终采用",
"改善被保留在实际成果中",
"未捕获保留证据,仍未知"
]
]
}
},
{
"id": "trajectory",
"title": "过程与结果:看到进展,也保留空白",
"text": "只看第一轮和最后一轮,会漏掉中间的无效尝试。把三次观测放回顺序中,可以确认后续出现了更低误差,但看不出它为何出现。\n\n这里不连接缺失点,也不从提交序号推导时间成本。没有耗时记录,就不能把一次更好的结果进一步解释成效率提高。",
"citations": [
{
"source_id": "facts",
"first_line": 1,
"last_line": 3
}
],
"series": {
"label": "固定窗口内的观测",
"unit": "误差分 · 更低更好",
"points": [
{
"label": "候选 A",
"value": 7
},
{
"label": "候选 B",
"value": null
},
{
"label": "候选 C",
"value": 3
}
]
},
"timeline": [
{
"label": "第一次",
"title": "有了可比较的起点",
"text": "候选 A 的误差为 7 分。"
},
{
"label": "第二次",
"title": "一次失败不能从故事中消失",
"text": "候选 B 无效,没有分数。失败仍是过程的一部分。"
},
{
"label": "第三次",
"title": "更低误差出现,但保留未明",
"text": "候选 C 的误差为 3 分;最终采用仍需另一份证据。"
}
]
},
{
"id": "limits",
"title": "关于系统强弱,证据支持到哪一步",
"text": "这条记录支持“窗口内出现过更好的有效候选”。它还不能回答是哪种机制促成改善:模型本身、额外搜索、工具反馈或结构化状态,都可能影响结果。\n\n如果没有相同条件下的对照,便无法把全部变化归于某一个系统。类似地,没有最终成果的保留记录,也无法把最佳观测值称为交付值。\n\n这些信息共同界定了判断的范围:**有进展的观测,有无效尝试,也有尚未回答的问题。**",
"citations": [
{
"source_id": "facts",
"first_line": 4,
"last_line": 4
}
]
}
],
"limitations": [
"本页是合成示例;不作为科研结论或产品效果证据。",
"未记录保留状态、耗时或匹配对照,不能补造这些事实。"
]
},
"outputs": {}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"goal_id": "synthetic-case-unread",
"parent_goal_id": "synthetic-benchmark",
"display_name": "另一 Case · 尚无解读",
"events": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"goal_id": "synthetic-benchmark",
"display_name": "Benchmark · 研究示例",
"events": []
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {requestJson} from "../../data/chat";
import {sourceReadApiUrl} from "../../data/local-status-query";
import {ContextHelp} from "./context-help";
import {ResearchSynthesis} from "./goal-research-synthesis";
import {ResearchCaseStudy} from "./research-case-study";
import type {TrajectoryCaseStudy} from "../../../../../../loopx/control_plane/capabilities/trajectory_case_study.js";
import "./goal-research-analysis.css";

// Transport views of the existing toolkit producer, not an assessment, score,
Expand All @@ -16,6 +18,8 @@ type Job = {status: string; attempt?: number; analysis_wall_seconds?: number | n
config?: {model: string; effort: string; service_tier: string}; served_service_tier?: string | null;
usage?: {input_tokens?: number; output_tokens?: number; reasoning_output_tokens?: number} | null};
type Pack = ResearchProgressPack & {source_count: number; analysis_key: string; method_version: string;
case_study?: TrajectoryCaseStudy;
case_study_summary?: {title: string; language: TrajectoryOutputLanguage};
display_request?: {schema_version: 'research_display_v1'; snapshot_key: string; source_id: string; source_namespace: string; eligible: number};
result_attempt?: number;
result_job?: Job;
Expand Down Expand Up @@ -346,6 +350,26 @@ export function ResearchReportSummary({analysis: a}: {analysis: Controller}) {
onExpand={origin => {a.trigger.current = origin; a.show();}} onCitation={a.readCitation}/></div> : null;
}

export function ResearchCaseReading({analysis: a, onHistory}: {analysis: Controller; onHistory: () => void}) {
const packs = (a.snapshot?.catalog.packs ?? []).filter(p => p.case_study_summary);
const p = a.pack;
const firstStudy = packs[0]?.pack_id;
useEffect(() => {
if (!a.loading && !a.readError && p && !p.case_study && firstStudy) a.select(firstStudy);
}, [a.loading, a.readError, p?.pack_id, Boolean(p?.case_study), firstStudy]);
return <section className="research-case-reader" aria-label={a.zh ? "Case 深度解读" : "Case studies"}>
<header className="case-reader-toolbar"><label>{a.zh ? "解读版本" : "Study version"}
<select aria-label={a.zh ? "选择解读版本" : "Select study version"} value={p?.case_study ? p.pack_id : ""} onChange={e => a.select(e.target.value)}>
{!p?.case_study ? <option value="">{a.zh ? "选择已保存的解读" : "Choose a saved study"}</option> : null}
{packs.map(pack => <option key={pack.pack_id} value={pack.pack_id}>{pack.title}</option>)}
</select></label><button type="button" onClick={onHistory}>{a.zh ? "研究记录与分析" : "Research records and analysis"} ↗</button></header>
<AnalysisReadState analysis={a}/>
{p?.case_study ? <ResearchCaseStudy key={p.pack_id} study={p.case_study} zh={a.zh} historical={p.applicability !== "current"}
scope={p.scope} onCitation={a.readCitation}/> : !a.loading && !a.readError ? <p>{a.zh ? "选择一份解读,阅读问题背景、策略差异、关键转折与证据。" : "Choose a study to read its background, approaches, turning points and evidence."}</p> : null}
<ResearchCitationPanel analysis={a}/>
</section>;
}

function CitedToolText({text, zh}: {text: string; zh: boolean}) {
// Decode only the saved direct shell-result envelope, never recursively
// reinterpret arbitrary JSON or infer successful work from its text.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {AlertCircle, ArrowLeft, ArrowRight, Circle, CircleHelp, FlaskConical, Gi
import {Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState} from "react";
import {defaultParseSearch} from "@tanstack/react-router";
import {ResearchRecordSearch} from "./research-record-browser";
import {ResearchAnalysisButton, ResearchAnalysisPanel, ResearchAnalysisStatus, ResearchCitationPanel, ResearchProgressSummary, ResearchReportSummary, useResearchAnalysis} from "./goal-research-analysis";
import {ResearchAnalysisButton, ResearchAnalysisPanel, ResearchAnalysisStatus, ResearchCitationPanel, ResearchProgressSummary, ResearchReportSummary, ResearchCaseReading, useResearchAnalysis} from "./goal-research-analysis";
import {buildExploreTimeline, parseExploreFindingSections, type TimelineFinding} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js";
import {GoalResearchTimeline} from "./goal-research-timeline";
import {requestJson} from "../../data/chat";
Expand Down Expand Up @@ -54,6 +54,22 @@ const viewFromUrl = (goal: string) => {
* a judge. All recorded updates survive; unsupported analysis stays unavailable. */
export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, readSourceUrl}: {goalId: string; active: boolean; zh: boolean; refreshRevision?: number; readSourceUrl?: string}) {
const analysis = useResearchAnalysis(goalId, active, zh, readSourceUrl);
const readCaseMode = () => {
const query = new URLSearchParams(window.location.search);
return query.get("research_goal") !== goalId || query.get("research_reading") !== "history";
};
const [caseReading, setCaseReading] = useState(readCaseMode);
useEffect(() => {
const changed = () => setCaseReading(readCaseMode());
changed(); window.addEventListener("popstate", changed);
return () => window.removeEventListener("popstate", changed);
}, [goalId, readSourceUrl]);
const setCaseMode = (reading: boolean) => {
const url = new URL(window.location.href);
url.searchParams.set("research_goal", goalId);
url.searchParams.set("research_reading", reading ? "case" : "history");
window.history.pushState({}, "", url); setCaseReading(reading);
};
const [read, setRead] = useState<Read>({events: [], total: null, complete: false});
const [originalDisplay, setOriginalDisplay] = useState(false);
const [displayIdentity, setDisplayIdentity] = useState<{events: ExploreHistoryEvent[]; namespace: string; key: string} | null>(null);
Expand Down Expand Up @@ -449,7 +465,10 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re
const emptyHistory = !busy && !readFailure && read.total === 0;
const emptyWithAnalysis = emptyHistory && Boolean(analysis.pack?.result || analysis.pack?.native_outcomes);
const emptyHistoryMessage = zh ? "这个 Goal 尚无研究历程" : "No research history for this Goal yet";
const hasCases = (analysis.snapshot?.catalog.packs ?? []).some(p => p.case_study_summary);
if (hasCases && caseReading) return <ResearchCaseReading analysis={analysis} onHistory={() => setCaseMode(false)}/>;
return <section className="research-history" aria-label={copy.title} aria-busy={busy}>
{hasCases ? <button type="button" className="case-return-reader" onClick={() => setCaseMode(true)}>{zh ? "← Case 深度解读" : "← Case studies"}</button> : null}
{progressiveOverview ? <>
<nav className="research-reading-order" aria-label={zh ? "研究阅读" : "Research reading"}>
<button type="button" onClick={() => jump(reportAnchor.current)}>{zh ? "汇报" : "Report"}</button><span aria-hidden>→</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
import {useState} from "react";
import {GoalResearchHistory} from "./goal-research-history";
import {GoalExploreResults} from "./goal-explore-results";
import {goalHierarchy} from "./goal-hierarchy";
import type {WorkspaceGoal} from "./personal-workspace-model";
import './research-case-study.css';

type ResearchGoal = Pick<WorkspaceGoal, 'goalId' | 'parentGoalId' | 'title' | 'sourceAccess'>;

/** Parentage organizes research within a source; it never combines evidence or execution scope. */
export function ResearchGoalNavigation({goalId, goals, zh, onSelect}: {
goalId: string; goals: ResearchGoal[]; zh: boolean; onSelect: (goalId: string) => void;
}) {
const selected = goals.find(g => g.goalId === goalId);
if (!selected) return null;
const tree = goalHierarchy(goals.filter(g => g.sourceAccess?.namespace === selected.sourceAccess?.namespace));
const parentId = tree.parents.get(goalId), parent = parentId ? tree.byId.get(parentId) : undefined;
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}
{children.length ? <><h2>{zh ? '子 Goal 研究' : 'Research in child Goals'}</h2>
<div className="research-child-goals">{children.map(child => <button type="button" key={child.goalId} onClick={() => onSelect(child.goalId)}>{child.title}<span aria-hidden="true">↗</span></button>)}</div></> : null}
</nav>;
}

/** One read surface for the selected original Goal; opening it never starts research. */
export function GoalResearchWorkbench(props: {goalId: string; active: boolean; zh: boolean; refreshRevision: number; readSourceUrl?: string}) {
export function GoalResearchWorkbench(props: {goalId: string; active: boolean; zh: boolean; refreshRevision: number; readSourceUrl?: string; goals?: ResearchGoal[]; onSelectGoal?: (goalId: string) => void}) {
const [evidenceOpen, setEvidenceOpen] = useState(false);
return <div className="goal-research-workbench">
{props.goals && props.onSelectGoal ? <ResearchGoalNavigation goalId={props.goalId} goals={props.goals} zh={props.zh} onSelect={props.onSelectGoal}/> : null}
<GoalResearchHistory {...props}/>
<details className="research-saved-evidence" open={evidenceOpen} onToggle={event => setEvidenceOpen(event.currentTarget.open)}>
<summary>{props.zh ? "阅读已保存的结论" : "Read saved findings"}</summary>
Expand Down
Loading
Loading