import React, { useState, useCallback, useRef, useEffect } from 'react'; import { Send, AlertCircle, Sparkles, Tag, CheckCircle, XCircle, MapPin } from 'lucide-react'; import { queryRagGrounding } from '../services/ragGroundingService'; import { RagGroundingQueryOptions, GroundingMetadata } from '../types/ragGrounding'; import { EnhancedChatMessageV2 } from './EnhancedChatMessageV2'; /** * 聊天消息接口 */ 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; }>; grounding_metadata?: GroundingMetadata; }; } /** * 聊天界面组件属性 */ interface ChatInterfaceProps { /** 会话ID,用于上下文保持 */ sessionId?: string; /** 最大保留消息数量 */ maxMessages?: number; /** 是否显示来源信息 */ showSources?: boolean; /** 是否启用图片卡片 */ enableImageCards?: boolean; /** 自定义样式类名 */ className?: string; /** 占位符文本 */ placeholder?: string; } /** * 聊天界面组件 * 遵循 Tauri 开发规范和 ag-ui 设计标准 * 支持上下文保留和最新3条记录限制 */ export const ChatInterface: React.FC = ({ sessionId = 'default-session', maxMessages = 3, showSources = true, enableImageCards = true, className = '', placeholder = '描述您的穿搭需求,比如场合、风格、身材特点等...' }) => { const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [selectedTags, setSelectedTags] = useState([]); const [allTags, setAllTags] = useState([]); const [isTagsExpanded, setIsTagsExpanded] = useState(false); const [showTagBubble, setShowTagBubble] = useState(false); const [bubbleTags, _setBubbleTags] = useState<{categories: string[], environment: string[]}>({categories: [], environment: []}); const [bubblePosition, _setBubblePosition] = useState<{x: number, y: number}>({x: 0, y: 0}); 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; } }, []); // 生成消息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, includeHistory: true, maxHistoryMessages: 7 }; const result = await queryRagGrounding(userMessage.content, options); if (result.success && result.data) { // 更新助手消息 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, grounding_metadata: result.data!.grounding_metadata } } : 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 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 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: string) => tagSet.add(tag)); // 添加环境标签 imageData.environment_tags.forEach((tag: string) => tagSet.add(tag)); // 添加模特相关标签 imageData.models.forEach((model: any) => { 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 extractedTags = extractAllTags(); // 合并提取的标签和已选择的标签,确保所有选中的标签都在列表中 const combinedTags = [...new Set([...extractedTags, ...selectedTags])]; setAllTags(combinedTags); }, [extractAllTags, selectedTags]); // 切换标签选择状态 const toggleTag = useCallback((tag: string) => { setSelectedTags(prev => { const newTags = prev.includes(tag) ? prev.filter(t => t !== tag) : [...prev, tag]; return newTags; }); }, []); // 切换标签展开状态 const toggleTagsExpanded = useCallback(() => { setIsTagsExpanded(prev => !prev); }, []); // 关闭标签气泡 const closeTagBubble = useCallback(() => { setShowTagBubble(false); }, []); // 清空选中的标签 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]); return (
{/* 聊天消息区域 */}
{messages.length === 0 ? (

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

{[ '夏日清新穿搭推荐', '职场女性如何搭配?', '约会穿什么显气质?', '小个子女生穿搭技巧', '秋冬外套怎么选?', '显瘦穿搭有什么秘诀?', '色彩搭配的基本原则', '配饰如何提升整体造型?' ].map((suggestion, index) => ( ))}
) : ( <> {messages.map((message) => ( ))} )}
{/* 错误提示 */} {error && (
{error}
)} {/* 固定底部输入区域 */}
{/* 标签选择区域 */} {allTags.length > 0 && (
{/* 标签头部 - 始终显示 */}
{/* 左侧:标签标题和已选标签 */}
穿搭标签 ({selectedTags.length} 已选) {/* 已选标签预览 */} {selectedTags.length > 0 && (
{selectedTags.slice(0, 3).map((tag, index) => ( {tag} ))} {selectedTags.length > 3 && ( +{selectedTags.length - 3} )}
)}
{/* 右侧:操作按钮和展开按钮 */}
{/* 操作按钮 */} {selectedTags.length > 0 && ( <> )} {/* 展开/折叠按钮 */}
{/* 标签内容 - 可折叠 */} {isTagsExpanded && (
{/* 标签列表 */}
{allTags.map((tag, index) => { const isSelected = selectedTags.includes(tag); return ( ); })}
)}
)} {/* 输入区域 */}