diff --git a/.github/GOVERNANCE.md b/.github/GOVERNANCE.md index d00a63c806..4b4d842779 100644 --- a/.github/GOVERNANCE.md +++ b/.github/GOVERNANCE.md @@ -322,12 +322,12 @@ voluntary: anyone may decline, narrow, pause, or hand back a scope without losing attribution for their work. Silence is not acceptance or approval. Status reflects accepted scopes and follow-up invitations in #4069 through -2026-10-22. The last column says what still separates each contact from a `CODEOWNERS` route under the +2026-10-24. The last column says what still separates each contact from a `CODEOWNERS` route under the [eligibility rule](#code-owner-eligibility). | Surface | Contact | Responsibility / status | Path to a code-owner route | | --- | --- | --- | --- | -| Chat/runtime session lifecycle | [`@Duang777`](https://github.com/Duang777) | Accepted 2026-09-08: managed-session resume/submit/close, request idempotency, focused regressions and follow-up fixes. Shared goal, quota, lease and permission contracts remain outside this assignment. | Has write access and an accepted scope; needs three qualifying in-scope cross-author reviews. | +| Chat/runtime session lifecycle and cohesive Host recovery | [`@Duang777`](https://github.com/Duang777) | Initial session scope accepted 2026-09-08; [expanded and accepted 2026-10-24](https://github.com/loopx-project/loopx/issues/4069#issuecomment-6377902612): first-review contact for resume/submit/close, request idempotency, focused regressions and follow-up maintenance, including Host/recovery surfaces within the same session boundary. Best-effort availability, no fixed SLA; will state on the PR when unavailable. Shared goal/quota/lease policy, canonical-authority promotion, provider activation, release and bypass decisions remain outside this scope. | Write remains active. No Maintain or code-owner change requested or granted; any future route still needs the existing qualifying cross-author reviews and a separate appointment PR. | | Shared goal authority qualification | [`@wchwawa`](https://github.com/wchwawa) | Accepted 2026-09-08: implementation review, writer/cursor recovery evidence and bounded qualification. Canonical-authority promotion, provider activation and shared-state policy require separate lead-maintainer review. | Has write access and an accepted scope; needs three in-scope cross-author reviews. | | Windows runtime correctness | [`@BigDataDZ`](https://github.com/BigDataDZ) | Accepted 2026-10-12: Python CLI Windows paths/encoding, filesystem behavior and native repro-first test qualification; best-effort availability and stated recusals retained. | Bounded Windows code-owner route recorded above; shared semantic and CI-policy decisions remain with existing owners. | | Usage and host usage ingestion | [`@liubf21`](https://github.com/liubf21) | Invited to coordinate usage correctness and historical-data compatibility review; no acceptance recorded. Pricing policy and unrelated host/session authority remain outside this scope. | Has write access; needs scope acceptance and in-scope reviews. | diff --git a/apps/presentation/dashboard/package-lock.json b/apps/presentation/dashboard/package-lock.json index 745733169c..f4556f1b67 100644 --- a/apps/presentation/dashboard/package-lock.json +++ b/apps/presentation/dashboard/package-lock.json @@ -25,7 +25,7 @@ "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.0", - "zod": "^4.4.3" + "zod": "^4.6.6" }, "devDependencies": { "@types/react": "^19.2.15", @@ -2289,9 +2289,9 @@ } }, "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.6.tgz", + "integrity": "sha512-4UmzQUF5fxvfcYHlbjBmejU+w+hbDnWFx4aS8kBx7nPBHcO2SqPbpAG8snbdg+Xse4pRefR98hgjFgIMCI/UhA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/apps/presentation/dashboard/package.json b/apps/presentation/dashboard/package.json index f8b54919ef..477929a0f1 100644 --- a/apps/presentation/dashboard/package.json +++ b/apps/presentation/dashboard/package.json @@ -98,7 +98,7 @@ "recharts": "^3.8.1", "tailwind-merge": "^3.6.0", "tailwindcss": "^4.3.0", - "zod": "^4.4.3" + "zod": "^4.6.6" }, "devDependencies": { "@types/react": "^19.2.15", diff --git a/apps/presentation/dashboard/src/data/chat.ts b/apps/presentation/dashboard/src/data/chat.ts index 258c576ef5..bdae58f1a9 100644 --- a/apps/presentation/dashboard/src/data/chat.ts +++ b/apps/presentation/dashboard/src/data/chat.ts @@ -2630,6 +2630,14 @@ const privateConversationSchema = z.object({ goal_active: z.boolean().optional(), monitor: z.object({todo_id: z.string(), status: z.string(), cadence_minutes: z.number().int().nullable(), next_due_at: z.string().nullable(), last_checked_at: z.string().nullable()}).nullable().optional(), + report: z.discriminatedUnion("available", [ + z.object({available: z.literal(true), enabled: z.boolean(), configured: z.boolean(), + schedule: z.record(z.string(), z.unknown()).nullable().optional(), next_due_at: z.string().nullable().optional(), + retained_window_due_at: z.string().nullable().optional(), generation_id: z.string().nullable().optional(), + delivery_status: z.string().optional(), publication_verified: z.boolean().optional(), + destination: z.string().optional()}), + z.object({available: z.literal(false), error_code: z.string()}), + ]).optional(), goal_id: z.string().optional(), agent_id: z.string().optional(), session_id: z.string().optional()}), z.object({available: z.literal(false), error_code: z.string()}), ]).optional(), @@ -2695,6 +2703,21 @@ export async function changePrivateAgentTarget(bindingId: string, revision: numb })); } +const stewardReportPlanSchema = z.object({ok: z.literal(true), applied: z.boolean(), stop_status: z.string().optional(), plan: z.object({ + schema_version: z.literal("loopx_steward_report_plan_v0"), plan_id: z.string().regex(/^[a-f0-9]{64}$/), +})}); +export async function configureStewardReport(bindingId: string, operation: "enable" | "pause" | "cancel", schedule?: Record) { + const path = "/api/chat/lark/private-conversations/managed-steward/report"; + const send = (body: unknown) => requestJson(path, {method: "POST", + headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)}); + const request = {binding_id: bindingId, operation, ...(operation === "enable" ? {schedule} : {})}; + const preview = stewardReportPlanSchema.parse(await send({...request, execute: false})); + const applied = stewardReportPlanSchema.parse(await send({...request, execute: true, plan_id: preview.plan.plan_id})); + if (!applied.applied) throw new Error("The original report setting was not applied."); + if (applied.stop_status === "unconfirmed") throw new Error("汇报计划已取消;正在生成的任务是否停止尚未确认。Report cancelled; the active task's stop is unconfirmed."); + return applied; +} + export async function changeOwnerNotifications(bindingId: string, bindingRevision: number, revision: number, enabled: boolean) { return privateConversationsSchema.parse(await requestJson("/api/chat/lark/private-conversations/owner-notifications", { method: "POST", headers: {"Content-Type": "application/json"}, diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css index ceca4ec511..a6be49801b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.css @@ -7,7 +7,7 @@ .research-analysis h3 { font-size: var(--text-heading); font-weight: 600; margin: 0; } .research-analysis h4 { font-size: var(--text-lead); font-weight: 600; line-height: 1.5; margin: 0 0 8px; } .research-analysis p { margin: 8px 0; } -.research-analysis header p, .analysis-scope, .analysis-boundary, .research-analysis small, .analysis-method, .analysis-finding > span { color: var(--pw-muted); font-size: var(--text-caption); } +.research-analysis header p, .analysis-scope, .analysis-boundary, .research-analysis small, .analysis-finding > span { color: var(--pw-muted); font-size: var(--text-caption); } .research-analysis button, .research-analysis select { font: inherit; color: inherit; border: 1px solid var(--pw-line); background: var(--pw-bg); border-radius: 6px; min-height: 44px; padding: 8px 12px; display: inline-flex; align-items: center; justify-content: center; gap: 6px; cursor: pointer; } .research-analysis button:disabled { opacity: .5; cursor: not-allowed; } .research-analysis button.analysis-primary { color: var(--pw-bg); background: var(--pw-text); border-color: var(--pw-text); } @@ -18,33 +18,44 @@ .analysis-actions small { flex-basis: 100%; } .analysis-warning { border-inline-start: 2px solid var(--pw-caution); padding-inline-start: 12px; } .analysis-error { color: var(--pw-red); } -.analysis-finding { border-block-start: 1px solid var(--pw-line); padding-block: 20px 8px; margin-top: 20px; max-width: 76ch; } +.analysis-finding { padding-block: 12px 8px; margin-top: 12px; max-width: 76ch; } .analysis-finding h4 { margin-top: 8px; } +.analysis-episode-columns, .analysis-episode > summary { display: grid; grid-template-columns: minmax(0, 1fr) 8rem 6rem 9rem 1rem; align-items: baseline; gap: 16px; } +.analysis-episode-columns { color: var(--pw-muted); padding-block: 8px; border-bottom: 1px solid var(--pw-line); } +.analysis-episode-group + .analysis-episode-group { margin-top: 12px; } +.research-analysis .analysis-shared-question { margin: 12px 0 4px; max-width: 76ch; color: var(--pw-muted); font-size: var(--text-body); } .analysis-episode { border-block-end: 1px solid var(--pw-line); } -.analysis-episode > summary { display: flex; align-items: center; gap: 16px; min-height: 64px; padding-block: 12px; cursor: pointer; list-style: none; } +.analysis-episode > summary { min-height: 48px; padding-block: 12px; cursor: pointer; list-style: none; } .analysis-episode > summary::-webkit-details-marker { display: none; } -.analysis-episode-name { flex: 1; min-width: 0; max-width: 76ch; font-weight: 500; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } +.analysis-episode-name { min-width: 0; max-width: 76ch; font-weight: 500; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } .analysis-episode[open] > summary .analysis-episode-name { display: block; } -.analysis-episode-verdicts { display: flex; flex-shrink: 0; flex-wrap: wrap; justify-content: end; gap: 4px 24px; } +.analysis-verdict-label { display: none; } +.analysis-episode-exceptions { display: flex; flex-direction: column; color: var(--pw-muted); } .analysis-episode > summary > svg { flex-shrink: 0; } .analysis-episode[open] > summary > svg { transform: rotate(180deg); } -.analysis-episode-body { padding-block: 8px 24px; max-width: 90ch; } +.analysis-episode-body { padding-block: 8px 24px; max-width: 90ch; font-size: var(--text-lead); } .analysis-episode-context { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; margin-block: 20px; } -.analysis-episode-context strong, .analysis-episode-context small { display: block; } -.analysis-episode-context strong { font-weight: 500; } +.analysis-episode-context > :only-child { grid-column: 1 / -1; max-width: 76ch; } +.analysis-reason-judgments { display: flex; flex-wrap: wrap; gap: 4px 20px; font-size: var(--text-body); } +.analysis-reason-judgments > span { display: inline-flex; flex-wrap: wrap; gap: 8px; } +.analysis-reason-judgments strong { font-weight: 500; } +.analysis-reason-judgments > span > span { color: var(--pw-muted); } +.analysis-episode-question { margin-top: 12px; } +.analysis-episode-question > summary { cursor: pointer; padding-block: 10px; min-height: 44px; } +.analysis-episode-question h5 { font: inherit; font-weight: 500; margin: 12px 0 0; } .analysis-judgments { border-top: 1px solid var(--pw-line); } .analysis-judgments details { border-bottom: 1px solid var(--pw-line); } .analysis-judgments summary { min-height: 48px; padding-block: 10px; cursor: pointer; display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } .analysis-judgments summary strong { font-weight: 500; } -.analysis-judgments summary span { color: var(--pw-muted); font-size: var(--text-caption); } +.analysis-judgments summary span { font-size: var(--text-body); } .analysis-judgments details > p, .analysis-citations { margin: 4px 0 12px; } .analysis-question { color: var(--pw-muted); font-size: var(--text-caption); } .analysis-citations { display: flex; flex-wrap: wrap; gap: 8px; } .analysis-citations button { font-size: var(--text-caption); } .analysis-source { border: 1px solid var(--pw-line); border-radius: 8px; padding: 16px; margin-block: 16px; } .analysis-source p { font-size: var(--text-caption); color: var(--pw-muted); } -.analysis-source pre { font-family: var(--font-mono); font-size: var(--text-caption); line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 360px; overflow: auto; } -.analysis-method { border-block-start: 1px solid var(--pw-line); margin-top: 16px; padding-top: 12px; } +.analysis-source pre { font-family: var(--font-mono); font-size: var(--text-body); line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 360px; overflow: auto; } +.analysis-method { border-block-start: 1px solid var(--pw-line); margin-top: 16px; padding-top: 12px; color: var(--pw-muted); font-size: var(--text-lead); } .analysis-method summary { min-height: 44px; display: flex; align-items: center; cursor: pointer; color: var(--pw-text); } .analysis-method dl { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 8px 16px; } .analysis-method dd { margin: 0; color: var(--pw-text); } @@ -58,7 +69,15 @@ .analysis-spin { animation: analysis-request-spin 1s linear infinite; } @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; } } +@media (max-width: 1024px) { + .analysis-episode-columns { display: none; } + .analysis-episode > summary { grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; } + .analysis-episode-name, .analysis-episode-verdict, .analysis-episode-exceptions { grid-column: 1; } + .analysis-verdict-label { display: inline; } + .analysis-episode > summary > svg { grid-column: 2; grid-row: 1; } + .analysis-episode-exceptions:empty { display: none; } +} +@media (max-width: 640px) { .research-analysis { padding: 16px; } .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)} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx index 4740fd4c2e..141c0a4498 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-analysis.tsx @@ -1,7 +1,7 @@ import {useCallback, useEffect, useRef, useState} from "react"; import {ArrowUpRight, ChevronDown, LoaderCircle, RefreshCw, Sparkles, X} from "lucide-react"; -import type {AssessmentCitation, TrajectoryEpisode, TrajectoryOutputLanguage, TrajectoryContinuation} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; -import {ResearchProgress, episodeQuestion, metricCopy, verdictLabel, type ResearchProgressPack} from "./goal-research-progress"; +import type {AssessmentCitation, TrajectoryAssessment, TrajectoryEpisode, TrajectoryOutputLanguage, TrajectoryContinuation} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; +import {ResearchProgress, episodeQuestionGroups, metricCopy, verdictLabel, type ResearchProgressPack} from "./goal-research-progress"; import {requestJson} from "../../data/chat"; import {sourceReadApiUrl} from "../../data/local-status-query"; import {ContextHelp} from "./context-help"; @@ -293,6 +293,63 @@ export function ResearchCitationPanel({analysis: a}: {analysis: Controller}) { ; } +function AnalysisEpisodes({pack, result, zh, onCitation}: {pack: Pack; result: TrajectoryAssessment; zh: boolean; onCitation: Controller["readCitation"]}) { + const [active, setActive] = useState(null); + const groups = episodeQuestionGroups(result, (pack.episodes ?? []).map(episode => ({episode_id: episode.id, episode})), pack.episodes); + const citationButtons = (citations: AssessmentCitation[]) =>
{citations.map(c => )}
; + return
+
{zh ? "研究片段" : "Episode"}{metricCopy.benefit_test[zh ? 0 : 1]}{metricCopy.evidence_adoption[zh ? 0 : 1]}{zh ? "需留意" : "Exceptions"}
+ {groups.map(group =>
+ {group.shared ?

{group.before}{zh ? "〔研究片段〕" : "[episode]"}{group.after}

: null} + {group.items.map(({record: {episode}, question}) => { + const rows = result.assessments.filter(row => row.episode_id === episode.id); + const primary = (["benefit_test", "evidence_adoption"] as const).map(metric => ({metric, + label: metricCopy[metric][zh ? 0 : 1], + verdict: rows.find(row => row.metric === metric)?.verdict, + eligible: episode.eligible_metrics.includes(metric)})); + const primaryText = primary.map(row => row.verdict ? verdictLabel(row.metric, row.verdict, zh) + : row.eligible ? (zh ? "未记录" : "Not recorded") : (zh ? "不适用" : "Ineligible")); + const unknown = rows.filter(row => row.verdict === "unknown").length; + const exceptions = rows.filter(row => row.metric !== "benefit_test" && row.metric !== "evidence_adoption" + && (row.metric === "redundancy" ? row.verdict === "yes" : row.verdict === "no")); + const unknownText = unknown ? (zh ? `证据不足 ${unknown} 项` : `Unknown ${unknown}`) : ""; + const exceptionText = exceptions.map(row => row.metric === "redundancy" ? verdictLabel(row.metric, row.verdict, zh) + : `${metricCopy[row.metric][zh ? 0 : 1]} · ${verdictLabel(row.metric, row.verdict, zh)}`); + // A provider may give several judgments the same verbatim explanation. + // Share it only with the identical ordered citations, retaining every + // metric and verdict. Different evidence remains a separate reading. + const evidenceGroups: Array = []; + for (const row of rows) { + const same = evidenceGroups.find(group => group[0]!.reason === row.reason + && JSON.stringify(group[0]!.citations) === JSON.stringify(row.citations)); + if (same) same.push(row); else evidenceGroups.push([row]); + } + const isPrimary = (group: typeof rows) => group.some(row => row.metric === "benefit_test" || row.metric === "evidence_adoption"); + const judgments = (group: typeof rows) =>
{group.map(row => {metricCopy[row.metric][zh ? 0 : 1]}{verdictLabel(row.metric, row.verdict, zh)})}
; + return
+ {event.preventDefault(); setActive(previous => previous === episode.id ? null : episode.id);}} + aria-label={[question, ...primary.map((row, i) => `${row.label} · ${primaryText[i]}`), unknownText, ...exceptionText].filter(Boolean).join(" · ")}> + {group.shared ? episode.id : question} + {primary.map((row, i) => {row.label} · {primaryText[i]})} + {unknown ? {unknownText} : null}{exceptionText.map(text => {text})} + + +
+
{evidenceGroups.filter(isPrimary).map(group =>
{judgments(group)}

{group[0]!.reason}

{citationButtons(group[0]!.citations)}
)}
+
{evidenceGroups.filter(group => !isPrimary(group)).map(group =>
+ {judgments(group)}

{group[0]!.reason}

{citationButtons(group[0]!.citations)} +
)}
+ {group.shared || episode.question !== question ?
{group.shared ? (zh ? "完整问题" : "Full question") : (zh ? "原始研究问题" : "Original research question")} + {group.shared ?

{question}

: null} + {episode.question !== question ? <>{group.shared ?
{zh ? "原文" : "Original"}
: null}

{episode.question}

: null} +
: null} +
+
; + })} +
)} +
; +} + export function ResearchAnalysisPanel({analysis: a}: {analysis: Controller}) { if (!a.location.open) return null; const zh = a.zh, p = a.pack, result = p?.result; @@ -304,7 +361,6 @@ export function ResearchAnalysisPanel({analysis: a}: {analysis: Controller}) { : benefit?.unknown ? (zh ? "原目标收益的检验情况仍未知" : "Original-objective utility testing remains unknown") : benefit?.yes ? (zh ? "模型记录了收益检验;这不等于收益为正" : "The model identified a utility test; this does not imply positive utility") : (zh ? "此方法未覆盖原目标收益检验" : "This method does not cover original-objective utility testing"); - const citationButtons = (citations: AssessmentCitation[]) =>
{citations.map(c => )}
; return
{if (event.key === "Escape") {event.stopPropagation(); a.close();}}}>

{zh ? "轨迹深度分析" : "Trajectory analysis"}

{p?.title ?? (zh ? "只分析已授权的固定轨迹,不改变研究历史。" : "Analyze an authorized fixed trajectory without changing its history.")}

{result ? : null}
{a.error ?
{zh ? "分析请求未完成,已保存结果保留。" : "The analysis request did not complete; saved results are preserved."}

{a.error}

: null} @@ -314,6 +370,7 @@ export function ResearchAnalysisPanel({analysis: a}: {analysis: Controller}) {
{p.episode_count} {zh ? "个研究片段" : "research episodes"} · {p.selection.exhaustive ? (zh ? "固定范围" : "Fixed scope") : (zh ? "定向抽样" : "Purposive sample")}

{p.selection.exhaustive ? (zh ? "覆盖此固定分析范围;不等于覆盖全部研究。" : "Covers this fixed analysis scope, not necessarily the whole research history.") : (zh ? "定向抽样,仅分析选定研究片段;范围外或未捕获的工作未纳入,不代表全程。" : "Purposive sample of selected episodes; work outside this scope or not captured is excluded. It does not represent the whole run.")}

{zh ? "正向诊断、证据采用与最终任务收益分别判断。没有综合分数,也不据此推断因果或整体研究质量。引用存在不代表模型解释已被独立核验。" : "Diagnostic value, evidence adoption and task utility are separate. There is no total score, causal claim or whole-run quality rating. Citations do not independently verify model interpretations."}

+ {result ? Object.values(metricCopy).map(label =>

{label[zh ? 0 : 1]} · {label[zh ? 2 : 3]}

) : null}

{zh ? "下方仍显示当前研究 Goal 的已记录历史。" : "The current research Goal’s recorded history remains below."}

@@ -332,21 +389,7 @@ export function ResearchAnalysisPanel({analysis: a}: {analysis: Controller}) { {failed && !a.pending ?

{p.job?.status === "cancelled" ? (zh ? "分析已取消,研究历史保持可读。" : "Analysis cancelled. Research history remains readable.") : (zh ? "上次分析未完成;可重试,原失败记录会保留。" : "The last analysis did not complete. Retry preserves its failure record.")}

: null} {result ? <>
{zh ? "Agent 判断 · 未校准" : "Agent interpretation · uncalibrated"}{p.applicability === "historical" ? (zh ? " · 历史结果" : " · Historical result") : ""}

{headline}

- {p.episodes?.map(episode =>
- {episodeQuestion(result, episode.id, episode)}{(["benefit_test", "evidence_adoption"] as const).map(metric => { - const row = result.assessments.find(item => item.episode_id === episode.id && item.metric === metric); - return row ? {metricCopy[metric][zh ? 0 : 1]} · {verdictLabel(metric, row.verdict, zh)} : null; - })} -
-
{zh ? "原始研究问题" : "Original research question"}{episode.id}

{episode.question}

-
{(["benefit_test", "evidence_adoption"] as const).map(metric => { - const row = result.assessments.find(item => item.episode_id === episode.id && item.metric === metric); - return row ?
{metric === "benefit_test" ? (zh ? "原目标收益的检验与缺口" : "Original-objective utility test and its gaps") : (zh ? "证据如何影响后续行动" : "How evidence informed subsequent action")}

{row.reason}

{citationButtons(row.citations)}
: null; - })}
-
{result.assessments.filter(row => row.episode_id === episode.id && row.metric !== "benefit_test" && row.metric !== "evidence_adoption").map(row => { - const label = metricCopy[row.metric]; - return
{label?.[zh ? 0 : 1] ?? row.metric}{verdictLabel(row.metric, row.verdict, zh)}

{label?.[zh ? 2 : 3]}

{row.reason}

{citationButtons(row.citations)}
; - })}
)} +
{zh ? "来源、方法、覆盖与分析成本" : "Sources, method, coverage and analysis cost"}

{p.objective}

{zh ? "来源范围" : "Source scope"}
{p.scope.case_id} · {p.scope.run_id}
{p.scope.window}
{zh ? "方法" : "Method"}
{result.method_version} · {zh ? "模型判断未校准" : "Uncalibrated model judgment"}
{zh ? "模型设置" : "Requested model"}
{p.config.model} / {p.config.effort} / {p.config.service_tier} · {zh ? "实际服务档位" : "Served tier"}: {resultJob?.served_service_tier ?? (zh ? "未知" : "unknown")}
{zh ? "分析用量" : "Analysis usage"}
{resultJob?.reused_from_analysis_key ? (zh ? "复用上一版判读,无新增模型调用;原用量保留在上一版。" : "Reused previous judgments without a new model call; original usage stays with the previous version.") : <>{resultJob?.analysis_wall_seconds == null ? "—" : `${resultJob.analysis_wall_seconds.toFixed(1)}s`} · {zh ? "输入" : "input"} {resultJob?.usage?.input_tokens?.toLocaleString() ?? "—"} / {zh ? "输出" : "output"} {resultJob?.usage?.output_tokens?.toLocaleString() ?? "—"} tokens · {zh ? "金额未采集,推理 token 已包含于输出" : "Dollar cost unmeasured; reasoning tokens are included in output"}}
{zh ? "覆盖" : "Coverage"}
{p.episode_count} {zh ? "个固定片段" : "fixed episodes"} · {p.source_count} {zh ? "份来源" : "sources"} · {zh ? "总体片段数" : "Population"}: {p.selection.population_episodes ?? (zh ? "未知" : "unknown")}
{zh ? "取样规则" : "Selection"}
{p.selection.rule}
{zh ? "版本" : "Revision"}
{p.scope.revision}
    {result.limitations.map((text, i) =>
  • {text}
  • )}

{zh ? "引用行存在不等于解释正确;本页不提供独立科学校准结论。" : "A valid citation range does not establish entailment or independent scientific calibration."}

: null} : null} diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css index e47549768d..630e9b9b1f 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.css @@ -1,9 +1,11 @@ .research-history { margin-block: 0; padding-block: 8px 24px; min-width: 0; } -.research-history-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } +.research-history-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; } .research-history-heading h2 { font-size: var(--text-heading); font-weight: 600; margin: 0 0 4px; } .research-history-heading > div:first-child { min-width: 0; } .research-history-title { display: flex; align-items: center; gap: 4px; } .research-history-title h2 { margin: 0; } +.research-history-meta { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0 16px; } +.research-history-meta .research-display-language { margin: 0; } .research-history-heading p, .research-history-coverage, .research-history-note { color: var(--pw-muted); font-size: var(--text-caption); line-height: 1.6; margin: 4px 0; } .research-history-controls { --research-control-height: 36px; display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: start; max-width: 100%; min-width: 0; gap: 4px 8px; } .research-history-controls button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: var(--research-control-height); padding: 6px 10px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-bg); font: inherit; font-size: var(--text-caption); } @@ -98,21 +100,21 @@ .research-timeline { display: grid; gap: 12px; min-width: 0; } .research-timeline-guide { display: flex; align-items: center; gap: 4px; color: var(--pw-muted); font-size: var(--text-caption); } .research-timeline-guide > .research-link { margin-inline-start: auto; } -.research-phases { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; } -.research-history .research-phase { display: flex; flex-direction: column; align-items: stretch; gap: 8px; min-width: 0; padding: 14px; border: 1px solid var(--pw-line); border-radius: 10px; background: var(--pw-bg); color: var(--pw-text); font: inherit; text-align: left; cursor: pointer; } -.research-history .research-phase:hover { border-color: var(--pw-line-strong); } +.research-phases { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } +.research-history .research-phase { display: flex; flex-direction: column; align-items: stretch; gap: 6px; min-width: 0; min-height: 44px; padding: 8px; border: 0; border-radius: 4px; background: transparent; color: var(--pw-text); font: inherit; text-align: left; cursor: pointer; } +.research-history .research-phase:hover { background: var(--pw-hover); } .research-history .research-phase.is-active { outline: 2px solid var(--pw-text); outline-offset: -1px; } -.research-phase-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; font-size: var(--text-body); } -.research-phase-head time { color: var(--pw-muted); font-size: var(--text-caption); white-space: nowrap; } -.research-phase-counts { display: flex; align-items: baseline; gap: 4px; font-size: var(--text-caption); color: var(--pw-muted); } -.research-phase-counts b { font-size: var(--text-heading); line-height: var(--leading-heading); color: var(--pw-text); font-weight: 600; } -.research-phase-counts b + b, .research-phase-counts b:not(:first-child) { margin-left: 12px; } +.research-phase-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0 8px; font-size: var(--text-body); } +.research-phase-head > span { color: var(--pw-muted); font-size: var(--text-caption); } +.research-phase-details { margin: 12px 0 0; } +.research-phase-details > div + div { margin-block-start: 12px; } +.research-phase-details dt { font-weight: 500; } +.research-phase-details dd { margin: 4px 0 0; } .research-phase-bar { display: flex; gap: 2px; height: 18px; border-radius: 4px; overflow: hidden; background: var(--pw-track); } .research-phase-bar span { display: flex; align-items: center; min-width: 4px; padding-inline: 6px; overflow: hidden; color: var(--pw-bg); font-size: var(--text-caption); font-weight: 600; white-space: nowrap; } .research-phase-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; font-size: var(--text-caption); color: var(--pw-text); } .research-phase-legend > span { display: inline-flex; align-items: center; gap: 4px; } .research-phase-legend i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; background: var(--research-tone); } -.research-phase-decisions { font-size: var(--text-caption); color: var(--pw-text); } .research-trail-list em, .research-finding-sections dt { font-style: normal; font-weight: 600; color: var(--pw-text); margin-right: 6px; } .is-confirmed { --research-tone: var(--pw-green); } .is-refuted { --research-tone: var(--pw-red); } @@ -191,7 +193,7 @@ .research-tick-key.is-stop, .research-tick-key.is-wait, .research-tick-key.is-ask_human { background: var(--pw-amber); } .research-decision-ticks .is-no_change, .research-tick-key.is-no_change { background: var(--pw-muted); } @media (max-width: 640px) { .research-unlinked-days { grid-template-columns: repeat(2, minmax(0, 1fr)); } .research-route-caption { grid-template-columns: minmax(0, 1fr); } .research-finding-sections { grid-template-columns: minmax(0, 1fr); gap: 4px; } .research-finding-sections dd { margin-bottom: 12px; } } -@media (max-width: 900px) { .research-phases { grid-template-columns: repeat(3, minmax(260px, 1fr)); overflow-x: auto; padding: 2px; scroll-snap-type: x proximity; } .research-phase { scroll-snap-align: start; } .research-trail-list { grid-template-columns: 1fr; } .research-timeline-grid { grid-template-columns: minmax(0, 1fr); } .research-row-label { flex-direction: row; flex-wrap: wrap; gap: 8px; } .research-row-plot { margin-bottom: 8px; } } +@media (max-width: 900px) { .research-trail-list { grid-template-columns: 1fr; } .research-timeline-grid { grid-template-columns: minmax(0, 1fr); } .research-row-label { flex-direction: row; flex-wrap: wrap; gap: 8px; } .research-row-plot { margin-bottom: 8px; } } @media (prefers-reduced-motion: reduce) { .research-history .research-timeline-marker:hover::after { transform: translate(-50%, -50%); } } /* A selected question is a reading surface; the whole history stays reachable. */ diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx index 899a5702e0..0809800fb1 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-history.tsx @@ -295,6 +295,10 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re const readingSelection = Boolean(selected || selectedMembers?.length); const selectedFinding = selected?.findings.find(finding => finding.result_id === selection.finding) ?? (!selection.finding ? selected?.findings.at(-1) : undefined); + const selectedEdges = selected ? value?.edges.filter(edge => edge.from_node === selected.id || edge.to_node === selected.id) ?? [] : []; + const selectedContextCitations = [...new Map((originalDisplay ? [] : currentDisplay?.fields ?? []) + .filter(field => field.event_id === selected?.latest.event_id || field.event_id === selectedFinding?.event_id) + .flatMap(field => field.context_citations ?? []).map(citation => [JSON.stringify(citation), citation])).values()]; const findingExceptions = selected ? [...new Set(selected.findings.map(finding => finding.status))] .filter(status => status !== "confirmed").map(status => `${stateLabel(status)} ${selected.findings.filter(finding => finding.status === status).length}`).join(" · ") : ""; // Pin the first displayed finding to its stable identity. New source records @@ -365,9 +369,7 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re {lens === "graph" ?

