修复评审器跨组件修改项跳转丢失并展开完整文档视图
- DiffView 聚焦 effect 依赖补上 before/after:stage 文本到达、 MergeView 重建后重新应用选区和滚动,跨组件点击不再停在顶部; - 非可编辑面板加 drawSelection,聚焦范围在浏览器中可见; - 移除 collapseUnchanged,评审始终可读完整未改动段落; - 滚动容器移到 .cm-mergeView 外层,编辑器高度自适应内容; - 新增 3 项 Vitest 覆盖折叠移除与聚焦时序,共 9 项通过; - README 与 explanation 同步当前检查数量。
This commit is contained in:
@@ -24,6 +24,7 @@ uploads/
|
||||
artifacts/
|
||||
outputs/
|
||||
experiments/
|
||||
research-wiki/scratch/
|
||||
*.cleaned.md
|
||||
report.json
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ profile 表达论文、政务文档、RAG、文档对比等不同需求。
|
||||
- 已有独立的 `reviewer/` 本地只读前端,服务只消费一次已发布的产物和定位文件,并与报告层共用纯 Python 快照重放逻辑,
|
||||
不导入组件、不调用流水线,也不重新清洗;
|
||||
- 第一批流水线已对 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、
|
||||
@@ -196,8 +196,8 @@ find research-wiki -maxdepth 2 -type f | sort
|
||||
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
|
||||
通过,Vitest 共 6 项测试通过,生产构建成功;并以一批真实的 5 文档、8 组件、155 条修改产物验证了接口读取和逐阶段哈希。
|
||||
通过,Vitest 共 9 项测试通过,生产构建成功;并以一批真实的 5 文档、8 组件、155 条修改产物验证了接口读取和逐阶段哈希。
|
||||
当前环境没有可用的图形浏览器,因此页面视觉布局尚未进行真实浏览器人工验收。`requires-python` 仍以
|
||||
`pyproject.toml` 声明的 Python 3.11 及以上为准;本次结果不等于已经在每个受支持版本上完成兼容性验证。
|
||||
|
||||
@@ -93,10 +93,10 @@ Markdown 只作为文本交给 CodeMirror,不进入 `innerHTML`。第一版不
|
||||
|
||||
## 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 通过;
|
||||
- reviewer ESLint、TypeScript、6 项 Vitest 和生产构建通过;
|
||||
- reviewer ESLint、TypeScript、9 项 Vitest 和生产构建通过;
|
||||
- 新运行 `clindb-first-batch-reviewer-v1` 的 5 份论文全部 `success`,共 155 条实际修改;
|
||||
- 5 份原文运行前后哈希不变;
|
||||
- 本地 API 成功校验 5 份文档、8 个组件和 40 个组件阶段;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { MergeView } from "@codemirror/merge";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView, lineNumbers } from "@codemirror/view";
|
||||
import { EditorSelection, EditorState } from "@codemirror/state";
|
||||
import { drawSelection, EditorView, lineNumbers } from "@codemirror/view";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
interface DiffViewProps {
|
||||
@@ -45,6 +45,7 @@ const readOnlyExtensions = [
|
||||
markdown(),
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.editable.of(false),
|
||||
drawSelection(),
|
||||
EditorView.lineWrapping,
|
||||
editorTheme,
|
||||
];
|
||||
@@ -64,7 +65,6 @@ export function DiffView({ before, after, beforeLabel, afterLabel, focusRange }:
|
||||
orientation: "a-b",
|
||||
gutter: true,
|
||||
highlightChanges: true,
|
||||
collapseUnchanged: { margin: 4, minSize: 8 },
|
||||
});
|
||||
merge.current = view;
|
||||
return () => {
|
||||
@@ -73,6 +73,8 @@ export function DiffView({ before, after, beforeLabel, afterLabel, focusRange }:
|
||||
};
|
||||
}, [before, after]);
|
||||
|
||||
// 切换组件时 stage 文本先到、旧视图上的提前滚动会被重建覆盖,所以聚焦
|
||||
// 必须等 MergeView 换成目标文本之后再执行。
|
||||
useEffect(() => {
|
||||
const view = merge.current;
|
||||
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 head = Math.min(Math.max(focusRange.end, anchor), view.a.state.doc.length);
|
||||
view.a.dispatch({
|
||||
selection: { anchor, head },
|
||||
selection: EditorSelection.range(anchor, head),
|
||||
effects: EditorView.scrollIntoView(anchor, { y: "center" }),
|
||||
});
|
||||
view.a.focus();
|
||||
}, [focusRange]);
|
||||
}, [focusRange, before, after]);
|
||||
|
||||
return (
|
||||
<section className="diff-shell" aria-label={`${beforeLabel}与${afterLabel}对比`}>
|
||||
|
||||
@@ -355,18 +355,22 @@ p {
|
||||
border-left: 1px solid #dcdbd3;
|
||||
}
|
||||
|
||||
.diff-host,
|
||||
.diff-host > .cm-mergeView {
|
||||
.diff-host {
|
||||
height: 510px;
|
||||
}
|
||||
|
||||
.diff-host .cm-mergeViewEditors {
|
||||
.diff-host > .cm-mergeView {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.diff-host .cm-mergeViewEditors {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.diff-host .cm-editor {
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.changes-panel {
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
|
||||
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", () => {
|
||||
it("keeps Markdown and raw HTML as inert editor text", () => {
|
||||
render(
|
||||
@@ -18,4 +32,68 @@ describe("DiffView", () => {
|
||||
expect(document.querySelector("img")).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 });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user