diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-help.css b/apps/presentation/dashboard/src/features/personal-workspace/context-help.css new file mode 100644 index 0000000000..6a6cc6c4bd --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-help.css @@ -0,0 +1,8 @@ +.context-help { display: inline-flex; vertical-align: middle; flex: 0 0 auto; } +.context-help > .context-help-trigger { display: inline-flex; align-items: center; justify-content: center; width: 32px; min-width: 32px; height: 32px; min-height: 32px; padding: 0; border: 0; border-radius: 6px; background: transparent; color: var(--pw-muted); cursor: help; } +.context-help > .context-help-trigger:hover, .context-help > .context-help-trigger[aria-expanded="true"] { color: var(--pw-text); background: var(--pw-hover); } +.context-help > .context-help-trigger:focus-visible { outline: 2px solid var(--pw-blue); outline-offset: -2px; } +.context-help-popover { position: fixed; inset: auto; margin: 0; box-sizing: border-box; width: min(360px, calc(100vw - 24px)); max-height: min(440px, calc(100dvh - 24px)); overflow: auto; padding: 16px; border: 1px solid var(--pw-line); border-radius: 8px; background: var(--pw-bg); color: var(--pw-text); box-shadow: 0 4px 16px color-mix(in srgb, var(--pw-text) 12%, transparent); font-size: var(--text-caption); font-weight: 400; line-height: var(--leading-prose); text-align: start; overflow-wrap: anywhere; } +.context-help-popover > strong { font-size: inherit; font-weight: 600; } +.context-help-popover p { margin: 8px 0 0; color: var(--pw-muted); font-size: inherit; line-height: inherit; } +@media (pointer: coarse), (max-width: 640px) { .context-help > .context-help-trigger { width: 44px; min-width: 44px; height: 44px; min-height: 44px; } } diff --git a/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx b/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx new file mode 100644 index 0000000000..b0ab105b1f --- /dev/null +++ b/apps/presentation/dashboard/src/features/personal-workspace/context-help.tsx @@ -0,0 +1,63 @@ +import {useEffect, useId, useRef, useState, type ReactNode} from "react"; +import {Info} from "lucide-react"; +import "./context-help.css"; + +/** Optional, non-interactive explanation. Outcomes and action consequences stay + * in the caller; this disclosure never performs the caller's operation. */ +export function ContextHelp({label, children}: {label: string; children: ReactNode}) { + const id = useId(); + const trigger = useRef(null); + const panel = useRef(null); + const pinned = useRef(false); + const timer = useRef | undefined>(undefined); + const [open, setOpen] = useState(false); + const clearTimer = () => clearTimeout(timer.current); + const place = () => { + const button = trigger.current, bubble = panel.current; + if (!button || !bubble) return; + const anchor = button.getBoundingClientRect(), bounds = bubble.getBoundingClientRect(); + bubble.style.left = `${Math.max(12, Math.min(anchor.left, window.innerWidth - bounds.width - 12))}px`; + bubble.style.top = `${Math.max(12, anchor.bottom + bounds.height + 8 <= window.innerHeight + ? anchor.bottom + 8 : anchor.top - bounds.height - 8)}px`; + }; + const show = () => {clearTimer(); panel.current?.showPopover(); place();}; + const dismiss = () => {clearTimer(); pinned.current = false; panel.current?.hidePopover();}; + const leave = () => { + clearTimer(); + timer.current = setTimeout(() => { + if (!pinned.current && document.activeElement !== trigger.current) dismiss(); + }, 180); + }; + useEffect(() => () => clearTimeout(timer.current), []); + useEffect(() => { + if (!open) return; + const escape = (event: KeyboardEvent) => { + if (event.key === "Escape" && panel.current?.matches(":popover-open")) { + event.preventDefault(); event.stopPropagation(); dismiss(); + } + }; + window.addEventListener("keydown", escape, true); + window.addEventListener("resize", place); + window.addEventListener("scroll", place, true); + return () => { + window.removeEventListener("keydown", escape, true); + window.removeEventListener("resize", place); + window.removeEventListener("scroll", place, true); + }; + }, [open]); + return
+ + +
; +} 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 d48d7e811c..b296520650 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 @@ -1,6 +1,9 @@ .research-analysis { margin-block: 16px; padding: 24px; border: 1px solid var(--pw-line); border-radius: 12px; min-width: 0; color: var(--pw-text); background: var(--pw-bg); font-size: var(--text-body); line-height: var(--leading-prose); overflow-wrap: anywhere; } .research-analysis > header, .analysis-source > header { display: flex; align-items: start; justify-content: space-between; gap: 16px; } .analysis-header-actions { display: flex; gap: 8px; flex-shrink: 0; } +.research-analysis-entry { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; } +.research-analysis-entry-actions, .analysis-scope { display: flex; align-items: center; gap: 4px; } +.research-analysis-entry > small { font-size: var(--text-caption); color: var(--pw-muted); } .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; } 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 be931d94f0..2097e1187c 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 @@ -3,6 +3,7 @@ import {ArrowUpRight, LoaderCircle, RefreshCw, Sparkles, X} from "lucide-react"; import type {AssessmentCitation, TrajectoryEpisode} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; import {ResearchProgress, metricCopy, verdictLabel, type ResearchProgressPack} from "./goal-research-progress"; import {requestJson} from "../../data/chat"; +import {ContextHelp} from "./context-help"; import "./goal-research-analysis.css"; // Transport views of the existing toolkit producer, not an assessment, score, @@ -164,20 +165,22 @@ const reasonCopy = (reason: string | null | undefined, zh: boolean) => { export function ResearchAnalysisButton({analysis: a}: {analysis: Controller}) { const unavailable = a.pack?.availability !== "available"; - // The adjacent source/cost line supplies scope before this one explicit - // request. Cached/historical results and unavailable states are reads only. + // The action keeps its separate-usage consequence visible. Further method + // explanation is optional; cached results and unavailable states are reads. const readOnly = unavailable || a.pack?.result || a.pending || a.recordedRunning; - return ; + + {a.pack ?

