From 1ec25065ad16cacd26c370d6352761af809d432b Mon Sep 17 00:00:00 2001
From: admin <572701190@qq.com>
Date: Sun, 19 Apr 2026 22:08:05 +0800
Subject: [PATCH] =?UTF-8?q?feat(ai):=20diff=E5=BC=B9=E7=AA=97=E6=96=87?=
=?UTF-8?q?=E6=A1=A3=E5=AF=B9=E6=AF=94=E9=AB=98=E4=BA=AE=20+=20=E4=BA=8C?=
=?UTF-8?q?=E6=AC=A1=E4=BF=AE=E6=94=B9=E6=9C=AA=E5=BC=B9=E7=AA=97=E4=BF=AE?=
=?UTF-8?q?=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 引入diff库,实现字符级差异比对
- diffModal左右两侧增加diff高亮:左侧删除内容标红,右侧新增内容标绿
- systemPrompt增加绝对强制条款:无论指令多小都必须返回updatedHtml
- 前端校验兜底:修改模式下未返回updatedHtml时在聊天面板给出提示
- confirmAiInjection注入前清理diff高亮span,避免污染编辑器
---
package-lock.json | 10 +++
package.json | 1 +
src/pages/ReportEditor.tsx | 75 ++++++++++++-----
工程分析/20260419_2159/实现方案.md | 128 +++++++++++++++++++++++++++++
工程分析/20260419_2159/测试方案.md | 49 +++++++++++
工程分析/20260419_2159/需求分析.md | 35 ++++++++
工程分析/经验记录.md | 34 ++++++++
7 files changed, 312 insertions(+), 20 deletions(-)
create mode 100644 工程分析/20260419_2159/实现方案.md
create mode 100644 工程分析/20260419_2159/测试方案.md
create mode 100644 工程分析/20260419_2159/需求分析.md
diff --git a/package-lock.json b/package-lock.json
index 01693cc..82cdf13 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
+ "diff": "^9.0.0",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
@@ -1957,6 +1958,15 @@
"node": ">=8"
}
},
+ "node_modules/diff": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz",
+ "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
diff --git a/package.json b/package.json
index 9e3207d..57eaf04 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
+ "diff": "^9.0.0",
"dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0",
diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx
index 8a0cf7d..3809ac7 100644
--- a/src/pages/ReportEditor.tsx
+++ b/src/pages/ReportEditor.tsx
@@ -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, '
\n').replace(/
/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(/\n/g, '
');
+ if (side === 'left' && part.removed) {
+ html += `${value}`;
+ } else if (side === 'right' && part.added) {
+ html += `${value}`;
+ } 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(/
/gi, '');
@@ -981,6 +1007,7 @@ export default function ReportEditor() {
const confirmAiInjection = (newHtml: string, regionId: string) => {
if (!editorRef.current) return;
+ const cleanHtml = newHtml.replace(/]*>(.*?)<\/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() {
-
-
-
-
-
AI 提议版本 (可直接编辑)
-
编辑态
+ {(() => {
+ const oldText = stripHtml(diffModal.originalHtml);
+ const newText = stripHtml(diffModal.newHtml);
+ const leftDiffHtml = computeDiffHtml(oldText, newText, 'left');
+ const rightDiffHtml = computeDiffHtml(oldText, newText, 'right');
+ return (
+
+
+
+
+ AI 提议版本 (可直接编辑)
+ 编辑态
+
+
setDiffModal(prev => prev ? { ...prev, newHtml: e.target.innerHTML } : null)}
+ dangerouslySetInnerHTML={{ __html: rightDiffHtml }}
+ style={{ fontFamily: 'SimSun, "Microsoft YaHei", serif', fontSize: '12pt', lineHeight: '1.5' }}
+ >
+
-
setDiffModal(prev => prev ? { ...prev, newHtml: e.target.innerHTML } : null)}
- dangerouslySetInnerHTML={{ __html: diffModal.newHtml }}
- style={{ fontFamily: 'SimSun, "Microsoft YaHei", serif', fontSize: '12pt', lineHeight: '1.5' }}
- >
-
-
+ );
+ })()}