{zh ? "按来源分组,实线为已记录关联,虚线仅表示分组;排列不代表时间或进展。未关联问题按首次记录 UTC 日期收纳。" : "Grouped by source. Solid lines are recorded relationships; dotted lines only group nodes. Position does not imply time or progress. Unlinked questions are collected by first-record UTC date."}

: null}

{zh ? "合拢仅为避免遮挡,不表示关联或因果;选择一次可读全部原始记录与依据。" : "Nearby points group to avoid overlap, without implying a relationship or causality. Select once to read every original record and source."}

-

{copy.time}{value?.startAt ? <> · — : null} · {busy ? `${copy.reading} ${loaded}${read.total ? ` / ${read.total}` : ""}` - : read.total === null ? (zh ? "来源尚未读到" : "Source has not been read") - : `${read.events.length} / ${read.total} ${copy.records}${read.complete ? "" : ` · ${copy.partial}`}`}

+
+
- {!readingSelection ? displayControl : null} +
+

{copy.time}{value?.startAt ? <> · — : null} · {busy ? `${copy.reading} ${loaded}${read.total ? ` / ${read.total}` : ""}` + : read.total === null ? (zh ? "来源尚未读到" : "Source has not been read") + : `${read.events.length} / ${read.total} ${copy.records}${read.complete ? "" : ` · ${copy.partial}`}`}

