修复评审器跨组件修改项跳转丢失并展开完整文档视图

- DiffView 聚焦 effect 依赖补上 before/after:stage 文本到达、
  MergeView 重建后重新应用选区和滚动,跨组件点击不再停在顶部;
- 非可编辑面板加 drawSelection,聚焦范围在浏览器中可见;
- 移除 collapseUnchanged,评审始终可读完整未改动段落;
- 滚动容器移到 .cm-mergeView 外层,编辑器高度自适应内容;
- 新增 3 项 Vitest 覆盖折叠移除与聚焦时序,共 9 项通过;
- README 与 explanation 同步当前检查数量。
This commit is contained in:
2026-08-24 08:56:46 +08:00
parent ac798a2610
commit efc1313d87
6 changed files with 99 additions and 14 deletions
+1
View File
@@ -24,6 +24,7 @@ uploads/
artifacts/ artifacts/
outputs/ outputs/
experiments/ experiments/
research-wiki/scratch/
*.cleaned.md *.cleaned.md
report.json report.json
+3 -3
View File
@@ -24,7 +24,7 @@ profile 表达论文、政务文档、RAG、文档对比等不同需求。
- 已有独立的 `reviewer/` 本地只读前端,服务只消费一次已发布的产物和定位文件,并与报告层共用纯 Python 快照重放逻辑, - 已有独立的 `reviewer/` 本地只读前端,服务只消费一次已发布的产物和定位文件,并与报告层共用纯 Python 快照重放逻辑,
不导入组件、不调用流水线,也不重新清洗; 不导入组件、不调用流水线,也不重新清洗;
- 第一批流水线已对 5 份论文 Markdown 完成保存型实验,5/5 成功,共记录 155 条修改,第二次运行零修改; - 第一批流水线已对 5 份论文 Markdown 完成保存型实验,5/5 成功,共记录 155 条修改,第二次运行零修改;
- 当前基础检查为 Ruff、mypy、229 项 pytest 测试,以及前端 ESLint、TypeScript、6 项 Vitest 测试和生产构建, - 当前基础检查为 Ruff、mypy、229 项 pytest 测试,以及前端 ESLint、TypeScript、9 项 Vitest 测试和生产构建,
实际命令见本文“当前可用检查”。 实际命令见本文“当前可用检查”。
项目还没有面向任意数据集的完整清洗规则集、Markdown/HTML 通用 parser、profile 格式、通用文件输入接口、公共 CLI、 项目还没有面向任意数据集的完整清洗规则集、Markdown/HTML 通用 parser、profile 格式、通用文件输入接口、公共 CLI、
@@ -196,8 +196,8 @@ find research-wiki -maxdepth 2 -type f | sort
git status --short git status --short
``` ```
上述 Python 验收已于 2026-08-23 在 Python 3.13.11 环境实际运行:Ruff 通过,mypy 检查 43 个源码、测试和实验脚本文件 上述 Python 验收已于 2026-08-24 在 Python 3.13.11 环境实际运行:Ruff 通过,mypy 检查 43 个源码、测试和实验脚本文件
无问题,pytest 共 229 项测试通过。评审器验收也在当前用户 nvm 的 Node.js 24.19.0 环境实际运行:ESLint 和 TypeScript 无问题,pytest 共 229 项测试通过。评审器验收也在当前用户 nvm 的 Node.js 24.19.0 环境实际运行:ESLint 和 TypeScript
通过,Vitest 共 6 项测试通过,生产构建成功;并以一批真实的 5 文档、8 组件、155 条修改产物验证了接口读取和逐阶段哈希。 通过,Vitest 共 9 项测试通过,生产构建成功;并以一批真实的 5 文档、8 组件、155 条修改产物验证了接口读取和逐阶段哈希。
当前环境没有可用的图形浏览器,因此页面视觉布局尚未进行真实浏览器人工验收。`requires-python` 仍以 当前环境没有可用的图形浏览器,因此页面视觉布局尚未进行真实浏览器人工验收。`requires-python` 仍以
`pyproject.toml` 声明的 Python 3.11 及以上为准;本次结果不等于已经在每个受支持版本上完成兼容性验证。 `pyproject.toml` 声明的 Python 3.11 及以上为准;本次结果不等于已经在每个受支持版本上完成兼容性验证。
@@ -93,10 +93,10 @@ Markdown 只作为文本交给 CodeMirror,不进入 `innerHTML`。第一版不
## 7. 当前验证结果和边界 ## 7. 当前验证结果和边界
2026-08-23 使用 Python 3.13.11 和当前用户 nvm 中的 Node.js 24.19.0 完成: 2026-08-24 使用 Python 3.13.11 和当前用户 nvm 中的 Node.js 24.19.0 完成:
- Python Ruff、mypy 和 229 项 pytest 通过; - Python Ruff、mypy 和 229 项 pytest 通过;
- reviewer ESLint、TypeScript、6 项 Vitest 和生产构建通过; - reviewer ESLint、TypeScript、9 项 Vitest 和生产构建通过;
- 新运行 `clindb-first-batch-reviewer-v1` 的 5 份论文全部 `success`,共 155 条实际修改; - 新运行 `clindb-first-batch-reviewer-v1` 的 5 份论文全部 `success`,共 155 条实际修改;
- 5 份原文运行前后哈希不变; - 5 份原文运行前后哈希不变;
- 本地 API 成功校验 5 份文档、8 个组件和 40 个组件阶段; - 本地 API 成功校验 5 份文档、8 个组件和 40 个组件阶段;
+7 -5
View File
@@ -1,7 +1,7 @@
import { markdown } from "@codemirror/lang-markdown"; import { markdown } from "@codemirror/lang-markdown";
import { MergeView } from "@codemirror/merge"; import { MergeView } from "@codemirror/merge";
import { EditorState } from "@codemirror/state"; import { EditorSelection, EditorState } from "@codemirror/state";
import { EditorView, lineNumbers } from "@codemirror/view"; import { drawSelection, EditorView, lineNumbers } from "@codemirror/view";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
interface DiffViewProps { interface DiffViewProps {
@@ -45,6 +45,7 @@ const readOnlyExtensions = [
markdown(), markdown(),
EditorState.readOnly.of(true), EditorState.readOnly.of(true),
EditorView.editable.of(false), EditorView.editable.of(false),
drawSelection(),
EditorView.lineWrapping, EditorView.lineWrapping,
editorTheme, editorTheme,
]; ];
@@ -64,7 +65,6 @@ export function DiffView({ before, after, beforeLabel, afterLabel, focusRange }:
orientation: "a-b", orientation: "a-b",
gutter: true, gutter: true,
highlightChanges: true, highlightChanges: true,
collapseUnchanged: { margin: 4, minSize: 8 },
}); });
merge.current = view; merge.current = view;
return () => { return () => {
@@ -73,6 +73,8 @@ export function DiffView({ before, after, beforeLabel, afterLabel, focusRange }:
}; };
}, [before, after]); }, [before, after]);
// 切换组件时 stage 文本先到、旧视图上的提前滚动会被重建覆盖,所以聚焦
// 必须等 MergeView 换成目标文本之后再执行。
useEffect(() => { useEffect(() => {
const view = merge.current; const view = merge.current;
if (view === null || focusRange === null || focusRange === undefined) { if (view === null || focusRange === null || focusRange === undefined) {
@@ -81,11 +83,11 @@ export function DiffView({ before, after, beforeLabel, afterLabel, focusRange }:
const anchor = Math.min(Math.max(focusRange.start, 0), view.a.state.doc.length); const anchor = Math.min(Math.max(focusRange.start, 0), view.a.state.doc.length);
const head = Math.min(Math.max(focusRange.end, anchor), view.a.state.doc.length); const head = Math.min(Math.max(focusRange.end, anchor), view.a.state.doc.length);
view.a.dispatch({ view.a.dispatch({
selection: { anchor, head }, selection: EditorSelection.range(anchor, head),
effects: EditorView.scrollIntoView(anchor, { y: "center" }), effects: EditorView.scrollIntoView(anchor, { y: "center" }),
}); });
view.a.focus(); view.a.focus();
}, [focusRange]); }, [focusRange, before, after]);
return ( return (
<section className="diff-shell" aria-label={`${beforeLabel}${afterLabel}对比`}> <section className="diff-shell" aria-label={`${beforeLabel}${afterLabel}对比`}>
+8 -4
View File
@@ -355,18 +355,22 @@ p {
border-left: 1px solid #dcdbd3; border-left: 1px solid #dcdbd3;
} }
.diff-host, .diff-host {
.diff-host > .cm-mergeView {
height: 510px; height: 510px;
} }
.diff-host .cm-mergeViewEditors { .diff-host > .cm-mergeView {
height: 100%; height: 100%;
overflow-y: auto;
overscroll-behavior: contain;
}
.diff-host .cm-mergeViewEditors {
min-height: 100%;
} }
.diff-host .cm-editor { .diff-host .cm-editor {
min-width: 0; min-width: 0;
height: 100%;
} }
.changes-panel { .changes-panel {
+78
View File
@@ -1,8 +1,22 @@
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { EditorView } from "@codemirror/view";
import { DiffView } from "../src/client/DiffView.js"; import { DiffView } from "../src/client/DiffView.js";
function leftPaneSelection(): { from: number; to: number } | null {
const pane = document.querySelector(".diff-host .cm-editor");
if (pane === null) {
return null;
}
const view = EditorView.findFromDOM(pane as HTMLElement);
if (view === null) {
return null;
}
const { from, to } = view.state.selection.main;
return { from, to };
}
describe("DiffView", () => { describe("DiffView", () => {
it("keeps Markdown and raw HTML as inert editor text", () => { it("keeps Markdown and raw HTML as inert editor text", () => {
render( render(
@@ -18,4 +32,68 @@ describe("DiffView", () => {
expect(document.querySelector("img")).toBeNull(); expect(document.querySelector("img")).toBeNull();
expect(document.querySelector("script")).toBeNull(); expect(document.querySelector("script")).toBeNull();
}); });
it("keeps long unchanged sections available in the full document view", () => {
const before = Array.from({ length: 30 }, (_, index) => `line ${index + 1}`);
const after = [...before];
after[14] = "changed line 15";
render(
<DiffView
before={before.join("\n")}
after={after.join("\n")}
beforeLabel="清洗前"
afterLabel="清洗后"
/>,
);
expect(document.querySelector(".cm-collapsedLines")).toBeNull();
});
it("re-applies the focus selection after the compared texts change", () => {
const longText = (mark: string) =>
Array.from({ length: 30 }, (_, index) => (index === 14 ? mark : `line ${index + 1}`)).join("\n");
const { rerender } = render(
<DiffView before={longText("old")} after={longText("new")} beforeLabel="清洗前" afterLabel="清洗后" />,
);
// 模拟点击跨组件修改项:focusRange 先落在旧文本上,stage 文本随后到达。
rerender(
<DiffView
before={longText("old")}
after={longText("new")}
beforeLabel="组件 1 执行前"
afterLabel="组件 1 执行后"
focusRange={{ start: 58, end: 61 }}
/>,
);
rerender(
<DiffView
before={longText("stage before")}
after={longText("stage after")}
beforeLabel="组件 2 执行前"
afterLabel="组件 2 执行后"
focusRange={{ start: 58, end: 71 }}
/>,
);
// 最终视图必须是组件 2 的文本,且选区重新落在新文本的目标范围上。
expect(screen.getByRole("region", { name: "组件 2 执行前与组件 2 执行后对比" })).toBeInTheDocument();
expect(leftPaneSelection()).toEqual({ from: 58, to: 71 });
});
it("applies the focus selection when it arrives with the initial texts", () => {
render(
<DiffView
before={"a\nb"}
after={"a\nc"}
beforeLabel="清洗前"
afterLabel="清洗后"
focusRange={{ start: 0, end: 1 }}
/>,
);
expect(leftPaneSelection()).toEqual({ from: 0, to: 1 });
});
}); });