diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index 81121a3..570c1af 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -883,6 +883,23 @@ export default function ReportEditor() { } } const targetRegionEl = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"] .ai-content`) as HTMLElement | null; + // 合并溢出的段落:浏览器 contentEditable 可能在回车时把

生成到 .ai-content 之外 + const aiRegion = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"]`); + if (aiRegion && targetRegionEl) { + let nextSibling = targetRegionEl.nextElementSibling; + while (nextSibling) { + const toMove = nextSibling; + nextSibling = nextSibling.nextElementSibling; + if (toMove.tagName === 'P') { + targetRegionEl.appendChild(toMove); + } + } + // 同步更新 contentRef 和草稿 + if (editorRef.current) { + contentRef.current = editorRef.current.innerHTML; + saveDraftToStorage(); + } + } const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').trim() : ''; const globalContextText = editorRef.current?.innerText || ''; let messageContent: any; diff --git a/工程分析/20260419_1805/实现方案.md b/工程分析/20260419_1805/实现方案.md new file mode 100644 index 0000000..6bed5dc --- /dev/null +++ b/工程分析/20260419_1805/实现方案.md @@ -0,0 +1,46 @@ +# 实现方案 + +## 修改文件 +- `src/pages/ReportEditor.tsx` + +## 修改位置:`handleAIGenerate` 函数内,获取 `currentHtml` 之前(约 line 885) + +**原代码**: +```tsx + const targetRegionEl = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"] .ai-content`) as HTMLElement | null; + const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').trim() : ''; +``` + +**新代码**: +```tsx + const targetRegionEl = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"] .ai-content`) as HTMLElement | null; + // 合并溢出的段落:浏览器 contentEditable 可能在回车时把

生成到 .ai-content 之外 + const aiRegion = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"]`); + if (aiRegion && targetRegionEl) { + let nextSibling = targetRegionEl.nextElementSibling; + while (nextSibling) { + const toMove = nextSibling; + nextSibling = nextSibling.nextElementSibling; + if (toMove.tagName === 'P') { + targetRegionEl.appendChild(toMove); + } + } + // 同步更新 contentRef 和草稿 + if (editorRef.current) { + contentRef.current = editorRef.current.innerHTML; + saveDraftToStorage(); + } + } + const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').trim() : ''; +``` + +**变更点**: +1. 新增 `aiRegion` 查询,获取 `.ai-region` 容器 +2. 遍历 `targetRegionEl.nextElementSibling`,把所有 `

` 标签移回 `targetRegionEl` +3. 移动完成后同步更新 `contentRef.current` 和调用 `saveDraftToStorage()` +4. 然后才获取 `currentHtml` + +**为什么这样可行**: +- `confirmAiInjection` 只替换 `.ai-content` 的 innerHTML +- 修复后 `.ai-content` 已包含所有段落,注入时自然替换全部 +- 溢出的段落不会再留在 `.ai-region` 内造成重复 diff --git a/工程分析/20260419_1805/测试方案.md b/工程分析/20260419_1805/测试方案.md new file mode 100644 index 0000000..6c806d8 --- /dev/null +++ b/工程分析/20260419_1805/测试方案.md @@ -0,0 +1,45 @@ +# 测试方案 + +## 测试环境 +- 浏览器访问 `http://localhost:4173/` +- 进入「图文报告生成」→ 新建报告 + +## 测试用例 1:溢出段落自动合并 + +**前置条件**: +手动构造一个 DOM 结构被「破坏」的 AI 区域(模拟用户回车导致段落溢出): +1. 插入 AI 可编辑区域「手术步骤」 +2. 在区域内输入第 2 段内容 +3. 在区域末尾按回车,输入第 3、4、5 段(观察 DOM,确认 `

` 标签变成了 `.ai-content` 的兄弟节点) + +**步骤**: +1. 勾选「允许修改正文」→ 选中「手术步骤」区域 +2. 发送「请完善手术步骤描述」 + +**预期结果**: +- diff 弹窗左侧「原始版本」应显示全部 2-5 段内容,而不只是第 2 段 +- diff 弹窗右侧「AI 提议版本」也显示完整的多段落内容 +- 确认注入后,编辑器中 AI 区域的全部内容被替换,没有重复段落 + +## 测试用例 2:正常结构不受影响 + +**前置条件**: +AI 可编辑区域内的所有段落都在 `.ai-content` 内部(正常结构)。 + +**步骤**: +1. 发送修改指令 + +**预期结果**: +- diff 弹窗左侧正常显示所有段落 +- 合并逻辑不会误删或误移任何内容 + +## 测试用例 3:编译与部署 + +**步骤**: +1. 执行 `npm run build` +2. 确认无 TypeScript 编译错误 +3. 预览服务正常启动并返回 200 + +**预期结果**: +- `vite build` 成功完成 +- 预览页面可正常访问 diff --git a/工程分析/20260419_1805/需求分析.md b/工程分析/20260419_1805/需求分析.md new file mode 100644 index 0000000..9e852f4 --- /dev/null +++ b/工程分析/20260419_1805/需求分析.md @@ -0,0 +1,40 @@ +# 需求分析 + +## 时间戳 +2026-04-19 18:05 + +## 需求来源 +用户发现 AI 修改确认弹窗的「原始版本」左侧只显示了一段内容,而编辑器中的 AI 可编辑区域实际上有 2-5 段内容。 + +## 现象 +从用户提供的 DOM 源码可以清楚看到: + +```html +

