feat(ai): diff弹窗文档对比高亮 + 二次修改未弹窗修复
- 引入diff库,实现字符级差异比对 - diffModal左右两侧增加diff高亮:左侧删除内容标红,右侧新增内容标绿 - systemPrompt增加绝对强制条款:无论指令多小都必须返回updatedHtml - 前端校验兜底:修改模式下未返回updatedHtml时在聊天面板给出提示 - confirmAiInjection注入前清理diff高亮span,避免污染编辑器
This commit is contained in:
@@ -12,6 +12,7 @@ import { User, Report, Template, CapturedFrame, SystemSettings, FormField, DEFAU
|
||||
import { defaultReportContent } from '../utils/defaultContent';
|
||||
import { printDocument } from '../utils/print';
|
||||
import { storage } from '../utils/storage';
|
||||
import { diffChars } from 'diff';
|
||||
|
||||
export default function ReportEditor() {
|
||||
const navigate = useNavigate();
|
||||
@@ -823,6 +824,28 @@ export default function ReportEditor() {
|
||||
}).filter(r => r.id);
|
||||
};
|
||||
|
||||
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, '<').replace(/>/g, '>').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;
|
||||
};
|
||||
|
||||
const toggleListening = () => {
|
||||
if (isListening) {
|
||||
setIsListening(false);
|
||||
@@ -952,6 +975,9 @@ export default function ReportEditor() {
|
||||
if (responseJson.reply) {
|
||||
setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: responseJson.reply }]);
|
||||
}
|
||||
if (aiModifyEnabled && !responseJson.updatedHtml) {
|
||||
setChatMessages(prev => [...prev, { id: Date.now().toString(), role: 'model', content: '【系统提示】AI 未能生成修改内容,请尝试重新描述您的需求。' }]);
|
||||
}
|
||||
if (responseJson.updatedHtml && aiModifyEnabled) {
|
||||
let cleanHtml = responseJson.updatedHtml;
|
||||
cleanHtml = cleanHtml.replace(/<br\s*\/?>/gi, '');
|
||||
@@ -981,6 +1007,7 @@ export default function ReportEditor() {
|
||||
|
||||
const confirmAiInjection = (newHtml: string, regionId: string) => {
|
||||
if (!editorRef.current) return;
|
||||
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();
|
||||
@@ -2623,27 +2650,35 @@ export default function ReportEditor() {
|
||||
</div>
|
||||
<button onClick={() => setDiffModal(null)} className="text-slate-400 hover:text-slate-600"><X size={20}/></button>
|
||||
</div>
|
||||
<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: diffModal.originalHtml }}></div>
|
||||
</div>
|
||||
<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>
|
||||
{(() => {
|
||||
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>
|
||||
<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
|
||||
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: diffModal.newHtml }}
|
||||
style={{ fontFamily: 'SimSun, "Microsoft YaHei", serif', fontSize: '12pt', lineHeight: '1.5' }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
<div className="px-6 py-4 border-t border-slate-100 flex justify-end gap-3 bg-white">
|
||||
<button onClick={() => setDiffModal(null)} className="px-6 py-2 rounded-lg text-slate-600 font-medium hover:bg-slate-100">放弃修改</button>
|
||||
<button onClick={() => confirmAiInjection(diffModal.newHtml, diffModal.targetId)} className="px-6 py-2 rounded-lg bg-blue-600 text-white font-medium hover:bg-blue-700 shadow-sm flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user