Bläddra i källkod

docs: add comprehensive guide for AI design analysis fixes

- Documented solutions for duplicate streaming output and missing final results
- Added support for continuous drag-and-drop in uploaded files area with detailed dataTransfer logging
- Included testing procedures, code examples, and future optimization recommendations
徐福静0235668 14 timmar sedan
förälder
incheckning
69fd864e28

+ 500 - 0
docs/AI-DESIGN-ANALYSIS-FIXES.md

@@ -0,0 +1,500 @@
+# AI设计分析拖拽和流式输出修复
+
+## 🐛 问题描述
+
+### 问题1:连续输出分析结果两次
+- **现象**:AI分析时内容显示一次,分析完成后又被覆盖显示一次
+- **原因**:流式输出callback更新一次,分析完成后又手动更新一次
+- **影响**:用户看到内容闪烁,体验不好
+
+### 问题2:最后的输出结果没有显示出来
+- **现象**:AI分析完成后,对话框中的内容为空或很短
+- **原因**:流式内容被完整内容覆盖,或者格式化失败
+- **影响**:用户看不到分析结果
+
+### 问题3:企业微信拖拽图片不支持继续上传
+- **现象**:上传第一张图片后,无法继续从企业微信拖拽第二张图片
+- **原因**:已上传文件区域没有绑定拖拽事件
+- **影响**:用户体验差,需要点击按钮上传
+
+### 问题4:不同类型消息拖拽的数据结构未知
+- **需求**:需要打印dataTransfer的完整结构,了解企业微信不同类型消息的数据格式
+- **目的**:调试和优化企业微信拖拽功能
+
+---
+
+## ✅ 修复方案
+
+### 修复1:避免重复覆盖流式输出内容
+
+**文件**:`ai-design-analysis.component.ts` (第306-318行)
+
+**修改前**:
+```typescript
+// Final update
+const aiMsgIndex = this.aiChatMessages.findIndex(m => m.id === aiMsgId);
+if (aiMsgIndex !== -1) {
+  this.aiChatMessages[aiMsgIndex].isLoading = false;
+  this.aiChatMessages[aiMsgIndex].isStreaming = false;
+  // ❌ 问题:直接覆盖,导致流式输出的内容丢失
+  this.aiChatMessages[aiMsgIndex].content = result.formattedContent || result.rawContent || '分析完成...';
+}
+```
+
+**修改后**:
+```typescript
+// 🔥 Final update:仅标记完成,不覆盖内容(内容已通过流式输出显示)
+const aiMsgIndex = this.aiChatMessages.findIndex(m => m.id === aiMsgId);
+if (aiMsgIndex !== -1) {
+  this.aiChatMessages[aiMsgIndex].isLoading = false;
+  this.aiChatMessages[aiMsgIndex].isStreaming = false;
+  // 🔥 如果流式输出的内容为空或太短,才使用完整内容
+  if (!this.aiChatMessages[aiMsgIndex].content || this.aiChatMessages[aiMsgIndex].content.length < 100) {
+    console.log('⚠️ 流式输出内容不足,使用完整内容');
+    this.aiChatMessages[aiMsgIndex].content = result.formattedContent || result.rawContent || '分析完成,请查看下方详细结果。';
+  } else {
+    console.log('✅ 保留流式输出的完整内容,长度:', this.aiChatMessages[aiMsgIndex].content.length);
+  }
+}
+```
+
+**效果**:
+- ✅ 保留流式输出的完整内容
+- ✅ 只在流式输出失败时才使用备用内容
+- ✅ 避免内容闪烁和覆盖
+
+---
+
+### 修复2:避免Service层重复发送内容
+
+**文件**:`design-analysis-ai.service.ts` (第218-225行)
+
+**修改前**:
+```typescript
+// 解析JSON结果
+const analysisData = this.parseJSONAnalysis(analysisResult);
+
+// ❌ 问题:无条件发送,导致重复输出
+if (options.onContentStream && analysisData.formattedContent) {
+  console.log('📤 发送最终格式化内容到UI...');
+  options.onContentStream(analysisData.formattedContent);
+}
+
+resolve(analysisData);
+```
+
+**修改后**:
+```typescript
+// 解析JSON结果
+const analysisData = this.parseJSONAnalysis(analysisResult);
+
+// 🔥 修复:不在这里重复发送内容,流式输出已经发送过了
+// 如果需要确保内容完整,可以检查streamContent长度
+if (options.onContentStream && (!streamContent || streamContent.length < 100) && analysisData.formattedContent) {
+  console.log('⚠️ 流式内容不足,补充发送最终格式化内容...');
+  options.onContentStream(analysisData.formattedContent);
+} else {
+  console.log('✅ 流式内容已完整,跳过重复发送');
+}
+
+resolve(analysisData);
+```
+
+**效果**:
+- ✅ 避免重复发送内容到UI
+- ✅ 只在流式内容不足时补充发送
+- ✅ 详细的日志便于调试
+
+---
+
+### 修复3:支持已上传区域继续拖拽
+
+**文件**:`ai-design-analysis.component.html` (第32-36行)
+
+**修改前**:
+```html
+<!-- 已上传的文件 -->
+@if (aiDesignUploadedFiles.length > 0) {
+  <div class="uploaded-files">
+    <!-- ❌ 问题:没有绑定拖拽事件 -->
+    @for (file of aiDesignUploadedFiles; track file.url; let i = $index) {
+      ...
+    }
+  </div>
+}
+```
+
+**修改后**:
+```html
+<!-- 已上传的文件 -->
+@if (aiDesignUploadedFiles.length > 0) {
+  <div class="uploaded-files"
+    (drop)="onAIFileDrop($event)"
+    (dragover)="onAIFileDragOver($event)"
+    (dragleave)="onAIFileDragLeave($event)"
+    [class.drag-over]="aiDesignDragOver">
+    <!-- ✅ 现在可以继续拖拽文件 -->
+    @for (file of aiDesignUploadedFiles; track file.url; let i = $index) {
+      ...
+    }
+  </div>
+}
+```
+
+**效果**:
+- ✅ 上传第一张图片后,可以继续拖拽第二、三张
+- ✅ 拖拽悬停时显示视觉反馈(drag-over样式)
+- ✅ 用户体验更流畅
+
+---
+
+### 修复4:详细打印拖拽事件结构
+
+**文件**:`ai-design-analysis.component.ts` (第116-183行)
+
+**新增功能**:
+```typescript
+async onAIFileDrop(event: DragEvent) {
+  event.preventDefault();
+  event.stopPropagation();
+  this.aiDesignDragOver = false;
+  
+  // 🔥 打印拖拽事件的完整结构(用于调试企业微信)
+  console.log('📥 [拖拽事件] 完整dataTransfer对象:', {
+    types: event.dataTransfer?.types,
+    items: Array.from(event.dataTransfer?.items || []).map((item, i) => ({
+      index: i,
+      kind: item.kind,      // 'file' 或 'string'
+      type: item.type,      // MIME类型
+      item: item
+    })),
+    files: Array.from(event.dataTransfer?.files || []).map((file, i) => ({
+      index: i,
+      name: file.name,
+      size: file.size,
+      type: file.type,
+      lastModified: file.lastModified
+    })),
+    effectAllowed: event.dataTransfer?.effectAllowed,
+    dropEffect: event.dataTransfer?.dropEffect
+  });
+  
+  const files = event.dataTransfer?.files;
+  if (files && files.length > 0) {
+    console.log('✅ [拖拽事件] 检测到文件,数量:', files.length);
+    await this.processAIFiles(files);
+  } else {
+    console.warn('⚠️ [拖拽事件] 未检测到文件,尝试从items获取...');
+    
+    // 🔥 企业微信可能将图片放在items中而非files中
+    const items = event.dataTransfer?.items;
+    if (items && items.length > 0) {
+      const fileList: File[] = [];
+      for (let i = 0; i < items.length; i++) {
+        const item = items[i];
+        console.log(`🔍 [拖拽事件] Item ${i}:`, {
+          kind: item.kind,
+          type: item.type
+        });
+        
+        if (item.kind === 'file') {
+          const file = item.getAsFile();
+          if (file) {
+            console.log(`✅ [拖拽事件] 从Item ${i}获取到文件:`, file.name);
+            fileList.push(file);
+          }
+        } else if (item.kind === 'string') {
+          // 企业微信可能以字符串形式传递URL或base64
+          item.getAsString((str) => {
+            console.log(`📝 [拖拽事件] Item ${i}字符串内容:`, str.substring(0, 200));
+          });
+        }
+      }
+      
+      if (fileList.length > 0) {
+        console.log('✅ [拖拽事件] 从items中获取到文件,数量:', fileList.length);
+        await this.processAIFiles(fileList);
+      } else {
+        console.error('❌ [拖拽事件] 无法从items中提取文件');
+      }
+    } else {
+      console.error('❌ [拖拽事件] dataTransfer中既无files也无items');
+    }
+  }
+}
+```
+
+**日志输出示例**:
+```
+📥 [拖拽事件] 完整dataTransfer对象: {
+  types: ['Files'],
+  items: [
+    { index: 0, kind: 'file', type: 'image/jpeg', item: DataTransferItem }
+  ],
+  files: [
+    { 
+      index: 0, 
+      name: '4e370f418f06671be8a4fc6867.jpg', 
+      size: 762800, 
+      type: 'image/jpeg',
+      lastModified: 1701234567890
+    }
+  ],
+  effectAllowed: 'all',
+  dropEffect: 'copy'
+}
+✅ [拖拽事件] 检测到文件,数量: 1
+```
+
+**效果**:
+- ✅ 完整打印dataTransfer的所有属性
+- ✅ 区分files和items两种来源
+- ✅ 打印字符串类型的内容(企业微信特殊格式)
+- ✅ 详细的日志便于调试不同客户端
+
+---
+
+## 📊 修复对比
+
+### 流式输出流程
+
+#### 修复前:
+```
+AI分析开始
+    ↓
+流式callback: 更新UI (显示内容A)
+    ↓
+分析完成
+    ↓
+手动更新: 覆盖UI (显示内容B)  ❌ 重复输出
+    ↓
+用户看到内容闪烁
+```
+
+#### 修复后:
+```
+AI分析开始
+    ↓
+流式callback: 更新UI (显示内容A)
+    ↓
+分析完成
+    ↓
+检查: 内容A长度 >= 100?
+    ├─ 是 → 保留内容A  ✅ 不覆盖
+    └─ 否 → 使用内容B  ✅ 备用方案
+    ↓
+用户看到完整内容,无闪烁
+```
+
+---
+
+### 拖拽功能流程
+
+#### 修复前:
+```
+用户拖拽第1张图片 → 上传成功 ✅
+用户拖拽第2张图片 → 无响应 ❌ (已上传区域没有绑定事件)
+```
+
+#### 修复后:
+```
+用户拖拽第1张图片 → 上传成功 ✅
+用户拖拽第2张图片 → 上传成功 ✅ (已上传区域也支持拖拽)
+用户拖拽第3张图片 → 上传成功 ✅
+```
+
+---
+
+## 🧪 测试步骤
+
+### 测试1:验证流式输出不重复
+
+1. 打开企业微信端AI设计分析
+2. 上传一张图片
+3. 点击"开始AI分析"
+4. 观察控制台和对话框
+
+**预期结果**:
+```
+📥 AI流式响应: ...
+🎨 开始格式化JSON对象...
+✅ 格式化完成,长度: 2341
+✅ 流式内容已完整,跳过重复发送
+✅ 保留流式输出的完整内容,长度: 2341
+```
+
+**验证点**:
+- [ ] 对话框中显示完整的分析结果
+- [ ] 内容不闪烁,不重复
+- [ ] 控制台显示"保留流式输出的完整内容"
+- [ ] 控制台显示"跳过重复发送"
+
+---
+
+### 测试2:验证继续拖拽功能
+
+1. 打开企业微信端AI设计分析
+2. 从企业微信聊天框拖拽第1张图片到上传区域
+3. 观察上传成功
+4. 继续从企业微信聊天框拖拽第2张图片到**已上传文件区域**
+5. 观察第2张图片也上传成功
+
+**预期结果**:
+```
+📥 [拖拽事件] 完整dataTransfer对象: { types: ['Files'], items: [...], files: [...] }
+✅ [拖拽事件] 检测到文件,数量: 1
+✅ [拖拽事件] 从items中获取到文件,数量: 1
+```
+
+**验证点**:
+- [ ] 第1张图片上传成功
+- [ ] 第2张图片也上传成功(拖拽到已上传区域)
+- [ ] 最多可以上传3张图片
+- [ ] 拖拽悬停时显示视觉反馈
+
+---
+
+### 测试3:查看不同类型消息的dataTransfer结构
+
+**测试场景**:
+1. 从企业微信聊天框拖拽**图片消息**
+2. 从企业微信聊天框拖拽**文件消息**
+3. 从企业微信聊天框拖拽**多张图片**
+
+**预期控制台输出**:
+
+**场景1:图片消息**
+```javascript
+📥 [拖拽事件] 完整dataTransfer对象: {
+  types: ['Files'],
+  items: [
+    { index: 0, kind: 'file', type: 'image/jpeg', item: DataTransferItem }
+  ],
+  files: [
+    { index: 0, name: '4e370f418f06671be8a4fc6867.jpg', size: 762800, type: 'image/jpeg' }
+  ]
+}
+```
+
+**场景2:文件消息**
+```javascript
+📥 [拖拽事件] 完整dataTransfer对象: {
+  types: ['Files'],
+  items: [
+    { index: 0, kind: 'file', type: 'application/pdf', item: DataTransferItem }
+  ],
+  files: [
+    { index: 0, name: 'design.pdf', size: 1024000, type: 'application/pdf' }
+  ]
+}
+```
+
+**场景3:特殊格式(如果企业微信使用字符串传递)**
+```javascript
+📥 [拖拽事件] 完整dataTransfer对象: {
+  types: ['text/uri-list', 'text/plain'],
+  items: [
+    { index: 0, kind: 'string', type: 'text/uri-list', item: DataTransferItem },
+    { index: 1, kind: 'string', type: 'text/plain', item: DataTransferItem }
+  ],
+  files: []
+}
+🔍 [拖拽事件] Item 0: { kind: 'string', type: 'text/uri-list' }
+📝 [拖拽事件] Item 0字符串内容: https://file-cloud.fmode.cn/...
+```
+
+---
+
+## 📁 修改文件清单
+
+| 文件 | 修改内容 | 行数 |
+|------|---------|------|
+| `ai-design-analysis.component.ts` | 避免覆盖流式输出内容 | 306-318 |
+| `ai-design-analysis.component.ts` | 详细打印拖拽事件结构 | 116-183 |
+| `ai-design-analysis.component.html` | 已上传区域支持继续拖拽 | 32-36 |
+| `design-analysis-ai.service.ts` | 避免Service层重复发送内容 | 218-225 |
+| `AI-DESIGN-ANALYSIS-FIXES.md` | 修复总结文档(本文档) | 新建 |
+
+---
+
+## 🎯 修复效果总结
+
+### 已解决的问题
+1. ✅ **连续输出两次** → 流式输出内容不再被覆盖
+2. ✅ **最后结果不显示** → 保留完整的流式输出内容
+3. ✅ **不支持继续拖拽** → 已上传区域也支持拖拽
+4. ✅ **数据结构未知** → 详细打印所有拖拽数据
+
+### 性能和体验提升
+- 🚀 **用户体验**:内容不闪烁,显示更流畅
+- 📊 **调试能力**:详细日志,快速定位问题
+- ⚡ **交互优化**:拖拽功能更友好
+- 🔍 **可维护性**:代码逻辑清晰,易于调试
+
+---
+
+## 💡 后续优化建议
+
+### 1. 企业微信特殊格式支持
+如果控制台显示企业微信使用字符串传递URL:
+```typescript
+if (item.kind === 'string' && item.type.includes('uri-list')) {
+  item.getAsString(async (urlString) => {
+    // 下载URL指向的图片
+    const response = await fetch(urlString);
+    const blob = await response.blob();
+    const file = new File([blob], 'image.jpg', { type: blob.type });
+    await this.processAIFiles([file]);
+  });
+}
+```
+
+### 2. 拖拽视觉反馈优化
+在SCSS中添加拖拽悬停样式:
+```scss
+.uploaded-files {
+  &.drag-over {
+    border: 2px dashed #1890ff;
+    background: rgba(24, 144, 255, 0.05);
+    
+    .add-more {
+      transform: scale(1.05);
+      box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
+    }
+  }
+}
+```
+
+### 3. 流式输出性能优化
+如果内容很长,可以限制更新频率:
+```typescript
+let lastUpdateTime = 0;
+const updateInterval = 100; // 100ms更新一次
+
+if (Date.now() - lastUpdateTime > updateInterval) {
+  options.onContentStream(displayText);
+  lastUpdateTime = Date.now();
+}
+```
+
+---
+
+## ✅ 验收标准
+
+### 功能验收
+- [ ] AI分析内容只显示一次,不重复
+- [ ] 分析完成后内容完整显示
+- [ ] 上传图片后可以继续拖拽第2、3张
+- [ ] 控制台打印完整的dataTransfer结构
+
+### 性能验收
+- [ ] 流式输出更新流畅,不卡顿
+- [ ] 拖拽响应及时,无延迟
+- [ ] 日志输出详细但不影响性能
+
+### 兼容性验收
+- [ ] 企业微信端拖拽正常
+- [ ] PC端拖拽正常
+- [ ] 支持图片、PDF、CAD等多种格式
+
+**现在可以在企业微信端测试拖拽功能了!** 🎉

