flnxi-Wu/components/AIChatWidget.tsx
2026-05-28 18:39:06 +08:00

685 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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 { cn } from '@/lib/ui-styles'
// 图片加载占位符组件
const ImageLoadingPlaceholder = () => {
return (
<div className="relative w-full aspect-square rounded-lg overflow-hidden border border-dashed border-slate-300 bg-slate-50 dark:border-slate-700 dark:bg-slate-800">
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center">
<div className="mx-auto h-6 w-6 rounded-full border-2 border-slate-300 border-t-slate-700 animate-spin dark:border-slate-600 dark:border-t-slate-200" />
<p className="mt-3 text-xs font-medium text-slate-500 dark:text-slate-400">图片处理中</p>
</div>
</div>
</div>
)
}
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 { data: session } = useSession()
const [isOpen, setIsOpen] = useState(false)
const [messages, setMessages] = useState<Message[]>([])
const [input, setInput] = useState('')
const [sending, setSending] = useState(false)
const [images, setImages] = useState<File[]>([])
const [imagePreviews, setImagePreviews] = useState<string[]>([])
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<HTMLDivElement>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const dropZoneRef = useRef<HTMLDivElement>(null)
const headerRef = useRef<HTMLDivElement>(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<HTMLInputElement>) => {
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 (
<div ref={containerRef} className="fixed bottom-6 right-6 z-50">
{/* AI聊天窗口 */}
{isOpen && (
<>
<div
className="fixed w-[420px] h-[680px] bg-white dark:bg-slate-900 rounded-lg border border-slate-200 dark:border-slate-700 shadow-sm overflow-hidden flex flex-col"
style={{
left: position.x ? `${position.x}px` : 'auto',
top: position.y ? `${position.y}px` : 'auto',
right: position.x ? 'auto' : '24px',
bottom: position.y ? 'auto' : '96px',
maxHeight: '80vh'
}}
>
{/* 头部 - 可拖拽 */}
<div
ref={headerRef}
onMouseDown={handleMouseDown}
className="px-5 py-3 border-b border-slate-200 bg-white dark:border-slate-700 dark:bg-slate-900 flex items-center justify-between cursor-move select-none"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-md bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-600 dark:text-slate-300">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-slate-900 dark:text-slate-100">助手</h3>
<p className="text-xs text-slate-500 dark:text-slate-400">文字和图片处理</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="text-slate-500 hover:text-slate-700 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-200 dark:hover:bg-slate-800 rounded-md p-2 transition-colors"
aria-label="关闭"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* 消息列表 */}
<div
ref={dropZoneRef}
className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-50 dark:bg-slate-950 relative"
style={{
scrollbarWidth: 'thin',
scrollbarColor: '#cbd5e1 transparent'
}}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
{/* 拖拽提示 */}
{isDragging && (
<div className="absolute inset-0 bg-slate-100/90 dark:bg-slate-900/90 z-10 flex items-center justify-center border border-dashed border-slate-400 dark:border-slate-600 m-2 rounded-lg">
<div className="text-center">
<svg className="w-10 h-10 mx-auto text-slate-500 dark:text-slate-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-base font-semibold text-slate-700 dark:text-slate-200">松开鼠标上传图片</p>
<p className="text-sm text-slate-500 dark:text-slate-400 mt-1">最多 5 张</p>
</div>
</div>
)}
{messages.length === 0 && !isDragging && (
<div className="text-center mt-12">
<div className="w-14 h-14 mx-auto mb-4 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-500 dark:text-slate-400">
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
</svg>
</div>
<h3 className="text-base font-semibold text-slate-800 dark:text-slate-200 mb-2">助手</h3>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-1">可以发送文字或图片</p>
<p className="text-sm text-slate-600 dark:text-slate-400 mb-3">支持图片处理和内容生成</p>
<div className="inline-flex items-center gap-2 px-3 py-1.5 bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-md text-sm">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
拖拽图片到此处快速上传
</div>
</div>
)}
{messages.map((msg) => (
<div
key={msg.id}
className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
<div className={`max-w-[85%] ${
msg.role === 'user'
? 'bg-slate-900 text-white dark:bg-slate-100 dark:text-slate-900'
: msg.role === 'assistant'
? 'bg-white dark:bg-slate-800 text-slate-900 dark:text-white border border-slate-200 dark:border-slate-700'
: 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-800'
} rounded-lg px-4 py-3`}>
{msg.content && <div className="whitespace-pre-wrap text-sm">{msg.content}</div>}
{/* 显示图片加载占位符 */}
{msg.isGenerating && (
<div className="mt-2">
{/* 如果有原图,显示原图的模糊占位符 */}
{msg.sourceImages && msg.sourceImages.length > 0 ? (
<div className={`grid gap-2 ${
msg.sourceImages.length === 1 ? 'grid-cols-1' :
msg.sourceImages.length === 2 ? 'grid-cols-2' :
'grid-cols-3'
}`}>
{msg.sourceImages.map((img, idx) => (
<div key={idx} className="relative rounded-lg overflow-hidden">
<Image
src={img}
alt={`Processing ${idx + 1}`}
width={200}
height={200}
unoptimized
className="w-full max-w-[200px] h-auto object-contain rounded-lg opacity-70"
/>
<div className="absolute inset-0 flex items-center justify-center bg-slate-900/50">
<div className="bg-slate-900 px-3 py-2 rounded-md">
<div className="flex items-center gap-3 text-white text-sm">
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span className="font-medium">处理中...</span>
</div>
</div>
</div>
</div>
))}
</div>
) : (
/* 没有原图,显示4个空白占位符(生成任务) */
msg.imageCount && msg.imageCount > 0 && (
<div className="grid grid-cols-4 gap-2">
{Array.from({ length: msg.imageCount }).map((_, idx) => (
<div key={idx}>
<ImageLoadingPlaceholder />
</div>
))}
</div>
)
)}
</div>
)}
{/* 显示实际图片 - 添加淡入动画 */}
{!msg.isGenerating && msg.images && msg.images.length > 0 && (
<div className={`mt-2 grid gap-2 ${
msg.images.length === 1 ? 'grid-cols-1' :
msg.images.length === 2 ? 'grid-cols-2' :
msg.images.length === 3 ? 'grid-cols-3' :
'grid-cols-4'
}`}>
{msg.images.map((img, idx) => (
<div
key={idx}
className="relative group rounded-lg overflow-hidden bg-slate-100 dark:bg-slate-700"
>
<Image
src={img}
alt={`Image ${idx + 1}`}
width={400}
height={400}
unoptimized
className={`w-full rounded-lg cursor-pointer hover:opacity-90 transition-opacity ${
msg.role === 'user'
? 'max-w-[200px] h-auto object-contain'
: 'aspect-square object-cover'
}`}
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()}-${idx + 1}.png`
link.click()
}}
className="absolute top-2 right-2 bg-slate-900/80 hover:bg-slate-900 text-white p-2 rounded-md opacity-0 group-hover:opacity-100 transition-opacity"
title="下载图片"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
</button>
</div>
))}
</div>
)}
<div className="text-xs opacity-60 mt-1">
{msg.timestamp.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* 图片预览区 */}
{imagePreviews.length > 0 && (
<div className="px-4 py-3 bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700">
<div className="flex gap-2 overflow-x-auto pb-1" style={{ scrollbarWidth: 'thin' }}>
{imagePreviews.map((preview, idx) => (
<div key={idx} className="relative flex-shrink-0 group">
<Image
src={preview}
alt={`Preview ${idx}`}
width={64}
height={64}
unoptimized
className="w-16 h-16 object-cover rounded-lg border border-slate-200 dark:border-slate-700"
/>
<button
onClick={() => removeImage(idx)}
className="absolute -top-1 -right-1 bg-red-500 text-white rounded-md w-5 h-5 flex items-center justify-center text-xs hover:bg-red-600"
aria-label="删除图片"
>
×
</button>
</div>
))}
</div>
</div>
)}
{/* 输入区 */}
<div className="p-4 bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-end gap-3">
<input
type="file"
ref={fileInputRef}
onChange={handleImageSelect}
accept="image/*"
multiple
className="hidden"
/>
{/* 上传按钮 */}
<button
onClick={() => fileInputRef.current?.click()}
className="p-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
title="上传图片"
aria-label="上传图片"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" 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>
</button>
{/* 输入框 */}
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}}
placeholder="输入消息..."
className="flex-1 px-3 py-2 border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 dark:text-white rounded-lg focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-700 focus:border-slate-900 dark:focus:border-slate-500 outline-none text-sm resize-none placeholder:text-slate-400 dark:placeholder:text-slate-500"
rows={2}
disabled={sending}
/>
{/* 发送按钮 */}
<button
onClick={handleSend}
disabled={(!input.trim() && images.length === 0) || sending}
className="px-4 py-2 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 rounded-lg hover:bg-slate-800 dark:hover:bg-white disabled:opacity-50 disabled:cursor-not-allowed font-medium text-sm h-[72px] flex items-center justify-center min-w-[70px] transition-colors"
>
{sending ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<span>发送中</span>
</div>
) : (
<div className="flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
<span>发送</span>
</div>
)}
</button>
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mt-2 text-center">
支持文字+图片聊天 · 可上传/拖拽/粘贴图片 · 最多5张
</p>
</div>
</div>
</>
)}
{/* 悬浮按钮 */}
<button
onClick={() => setIsOpen(!isOpen)}
className="w-12 h-12 rounded-lg border border-slate-300 bg-white text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 transition-colors flex items-center justify-center"
aria-label="AI助手"
>
{isOpen ? (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
)}
</button>
</div>
)
}