|
|
@@ -2,11 +2,12 @@ import { Component, OnInit, OnDestroy, Input, ChangeDetectionStrategy, ChangeDet
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
-import { WxworkAuth, FmodeParse } from 'fmode-ng/core';
|
|
|
+import { WxworkAuth, FmodeParse, NovaStorage } from 'fmode-ng/core';
|
|
|
import { IonIcon } from '@ionic/angular/standalone';
|
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
|
import { ProductSpaceService, Project } from '../../../services/product-space.service';
|
|
|
import { ProjectFileService } from '../../../services/project-file.service';
|
|
|
+import { DesignAnalysisAIService } from '../../../services/design-analysis-ai.service';
|
|
|
import { ColorGetDialogComponent } from '../../../components/color-get/color-get-dialog.component';
|
|
|
import { completionJSON } from 'fmode-ng/core';
|
|
|
import { addIcons } from 'ionicons';
|
|
|
@@ -192,22 +193,44 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
};
|
|
|
} = {};
|
|
|
|
|
|
- // AI聊天相关
|
|
|
+ // AI设计分析对话相关
|
|
|
+ showAIDesignDialog: boolean = false;
|
|
|
+ aiDesignDialogVisible = false;
|
|
|
+ aiDesignUploading: boolean = false;
|
|
|
+ aiDesignCurrentSpace: any = null;
|
|
|
+ aiDesignUploadedImages: string[] = [];
|
|
|
+ aiDesignUploadedFiles: any[] = []; // 存储文件信息(类型、名称等)
|
|
|
+ aiDesignTextDescription = '';
|
|
|
+ aiDesignAnalyzing = false;
|
|
|
+ aiDesignAnalysisResult: any = null;
|
|
|
+ aiDesignGeneratingReport = false;
|
|
|
+ aiDesignReport = '';
|
|
|
+ aiDesignReportConfirmed = false;
|
|
|
+ aiDesignDragOver = false; // 拖拽状态
|
|
|
+
|
|
|
+ // AI对话系统
|
|
|
aiChatMessages: Array<{
|
|
|
id: string;
|
|
|
role: 'user' | 'assistant';
|
|
|
content: string;
|
|
|
timestamp: Date;
|
|
|
- analysisType?: string;
|
|
|
+ images?: string[];
|
|
|
+ isLoading?: boolean;
|
|
|
+ isStreaming?: boolean; // 流式输出中
|
|
|
+ liked?: boolean;
|
|
|
+ disliked?: boolean;
|
|
|
}> = [];
|
|
|
-
|
|
|
+ aiChatInput: string = '';
|
|
|
+ deepThinkingEnabled: boolean = false;
|
|
|
+ @ViewChild('chatMessagesWrapper') chatMessagesWrapper!: ElementRef;
|
|
|
+ @ViewChild('chatInput') chatInputElement!: ElementRef;
|
|
|
+
|
|
|
// AI分析状态
|
|
|
aiAnalyzing: boolean = false;
|
|
|
aiAnalyzingImages: boolean = false;
|
|
|
aiAnalyzingCAD: boolean = false;
|
|
|
aiGeneratingComprehensive: boolean = false;
|
|
|
showAIChat: boolean = false;
|
|
|
- aiChatInput: string = '';
|
|
|
|
|
|
// AI分析配置
|
|
|
private readonly AI_MODEL = 'fmode-1.6-cn';
|
|
|
@@ -226,6 +249,7 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
private cdr: ChangeDetectorRef,
|
|
|
private productSpaceService: ProductSpaceService,
|
|
|
private projectFileService: ProjectFileService,
|
|
|
+ private designAnalysisAIService: DesignAnalysisAIService,
|
|
|
private dialog: MatDialog
|
|
|
) {}
|
|
|
|
|
|
@@ -451,6 +475,12 @@ export class StageRequirementsComponent implements OnInit, OnDestroy {
|
|
|
// 加载已保存的AI分析结果
|
|
|
await this.loadAnalysisResults();
|
|
|
|
|
|
+ // 自动选择第一个空间用于AI分析
|
|
|
+ if (this.projectProducts.length > 0 && !this.aiDesignCurrentSpace) {
|
|
|
+ this.aiDesignCurrentSpace = this.projectProducts[0];
|
|
|
+ console.log('✨ 自动选择第一个空间用于AI分析:', this.aiDesignCurrentSpace.name);
|
|
|
+ }
|
|
|
+
|
|
|
this.cdr.markForCheck();
|
|
|
|
|
|
} catch (err) {
|
|
|
@@ -2935,4 +2965,1238 @@ ${context}
|
|
|
const cad = this.getSpaceCADFiles(spaceId).length;
|
|
|
return { images, cad, total: images + cad };
|
|
|
}
|
|
|
+
|
|
|
+ // ===== AI设计分析对话功能 =====
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 选择AI分析空间
|
|
|
+ */
|
|
|
+ selectAISpace(space: any): void {
|
|
|
+ this.aiDesignCurrentSpace = space;
|
|
|
+
|
|
|
+ // 从保存的数据中加载(如果存在)
|
|
|
+ const projectData = this.project?.get('data') || {};
|
|
|
+
|
|
|
+ // 加载对话历史
|
|
|
+ const aiChatHistory = projectData.aiChatHistory || {};
|
|
|
+ if (aiChatHistory[space.id]) {
|
|
|
+ this.aiChatMessages = aiChatHistory[space.id].messages.map((m: any) => ({
|
|
|
+ id: `${m.role}-${Date.now()}-${Math.random()}`,
|
|
|
+ role: m.role,
|
|
|
+ content: m.content,
|
|
|
+ timestamp: new Date(m.timestamp),
|
|
|
+ images: m.images
|
|
|
+ }));
|
|
|
+ console.log('💬 加载对话历史:', this.aiChatMessages.length, '条消息');
|
|
|
+ } else {
|
|
|
+ this.aiChatMessages = [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 优先从确认的报告中加载
|
|
|
+ const designReports = projectData.designReports || {};
|
|
|
+ if (designReports[space.id]) {
|
|
|
+ const savedData = designReports[space.id];
|
|
|
+ console.log('✅ 加载已保存的AI分析数据:', space.name);
|
|
|
+ this.aiDesignUploadedImages = savedData.images || [];
|
|
|
+ this.aiDesignUploadedFiles = savedData.files || [];
|
|
|
+ this.aiDesignTextDescription = savedData.textDescription || '';
|
|
|
+ this.aiDesignAnalysisResult = savedData.analysisData || null;
|
|
|
+ this.aiDesignReport = savedData.report || '';
|
|
|
+ this.aiDesignReportConfirmed = !!savedData.confirmedAt;
|
|
|
+
|
|
|
+ // 如果有对话历史但消息列表为空,从保存的对话历史加载
|
|
|
+ if (this.aiChatMessages.length === 0 && savedData.chatHistory) {
|
|
|
+ this.aiChatMessages = savedData.chatHistory.map((m: any) => ({
|
|
|
+ id: `${m.role}-${Date.now()}-${Math.random()}`,
|
|
|
+ role: m.role,
|
|
|
+ content: m.content,
|
|
|
+ timestamp: new Date(m.timestamp)
|
|
|
+ }));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果没有确认的报告,尝试从分析结果中加载
|
|
|
+ const aiDesignAnalysis = projectData.aiDesignAnalysis || {};
|
|
|
+ if (aiDesignAnalysis[space.id]) {
|
|
|
+ const savedData = aiDesignAnalysis[space.id];
|
|
|
+ console.log('✅ 加载临时AI分析数据:', space.name);
|
|
|
+ this.aiDesignUploadedImages = savedData.images || [];
|
|
|
+ this.aiDesignUploadedFiles = savedData.files || [];
|
|
|
+ this.aiDesignTextDescription = savedData.textDescription || '';
|
|
|
+ this.aiDesignAnalysisResult = savedData.analysisData || null;
|
|
|
+ this.aiDesignReport = '';
|
|
|
+ this.aiDesignReportConfirmed = false;
|
|
|
+ } else {
|
|
|
+ console.log('ℹ️ 该空间暂无AI分析数据:', space.name);
|
|
|
+ this.aiDesignUploadedImages = [];
|
|
|
+ this.aiDesignUploadedFiles = [];
|
|
|
+ this.aiDesignTextDescription = '';
|
|
|
+ this.aiDesignAnalysisResult = null;
|
|
|
+ this.aiDesignReport = '';
|
|
|
+ this.aiDesignReportConfirmed = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ console.log('✨ 选择AI分析空间:', space.name);
|
|
|
+
|
|
|
+ // 滚动到底部(如果有对话历史)
|
|
|
+ if (this.aiChatMessages.length > 0) {
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 打开AI设计分析对话框(兼容原按钮)
|
|
|
+ */
|
|
|
+ openAIDesignDialog(space: any): void {
|
|
|
+ // 兼容空间需求管理中的AI按钮,直接选择空间
|
|
|
+ this.selectAISpace(space);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重置AI分析
|
|
|
+ */
|
|
|
+ resetAIAnalysis(): void {
|
|
|
+ this.aiDesignUploadedImages = [];
|
|
|
+ this.aiDesignUploadedFiles = [];
|
|
|
+ this.aiDesignTextDescription = '';
|
|
|
+ this.aiDesignAnalysisResult = null;
|
|
|
+ this.aiDesignReport = '';
|
|
|
+ this.aiDesignReportConfirmed = false;
|
|
|
+ this.aiDesignAnalyzing = false;
|
|
|
+ this.aiDesignGeneratingReport = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ console.log('🔄 重置AI分析');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 触发AI对话框文件选择
|
|
|
+ */
|
|
|
+ triggerAIDialogFileInput(): void {
|
|
|
+ const element = document.getElementById('aiDesignFileInput') as HTMLInputElement;
|
|
|
+ if (element) {
|
|
|
+ element.click();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理AI对话框文件选择
|
|
|
+ */
|
|
|
+ async handleAIFileSelect(event: Event): Promise<void> {
|
|
|
+ const input = event.target as HTMLInputElement;
|
|
|
+
|
|
|
+ if (!input.files || input.files.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.handleAIFileUpload(Array.from(input.files));
|
|
|
+
|
|
|
+ // 清空input,允许重复选择同一文件
|
|
|
+ input.value = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 移除AI对话框中的图片
|
|
|
+ */
|
|
|
+ removeAIDialogImage(index: number): void {
|
|
|
+ this.aiDesignUploadedImages.splice(index, 1);
|
|
|
+ this.aiDesignUploadedFiles.splice(index, 1);
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理文件拖拽进入
|
|
|
+ */
|
|
|
+ onAIFileDragOver(event: DragEvent): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.aiDesignDragOver = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理文件拖拽离开
|
|
|
+ */
|
|
|
+ onAIFileDragLeave(event: DragEvent): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.aiDesignDragOver = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理文件拖放
|
|
|
+ */
|
|
|
+ async onAIFileDrop(event: DragEvent): Promise<void> {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.aiDesignDragOver = false;
|
|
|
+
|
|
|
+ const files = event.dataTransfer?.files;
|
|
|
+ if (!files || files.length === 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await this.handleAIFileUpload(Array.from(files));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理AI文件上传(统一处理点击和拖拽)
|
|
|
+ */
|
|
|
+ private async handleAIFileUpload(files: File[]): Promise<void> {
|
|
|
+ const maxFiles = 20; // 扩展至20个文件
|
|
|
+ const remainingSlots = maxFiles - this.aiDesignUploadedImages.length;
|
|
|
+
|
|
|
+ if (remainingSlots <= 0) {
|
|
|
+ window?.fmode?.alert(`最多只能上传${maxFiles}个文件`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 支持的文件类型(扩展)
|
|
|
+ const supportedTypes = [
|
|
|
+ // 图片格式
|
|
|
+ 'image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml',
|
|
|
+ // 文档格式
|
|
|
+ 'application/pdf',
|
|
|
+ // CAD格式
|
|
|
+ 'image/vnd.dwg', 'application/acad', 'application/x-acad',
|
|
|
+ 'application/x-dwg', 'application/x-dxf', 'image/x-dwg',
|
|
|
+ // Office格式
|
|
|
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // docx
|
|
|
+ 'application/msword', // doc
|
|
|
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // xlsx
|
|
|
+ 'application/vnd.ms-excel', // xls
|
|
|
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // pptx
|
|
|
+ 'application/vnd.ms-powerpoint', // ppt
|
|
|
+ // 其他
|
|
|
+ 'text/plain'
|
|
|
+ ];
|
|
|
+
|
|
|
+ const filesToUpload = files.slice(0, remainingSlots);
|
|
|
+ this.aiDesignUploading = true;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ try {
|
|
|
+ const cid = localStorage.getItem('company');
|
|
|
+ if (!cid) {
|
|
|
+ throw new Error('公司ID未找到');
|
|
|
+ }
|
|
|
+ const storage = await NovaStorage.withCid(cid);
|
|
|
+
|
|
|
+ for (const file of filesToUpload) {
|
|
|
+ // 检查文件类型
|
|
|
+ const fileExt = file.name.split('.').pop()?.toLowerCase();
|
|
|
+ const supportedExtensions = [
|
|
|
+ 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'tiff', 'svg',
|
|
|
+ 'pdf', 'dwg', 'dxf',
|
|
|
+ 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'
|
|
|
+ ];
|
|
|
+ const isSupported = supportedTypes.includes(file.type) ||
|
|
|
+ supportedExtensions.includes(fileExt || '');
|
|
|
+
|
|
|
+ if (!isSupported) {
|
|
|
+ console.warn(`文件 ${file.name} 格式不支持,跳过`);
|
|
|
+ window?.fmode?.alert(`文件格式不支持: ${file.name}\n支持的格式: 图片、PDF、CAD、Office文档`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (file.size > 50 * 1024 * 1024) {
|
|
|
+ console.warn(`文件 ${file.name} 超过50MB限制,跳过`);
|
|
|
+ window?.fmode?.alert(`文件超过50MB限制: ${file.name}`);
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 上传文件
|
|
|
+ const uploadedFile = await storage.upload(file, {
|
|
|
+ prefixKey: `ai-design-analysis/${this.projectId}`,
|
|
|
+ onProgress: (progress: { total: { percent: number } }) => {
|
|
|
+ console.log(`上传进度: ${Math.round(progress.total.percent)}%`);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存文件信息
|
|
|
+ this.aiDesignUploadedImages.push(uploadedFile.url);
|
|
|
+ this.aiDesignUploadedFiles.push({
|
|
|
+ url: uploadedFile.url,
|
|
|
+ name: file.name,
|
|
|
+ type: file.type,
|
|
|
+ size: file.size,
|
|
|
+ extension: fileExt
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ console.log(`✅ 已上传${this.aiDesignUploadedImages.length}个文件`);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('上传失败:', error);
|
|
|
+ window?.fmode?.alert('文件上传失败,请重试');
|
|
|
+ } finally {
|
|
|
+ this.aiDesignUploading = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开始AI分析(直接调用AI进行真实分析)
|
|
|
+ */
|
|
|
+ async startAIDesignAnalysis(): Promise<void> {
|
|
|
+ if (this.aiDesignUploadedImages.length === 0) {
|
|
|
+ window?.fmode?.alert('请先上传参考图片');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果已有对话记录,提示用户使用对话输入
|
|
|
+ if (this.aiChatMessages.length > 0) {
|
|
|
+ window?.fmode?.alert('请在对话框中输入您的需求');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 添加用户消息(简短描述,不显示完整提示词)
|
|
|
+ const userMessage = {
|
|
|
+ id: `user-${Date.now()}`,
|
|
|
+ role: 'user' as const,
|
|
|
+ content: this.aiDesignTextDescription || `请对这${this.aiDesignUploadedImages.length}张参考图片进行专业的室内设计分析`,
|
|
|
+ timestamp: new Date(),
|
|
|
+ images: [...this.aiDesignUploadedImages]
|
|
|
+ };
|
|
|
+
|
|
|
+ this.aiChatMessages.push(userMessage);
|
|
|
+
|
|
|
+ // 添加AI流式输出消息(初始为空,会实时更新)
|
|
|
+ const aiStreamMessage = {
|
|
|
+ id: `ai-${Date.now()}`,
|
|
|
+ role: 'assistant' as const,
|
|
|
+ content: '', // 初始为空,会实时更新
|
|
|
+ timestamp: new Date(),
|
|
|
+ isStreaming: true // 标记为流式输出中
|
|
|
+ };
|
|
|
+
|
|
|
+ this.aiChatMessages.push(aiStreamMessage);
|
|
|
+ this.aiDesignAnalyzing = true;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+
|
|
|
+ // 直接调用AI分析服务(使用空的对话历史,首次分析)
|
|
|
+ console.log('🤖 开始AI图片分析...');
|
|
|
+ console.log('📸 图片数量:', this.aiDesignUploadedImages.length);
|
|
|
+ console.log('🏠 空间类型:', this.aiDesignCurrentSpace?.name);
|
|
|
+
|
|
|
+ const analysisResult = await this.designAnalysisAIService.analyzeReferenceImages({
|
|
|
+ images: this.aiDesignUploadedImages,
|
|
|
+ textDescription: this.aiDesignTextDescription,
|
|
|
+ spaceType: this.aiDesignCurrentSpace?.name || '',
|
|
|
+ conversationHistory: [], // 首次分析,不传递历史
|
|
|
+ deepThinking: this.deepThinkingEnabled,
|
|
|
+ onProgressChange: (progress) => {
|
|
|
+ console.log('📊 AI分析进度:', progress);
|
|
|
+ },
|
|
|
+ // 🔥 流式输出回调:实时更新消息内容
|
|
|
+ onContentStream: (content) => {
|
|
|
+ const streamMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
+ if (streamMsg) {
|
|
|
+ streamMsg.content = content;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ // 滚动到底部以显示新内容
|
|
|
+ setTimeout(() => this.scrollToBottom(), 50);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 标记流式输出完成(保留流式输出的原始内容,不再重新格式化)
|
|
|
+ const finalMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
+ if (finalMsg) {
|
|
|
+ finalMsg.isStreaming = false;
|
|
|
+ // 保持流式输出的内容不变,已经是完整的AI返回内容
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存最新的分析结果
|
|
|
+ this.aiDesignAnalysisResult = analysisResult;
|
|
|
+
|
|
|
+ // 保存对话记录到项目
|
|
|
+ await this.saveChatHistory();
|
|
|
+
|
|
|
+ console.log('✅ AI分析完成');
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ AI分析失败:', error);
|
|
|
+
|
|
|
+ // 移除流式输出消息或加载消息
|
|
|
+ this.aiChatMessages = this.aiChatMessages.filter(m => !m.isLoading && !m.isStreaming);
|
|
|
+
|
|
|
+ // 添加错误消息
|
|
|
+ this.aiChatMessages.push({
|
|
|
+ id: `ai-error-${Date.now()}`,
|
|
|
+ role: 'assistant' as const,
|
|
|
+ content: `抱歉,分析过程出现错误:${error.message || '未知错误'}。请重试。`,
|
|
|
+ timestamp: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ window?.fmode?.alert('AI分析失败: ' + (error.message || '未知错误'));
|
|
|
+ } finally {
|
|
|
+ this.aiDesignAnalyzing = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成客户报告
|
|
|
+ */
|
|
|
+ async generateClientReport(): Promise<void> {
|
|
|
+ if (!this.aiDesignAnalysisResult) {
|
|
|
+ window?.fmode?.alert('请先完成AI分析');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.aiDesignGeneratingReport = true;
|
|
|
+ this.aiDesignReport = '';
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ console.log('📝 开始生成报告...');
|
|
|
+
|
|
|
+ this.aiDesignReport = await this.designAnalysisAIService.generateClientReport({
|
|
|
+ analysisData: this.aiDesignAnalysisResult,
|
|
|
+ spaceName: this.aiDesignCurrentSpace?.name || '空间',
|
|
|
+ onContentChange: (content) => {
|
|
|
+ this.aiDesignReport = content;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('✅ 报告生成完成');
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ 生成报告失败:', error);
|
|
|
+ window?.fmode?.alert('生成报告失败: ' + (error.message || '未知错误'));
|
|
|
+ } finally {
|
|
|
+ this.aiDesignGeneratingReport = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确认报告
|
|
|
+ */
|
|
|
+ async confirmDesignReport(): Promise<void> {
|
|
|
+ if (!this.aiDesignReport) {
|
|
|
+ window?.fmode?.alert('报告尚未生成');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ this.aiDesignReportConfirmed = true;
|
|
|
+
|
|
|
+ if (this.project && this.aiDesignCurrentSpace?.id) {
|
|
|
+ const projectData = this.project.get('data') || {};
|
|
|
+
|
|
|
+ if (!projectData.designReports) {
|
|
|
+ projectData.designReports = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ projectData.designReports[this.aiDesignCurrentSpace.id] = {
|
|
|
+ report: this.aiDesignReport,
|
|
|
+ analysisData: this.aiDesignAnalysisResult,
|
|
|
+ images: this.aiDesignUploadedImages,
|
|
|
+ files: this.aiDesignUploadedFiles,
|
|
|
+ textDescription: this.aiDesignTextDescription,
|
|
|
+ confirmedAt: new Date().toISOString(),
|
|
|
+ confirmedBy: this.currentUser?.id || 'unknown'
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('💾 保存数据:', {
|
|
|
+ spaceId: this.aiDesignCurrentSpace.id,
|
|
|
+ spaceName: this.aiDesignCurrentSpace.name,
|
|
|
+ imagesCount: this.aiDesignUploadedImages.length,
|
|
|
+ filesCount: this.aiDesignUploadedFiles.length,
|
|
|
+ hasAnalysisResult: !!this.aiDesignAnalysisResult,
|
|
|
+ hasReport: !!this.aiDesignReport
|
|
|
+ });
|
|
|
+
|
|
|
+ this.project.set('data', projectData);
|
|
|
+ await this.project.save();
|
|
|
+
|
|
|
+ console.log('✅ 报告已保存');
|
|
|
+ }
|
|
|
+
|
|
|
+ window?.fmode?.alert('设计分析报告已确认并保存');
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 保存报告失败:', error);
|
|
|
+ window?.fmode?.alert('保存报告失败,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取场景类型中文名
|
|
|
+ */
|
|
|
+ getSceneTypeName(type: string): string {
|
|
|
+ const typeMap: { [key: string]: string } = {
|
|
|
+ '客餐厅': '客餐厅',
|
|
|
+ '厨房': '厨房',
|
|
|
+ '卧室': '卧室',
|
|
|
+ '阳台': '阳台',
|
|
|
+ '卫生间': '卫生间',
|
|
|
+ '其他': '其他'
|
|
|
+ };
|
|
|
+ return typeMap[type] || type;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取空间基调图标
|
|
|
+ */
|
|
|
+ getSpaceToneIcon(tone: string): string {
|
|
|
+ const toneIcons: Record<string, string> = {
|
|
|
+ '温馨': '🏡',
|
|
|
+ '现代': '🏢',
|
|
|
+ '轻奢': '💎',
|
|
|
+ '简约': '⬜',
|
|
|
+ '古典': '🏛️',
|
|
|
+ '自然': '🌿'
|
|
|
+ };
|
|
|
+ return toneIcons[tone] || '🏠';
|
|
|
+ }
|
|
|
+
|
|
|
+ // ===== AI对话系统功能方法 =====
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 输入框变化事件 - 自动调整高度
|
|
|
+ */
|
|
|
+ onChatInputChange(event: any): void {
|
|
|
+ const textarea = event.target;
|
|
|
+ if (textarea) {
|
|
|
+ textarea.style.height = 'auto';
|
|
|
+ textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 输入框回车事件 - Shift+Enter换行,Enter发送
|
|
|
+ */
|
|
|
+ onChatInputEnter(event: Event): void {
|
|
|
+ const keyEvent = event as KeyboardEvent;
|
|
|
+ if (keyEvent.key === 'Enter' && !keyEvent.shiftKey) {
|
|
|
+ keyEvent.preventDefault();
|
|
|
+ this.sendChatMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用快捷提示
|
|
|
+ */
|
|
|
+ useQuickPrompt(prompt: string): void {
|
|
|
+ this.aiChatInput = prompt;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ // 聚焦到输入框
|
|
|
+ setTimeout(() => {
|
|
|
+ this.chatInputElement?.nativeElement?.focus();
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发送聊天消息
|
|
|
+ */
|
|
|
+ async sendChatMessage(): Promise<void> {
|
|
|
+ const message = this.aiChatInput?.trim();
|
|
|
+
|
|
|
+ if (!message || this.aiDesignAnalyzing) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查是否有图片
|
|
|
+ if (this.aiDesignUploadedImages.length === 0) {
|
|
|
+ window?.fmode?.alert('请先上传参考图片');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 添加用户消息
|
|
|
+ const userMessage = {
|
|
|
+ id: `user-${Date.now()}`,
|
|
|
+ role: 'user' as const,
|
|
|
+ content: message,
|
|
|
+ timestamp: new Date(),
|
|
|
+ images: [...this.aiDesignUploadedImages]
|
|
|
+ };
|
|
|
+
|
|
|
+ this.aiChatMessages.push(userMessage);
|
|
|
+ this.aiChatInput = '';
|
|
|
+
|
|
|
+ // 重置输入框高度
|
|
|
+ if (this.chatInputElement) {
|
|
|
+ this.chatInputElement.nativeElement.style.height = 'auto';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加AI流式输出消息(初始为空,会实时更新)
|
|
|
+ const aiStreamMessage = {
|
|
|
+ id: `ai-${Date.now()}`,
|
|
|
+ role: 'assistant' as const,
|
|
|
+ content: '', // 初始为空,会实时更新
|
|
|
+ timestamp: new Date(),
|
|
|
+ isStreaming: true // 标记为流式输出中
|
|
|
+ };
|
|
|
+
|
|
|
+ this.aiChatMessages.push(aiStreamMessage);
|
|
|
+ this.aiDesignAnalyzing = true;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+
|
|
|
+ // 构建对话历史
|
|
|
+ const conversationHistory = this.aiChatMessages
|
|
|
+ .filter(m => !m.isLoading && !m.isStreaming && m.role !== 'assistant' || (m.role === 'assistant' && m.content))
|
|
|
+ .map(m => ({
|
|
|
+ role: m.role,
|
|
|
+ content: m.content
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 调用AI分析
|
|
|
+ console.log('🤖 开始AI对话分析...');
|
|
|
+ console.log('💬 对话历史:', conversationHistory);
|
|
|
+ console.log('💡 深度思考模式:', this.deepThinkingEnabled);
|
|
|
+
|
|
|
+ const analysisResult = await this.designAnalysisAIService.analyzeReferenceImages({
|
|
|
+ images: this.aiDesignUploadedImages,
|
|
|
+ textDescription: message,
|
|
|
+ spaceType: this.aiDesignCurrentSpace?.name || '',
|
|
|
+ conversationHistory: conversationHistory,
|
|
|
+ deepThinking: this.deepThinkingEnabled,
|
|
|
+ onProgressChange: (progress) => {
|
|
|
+ console.log('📊 AI思考进度:', progress);
|
|
|
+ },
|
|
|
+ // 🔥 流式输出回调:实时更新消息内容
|
|
|
+ onContentStream: (content) => {
|
|
|
+ const streamMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
+ if (streamMsg) {
|
|
|
+ streamMsg.content = content;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ // 滚动到底部以显示新内容
|
|
|
+ setTimeout(() => this.scrollToBottom(), 50);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 标记流式输出完成(保留流式输出的原始内容,不再重新格式化)
|
|
|
+ const finalMsg = this.aiChatMessages.find(m => m.id === aiStreamMessage.id);
|
|
|
+ if (finalMsg) {
|
|
|
+ finalMsg.isStreaming = false;
|
|
|
+ // 保持流式输出的内容不变,已经是完整的AI返回内容
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存最新的分析结果
|
|
|
+ this.aiDesignAnalysisResult = analysisResult;
|
|
|
+
|
|
|
+ // 保存对话记录到项目
|
|
|
+ await this.saveChatHistory();
|
|
|
+
|
|
|
+ console.log('✅ AI对话完成');
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ AI对话失败:', error);
|
|
|
+
|
|
|
+ // 移除流式输出消息或加载消息
|
|
|
+ this.aiChatMessages = this.aiChatMessages.filter(m => !m.isLoading && !m.isStreaming);
|
|
|
+
|
|
|
+ // 添加错误消息
|
|
|
+ this.aiChatMessages.push({
|
|
|
+ id: `ai-error-${Date.now()}`,
|
|
|
+ role: 'assistant' as const,
|
|
|
+ content: `抱歉,分析过程出现错误:${error.message || '未知错误'}。请重试或修改您的问题。`,
|
|
|
+ timestamp: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ window?.fmode?.alert('AI分析失败: ' + (error.message || '未知错误'));
|
|
|
+ } finally {
|
|
|
+ this.aiDesignAnalyzing = false;
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化AI响应(优化显示效果)
|
|
|
+ */
|
|
|
+ private formatAIResponse(analysisResult: any): string {
|
|
|
+ if (!analysisResult) {
|
|
|
+ return '❌ 分析结果为空';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有原始内容,直接使用(AI返回的是纯文字格式)
|
|
|
+ if (analysisResult.rawContent) {
|
|
|
+ return analysisResult.rawContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 否则格式化结构化数据(兼容模式)
|
|
|
+ let response = '';
|
|
|
+
|
|
|
+ // 添加分析标题
|
|
|
+ response += `# ✨ AI设计分析报告\n\n`;
|
|
|
+ response += `> 基于豆包1.6模型的"灯光+材质"细节落地分析\n\n`;
|
|
|
+ response += `---\n\n`;
|
|
|
+
|
|
|
+ // 场景识别
|
|
|
+ if (analysisResult.sceneRecognition && Object.keys(analysisResult.sceneRecognition).length > 0) {
|
|
|
+ response += `## 🏠 场景识别\n\n`;
|
|
|
+ if (analysisResult.sceneRecognition.spaceType) {
|
|
|
+ response += `- **空间类型**:${analysisResult.sceneRecognition.spaceType}\n`;
|
|
|
+ }
|
|
|
+ if (analysisResult.sceneRecognition.confidence) {
|
|
|
+ response += `- **识别置信度**:${analysisResult.sceneRecognition.confidence}\n`;
|
|
|
+ }
|
|
|
+ if (analysisResult.sceneRecognition.evidence) {
|
|
|
+ response += `- **识别依据**:${analysisResult.sceneRecognition.evidence}\n`;
|
|
|
+ }
|
|
|
+ response += '\n';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 整体基调
|
|
|
+ if (analysisResult.overallTone && Object.keys(analysisResult.overallTone).length > 0) {
|
|
|
+ response += `## 🎭 整体基调\n\n`;
|
|
|
+ if (analysisResult.overallTone.primary) {
|
|
|
+ response += `- **主基调**:${analysisResult.overallTone.primary}\n`;
|
|
|
+ }
|
|
|
+ if (analysisResult.overallTone.secondary) {
|
|
|
+ response += `- **次基调**:${analysisResult.overallTone.secondary}\n`;
|
|
|
+ }
|
|
|
+ if (analysisResult.overallTone.description) {
|
|
|
+ response += `- **基调特征**:${analysisResult.overallTone.description}\n`;
|
|
|
+ }
|
|
|
+ response += '\n';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设计维度分析
|
|
|
+ if (analysisResult.designDimensions) {
|
|
|
+ response += `## 🎨 设计维度分析\n\n`;
|
|
|
+
|
|
|
+ const dims = analysisResult.designDimensions;
|
|
|
+
|
|
|
+ if (dims.colorSystem) {
|
|
|
+ response += `### 🌈 色彩系统\n\n`;
|
|
|
+ if (dims.colorSystem.primaryColors?.length > 0) {
|
|
|
+ response += `- **主色调**:${dims.colorSystem.primaryColors.join(', ')}\n`;
|
|
|
+ }
|
|
|
+ if (dims.colorSystem.secondaryColors?.length > 0) {
|
|
|
+ response += `- **辅助色**:${dims.colorSystem.secondaryColors.join(', ')}\n`;
|
|
|
+ }
|
|
|
+ response += '\n';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dims.lightingDesign) {
|
|
|
+ response += `### 💡 灯光设计\n\n`;
|
|
|
+ response += `${dims.lightingDesign.description || '暂无描述'}\n\n`;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dims.materialAnalysis) {
|
|
|
+ response += `### 🏗️ 材质分析\n\n`;
|
|
|
+ response += `${dims.materialAnalysis.description || '暂无描述'}\n\n`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 优化建议
|
|
|
+ if (analysisResult.suggestions && analysisResult.suggestions.length > 0) {
|
|
|
+ response += `## ✨ 专业优化建议\n\n`;
|
|
|
+ analysisResult.suggestions.forEach((suggestion: string, index: number) => {
|
|
|
+ response += `${index + 1}. ${suggestion}\n`;
|
|
|
+ });
|
|
|
+ response += '\n';
|
|
|
+ }
|
|
|
+
|
|
|
+ return this.enhanceMarkdownFormat(response);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 增强Markdown格式(添加更多视觉元素)
|
|
|
+ */
|
|
|
+ private enhanceMarkdownFormat(content: string): string {
|
|
|
+ // 为表格添加样式标记
|
|
|
+ let enhanced = content;
|
|
|
+
|
|
|
+ // 确保表格格式正确
|
|
|
+ enhanced = enhanced.replace(/\n\|/g, '\n|');
|
|
|
+
|
|
|
+ // 为重要信息添加强调
|
|
|
+ enhanced = enhanced.replace(/RGB\((\d+),\s*(\d+),\s*(\d+)\)/g, '**RGB($1, $2, $3)**');
|
|
|
+ enhanced = enhanced.replace(/(\d+)lux/g, '**$1lux**');
|
|
|
+ enhanced = enhanced.replace(/(\d+)K/g, '**$1K**');
|
|
|
+ enhanced = enhanced.replace(/Ra(\d+)/g, '**Ra$1**');
|
|
|
+ enhanced = enhanced.replace(/(\d+)mm/g, '**$1mm**');
|
|
|
+
|
|
|
+ return enhanced;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化消息内容 - 纯文本美观排版
|
|
|
+ */
|
|
|
+ formatMessageContent(content: string): string {
|
|
|
+ if (!content) return '';
|
|
|
+
|
|
|
+ let formatted = content;
|
|
|
+
|
|
|
+ // 将连续的换行符转换为段落
|
|
|
+ formatted = formatted.replace(/\n\n+/g, '</p><p class="content-paragraph">');
|
|
|
+
|
|
|
+ // 单个换行符转换为 <br>
|
|
|
+ formatted = formatted.replace(/\n/g, '<br>');
|
|
|
+
|
|
|
+ // 包裹在段落标签中
|
|
|
+ formatted = `<p class="content-paragraph">${formatted}</p>`;
|
|
|
+
|
|
|
+ // 识别并标记标题行(以中文冒号或数字开头的行)
|
|
|
+ formatted = formatted.replace(/<p class="content-paragraph">([一二三四五六七八九十]+、.*?)<br>/g, '<h4 class="content-heading">$1</h4><p class="content-paragraph">');
|
|
|
+ formatted = formatted.replace(/<p class="content-paragraph">(\d+[\.|、].*?)<br>/g, '<h4 class="content-heading">$1</h4><p class="content-paragraph">');
|
|
|
+
|
|
|
+ // 识别列表项(以 - 或 • 开头)
|
|
|
+ formatted = formatted.replace(/- (.*?)<br>/g, '<li class="content-list-item">$1</li>');
|
|
|
+ formatted = formatted.replace(/• (.*?)<br>/g, '<li class="content-list-item">$1</li>');
|
|
|
+
|
|
|
+ // 包裹连续的列表项
|
|
|
+ formatted = formatted.replace(/(<li class="content-list-item">.*?<\/li>)+/g, '<ul class="content-list">$&</ul>');
|
|
|
+
|
|
|
+ // 清理空段落
|
|
|
+ formatted = formatted.replace(/<p class="content-paragraph"><\/p>/g, '');
|
|
|
+
|
|
|
+ return formatted;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 保存对话历史到项目
|
|
|
+ */
|
|
|
+ private async saveChatHistory(): Promise<void> {
|
|
|
+ if (!this.project || !this.aiDesignCurrentSpace?.id) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const projectData = this.project.get('data') || {};
|
|
|
+
|
|
|
+ if (!projectData.aiChatHistory) {
|
|
|
+ projectData.aiChatHistory = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ projectData.aiChatHistory[this.aiDesignCurrentSpace.id] = {
|
|
|
+ messages: this.aiChatMessages.map(m => ({
|
|
|
+ role: m.role,
|
|
|
+ content: m.content,
|
|
|
+ timestamp: m.timestamp.toISOString(),
|
|
|
+ images: m.images
|
|
|
+ })),
|
|
|
+ lastUpdated: new Date().toISOString()
|
|
|
+ };
|
|
|
+
|
|
|
+ this.project.set('data', projectData);
|
|
|
+ await this.project.save();
|
|
|
+
|
|
|
+ console.log('💾 对话历史已保存');
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存对话历史失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 滚动到底部
|
|
|
+ */
|
|
|
+ private scrollToBottom(): void {
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.chatMessagesWrapper) {
|
|
|
+ const element = this.chatMessagesWrapper.nativeElement;
|
|
|
+ element.scrollTop = element.scrollHeight;
|
|
|
+ }
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 打开附件对话框
|
|
|
+ */
|
|
|
+ openAttachmentDialog(): void {
|
|
|
+ console.log('📎 打开附件对话框');
|
|
|
+ // 触发文件上传
|
|
|
+ const fileInput = document.getElementById('aiDesignFileInput') as HTMLInputElement;
|
|
|
+ if (fileInput) {
|
|
|
+ fileInput.click();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 切换深度思考模式
|
|
|
+ */
|
|
|
+ toggleDeepThinking(): void {
|
|
|
+ this.deepThinkingEnabled = !this.deepThinkingEnabled;
|
|
|
+ console.log('💡 深度思考模式:', this.deepThinkingEnabled ? '已开启' : '已关闭');
|
|
|
+
|
|
|
+ const status = this.deepThinkingEnabled ? '已开启' : '已关闭';
|
|
|
+ window?.fmode?.alert(`深度思考模式${status}`);
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 复制消息内容
|
|
|
+ */
|
|
|
+ copyMessage(content: string): void {
|
|
|
+ if (!content) return;
|
|
|
+
|
|
|
+ // 移除HTML标签
|
|
|
+ const textContent = content.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
|
|
+
|
|
|
+ navigator.clipboard.writeText(textContent).then(() => {
|
|
|
+ console.log('📋 消息已复制');
|
|
|
+ window?.fmode?.alert('消息已复制到剪贴板');
|
|
|
+ }).catch(err => {
|
|
|
+ console.error('复制失败:', err);
|
|
|
+ window?.fmode?.alert('复制失败');
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 重新生成消息
|
|
|
+ */
|
|
|
+ async regenerateMessage(message: any): Promise<void> {
|
|
|
+ // 找到该消息的索引
|
|
|
+ const index = this.aiChatMessages.findIndex(m => m.id === message.id);
|
|
|
+ if (index === -1) return;
|
|
|
+
|
|
|
+ // 找到之前的用户消息
|
|
|
+ let userMessageIndex = index - 1;
|
|
|
+ while (userMessageIndex >= 0 && this.aiChatMessages[userMessageIndex].role !== 'user') {
|
|
|
+ userMessageIndex--;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (userMessageIndex < 0) {
|
|
|
+ window?.fmode?.alert('找不到对应的用户消息');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const userMessage = this.aiChatMessages[userMessageIndex];
|
|
|
+
|
|
|
+ // 移除当前AI消息
|
|
|
+ this.aiChatMessages.splice(index, 1);
|
|
|
+
|
|
|
+ // 重新发送
|
|
|
+ this.aiChatInput = userMessage.content;
|
|
|
+ await this.sendChatMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 点赞消息
|
|
|
+ */
|
|
|
+ likeMessage(message: any): void {
|
|
|
+ message.liked = !message.liked;
|
|
|
+ if (message.liked) {
|
|
|
+ message.disliked = false;
|
|
|
+ }
|
|
|
+ console.log('👍 消息反馈: 有帮助');
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 点踩消息
|
|
|
+ */
|
|
|
+ dislikeMessage(message: any): void {
|
|
|
+ message.disliked = !message.disliked;
|
|
|
+ if (message.disliked) {
|
|
|
+ message.liked = false;
|
|
|
+ }
|
|
|
+ console.log('👎 消息反馈: 无帮助');
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清空对话
|
|
|
+ */
|
|
|
+ clearChat(): void {
|
|
|
+ if (this.aiChatMessages.length === 0) return;
|
|
|
+
|
|
|
+ if (confirm('确定要清空所有对话记录吗?')) {
|
|
|
+ this.aiChatMessages = [];
|
|
|
+ this.aiChatInput = '';
|
|
|
+ console.log('🗑️ 对话已清空');
|
|
|
+ window?.fmode?.alert('对话已清空');
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出对话
|
|
|
+ */
|
|
|
+ exportChat(): void {
|
|
|
+ if (this.aiChatMessages.length === 0) {
|
|
|
+ window?.fmode?.alert('没有对话记录可导出');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let exportContent = `# AI设计分析对话记录\n\n`;
|
|
|
+ exportContent += `项目: ${this.project?.get('name') || '未命名项目'}\n`;
|
|
|
+ exportContent += `空间: ${this.aiDesignCurrentSpace?.name || '未知空间'}\n`;
|
|
|
+ exportContent += `导出时间: ${new Date().toLocaleString()}\n\n`;
|
|
|
+ exportContent += `---\n\n`;
|
|
|
+
|
|
|
+ this.aiChatMessages.forEach((message, index) => {
|
|
|
+ const role = message.role === 'user' ? '👤 用户' : '🤖 AI助手';
|
|
|
+ const time = message.timestamp.toLocaleTimeString();
|
|
|
+ const content = message.content.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
|
|
+
|
|
|
+ exportContent += `## ${role} [${time}]\n\n`;
|
|
|
+ exportContent += `${content}\n\n`;
|
|
|
+
|
|
|
+ if (message.images && message.images.length > 0) {
|
|
|
+ exportContent += `附图: ${message.images.length} 张\n\n`;
|
|
|
+ }
|
|
|
+
|
|
|
+ exportContent += `---\n\n`;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建下载
|
|
|
+ const blob = new Blob([exportContent], { type: 'text/markdown;charset=utf-8' });
|
|
|
+ const url = URL.createObjectURL(blob);
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = url;
|
|
|
+ link.download = `AI对话记录_${this.aiDesignCurrentSpace?.name}_${Date.now()}.md`;
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ URL.revokeObjectURL(url);
|
|
|
+
|
|
|
+ console.log('💾 对话已导出');
|
|
|
+ window?.fmode?.alert('对话已导出');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 确认当前分析结果
|
|
|
+ */
|
|
|
+ async confirmCurrentAnalysis(): Promise<void> {
|
|
|
+ if (this.aiChatMessages.length === 0) {
|
|
|
+ window?.fmode?.alert('没有分析结果可确认');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 整合所有AI回复作为最终报告
|
|
|
+ const aiReplies = this.aiChatMessages
|
|
|
+ .filter(m => m.role === 'assistant' && m.content && !m.isLoading)
|
|
|
+ .map(m => m.content)
|
|
|
+ .join('\n\n---\n\n');
|
|
|
+
|
|
|
+ this.aiDesignReport = aiReplies;
|
|
|
+ this.aiDesignReportConfirmed = true;
|
|
|
+
|
|
|
+ // 保存到项目
|
|
|
+ if (this.project && this.aiDesignCurrentSpace?.id) {
|
|
|
+ const projectData = this.project.get('data') || {};
|
|
|
+
|
|
|
+ if (!projectData.designReports) {
|
|
|
+ projectData.designReports = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ projectData.designReports[this.aiDesignCurrentSpace.id] = {
|
|
|
+ report: this.aiDesignReport,
|
|
|
+ analysisData: this.aiDesignAnalysisResult,
|
|
|
+ images: this.aiDesignUploadedImages,
|
|
|
+ files: this.aiDesignUploadedFiles,
|
|
|
+ chatHistory: this.aiChatMessages.map(m => ({
|
|
|
+ role: m.role,
|
|
|
+ content: m.content,
|
|
|
+ timestamp: m.timestamp.toISOString()
|
|
|
+ })),
|
|
|
+ confirmedAt: new Date().toISOString(),
|
|
|
+ confirmedBy: this.currentUser?.id || 'unknown'
|
|
|
+ };
|
|
|
+
|
|
|
+ this.project.set('data', projectData);
|
|
|
+ await this.project.save();
|
|
|
+
|
|
|
+ console.log('✅ 分析结果已确认并保存');
|
|
|
+
|
|
|
+ // 询问是否生成客户报告
|
|
|
+ const result = await window?.fmode?.confirm(
|
|
|
+ '分析结果已保存!\n\n是否立即生成客户报告?\n客户报告将整理分析结果为专业的结构化文档,可直接发送给客户。'
|
|
|
+ );
|
|
|
+
|
|
|
+ if (result) {
|
|
|
+ await this.generateAndShowClientReport();
|
|
|
+ } else {
|
|
|
+ window?.fmode?.alert('已保存分析结果,您可以稍后在报告区域生成客户报告。');
|
|
|
+ }
|
|
|
+
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 确认分析结果失败:', error);
|
|
|
+ window?.fmode?.alert('确认失败,请重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成并显示客户报告
|
|
|
+ */
|
|
|
+ async generateAndShowClientReport(): Promise<void> {
|
|
|
+ if (!this.aiDesignReport) {
|
|
|
+ window?.fmode?.alert('请先确认分析结果');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('🤖 正在生成客户报告...');
|
|
|
+
|
|
|
+ const loading = window?.fmode?.loading('正在生成客户报告,请稍候...');
|
|
|
+
|
|
|
+ // 调用AI服务生成结构化客户报告
|
|
|
+ const clientReport = await this.designAnalysisAIService.generateClientReport({
|
|
|
+ analysisData: {
|
|
|
+ report: this.aiDesignReport,
|
|
|
+ analysisResult: this.aiDesignAnalysisResult,
|
|
|
+ spaceInfo: this.aiDesignCurrentSpace
|
|
|
+ },
|
|
|
+ spaceName: this.aiDesignCurrentSpace?.name || '未命名空间',
|
|
|
+ onContentChange: (content) => {
|
|
|
+ if (loading) {
|
|
|
+ loading.message = '正在生成报告...' + content.length + '字';
|
|
|
+ }
|
|
|
+ },
|
|
|
+ loading
|
|
|
+ });
|
|
|
+
|
|
|
+ loading?.close();
|
|
|
+
|
|
|
+ // 保存客户报告
|
|
|
+ if (this.project && this.aiDesignCurrentSpace?.id) {
|
|
|
+ const projectData = this.project.get('data') || {};
|
|
|
+
|
|
|
+ if (!projectData.designReports) {
|
|
|
+ projectData.designReports = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!projectData.designReports[this.aiDesignCurrentSpace.id]) {
|
|
|
+ projectData.designReports[this.aiDesignCurrentSpace.id] = {};
|
|
|
+ }
|
|
|
+
|
|
|
+ projectData.designReports[this.aiDesignCurrentSpace.id].clientReport = clientReport;
|
|
|
+ projectData.designReports[this.aiDesignCurrentSpace.id].clientReportGeneratedAt = new Date().toISOString();
|
|
|
+
|
|
|
+ this.project.set('data', projectData);
|
|
|
+ await this.project.save();
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('✅ 客户报告生成完成');
|
|
|
+
|
|
|
+ // 显示报告预览对话框
|
|
|
+ await window?.fmode?.confirm(
|
|
|
+ `客户报告生成成功!\n\n报告已保存,您可以:\n1. 在项目详情中查看完整报告\n2. 导出为PDF发送给客户\n3. 复制内容分享给客户\n\n是否立即查看报告?`
|
|
|
+ );
|
|
|
+
|
|
|
+ // TODO: 这里可以打开报告预览对话框或跳转到报告页面
|
|
|
+
|
|
|
+ } catch (error: any) {
|
|
|
+ console.error('❌ 生成客户报告失败:', error);
|
|
|
+ window?.fmode?.alert('生成报告失败: ' + (error.message || '未知错误'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 打开技能对话框
|
|
|
+ */
|
|
|
+ openSkillsDialog(): void {
|
|
|
+ console.log('🧩 打开技能对话框');
|
|
|
+ window?.fmode?.alert('技能功能开发中...');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 剪切文本
|
|
|
+ */
|
|
|
+ cutText(): void {
|
|
|
+ const selection = window.getSelection();
|
|
|
+ const selectedText = selection?.toString();
|
|
|
+
|
|
|
+ if (selectedText) {
|
|
|
+ // 复制到剪贴板
|
|
|
+ navigator.clipboard.writeText(selectedText).then(() => {
|
|
|
+ console.log('✂️ 文本已剪切:', selectedText);
|
|
|
+ window?.fmode?.alert('文本已剪切');
|
|
|
+
|
|
|
+ // 删除选中的文本
|
|
|
+ const textarea = document.querySelector('.compact-editor-input') as HTMLTextAreaElement;
|
|
|
+ if (textarea) {
|
|
|
+ const start = textarea.selectionStart;
|
|
|
+ const end = textarea.selectionEnd;
|
|
|
+ const text = textarea.value;
|
|
|
+ this.aiDesignTextDescription = text.substring(0, start) + text.substring(end);
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ }
|
|
|
+ }).catch(err => {
|
|
|
+ console.error('剪切失败:', err);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ window?.fmode?.alert('请先选择要剪切的文本');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开始语音通话
|
|
|
+ */
|
|
|
+ startVoiceCall(): void {
|
|
|
+ console.log('📞 开始语音通话');
|
|
|
+ window?.fmode?.alert('语音通话功能开发中...');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 开始语音输入
|
|
|
+ */
|
|
|
+ startVoiceInput(): void {
|
|
|
+ console.log('🎤 开始语音输入');
|
|
|
+
|
|
|
+ // 检查浏览器是否支持语音识别
|
|
|
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
|
|
+
|
|
|
+ if (!SpeechRecognition) {
|
|
|
+ window?.fmode?.alert('您的浏览器不支持语音识别功能');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const recognition = new SpeechRecognition();
|
|
|
+ recognition.lang = 'zh-CN';
|
|
|
+ recognition.continuous = false;
|
|
|
+ recognition.interimResults = false;
|
|
|
+
|
|
|
+ recognition.onstart = () => {
|
|
|
+ console.log('🎤 语音识别已启动');
|
|
|
+ window?.fmode?.alert('请开始说话...');
|
|
|
+ };
|
|
|
+
|
|
|
+ recognition.onresult = (event: any) => {
|
|
|
+ const transcript = event.results[0][0].transcript;
|
|
|
+ console.log('🎤 识别结果:', transcript);
|
|
|
+
|
|
|
+ // 将识别结果添加到文本框
|
|
|
+ if (this.aiDesignTextDescription) {
|
|
|
+ this.aiDesignTextDescription += ' ' + transcript;
|
|
|
+ } else {
|
|
|
+ this.aiDesignTextDescription = transcript;
|
|
|
+ }
|
|
|
+ this.cdr.markForCheck();
|
|
|
+ };
|
|
|
+
|
|
|
+ recognition.onerror = (event: any) => {
|
|
|
+ console.error('🎤 语音识别错误:', event.error);
|
|
|
+ window?.fmode?.alert('语音识别失败: ' + event.error);
|
|
|
+ };
|
|
|
+
|
|
|
+ recognition.onend = () => {
|
|
|
+ console.log('🎤 语音识别已结束');
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ recognition.start();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('启动语音识别失败:', error);
|
|
|
+ window?.fmode?.alert('启动语音识别失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|