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

5.6 KiB
Raw Blame History

实现方案

修改文件

  • src/pages/ReportEditor.tsx
  • package.json / package-lock.json(已安装 diff 库)

依赖安装

npm install diff --save

已完成。

修改 1导入 diff 库 + 增加辅助函数

ReportEditor.tsx 顶部 imports 区域增加:

import { diffChars } from 'diff';

在组件内部增加辅助函数(建议放在 checkAiRegions 之后):

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) 分支之前,增加兜底提示:

      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

      {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

  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);
      // ... 后续动画和保存逻辑不变