"use client"; import React, { useState, useEffect } from "react"; import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors, Expand, Maximize2, Move3D, Droplet } from "lucide-react"; import { toast } from "sonner"; import { ImageAnnotator } from "./image-annotator"; import { AngleEditDialog } from "./angle-edit-dialog"; import { buildImageProxyUrl } from "@/lib/image-delivery"; function triggerBrowserDownload( url: string, filename?: string, options?: { openInNewTab?: boolean } ) { const link = document.createElement("a"); link.href = url; if (filename) { link.download = filename; } if (options?.openInNewTab) { link.target = "_blank"; } link.rel = "noopener noreferrer"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } export function ImagePreview({ src, alt, onImageEdit, onAnnotate, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, onImageAngleEdit, onImageWatermarkRemove, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, imageIndex, compactGrid = false }: { 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; onImageAngleEdit?: (imageUrl: string) => void; onImageWatermarkRemove?: (imageUrl: string) => void; isProcessingEnhance?: boolean; isProcessingRmbg?: boolean; isProcessingExtend?: boolean; isProcessingAngleEdit?: boolean; isProcessingWatermarkRemove?: boolean; imageIndex?: number; compactGrid?: boolean; }) { const [isOpen, setIsOpen] = useState(false); const [isAnnotationMode, setIsAnnotationMode] = useState(false); const [isDetecting, setIsDetecting] = useState(false); const [showAnnotator, setShowAnnotator] = useState(false); const [showResizeEditor, setShowResizeEditor] = useState(false); const [showAngleEditDialog, setShowAngleEditDialog] = useState(false); // 调试日志:监控按钮状态变化 useEffect(() => { console.log("[ImagePreview] 按钮状态更新:", { isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, src: src?.substring(0, 50) }); }, [isProcessingEnhance, isProcessingRmbg, isProcessingExtend, isProcessingAngleEdit, isProcessingWatermarkRemove, src]); // 解析 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; const applySizeInfo = (rawSize?: string) => { if (!rawSize) return; const normalized = rawSize.trim().toLowerCase(); if (normalized === "4k") { is4K = true; maxWidth = 500; return; } if (normalized === "2k") { is4K = false; maxWidth = 400; return; } if (normalized === "1k") { is4K = false; maxWidth = 300; return; } // 仅纯数字时才按像素宽度解析,避免 "2k" 被 parseInt 误解析成 2 if (/^\d+$/.test(normalized)) { maxWidth = parseInt(normalized, 10); } }; // 最新格式:使用 ||| 分隔 (不会与 LaTeX 数学公式或 Markdown 表格冲突) if (alt && alt.includes("thumb|||")) { const content = alt.substring(alt.indexOf("thumb|||") + 8); const parts = content.split("|||"); originalUrl = parts[0]; applySizeInfo(parts[1]); 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]; applySizeInfo(parts[1]); 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]; applySizeInfo(parts[1]); 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("fal-image-thumb-")) { originalUrl = src.replace("fal-image-thumb-", "fal-image-orig-"); console.log("[ImagePreview] Inferred original URL from fal-image-thumb filename"); } else if (src.includes("snapgen-image-thumb-")) { originalUrl = src.replace("snapgen-image-thumb-", "snapgen-image-orig-"); console.log("[ImagePreview] Inferred original URL from snapgen-image-thumb filename"); } else if (src.includes("bananaimage-thumb-")) { originalUrl = src.replace("bananaimage-thumb-", "bananaimage-orig-"); console.log("[ImagePreview] Inferred original URL from bananaimage-thumb filename"); } else if (src.includes("visionary-thumb-")) { originalUrl = src.replace("visionary-thumb-", "visionary-orig-"); console.log("[ImagePreview] Inferred original URL from visionary-thumb filename"); } else if (src.includes("atlascloud-thumb-")) { originalUrl = src.replace("atlascloud-thumb-", "atlascloud-orig-"); console.log("[ImagePreview] Inferred original URL from Atlas Cloud 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 previewWidth = Math.min(2200, Math.max(640, Math.round(maxWidth * 2))); const previewSrc = buildImageProxyUrl(thumbnail || original, { width: previewWidth, quality: 86, format: "webp", }); // 下载图片函数(下载原图) const handleDownload = async () => { const safeOriginalUrl = String(original || "").trim(); if (!safeOriginalUrl) { toast.error("下载失败,请重试"); return; } const filename = `generated-image-${Date.now()}.jpg`; try { const response = await fetch( buildImageProxyUrl(safeOriginalUrl, { delivery: "proxy" }) ); if (!response.ok) { throw new Error(`下载失败(${response.status})`); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); triggerBrowserDownload(url, filename); window.URL.revokeObjectURL(url); toast.success("下载成功"); } catch (error) { console.error("下载失败:", error); try { triggerBrowserDownload(safeOriginalUrl, filename); toast.message("代理下载失败,已切换为原图直链下载"); } catch (fallbackError) { console.error("原图直链下载失败:", fallbackError); try { triggerBrowserDownload(safeOriginalUrl, undefined, { openInNewTab: true }); toast.message("已打开原图,可直接另存为"); } catch (openError) { console.error("打开原图失败:", openError); toast.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 { if (event.currentTarget.src !== (thumbnail || original)) { event.currentTarget.src = thumbnail || original; } }} onClick={() => setIsOpen(true)} /> {/* 左上角:变清晰、抠图和图像拓展按钮 - 始终显示 */} {(onImageEnhance || onImageRmbg || onImageExtend) && (
{onImageEnhance && ( )} {onImageRmbg && ( )} {onImageExtend && ( )} {onImageAngleEdit && ( )} {onImageWatermarkRemove && ( )}
)} {/* 悬停时显示的操作按钮 */}
{/* 画图按钮 */} {onDrawingComplete && ( )} {/* 编辑按钮 */} {onImageEdit && ( )} {/* 下载按钮 */}
{/* 弹窗 */} {isOpen && (
setIsOpen(false)} > {/* 按钮组 */}
{/* 下载按钮 (新增) */} {/* 定位标注按钮 */} {onAnnotate && ( )} {/* 画图标注按钮 */} {onDrawingComplete && ( )} {/* 编辑按钮 */} {onImageEdit && ( )} {/* 下载按钮 */} {/* 关闭按钮 */}
{/* 标注模式提示 */} {isAnnotationMode && (
点击图片中的物体进行标注
)} {/* 大图(显示原图)*/}
e.stopPropagation()} > Generated Image - Original {/* 加载状态 */} {isDetecting && (
正在识别...
)}
)} {/* 图片编辑器 */} {showAnnotator && onDrawingComplete && ( { onDrawingComplete(annotatedImageUrl); setShowAnnotator(false); toast.success("编辑图片已添加到聊天"); }} submitMode="publish-new-canvas" onCancel={() => setShowAnnotator(false)} /> )} {/* 尺寸调整编辑器 */} {showResizeEditor && ( { // 尺寸调整完成后,将图片添加到聊天 if (onDrawingComplete) { onDrawingComplete(resizedImageUrl); toast.success("尺寸调整图片已添加到聊天"); } setShowResizeEditor(false); }} onCancel={() => setShowResizeEditor(false)} /> )} {/* 角度调整对话框 */} {showAngleEditDialog && onImageAngleEdit && ( { onImageAngleEdit(resultUrl); setShowAngleEditDialog(false); toast.success("角度调整图片已生成"); }} /> )} ); }