+

2.腹腔镜探查:...

+
+

3.切除胆囊:...

+

4.检查腹腔内无活动性出血及漏胆后:...

+

5.手术顺利,麻醉满意:...

+``` + +- `.ai-content` 内只有第 2 段 +- 第 3、4、5 段变成了 `.ai-content` 的**兄弟节点**(在 `.ai-region` 内但在 `.ai-content` 外) +- 左侧 diff 弹窗只显示 `.ai-content.innerHTML`,所以只有第 2 段 + +## 根因分析 + +**浏览器 contentEditable 机制的锅**: +当用户在 `.ai-content`(一个 contentEditable 的 div)内按回车换行,或粘贴包含多个段落的外部文本时,浏览器的默认行为会截断当前的 `div`,在同级生成新的 `

` 标签。这导致后续的段落脱离了 `.ai-content` 父容器,变成了 `.ai-region` 的直接子节点。 + +## 解决方向 + +**代码层面修复**:在 `handleAIGenerate` 获取 `currentHtml` 之前,自动把 `.ai-region` 内 `.ai-content` 之外的 `

` 节点移回 `.ai-content`。这样: +1. `currentHtml` 就能包含所有段落 +2. 左侧 diff 弹窗显示全部内容 +3. `confirmAiInjection` 注入时替换 `.ai-content`,此时 `.ai-content` 已包含所有段落 + +## 约束条件 +- 只移动 `.ai-content` 之后的 `

` 节点,不移动 `.ai-region-label` 或其他元素 +- 移动后要同步更新 `contentRef.current` 和 `saveDraftToStorage()` +- 不改变现有 diff 弹窗和注入逻辑 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 0b35de5..5257fd2 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -774,3 +774,46 @@ AI 修改确认弹窗右侧出现了不属于目标区域的内容:术后情 **D. 后续如何避免问题** - 在向大模型发送局部修改请求时,**必须设置严格的内容边界(Fencing)**。全局上下文可以提供给 AI 作为背景理解,但必须在 Prompt 中明确声明"仅供理解,严禁输出"。 - 避免使用"补充完善""基于全局信息扩展"等容易被大模型过度解读的措辞。大模型会尽其所能地"满足"用户的指令,即使这意味着越界生成。 + + +--- + +## 记录 34:contentEditable 回车导致段落溢出 .ai-content + +**A. 具体问题** +AI 修改确认弹窗的「原始版本」左侧只显示了 AI 可编辑区域中的一段内容,但编辑器中该区域实际上有 2-5 段。从 DOM 源码可以看到: +```html +

第2段

+

第3段

+

第4段

+

第5段

+``` +第 3-5 段变成了 `.ai-content` 的兄弟节点,不在 `.ai-content` 内部。 + +**B. 产生问题原因** +浏览器原生 `contentEditable` 机制在用户按回车换行时,会截断当前的块级容器(`.ai-content` div),在同级生成新的 `

` 标签。这导致后续段落脱离了 `.ai-content` 父容器,变成了 `.ai-region` 的直接子节点。 + +**C. 解决问题方案** +在 `handleAIGenerate` 获取 `currentHtml` 之前,增加溢出段落合并逻辑: +```ts +const aiRegion = editorRef.current?.querySelector(`.ai-region[data-ai-id="${actualTargetId}"]`); +if (aiRegion && targetRegionEl) { + let nextSibling = targetRegionEl.nextElementSibling; + while (nextSibling) { + const toMove = nextSibling; + nextSibling = nextSibling.nextElementSibling; + if (toMove.tagName === 'P') { + targetRegionEl.appendChild(toMove); + } + } + if (editorRef.current) { + contentRef.current = editorRef.current.innerHTML; + saveDraftToStorage(); + } +} +``` +遍历 `.ai-content` 之后的所有兄弟节点,把 `

` 标签移回 `.ai-content` 内,然后同步更新 contentRef 和草稿。 + +**D. 后续如何避免问题** +- `contentEditable` 中的嵌套容器(如 `.ai-content`)在用户输入时极易被浏览器原生编辑行为破坏结构。任何依赖特定 DOM 层级关系的功能,都必须在读取数据前做**结构完整性检查和修复**。 +- 对于 AI 区域这类核心功能,应考虑在编辑器层面增加 `keydown`/`paste` 事件拦截,或改用更可控的编辑方案(如 ProseMirror/Slate)来替代原生 `contentEditable`。