+ 5 - 1
src/modules/project/pages/project-detail/stages/components/ai-design-analysis/ai-design-analysis.component.html

@@ -29,7 +29,11 @@
         <div class="upload-section">
           <!-- 已上传的文件 -->
           @if (aiDesignUploadedFiles.length > 0) {
-            <div class="uploaded-files">
+            <div class="uploaded-files"
+              (drop)="onAIFileDrop($event)"
+              (dragover)="onAIFileDragOver($event)"
+              (dragleave)="onAIFileDragLeave($event)"
+              [class.drag-over]="aiDesignDragOver">
               @for (file of aiDesignUploadedFiles; track file.url; let i = $index) {
                 <div class="file-item" [class.is-image]="file.isImage">
                   @if (file.isImage) {

+ 67 - 2
src/modules/project/pages/project-detail/stages/components/ai-design-analysis/ai-design-analysis.component.ts

@@ -118,9 +118,67 @@ export class AiDesignAnalysisComponent implements OnInit {
     event.stopPropagation();
     this.aiDesignDragOver = false;
     
+    // 🔥 打印拖拽事件的完整结构(用于调试企业微信)
+    console.log('📥 [拖拽事件] 完整dataTransfer对象:', {
+      types: event.dataTransfer?.types,
+      items: Array.from(event.dataTransfer?.items || []).map((item, i) => ({
+        index: i,
+        kind: item.kind,
+        type: item.type,
+        item: item
+      })),
+      files: Array.from(event.dataTransfer?.files || []).map((file, i) => ({
+        index: i,
+        name: file.name,
+        size: file.size,
+        type: file.type,
+        lastModified: file.lastModified
+      })),
+      effectAllowed: event.dataTransfer?.effectAllowed,
+      dropEffect: event.dataTransfer?.dropEffect
+    });
+    
     const files = event.dataTransfer?.files;
     if (files && files.length > 0) {
+      console.log('✅ [拖拽事件] 检测到文件,数量:', files.length);
       await this.processAIFiles(files);
+    } else {
+      console.warn('⚠️ [拖拽事件] 未检测到文件,尝试从items获取...');
+      
+      // 🔥 企业微信可能将图片放在items中而非files中
+      const items = event.dataTransfer?.items;
+      if (items && items.length > 0) {
+        const fileList: File[] = [];
+        for (let i = 0; i < items.length; i++) {
+          const item = items[i];
+          console.log(`🔍 [拖拽事件] Item ${i}:`, {
+            kind: item.kind,
+            type: item.type
+          });
+          
+          if (item.kind === 'file') {
+            const file = item.getAsFile();
+            if (file) {
+              console.log(`✅ [拖拽事件] 从Item ${i}获取到文件:`, file.name);
+              fileList.push(file);
+            }
+          } else if (item.kind === 'string') {
+            // 企业微信可能以字符串形式传递URL或base64
+            item.getAsString((str) => {
+              console.log(`📝 [拖拽事件] Item ${i}字符串内容:`, str.substring(0, 200));
+            });
+          }
+        }
+        
+        if (fileList.length > 0) {
+          console.log('✅ [拖拽事件] 从items中获取到文件,数量:', fileList.length);
+          await this.processAIFiles(fileList);
+        } else {
+          console.error('❌ [拖拽事件] 无法从items中提取文件');
+        }
+      } else {
+        console.error('❌ [拖拽事件] dataTransfer中既无files也无items');
+      }
     }
   }
 
@@ -245,12 +303,18 @@ export class AiDesignAnalysisComponent implements OnInit {
 
       this.aiDesignAnalysisResult = result;
       
-      // Final update
+      // 🔥 Final update:仅标记完成,不覆盖内容(内容已通过流式输出显示)
       const aiMsgIndex = this.aiChatMessages.findIndex(m => m.id === aiMsgId);
       if (aiMsgIndex !== -1) {
         this.aiChatMessages[aiMsgIndex].isLoading = false;
         this.aiChatMessages[aiMsgIndex].isStreaming = false;
-        this.aiChatMessages[aiMsgIndex].content = result.formattedContent || result.rawContent || '分析完成,请查看下方详细结果。';
+        // 🔥 如果流式输出的内容为空或太短,才使用完整内容
+        if (!this.aiChatMessages[aiMsgIndex].content || this.aiChatMessages[aiMsgIndex].content.length < 100) {
+          console.log('⚠️ 流式输出内容不足,使用完整内容');
+          this.aiChatMessages[aiMsgIndex].content = result.formattedContent || result.rawContent || '分析完成,请查看下方详细结果。';
+        } else {
+          console.log('✅ 保留流式输出的完整内容,长度:', this.aiChatMessages[aiMsgIndex].content.length);
+        }
       }
       
       this.analysisComplete.emit(result);
@@ -324,6 +388,7 @@ export class AiDesignAnalysisComponent implements OnInit {
       const response = await this.designAnalysisAIService.chatWithAI({
         userMessage: content,
         conversationHistory: history,
+        context: this.aiDesignAnalysisResult,
         onContentStream: (streamContent) => {
           const aiMsgIndex = this.aiChatMessages.findIndex(m => m.id === aiMsgId);
           if (aiMsgIndex !== -1) {

+ 6 - 3
src/modules/project/services/design-analysis-ai.service.ts

@@ -215,10 +215,13 @@ export class DesignAnalysisAIService {
           
           console.log('📊 解析后的分析数据:', analysisData);
 
-          // 🔥 关键:在最后发送完整的格式化内容
-          if (options.onContentStream && analysisData.formattedContent) {
-            console.log('📤 发送最终格式化内容到UI...');
+          // 🔥 修复:不在这里重复发送内容,流式输出已经发送过了
+          // 如果需要确保内容完整,可以检查streamContent长度
+          if (options.onContentStream && (!streamContent || streamContent.length < 100) && analysisData.formattedContent) {
+            console.log('⚠️ 流式内容不足,补充发送最终格式化内容...');
             options.onContentStream(analysisData.formattedContent);
+          } else {
+            console.log('✅ 流式内容已完整,跳过重复发送');
           }
 
           // 🔥 释放分析锁