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 @@ -58,3 +58,11 @@
@keyframes analysis-request-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .analysis-spin { animation: none; } }
@media (max-width: 640px) { .research-analysis { padding: 16px; } .analysis-episode > summary { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; } .analysis-episode-verdicts { grid-column: 1; grid-row: 2; flex-direction: column; gap: 0; } .analysis-episode > summary > svg { grid-column: 2; grid-row: 1 / 3; } .analysis-episode-context { grid-template-columns: 1fr; gap: 12px; } .analysis-method dl { grid-template-columns: 1fr; gap: 2px; } .analysis-method dd { margin-bottom: 8px; } .analysis-pack-choice { flex-direction: column; align-items: stretch; } }

/* Readback has one status surface, shared by the summary and its detail. */
.analysis-read-error,.analysis-read-empty{display:flex;align-items:center;flex-wrap:wrap;gap:8px;margin-block:12px;color:var(--pw-muted)}
.analysis-read-error>button,.analysis-read-empty>button{display:inline-flex;align-items:center;gap:4px;min-height:44px;border:1px solid var(--pw-line);border-radius:6px;padding:8px 12px;color:var(--pw-text);background:var(--pw-bg);font:inherit;cursor:pointer}
.analysis-read-error>button:disabled,.analysis-read-empty>button:disabled{opacity:.5;cursor:default}
.analysis-read-loading{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;padding-block:16px}
.analysis-read-loading>span{height:5rem;border-radius:6px;background:var(--pw-line)}
@media(max-width:640px){.analysis-read-loading{grid-template-columns:repeat(2,minmax(0,1fr))}}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ function NativeResults({native, zh, onCitation}: {native: ResearchProgressPack["
</section>;
}

export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelect, onCitation}: {pack: ResearchProgressPack | null; packs: Pick<ResearchProgressPack, "pack_id" | "title">[]; selectedId: string; error: string; zh: boolean; onSelect: (id: string) => void; onCitation: (citation: AssessmentCitation, origin?: HTMLButtonElement) => void}) {
export function ResearchProgress({pack: p, packs, selectedId, zh, onSelect, onCitation}: {pack: ResearchProgressPack | null; packs: Pick<ResearchProgressPack, "pack_id" | "title">[]; selectedId: string; zh: boolean; onSelect: (id: string) => void; onCitation: (citation: AssessmentCitation, origin?: HTMLButtonElement) => void}) {
const measured = p?.solver_usage, usage = measured?.projection, result = p?.result;
const number = (n: number) => new Intl.NumberFormat(zh ? "zh-CN" : "en", {maximumFractionDigits: 1}).format(n);
const percent = (n: number) => new Intl.NumberFormat(zh ? "zh-CN" : "en", {style: "percent", maximumFractionDigits: 0}).format(n);
Expand All @@ -98,9 +98,6 @@ export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelec
<p>{p?.selection.exhaustive ? (zh ? "仅覆盖声明的固定分析范围,不等于覆盖全部研究。" : "Covers the declared fixed analysis scope, not necessarily the whole study.") : p?.selection.population_episodes == null ? (zh ? "定向抽样,全程覆盖未知。" : "Purposive sample; whole-run coverage is unknown.") : (zh ? "定向抽样,不代表全程。" : "Purposive sample, not the whole run.")}</p>
<p>{zh ? "原生测量缺失时保持未采集;分析器判读不能补成实际收益。完整取样规则与版本在“来源与用量”中。" : "Missing native measurements remain uncaptured; judgments cannot supply realized utility. Full selection rules and revisions are under Sources and usage."}</p>
</ContextHelp></div>{p ? <p className="research-progress-scope" data-pack-id={p.pack_id}>{number(p.episode_count)} {zh ? "个片段" : "episodes"} · {p.selection.exhaustive ? (zh ? "固定范围" : "Fixed scope") : (zh ? "定向抽样" : "Purposive sample")}{result ? (zh ? " · 未校准" : " · Uncalibrated") : ""}</p> : null}</div>{packs.length > 1 ? <label>{zh ? "评估范围" : "Evaluation scope"}<select value={selectedId} onChange={e => onSelect(e.target.value)}>{packs.map(pack => <option key={pack.pack_id} value={pack.pack_id}>{pack.title}</option>)}</select></label> : null}</header>
{error ? <p className="research-progress-warning" role="alert">{p
? (zh ? "无法核对本次来源;已显示的值仅为上次读回。" : "The source could not be refreshed. Displayed values are from the last readback.")
: (zh ? "所选来源暂不可读,未显示其他范围的结果。" : "The selected source could not be read. No result from another scope is shown.")} {error}</p> : null}
{p ? <>
{p.applicability === "historical" ? <p className="research-progress-warning" role="status">{zh ? "历史分析:来源或方法已变化,不能当作当前判断。" : "Historical analysis: the source or method changed; these are not current judgments."}</p> : null}
{p.native_outcomes ? <RetainedImprovement native={p.native_outcomes} zh={zh} onCitation={onCitation}/> : null}
Expand Down Expand Up @@ -178,6 +175,6 @@ export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelec
<dt>{zh ? "覆盖边界" : "Coverage limits"}</dt><dd>{zh ? "仅本次完整捕获;继承前缀、设置、原生评测、操作者及后续未完成轮不包含。金额与全研究总成本未知。" : "This complete capture only. Inherited prefixes, setup, native evaluation, operator work and later incomplete Turns are excluded. Dollar and total study costs are unknown."}</dd>
</dl></> : null}
</section></details>
</> : !error ? <p role="status">{zh ? "正在核对此范围的分析来源…" : "Checking analysis sources for this scope…"}</p> : null}
</> : null}
</section>;
}
7 changes: 6 additions & 1 deletion docs/development/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,12 @@ observation"). Reduced motion stops the arc; the ring and core still read live.
turning a read-only source into a setup task. A known empty state and an
unobserved source remain different states.
- **Loading** holds the final geometry with a quiet skeleton; a region does
not collapse to one spinner and then jump.
not collapse to one spinner and then jump. An initial read is not evidence of
an empty or disconnected source. Bind loading, errors and results to the same
Goal/source/language/selection; a delayed response cannot borrow another
scope's content. Keep one read-status surface across summary and detail,
with retry available where the user encountered the failure. A failed refresh
may retain the same scope's last result, visibly marked as such.
- **Region failure** is one announced line (icon, plain cause, how to retry)
sized to its content. Reserve banners for a whole-surface failure.
- **Unknown** stays visible as "—" with its reason; it never becomes zero or
Expand Down
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.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Source(GoalSource):
events += [build_explore_edge_event(goal_id=self.goal_id,from_node='route'+str(i),to_node='route'+str(i+1),edge_type='depends_on',recorded_at='2026-01-01T23:%02d:00Z'%(i*30)) for i in range(2)]
return events
assets=pathlib.Path(sys.argv[2])
servers=[RuntimeWorkspaceServer(Source(n),assets) for n in ('Retained','Fresh','Distinct')]
servers=[RuntimeWorkspaceServer(Source(n),assets,analysis_runtime_root=pathlib.Path(sys.argv[1]).parent / "analysis") for n in ('Retained','Fresh','Distinct')]
catalog=[dict(id=s.source.id,label=s.source.label,status_url=s.status_url) for s in servers]
for s in servers:
s.catalog=catalog
Expand All @@ -61,12 +61,47 @@ try {
page.on("pageerror", error => errors.push(String(error)));
const url = new URL("/chat/", catalog[0].status_url);
url.search = new URLSearchParams({ goalId: "same-goal", view: "overview", statusUrl: process.env.LOOPX_RUNTIME_READER_BEFORE === "1" ? catalog[0].status_url : "/status.json" });
let holdAnalysis = true, failAnalysis = false, releaseAnalysis, readStarted;
let analysisWrites = 0;
await page.route("**/api/chat/research-analysis?*", async route => {
if (route.request().method() !== "GET") {analysisWrites++; return route.abort();}
if (holdAnalysis) {readStarted(); await new Promise(resolve => {releaseAnalysis = resolve;});}
if (failAnalysis) return route.fulfill({status:503, json:{error:"Synthetic analysis transport failure"}});
return route.continue();
});
await page.goto(url.toString());
await page.getByRole("heading", { name: "Retained research", exact: true }).waitFor();
if (process.env.LOOPX_RUNTIME_READER_BEFORE === "1") {
await page.screenshot({path:resolve(output,"before-desktop.png")});
} else {
const initialRead = new Promise(resolve => {readStarted = resolve;});
await page.locator(".personal-goal-tabs").getByRole("button", {name:"研究", exact:true}).click();
await initialRead;
await page.locator(".research-history-coverage").filter({hasText:"92 / 92"}).waitFor();
const history = page.getByRole("region", {name:"研究历程",exact:true});
await page.screenshot({path:resolve(output,"analysis-pending-desktop.png")});
assert.doesNotMatch(await history.innerText(),/尚未接入深度分析|尚无可分析/,
"A pending read does not prove an analysis connection is absent");
await history.getByRole("status",{name:"正在读取分析"}).waitFor();
failAnalysis = true; holdAnalysis = false; releaseAnalysis();
const failure = history.getByRole("alert");
await failure.waitFor();
assert.equal(await history.locator(".research-analysis").count(),0,"Retry is available without opening the analysis panel");
await page.screenshot({path:resolve(output,"analysis-failure-desktop.png")});
const retryRead = new Promise(resolve => {readStarted = resolve;});
holdAnalysis = true;
await failure.getByRole("button",{name:"重试读取",exact:true}).click();
await retryRead;
assert.equal(await failure.getByRole("button",{name:"重试读取",exact:true}).isDisabled(),true);
assert.doesNotMatch(await history.innerText(),/尚未接入深度分析|尚无可分析/);
await page.setViewportSize({width:390,height:844});
await page.screenshot({path:resolve(output,"analysis-retry-phone.png")});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false);
failAnalysis = false; holdAnalysis = false; releaseAnalysis();
await failure.waitFor({state:"hidden"});
await history.locator(".analysis-read-loading").waitFor({state:"hidden"});
assert.equal(analysisWrites,0,"Read recovery never starts an analysis");
await page.setViewportSize({width:1512,height:982});
await page.locator(".research-history-coverage").filter({hasText:"92 / 92"}).waitFor();
assert.equal(new URL(new URL(page.url()).searchParams.get("statusUrl")).searchParams.get("source_namespace"),new URL(catalog[0].status_url).searchParams.get("source_namespace"),"the initial relative entry binds its physical source before subsequent navigation");
assert.equal(await page.getByText("运行来源 · 只读", {exact:true}).count(), 1);
Expand Down Expand Up @@ -156,6 +191,7 @@ try {
await page.setViewportSize({width:390,height:844});
await page.screenshot({path:resolve(output,"phone.png")});
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false);
assert.equal(analysisWrites,0);
assert.deepEqual(errors,[]);
}
console.log("runtime source workspace browser smoke: ok");
Expand Down
Loading