修复评审器跨组件修改项跳转丢失并展开完整文档视图
- 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/
|
artifacts/
|
||||||
outputs/
|
outputs/
|
||||||
experiments/
|
experiments/
|
||||||
|
research-wiki/scratch/
|
||||||
*.cleaned.md
|
*.cleaned.md
|
||||||
report.json
|
report.json
|
||||||
|
|
||||||
|
|||||||
@@ -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 个组件阶段;
|
||||||
|
|||||||
@@ -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}对比`}>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user