"use client"; import React, { useState, useRef, useEffect } from "react"; import useSWR from "swr"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Upload, Plus, Trash2, Download, RefreshCw, Image as ImageIcon, Loader2, Layers, Sparkles, ArrowLeft } from "lucide-react"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; const fetcher = (url: string) => fetch(url).then((res) => res.json()); // 场景项数据结构 export interface SceneItem { id: string; image: string; // 场景图 URL 或 Base64 customPrompt: string; // 该场景的补充关键词 targetName: string; // 背景中需要被替换的物体名称 status: 'pending' | 'processing' | 'completed' | 'failed'; result?: string; // 生成结果缩略图 URL originalUrl?: string; // 生成结果原图 URL(用于下载) error?: string; // 错误信息 } interface SceneFusionViewProps { onBack: () => void; } export function SceneFusionView({ onBack }: SceneFusionViewProps) { // 获取场景裂变配置(使用公开 API 端点) const { data: configData } = useSWR( "/api/scene-fusion/config", fetcher ); const config = configData as { enabled?: boolean; defaultPrompt?: string; maxScenes?: number; } | undefined; const defaultPrompt = config?.defaultPrompt || "将图1的产品替换到图2的场景中,保持图1产品的外观、颜色和细节不变,自然融合到图2的环境中。"; const maxScenes = config?.maxScenes || 10; // 比例选项 const aspectRatioOptions = [ { label: "自动", value: "auto" }, { label: "1:1", value: "1:1" }, { label: "4:3", value: "4:3" }, { label: "3:4", value: "3:4" }, { label: "16:9", value: "16:9" }, { label: "9:16", value: "9:16" }, ]; // 分辨率选项 const imageSizeOptions = [ { label: "标准 (1K)", value: "1K" }, { label: "高清 (2K)", value: "2K" }, { label: "超清 (4K)", value: "4K" }, ]; // 状态管理 const [productImage, setProductImage] = useState(null); const [productName, setProductName] = useState(""); // 产品名称(如"沙发") const [scenes, setScenes] = useState([]); const [isProcessing, setIsProcessing] = useState(false); const [aspectRatio, setAspectRatio] = useState("auto"); // 图片比例 const [imageSize, setImageSize] = useState("2K"); // 图片分辨率 // 文件上传 Input 引用 const productInputRef = useRef(null); const sceneInputRef = useRef(null); // 处理产品图上传 const handleProductUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 10 * 1024 * 1024) { toast.error("图片大小不能超过 10MB"); return; } const reader = new FileReader(); reader.onload = (e) => { setProductImage(e.target?.result as string); }; reader.readAsDataURL(file); // 重置 input,允许重复选择同一文件 e.target.value = ""; }; // 处理场景图上传 const handleSceneUpload = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length === 0) return; if (scenes.length + files.length > maxScenes) { toast.error(`最多只能添加 ${maxScenes} 张场景参考图`); return; } files.forEach(file => { if (file.size > 10 * 1024 * 1024) { toast.error(`图片 ${file.name} 大小不能超过 10MB`); return; } const reader = new FileReader(); reader.onload = (e) => { const newScene: SceneItem = { id: Math.random().toString(36).substring(7), image: e.target?.result as string, customPrompt: "", targetName: "", // 背景中需要被替换的物体名称 status: 'pending' }; setScenes(prev => [...prev, newScene]); }; reader.readAsDataURL(file); }); // 重置 input e.target.value = ""; }; // 移除场景 const removeScene = (id: string) => { setScenes(prev => prev.filter(scene => scene.id !== id)); }; // 更新场景关键词 const updateScenePrompt = (id: string, prompt: string) => { setScenes(prev => prev.map(scene => scene.id === id ? { ...scene, customPrompt: prompt } : scene )); }; // 更新场景中需要被替换的物体名称 const updateSceneTargetName = (id: string, targetName: string) => { setScenes(prev => prev.map(scene => scene.id === id ? { ...scene, targetName } : scene )); }; // 开始裂变 const startFusion = async () => { if (config?.enabled === false) { toast.error("场景裂变功能已被管理员禁用"); return; } if (!productImage) { toast.error("请先上传产品图"); return; } if (scenes.length === 0) { toast.error("请至少添加一张场景参考图"); return; } setIsProcessing(true); // 将所有 pending 或 failed 的状态改为 processing const scenesToProcess = scenes.filter(s => s.status === 'pending' || s.status === 'failed'); setScenes(prev => prev.map(s => (s.status === 'pending' || s.status === 'failed') ? { ...s, status: 'processing' } : s )); try { console.log('[SceneFusion Client] Starting streaming API call with', scenesToProcess.length, 'scenes'); // 创建 AbortController 用于超时控制(10分钟) const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10 * 60 * 1000); // 调用后端 API(流式响应) const response = await fetch('/api/scene-fusion', { method: 'POST', headers: { 'Content-Type': 'application/json', }, signal: controller.signal, body: JSON.stringify({ productImage, productName: productName || undefined, // 产品名称 aspectRatio, // 图片比例 imageSize, // 图片分辨率 scenes: scenesToProcess.map(s => ({ id: s.id, image: s.image, targetName: s.targetName || undefined, // 背景中需要被替换的物体名称 customPrompt: s.customPrompt || undefined, })), }), }); clearTimeout(timeoutId); console.log('[SceneFusion Client] Response received, status:', response.status); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || '处理失败'); } // 流式读取响应 const reader = response.body?.getReader(); const decoder = new TextDecoder(); if (!reader) { throw new Error('无法读取响应流'); } let buffer = ''; let totalScenes = 0; let completedCount = 0; let failedCount = 0; while (true) { const { done, value } = await reader.read(); if (done) { console.log('[SceneFusion Client] Stream completed'); break; } // 解码数据并添加到缓冲区 buffer += decoder.decode(value, { stream: true }); // 处理完整的行(以\n分隔) const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 保留最后一个不完整的行 for (const line of lines) { if (!line.trim()) continue; try { const message = JSON.parse(line); console.log('[SceneFusion Client] Received message:', message.type); if (message.type === 'start') { totalScenes = message.total; console.log('[SceneFusion Client] Total scenes:', totalScenes); } else if (message.type === 'result') { const result = message.data; console.log('[SceneFusion Client] Scene result:', result.id, result.status); // 立即更新对应场景的状态 setScenes(prev => prev.map(s => { if (s.id === result.id) { if (result.status === 'completed') { completedCount++; toast.success(`场景 ${completedCount} 生成完成!`); return { ...s, status: 'completed', result: result.result, originalUrl: result.originalUrl, }; } else { failedCount++; return { ...s, status: 'failed', error: result.error || '生成失败' }; } } return s; })); } else if (message.type === 'done') { const { summary } = message; console.log('[SceneFusion Client] All done:', summary); if (summary.failed > 0) { toast.warning(`批量处理完成:${summary.success} 成功,${summary.failed} 失败`); } else { toast.success(`批量处理完成:${summary.success} 张图片已生成`); } } else if (message.type === 'error') { console.error('[SceneFusion Client] Stream error:', message.error); toast.error(`处理失败:${message.error || '未知错误'}`); // 将所有处理中的场景标记为失败 setScenes(prev => prev.map(s => s.status === 'processing' ? { ...s, status: 'failed', error: message.error || '处理失败' } : s )); } } catch (parseError) { console.error('[SceneFusion Client] Failed to parse message:', parseError, 'Line:', line); } } } } catch (error) { console.error('[SceneFusion] Error:', error); toast.error(error instanceof Error ? error.message : '处理失败'); // 将 processing 状态改回 failed setScenes(prev => prev.map(s => s.status === 'processing' ? { ...s, status: 'failed', error: '处理失败' } : s )); } finally { setIsProcessing(false); } }; // 重新生成单个场景 const regenerateScene = async (id: string) => { if (!productImage) return; const scene = scenes.find(s => s.id === id); if (!scene) return; setScenes(prev => prev.map(s => s.id === id ? { ...s, status: 'processing', error: undefined } : s )); try { const response = await fetch('/api/scene-fusion', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ productImage, productName: productName || undefined, aspectRatio, // 图片比例 imageSize, // 图片分辨率 scenes: [{ id: scene.id, image: scene.image, targetName: scene.targetName || undefined, customPrompt: scene.customPrompt || undefined, }], }), }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || '处理失败'); } const result = data.results?.[0]; if (result?.status === 'completed') { setScenes(prev => prev.map(s => s.id === id ? { ...s, status: 'completed', result: result.result, originalUrl: result.originalUrl, } : s )); toast.success('重新生成成功'); } else { throw new Error(result?.error || '生成失败'); } } catch (error) { console.error('[SceneFusion] Regenerate error:', error); setScenes(prev => prev.map(s => s.id === id ? { ...s, status: 'failed', error: error instanceof Error ? error.message : '处理失败' } : s )); toast.error(error instanceof Error ? error.message : '重新生成失败'); } }; // 下载图片 const downloadImage = (url: string, filename: string) => { const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); }; // 全部下载 const downloadAll = async () => { const completedScenes = scenes.filter(s => s.status === 'completed' && (s.originalUrl || s.result)); if (completedScenes.length === 0) { toast.error("没有可下载的图片"); return; } toast.info(`正在下载 ${completedScenes.length} 张图片...`); for (let i = 0; i < completedScenes.length; i++) { const scene = completedScenes[i]; const url = scene.originalUrl || scene.result!; // 延迟下载避免浏览器阻止 setTimeout(() => { downloadImage(url, `scene-fusion-${i + 1}.png`); }, i * 300); } }; return (
{/* 顶部标题栏 - 已在外部显示,此处隐藏 */ /*
场景裂变
上传1张产品图和多张场景图,AI 自动将产品融合到所有场景中
*/}
{/* 左侧:输入区域 */}
{/* 1. 产品图上传 */}
{productImage && ( )}
!productImage && productInputRef.current?.click()} > {productImage ? (
产品图
) : (
点击上传产品图
)}
{/* 产品名称输入 */} {productImage && (
setProductName(e.target.value)} className="h-9 text-sm" disabled={isProcessing} />

输入产品名称可以让 AI 更精准理解

)}
{/* 1.5 图片尺寸设置 */}
{/* 比例选择 */}
{aspectRatioOptions.map((option) => ( ))}
{/* 分辨率选择 */}
{imageSizeOptions.map((option) => ( ))}
{/* 2. 场景图列表 */}
{scenes.length === 0 && (
sceneInputRef.current?.click()} >

点击添加场景参考图

支持批量上传,最多{maxScenes}张

)} {scenes.map((scene, index) => (
{/* 缩略图 */}
{`场景
{/* 输入区域 */}
场景 {index + 1} {!isProcessing && ( )}
updateSceneTargetName(scene.id, e.target.value)} className="h-7 text-xs" disabled={isProcessing} /> updateScenePrompt(scene.id, e.target.value)} className="h-7 text-xs" disabled={isProcessing} />
))}
{/* 右侧:结果展示区域 */}

生成结果

{scenes.some(s => s.status === 'completed') && ( )}
{scenes.length === 0 ? (

添加场景图并开始裂变,结果将显示在这里

) : (
{scenes.map((scene, index) => (
场景 {index + 1} {scene.status === 'completed' ? '完成' : scene.status === 'failed' ? '失败' : scene.status === 'processing' ? '生成中' : '等待'}
{scene.result ? ( 生成结果 ) : (
{scene.status === 'processing' ? ( <> {/* 背景模糊图 - 使用更大的负边距和缩放确保完全覆盖,消除黑边 */} 生成中 {/* 动态遮罩层 */}
{/* 动态光效 - 模拟扫描效果 */}
{/* 中心 Loading */}
{/* 外圈脉冲 */}
正在融合...
) : scene.status === 'failed' ? (

{scene.error || "生成失败"}

) : (
等待处理
)}
)} {/* 悬停操作层 */} {scene.status === 'completed' && (
)}
))}
)}
); }