From 07ecd9cee7fd3c6f061e90fa1def5c388e759de7 Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 21 Jul 2025 22:56:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E4=B8=BA=E5=A5=B3=E8=A3=85=E7=A9=BF=E6=90=AD?= =?UTF-8?q?=E4=B8=93=E4=B8=9A=E9=A1=BE=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新UI主题为粉色系,适配女装穿搭业务 - 默认展示12张图片卡片,支持展开查看全部 - 默认隐藏AI文字回答,点击查看详情时显示 - 新增智能标签汇总功能,支持多选标签生成搜索 - 优化提示词和建议问题,专注女装穿搭场景 - 修复加载状态显示问题,优化用户体验 - 支持gs://到Google Storage的URI转换 - 增强图片卡片交互,悬停显示查看原图按钮 --- .../src/infrastructure/gemini_service.rs | 96 +- .../infrastructure/gemini_service_tests.rs | 2 +- .../commands/rag_grounding_commands.rs | 1 + apps/desktop/src/App.tsx | 8 + apps/desktop/src/components/ChatInterface.tsx | 963 ++++++++++++++++++ apps/desktop/src/data/tools.ts | 24 +- apps/desktop/src/pages/tools/ChatTestPage.tsx | 215 ++++ apps/desktop/src/pages/tools/ChatTool.tsx | 186 ++++ .../src/services/ragGroundingService.ts | 38 + apps/desktop/src/types/ragGrounding.ts | 5 +- apps/desktop/src/utils/testChatFunction.ts | 254 +++++ docs/rag-grounding-api.md | 2 +- 12 files changed, 1772 insertions(+), 22 deletions(-) create mode 100644 apps/desktop/src/components/ChatInterface.tsx create mode 100644 apps/desktop/src/pages/tools/ChatTestPage.tsx create mode 100644 apps/desktop/src/pages/tools/ChatTool.tsx create mode 100644 apps/desktop/src/utils/testChatFunction.ts diff --git a/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs b/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs index 38f56cb..84997d1 100644 --- a/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs +++ b/apps/desktop/src-tauri/src/infrastructure/gemini_service.rs @@ -8,7 +8,7 @@ use tokio::fs; use reqwest::multipart; // 导入容错JSON解析器 -use crate::infrastructure::tolerant_json_parser::{TolerantJsonParser, ParserConfig}; +use crate::infrastructure::tolerant_json_parser::{TolerantJsonParser, ParserConfig, RecoveryStrategy}; use std::sync::{Arc, Mutex}; /// Gemini API配置 @@ -165,7 +165,7 @@ impl Default for RagGroundingConfig { Self { project_id: "gen-lang-client-0413414134".to_string(), location: "global".to_string(), - data_store_id: "default_data_store".to_string(), + data_store_id: "searchable-model-images_1752827560253".to_string(), // 使用存在的数据存储 model_id: "gemini-2.5-flash".to_string(), temperature: 1.0, max_output_tokens: 8192, @@ -203,8 +203,7 @@ pub struct GroundingMetadata { pub struct GroundingSource { pub title: String, pub uri: Option, - pub snippet: String, - pub relevance_score: Option, + pub content: Option, } /// Vertex AI Search 工具配置 @@ -969,6 +968,7 @@ impl GeminiService { match self.send_rag_grounding_request(&generate_url, &client_config, &request_data).await { Ok(response) => { let elapsed = start_time.elapsed(); + println!("✅ RAG Grounding查询完成,耗时: {:?}", elapsed); return Ok(RagGroundingResponse { @@ -1025,8 +1025,25 @@ impl GeminiService { /// 解析RAG Grounding响应 fn parse_rag_grounding_response(&self, response_text: &str) -> Result { - let response_json: serde_json::Value = serde_json::from_str(response_text) - .map_err(|e| anyhow!("解析RAG响应JSON失败: {}", e))?; + // 使用容错JSON解析器 + let mut parser = TolerantJsonParser::new(Some(ParserConfig { + max_text_length: 10_000_000, // 10MB限制 + enable_comments: true, + enable_unquoted_keys: true, + enable_trailing_commas: true, + timeout_ms: 30000, // 30秒超时 + recovery_strategies: vec![ + RecoveryStrategy::StandardJson, + RecoveryStrategy::ManualFix, + RecoveryStrategy::RegexExtract, + RecoveryStrategy::PartialParse, + ], + }))?; + + let (response_json, parse_stats) = parser.parse(response_text) + .map_err(|e| anyhow!("容错JSON解析失败: {}", e))?; + + // 提取主要回答内容 let answer = response_json @@ -1061,18 +1078,52 @@ impl GeminiService { .and_then(|arr| arr.first()) .and_then(|candidate| candidate.get("groundingMetadata"))?; + // 打印grounding元数据的原始结构 + /** + * "groundingMetadata": { + "retrievalQueries": [ + "comfortable clothes for hot weather travel women", + "modest clothing tips Thailand tourist female", + "what to wear in Thailand female tourist summer", + "white woman tropical vacation outfits Thailand summer" + ], + "groundingChunks": [ + { + "retrievedContext": { + "uri": "gs://fashion_image_block/gallery_v2/models/Instagram_21129657_FREYA_KILLIN_20230828155546_1.jpg", + "title": "model", + "text": {} + } + }, + */ let sources = grounding_metadata - .get("groundingSources") + .get("groundingChunks") .and_then(|sources| sources.as_array()) .map(|sources_array| { + sources_array .iter() - .filter_map(|source| { + .enumerate() + .filter_map(|(index, chunk)| { + // 从 retrievedContext 中获取数据 + let retrieved_context = chunk.get("retrievedContext")?; + + let title = retrieved_context.get("title")?.as_str()?.to_string(); + + // 这个uri需要通过 convert_s3_to_cdn_url 转换 + let uri = retrieved_context.get("uri") + .and_then(|u| u.as_str()) + .map(|s| Self::convert_s3_to_cdn_url(s)); + + // 这个是个json + let content = retrieved_context.get("text").cloned(); + + + Some(GroundingSource { - title: source.get("title")?.as_str()?.to_string(), - uri: source.get("uri").and_then(|u| u.as_str()).map(|s| s.to_string()), - snippet: source.get("snippet")?.as_str()?.to_string(), - relevance_score: source.get("relevanceScore").and_then(|s| s.as_f64()).map(|f| f as f32), + title, + uri, + content, }) }) .collect() @@ -1080,7 +1131,7 @@ impl GeminiService { .unwrap_or_default(); let search_queries = grounding_metadata - .get("searchQueries") + .get("retrievalQueries") .and_then(|queries| queries.as_array()) .map(|queries_array| { queries_array @@ -1095,6 +1146,25 @@ impl GeminiService { search_queries, }) } + + fn convert_s3_to_cdn_url(s3_url: &str) -> String { + if s3_url.starts_with("s3://ap-northeast-2/modal-media-cache/") { + // 将 s3://ap-northeast-2/modal-media-cache/ 替换为 https://cdn.roasmax.cn/ + s3_url.replace("s3://ap-northeast-2/modal-media-cache/", "https://cdn.roasmax.cn/") + } else if s3_url.starts_with("gs://fashion_image_block/") { + // 将 gs://fashion_image_block/ 替换为 https://cdn.roasmax.cn/fashion_image_block/ + s3_url.replace("gs://", "https://storage.googleapis.com/") + } else if s3_url.starts_with("gs://") { + // 处理其他 gs:// 格式,转换为通用CDN格式 + s3_url.replace("gs://", "https://storage.googleapis.com/") + } else if s3_url.starts_with("s3://") { + // 处理其他 s3:// 格式,转换为通用CDN格式 + s3_url.replace("s3://", "https://cdn.roasmax.cn/") + } else { + // 如果不是预期的S3格式,返回原URL + s3_url.to_string() + } + } } // 导入测试文件 diff --git a/apps/desktop/src-tauri/src/infrastructure/gemini_service_tests.rs b/apps/desktop/src-tauri/src/infrastructure/gemini_service_tests.rs index 1e8f412..56d3b87 100644 --- a/apps/desktop/src-tauri/src/infrastructure/gemini_service_tests.rs +++ b/apps/desktop/src-tauri/src/infrastructure/gemini_service_tests.rs @@ -26,7 +26,7 @@ mod rag_grounding_tests { assert_eq!(config.project_id, "gen-lang-client-0413414134"); assert_eq!(config.location, "global"); - assert_eq!(config.data_store_id, "default_data_store"); + assert_eq!(config.data_store_id, "jeans_pattern_data_store"); assert_eq!(config.model_id, "gemini-2.5-flash"); assert_eq!(config.temperature, 1.0); assert_eq!(config.max_output_tokens, 8192); diff --git a/apps/desktop/src-tauri/src/presentation/commands/rag_grounding_commands.rs b/apps/desktop/src-tauri/src/presentation/commands/rag_grounding_commands.rs index ae7a3b9..79695b6 100644 --- a/apps/desktop/src-tauri/src/presentation/commands/rag_grounding_commands.rs +++ b/apps/desktop/src-tauri/src/presentation/commands/rag_grounding_commands.rs @@ -26,6 +26,7 @@ pub async fn query_rag_grounding( })?; println!("✅ RAG Grounding查询成功,响应时间: {}ms", response.response_time_ms); + Ok(response) } diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index f09a441..8257bc0 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -13,6 +13,8 @@ import Tools from './pages/Tools'; import DataCleaningTool from './pages/tools/DataCleaningTool'; import JsonParserTool from './pages/tools/JsonParserTool'; import DebugPanelTool from './pages/tools/DebugPanelTool'; +import ChatTool from './pages/tools/ChatTool'; +import ChatTestPage from './pages/tools/ChatTestPage'; import OutfitMatch from './pages/OutfitMatch'; import Navigation from './components/Navigation'; import { NotificationSystem, useNotifications } from './components/NotificationSystem'; @@ -22,6 +24,10 @@ import { CreateProjectRequest, UpdateProjectRequest } from './types/project'; import "./App.css"; import './styles/design-system.css'; import './styles/animations.css'; +// 导入测试工具(仅在开发环境) +if (import.meta.env.DEV) { + import('./utils/testChatFunction'); +} /** * 主应用组件 * 遵循 Tauri 开发规范的应用架构设计 @@ -90,6 +96,8 @@ function App() { } /> } /> } /> + } /> + } /> diff --git a/apps/desktop/src/components/ChatInterface.tsx b/apps/desktop/src/components/ChatInterface.tsx new file mode 100644 index 0000000..7af075b --- /dev/null +++ b/apps/desktop/src/components/ChatInterface.tsx @@ -0,0 +1,963 @@ +import React, { useState, useCallback, useRef, useEffect } from 'react'; +import { + MessageCircle, + Send, + Trash2, + Bot, + User, + AlertCircle, + Sparkles, + Clock, + CheckCircle, + XCircle, + Terminal, + Copy, + ExternalLink, + Tag, + Palette, + Calendar, + MapPin +} from 'lucide-react'; +import { queryRagGrounding } from '../services/ragGroundingService'; +import { RagGroundingQueryOptions, RagGroundingResponse } from '../types/ragGrounding'; + +/** + * 聊天消息接口 + */ +interface ChatMessage { + id: string; + type: 'user' | 'assistant'; + content: string; + timestamp: Date; + status?: 'sending' | 'sent' | 'error'; + metadata?: { + responseTime?: number; + modelUsed?: string; + sources?: Array<{ + title: string; + uri?: string; + content?: any; + }>; + }; +} + +/** + * 聊天界面组件属性 + */ +interface ChatInterfaceProps { + /** 会话ID,用于上下文保持 */ + sessionId?: string; + /** 最大保留消息数量 */ + maxMessages?: number; + /** 是否显示来源信息 */ + showSources?: boolean; + /** 自定义样式类名 */ + className?: string; + /** 占位符文本 */ + placeholder?: string; +} + +/** + * 聊天界面组件 + * 遵循 Tauri 开发规范和 ag-ui 设计标准 + * 支持上下文保留和最新3条记录限制 + */ +export const ChatInterface: React.FC = ({ + sessionId = 'default-session', + maxMessages = 3, + showSources = true, + className = '', + placeholder = '描述您的穿搭需求,比如场合、风格、身材特点等...' +}) => { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const [expandedSources, setExpandedSources] = useState>({}); + const [showAnswers, setShowAnswers] = useState>({}); + const [selectedTags, setSelectedTags] = useState([]); + const [allTags, setAllTags] = useState([]); + + const inputRef = useRef(null); + const chatContainerRef = useRef(null); + const messagesEndRef = useRef(null); + + // 自动滚动到底部 + const scrollToBottom = useCallback(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, []); + + // 当消息更新时自动滚动 + useEffect(() => { + scrollToBottom(); + }, [messages, scrollToBottom]); + + // 解析图片内容数据 + const parseImageContent = useCallback((content: any) => { + if (!content || typeof content !== 'object') return null; + + try { + return { + categories: content.categories || [], + description: content.description || '', + environment_tags: content.environment_tags || [], + environment_color: content.environment_color_pattern?.rgb_hex || '#f0f0f0', + models: content.models || [], + releaseDate: content.releaseDate || '', + runtime: content.runtime || '' + }; + } catch (error) { + console.error('解析图片内容失败:', error); + return null; + } + }, []); + + // 渲染图片卡片 + const renderImageCard = useCallback((source: any, index: number) => { + const imageData = parseImageContent(source.content); + if (!imageData || !source.uri) return null; + + return ( +
+ {/* 图片 */} +
+ {source.title { + const target = e.target as HTMLImageElement; + target.parentElement?.querySelector('.loading-placeholder')?.classList.add('hidden'); + }} + onError={(e) => { + const target = e.target as HTMLImageElement; + target.style.display = 'none'; + target.parentElement?.querySelector('.error-placeholder')?.classList.remove('hidden'); + target.parentElement?.querySelector('.loading-placeholder')?.classList.add('hidden'); + }} + /> + + {/* 加载占位符 */} +
+
+
+ + {/* 错误占位符 */} +
+
+ + 加载失败 +
+
+ + {/* 运行时间标签 */} + {imageData.runtime && ( +
+ + {imageData.runtime} +
+ )} + + {/* 悬停时显示的查看按钮 */} +
+ +
+
+ + {/* 内容信息 */} +
+ {/* 描述 */} + {imageData.description && ( +

+ {imageData.description} +

+ )} + + {/* 分类标签 */} + {imageData.categories.length > 0 && ( +
+ {imageData.categories.slice(0, 4).map((category: string, catIndex: number) => ( + + + {category} + + ))} + {imageData.categories.length > 4 && ( + +{imageData.categories.length - 4} + )} +
+ )} + + {/* 环境标签 */} + {imageData.environment_tags.length > 0 && ( +
+ {imageData.environment_tags.slice(0, 3).map((tag: string, tagIndex: number) => ( + + + {tag} + + ))} +
+ )} + + {/* 底部信息栏 */} +
+ {imageData.environment_color && ( +
+ +
+
+ )} + + {imageData.releaseDate && ( +
+ + {new Date(imageData.releaseDate).toLocaleDateString('zh-CN')} +
+ )} + + {/* 模特数量 */} + {imageData.models.length > 0 && ( +
+ + {imageData.models.length} +
+ )} +
+
+
+ ); + }, [parseImageContent]); + + // 生成消息ID + const generateMessageId = useCallback(() => { + return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; + }, []); + + // 限制消息数量,保留最新的消息 + const limitMessages = useCallback((newMessages: ChatMessage[]) => { + if (newMessages.length <= maxMessages * 2) { + return newMessages; + } + + // 保留最新的 maxMessages 对话(用户+助手消息对) + const pairs: ChatMessage[][] = []; + let currentPair: ChatMessage[] = []; + + for (let i = newMessages.length - 1; i >= 0; i--) { + const message = newMessages[i]; + if (message.type === 'assistant') { + currentPair.unshift(message); + if (currentPair.length === 2) { + pairs.unshift([...currentPair]); + currentPair = []; + } + } else if (message.type === 'user') { + currentPair.unshift(message); + } + } + + // 如果有未完成的对话,也要保留 + if (currentPair.length > 0) { + pairs.unshift(currentPair); + } + + // 只保留最新的 maxMessages 对话 + const limitedPairs = pairs.slice(0, maxMessages); + return limitedPairs.flat(); + }, [maxMessages]); + + // 发送消息 + const handleSendMessage = useCallback(async () => { + if (!input.trim() || isLoading) return; + + const userMessage: ChatMessage = { + id: generateMessageId(), + type: 'user', + content: input.trim(), + timestamp: new Date(), + status: 'sent' + }; + + const assistantMessage: ChatMessage = { + id: generateMessageId(), + type: 'assistant', + content: '', + timestamp: new Date(), + status: 'sending' + }; + + // 添加用户消息和占位助手消息 + setMessages(prev => limitMessages([...prev, userMessage, assistantMessage])); + setInput(''); + setIsLoading(true); + setError(null); + + try { + const options: RagGroundingQueryOptions = { + sessionId, + includeMetadata: showSources, + timeout: 30000 + }; + + const result = await queryRagGrounding(userMessage.content, options); + + if (result.success && result.data) { + // 在控制台打印详细的回复结果 + console.group('🤖 AI 回复结果'); + console.log('📝 用户问题:', userMessage.content); + console.log('💬 AI 回答:', result.data.answer); + console.log('⏱️ 响应时间:', result.data.response_time_ms + 'ms'); + console.log('🤖 使用模型:', result.data.model_used); + console.log('📊 总耗时:', result.totalTime + 'ms'); + + if (result.data.grounding_metadata?.sources && result.data.grounding_metadata.sources.length > 0) { + console.log('📚 参考来源 (' + result.data.grounding_metadata.sources.length + ' 条):'); + result.data.grounding_metadata.sources.forEach((source, index) => { + console.log(` ${index + 1}. ${source.title}`); + if (source.content) { + console.log(` 内容: ${JSON.stringify(source.content, null, 2)}`); + } + if (source.uri) { + console.log(` 链接: ${source.uri}`); + } + }); + } + + if (result.data.grounding_metadata?.search_queries && result.data.grounding_metadata.search_queries.length > 0) { + console.log('🔍 搜索查询:', result.data.grounding_metadata.search_queries); + } + + console.log('🕐 查询时间:', new Date().toLocaleString()); + console.groupEnd(); + + // 更新助手消息 + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + content: result.data!.answer, + status: 'sent', + metadata: { + responseTime: result.data!.response_time_ms, + modelUsed: result.data!.model_used, + sources: result.data!.grounding_metadata?.sources + } + } + : msg + )); + } else { + throw new Error(result.error || '查询失败'); + } + } catch (err) { + const errorMessage = err instanceof Error ? err.message : '未知错误'; + setError(errorMessage); + + // 更新助手消息为错误状态 + setMessages(prev => prev.map(msg => + msg.id === assistantMessage.id + ? { + ...msg, + content: `抱歉,查询时出现错误:${errorMessage}`, + status: 'error' + } + : msg + )); + } finally { + setIsLoading(false); + } + }, [input, isLoading, sessionId, showSources, generateMessageId, limitMessages]); + + // 处理键盘事件 + const handleKeyPress = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }, [handleSendMessage]); + + // 清除聊天历史 + const handleClearChat = useCallback(() => { + setMessages([]); + setError(null); + }, []); + + // 重试最后一条消息 + const handleRetry = useCallback(() => { + if (messages.length >= 2) { + const lastUserMessage = messages[messages.length - 2]; + if (lastUserMessage.type === 'user') { + setInput(lastUserMessage.content); + setMessages(prev => prev.slice(0, -2)); + if (inputRef.current) { + inputRef.current.focus(); + } + } + } + }, [messages]); + + // 切换图片展开状态 + const toggleSourcesExpanded = useCallback((messageId: string) => { + setExpandedSources(prev => ({ + ...prev, + [messageId]: !prev[messageId] + })); + }, []); + + // 切换AI回答显示状态 + const toggleAnswerVisibility = useCallback((messageId: string) => { + setShowAnswers(prev => ({ + ...prev, + [messageId]: !prev[messageId] + })); + }, []); + + // 提取所有图片的标签 + const extractAllTags = useCallback(() => { + const tagSet = new Set(); + + messages.forEach(message => { + if (message.type === 'assistant' && message.metadata?.sources) { + message.metadata.sources.forEach(source => { + const imageData = parseImageContent(source.content); + if (imageData) { + // 添加分类标签 + imageData.categories.forEach(tag => tagSet.add(tag)); + // 添加环境标签 + imageData.environment_tags.forEach(tag => tagSet.add(tag)); + // 添加模特相关标签 + imageData.models.forEach(model => { + if (model.products) { + model.products.forEach((product: any) => { + if (product.category) { + product.category.forEach((cat: string) => tagSet.add(cat)); + } + }); + } + }); + } + }); + } + }); + + return Array.from(tagSet).sort(); + }, [messages, parseImageContent]); + + // 当消息更新时,更新标签列表 + useEffect(() => { + const tags = extractAllTags(); + setAllTags(tags); + }, [extractAllTags]); + + // 切换标签选择状态 + const toggleTag = useCallback((tag: string) => { + setSelectedTags(prev => + prev.includes(tag) + ? prev.filter(t => t !== tag) + : [...prev, tag] + ); + }, []); + + // 清空选中的标签 + const clearSelectedTags = useCallback(() => { + setSelectedTags([]); + }, []); + + // 根据选中的标签生成搜索文本 + const generateSearchFromTags = useCallback(() => { + if (selectedTags.length === 0) return ''; + + const tagGroups = { + style: [] as string[], + color: [] as string[], + occasion: [] as string[], + other: [] as string[] + }; + + selectedTags.forEach(tag => { + if (tag.includes('色') || tag.includes('黑') || tag.includes('白') || tag.includes('红') || tag.includes('蓝') || tag.includes('粉') || tag.includes('绿') || tag.includes('黄') || tag.includes('紫') || tag.includes('橙')) { + tagGroups.color.push(tag); + } else if (tag.includes('职场') || tag.includes('约会') || tag.includes('度假') || tag.includes('休闲') || tag.includes('正式') || tag.includes('运动')) { + tagGroups.occasion.push(tag); + } else if (tag.includes('甜美') || tag.includes('优雅') || tag.includes('性感') || tag.includes('简约') || tag.includes('复古') || tag.includes('时尚')) { + tagGroups.style.push(tag); + } else { + tagGroups.other.push(tag); + } + }); + + let searchText = '我想要'; + if (tagGroups.style.length > 0) { + searchText += `${tagGroups.style.join('、')}风格的`; + } + if (tagGroups.color.length > 0) { + searchText += `${tagGroups.color.join('、')}`; + } + if (tagGroups.occasion.length > 0) { + searchText += `适合${tagGroups.occasion.join('、')}的`; + } + if (tagGroups.other.length > 0) { + searchText += `${tagGroups.other.join('、')}`; + } + searchText += '穿搭推荐'; + + return searchText; + }, [selectedTags]); + + // 复制消息内容到剪贴板 + const handleCopyMessage = useCallback(async (message: ChatMessage) => { + try { + await navigator.clipboard.writeText(message.content); + console.log('📋 已复制到剪贴板:', message.content); + + // 如果是AI回复,同时在控制台显示详细信息 + if (message.type === 'assistant' && message.metadata) { + console.group('📋 复制的AI回复详情'); + console.log('💬 回复内容:', message.content); + console.log('⏱️ 响应时间:', message.metadata.responseTime + 'ms'); + console.log('🤖 使用模型:', message.metadata.modelUsed); + console.log('🕐 回复时间:', message.timestamp.toLocaleString()); + + if (message.metadata.sources && message.metadata.sources.length > 0) { + console.log('📚 参考来源:'); + message.metadata.sources.forEach((source, index) => { + console.log(` ${index + 1}. ${source.title}`); + if (source.content) { + console.log(` 内容: ${JSON.stringify(source.content, null, 2)}`); + } + if (source.uri) { + console.log(` 链接: ${source.uri}`); + } + }); + } + console.groupEnd(); + } + } catch (err) { + console.error('❌ 复制失败:', err); + } + }, []); + + // 在控制台显示消息详情 + const handleShowMessageDetails = useCallback((message: ChatMessage) => { + if (message.type === 'assistant') { + console.group('🔍 AI消息详情'); + console.log('💬 回复内容:', message.content); + console.log('🕐 时间戳:', message.timestamp.toLocaleString()); + console.log('📊 状态:', message.status); + + if (message.metadata) { + console.log('⏱️ 响应时间:', message.metadata.responseTime + 'ms'); + console.log('🤖 使用模型:', message.metadata.modelUsed); + + if (message.metadata.sources && message.metadata.sources.length > 0) { + console.log('📚 参考来源 (' + message.metadata.sources.length + ' 条):'); + message.metadata.sources.forEach((source, index) => { + console.log(` 📖 ${index + 1}. ${source.title}`); + if (source.content) { + console.log(` 📄 内容: ${JSON.stringify(source.content, null, 2)}`); + } + if (source.uri) { + console.log(` 🔗 链接: ${source.uri}`); + } + }); + } + } + console.groupEnd(); + } else { + console.group('👤 用户消息详情'); + console.log('💬 消息内容:', message.content); + console.log('🕐 时间戳:', message.timestamp.toLocaleString()); + console.log('📊 状态:', message.status); + console.groupEnd(); + } + }, []); + + return ( +
+ {/* 聊天头部 */} +
+
+
+ +
+
+

