From c7e7033e7d6ed714de62bd0e32ee3e7f672a4c84 Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sun, 19 Apr 2026 04:15:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(ai):=20diff=E5=BC=B9=E7=AA=97=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=AE=8C=E6=95=B4=E6=80=A7=E4=BC=98=E5=8C=96=20+=20HT?= =?UTF-8?q?ML=E7=A9=BA=E8=A1=8C=E6=B8=85=E6=B4=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - systemPrompt和promptText中明确要求AI生成完整多段落内容,不要只改写现有段落 - systemPrompt增加HTML格式约束:

标签包裹、禁止
和换行符、紧凑HTML - setDiffModal和execCmd之前增加正则清洗:移除
、移除

间空白、trim首尾 --- src/pages/ReportEditor.tsx | 12 ++-- 工程分析/20260419_0413/实现方案.md | 101 +++++++++++++++++++++++++++++ 工程分析/20260419_0413/测试方案.md | 52 +++++++++++++++ 工程分析/20260419_0413/需求分析.md | 39 +++++++++++ 工程分析/经验记录.md | 31 +++++++++ 5 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 工程分析/20260419_0413/实现方案.md create mode 100644 工程分析/20260419_0413/测试方案.md create mode 100644 工程分析/20260419_0413/需求分析.md diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index 2cb0c86..3bcb59b 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -892,7 +892,7 @@ export default function ReportEditor() { if (aiModifyEnabled && targetRegionEl) { promptText += `【你需要进行修改的目标区域 HTML 源码】:\n${currentHtml || '(当前区域为空)'}\n\n`; } - promptText += `【医生指令】: ${text}`; + promptText += `【医生指令】: ${text}\n\n【格式要求】:\n1. 生成完整、结构化的多段落 HTML 内容,不要只改写现有段落\n2. 段落使用

标签,段落之间不要使用
标签或换行符\n3. 输出紧凑 HTML,标签间不要有空格或换行`; if (allImages.length > 0) { messageContent = []; allImages.forEach(url => { @@ -903,7 +903,7 @@ export default function ReportEditor() { messageContent = promptText; } const systemPrompt = aiModifyEnabled - ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写并输出目标区域的 HTML。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复话术)和 "updatedHtml"(修改后的完整内部 HTML 代码)两个字段\n3. 绝对不要包含任何 Markdown 标记(如 ```json)' + ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写并输出目标区域的 HTML。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复话术)和 "updatedHtml"(修改后的完整内部 HTML 代码)两个字段\n3. updatedHtml 必须生成完整、结构化的多段落内容,不要只改写现有段落,要基于全局信息补充完善\n4. 段落必须使用

标签包裹,段落之间绝对不要使用
标签,也不要使用任何换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json)' : '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记'; const response = await fetch(`${apiEndpoint}/chat/completions`, { method: 'POST', @@ -936,15 +936,19 @@ export default function ReportEditor() { setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: responseJson.reply }]); } if (responseJson.updatedHtml && aiModifyEnabled) { + let cleanHtml = responseJson.updatedHtml; + cleanHtml = cleanHtml.replace(//gi, ''); + cleanHtml = cleanHtml.replace(/<\/p>\s*

/gi, '

'); + cleanHtml = cleanHtml.trim(); if (targetRegionEl) { setDiffModal({ isOpen: true, originalHtml: currentHtml, - newHtml: responseJson.updatedHtml, + newHtml: cleanHtml, targetId: actualTargetId }); } else { - execCmd('insertHTML', responseJson.updatedHtml); + execCmd('insertHTML', cleanHtml); } } setAiUploadedImages([]); diff --git a/工程分析/20260419_0413/实现方案.md b/工程分析/20260419_0413/实现方案.md new file mode 100644 index 0000000..310b5d3 --- /dev/null +++ b/工程分析/20260419_0413/实现方案.md @@ -0,0 +1,101 @@ +# 实现方案 + +## 修改文件 +- `src/pages/ReportEditor.tsx` + +## 修改位置 1:System Prompt 强化(约 line 905-907) + +在 systemPrompt 中增加对「生成完整性」和「HTML 紧凑性」的明确要求。 + +**原代码**: +```tsx + const systemPrompt = aiModifyEnabled + ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写并输出目标区域的 HTML。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复话术)和 "updatedHtml"(修改后的完整内部 HTML 代码)两个字段\n3. 绝对不要包含任何 Markdown 标记(如 ```json)' + : '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记'; +``` + +**新代码**: +```tsx + const systemPrompt = aiModifyEnabled + ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写并输出目标区域的 HTML。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复话术)和 "updatedHtml"(修改后的完整内部 HTML 代码)两个字段\n3. updatedHtml 必须生成完整、结构化的多段落内容,不要只改写现有段落,要基于全局信息补充完善\n4. 段落必须使用

标签包裹,段落之间绝对不要使用
标签,也不要使用任何换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json)' + : '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记'; +``` + +**变更点**: +1. 修改模式 systemPrompt 增加第 3 点:明确要求生成完整、结构化的多段落内容,基于全局信息补充完善 +2. 修改模式 systemPrompt 增加第 4 点:段落必须用 `

` 包裹,禁止 `
` 和 `\n` +3. 修改模式 systemPrompt 增加第 5 点:HTML 必须紧凑,标签间不要有空格或换行 + +## 修改位置 2:Prompt 文本强化(约 line 891-895) + +在 `promptText` 中增加对「生成完整性」的强调。 + +**原代码**: +```tsx + let promptText = `【全局手术报告参考内容】:\n${globalContextText}\n\n`; + if (aiModifyEnabled && targetRegionEl) { + promptText += `【你需要进行修改的目标区域 HTML 源码】:\n${currentHtml || '(当前区域为空)'}\n\n`; + } + promptText += `【医生指令】: ${text}`; +``` + +**新代码**: +```tsx + let promptText = `【全局手术报告参考内容】:\n${globalContextText}\n\n`; + if (aiModifyEnabled && targetRegionEl) { + promptText += `【你需要进行修改的目标区域 HTML 源码】:\n${currentHtml || '(当前区域为空)'}\n\n`; + } + promptText += `【医生指令】: ${text}\n\n【格式要求】:\n1. 生成完整、结构化的多段落 HTML 内容,不要只改写现有段落\n2. 段落使用

标签,段落之间不要使用
标签或换行符\n3. 输出紧凑 HTML,标签间不要有空格或换行`; +``` + +**变更点**: +1. 在医生指令后追加「格式要求」段落 +2. 明确要求生成完整多段落内容,不要只改写现有段落 +3. 强调 `

` 标签、禁止 `
`、禁止换行符、紧凑 HTML + +## 修改位置 3:HTML 清洗后处理(约 line 938-945) + +在 `setDiffModal` 之前对 `updatedHtml` 进行正则清洗。 + +**原代码**: +```tsx + if (responseJson.updatedHtml && aiModifyEnabled) { + if (targetRegionEl) { + setDiffModal({ + isOpen: true, + originalHtml: currentHtml, + newHtml: responseJson.updatedHtml, + targetId: actualTargetId + }); + } else { + execCmd('insertHTML', responseJson.updatedHtml); + } + } +``` + +**新代码**: +```tsx + if (responseJson.updatedHtml && aiModifyEnabled) { + let cleanHtml = responseJson.updatedHtml; + cleanHtml = cleanHtml.replace(//gi, ''); + cleanHtml = cleanHtml.replace(/<\/p>\s*

/gi, '

'); + cleanHtml = cleanHtml.trim(); + if (targetRegionEl) { + setDiffModal({ + isOpen: true, + originalHtml: currentHtml, + newHtml: cleanHtml, + targetId: actualTargetId + }); + } else { + execCmd('insertHTML', cleanHtml); + } + } +``` + +**变更点**: +1. 新增 `cleanHtml` 变量,初始值为 `responseJson.updatedHtml` +2. 移除 `
` 标签(不区分大小写) +3. 移除 `

` 和 `

` 之间的空白字符(空格、换行、回车) +4. 移除首尾空白 +5. `setDiffModal` 和 `execCmd` 均使用 `cleanHtml` diff --git a/工程分析/20260419_0413/测试方案.md b/工程分析/20260419_0413/测试方案.md new file mode 100644 index 0000000..a068da5 --- /dev/null +++ b/工程分析/20260419_0413/测试方案.md @@ -0,0 +1,52 @@ +# 测试方案 + +## 测试环境 +- 浏览器访问 `http://localhost:4173/` +- 进入「图文报告生成」→ 新建报告 + +## 测试用例 1:AI 生成完整多段落内容 + +**步骤**: +1. 编辑器中插入一个 AI 可编辑区域 +2. 在区域中只写一句话(如「建立气腹」) +3. 在编辑器其他位置写入完整的手术报告信息(患者信息、其他步骤等) +4. 勾选「允许修改正文」→ 发送「请完善手术步骤描述」 + +**预期结果**: +- diff 弹窗左侧显示原有的一句话 +- diff 弹窗右侧显示 AI 生成的完整多段落内容(包含多个 `

` 标签) +- 内容应基于全局报告信息补充完善,不只是改写原有的一句话 + +## 测试用例 2:右侧无多余空行 + +**步骤**: +1. 编辑器中插入 AI 可编辑区域,写入多段内容(如 3 个 `

` 段落) +2. 勾选「允许修改正文」→ 发送「请润色这段内容」 +3. 观察 diff 弹窗左右两侧的段落间距 + +**预期结果**: +- 左右两侧段落间距一致 +- 右侧 AI 版本不应出现额外的空行或 `
` +- 段落间仅由 `

` 标签的自然 margin 分隔 + +## 测试用例 3:HTML 清洗兜底 + +**步骤**: +1. 触发 AI 修改,在浏览器 DevTools 中查看 `responseJson.updatedHtml` 原始值 +2. 确认原始值中可能包含 `
` 或 `\n` +3. 观察 diff 弹窗右侧最终渲染结果 + +**预期结果**: +- 即使原始返回值包含 `
` 或 `\n`,diff 弹窗右侧也不应显示多余空行 +- 清洗后的 HTML 结构紧凑 + +## 测试用例 4:编译与部署 + +**步骤**: +1. 执行 `npm run build` +2. 确认无 TypeScript 编译错误 +3. 预览服务正常启动并返回 200 + +**预期结果**: +- `vite build` 成功完成 +- 预览页面可正常访问 diff --git a/工程分析/20260419_0413/需求分析.md b/工程分析/20260419_0413/需求分析.md new file mode 100644 index 0000000..8140dbb --- /dev/null +++ b/工程分析/20260419_0413/需求分析.md @@ -0,0 +1,39 @@ +# 需求分析 + +## 时间戳 +2026-04-19 04:13 + +## 需求来源 +用户在使用 AI 修改确认弹窗时遇到两个问题: +1. diff 弹窗左侧原始版本只显示一段内容,希望 AI 能一次性把全部内容都修改好 +2. diff 弹窗右侧 AI 提议版本的段落间有多余空行,希望结构和左侧保持一致 + +## 问题 1:左侧只显示一段 + +**现象**:diff 弹窗左侧「原始版本」只展示了一段文本。 + +**根因分析**: +- `currentHtml` 取自 `.ai-content` 的 `innerHTML`,如果 `.ai-content` 内确实只有一段内容,左侧自然只显示一段 +- 这不是代码 Bug,而是当前 AI 区域的内容组织问题 +- 但用户期望 AI 能基于全局上下文生成更丰富、更完整的内容,而不是仅改写当前已有的段落 + +**解决方向**: +- 在 systemPrompt 和 prompt 中明确要求 AI 生成完整的、结构化的多段落内容 +- 强调 AI 应基于全局参考内容补充和完善目标区域,而不是仅做局部改写 + +## 问题 2:右侧多余空行 + +**现象**:diff 弹窗右侧「AI 提议版本」的段落间出现额外空行,与左侧结构不一致。 + +**根因分析**: +1. 大模型返回的 HTML 中可能包含 `
` 标签或 `\n` 换行符 +2. `

\n

` 中的换行符会被浏览器解析为文本节点,产生额外空白 +3. 不同 LLM 的输出格式随机性导致 HTML 结构不统一 + +**解决方向**: +1. **输入端控制**:在 systemPrompt 中明确要求紧凑 HTML(禁止 `
`、禁止换行符) +2. **输出端兜底**:在 `setDiffModal` 之前对 `updatedHtml` 进行正则清洗,移除多余空行和 `
` + +## 约束条件 +- 保持现有 diff 弹窗的左右分栏结构不变 +- 清洗逻辑不应破坏合法的 HTML 结构(如 `

` 标签内的内容) diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 3d1db37..63f5427 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -718,3 +718,34 @@ ReportEditor 采用 `useRef` 作为自动保存的数据快照机制(避免 Re **D. 后续如何避免问题** - 当功能存在「全局开关 + 局部选择器」两层控制时,**全局开关开启后应自动兜底局部选择器**,避免因用户遗漏局部配置而导致功能降级。 - System Prompt 中应显式标注当前模式名称(如「修改模式」「对话模式」),大模型对显式标签的遵循度远高于隐式条件推断。 + + +--- + +## 记录 32:AI diff 弹窗内容不完整 + 右侧多余空行 + +**A. 具体问题** +1. AI 修改确认弹窗左侧原始版本只显示一段内容,用户希望 AI 能一次性生成完整的多段落内容。 +2. diff 弹窗右侧 AI 提议版本的段落间出现额外空行,与左侧结构不一致。 + +**B. 产生问题原因** +1. **内容不完整**:大模型被给予的目标区域源码(`currentHtml`)可能只有一段,且 systemPrompt 没有明确要求「生成完整、结构化的多段落内容」,导致 AI 只做局部改写。 +2. **多余空行**:大模型返回的 HTML 中常包含 `
` 标签或 `\n` 换行符。`

\n

` 中的换行符会被浏览器解析为文本节点,产生额外空白。 + +**C. 解决问题方案** +1. **输入端控制(System Prompt + Prompt)**: + - systemPrompt 增加明确要求:`updatedHtml 必须生成完整、结构化的多段落内容,不要只改写现有段落` + - systemPrompt 增加 HTML 格式约束:`段落必须使用

标签包裹,段落之间绝对不要使用
标签,也不要使用任何换行符` + - promptText 末尾追加「格式要求」段落,再次强调完整多段落、`

` 标签、禁止 `
`、紧凑 HTML +2. **输出端兜底(正则清洗)**: + ```ts + let cleanHtml = responseJson.updatedHtml; + cleanHtml = cleanHtml.replace(//gi, ''); + cleanHtml = cleanHtml.replace(/<\/p>\s*

/gi, '

'); + cleanHtml = cleanHtml.trim(); + ``` + 在 `setDiffModal` 和 `execCmd` 之前统一清洗,确保右侧渲染结构与左侧一致。 + +**D. 后续如何避免问题** +- 当大模型返回的 HTML 需要在前端渲染时,**必须同时在输入端(prompt)和输出端(后处理)进行格式约束**,单靠一端无法完全控制不同 LLM 的输出随机性。 +- 对于「生成完整性」类需求,必须在 prompt 中明确使用「必须生成完整...」「不要只改写...」等强制性措辞,否则大模型倾向于做最小化修改。