Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -338,9 +338,9 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
const clusterY = layout.length ? Math.max(...layout.map(item => item.y + 124)) + 24 : 24;
const width = Math.max(600, grouped.groups.length ? 944 : 0, ...layout.map(item => item.x + 230));
const height = Math.max(230, ...layout.map(item => item.y + 124), grouped.groups.length ? clusterY + Math.ceil(grouped.groups.length / 4) * 124 : 0);
const select = (next: Selection, focus = true) => {
const select = (next: Selection, focus = true, openingControl?: HTMLButtonElement) => {
if (focus && (next.nodes.length || next.day !== null)) {
opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
opener.current = openingControl ?? (document.activeElement instanceof HTMLElement ? document.activeElement : null);
focusPending.current = true;
}
next = {...next, question: next.question ?? !next.finding,
Expand Down Expand Up @@ -426,7 +426,9 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
</ContextHelp>
</div> : null;
const historyView = !value ? null : lens === "timeline" && timeline ? <div className="research-history-canvas is-timeline" ref={graph} tabIndex={-1}>
<GoalResearchTimeline timeline={timeline} zh={zh} selectedIds={selection.nodes} selectedEventId={selectedFinding?.event_id} selectedDay={selection.day} dayGroups={grouped.groups} onGroupSelect={day => select({nodes: [], day})} phase={phase} onPhase={next => chooseView(lens, next)} onSelect={(id, finding) => choose(id, true, finding)} onSelectMany={nodes => select({nodes, day: null})} timeLabel={fullTime} dayLabel={dayTime}/>
<GoalResearchTimeline timeline={timeline} zh={zh} selectedIds={selection.nodes} selectedEventId={selectedFinding?.event_id} selectedDay={selection.day} dayGroups={grouped.groups}
onGroupSelect={(day, button) => select({nodes: [], day}, true, button)} phase={phase} onPhase={next => chooseView(lens, next)}
onSelect={(id, finding) => choose(id, true, finding)} onSelectMany={(nodes, button) => select({nodes, day: null}, true, button)} timeLabel={fullTime} dayLabel={dayTime}/>
</div> : <div className="research-history-canvas" ref={graph} tabIndex={0} aria-label={zh ? "完整已载入研究关系图;可滚动" : "Full loaded research graph; scrollable"}>
<div className="research-history-stage" style={{width, height}}>
<svg width={width} height={height} aria-hidden="true" className="research-history-edges">
Expand Down Expand Up @@ -458,7 +460,7 @@ export function GoalResearchHistory({goalId, originalGoalId = goalId, preferHist
</span> : <span className="research-node-count"><time>{timeLabel(node.updates[0]?.recorded_at)}</time>{node.updates.length > 1 ? ` · ${node.updates.length} ${copy.changes}` : ""}{node.findings.length ? ` · ${findingCopy.count(node.findings.length)}` : ""}</span>}
</button>})}
{grouped.groups.map((group, index) => <button type="button" key={`day:${group.day}`} className={`research-history-node research-history-group${selectedGroup === group ? " is-selected" : ""}`} style={{left: (index % 4) * 230 + 24, top: clusterY + Math.floor(index / 4) * 124}}
data-research-day={group.day} aria-pressed={selectedGroup === group} onClick={() => select({nodes: [], day: group.day})}>
data-research-day={group.day} aria-pressed={selectedGroup === group} onClick={event => select({nodes: [], day: group.day}, true, event.currentTarget)}>
<span className="research-node-meta"><Circle size={12} aria-hidden/>{zh ? "未关联问题" : "Unlinked questions"}</span>
<strong>{group.day ? `${group.day} UTC` : copy.unknown}</strong><span className="research-node-count">{group.nodes.length} {group.nodes.length === 1 ? copy.question : copy.questions} · {zh ? "展开全部" : "read all"}</span>
</button>)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import {ContextHelp} from "./context-help";
type Copy = Record<string, string>;
type Props = {
timeline: ExploreTimeline; zh: boolean; selectedIds: readonly string[]; selectedEventId?: string; phase: number | null;
selectedDay: string | null; dayGroups: ReturnType<typeof groupUnlinkedHistoryQuestions>["groups"]; onGroupSelect: (day: string) => void;
onSelect: (id: string, finding?: string) => void; onSelectMany: (ids: string[]) => void; onPhase: (index: number | null) => void;
selectedDay: string | null; dayGroups: ReturnType<typeof groupUnlinkedHistoryQuestions>["groups"]; onGroupSelect: (day: string, button: HTMLButtonElement) => void;
onSelect: (id: string, finding?: string) => void; onSelectMany: (ids: string[], button: HTMLButtonElement) => void; onPhase: (index: number | null) => void;
timeLabel: (value: number) => string; dayLabel: (value: number) => string;
};

Expand Down Expand Up @@ -84,7 +84,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
: `${ids.length} nearby research items · ${timeLabel(group.items[0]!.first!)} — ${timeLabel(group.items.at(-1)!.first!)} · Read every record and source`;
return <button key={ids[0]} type="button" title={title} aria-label={title} aria-pressed={selected} data-research-nodes={ids.join(" ")}
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)}><span>{ids.length}</span></button>;
style={{left: pct(group.at)}} onClick={event => onSelectMany(ids, event.currentTarget)}><span>{ids.length}</span></button>;
});
const trail = groupExploreTimelineDecisions(timeline, phase);
const decisionOutcomes = [...new Set(timeline.decisions.map(item => item.sections!.decision!.outcome))];
Expand Down Expand Up @@ -189,7 +189,7 @@ export function GoalResearchTimeline({timeline, zh, selectedIds, selectedEventId
<div className="research-row-label"><strong>{copy.singles} · {groupedIds.size}</strong></div>
<div className="research-unlinked-days">{dayGroups.map(group => <button type="button" key={group.day} data-research-day={group.day} aria-pressed={selectedDay === group.day}
className={`${selectedDay === group.day ? "is-selected" : ""}${group.nodes.some(node => inPhase(timeline.nodes.get(node.id)!.first)) ? "" : " is-dimmed"}`}
onClick={() => onGroupSelect(group.day)}><time>{group.day || (zh ? "时间未知" : "Unknown time")}</time><strong>{group.nodes.length} {group.nodes.length === 1 ? copy.question : copy.questions}</strong></button>)}</div>
onClick={event => onGroupSelect(group.day, event.currentTarget)}><time>{group.day || (zh ? "时间未知" : "Unknown time")}</time><strong>{group.nodes.length} {group.nodes.length === 1 ? copy.question : copy.questions}</strong></button>)}</div>
</> : null}
{swarm.length ? <>
<div className="research-row-label"><strong>{copy.singles} · {swarm.length}</strong></div>
Expand Down
12 changes: 11 additions & 1 deletion examples/personal-workspace-browser/research-route-reading.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,17 @@ export const researchRouteReadingScenario = {
await page.emulateMedia({reducedMotion: "reduce"});
await page.evaluate(value => localStorage.setItem("loopx-pw-locale", value), locale);
await page.goto(url);
await history.locator('[data-research-day="2026-01-04"]').click();
const day = history.locator('[data-research-day="2026-01-04"]');
await day.waitFor();
await history.locator(".research-history-canvas").focus();
// WebKit can activate the button while leaving focus on the canvas.
await day.evaluate(button => button.click());
await reader.locator(".research-question-index button").nth(3).click();
await page.keyboard.press("Escape");
await reader.locator(".research-question-index").waitFor();
await page.keyboard.press("Escape");
await page.waitForFunction(() => document.activeElement?.getAttribute("data-research-day") === "2026-01-04");
await day.click();
assert.equal(await reader.locator(".research-question-index li").count(), 24);
await reader.locator(".research-question-index button").nth(3).click();
const questions = reader.getByRole("combobox", {name: zh ? "选择同组研究问题" : "Choose a question in this group"});
Expand Down
Loading