AI 智能助手

+

基于 RAG 检索增强生成

+
+
+ +
+ {messages.length > 0 && ( + + )} +
+
+ + 保留最新 {maxMessages} 条对话 +
+
+ + 详细日志请查看浏览器控制台 +
+
+
+
+ + {/* 聊天消息区域 */} +
+ {messages.length === 0 ? ( +
+
+ +
+

时尚穿搭顾问

+

+ 我是您的专属时尚顾问,基于丰富的女装穿搭知识库,为您提供个性化的搭配建议和时尚指导。 +

+
+ {[ + '夏日清新穿搭推荐', + '职场女性如何搭配?', + '约会穿什么显气质?', + '小个子女生穿搭技巧', + '秋冬外套怎么选?', + '显瘦穿搭有什么秘诀?', + '色彩搭配的基本原则', + '配饰如何提升整体造型?' + ].map((suggestion, index) => ( + + ))} +
+
+ ) : ( + <> + {messages.map((message) => ( +
+ {message.type === 'assistant' && ( +
+ +
+ )} + +
+ {/* 用户消息或AI消息的文字回答(可选显示) */} + {(message.type === 'user' || message.status === 'sending' || showAnswers[message.id]) && ( +
+ {message.status === 'sending' ? ( +
+
+ 正在为您搭配中... +
+ ) : ( +
{message.content}
+ )} +
+ )} + + {/* AI消息的查看详情按钮(当文字回答隐藏且不在加载状态时显示) */} + {message.type === 'assistant' && message.status !== 'sending' && !showAnswers[message.id] && ( +
+ +
+ )} + + {/* AI消息的隐藏回答按钮(当文字回答显示且不在加载状态时显示) */} + {message.type === 'assistant' && message.status !== 'sending' && showAnswers[message.id] && ( +
+ +
+ )} + + {/* 消息状态和元数据 */} +
+ + {message.timestamp.toLocaleTimeString()} + + + {message.status === 'sent' && ( + + )} + {message.status === 'error' && ( + + )} + + {message.metadata?.responseTime && ( + + + {message.metadata.responseTime}ms + + )} + + {/* 操作按钮 */} +
+ + +
+
+ + {/* 来源信息 - 图片卡片展示 */} + {showSources && message.metadata?.sources && message.metadata.sources.length > 0 && ( +
+
+
+
+
+ 时尚穿搭参考 ({message.metadata.sources.length} 张图片) +
+
+
+ 悬停查看原图 +
+
+ + {/* 图片展示 - 响应式网格 */} +
+ {(() => { + const isExpanded = expandedSources[message.id]; + const defaultDisplayCount = 12; // 默认显示12张图片 + const displaySources = isExpanded + ? message.metadata.sources + : message.metadata.sources.slice(0, defaultDisplayCount); + + return displaySources.map((source, index) => + renderImageCard(source, index) + ); + })()} +
+ + {/* 展开/收起按钮 */} + {message.metadata.sources.length > 12 && ( +
+ +
+ )} +
+ )} +
+ + {message.type === 'user' && ( +
+ +
+ )} +
+ ))} +
+ + )} +
+ + {/* 错误提示 */} + {error && ( +
+ + {error} + +
+ )} + + {/* 标签选择区域 */} + {allTags.length > 0 && ( +
+
+
+ + + 穿搭标签 ({selectedTags.length} 已选) + +
+
+ {selectedTags.length > 0 && ( + + )} + {selectedTags.length > 0 && ( + + )} +
+
+ + {/* 标签列表 */} +
+ {allTags.map((tag, index) => { + const isSelected = selectedTags.includes(tag); + return ( + + ); + })} +
+
+ )} + + {/* 输入区域 */} +
+
+
+