+ {!readingSelection ? displayControl : null} +
{error || history.error ?

{copy.failure} {error || history.error}

: null} {!busy && !error && read.total === 0 ?

{zh ? "这个 Goal 尚无已记录的研究历程。" : "This Goal has no recorded research history yet."}

: null} @@ -423,10 +433,15 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0, re {selectedFinding ? findingElement(selectedFinding, selected.latest.title) :

{zh ? "此记录中找不到所选结论;可从列表选择其他结论。" : "The selected finding is absent from this snapshot. Other findings remain available."}

} : <>{selected.latest.summary ?

{selected.latest.summary}

: null}

{zh ? "此节点没有附属结论记录。" : "No finding is attached to this node."}

}
{zh ? "来源与历史" : "Sources and history"} + {selectedContextCitations.length ? <>

{zh ? "译文依据" : "Translation context"}

+
{selectedContextCitations.map(citation => )}
: null} {selected.findings.length && selected.latest.summary ?

{selected.latest.summary}

: null} {measurementSummary(selected.id)}

{copy.versions}

{selected.latest.evidence_refs.length ? selected.latest.evidence_refs.map(ref => {ref}) :

{zh ? "产物版本与来源引用未记录。" : "Artifact version and source references were not recorded."}

} -

{copy.links}

    {value.edges.filter(edge => edge.from_node === selected.id || edge.to_node === selected.id).map(edge =>
  • {positions.get(edge.from_node)?.node.latest.title ?? (zh ? "来源节点未记录" : "Source node not recorded")} → {relationLabel(edge.edge_type)} → {positions.get(edge.to_node)?.node.latest.title ?? (zh ? "目标节点未记录" : "Target node not recorded")}{edge.summary ? ` — ${edge.summary}` : ""}
  • )}