{a.zh ? "分析请求设置(固定):" : "Fixed analysis request: "}{a.pack.config.model} / {a.pack.config.effort} / {a.pack.config.service_tier}

: null} +

{a.pack?.result ? (a.zh ? "所选范围已有分析,查看不调用 Agent。" : "An analysis exists for the selected scope. Viewing never invokes an Agent.") : (a.zh ? "点击深度分析使用本机 Codex 账户,单独产生分析用量;普通浏览不调用 Agent。" : "Deep analysis uses the local Codex account with separate analysis usage; ordinary browsing never invokes an Agent.")}

+

{a.zh ? "只分析已授权的固定轨迹,不改变研究历史。范围、方法与来源版本可在结果中查看。" : "Analyzes an authorized fixed trajectory without changing research history. Scope, method and source revisions remain available with the result."}

+
{a.pack && !a.pack.result && !unavailable ? {a.pack.config.model} · {a.zh ? "单独用量" : "Separate usage"} : null}; } -export function ResearchAnalysisScope({analysis: a}: {analysis: Controller}) { +export function ResearchAnalysisStatus({analysis: a}: {analysis: Controller}) { const p = a.pack; if (!p) return

{a.loading ? (a.zh ? "正在核对可分析来源;浏览不调用 Agent。" : "Checking analysis sources. Browsing never invokes an Agent.") : reasonCopy(a.snapshot?.catalog.reason_code, a.zh)}

; - return

{p.result ? (a.zh ? "所选范围已有分析,查看不调用 Agent。" : "An analysis exists for the selected scope. Viewing never invokes an Agent.") : <> - {a.zh ? "分析请求设置(固定):" : "Fixed analysis request: "}{p.config.model} / {p.config.effort} / {p.config.service_tier} · {a.zh ? "使用本机 Codex 产生单独分析用量;浏览不调用 Agent。" : "Uses the local Codex account with separate analysis usage; browsing never invokes an Agent."} - }

; + return !p.result && p.availability !== "available" ?

{reasonCopy(p.reason_code, a.zh)}

: null; } export function ResearchProgressSummary({analysis: a}: {analysis: Controller}) { @@ -203,21 +206,25 @@ export function ResearchAnalysisPanel({analysis: a}: {analysis: Controller}) { {a.loading && !p ?

{zh ? "正在核对可分析来源…" : "Checking authorized sources…"}

: null} {!a.loading && !p ?

{reasonCopy(a.snapshot?.catalog.reason_code, zh)}

: null} {p ? <> -

{p.selection.exhaustive ? (zh ? "覆盖此固定分析范围;不等于覆盖全部研究。" : "Covers this fixed analysis scope, not necessarily the whole research history.") : (zh ? "目的抽样,仅此片段;不代表整场研究。" : "Purposive sample of this episode, not the whole run.")} {zh ? "下方仍显示当前研究 Goal 的已记录历史。" : "The current research Goal’s recorded history remains below."}

+
{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."}

+

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

+
{!available ?

{reasonCopy(p.reason_code, zh)}

: null} {!result || a.pending || a.recordedRunning ?
{a.pending || a.recordedRunning ? <>

{a.pending ? : null}{a.pending ? (zh ? "正在处理分析请求…" : "Processing analysis request…") : (zh ? "上次记录仍待分析结果;当前执行状态尚未核验。" : "The last receipt awaits a result; current execution is unverified.")}

: result ? - : } + : } {!result && !a.pending && !a.recordedRunning ? {zh ? "使用本机 Codex 账户,单独产生分析用量;浏览不调用 Agent。" : "Uses the local Codex account with separate analysis usage. Browsing never invokes an Agent."} : null}
: null} {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}

{zh ? "正向诊断、证据采用与最终任务收益分别判断。没有综合分数,也不据此推断因果或整体研究质量。" : "Diagnostic value, evidence adoption and task utility are separate. There is no total score, causal claim or whole-run quality rating."}

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

{headline}

{p.episodes?.map(episode =>

{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")}{zh ? "模型解释,未独立核验" : "Model interpretation, not independently verified"}

{row.reason}

{citationButtons(row.citations)}
: null; + 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; })}
{zh ? "展开逐项判断与证据" : "Inspect individual judgments and evidence"}
{result.assessments.filter(row => row.episode_id === episode.id).map(row => { const label = metricCopy[row.metric]; 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 3958150e55..e01cd3432b 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,8 +1,11 @@ .research-history { margin-block: 0; border-block: 1px solid var(--pw-line); padding-block: 24px; min-width: 0; } .research-history-heading { display: flex; align-items: flex-start; 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-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 { display: flex; align-items: center; flex-shrink: 0; gap: 8px; } +.research-history-controls { display: flex; align-items: center; flex-wrap: wrap; max-width: 100%; min-width: 0; gap: 8px; } .research-history-controls button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 36px; padding: 6px 10px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-bg); font: inherit; font-size: var(--text-caption); } .research-history-controls button:disabled { color: var(--pw-muted); opacity: .6; cursor: not-allowed; } .research-history-layout { display: grid; min-width: 0; margin-top: 16px; border: 1px solid var(--pw-line); border-radius: 12px; overflow: hidden; } @@ -53,6 +56,8 @@ .research-history-controls .research-lens button[aria-pressed="true"] { background: var(--pw-text); color: var(--pw-bg); } .research-history-canvas.is-timeline { max-height: none; overflow: visible; background: var(--pw-bg); padding: 16px; } .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; } .goal-overview .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; } .goal-overview .research-phase:hover { border-color: var(--pw-line-strong); } @@ -86,7 +91,10 @@ .research-row-plot { position: relative; min-width: 0; padding-inline: 22px; } .research-density { display: flex; align-items: flex-end; gap: 2px; height: 56px; } .research-density span { flex: 1; min-width: 1px; border-radius: 2px 2px 0 0; background: var(--pw-blue-soft); border-top: 2px solid var(--pw-blue); } -.research-density span.is-gap { background: var(--pw-red-bg); border-top: 0; } +.research-density span.is-gap, .research-gap-key { background: var(--pw-red-bg); } +.research-density span.is-gap { border-top: 0; } +.research-gap-key { display: inline-block; width: 10px; height: 10px; border: 1px solid var(--pw-red); border-radius: 2px; } +.research-bar-key { display: inline-block; width: 10px; height: 10px; background: var(--pw-blue-soft); border-top: 2px solid var(--pw-blue); border-radius: 2px 2px 0 0; } .research-decision-ticks { position: relative; height: 12px; margin-top: 4px; } .research-decision-ticks span { position: absolute; top: 0; width: 3px; height: 10px; margin-left: -1px; border-radius: 1px; background: var(--pw-blue); } .research-decision-ticks span.is-replan, .research-tick-key.is-replan { background: var(--pw-orbit); } 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 eb68e802ed..b2a94b6e84 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 @@ -1,10 +1,11 @@ import {useEffect, useId, useMemo, useRef, useState} from "react"; import {defaultParseSearch} from "@tanstack/react-router"; import {ArrowLeft, Circle, FlaskConical, GitBranch, RefreshCw} from "lucide-react"; -import {ResearchAnalysisButton, ResearchAnalysisPanel, ResearchAnalysisScope, ResearchProgressSummary, useResearchAnalysis} from "./goal-research-analysis"; +import {ResearchAnalysisButton, ResearchAnalysisPanel, ResearchAnalysisStatus, ResearchProgressSummary, useResearchAnalysis} from "./goal-research-analysis"; import {buildExploreTimeline, parseExploreFindingSections} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js"; import {GoalResearchTimeline} from "./goal-research-timeline"; import {requestJson} from "../../data/chat"; +import {ContextHelp} from "./context-help"; import {groupUnlinkedHistoryQuestions, layoutExploreHistory, parseExploreHistoryPage, projectExploreHistory, type ExploreHistoryEvent} from "../../../../../../loopx/control_plane/presentation/explore_history.js"; import "./goal-research-history.css"; @@ -167,19 +168,22 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0}: { const choose = (id: string, focus = true) => select({nodes: id ? [id] : [], day: null}, focus); if (!busy && !error && !read.events.length && !analysis.pack && !analysis.snapshot?.catalog.packs.length) return null; return
-

{copy.title}

-

{copy.scope}{value?.startAt ? <> · {copy.time} — : null}

+

{copy.title}

+

{copy.scope} · {copy.time}{zh ? "。" : ". "}{copy.coverage}{zh ? "。" : "."}

{copy.interpretation}{zh ? "。" : "."}

+ {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.scope}{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}`}`}

{selection.nodes.length || selection.day !== null ? : null}
{(["timeline", "graph"] as const).map(item => )}
-

{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}`}`} · {copy.coverage}

{error || history.error ?

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

: null} - + {busy && !read.events.length ?
{copy.loading}…
: null} @@ -224,7 +228,6 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0}: {

{selected.latest.title}

{selected.latest.summary || (zh ? "此节点尚无详细说明。" : "No detail was recorded for this node.")}

{selected.latest.blocked_reason ?

{selected.latest.blocked_reason}

: null}

{copy.evidence}

-

{copy.interpretation}

{selected.findings.length ? selected.findings.map(findingElement) :

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

}

{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}` : ""}
  • )}
@@ -233,8 +236,6 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0}: {
: null} {selectedMembers?.length ?
{if (event.key === "Escape") {event.stopPropagation(); choose("", false); graph.current?.focus();}}}>

{selectedGroup ? `${selectedGroup.day || copy.unknown} · ${zh ? "未关联问题" : "Unlinked questions"}` : (zh ? `相邻研究项 · ${selectedMembers.length}` : `Nearby research items · ${selectedMembers.length}`)}

-

{selectedGroup ? (zh ? "这里只按首次记录日期收纳,不推断共同路线或因果。每个问题的记录与依据保留如下。" : "Grouped only by first recorded date, without inferring a shared route or causality. Every question and its evidence remains below.") : (zh ? "这些点在当前宽度下互相遮挡,合拢后一次展开。每项研究的原始时间、结论与依据完整保留;合拢不表示关联或因果。" : "These points would overlap at the current width and open together. Every original time, finding and source remains; grouping implies no relationship or cause.")}

-

{copy.interpretation}

{selectedMembers.map(node =>

{node.latest.title}

{stateLabel(node.latest.status)} · {node.latest.summary}

{node.latest.evidence_refs.map(ref => {ref})} {node.findings.map(findingElement)} {updateRecords(node.revisions)} @@ -246,8 +247,6 @@ export function GoalResearchHistory({goalId, active, zh, refreshRevision = 0}: { {value?.missingLinks.length ?

{copy.missing} · {value.missingLinks.length}

: null} {value?.unknownTimeCount ?

{copy.unknown} · {value.unknownTimeCount}

: null} {value?.unknownKindCount ?

{zh ? "未识别类型保留于已读记录" : "Unrecognized types remain in loaded records"} · {value.unknownKindCount}

: null} - {value?.nodes.length ?

{zh ? "按来源分组,实线为已记录关联,虚线仅表示分组;排列不代表时间或进展。" : "Grouped by source. Solid lines are recorded relationships; dotted lines only group nodes. Position does not imply time or progress."}

: null} - {grouped.groups.length ?

{zh ? "未关联问题按首次记录日期收纳;每组可一次展开,不遗漏更早或更晚的记录。" : "Unlinked questions are collected by first recorded date. Open a group once to read every member and version."}

: null} {value?.events.length ?
setRecordsOpen(event.currentTarget.open)}>{zh ? "全部已读记录" : "All loaded records"} · {value.events.length} {recordsOpen ? <>
    {value.events.slice(0, recordLimit).map(event =>
  1. {event.title} · {stateLabel(event.status)}{event.summary ? ` — ${event.summary}` : ""}{event.parent_id ? {copy.group}: {event.parent_id} : null}{event.evidence_refs.map(ref => {ref})}{event.run_id ? {event.run_id} : null}
  2. )}
{recordLimit < value.events.length ? : null} : null} 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 45c0b81786..2776b3530d 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,6 +1,8 @@ .research-progress { margin-block: 16px; padding-block: 16px; border-block: 1px solid var(--pw-line); 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-body); font-weight: 600; } +.research-progress-title { display: flex; align-items: center; gap: 4px; min-width: 0; } +.research-progress-scope { margin-block: 4px 8px; } .research-progress label { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: var(--text-caption); } .research-progress select { min-width: 0; max-width: 400px; min-height: 44px; border: 1px solid var(--pw-line); border-radius: 6px; padding: 8px; background: var(--pw-bg); color: var(--pw-text); font: inherit; } .research-progress p, .research-progress small { color: var(--pw-muted); font-size: var(--text-caption); line-height: var(--leading-prose); } 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 9d6d529444..99f4410210 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 @@ -2,6 +2,7 @@ import {ArrowUpRight, ChevronDown} from "lucide-react"; import {TRAJECTORY_METRICS} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; import type {AssessmentCitation, AssessmentVerdict, TrajectoryAssessment, TrajectoryEpisode, TrajectoryInput, TrajectoryMetric, TrajectorySolverUsage} from "../../../../../../loopx/control_plane/capabilities/trajectory_assessment_types.js"; import type {TrajectoryNativeOutcomes} from "../../../../../../loopx/control_plane/capabilities/trajectory_native_outcomes.js"; +import {ContextHelp} from "./context-help"; import "./goal-research-progress.css"; // Read models from benchmark-toolkit. This view formats existing counts and @@ -31,11 +32,14 @@ export function verdictLabel(metric: TrajectoryMetric, verdict: AssessmentVerdic // deciding adoption, or borrowing values from another pack. function NativeResults({native, zh, onCitation}: {native: ResearchProgressPack["native_outcomes"]; zh: boolean; onCitation: (citation: AssessmentCitation, origin?: HTMLButtonElement) => void}) { const outcomes = native?.projection; - if (!native || !outcomes) return

{zh ? "原生测量:尚无同范围捕获;分析器判读不能补成实际收益。" : "Native measurements: no capture for this scope. Analyzer judgments cannot supply realized utility."}

; + if (!native || !outcomes) return

{zh ? "原生测量 · 未采集" : "Native measurements · not captured"}

; const historical = native.applicability === "historical"; const points = [["initial", "首次提交", "Initial submission"], ["best_valid", "窗口内最佳有效值", "Best valid in capture"], ["retained", "原生保留结果", "Native retained result"], ["final_valid", "最后有效提交", "Final valid submission"]] as const; return
-

{historical ? (zh ? "历史原生测量" : "Historical native measurements") : (zh ? "原生测量" : "Native measurements")}

+

{historical ? (zh ? "历史原生测量" : "Historical native measurements") : (zh ? "原生测量" : "Native measurements")}

+

{zh ? "最佳只限本次捕获窗口;原生保留与最后提交可能不同。未采集的值保留未知,不记零。" : "Best is limited to this capture; retained and final submissions can differ. Uncaptured values remain unknown, not zero."}

+

{zh ? "检验过收益、采用了证据及原目标达成各需各自证据,不能由这些数值推断。" : "Utility testing, evidence adoption and original-objective success each need their own evidence; these values cannot establish them."}

+
{historical ?

{zh ? "来源已变化或不可读;以下仅为历史捕获,当前结果未核验。" : "The source changed or is unavailable. These are historical captures; current results are unverified."}

: null}

{outcomes.metric.name} · {outcomes.metric.unit} · {outcomes.metric.higher_is_better ? (zh ? "越高越好" : "higher is better") : (zh ? "越低越好" : "lower is better")} · {outcomes.valid_count} / {outcomes.sample_count} {zh ? "项有效结果" : "valid results"}

{outcomes.complete ? (zh ? "声明为完整前缀" : "Declared complete prefix") : (zh ? "部分窗口,起点未知" : "Partial window; initial unknown")} · —

@@ -44,9 +48,8 @@ function NativeResults({native, zh, onCitation}: {native: ResearchProgressPack[" return
{zh ? cn : en}
{point ? <> {zh ? "提交" : "Submission"} {point.submission_index} - : {zh ? "— 未知,不记零" : "— Unknown, not zero"}}
; + : {zh ? "— 未知" : "— Unknown"}}
; })} -

{zh ? "最佳只限上述窗口;原生保留与最后提交可能不同。检验过收益、采用了证据及原目标达成各需各自证据,不能由这些数值推断。" : "Best is limited to this capture; retained and final submissions can differ. Utility testing, evidence adoption and original-objective success each need their own evidence."}

{zh ? "测量来源与限制" : "Measurement sources and limits"}

{native.artifact}
{native.artifact_sha256}

{points.map(([key, cn, en]) => { @@ -65,15 +68,20 @@ export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelec const duration = (s: number) => `${number(s / 60)} ${zh ? "分钟" : "min"}`; const stamp = (s: string) => new Intl.DateTimeFormat(zh ? "zh-CN" : "en", {timeZone: "UTC", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false}).format(new Date(s)); return
-

{zh ? "研究效果判读" : "Research effectiveness"}

{packs.length > 1 ? : null}
+

{zh ? "研究效果判读" : "Research effectiveness"}

+ {p ?

{p.title}

: null} +

{zh ? "分析器判读未校准,仅限所选片段;范围外或未捕获的工作未纳入。数字为“是 / 适用片段”,未知仍在分母中;点选指标可看依据。" : "Uncalibrated judgments of selected episodes only; work outside this scope or not captured is excluded. Counts are yes / eligible episodes, including unknowns. Select a metric for evidence."}

+

{zh ? "收益检验的“是”不保证收益为正;无益重复的“是”表示发现问题。六项分别判断,不合成总分、成功率或原生收益。" : "Yes for benefit testing does not imply positive utility; yes for repetition identifies a problem. The six judgments are not a total score, success rate or native utility."}

+

{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.")}

+

{zh ? "原生测量缺失时保持未采集;分析器判读不能补成实际收益。完整取样规则与版本在“来源、方法与覆盖”中。" : "Missing native measurements remain uncaptured; judgments cannot supply realized utility. Full selection rules and revisions are under Sources, method and coverage."}

+
{packs.length > 1 ? : null}
{error ?

{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}

: null} {p ? <> -

{p.title} · {number(p.episode_count)} {zh ? "个固定研究片段" : "fixed episodes"} · {p.selection.exhaustive ? (zh ? "仅覆盖声明范围" : "Declared scope only") : (zh ? "目的抽样,不代表全程" : "Purposive sample, not the whole run")}

+

{number(p.episode_count)} {zh ? "个研究片段" : "research episodes"} · {p.selection.exhaustive ? (zh ? "固定范围" : "Fixed scope") : (zh ? "定向抽样" : "Purposive sample")}{result ? (zh ? " · 未校准" : " · Uncalibrated") : ""}

{p.applicability === "historical" ?

{zh ? "历史分析:来源或方法已变化,不能当作当前判断。" : "Historical analysis: the source or method changed; these are not current judgments."}

: null} {result ?
-

{zh ? "分析器逐项判读 · 未校准。数字为“是 / 适用片段”,未知仍在分母中;展开任一项看理由与证据。" : "Uncalibrated analyzer judgments. Counts are yes / eligible episodes, including unknowns in the denominator. Expand a metric for reasons and evidence."}

{TRAJECTORY_METRICS.map(metric => { const label = metricCopy[metric], row = result.metrics.find(item => item.metric === metric); @@ -82,7 +90,7 @@ export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelec {label[zh ? 0 : 1]} {row?.eligible ? `${number(row.yes)} / ${number(row.eligible)}` : "—"} {!row ? (zh ? "没有此指标记录" : "No metric record") : !row.eligible ? (zh ? "不适用" : "N/A") : assessments.length === 1 ? verdictLabel(metric, assessments[0].verdict, zh) : (zh ? "是 / 适用片段" : "Yes / eligible episodes")} - {row?.eligible ? {zh ? "否" : "No"} {number(row.no)} · {zh ? "未知" : "Unknown"} {number(row.unknown)} : null} + {row?.eligible && (row.no || row.unknown) ? {[row.no ? `${zh ? "否" : "No"} ${number(row.no)}` : "", row.unknown ? `${zh ? "未知" : "Unknown"} ${number(row.unknown)}` : ""].filter(Boolean).join(" · ")} : null}

{label[zh ? 2 : 3]}

{metric === "benefit_test" ?

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

: null} @@ -98,8 +106,7 @@ export function ResearchProgress({pack: p, packs, selectedId, error, zh, onSelec
; })} -

{zh ? "六项分别判断,不合成总分、成功率或原生收益。" : "Six separate judgments, not a total score, success rate or native utility."}

- :

{zh ? "此范围尚无分析器判读。已有用量不能替代效果;按需深度分析后在此展示。" : "No analyzer judgments exist for this scope. Usage is not effectiveness; requested deep analysis will appear here."}

} + :

{zh ? "所选范围尚未分析" : "No analysis for the selected scope"}

}
{zh ? "来源、方法与覆盖" : "Sources, method and coverage"}
{zh ? "题目 / 运行" : "Case / run"}
{p.scope.case_id}
{p.scope.run_id}
{zh ? "证据窗口" : "Evidence window"}
{p.scope.window}
{zh ? "取样规则" : "Selection rule"}
{p.selection.rule}
diff --git a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx index 580aef9029..92c0ee22bb 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-results.tsx @@ -41,8 +41,7 @@ export function GoalResearchResults({goalId, api, zh}: {goalId: string; api: Goa return
-

{zh ? "研究成果" : "Research results"}

-

{zh ? "查看本 Goal 已保存的研究结论、反证和待复核事项。" : "Review this Goal’s saved conclusions, counterevidence and open questions."}

+

{zh ? "研究成果" : "Research results"}

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 1002b6d3c6..c11fc1b86a 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 @@ -2,6 +2,7 @@ import {useLayoutEffect, useRef, useState, type CSSProperties} from "react"; import type {ExploreTimeline, TimelineFinding, TimelineNode, TimelinePhase} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js"; import {exploreTimelinePhaseAt, groupExploreTimelinePoints, selectExploreTimelineDecisions} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js"; import type {groupUnlinkedHistoryQuestions} from "../../../../../../loopx/control_plane/presentation/explore_history.js"; +import {ContextHelp} from "./context-help"; /** Default whole-history reading: thirds, recording density, recorded routes, * unlinked questions and explicit decisions on one record-time axis. @@ -33,20 +34,20 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da const copy: Copy = zh ? { early: "前段", middle: "中段", late: "后段", items: "研究项", findings: "结论", decisions: "显式决策", none: "未记录显式决策", changed: "变化", stopped: "停止", retained: "保留", - unstructured: "结论未按 writeback 结构记录,无法拆出依据与决策", + unstructured: "决策未结构化", unstructuredHint: "结论未按 writeback 结构记录,无法拆出依据与决策。", density: "记录密度", densityHint: "柱高为事件数;浅红为无记录空档", ticks: "转向 / 继续", - routes: "研究路线", routeHint: "仅由显式关联或父子分组相连", singles: "未关联研究项", singlesHint: "不推断路线或因果", - trail: "决策轨迹", trailHint: "保留各轮显式决策,按记录时间最近在前", more: "显示更多决策", all: "全部时段", + routes: "研究路线", routeHint: "仅由显式关联或父子分组相连", singles: "未关联研究项", + trail: "决策轨迹", more: "显示更多决策", all: "全部时段", span: "记录跨度", start: "起", end: "止", replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定", confirmed: "来源记为确认", refuted: "已反驳", tentative: "待检查", unlabeled: "无结论", phaseNote: "按记录时间三等分,仅为阅读分组,不是预声明的评估窗口。", noTime: "没有可用的记录时间,无法放上时间轴;下方记录仍可完整阅读。", } : { early: "Early", middle: "Middle", late: "Late", items: "Research items", findings: "Findings", decisions: "Explicit decisions", none: "No explicit decision recorded", changed: "Changed", stopped: "Stopped", retained: "Retained", - unstructured: "Findings were not recorded in writeback sections; evidence and decisions cannot be separated", + unstructured: "Unstructured decisions", unstructuredHint: "Findings were not recorded in writeback sections; evidence and decisions cannot be separated.", density: "Recording density", densityHint: "Bar height is events; pale red is a recording gap", ticks: "Replan / continue", - routes: "Research routes", routeHint: "Connected only by recorded relationships or parent grouping", singles: "Unlinked research items", singlesHint: "No route or causality inferred", - trail: "Decision history", trailHint: "Every recorded decision, newest record time first", more: "Show more decisions", all: "Whole history", + routes: "Research routes", routeHint: "Connected only by recorded relationships or parent grouping", singles: "Unlinked research items", + trail: "Decision history", more: "Show more decisions", all: "Whole history", span: "Recorded span", start: "From", end: "To", replan: "Replan", continue: "Continue", stop: "Stop", wait: "Wait", no_change: "No change", ask_human: "Ask human", confirmed: "Recorded confirmation", refuted: "Refuted", tentative: "Tentative", unlabeled: "No finding", phaseNote: "Equal thirds of record time, for reading only; not a pre-declared evaluation window.", noTime: "No usable record time; the history cannot be placed on a time axis. All records remain readable below.", @@ -59,6 +60,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da const label = (key: string) => copy[key] ?? key; const hours = (from: number, to: number) => { const value = (to - from) / 3_600_000; return value >= 48 ? `${(value / 24).toFixed(1)} ${zh ? "天" : "d"}` : `${value.toFixed(1)} ${zh ? "小时" : "h"}`; }; const maxEvents = Math.max(1, ...timeline.bins.map(bin => bin.events)); + const hasRecordingGaps = timeline.bins.some(bin => !bin.events); const ticks: number[] = []; const midnight = new Date(start); midnight.setHours(24, 0, 0, 0); for (let at = midnight.getTime(); at <= end; at += 86_400_000) ticks.push(at); @@ -103,10 +105,19 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da ; }; return
+
{zh ? "记录时间" : "Record time"} +

{copy.phaseNote}

{copy.densityHint}{zh ? "。" : ". "}{zh ? "无记录空档不代表研究停止。" : "A recording gap does not establish that research stopped."}

+ {!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."}

+
{phase !== null ? : null}
{timeline.phases.map(phaseCard)}
-

{copy.phaseNote}{phase !== null ? <> · : null}

-
{copy.density}{copy.densityHint}{decisionOutcomes.length ? {decisionOutcomes.map(outcome => {label(outcome)})} : null}
+
{copy.density} +