"use client"; import React, { useState } from "react"; import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand } from "lucide-react"; import { toast } from "sonner"; import { ImageAnnotator } from "./image-annotator"; export function ImagePreview({ src, alt, onImageEdit, onAnnotate, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, imageIndex }: { src: string; alt?: string; onImageEdit?: (imageUrl: string) => void; onAnnotate?: (annotation: { imageIndex: number; description: string; formattedText: string; }) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; onImageEnhance?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void; isProcessingEnhance?: boolean; isProcessingRmbg?: boolean; isProcessingExtend?: boolean; imageIndex?: number; }) { const [isOpen, setIsOpen] = useState(false); const [isAnnotationMode, setIsAnnotationMode] = useState(false); const [isDetecting, setIsDetecting] = useState(false); const [showAnnotator, setShowAnnotator] = useState(false); // 解析 alt 文本,提取原图 URL 和宽度 // 格式优先级: // 1. thumb|||原图URL|||imageSize (最新格式,推荐,不会与 LaTeX/Markdown 冲突) // 2. thumb$$原图URL$$imageSize (旧格式,会被 LaTeX 解析器干扰) // 3. thumb|原图URL|imageSize (最旧格式) const parseImageUrls = () => { // 调试日志:查看接收到的 alt 和 src console.log("[ImagePreview] Parsing alt:", alt, "src:", src?.substring(0, 80)); let originalUrl = src; let maxWidth = 500; let is4K = false; let parsed = false; // 最新格式:使用 ||| 分隔 (不会与 LaTeX 数学公式或 Markdown 表格冲突) if (alt && alt.includes("thumb|||")) { const content = alt.substring(alt.indexOf("thumb|||") + 8); const parts = content.split("|||"); originalUrl = parts[0]; const extraInfo = parts[1]; if (extraInfo === "4K") { is4K = true; } else if (extraInfo === "2K") { // 2K 不是 4K,但也是高质量 is4K = false; } else if (extraInfo && !isNaN(parseInt(extraInfo, 10))) { maxWidth = parseInt(extraInfo, 10); } parsed = true; console.log("[ImagePreview] Parsed with ||| format:", { originalUrl: originalUrl?.substring(0, 80), is4K }); } // 旧格式:使用 $$ 分隔 (可能被 LaTeX 干扰,保持向后兼容) else if (alt && alt.includes("thumb$$")) { const content = alt.substring(alt.indexOf("thumb$$") + 7); const parts = content.split("$$"); originalUrl = parts[0]; const extraInfo = parts[1]; if (extraInfo === "4K") { is4K = true; } else if (extraInfo && !isNaN(parseInt(extraInfo, 10))) { maxWidth = parseInt(extraInfo, 10); } parsed = true; console.log("[ImagePreview] Parsed with $$ format:", { originalUrl: originalUrl?.substring(0, 80), is4K }); } // 最旧格式:使用 | 分隔 else if (alt && alt.includes("thumb|")) { const content = alt.substring(alt.indexOf("thumb|") + 6); const parts = content.split("|"); originalUrl = parts[0]; const extraInfo = parts[1]; if (extraInfo === "4K") { is4K = true; } else if (extraInfo && !isNaN(parseInt(extraInfo, 10))) { maxWidth = parseInt(extraInfo, 10); } parsed = true; console.log("[ImagePreview] Parsed with | format:", { originalUrl: originalUrl?.substring(0, 80), is4K }); } // 🛡️ 兜底策略:如果解析失败,或者解析出的 originalUrl 与 src 相同(意味着没提取到), // 尝试利用文件命名规律推导原图 URL if (!parsed || originalUrl === src) { if (src.includes("gemini-thumb-")) { originalUrl = src.replace("gemini-thumb-", "gemini-orig-"); console.log("[ImagePreview] Inferred original URL from gemini-thumb filename"); } else if (src.includes("openai-thumb-")) { originalUrl = src.replace("openai-thumb-", "openai-orig-"); console.log("[ImagePreview] Inferred original URL from openai-thumb filename"); } else if (src.includes("/thumb/") || src.includes("-thumb")) { // 尝试从S3 URL中推断原图URL(常见格式:xxx-thumb.jpg -> xxx.jpg 或 xxx/thumb/xxx -> xxx/xxx) originalUrl = src.replace(/-thumb\./g, ".").replace(/\/thumb\//g, "/"); console.log("[ImagePreview] Inferred original URL from S3 path pattern"); } else { // 如果无法推断,且src看起来已经是完整的URL(不是缩略图),直接使用src作为原图 // 否则记录警告 if (src.startsWith("http://") || src.startsWith("https://")) { // 如果src已经是完整URL且不包含thumb标识,可能是原图 if (!src.includes("thumb") && !src.includes("thumbnail")) { originalUrl = src; console.log("[ImagePreview] Using src as original URL (no thumb identifier found)"); } else { console.log("[ImagePreview] ⚠️ Could not parse original URL, using thumbnail as fallback"); } } else { console.log("[ImagePreview] ⚠️ Could not parse original URL, using thumbnail as fallback"); } } } return { thumbnail: src, original: originalUrl, maxWidth, is4K, }; }; const { thumbnail, original, maxWidth, is4K } = parseImageUrls(); // 下载图片函数(下载原图) const handleDownload = async () => { try { const response = await fetch(original); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `gemini-image-${Date.now()}.jpg`; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); } 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 clickX = Math.round(e.clientX - rect.left); const clickY = Math.round(e.clientY - rect.top); setIsDetecting(true); toast.info("正在识别物体..."); try { const response = await fetch("/api/detect-object", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageUrl: original, 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}`); onAnnotate({ imageIndex: result.data.imageIndex, description: result.data.description, formattedText: result.data.formattedText, }); setIsOpen(false); setIsAnnotationMode(false); } else { toast.error(result.error || "识别失败,请重试"); } } catch (error) { console.error("物体检测失败:", error); toast.error("识别失败,请检查网络连接"); } finally { setIsDetecting(false); } }; return ( <> {/* 缩略图(显示压缩后的缩略图)*/}
Generated Image setIsOpen(true)} /> {/* 左上角:变清晰、抠图和图像拓展按钮 - 始终显示 */} {(onImageEnhance || onImageRmbg || onImageExtend) && (
{onImageEnhance && ( )} {onImageRmbg && ( )} {onImageExtend && ( )}
)} {/* 悬停时显示的操作按钮 */}
{/* 画图按钮 */} {onDrawingComplete && ( )} {/* 编辑按钮 */} {onImageEdit && ( )} {/* 下载按钮 */}
{/* 弹窗 */} {isOpen && (
setIsOpen(false)} > {/* 按钮组 */}
{/* 下载按钮 (新增) */} {/* 定位标注按钮 */} {onAnnotate && ( )} {/* 画图标注按钮 */} {onDrawingComplete && ( )} {/* 编辑按钮 */} {onImageEdit && ( )} {/* 下载按钮 */} {/* 关闭按钮 */}
{/* 标注模式提示 */} {isAnnotationMode && (
点击图片中的物体进行标注
)} {/* 大图(显示原图)*/}
e.stopPropagation()} > Generated Image - Original {/* 加载状态 */} {isDetecting && (
正在识别...
)}
)} {/* 图片编辑器 */} {showAnnotator && onDrawingComplete && ( { onDrawingComplete(annotatedImageUrl); setShowAnnotator(false); toast.success("编辑图片已添加到聊天"); }} onCancel={() => setShowAnnotator(false)} /> )} ); }