+ {selectedEdges.length ? <>

{copy.links}

    {selectedEdges.map(edge =>
  • {positions.get(edge.from_node)?.node.latest.title ?? (zh ? "来源节点未记录" : "Source node not recorded")} → {relationLabel(edge.edge_type)} → {positions.get(edge.to_node)?.node.latest.title ?? (zh ? "目标节点未记录" : "Target node not recorded")}{edge.summary ? ` — ${edge.summary}` : ""}
  • )}
: null} {selected.latest.parent_id ?

{copy.group} · {positions.get(selected.latest.parent_id)?.node.latest.title ?? "—"}

: null}

{copy.updates}

{updateRecords(selected.revisions)}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css index fc359252b4..d1a782b36e 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.css @@ -1,4 +1,4 @@ -.research-progress { margin-block: 16px 0; padding-block: 8px 0; color: var(--pw-text); min-width: 0; overflow-wrap: anywhere; } +.research-progress { container-type: inline-size; margin-block: 16px 0; padding-block: 8px 0; color: var(--pw-text); min-width: 0; overflow-wrap: anywhere; } .research-progress > header { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } .research-progress h3 { margin: 0; font-size: var(--text-lead); font-weight: 600; } .research-progress-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; } @@ -42,17 +42,21 @@ .research-effect-detail-heading > button { display: grid; place-items: center; width: 44px; height: 44px; background: transparent; border: 0; border-radius: 4px; color: var(--pw-text); cursor: pointer; } .research-effect-detail-heading > button:hover { background: var(--pw-hover); } .research-effect-detail-heading > button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } -.research-effect-help { display: flex; align-items: center; gap: 4px; padding-block: 4px 12px; color: var(--pw-muted); font-size: var(--text-caption); } .research-assessments { border-block-start: 1px solid var(--pw-line); } +.research-assessment-group + .research-assessment-group { margin-block-start: 20px; } +.research-progress .research-assessment-shared-question { margin: 0; padding-block: 16px 8px; max-width: 80ch; color: var(--pw-muted); font-size: var(--text-body); } +.research-assessment-shared-question span { color: var(--pw-muted); } .research-assessment { border-block-end: 1px solid var(--pw-line); } -.research-assessment > summary { display: grid; grid-template-columns: minmax(0, 1fr) auto 14px; align-items: center; gap: 16px; padding-block: 14px; list-style: none; font-size: var(--text-body); line-height: var(--leading-body); } +.research-assessment > summary { display: grid; grid-template-columns: minmax(0, 1fr) auto 14px; align-items: center; gap: 16px; padding-block: 14px; list-style: none; font-size: var(--text-lead); line-height: var(--leading-body); } .research-assessment > summary::-webkit-details-marker { display: none; } .research-assessment-name { min-width: 0; font-weight: 500; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } .research-assessment[open] > summary .research-assessment-name { display: block; } -.research-assessment-verdict { color: var(--pw-muted); } +.research-assessment-verdict { color: var(--pw-muted); font-size: var(--text-body); } .research-assessment[open] > summary > svg { transform: rotate(180deg); } .research-assessment-body { max-width: 80ch; padding-block: 0 16px; } -.research-progress .research-assessment-body p { margin-block: 0 12px; font-size: var(--text-body); color: var(--pw-text); } +.research-progress .research-assessment-body p { margin-block: 0 12px; font-size: var(--text-lead); color: var(--pw-text); } +.research-assessment-original > summary { color: var(--pw-muted); font-size: var(--text-body); } +.research-assessment-original h5 { margin-block: 16px 8px; color: var(--pw-muted); font-size: var(--text-body); font-weight: 500; } .research-assessment > summary:focus-visible, .research-effect-citations button:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } .research-progress-basis { border-block-start: 1px solid var(--pw-line); margin-block-start: 12px; } .research-progress-basis > summary { font-size: var(--text-body); } @@ -60,7 +64,7 @@ .research-usage h4 { font-size: var(--text-body); font-weight: 500; margin: 0; } .research-effect-evidence h4 { font-size: var(--text-caption); line-height: var(--leading-prose); margin: 8px 0; } .research-effect-citations { display: flex; flex-wrap: wrap; gap: 4px 20px; } -.research-progress .research-effect-citations button { display: inline-flex; align-items: center; gap: 4px; min-height: 44px; font: inherit; font-size: var(--text-body); background: transparent; border: 0; border-radius: 0; padding: 4px 0; color: var(--pw-text); cursor: pointer; } +.research-progress .research-effect-citations button { display: inline-flex; align-items: center; gap: 4px; min-height: 44px; font: inherit; font-size: var(--text-body); background: transparent; border: 0; border-radius: 0; padding: 4px 0; color: var(--pw-muted); cursor: pointer; } .research-effect-citations button:hover { text-decoration: underline; } .research-native-results { border-block-start: 1px solid var(--pw-line); margin-block-start: 16px; padding-block-start: 16px; } .research-native-results h4 { margin: 0; font-size: var(--text-caption); font-weight: 600; } @@ -92,6 +96,9 @@ .research-usage-tail { border-inline-start: 2px solid var(--pw-caution); padding-inline-start: 16px; margin-block: 20px; } .research-usage-tail strong { font-weight: 500; color: var(--pw-text); } .research-request-coverage { border-block-start: 1px solid var(--pw-line); padding-block-start: 16px; margin-block: 20px; } +.research-input-share-value { display: flex; align-items: baseline; flex-wrap: wrap; gap: 12px; } +.research-input-share-value strong { font-size: var(--text-title); font-variant-numeric: tabular-nums; } +.research-input-share-value > span { color: var(--pw-muted); } .research-request-values { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 24px; margin-block: 12px; } .research-request-values > div { min-width: 0; } .research-request-values dt { font-size: var(--text-body); color: var(--pw-muted); } @@ -103,12 +110,17 @@ .research-source-detail { border-block-start: 1px solid var(--pw-line); margin-block-start: 16px; } .research-progress-basis summary:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: 2px; } .research-progress-warning { border-inline-start: 2px solid var(--pw-caution); padding-inline-start: 12px; } -@media (max-width: 1200px) { .research-effect-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@container (max-width: 840px) { .research-effect-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@container (max-width: 440px) { + .research-effect-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .research-effect-metrics:lang(zh) { grid-template-columns: repeat(3, minmax(0, 1fr)); } +} +@container (max-width: 320px) { .research-effect-metrics:lang(zh) { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 640px) { .research-progress > header { flex-direction: column; gap: 8px; } .research-progress label { flex-direction: column; align-items: stretch; width: 100%; } .research-progress select { max-width: 100%; } - .research-effect-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 16px; } + .research-effect-metrics { gap: 8px; } .research-assessment > summary { gap: 8px; } .research-native-values { grid-template-columns: repeat(2, minmax(0, 1fr)); } .research-progress-values { grid-template-columns: 1fr; gap: 16px; } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx index 19756f8b1e..c1e6e2e110 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-progress.tsx @@ -29,11 +29,69 @@ export function verdictLabel(metric: TrajectoryMetric, verdict: AssessmentVerdic return verdict === "yes" ? (zh ? "有依据" : "Supported") : (zh ? "未建立" : "Not established"); } +function LoopXInputShare({usage, zh}: {usage: TrajectorySolverUsage | undefined; zh: boolean}) { + const attribution = usage?.loopx_input; + const percent = (n: number) => new Intl.NumberFormat(zh ? "zh-CN" : "en", {style: "percent", maximumFractionDigits: 1}).format(n); + const measured = attribution && attribution.status !== "unavailable" ? attribution : null; + const estimated = attribution?.method === "tiktoken_o200k_base_input_estimate_v1"; + return
+

{estimated ? (zh ? "LoopX Turn packet · 估计" : "LoopX Turn packet · estimate") : (zh ? "LoopX 输入占比" : "LoopX input share")}

+

{zh ? "同一捕获窗口内,LoopX packet 进入各次模型请求的 token 总量 ÷ 全部请求输入 token 总量,包含缓存输入。重复消费按各次请求计算,同一响应的重复回执只计一次。" : "LoopX packet tokens processed across requests divided by all request input tokens in the same capture, including cached input. Reprocessed context counts in each request; duplicate receipts for one response count once."}

+

{estimated ? (zh ? "分子仅计宿主明确记录的 Turn packet 文本,使用 o200k_base 分词估计;分母是同请求的原生输入计数。尚未核验原生模型分词器,不含其他 LoopX 命令输出、封装或压缩摘要,缓存归属未知;这是已记录部分的估计,不是 provider 实测或净效率损失。" : "Only explicitly recorded Turn packet text is estimated with o200k_base; the denominator is native input for the same requests. The provider model's tokenizer is unverified. Other LoopX tool output, framing and compacted summaries are excluded; cache allocation is unknown. This estimates the recorded component, not provider-measured attribution or net efficiency loss.") : (zh ? "需要宿主提供逐请求 token 归因;包字节、字符数和命令次数不能替代。缺失归因保持未知;上下文压缩也不自动全部归为 LoopX。这个比例衡量结构开销,不能单独证明净效率损失或因果影响。" : "Requires host-supplied per-request token attribution, not packet bytes, characters or command counts. Missing attribution stays unknown; compaction is not automatically all attributed to LoopX. This measures input overhead, not net efficiency loss or causal impact.")}

+
+

{measured?.input_fraction == null ? "—" : percent(measured.input_fraction)} + {measured ? {measured.attributed_requests} / {measured.total_requests} {estimated ? (zh ? "次请求已匹配" : "requests matched") : (zh ? "次请求已归因" : "requests attributed")} : null} +

+ {!attribution ?

{zh ? "未采集 packet 的 token 归因" : "Packet token attribution was not captured"}

+ : attribution.status === "unavailable" ?

{estimated ? (zh ? "输入估计暂不可用,占比未知。" : "Input estimate unavailable; share is unknown.") : (zh ? "归因记录未通过核对,占比未知。" : "Attribution records could not be reconciled; share is unknown.")}

+ : attribution.status === "partial" ?

{estimated ? (zh ? "部分请求已匹配 · 全窗口占比未知" : "Some requests matched · whole-capture share unknown") : (zh ? "部分归因 · 全窗口占比未知" : "Partial attribution · whole-capture share unknown")}

+ : attribution.total_input_tokens === 0 ?

{zh ? "捕获输入为零,比例不适用。" : "Captured input is zero; the ratio is undefined."}

: null} + {attribution ?
{estimated ? (zh ? "估计明细" : "Estimate details") : (zh ? "归因明细" : "Attribution details")} + {measured ?
+
{estimated ? (zh ? "已记录 packet 文本估计" : "Recorded packet text estimate") : (zh ? "已归因 LoopX 输入" : "Attributed LoopX input")}
{measured.loopx_input_tokens.toLocaleString()} tokens
+
{zh ? "全部请求输入" : "All request input"}
{measured.total_input_tokens.toLocaleString()} tokens
+
{zh ? "已覆盖请求输入" : "Input in attributed requests"}
{measured.attributed_input_tokens.toLocaleString()} tokens
+
{zh ? "LoopX 输入中的缓存" : "Cached subset of LoopX input"}
{measured.loopx_cached_input_tokens?.toLocaleString() ?? "—"} tokens
+
{zh ? "占未缓存输入" : "Share of uncached input"}
{measured.uncached_input_fraction == null ? "—" : percent(measured.uncached_input_fraction)}
+ {measured.status === "partial" && measured.input_fraction_bounds ? <>
{zh ? "缺失归因的可能范围" : "Possible range with missing attribution"}
{measured.input_fraction_bounds.map(percent).join(" – ")}

{zh ? "假定未归因请求全非 LoopX / 全为 LoopX;不是置信区间。" : "Unattributed requests treated as none / all LoopX; not a confidence interval."}

: null} +
:

{attribution.status === "unavailable" ? attribution.reason : null}

} + {measured?.estimate_basis ?

{measured.estimate_basis.tokenizer} {measured.estimate_basis.version} · {measured.estimate_basis.encoding}
{measured.estimate_basis.models.join(", ")}

: null} +

{attribution.method}

+
: null} +
; +} + // The assessment owner supplies localized questions. Both readers use that // copy without inventing short titles or translating the original evidence. export const episodeQuestion = (result: TrajectoryAssessment, id: string, episode?: TrajectoryEpisode) => result.episode_questions?.find(question => question.episode_id === id)?.question ?? episode?.question ?? id; +// Factor only an exact, single occurrence of the declared episode identity. +// This is lossless presentation, not a short-title generator or prose classifier. +export function episodeQuestionGroups(result: TrajectoryAssessment, records: T[], episodes: TrajectoryEpisode[] | undefined) { + const groups: Array<{before: string; after: string; exact: boolean; items: Array<{record: T; question: string}>}> = []; + for (const record of records) { + const id = record.episode_id; + const question = episodeQuestion(result, id, episodes?.find(episode => episode.id === id)); + const index = question.indexOf(id); + const exact = index >= 0 && question.lastIndexOf(id) === index + && !/[\w./-]/u.test(question[index - 1] ?? "") && !/[\w./-]/u.test(question[index + id.length] ?? ""); + const before = exact ? question.slice(0, index) : question, after = exact ? question.slice(index + id.length) : ""; + const prior = groups.at(-1); + if (exact && prior?.exact && prior.before === before && prior.after === after) prior.items.push({record, question}); + else groups.push({before, after, exact, items: [{record, question}]}); + } + // Factor repeated prose only when it outweighs the identities. This also + // serves a pair of long questions; preserve source order and every verdict. + return groups.flatMap(group => { + const shared = group.before.length + group.after.length; + const identities = group.items.reduce((sum, item) => sum + item.record.episode_id.length, 0); + return group.items.length >= 2 && shared * (group.items.length - 1) > identities + ? [{...group, shared: true}] + : group.items.map(item => ({before: "", after: "", items: [item], shared: false})); + }); +} + // Format the native owner's existing projection, without deriving a gain, // deciding adoption, or borrowing values from another pack. function RetainedImprovement({native, zh, onCitation}: {native: NonNullable; zh: boolean; onCitation: (citation: AssessmentCitation, origin?: HTMLButtonElement) => void}) { @@ -126,28 +184,32 @@ function ResearchMetrics({pack: p, zh, onCitation}: {pack: ResearchProgressPack {active ?
{if (event.key === "Escape") {event.stopPropagation(); close();}}}> -

{active.label[zh ? 0 : 1]}

{(() => { const {metric, label, row, assessments, excluded} = active; return <> -
{zh ? "逐项判读" : "Episode judgments"} +

{label[zh ? 0 : 1]}

{label[zh ? 2 : 3]}

{metric === "benefit_test" ?

{zh ? "“是”只表示做过收益检验,不保证收益为正;“否”不是收益为零。" : "Yes means utility was tested, not that utility improved. No is not zero utility."}

: null} {metric === "redundancy" ?

{zh ? "“是”表示发现无益重复,不能当作正向表现。" : "Yes means avoidable repetition was found; it is not a positive outcome."}

: null} {row?.assessed_fraction != null ?

{zh ? "已得到明确判读的覆盖" : "Coverage with a decisive judgment"} {percent(row.assessed_fraction)}{row.unknown && row.missing_data_bounds ? <> · {zh ? "未知分别全为否/全为是时,“是”占比" : "Yes share if unknowns are all no / all yes"} {percent(row.missing_data_bounds[0])}–{percent(row.missing_data_bounds[1])} ({zh ? "不是置信区间" : "not a confidence interval"}) : null}

: null} -
+
{excluded.length ?

{zh ? "未列入此指标的片段" : "Episodes excluded from this metric"}

    {excluded.map(episode =>
  • {episodeQuestion(result, episode.id, episode)}
  • )}
: null} -
{assessments.map(assessment => { +
{episodeQuestionGroups(result, assessments, p.episodes).map(group =>
+ {group.shared ?

{group.before}{zh ? "〔研究片段〕" : "[episode]"}{group.after}

: null} + {group.items.map(({record: assessment, question}) => { const episode = p.episodes?.find(item => item.id === assessment.episode_id); return
- {episodeQuestion(result, assessment.episode_id, episode)}{verdictLabel(metric, assessment.verdict, zh)} + {group.shared ? assessment.episode_id : question}{verdictLabel(metric, assessment.verdict, zh)}
-
{zh ? "原始研究问题" : "Original research question"}{assessment.episode_id}{episode ?

{episode.question}

: null}
-

{assessment.reason}

+

{assessment.reason}

{assessment.citations.map(c => )}
+ {group.shared || (episode && episode.question !== question) ?
{group.shared ? (zh ? "完整问题" : "Full question") : (zh ? "原始研究问题" : "Original research question")} + {group.shared ?

{question}

: null} + {episode && episode.question !== question ? <>{group.shared ?
{zh ? "原文" : "Original"}
: null}

{episode.question}

: null} +
: null}
; - })}
+ })}
)}
{!row?.eligible ?

{zh ? "所选片段没有此指标的适用判读;未记作零。" : "No eligible judgment for this metric in the selected episodes; not recorded as zero."}

: null} ; @@ -194,6 +256,7 @@ export function ResearchProgress({pack: p, packs, selectedId, zh, onSelect, onCi

{incomplete.usage ? <>{zh ? "其中末轮" : "Of which, unfinished Turn"} · {zh ? "输入" : "input"} {number(incomplete.usage.input_tokens)} · {zh ? "输出" : "output"} {number(incomplete.usage.output_tokens)} tokens : (zh ? "末轮尚无计数观测,不记零" : "No counter observation for the unfinished Turn; not zero")}

{zh ? "仅含已持久化用量,不能据此判定运行已结束。" : "Persisted usage only; this does not establish termination."}

: null} + {usage ?

{zh ? "请求覆盖" : "Request coverage"}

{zh ? "逐响应去重后,核对轮次与会话累计量、上下文压缩记录及会话计数。以下是独立核对口径,与上方会话计数不相加,也不改写 Goal 账本。" : "Deduplicated responses are reconciled against Turn and session totals, compaction records and session counters. This separate basis is not added to the session counters above and does not rewrite the Goal ledger."}

diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx index 38d872a0c4..2343a1982f 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx @@ -86,16 +86,24 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da const groupedIds = new Set(dayGroups.flatMap(group => group.nodes.map(node => node.id))); const swarm = timeline.singles.filter(id => !groupedIds.has(id)).map(id => timeline.nodes.get(id)!).filter(item => item.first !== null); const unknownTime = [...timeline.nodes.values()].filter(item => item.first === null).length; - const phaseCard = (item: TimelinePhase) => { + const phaseName = (item: TimelinePhase) => [copy.early, copy.middle, copy.late][item.index]; + const phaseDescription = (item: TimelinePhase) => { const counts = Object.entries(item.decisions); + return [ + `${timeLabel(item.start)} — ${timeLabel(item.end)}`, + `${item.nodes} ${copy.items} · ${item.findings} ${copy.findings}`, + LABELS.map(key => `${label(key)} ${item.labels[key] ?? 0}`).join(" · "), + timeline.decisions.length ? (counts.length ? `${copy.decisions} · ${counts.map(([key, value]) => `${label(key)} ${value}`).join(" · ")}` : (timeline.structuredCount ? copy.none : copy.unstructured)) : null, + ].filter(Boolean).join(" · "); + }; + const phaseCard = (item: TimelinePhase) => { return ; }; return
@@ -104,6 +112,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da {!timeline.structuredCount ?

{copy.unstructuredHint}

: null}

{copy.routeHint}{zh ? "。" : ". "}{zh ? "相邻点合拢,数字为研究项数,一次展开原始时间与依据。" : "Nearby points group; the number counts research items. Select once for original times and sources."}

{zh ? "未关联问题按首次记录 UTC 日期收纳,不推断路线或因果。决策轨迹保留各轮显式决策,最新记录在前。" : "Unlinked questions group by first-record UTC date, without inferring a route or cause. Decision history retains each explicit decision, newest record first."}

+
{timeline.phases.map(item =>
{phaseName(item)}
{phaseDescription(item)}
)}
{phase !== null ? : null}
{LABELS.map(key =>