From d73c216732387e05d444cd9b609864eb1696d304 Mon Sep 17 00:00:00 2001 From: huangruiteng Date: Mon, 26 Oct 2026 15:09:31 +0800 Subject: [PATCH 1/2] fix(research): group decision history beneath each question Signed-off-by: huangruiteng --- .../goal-research-history.css | 24 ++++--- .../goal-research-timeline.tsx | 66 ++++++++++++++----- .../research-finding-reader.mjs | 37 +++++++++-- .../presentation/explore_timeline.ts | 18 +++++ .../control_plane_ts/explore_history.test.ts | 22 +++++++ 5 files changed, 137 insertions(+), 30 deletions(-) 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 58702688d6..cc02cde652 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 @@ -190,14 +190,21 @@ .research-trail { border-top: 1px solid var(--pw-line); padding-top: 20px; } .research-trail h3 { display: flex; align-items: baseline; gap: 10px; margin: 0 0 12px; font-size: var(--text-lead); font-weight: 600; } .research-trail h3 small { color: var(--pw-muted); font-size: var(--text-caption); font-weight: 400; } -.research-trail-list { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 0; padding: 0; list-style: none; } -.research-history .research-trail-list button { display: flex; flex-direction: column; align-items: stretch; gap: 8px; width: 100%; height: 100%; padding: 16px; border: 1px solid var(--pw-line); border-radius: 10px; background: var(--pw-bg); color: var(--pw-text); font: inherit; font-size: var(--text-caption); line-height: var(--leading-prose); text-align: left; cursor: pointer; transition: border-color var(--motion-fast) var(--ease-standard), background-color var(--motion-fast) var(--ease-standard); } -.research-history .research-trail-list button:hover { border-color: var(--pw-line-strong); background: var(--pw-hover); } -.research-history .research-trail-list button.is-selected { border-color: var(--pw-text); outline: 1px solid var(--pw-text); } -.research-trail-list strong { font-size: var(--text-body); line-height: var(--leading-body); font-weight: 600; display: block; overflow-wrap: anywhere; } -.research-trail-list span:not(.research-trail-meta) { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.research-trail-groups { margin: 0; padding: 0; list-style: none; } +.research-trail-question { padding-block: 20px; border-bottom: 1px solid var(--pw-line); } +.research-trail-question:first-child { padding-top: 0; } +.research-trail-question h4 { margin: 0 0 8px; font-size: var(--text-lead); line-height: var(--leading-prose); font-weight: 600; overflow-wrap: anywhere; } +.research-trail-question > header { margin-bottom: 16px; } +.research-trail-list { display: grid; gap: 0; margin: 0 0 0 8px; padding: 0 0 0 16px; border-left: 1px solid var(--pw-line); list-style: none; } +.research-trail-list > li { position: relative; min-width: 0; } +.research-trail-list > li::before { content: ""; position: absolute; top: 23px; left: -20px; width: 7px; height: 7px; border-radius: 50%; background: var(--pw-muted); box-shadow: 0 0 0 4px var(--pw-bg); } +.research-history .research-trail-list button { display: flex; flex-direction: column; align-items: stretch; gap: 6px; width: 100%; min-height: 44px; padding: 12px; border: 0; border-radius: 6px; background: none; color: var(--pw-text); font: inherit; font-size: var(--text-body); line-height: var(--leading-prose); text-align: left; cursor: pointer; transition: background-color var(--motion-fast) var(--ease-standard); } +.research-history .research-trail-list button:hover { background: var(--pw-hover); } +.research-history .research-trail-list button.is-selected { outline: 1px solid var(--pw-line-strong); } +.research-trail-change { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; } .research-trail-list .is-muted, .research-finding-sections .is-muted { color: var(--pw-muted); } -.research-trail-meta { display: flex; align-items: center; gap: 8px; color: var(--pw-muted); } +.research-trail-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; color: var(--pw-muted); font-size: var(--text-caption); } +.research-trail-question > .research-link { margin: 8px 0 0 36px; min-height: 44px; } .research-outcome { display: inline-block; padding: 1px 8px; border-radius: 999px; font-size: var(--text-caption); font-weight: 600; background: var(--pw-blue-soft); color: var(--pw-blue-ink); } .research-outcome.is-replan { background: var(--pw-tone); color: var(--pw-orbit); } .research-outcome.is-stop, .research-outcome.is-ask_human { background: var(--pw-amber-bg); color: var(--pw-text); } @@ -216,8 +223,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: 1200px) { .research-trail-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } } -@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 (max-width: 900px) { .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-timeline.tsx b/apps/presentation/dashboard/src/features/personal-workspace/goal-research-timeline.tsx index ffb4354ba0..9bd6c24157 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 @@ -1,7 +1,7 @@ import {CircleHelp} from "lucide-react"; 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 {ExploreTimeline, TimelineNode, TimelinePhase} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js"; +import {exploreTimelinePhaseAt, groupExploreTimelinePoints, groupExploreTimelineDecisions} from "../../../../../../loopx/control_plane/presentation/explore_timeline.js"; import type {groupUnlinkedHistoryQuestions} from "../../../../../../loopx/control_plane/presentation/explore_history.js"; import {ContextHelp} from "./context-help"; @@ -38,7 +38,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da unstructured: "决策未结构化", unstructuredHint: "结论未按 writeback 结构记录,无法拆出依据与决策。", density: "记录密度", densityHint: "柱高为事件数;浅红为无记录空档", ticks: "转向 / 继续", routes: "研究路线", routeHint: "仅由显式关联或父子分组相连", singles: "未关联研究项", - trail: "决策轨迹", more: "显示更多决策", all: "全部时段", + trail: "决策轨迹", more: "显示更早记录", moreQuestions: "显示更多问题", records: "条决策记录", latestRecord: "最近记录", unknownTime: "时间未知", all: "全部时段", span: "记录跨度", start: "起点", end: "末端", replan: "转向", continue: "继续", stop: "停止", wait: "等待", no_change: "无变化", ask_human: "请人决定", confirmed: "来源记为确认", refuted: "已反驳", tentative: "待检查", unlabeled: "无结论", phaseNote: "按记录时间三等分,仅为阅读分组,不是预声明的评估窗口。", noTime: "没有可用的记录时间,无法放上时间轴;下方记录仍可完整阅读。", @@ -48,7 +48,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da 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", - trail: "Decision history", more: "Show more decisions", all: "Whole history", + trail: "Decision history", more: "Show earlier records", moreQuestions: "Show more questions", records: "decision records", latestRecord: "Most recent record", unknownTime: "Unknown time", all: "Whole history", span: "Recorded span", start: "Start", end: "End", 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.", @@ -82,7 +82,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da className={`research-timeline-marker is-cluster${selected ? " is-selected" : ""}${group.items.some(item => inPhase(item.first)) ? "" : " is-dimmed"}`} style={{left: pct(group.at)}} onClick={() => onSelectMany(ids)}>{ids.length}; }); - const trail = selectExploreTimelineDecisions(timeline, phase); + const trail = groupExploreTimelineDecisions(timeline, phase); const decisionOutcomes = [...new Set(timeline.decisions.map(item => item.sections!.decision!.outcome))]; 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); @@ -112,7 +112,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedDay, da

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

+

{zh ? "未关联问题按首次记录 UTC 日期收纳,不推断路线或因果。决策轨迹按研究问题归组,组内保留各轮显式决策,最近记录在前;同名问题不会自动合并。" : "Unlinked questions group by first-record UTC date, without inferring a route or cause. Decision history groups explicit decisions by question, newest record first; matching titles do not merge questions."}

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