feat: 增强AI聊天助手 - 完整图片支持
新增功能: ✅ 支持发送图片(点击上传、拖拽、粘贴) ✅ 支持接收AI返回的图片(Base64、inline data) ✅ 图片预览和下载功能 ✅ 点击图片新窗口查看 ✅ 拖拽上传可视化提示 ✅ 支持粘贴剪贴板图片 ✅ 最多支持5张图片同时发送 ✅ 对话历史保留图片信息 图片处理: - 自动转换为Base64格式 - 支持多种图片格式(PNG、JPG、GIF等) - 图片预览带删除按钮 - 图片下载功能 - Hover显示下载按钮 用户体验: - 拖拽区域高亮提示 - 实时图片预览 - 图片点击放大 - 支持键盘快捷键 - 响应式设计 这是一个功能完整的AI图片编辑助手!
This commit is contained in:
parent
d0dc94e36d
commit
b3de06ea39
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user