fix(editor): AI只聊天不修改——解绑SystemPrompt目标区域依赖 + 增加光标插入降级

- systemPrompt条件从'aiModifyEnabled && targetRegionEl'改为'aiModifyEnabled',确保开启修改模式后大模型始终返回updatedHtml
- 接收updatedHtml逻辑增加if/else分支:targetRegionEl存在时走diff弹窗,不存在时调用execCmd('insertHTML')降级插入光标位置
- 参考参考-ReportEditor.tsx中injectAIText的降级机制
This commit is contained in:
2026-04-19 03:47:14 +08:00
parent 854a00c2fa
commit c1d2438d2b
5 changed files with 217 additions and 9 deletions

View File

@@ -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) {
if (responseJson.updatedHtml && aiModifyEnabled) {
if (targetRegionEl) {
setDiffModal({
isOpen: true,
originalHtml: currentHtml,
newHtml: responseJson.updatedHtml,
targetId: aiTargetRegion
});
} else {
execCmd('insertHTML', responseJson.updatedHtml);
}
}
setAiUploadedImages([]);
setAiSelectedFrames([]);

View File

@@ -0,0 +1,66 @@
# 实现方案
## 修改文件
- `src/pages/ReportEditor.tsx`
## 修改位置:`handleAIGenerate` 函数(约 line 897-937
### 修改 1System 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` 保持不变

View File

@@ -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` 成功完成
- 预览页面可正常访问

View File

@@ -0,0 +1,53 @@
# 需求分析
## 时间戳
2026-04-19 03:44
## 需求来源
用户在 report-editor 中输入「请随机填充文本内容」AI 在聊天气泡中正常回复,但编辑器中的「手术步骤-AI可编辑部分」没有修改。
## 现象
1. AI 聊天面板有输出reply
2. 编辑器中的 AI 可编辑区域内容未更新
3. 没有触发 diff 确认弹窗
## 根因分析
对比当前代码与 `参考-ReportEditor.tsx`,问题有两个层面:
### 层面 1System 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'` 可保留,但需通过降级逻辑补偿

View File

@@ -650,3 +650,34 @@ ReportEditor 采用 `useRef` 作为自动保存的数据快照机制(避免 Re
**D. 后续如何避免问题**
- 在复杂 React 组件(尤其是与 contentEditable 共存)中使用 Checkbox 时,**优先使用 `<label>` 直接包裹 `<input>`** 的写法,避免依赖 `id`/`htmlFor`。
- 向大模型发送局部修改请求时,**必须同时提供全局上下文**,否则 AI 无法基于文档其他部分的信息进行推理和修改。
---
## 记录 30AI「只聊天不干活」——System Prompt 过度依赖目标区域 + 缺少降级插入
**A. 具体问题**
用户在 report-editor 中输入「请随机填充文本内容」AI 聊天面板有输出,但编辑器中的 AI 可编辑区域没有任何更新。
**B. 产生问题原因**
1. **System Prompt 条件过于严苛**`systemPrompt` 的构建条件是 `aiModifyEnabled && targetRegionEl`。由于 `aiTargetRegion` 默认值为 `'none'`,如果用户未在下拉框中明确选中区域,`targetRegionEl` 为 `null`systemPrompt 降级为纯聊天模式,大模型根本不会返回 `updatedHtml` 字段。
2. **接收逻辑缺少降级**`responseJson.updatedHtml` 的接收条件是 `aiModifyEnabled && targetRegionEl`,同样因为 `targetRegionEl` 为 `null` 而被跳过。即使大模型返回了 HTML也会被丢弃。
3. **缺少光标插入降级**:参考代码 `injectAIText` 中,当找不到目标区域时,会调用 `execCmd('insertHTML', htmlContent)` 将内容直接插入当前光标位置。当前代码完全没有这种降级机制。
**C. 解决问题方案**
1. **解绑 System Prompt**:将条件从 `aiModifyEnabled && targetRegionEl` 改为 `aiModifyEnabled`,让大模型在「允许修改正文」开启时始终返回 `updatedHtml`。
2. **增加降级插入逻辑**
```ts
if (responseJson.updatedHtml && aiModifyEnabled) {
if (targetRegionEl) {
setDiffModal({...}); // 原有流程:目标区域存在时走 diff 弹窗
} else {
execCmd('insertHTML', responseJson.updatedHtml); // 降级:插入光标位置
}
}
```
3. 复用当前代码已存在的 `execCmd` 辅助函数,自动处理 focus、contentRef 更新和草稿保存。
**D. 后续如何避免问题**
- 设计「修改/生成」类 AI 功能时,**systemPrompt 的条件应尽量只依赖用户意图开关**(如 `aiModifyEnabled`),而非依赖具体 UI 状态如某个下拉框是否选中。UI 状态应只影响「如何注入结果」,不应影响「是否要求模型生成结果」。
- 任何「目标区域注入」逻辑都必须配备**降级方案**(如光标处插入),防止因用户未选中区域而导致功能完全失效。