Files
Mdeical_Sur_Report/工程分析/20260419_2159/实现方案.md
admin 1ec25065ad feat(ai): diff弹窗文档对比高亮 + 二次修改未弹窗修复
- 引入diff库,实现字符级差异比对
- diffModal左右两侧增加diff高亮:左侧删除内容标红,右侧新增内容标绿
- systemPrompt增加绝对强制条款:无论指令多小都必须返回updatedHtml
- 前端校验兜底:修改模式下未返回updatedHtml时在聊天面板给出提示
- confirmAiInjection注入前清理diff高亮span,避免污染编辑器
2026-04-19 22:08:05 +08:00

129 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实现方案
## 修改文件
- `src/pages/ReportEditor.tsx`
- `package.json` / `package-lock.json`(已安装 `diff` 库)
## 依赖安装
```bash
npm install diff --save
```
已完成。
## 修改 1导入 diff 库 + 增加辅助函数
`ReportEditor.tsx` 顶部 imports 区域增加:
```tsx
import { diffChars } from 'diff';
```
在组件内部增加辅助函数(建议放在 `checkAiRegions` 之后):
```tsx
const stripHtml = (html: string): string => {
const tmp = document.createElement('div');
tmp.innerHTML = html.replace(/<\/p>/gi, '</p>\n').replace(/<br\s*\/?>/gi, '\n');
return (tmp.innerText || tmp.textContent || '').trim();
};
const computeDiffHtml = (oldText: string, newText: string, side: 'left' | 'right'): string => {
const diffs = diffChars(oldText, newText);
let html = '';
for (const part of diffs) {
let value = part.value.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
if (side === 'left' && part.removed) {
html += `<span class="diff-removed" style="background-color:#fee2e2;color:#dc2626;text-decoration:line-through;">${value}</span>`;
} else if (side === 'right' && part.added) {
html += `<span class="diff-added" style="background-color:#dcfce7;color:#16a34a;font-weight:500;">${value}</span>`;
} else if (!part.added && !part.removed) {
html += value;
}
}
return html;
};
```
## 修改 2强化 systemPrompt约 line 922
在修改模式的 systemPrompt 中增加第 7 条:
```
7. ⚠️ 绝对强制:无论用户的修改指令多么微小,你都必须返回 updatedHtml。绝对不允许只返回 reply 而不返回 updatedHtml
```
## 修改 3前端校验兜底约 line 955
`if (responseJson.updatedHtml && aiModifyEnabled)` 分支之前,增加兜底提示:
```tsx
if (aiModifyEnabled && !responseJson.updatedHtml) {
setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: '【系统提示】AI 未能生成修改内容,请尝试重新描述您的需求。' }]);
}
```
## 修改 4diffModal 渲染逻辑(约 line 2612-2653
**原代码**
左侧直接渲染 `diffModal.originalHtml`,右侧直接渲染 `diffModal.newHtml`
**新代码**
`diffModal` 渲染区域内部,计算 diff 高亮 HTML
```tsx
{diffModal && diffModal.isOpen && (
<div className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-[100] flex items-center justify-center p-4">
{/* ... 弹窗头部 ... */}
{(() => {
const oldText = stripHtml(diffModal.originalHtml);
const newText = stripHtml(diffModal.newHtml);
const leftDiffHtml = computeDiffHtml(oldText, newText, 'left');
const rightDiffHtml = computeDiffHtml(oldText, newText, 'right');
return (
<div className="flex-1 overflow-hidden flex gap-4 p-6 bg-slate-100">
{/* 左侧:原始版本 + 删除高亮 */}
<div className="flex-1 flex flex-col bg-white border border-red-200 rounded-xl overflow-hidden shadow-sm">
<div className="bg-red-50 px-3 py-2 text-xs font-bold text-red-600 border-b border-red-100 uppercase tracking-wider"></div>
<div className="p-4 flex-1 overflow-y-auto opacity-70 cursor-not-allowed custom-scrollbar"
dangerouslySetInnerHTML={{ __html: leftDiffHtml }}></div>
</div>
{/* 右侧AI 版本 + 新增高亮 */}
<div className="flex-1 flex flex-col bg-white border border-green-400 rounded-xl overflow-hidden shadow-md relative">
<div className="bg-green-50 px-3 py-2 text-xs font-bold text-green-700 border-b border-green-200 uppercase tracking-wider flex justify-between">
<span>AI ()</span>
<span className="text-[10px] bg-green-200 px-1.5 py-0.5 rounded text-green-800"></span>
</div>
<div
className="p-4 flex-1 overflow-y-auto outline-none custom-scrollbar"
contentEditable
suppressContentEditableWarning
onBlur={(e) => setDiffModal(prev => prev ? { ...prev, newHtml: e.target.innerHTML } : null)}
dangerouslySetInnerHTML={{ __html: rightDiffHtml }}
style={{ fontFamily: 'SimSun, "Microsoft YaHei", serif', fontSize: '12pt', lineHeight: '1.5' }}
></div>
</div>
</div>
);
})()}
{/* ... 底部按钮 ... */}
</div>
)}
```
**注意**:使用 IIFE立即执行函数在 JSX 中计算 diff避免在渲染外额外处理。
## 修改 5confirmAiInjection 清理 diff 高亮(约 line 981
在注入前去掉 diff 高亮 span
```tsx
const confirmAiInjection = (newHtml: string, regionId: string) => {
if (!editorRef.current) return;
// 去掉 diff 高亮 span避免污染编辑器
const cleanHtml = newHtml.replace(/<span class="diff-(added|removed)"[^>]*>(.*?)<\/span>/gi, '$2');
const targetContent = editorRef.current.querySelector(`.ai-region[data-ai-id="${regionId}"] .ai-content`) as HTMLElement;
if (targetContent) {
targetContent.focus();
const sel = window.getSelection();
const range = document.createRange();
range.selectNodeContents(targetContent);
sel?.removeAllRanges();
sel?.addRange(range);
document.execCommand('insertHTML', false, cleanHtml);
// ... 后续动画和保存逻辑不变
```