feat(ux): R1+R2 审稿裁决人体学 — 冲突卡内联片段 + 分组/排序/跳下一条/主次按钮
R1: 新 lib/review/snippet.ts 从 where 解析段号取终稿命中段预览,ConflictCard 渲染引用块。 R2: 新 lib/review/grouping.ts 按 type 分组+严重度排序(仅改显示顺序,保留原始 index 守 conflict_index 不变量),组计数徽标、跳到下一条未裁决(环回)、采纳改法主按钮/忽略手改次级。 TDD: snippet 8 测 + grouping 10 测。前端门禁绿: lint/tsc/vitest 201/build。
This commit is contained in:
27
apps/web/lib/review/snippet.ts
Normal file
27
apps/web/lib/review/snippet.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
// R1 · 冲突卡内联上下文片段(UX §6.4)。
|
||||
// `where` 是 LLM 给的文字定位(如「第 3 段,主角忽然示弱」),M2 无精确字符 offset。
|
||||
// 这里按段级回放:从 where 解析 1-based 段号 → 取终稿对应段 → 截断成预览。
|
||||
// 纯逻辑,与 ReviewReport.segmentText 同口径(空行切段),便于 node 环境单测。
|
||||
|
||||
// 预览窗口最大字符数(命中段超长时截断,避免撑爆卡片)。
|
||||
const PREVIEW_MAX = 80;
|
||||
|
||||
// 从 where 文案解析 1-based 段号(「第 3 段」→ 3;范围「第 4–6 段」取首个)。无则 null。
|
||||
export function parseParagraphNo(where: string): number | null {
|
||||
const match = where.match(/第\s*(\d+)/);
|
||||
if (!match) return null;
|
||||
const n = Number(match[1]);
|
||||
return Number.isInteger(n) && n > 0 ? n : null;
|
||||
}
|
||||
|
||||
// 取终稿第 N 段(1-based)正文预览;无段号/越界/空段 → null(卡片回退到「跳转」按钮)。
|
||||
export function conflictSnippet(finalText: string, where: string): string | null {
|
||||
const no = parseParagraphNo(where);
|
||||
if (no === null) return null;
|
||||
const paragraphs = finalText.split(/\n{2,}/);
|
||||
const paragraph = paragraphs[no - 1]?.trim();
|
||||
if (!paragraph) return null;
|
||||
return paragraph.length <= PREVIEW_MAX
|
||||
? paragraph
|
||||
: `${paragraph.slice(0, PREVIEW_MAX)}…`;
|
||||
}
|
||||
Reference in New Issue
Block a user