feat: 增强AI聊天助手 - 完整图片支持

新增功能:
✅ 支持发送图片(点击上传、拖拽、粘贴)
✅ 支持接收AI返回的图片(Base64、inline data)
✅ 图片预览和下载功能
✅ 点击图片新窗口查看
✅ 拖拽上传可视化提示
✅ 支持粘贴剪贴板图片
✅ 最多支持5张图片同时发送
✅ 对话历史保留图片信息

图片处理:
- 自动转换为Base64格式
- 支持多种图片格式(PNG、JPG、GIF等)
- 图片预览带删除按钮
- 图片下载功能
- Hover显示下载按钮

用户体验:
- 拖拽区域高亮提示
- 实时图片预览
- 图片点击放大
- 支持键盘快捷键
- 响应式设计

这是一个功能完整的AI图片编辑助手!
This commit is contained in:
chenpei 2025-10-27 17:49:50 +08:00
parent d0dc94e36d
commit b3de06ea39

View File

@ -20,9 +20,11 @@ export default function AIChatWidget() {
const [sending, setSending] = useState(false)
const [images, setImages] = useState<File[]>([])
const [imagePreviews, setImagePreviews] = useState<string[]>([])
const [isDragging, setIsDragging] = useState(false)
const messagesEndRef = useRef<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const dropZoneRef = useRef<HTMLDivElement>(null)
// 点击外部关闭
useEffect(() => {
@ -43,6 +45,36 @@ export default function AIChatWidget() {
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<HTMLInputElement>) => {
const files = Array.from(e.target.files || [])
@ -66,6 +98,35 @@ export default function AIChatWidget() {
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
@ -104,11 +165,20 @@ export default function AIChatWidget() {
})
})
// 构建对话历史
const conversationHistory = messages.map(msg => ({
role: msg.role,
content: msg.content
}))
// 构建对话历史(保留图片信息)
const conversationHistory = messages.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',
@ -216,12 +286,32 @@ export default function AIChatWidget() {
</div>
{/* 消息列表 */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 dark:bg-gray-900">
{messages.length === 0 && (
<div
ref={dropZoneRef}
className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50 dark:bg-gray-900 relative"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 拖拽提示 */}
{isDragging && (
<div className="absolute inset-0 bg-blue-500/20 dark:bg-blue-400/20 backdrop-blur-sm z-10 flex items-center justify-center border-2 border-dashed border-blue-500 dark:border-blue-400 m-2 rounded-lg">
<div className="text-center">
<svg className="w-16 h-16 mx-auto text-blue-500 dark:text-blue-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-lg font-semibold text-blue-600 dark:text-blue-400">松开鼠标上传图片</p>
<p className="text-sm text-blue-500 dark:text-blue-300 mt-1">支持多张图片(最多5张)</p>
</div>
</div>
)}
{messages.length === 0 && !isDragging && (
<div className="text-center text-gray-500 dark:text-gray-400 mt-8">
<p className="mb-2">👋 你好!我是AI助手</p>
<p className="text-sm">可以发送文字或图片与我聊天</p>
<p className="text-sm">支持图片编辑、内容生成等功能</p>
<p className="text-sm text-blue-500 dark:text-blue-400 mt-2">💡 拖拽图片到此处快速上传</p>
</div>
)}
@ -241,12 +331,35 @@ export default function AIChatWidget() {
{msg.images && msg.images.length > 0 && (
<div className="mt-2 space-y-2">
{msg.images.map((img, idx) => (
<div key={idx} className="relative rounded overflow-hidden">
<div key={idx} className="relative group rounded overflow-hidden bg-gray-100 dark:bg-gray-700">
<img
src={img}
alt={`Image ${idx + 1}`}
className="w-full rounded"
className="w-full rounded cursor-pointer hover:opacity-90 transition-opacity"
onClick={() => {
// 点击图片在新窗口打开
const win = window.open()
if (win) {
win.document.write(`<img src="${img}" style="max-width:100%;height:auto;" />`)
}
}}
/>
{/* 下载按钮 */}
<button
onClick={(e) => {
e.stopPropagation()
const link = document.createElement('a')
link.href = img
link.download = `image-${Date.now()}.png`
link.click()
}}
className="absolute top-2 right-2 bg-black/50 hover:bg-black/70 text-white p-1.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
title="下载图片"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
</div>
))}
</div>
@ -322,7 +435,7 @@ export default function AIChatWidget() {
</button>
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持文字+图片聊天,可上传多张图片
💡 支持文字+图片聊天 · 可上传/拖拽/粘贴图片 · 最多5张
</p>
</div>
</div>