"use client"; import { useEffect, useId, useRef, useState } from "react"; import { LayoutTemplate, Loader2, Upload } from "lucide-react"; import { toast } from "sonner"; import { AgentPreviewImage } from "@/components/agents/AgentPreviewImage"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { imageGenerationService } from "@/services/canvas"; const POLL_INTERVAL_MS = 2500; const POLL_LIMIT = 240; export type AgentAiTemplateResult = { imageUrl: string; sourceImageUrl: string; referenceImageUrl: string; }; type AgentAiTemplateDialogProps = { open: boolean; sourceImageUrl?: string | null; model?: string | null; imageSize?: string | null; projectId: string; onOpenChange: (open: boolean) => void; onProcessingChange?: (processing: boolean, sourceImageUrl: string) => void; onResultCreated: (result: AgentAiTemplateResult) => void | Promise; }; function isHttpImageUrl(value?: string | null) { return /^https?:\/\//i.test(String(value || "").trim()); } function sleep(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } async function pollAiTemplateTask(taskId: string) { for (let attempt = 0; attempt < POLL_LIMIT; attempt += 1) { await sleep(POLL_INTERVAL_MS); const taskResult = await imageGenerationService.getImageTask(taskId); const task = taskResult.data?.task; if (!taskResult.success || !task) { continue; } const status = String(task.status || "").toLowerCase(); if (status === "succeeded" || status === "completed") { const image = task.result?.images?.[0]; const imageUrl = String(image?.originalUrl || image?.thumbnailUrl || "").trim(); if (!isHttpImageUrl(imageUrl)) { throw new Error("AI套模版已完成但未返回结果图片"); } return imageUrl; } if (["failed", "cancelled", "expired", "banned"].includes(status)) { throw new Error(task.error || "AI套模版生成失败"); } } throw new Error("AI套模版任务等待超时,请稍后在结果区查看"); } export function AgentAiTemplateDialog({ open, sourceImageUrl, model, imageSize, projectId, onOpenChange, onProcessingChange, onResultCreated, }: AgentAiTemplateDialogProps) { const referenceUploadId = useId(); const [referenceImageUrl, setReferenceImageUrl] = useState(""); const [supplementalPrompt, setSupplementalPrompt] = useState(""); const [isUploading, setIsUploading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const normalizedSourceUrl = String(sourceImageUrl || "").trim(); const onProcessingChangeRef = useRef(onProcessingChange); const onResultCreatedRef = useRef(onResultCreated); useEffect(() => { onProcessingChangeRef.current = onProcessingChange; onResultCreatedRef.current = onResultCreated; }, [onProcessingChange, onResultCreated]); useEffect(() => { if (!open || isSubmitting) return; setReferenceImageUrl(""); setSupplementalPrompt(""); setIsUploading(false); }, [isSubmitting, open, normalizedSourceUrl]); const handleReferenceUpload = async (file?: File) => { if (!file) return; if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } setIsUploading(true); try { const formData = new FormData(); formData.append("file", file); const response = await fetchUploadWithRetry(formData); const payload = await response.json().catch(() => null); const uploadedUrl = String(payload?.url || "").trim(); if (!response.ok || !isHttpImageUrl(uploadedUrl)) { throw new Error(payload?.error || "参考图上传失败"); } setReferenceImageUrl(uploadedUrl); toast.success("版式参考图已上传"); } catch (error) { toast.error(error instanceof Error ? error.message : "参考图上传失败"); } finally { setIsUploading(false); } }; const handleSubmit = async () => { const normalizedReferenceUrl = referenceImageUrl.trim(); if (!isHttpImageUrl(normalizedSourceUrl)) { toast.error("当前主图结果地址不可用"); return; } if (!isHttpImageUrl(normalizedReferenceUrl)) { toast.error("请先上传或填写版式参考图"); return; } if (isSubmitting) return; setIsSubmitting(true); onProcessingChangeRef.current?.(true, normalizedSourceUrl); try { const submitResult = await imageGenerationService.layoutReplace({ sourceImageUrl: normalizedSourceUrl, referenceImageUrl: normalizedReferenceUrl, supplementalPrompt: supplementalPrompt.trim() || undefined, model: model && model !== "auto" ? model : undefined, imageSize: imageSize || "2K", agentProjectId: projectId, referenceLabel: "风格化主图 AI 套模版参考图", debugMeta: { operation: "agent-main-ai-template", source: "agent-main-result-card", }, }); const taskId = String(submitResult.data?.taskId || "").trim(); if (!submitResult.success || !taskId) { throw new Error(submitResult.error || "AI套模版提交失败"); } onOpenChange(false); toast.success("AI套模版已进入生图队列"); const resultImageUrl = await pollAiTemplateTask(taskId); await onResultCreatedRef.current({ imageUrl: resultImageUrl, sourceImageUrl: normalizedSourceUrl, referenceImageUrl: normalizedReferenceUrl, }); toast.success("AI套模版结果已加入主图结果区"); } catch (error) { toast.error(error instanceof Error ? error.message : "AI套模版生成失败"); } finally { setIsSubmitting(false); onProcessingChangeRef.current?.(false, normalizedSourceUrl); } }; return ( { if (!isUploading) { onOpenChange(nextOpen); } }} > AI套模版 保留参考图的版式、文字和图标,仅将产品主体替换为当前主图。
{isHttpImageUrl(normalizedSourceUrl) ? ( ) : null}
setReferenceImageUrl(event.currentTarget.value)} placeholder="填写图片链接或上传" value={referenceImageUrl} /> { const file = event.currentTarget.files?.[0]; event.currentTarget.value = ""; void handleReferenceUpload(file); }} type="file" />
{isUploading ? ( ) : isHttpImageUrl(referenceImageUrl) ? ( ) : ( )}