"use client"; import { useState } from "react"; import Image from "next/image"; import type { Attachment } from "@/lib/types"; import { Loader } from "./elements/loader"; import { CrossSmallIcon } from "./icons"; import { Button } from "./ui/button"; import { X, Download, Target, Paintbrush } from "lucide-react"; import { toast } from "sonner"; import { ImageAnnotator } from "./image-annotator"; export const PreviewAttachment = ({ attachment, isUploading = false, onRemove, onDragStart, onDragEnd, onDragOver, onDrop, draggable = false, onAnnotate, onDrawingComplete, imageIndex, uploadProgress, uploadStatus, }: { attachment: Attachment; isUploading?: boolean; uploadProgress?: number; uploadStatus?: "preparing" | "uploading" | "finalizing" | "failed"; onRemove?: () => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: (e: React.DragEvent) => void; onDragOver?: (e: React.DragEvent) => void; onDrop?: (e: React.DragEvent) => void; draggable?: boolean; onAnnotate?: (annotation: { imageIndex: number; description: string; formattedText: string; }) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; imageIndex?: number; }) => { const { name, url, contentType } = attachment; const isUploadFailed = uploadStatus === "failed"; const isLocalPreviewUrl = url?.startsWith("blob:") || url?.startsWith("data:"); const shouldShowImage = Boolean(url && contentType?.startsWith("image")); const normalizedUploadProgress = typeof uploadProgress === "number" && Number.isFinite(uploadProgress) ? Math.max(0, Math.min(100, Math.round(uploadProgress))) : null; const uploadStatusLabel = uploadStatus === "failed" ? "上传失败" : uploadStatus === "preparing" ? "处理中" : uploadStatus === "finalizing" ? "保存中" : "上传中"; const [isOpen, setIsOpen] = useState(false); const [isDragging, setIsDragging] = useState(false); const [isAnnotationMode, setIsAnnotationMode] = useState(false); const [isDetecting, setIsDetecting] = useState(false); const [showAnnotator, setShowAnnotator] = useState(false); const [annotationPoints, setAnnotationPoints] = useState>([]); // 下载图片函数 const handleDownload = async () => { try { const response = await fetch(url); const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = downloadUrl; link.download = name ?? `image-${Date.now()}.jpg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(downloadUrl); } catch (error) { console.error("下载失败:", error); } }; // 处理图片点击(标注模式) const handleImageClick = async (e: React.MouseEvent) => { if (!isAnnotationMode || isDetecting || !onAnnotate) return; const img = e.currentTarget; const rect = img.getBoundingClientRect(); // 计算点击位置在显示区域的坐标 const displayClickX = e.clientX - rect.left; const displayClickY = e.clientY - rect.top; // 计算缩放比例 const scaleX = img.naturalWidth / rect.width; const scaleY = img.naturalHeight / rect.height; // 转换为原始图片坐标 const clickX = Math.round(displayClickX * scaleX); const clickY = Math.round(displayClickY * scaleY); console.log('[标注调试]', { display: { x: displayClickX, y: displayClickY, width: rect.width, height: rect.height }, original: { x: clickX, y: clickY, width: img.naturalWidth, height: img.naturalHeight }, scale: { x: scaleX, y: scaleY } }); setIsDetecting(true); toast.info("正在识别物体..."); try { const response = await fetch("/api/detect-object", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageUrl: url, clickX, clickY, imageIndex: imageIndex || 1, imageWidth: img.naturalWidth, imageHeight: img.naturalHeight, }), }); const result = await response.json(); if (result.success && result.data) { toast.success(`识别成功:${result.data.description}`); // 添加标注点到预览图 setAnnotationPoints(prev => [...prev, { x: displayClickX, y: displayClickY, description: result.data.description, }]); onAnnotate({ imageIndex: result.data.imageIndex, description: result.data.description, formattedText: result.data.formattedText, }); // 退出标注模式但保持弹窗打开,让用户看到标注点 setIsAnnotationMode(false); } else { toast.error(result.error || "识别失败,请重试"); } } catch (error) { console.error("物体检测失败:", error); toast.error("识别失败,请检查网络连接"); } finally { setIsDetecting(false); } }; return ( <>
!isUploading && contentType?.startsWith("image") && setIsOpen(true)} draggable={draggable && !isUploading} onDragStart={(e) => { if (draggable && !isUploading) { setIsDragging(true); onDragStart?.(e); } }} onDragEnd={(e) => { setIsDragging(false); onDragEnd?.(e); }} onDragOver={onDragOver} onDrop={onDrop} > {shouldShowImage ? ( isLocalPreviewUrl ? ( {name ) : ( {name ) ) : (
文件
)} {isUploading && (
{isUploadFailed ? : } {isUploadFailed ? uploadStatusLabel : normalizedUploadProgress === null ? uploadStatusLabel : `${normalizedUploadProgress}%`} {!isUploadFailed && (
)}
)} {onRemove && (!isUploading || isUploadFailed) && ( )}
{name}
{/* 图片放大预览弹窗 */} {isOpen && contentType?.startsWith("image") && (
{ setIsOpen(false); setIsAnnotationMode(false); setAnnotationPoints([]); }} > {/* 弹窗卡片 */}
e.stopPropagation()} > {/* 按钮组 */}
{/* 定位标注按钮 */} {onAnnotate && ( )} {/* 画图标注按钮 */} {onDrawingComplete && ( )} {/* 下载按钮 */} {/* 关闭按钮 */}
{/* 标注模式提示 */} {isAnnotationMode && (
💡 点击图片中的物体进行标注
)} {/* 图片容器 */}
{name {/* 标注点显示 */} {annotationPoints.map((point, index) => (
{/* 标注图标 */}
📍
{/* 标注描述 */}
{point.description}
))} {/* 加载状态 */} {isDetecting && (
正在识别物体...
)}
{/* 底部信息栏 */}

{name}

)} {/* 图片编辑器 */} {showAnnotator && onDrawingComplete && ( { onDrawingComplete(annotatedImageUrl); setShowAnnotator(false); toast.success("编辑图片已添加到聊天"); }} submitMode="publish-new-canvas" onCancel={() => setShowAnnotator(false)} /> )} ); };