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 @@ -209,7 +209,7 @@ export function ResearchAnalysisButton({analysis: a}: {analysis: Controller}) {
// explanation is optional; cached results and unavailable states are reads.
const readOnly = unavailable || a.readError || a.pack?.result || a.pending || a.recordedRunning;
return <div className="research-analysis-entry"><div className="research-analysis-entry-actions"><button ref={a.trigger} type="button" disabled={a.loading && !a.pack} onClick={() => readOnly ? a.show() : void a.execute(a.pack?.job?.status === "failed" || a.pack?.job?.status === "cancelled")} aria-expanded={a.location.open}>
<Sparkles size={14} aria-hidden/>{a.pack?.result ? (a.zh ? "查看分析" : "View analysis") : analysisActionLabel(a.pack, a.zh)}
<Sparkles size={14} aria-hidden/>{a.readOnly || a.pack?.result ? (a.zh ? "查看分析" : "View analysis") : analysisActionLabel(a.pack, a.zh)}
</button><ContextHelp label={sourceUnavailable ? (a.zh ? "分析来源未就绪" : "Analysis source unavailable") : (a.zh ? "分析说明" : "About deep analysis")} icon={sourceUnavailable ? CircleHelp : undefined}>
{sourceUnavailable ? <p>{reasonCopy(a.pack?.reason_code ?? a.snapshot?.catalog.reason_code, a.zh)}</p> : null}
{a.pack ? <p>{a.zh ? "分析请求设置(固定):" : "Fixed analysis request: "}{a.pack.config.model} / {a.pack.config.effort} / {a.pack.config.service_tier}</p> : null}
Expand Down Expand Up @@ -480,8 +480,8 @@ export function ResearchAnalysisPanel({analysis: a, onHistory}: {analysis: Contr
const zh = a.zh, p = a.pack, result = p?.result;
const resultJob = p?.result_job ?? p?.job;
const failed = p?.job?.status === "failed" || p?.job?.status === "cancelled";
const sourceAvailable = p?.availability === "available" && !a.readOnly;
const available = sourceAvailable && !a.readError;
const sourceAvailable = p?.availability === "available";
const available = sourceAvailable && !a.readOnly && !a.readError;
const benefit = result?.metrics.find(m => m.metric === "benefit_test");
const headline = !result ? null : benefit?.no ? (zh ? "仍有研究片段尚未检验原目标收益" : "Some episodes have not tested original-objective utility")
: benefit?.unknown ? (zh ? "原目标收益的检验情况仍未知" : "Original-objective utility testing remains unknown")
Expand All @@ -507,12 +507,13 @@ export function ResearchAnalysisPanel({analysis: a, onHistory}: {analysis: Contr
<small>{zh ? "重新分析完整固定范围,产生新的分析用量;保留上一轮结果。" : "Reanalyzes the full fixed scope with new analysis usage; the previous result is preserved."}</small>
</div> : null}
{!result || a.pending || a.recordedRunning ? <div className="analysis-actions">
{a.pending || a.recordedRunning ? <><p role="status">{a.pending ? <LoaderCircle size={15} aria-hidden className="analysis-spin"/> : null}{a.pending ? (zh ? "正在处理分析请求…" : "Processing analysis request…") : (zh ? "上次记录仍待分析结果;当前执行状态尚未核验。" : "The last receipt awaits a result; current execution is unverified.")}</p><button type="button" onClick={() => void a.cancel()} disabled={a.cancelPending || p.job?.cancel_requested}>{p.job?.cancel_requested ? (zh ? "已请求取消" : "Cancellation requested") : a.cancelPending ? (zh ? "正在请求取消…" : "Requesting cancellation…") : (zh ? "取消分析" : "Cancel analysis")}</button></>
{a.pending || a.recordedRunning ? <><p role="status">{a.pending ? <LoaderCircle size={15} aria-hidden className="analysis-spin"/> : null}{a.pending ? (zh ? "正在处理分析请求…" : "Processing analysis request…") : (zh ? "上次记录仍待分析结果;当前执行状态尚未核验。" : "The last receipt awaits a result; current execution is unverified.")}</p><button type="button" onClick={() => void a.cancel()} disabled={a.readOnly || a.cancelPending || p.job?.cancel_requested}>{p.job?.cancel_requested ? (zh ? "已请求取消" : "Cancellation requested") : a.cancelPending ? (zh ? "正在请求取消…" : "Requesting cancellation…") : (zh ? "取消分析" : "Cancel analysis")}</button></>
: result ? <button type="button" onClick={() => void a.read().catch(() => undefined)}><RefreshCw size={14} aria-hidden/>{zh ? "刷新结果" : "Refresh result"}</button>
: a.readOnly ? <><p className={sourceAvailable ? undefined : "analysis-warning"}>{sourceAvailable ? (zh ? "此来源尚无已保存的分析。" : "This source has no saved analysis yet.") : reasonCopy(p.reason_code, zh)}</p><button type="button" onClick={() => void a.read().catch(() => undefined)}>{zh ? "刷新结果" : "Refresh result"}</button></>
: <button className="analysis-primary" type="button" disabled={!available} onClick={() => void a.execute(failed)}><Sparkles size={15} aria-hidden/>{failed ? (zh ? "重试分析" : "Retry analysis") : p.continuation ? analysisActionLabel(p, zh) : (zh ? "分析所选范围" : "Analyze selected scope")}</button>}
{!result && !a.pending && !a.recordedRunning ? <small>{p.continuation?.can_reuse ? (zh ? "复用已有判读,无需新模型调用。" : "Reuses existing judgments without a new model call.") : zh ? "使用本机 Codex 账户,单独产生分析用量;浏览不调用 Agent。" : "Uses the local Codex account with separate analysis usage. Browsing never invokes an Agent."}</small> : null}
{!result && !a.readOnly && !a.pending && !a.recordedRunning ? <small>{p.continuation?.can_reuse ? (zh ? "复用已有判读,无需新模型调用。" : "Reuses existing judgments without a new model call.") : zh ? "使用本机 Codex 账户,单独产生分析用量;浏览不调用 Agent。" : "Uses the local Codex account with separate analysis usage. Browsing never invokes an Agent."}</small> : null}
</div> : null}
{failed && !a.pending ? <p className="analysis-warning" role="status">{p.job?.status === "cancelled" ? (zh ? "分析已取消,研究历史保持可读。" : "Analysis cancelled. Research history remains readable.") : (zh ? "上次分析未完成;可重试,原失败记录会保留。" : "The last analysis did not complete. Retry preserves its failure record.")}</p> : null}
{failed && !a.pending ? <p className="analysis-warning" role="status">{p.job?.status === "cancelled" ? (zh ? "分析已取消,研究历史保持可读。" : "Analysis cancelled. Research history remains readable.") : a.readOnly ? (zh ? "上次分析未完成;失败记录已保留。" : "The last analysis did not complete; its failure record is preserved.") : (zh ? "上次分析未完成;可重试,原失败记录会保留。" : "The last analysis did not complete. Retry preserves its failure record.")}</p> : null}
{failed && !a.pending ? <p className="analysis-attempt-usage" aria-label={zh ? "本次分析用量" : "Current analysis usage"}>
{zh ? "本次尝试" : "This attempt"} · {p.job?.analysis_wall_seconds == null ? (zh ? "耗时未知" : "Time unknown") : `${p.job.analysis_wall_seconds.toFixed(1)}s`} · {zh ? "输入" : "input"} {p.job?.usage?.input_tokens?.toLocaleString() ?? "—"} / {zh ? "输出" : "output"} {p.job?.usage?.output_tokens?.toLocaleString() ?? "—"} tokens
</p> : null}
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.
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.
84 changes: 84 additions & 0 deletions examples/personal-workspace-browser/research-history.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,90 @@ export const researchHistoryScenario = {
await synthesisPage.screenshot({path: resolve(outputDir, 'research-related-episodes-mobile-en.png'), animations: 'disabled'});
await englishReview.getByRole('button', {name: 'Back to report', exact: true}).click();
assert(await englishOrigin.evaluate(e => e === document.activeElement));

// Reading a remote source and executing on it are separate facts. Keep
// the real saved HTTP/store result, changing only source identity and
// explicit transport observations for the negative/recovery cases.
const readonlyStatus = `${synthesisOwner.base}/status.json?goal_id=synthetic-research-readback`;
await synthesisPage.route(target => target.pathname === '/status.json', async route => {
if (new URL(route.request().url()).searchParams.get('view') === 'workspace-directory') {
return route.fulfill({status: 404, json: {error: 'Use the complete captured status source'}});
}
const response = await route.fetch(), value = await response.json();
value.local_dashboard_api = {...value.local_dashboard_api, read_only_sources: [
{id: 'research-readonly', label: 'Read-only research', status_url: readonlyStatus},
]};
await route.fulfill({response, json: value});
});
let readonlyState = 'saved', readonlyWrites = 0;
await synthesisPage.route(target => target.pathname === '/api/chat/research-analysis', async route => {
if (route.request().method() !== 'GET') {
readonlyWrites++;
return route.fulfill({status: 405, json: {error: 'Read-only source'}});
}
if (readonlyState === 'read-error') return route.fulfill({status: 503, json: {error: 'Synthetic read failure'}});
const response = await route.fetch(), value = await response.json();
assert.notEqual(value.execution_enabled, true, 'The isolated owner does not enable model dispatch');
if (readonlyState === 'empty' || readonlyState === 'failed') value.pack.result = null;
if (readonlyState === 'empty') value.pack.job = null;
if (readonlyState === 'failed') value.pack.job = {status: 'failed', attempt: 2,
analysis_wall_seconds: 4, usage: {input_tokens: 10, output_tokens: 5}};
if (readonlyState === 'running') value.pack.job = {status: 'running', attempt: 2};
if (readonlyState === 'changed') Object.assign(value.pack, {availability: 'unavailable', reason_code: 'source_changed'});
await route.fulfill({response, json: value});
});
const readonlyQuery = new URLSearchParams({goalId: 'synthetic-research-readback', view: 'research',
research_analysis: 'synthetic-research-readback', statusUrl: readonlyStatus});
const readonlyPanel = synthesisPage.locator('.research-analysis');
for (const [locale, unavailableText, emptyText] of [
['zh-CN', '分析暂不可用,请刷新重试。', '此来源尚无已保存的分析。'],
['en', 'Analysis is unavailable; refresh to retry.', 'This source has no saved analysis yet.'],
]) {
readonlyState = 'saved';
await synthesisPage.evaluate(locale => localStorage.setItem('loopx-pw-locale', locale), locale);
await synthesisPage.goto(`${synthesisOwner.base}/chat/?${readonlyQuery}`, {waitUntil: 'networkidle'});
await readonlyPanel.locator('.research-report-tabs').waitFor();
assert.equal(await readonlyPanel.getByText(unavailableText, {exact: true}).count(), 0,
'A readable saved report is not unavailable merely because this source cannot execute');
readonlyState = 'empty';
await synthesisPage.reload({waitUntil: 'networkidle'});
await readonlyPanel.getByText(emptyText, {exact: true}).waitFor();
assert.equal(await readonlyPanel.locator('.analysis-primary').count(), 0,
'A read-only empty source offers reading recovery instead of a generation action');
assert.equal(await readonlyPanel.getByText(unavailableText, {exact: true}).count(), 0);
if (locale === 'zh-CN') {
await readonlyPanel.evaluate(e => e.scrollIntoView({block: 'start', behavior: 'instant'}));
await synthesisPage.screenshot({path: resolve(outputDir, 'readonly-analysis-empty-phone.png'), animations: 'disabled'});
}
}
await synthesisPage.evaluate(() => localStorage.setItem('loopx-pw-locale', 'zh-CN'));
readonlyState = 'running';
await synthesisPage.reload({waitUntil: 'networkidle'});
assert(await readonlyPanel.getByRole('button', {name: '取消分析', exact: true}).isDisabled(),
'A stored running receipt cannot enable cancellation on a read-only source');
await readonlyPanel.evaluate(e => e.scrollIntoView({block: 'start', behavior: 'instant'}));
await synthesisPage.screenshot({path: resolve(outputDir, 'readonly-analysis-running-phone.png'), animations: 'disabled'});
readonlyState = 'failed';
await synthesisPage.reload({waitUntil: 'networkidle'});
await readonlyPanel.getByText('上次分析未完成;失败记录已保留。', {exact: true}).waitFor();
assert.match(await readonlyPanel.getByLabel('本次分析用量').innerText(), /4\.0s.*10.*5/s);
assert.equal(await readonlyPanel.getByRole('button', {name: '重试分析', exact: true}).count(), 0);
await readonlyPanel.evaluate(e => e.scrollIntoView({block: 'start', behavior: 'instant'}));
await synthesisPage.screenshot({path: resolve(outputDir, 'readonly-analysis-failed-phone.png'), animations: 'disabled'});
readonlyState = 'changed';
await synthesisPage.reload({waitUntil: 'networkidle'});
await readonlyPanel.getByText('原始来源已变化;保留历史分析,重新核对后才能再次分析。', {exact: true}).waitFor();
await readonlyPanel.locator('.research-report-tabs').waitFor();
readonlyState = 'saved';
await synthesisPage.reload({waitUntil: 'networkidle'});
readonlyState = 'read-error';
await readonlyPanel.getByRole('button', {name: '刷新分析结果', exact: true}).click();
await readonlyPanel.getByRole('alert').filter({hasText: '刷新失败,保留上次结果。'}).waitFor();
await readonlyPanel.locator('.research-report-tabs').waitFor();
readonlyState = 'saved';
await readonlyPanel.getByRole('button', {name: '重试读取', exact: true}).click();
await readonlyPanel.getByRole('alert').waitFor({state: 'hidden'});
assert.equal(readonlyWrites, 0, 'Reading, failed reads and readonly job observations never request execution');
const readback = await (await synthesisPage.request.get(`${synthesisOwner.base}/fixture/readback`)).json();
assert.equal(readback.source_unchanged, true);
assert.equal(readback.mutation_attempts, 0, 'Reading either layer and citations starts no model or state write');
Expand Down
Loading