"use client"; import { Check, ImageIcon, ImagePlus, Loader2, Search, Upload, X } from "lucide-react"; import { useCallback, useMemo, useRef, useState } from "react"; 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { IMAGE_UPLOAD_MAX_BYTES, IMAGE_UPLOAD_TOO_LARGE_MESSAGE } from "@/lib/image-upload-limits"; import { imageGenerationService } from "@/services/canvas"; import type { AgentWorkflowMaterialImage, AgentWorkflowMaterialQueryType, AgentWorkflowMaterialSearchResult, AgentWorkflowMaterialSkuOption, } from "@/types/agent"; export type CanvasAddImageDialogImage = AgentWorkflowMaterialImage; interface CanvasAddImageDialogProps { materialSearchEnabled: boolean; onAddImages: (images: CanvasAddImageDialogImage[]) => Promise | void; onCreateBlankImage: () => void; onOpenChange: (open: boolean) => void; open: boolean; } function getMaterialSearchError(payload: any, fallback = "搜索失败") { const error = payload?.error; if (typeof error === "string") return error; if (error?.message) return String(error.message); return fallback; } function normalizeImages(result?: AgentWorkflowMaterialSearchResult | null) { return Array.isArray(result?.images) ? result.images.filter((image) => String(image.imageUrl || "").trim()) : []; } function MaterialImageCard({ image, selected, onToggle, }: { image: AgentWorkflowMaterialImage; selected: boolean; onToggle: (id: string) => void; }) { return ( ); } function SkuOptionCard({ option, onSelect, }: { option: AgentWorkflowMaterialSkuOption; onSelect: (skuName: string) => void; }) { return ( ); } export function CanvasAddImageDialog({ materialSearchEnabled, onAddImages, onCreateBlankImage, onOpenChange, open, }: CanvasAddImageDialogProps) { const [query, setQuery] = useState(""); const [queryMode, setQueryMode] = useState("spu"); const [result, setResult] = useState(null); const [selectedImageIds, setSelectedImageIds] = useState([]); const [isSearching, setIsSearching] = useState(false); const [isAdding, setIsAdding] = useState(false); const [isUploading, setIsUploading] = useState(false); const localUploadInputRef = useRef(null); const [errorText, setErrorText] = useState(null); const images = useMemo(() => normalizeImages(result), [result]); const skuOptions = Array.isArray(result?.skuOptions) ? result.skuOptions : []; const selectedImages = useMemo( () => images.filter((image) => selectedImageIds.includes(image.id)), [images, selectedImageIds] ); const resetDialogState = useCallback(() => { setQuery(""); setQueryMode("spu"); setResult(null); setSelectedImageIds([]); setErrorText(null); }, []); const handleDialogOpenChange = useCallback( (nextOpen: boolean) => { if (!nextOpen && (isUploading || isAdding)) return; if (!nextOpen) { resetDialogState(); } onOpenChange(nextOpen); }, [isAdding, isUploading, onOpenChange, resetDialogState] ); const closeDialog = useCallback(() => { resetDialogState(); onOpenChange(false); }, [onOpenChange, resetDialogState]); const handleSearch = async (nextQuery?: string, nextMode?: AgentWorkflowMaterialQueryType) => { if (!materialSearchEnabled) return; const keyword = String(nextQuery ?? query).trim(); const mode = nextMode || queryMode; if (!keyword) { setErrorText("请输入 SPU 或 SKU"); return; } try { setIsSearching(true); setErrorText(null); const searchParams = new URLSearchParams({ keyword, source: "primary", queryType: mode, appearanceRecognitionEnabled: "false", }); const response = await fetch( `/api/agent-workflow/material-search?${searchParams.toString()}`, { cache: "no-store" } ); const payload = await response.json().catch(() => ({})); if (!response.ok || payload?.success !== true || !payload?.data) { throw new Error(getMaterialSearchError(payload)); } setResult(payload.data); setSelectedImageIds( Array.isArray(payload.data?.recommendedImageIds) ? payload.data.recommendedImageIds.filter((id: string) => payload.data.images?.some((image: AgentWorkflowMaterialImage) => image.id === id) ) : [] ); } catch (error) { setResult(null); setSelectedImageIds([]); setErrorText(error instanceof Error ? error.message : "搜索失败"); } finally { setIsSearching(false); } }; const handleSelectSku = (skuName: string) => { setQuery(skuName); setQueryMode("sku"); void handleSearch(skuName, "sku"); }; const handleToggleImage = (imageId: string) => { setSelectedImageIds((current) => current.includes(imageId) ? current.filter((id) => id !== imageId) : [...current, imageId] ); }; const handleAddSelected = async () => { if (!selectedImages.length) return; try { setIsAdding(true); await onAddImages(selectedImages); closeDialog(); } finally { setIsAdding(false); } }; const handleLocalUpload = async (files: File[]) => { if (!files.length || isUploading || isAdding) return; if (files.some((file) => !file.type.startsWith("image/"))) { setErrorText("请选择图片文件"); return; } if (files.some((file) => file.size > IMAGE_UPLOAD_MAX_BYTES)) { setErrorText(IMAGE_UPLOAD_TOO_LARGE_MESSAGE); return; } setIsUploading(true); setErrorText(null); try { const uploadedImages: CanvasAddImageDialogImage[] = []; const failures: string[] = []; for (const [index, file] of files.entries()) { const upload = await imageGenerationService.uploadImage(file); if (!upload.success || !upload.data?.url) { failures.push(`${file.name}:${upload.error || "图片上传失败"}`); continue; } uploadedImages.push({ id: upload.data.assetId || crypto.randomUUID(), key: upload.data.pathname || file.name, label: file.name.replace(/\.[^.]+$/, "") || file.name, fileName: file.name, imageUrl: upload.data.url, assetId: upload.data.assetId, mimeType: file.type, sourceType: "upload", group: "other", sortOrder: index, }); } if (uploadedImages.length) await onAddImages(uploadedImages); if (failures.length) setErrorText(failures.join(";")); else closeDialog(); } catch (error) { setErrorText(error instanceof Error ? error.message : "图片上传失败,请重试"); } finally { setIsUploading(false); } }; return ( 添加图片 从本地上传图片、添加空白图片节点,或检索 SPU / SKU 白底图。
{ const files = Array.from(event.target.files || []); event.target.value = ""; void handleLocalUpload(files); }} ref={localUploadInputRef} type="file" /> {materialSearchEnabled ? (
{ setQuery(event.target.value); setErrorText(null); }} onKeyDown={(event) => { if (event.key === "Enter") { void handleSearch(); } }} placeholder={queryMode === "spu" ? "输入 SPU 查询 SKU" : "输入 SKU 查询白底图"} value={query} /> {query ? ( ) : null}
) : null}
{!materialSearchEnabled ? (
风格化功能或素材搜索步骤未开启,可从本地上传或添加空白图片。
) : null}
{errorText ? (
{errorText}
) : null} {skuOptions.length > 0 ? (
选择 SKU
命中 {skuOptions.length} 个 SKU,选择有白底图的 SKU 继续拉取图片。
{skuOptions.map((option) => ( ))}
) : null} {images.length > 0 ? (
白底图素材 {result?.matchedSku ? ` · ${result.matchedSku}` : ""}
共 {images.length} 张,已选 {selectedImages.length} 张。
{images.map((image) => ( ))}
) : materialSearchEnabled && !isSearching && !skuOptions.length && result ? (
当前没有可添加的白底图。
) : null}
); }