"use client"; import { useEffect, useMemo, useState } from "react"; import { Plus, Sparkles } from "lucide-react"; import { AgentWorkflowMaterialPicker } from "@/components/agents/AgentWorkflowMaterialPicker"; import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config"; import { cn } from "@/lib/utils"; import type { AgentWorkflowInputs } from "@/components/agents/AgentWorkflowPanel"; import type { AgentWorkflowMaterialSearchResult, AgentWorkflowMaterialQueryType, AgentWorkflowMaterialSelections, } from "@/types/agent"; interface AgentWorkflowMaterialSelectionPanelProps { inputs: AgentWorkflowInputs; onInputsChange: (next: AgentWorkflowInputs) => void; materials: AgentWorkflowMaterialSelections; onMaterialsChange: (next: AgentWorkflowMaterialSelections) => void; productSelectionLimit?: number | null; title?: string; description?: string; disabled?: boolean; disabledHint?: string; theme?: "light" | "dark"; } export function AgentWorkflowMaterialSelectionPanel({ inputs, onInputsChange, materials, onMaterialsChange, productSelectionLimit = null, title, description, disabled = false, disabledHint = "请先在下方选择一个生图模式,再启用 SKU / SPU 搜图。", theme = "light", }: AgentWorkflowMaterialSelectionPanelProps) { const hasSecondaryMaterialContent = Boolean(String(inputs.secondarySku || "").trim()) || Boolean(materials.secondary.result) || materials.secondary.selectedImageIds.length > 0; const buildLoadingMaterialResult = (params: { keyword: string; currentResult?: AgentWorkflowMaterialSearchResult | null; placeholderCount: number; }): AgentWorkflowMaterialSearchResult => { const count = Math.max(4, Math.min(params.placeholderCount || 8, 16)); return { query: params.keyword, queryType: "sku", matchedSku: params.keyword, matchedSpu: params.currentResult?.matchedSpu || null, materialId: params.currentResult?.materialId || null, productModel: params.currentResult?.productModel || null, zipUrl: null, fixedCount: count, otherCount: 0, totalCount: count, skuOptions: [], images: Array.from({ length: count }, (_, index) => ({ id: `loading-${params.keyword}-${index + 1}`, key: `loading-${params.keyword}-${index + 1}`, label: `加载中 ${index + 1}`, fileName: `正在加载白底图 ${index + 1}`, imageUrl: "", assetId: null, mimeType: null, group: "fixed", sortOrder: index, })), }; }; const [materialSearchState, setMaterialSearchState] = useState<{ primary: { isLoading: boolean; errorText: string | null }; secondary: { isLoading: boolean; errorText: string | null }; }>({ primary: { isLoading: false, errorText: null }, secondary: { isLoading: false, errorText: null }, }); const [materialSearchMode, setMaterialSearchMode] = useState<{ primary: AgentWorkflowMaterialQueryType; secondary: AgentWorkflowMaterialQueryType; }>({ primary: "sku", secondary: "sku", }); const [isSecondaryMaterialVisible, setIsSecondaryMaterialVisible] = useState( hasSecondaryMaterialContent ); const primarySelectedCount = materials.primary.selectedImageIds.length; const secondarySelectedCount = materials.secondary.selectedImageIds.length; const primarySelectionLimit = typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 ? Math.max(productSelectionLimit - secondarySelectedCount, primarySelectedCount, 0) : null; const secondarySelectionLimit = typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 ? Math.max(productSelectionLimit - primarySelectedCount, secondarySelectedCount, 0) : null; const primarySelectedImages = useMemo( () => (materials.primary.result?.images || []).filter((image) => materials.primary.selectedImageIds.includes(image.id) ), [materials.primary.result?.images, materials.primary.selectedImageIds] ); const secondarySelectedImages = useMemo( () => (materials.secondary.result?.images || []).filter((image) => materials.secondary.selectedImageIds.includes(image.id) ), [materials.secondary.result?.images, materials.secondary.selectedImageIds] ); const slotPreviewCount = useMemo(() => { if ( typeof productSelectionLimit === "number" && Number.isFinite(productSelectionLimit) && productSelectionLimit > 0 ) { return Math.round(productSelectionLimit); } return primarySelectedImages.length + secondarySelectedImages.length; }, [primarySelectedImages.length, productSelectionLimit, secondarySelectedImages.length]); const slotPreviewItems = useMemo(() => { const orderedSources = [ ...primarySelectedImages.map((image, index) => ({ source: "主商品" as const, image, sourceIndex: index, })), ...secondarySelectedImages.map((image, index) => ({ source: "搭配商品" as const, image, sourceIndex: index, })), ]; return Array.from({ length: slotPreviewCount }, (_, index) => { const assigned = orderedSources[index] || null; return { slotLabel: `产品图${index + 1}`, assigned, }; }); }, [primarySelectedImages, secondarySelectedImages, slotPreviewCount]); const hasAnyQueryInput = Boolean(String(inputs.primarySku || "").trim()) || Boolean(String(inputs.secondarySku || "").trim()); const hasSkuSelectionPending = Boolean(materials.primary.result?.skuOptions?.length) || Boolean(materials.secondary.result?.skuOptions?.length); const hasAnyMaterialResult = Boolean(materials.primary.result?.images?.length) || Boolean(materials.secondary.result?.images?.length) || hasSkuSelectionPending; const hasAnySelectedImages = materials.primary.selectedImageIds.length > 0 || materials.secondary.selectedImageIds.length > 0; const materialGuideSteps = [ { badge: "01", label: "输入型号", description: "先选择按 SPU 或 SKU 搜索,再输入商品型号。", status: hasAnyQueryInput ? "completed" : "current", }, { badge: "02", label: "选择SKU", description: "如果是 SPU 查询,先从返回的 SKU 列表里点选具体 SKU。", status: hasSkuSelectionPending ? "current" : hasAnyMaterialResult ? "completed" : "pending", }, { badge: "03", label: "勾选图片", description: "勾选你要带入生图的白底图,系统会自动映射到产品槽位。", status: hasAnySelectedImages ? "completed" : hasAnyMaterialResult ? "current" : "pending", }, ] as const; useEffect(() => { if (hasSecondaryMaterialContent) { setIsSecondaryMaterialVisible(true); } }, [hasSecondaryMaterialContent]); const updateMaterialSearchState = ( source: "primary" | "secondary", updates: Partial<{ isLoading: boolean; errorText: string | null }> ) => { setMaterialSearchState((prev) => ({ ...prev, [source]: { ...prev[source], ...updates, }, })); }; const handleMaterialInputChange = (source: "primary" | "secondary", value: string) => { const inputKey = source === "primary" ? "primarySku" : "secondarySku"; const trimmedValue = value.trim().toUpperCase(); const currentQuery = String(materials[source]?.result?.query || "").trim().toUpperCase(); onInputsChange({ ...inputs, [inputKey]: value, }); if ( trimmedValue !== currentQuery && (materials[source]?.result || materials[source]?.selectedImageIds.length) ) { onMaterialsChange({ ...materials, [source]: createEmptyAgentWorkflowMaterialSelection(), }); } if (materialSearchState[source].errorText) { updateMaterialSearchState(source, { errorText: null }); } }; const handleSearchMaterials = async ( source: "primary" | "secondary", options?: { keyword?: string; queryType?: AgentWorkflowMaterialQueryType; } ) => { const keyword = String(options?.keyword || (source === "primary" ? inputs.primarySku : inputs.secondarySku)).trim(); const queryType = options?.queryType || materialSearchMode[source]; if (!keyword) { updateMaterialSearchState(source, { errorText: "请先输入搜索关键词" }); return; } try { updateMaterialSearchState(source, { isLoading: true, errorText: null }); const response = await fetch( `/api/agent-workflow/material-search?keyword=${encodeURIComponent(keyword)}&source=${source}&queryType=${queryType}`, { cache: "no-store" } ); const result = await response.json().catch(() => ({})); if (!response.ok || !result?.success || !result?.data) { throw new Error(result?.error?.message || result?.error || "搜索失败"); } onMaterialsChange({ ...materials, [source]: { result: result.data, selectedImageIds: [], }, }); } catch (error) { updateMaterialSearchState(source, { errorText: error instanceof Error ? error.message : "搜索失败", }); } finally { updateMaterialSearchState(source, { isLoading: false }); } }; const isDark = theme === "dark"; return (
{title || description ? (
{title ? (
{title}
) : null} {description ? (
{description}
) : null}
) : null}
选图流程
按这 3 步操作就能完成输入图准备,避免来回试错。
{materialGuideSteps.map((step) => (
{step.badge}
{step.label}
{step.description}
))}
{slotPreviewItems.length > 0 ? (
产品图槽位映射
当前已选图片会按顺序自动带入后续生图槽位,先主商品,后搭配商品。
{slotPreviewItems.map((item, index) => { const source = item.assigned?.source || null; const previewUrl = item.assigned?.image.imageUrl || ""; const isAssigned = Boolean(item.assigned && previewUrl); return (
{isAssigned ? ( {item.assigned?.image.label ) : (
待选择
)}
{item.slotLabel}
{source || "空槽位"}
{isAssigned ? item.assigned?.image.label || `已映射到${item.slotLabel}` : `${item.slotLabel}待补图`}
{isAssigned ? `${source}第 ${(item.assigned?.sourceIndex || 0) + 1} 张` : "先在下方搜索并选择图片"}
); })}
) : null} 0 ? `当前模式产品图总上限 ${productSelectionLimit} 张` : undefined } onValueChange={(value) => handleMaterialInputChange("primary", value)} onQueryModeChange={(nextMode) => setMaterialSearchMode((prev) => ({ ...prev, primary: nextMode, })) } onSearch={() => { void handleSearchMaterials("primary"); }} onSkuOptionSelect={(skuName) => { const selectedOption = materials.primary.result?.skuOptions?.find( (option) => option.skuName === skuName ); const placeholderCount = selectedOption?.whiteBackgroundCount || 8; onInputsChange({ ...inputs, primarySku: skuName, }); onMaterialsChange({ ...materials, primary: { result: buildLoadingMaterialResult({ keyword: skuName, currentResult: materials.primary.result, placeholderCount, }), selectedImageIds: [], }, }); setMaterialSearchMode((prev) => ({ ...prev, primary: "sku", })); void handleSearchMaterials("primary", { keyword: skuName, queryType: "sku", }); }} theme={theme} onSelectedImageIdsChange={(next) => onMaterialsChange({ ...materials, primary: { ...materials.primary, selectedImageIds: next, }, }) } /> {isSecondaryMaterialVisible ? ( 0 ? `当前模式产品图总上限 ${productSelectionLimit} 张` : undefined } onValueChange={(value) => handleMaterialInputChange("secondary", value)} onQueryModeChange={(nextMode) => setMaterialSearchMode((prev) => ({ ...prev, secondary: nextMode, })) } onSearch={() => { void handleSearchMaterials("secondary"); }} onSkuOptionSelect={(skuName) => { const selectedOption = materials.secondary.result?.skuOptions?.find( (option) => option.skuName === skuName ); const placeholderCount = selectedOption?.whiteBackgroundCount || 8; onInputsChange({ ...inputs, secondarySku: skuName, }); onMaterialsChange({ ...materials, secondary: { result: buildLoadingMaterialResult({ keyword: skuName, currentResult: materials.secondary.result, placeholderCount, }), selectedImageIds: [], }, }); setMaterialSearchMode((prev) => ({ ...prev, secondary: "sku", })); void handleSearchMaterials("secondary", { keyword: skuName, queryType: "sku", }); }} theme={theme} onSelectedImageIdsChange={(next) => onMaterialsChange({ ...materials, secondary: { ...materials.secondary, selectedImageIds: next, }, }) } /> ) : ( )}
); }