/** * AIChatWidget 组件 * * AI聊天助手组件 * 支持文字和图片输入,AI生成图片和内容 * 支持窗口拖拽、图片上传、粘贴等功能 * 使用三层架构样式系统,支持深浅色主题 * * @created 2025-01-XX * @component */ 'use client' import { useState, useEffect, useRef } from 'react' import { useSession } from 'next-auth/react' import Image from 'next/image' import { useThemeStyles, cn } from '@/lib/ui-styles' // 添加动画样式 const shimmerStyle = ` @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } @keyframes breathe { 0%, 100% { filter: blur(15px) brightness(0.7); transform: scale(1); } 50% { filter: blur(12px) brightness(0.8); transform: scale(1.02); } } ` // 图片加载占位符组件 const ImageLoadingPlaceholder = () => { return (
{/* 动态模糊背景 */}
{/* 中心图标 */}
{/* 旋转的光圈 */}
{/* 中心图标 */}

正在生成图片...

AI正在为您创作中

) } interface Message { id: string role: 'user' | 'assistant' | 'system' content: string images?: string[] timestamp: Date isGenerating?: boolean // 是否正在生成图片 imageCount?: number // 预期生成的图片数量 sourceImages?: string[] // 用户发送的原始图片(用于显示模糊占位符) } export default function AIChatWidget() { const styles = useThemeStyles() const { data: session } = useSession() const [isOpen, setIsOpen] = useState(false) const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [sending, setSending] = useState(false) const [images, setImages] = useState([]) const [imagePreviews, setImagePreviews] = useState([]) const [isDragging, setIsDragging] = useState(false) const [windowDragging, setWindowDragging] = useState(false) const [position, setPosition] = useState({ x: 0, y: 0 }) const [dragStart, setDragStart] = useState({ x: 0, y: 0 }) const messagesEndRef = useRef(null) const fileInputRef = useRef(null) const containerRef = useRef(null) const dropZoneRef = useRef(null) const headerRef = useRef(null) // 窗口拖拽功能 const handleMouseDown = (e: React.MouseEvent) => { if (e.button !== 0) return // 只响应左键 setWindowDragging(true) setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y }) } useEffect(() => { const handleMouseMove = (e: MouseEvent) => { if (!windowDragging) return setPosition({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }) } const handleMouseUp = () => { setWindowDragging(false) } if (windowDragging) { document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) } return () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } }, [windowDragging, dragStart]) // 点击外部关闭 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpen(false) } } if (isOpen) { document.addEventListener('mousedown', handleClickOutside) } return () => document.removeEventListener('mousedown', handleClickOutside) }, [isOpen]) // 自动滚动到底部 useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) // 处理粘贴图片 useEffect(() => { const handlePaste = (e: ClipboardEvent) => { if (!isOpen) return const items = e.clipboardData?.items if (!items) return for (let i = 0; i < items.length; i++) { if (items[i].type.indexOf('image') !== -1) { const file = items[i].getAsFile() if (file) { setImages(prev => [...prev, file].slice(0, 5)) const reader = new FileReader() reader.onloadend = () => { setImagePreviews(prev => [...prev, reader.result as string]) } reader.readAsDataURL(file) } } } } if (isOpen) { document.addEventListener('paste', handlePaste) } return () => document.removeEventListener('paste', handlePaste) }, [isOpen]) // 处理图片选择 const handleImageSelect = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) if (files.length > 0) { setImages(prev => [...prev, ...files].slice(0, 5)) // 最多5张图片 // 生成预览 files.forEach(file => { const reader = new FileReader() reader.onloadend = () => { setImagePreviews(prev => [...prev, reader.result as string]) } reader.readAsDataURL(file) }) } } // 移除图片 const removeImage = (index: number) => { setImages(prev => prev.filter((_, i) => i !== index)) setImagePreviews(prev => prev.filter((_, i) => i !== index)) } // 处理拖拽上传 const handleDragOver = (e: React.DragEvent) => { e.preventDefault() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) } const handleDrop = (e: React.DragEvent) => { e.preventDefault() setIsDragging(false) const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/')) if (files.length > 0) { setImages(prev => [...prev, ...files].slice(0, 5)) files.forEach(file => { const reader = new FileReader() reader.onloadend = () => { setImagePreviews(prev => [...prev, reader.result as string]) } reader.readAsDataURL(file) }) } } // 发送消息 const handleSend = async () => { if ((!input.trim() && images.length === 0) || sending || !session) return setSending(true) const userMessage: Message = { id: Date.now().toString(), role: 'user', content: input.trim(), images: imagePreviews, timestamp: new Date() } setMessages(prev => [...prev, userMessage]) setInput('') const currentImages = imagePreviews setImagePreviews([]) setImages([]) try { // 检测是否需要生成图片 const shouldShowImagePlaceholder = currentImages.length > 0 || userMessage.content.includes('生成') || userMessage.content.includes('创建') || userMessage.content.includes('画') || userMessage.content.includes('图片') || userMessage.content.includes('图') // 如果需要生成图片,在用户消息中添加指令 let finalMessageContent = userMessage.content if (shouldShowImagePlaceholder && !userMessage.content.includes('4张') && !userMessage.content.includes('四张')) { finalMessageContent += '\n\n(请生成4张不同风格或角度的图片)' } // 构建消息内容 let messageContent: any = [] if (finalMessageContent) { messageContent.push({ type: 'text', text: finalMessageContent }) } // 添加图片 currentImages.forEach(img => { messageContent.push({ type: 'image_url', image_url: { url: img } }) }) // 构建对话历史(保留图片信息,支持上下文连续对话) const conversationHistory = messages .filter(msg => !msg.isGenerating) // 过滤掉正在生成的临时消息 .map(msg => { if (msg.images && msg.images.length > 0) { const parts: any[] = [] if (msg.content) { parts.push({ type: 'text', text: msg.content }) } msg.images.forEach(img => { parts.push({ type: 'image_url', image_url: { url: img } }) }) return { role: msg.role, content: parts } } return { role: msg.role, content: msg.content } }) conversationHistory.push({ role: 'user', content: messageContent.length === 1 && messageContent[0].type === 'text' ? messageContent[0].text : messageContent }) // 先添加一个加载中的占位消息 const tempAiMessageId = (Date.now() + 1).toString() const tempAiMessage: Message = { id: tempAiMessageId, role: 'assistant', content: '', timestamp: new Date(), isGenerating: shouldShowImagePlaceholder, imageCount: shouldShowImagePlaceholder ? 4 : 0, sourceImages: currentImages.length > 0 ? currentImages : undefined // 保存用户发送的原图 } setMessages(prev => [...prev, tempAiMessage]) // 调用API const response = await fetch('/api/ai-chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: conversationHistory }) }) if (!response.ok) { throw new Error('发送失败') } const data = await response.json() // 解析AI响应 let aiContent = '' let aiImages: string[] = [] if (data.candidates && data.candidates.length > 0) { const candidate = data.candidates[0] if (candidate.content && candidate.content.parts) { for (const part of candidate.content.parts) { if (part.text) { // 检查是否包含图片 const base64ImageMatch = part.text.match(/!\[image\]\(data:image\/([^;]+);base64,([^)]+)\)/) if (base64ImageMatch) { const mimeType = base64ImageMatch[1] const base64Data = base64ImageMatch[2] aiImages.push(`data:image/${mimeType};base64,${base64Data}`) } // 移除Markdown图片语法 const cleanText = part.text .replace(/!\[image\]\(data:image\/[^;]+;base64,[^)]+\)/g, '') .replace(/!\[Image\]\([^)]+\)/g, '') .trim() if (cleanText) { aiContent += cleanText } } else if (part.inlineData && part.inlineData.data) { const mimeType = part.inlineData.mimeType || 'image/png' aiImages.push(`data:${mimeType};base64,${part.inlineData.data}`) } } } } // 更新临时占位消息为实际内容 setMessages(prev => prev.map(msg => msg.id === tempAiMessageId ? { ...msg, content: aiContent || '收到您的消息了', images: aiImages.length > 0 ? aiImages : undefined, isGenerating: false } : msg )) } catch (error) { console.error('发送失败:', error) const errorMessage: Message = { id: (Date.now() + 1).toString(), role: 'system', content: '发送失败,请重试', timestamp: new Date() } setMessages(prev => [...prev, errorMessage]) } finally { setSending(false) } } if (!session) return null return (
{/* AI聊天窗口 - 苹果风格 */} {isOpen && ( <>