diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index e9205e4..1327bef 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -894,8 +894,8 @@ export default function ReportEditor() { } else { messageContent = promptText; } - const systemPrompt = aiModifyEnabled && targetRegionEl - ? '你是一名专业的外科医生助理。我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写并输出目标区域的 HTML。\n重要指令:你必须严格返回合法的 JSON 对象,绝对不要包含任何 Markdown 标记(如 ```json)。\nJSON 格式如下:\n{ "reply": "简短的回复话术", "updatedHtml": "修改后的完整内部 HTML 代码" }' + const systemPrompt = aiModifyEnabled + ? '你是一名专业的外科医生助理。我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写或生成目标区域的 HTML。\n重要指令:你必须严格返回合法的 JSON 对象,绝对不要包含任何 Markdown 标记(如 ```json)。\nJSON 格式如下:\n{ "reply": "简短的回复话术", "updatedHtml": "修改后的完整内部 HTML 代码" }' : '你是一名专业的外科医生助理。请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:严格返回合法的 JSON 对象。\nJSON 格式如下:\n{ "reply": "你的分析和回答" }'; const response = await fetch(`${apiEndpoint}/chat/completions`, { method: 'POST', @@ -927,13 +927,17 @@ export default function ReportEditor() { if (responseJson.reply) { setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: responseJson.reply }]); } - if (responseJson.updatedHtml && aiModifyEnabled && targetRegionEl) { - setDiffModal({ - isOpen: true, - originalHtml: currentHtml, - newHtml: responseJson.updatedHtml, - targetId: aiTargetRegion - }); + if (responseJson.updatedHtml && aiModifyEnabled) { + if (targetRegionEl) { + setDiffModal({ + isOpen: true, + originalHtml: currentHtml, + newHtml: responseJson.updatedHtml, + targetId: aiTargetRegion + }); + } else { + execCmd('insertHTML', responseJson.updatedHtml); + } } setAiUploadedImages([]); setAiSelectedFrames([]); diff --git a/工程分析/20260419_0344/实现方案.md b/工程分析/20260419_0344/实现方案.md new file mode 100644 index 0000000..cc7a553 --- /dev/null +++ b/工程分析/20260419_0344/实现方案.md @@ -0,0 +1,66 @@ +# 实现方案 + +## 修改文件 +- `src/pages/ReportEditor.tsx` + +## 修改位置:`handleAIGenerate` 函数(约 line 897-937) + +### 修改 1:System Prompt 条件解绑 + +**原代码**(line 897-899): +```tsx +const systemPrompt = aiModifyEnabled && targetRegionEl + ? '你是一名专业的外科医生助理。我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写并输出目标区域的 HTML。\n重要指令:你必须严格返回合法的 JSON 对象,绝对不要包含任何 Markdown 标记(如 ```json)。\nJSON 格式如下:\n{ "reply": "简短的回复话术", "updatedHtml": "修改后的完整内部 HTML 代码" }' + : '你是一名专业的外科医生助理。请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:严格返回合法的 JSON 对象。\nJSON 格式如下:\n{ "reply": "你的分析和回答" }'; +``` + +**新代码**: +```tsx +const systemPrompt = aiModifyEnabled + ? '你是一名专业的外科医生助理。我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据全局内容和用户的【医生指令】,直接重写或生成目标区域的 HTML。\n重要指令:你必须严格返回合法的 JSON 对象,绝对不要包含任何 Markdown 标记(如 ```json)。\nJSON 格式如下:\n{ "reply": "简短的回复话术", "updatedHtml": "修改后的完整内部 HTML 代码" }' + : '你是一名专业的外科医生助理。请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:严格返回合法的 JSON 对象。\nJSON 格式如下:\n{ "reply": "你的分析和回答" }'; +``` + +**变更点**:条件从 `aiModifyEnabled && targetRegionEl` 改为 `aiModifyEnabled`,解绑对目标区域存在的强依赖。 + +### 修改 2:接收逻辑增加降级插入 + +**原代码**(line 930-937): +```tsx +if (responseJson.updatedHtml && aiModifyEnabled && targetRegionEl) { + setDiffModal({ + isOpen: true, + originalHtml: currentHtml, + newHtml: responseJson.updatedHtml, + targetId: aiTargetRegion + }); +} +``` + +**新代码**: +```tsx +if (responseJson.updatedHtml && aiModifyEnabled) { + if (targetRegionEl) { + setDiffModal({ + isOpen: true, + originalHtml: currentHtml, + newHtml: responseJson.updatedHtml, + targetId: aiTargetRegion + }); + } else { + execCmd('insertHTML', responseJson.updatedHtml); + } +} +``` + +**变更点**: +1. 接收条件从 `responseJson.updatedHtml && aiModifyEnabled && targetRegionEl` 放宽为 `responseJson.updatedHtml && aiModifyEnabled` +2. 增加 `if/else` 分支: + - `targetRegionEl` 存在 → 走 diff 弹窗(原有流程) + - `targetRegionEl` 不存在 → 调用 `execCmd('insertHTML', ...)` 直接插入光标位置(参考代码降级逻辑) +3. `execCmd` 为当前代码已存在的辅助函数,会自动处理 focus、execCommand、contentRef 更新和草稿保存 + +## 不修改的内容 +- `aiTargetRegion` 默认值 `'none'` 保持不变 +- `promptText` 构建逻辑保持不变(全局上下文 + 目标区域源码 + 医生指令) +- Diff 弹窗和 `confirmAiInjection` 保持不变 diff --git a/工程分析/20260419_0344/测试方案.md b/工程分析/20260419_0344/测试方案.md new file mode 100644 index 0000000..b19a6b5 --- /dev/null +++ b/工程分析/20260419_0344/测试方案.md @@ -0,0 +1,54 @@ +# 测试方案 + +## 测试环境 +- 浏览器访问 `http://localhost:4173/` +- 进入「图文报告生成」→ 新建报告 + +## 测试用例 1:开启修改模式 + 选中目标区域 → Diff 弹窗 + +**步骤**: +1. 编辑器中插入一个 AI 可编辑区域(如「手术步骤」) +2. AI 面板底部勾选「允许修改正文」 +3. 下拉框选中「手术步骤」区域 +4. 输入「请随机填充文本内容」并发送 + +**预期结果**: +- AI 聊天面板有回复 +- 弹出 diff 确认弹窗,展示原文 vs AI 修改后的 HTML +- 点击确认后,目标区域内容更新 + +## 测试用例 2:开启修改模式 + 未选中目标区域 → 光标插入 + +**步骤**: +1. 编辑器中不插入任何 AI 可编辑区域(或保持下拉框为「无可用 AI 区域」/不选) +2. AI 面板底部勾选「允许修改正文」 +3. 在编辑器正文处点击放置光标 +4. 输入「请随机填充文本内容」并发送 + +**预期结果**: +- AI 聊天面板有回复 +- **不弹出 diff 弹窗** +- AI 生成的内容直接插入到编辑器当前光标位置 +- 编辑器内容自动保存到草稿 + +## 测试用例 3:关闭修改模式 → 纯聊天 + +**步骤**: +1. AI 面板底部取消勾选「允许修改正文」 +2. 输入「请随机填充文本内容」并发送 + +**预期结果**: +- AI 聊天面板有回复 +- 编辑器内容不发生任何变化 +- 不弹出 diff 弹窗 + +## 测试用例 4:编译与部署 + +**步骤**: +1. 执行 `npm run build` +2. 确认无 TypeScript 编译错误 +3. 预览服务正常启动并返回 200 + +**预期结果**: +- `vite build` 成功完成 +- 预览页面可正常访问 diff --git a/工程分析/20260419_0344/需求分析.md b/工程分析/20260419_0344/需求分析.md new file mode 100644 index 0000000..4524c79 --- /dev/null +++ b/工程分析/20260419_0344/需求分析.md @@ -0,0 +1,53 @@ +# 需求分析 + +## 时间戳 +2026-04-19 03:44 + +## 需求来源 +用户在 report-editor 中输入「请随机填充文本内容」,AI 在聊天气泡中正常回复,但编辑器中的「手术步骤-AI可编辑部分」没有修改。 + +## 现象 +1. AI 聊天面板有输出(reply) +2. 编辑器中的 AI 可编辑区域内容未更新 +3. 没有触发 diff 确认弹窗 + +## 根因分析 + +对比当前代码与 `参考-ReportEditor.tsx`,问题有两个层面: + +### 层面 1:System Prompt 条件过于严苛 +当前代码: +```tsx +const systemPrompt = aiModifyEnabled && targetRegionEl ? '...修改HTML...' : '...纯聊天...' +``` +`targetRegionEl` 的获取依赖 `aiTargetRegion`(默认 `'none'`)。如果用户未在下拉框中选中具体区域,`targetRegionEl` 为 `null`,systemPrompt 降级为纯聊天模式,大模型根本不会返回 `updatedHtml` 字段。 + +参考代码: +```tsx +const systemPrompt = aiModifyEnabled ? '...修改HTML...' : '...纯聊天...' +``` +只依赖 `aiModifyEnabled`,只要用户开启「允许修改正文」,大模型就会返回 `updatedHtml`。 + +### 层面 2:接收逻辑缺少降级插入 +当前代码: +```tsx +if (responseJson.updatedHtml && aiModifyEnabled && targetRegionEl) { + setDiffModal({...}) +} +``` +同样因为 `targetRegionEl` 为 `null`,即使大模型返回了 `updatedHtml`,也会被丢弃。 + +参考代码 `injectAIText`: +```tsx +if (targetRegion) { + targetRegion.innerHTML = ... // 注入目标区域 +} else { + execCmd('insertHTML', htmlContent); // 降级:插入光标位置 +} +``` +找不到目标区域时,直接将内容插入编辑器当前光标位置。 + +## 约束条件 +- 当前代码已有 `execCmd` 辅助函数,可直接复用 +- 需要保留 diff 弹窗流程(目标区域存在时) +- `aiTargetRegion` 默认值 `'none'` 可保留,但需通过降级逻辑补偿 diff --git a/工程分析/经验记录.md b/工程分析/经验记录.md index 84bfe81..2a64c21 100644 --- a/工程分析/经验记录.md +++ b/工程分析/经验记录.md @@ -650,3 +650,34 @@ ReportEditor 采用 `useRef` 作为自动保存的数据快照机制(避免 Re **D. 后续如何避免问题** - 在复杂 React 组件(尤其是与 contentEditable 共存)中使用 Checkbox 时,**优先使用 `