diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 976f94eee..3640af380 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -105,6 +105,7 @@ import { } from "@/lib/agents/generation-project-state"; import { agentProjectService } from "@/services/agent-projects"; import type { + AgentPreset, AgentGenerationProjectWorkspaceState, AgentModeWorkspaceState, AgentWorkflowComposerContext, @@ -1354,6 +1355,36 @@ export default function CanvasEditorPage({ params }: PageProps) { workflowMaterials, workflowSelections, ]); + const { data: selectedAgentPresetResponse } = useSWR<{ + success?: boolean; + data?: AgentPreset; + }>(selectedAgentPresetId ? `/api/agents/${selectedAgentPresetId}` : null, fetcher); + const selectedAgentPresetData = useMemo( + () => + selectedAgentPresetResponse?.success && selectedAgentPresetResponse.data + ? selectedAgentPresetResponse.data + : null, + [selectedAgentPresetResponse] + ); + const selectedAgentPresetModeConfig = useMemo( + () => normalizeAgentPresetModeConfig(selectedAgentPresetData?.defaultParams), + [selectedAgentPresetData?.defaultParams] + ); + const materialProductSelectionLimit = useMemo(() => { + if (selectedAgentPresetData?.type !== "image") { + return null; + } + if (selectedAgentPresetModeConfig.modeType === "scene_generation") { + return selectedAgentPresetModeConfig.scene.productImageSlotCount; + } + if (selectedAgentPresetModeConfig.modeType === "detail_replacement") { + return selectedAgentPresetModeConfig.detail.productImageSlotCount; + } + if (selectedAgentPresetModeConfig.modeType === "sku_replacement") { + return selectedAgentPresetModeConfig.sku.productImageSlotCount; + } + return 2; + }, [selectedAgentPresetData?.type, selectedAgentPresetModeConfig]); const workflowProgress = useMemo( () => getAgentWorkflowProgress({ @@ -1535,6 +1566,7 @@ export default function CanvasEditorPage({ params }: PageProps) { onInputsChange={setWorkflowInputs} materials={workflowMaterials} onMaterialsChange={setWorkflowMaterials} + productSelectionLimit={materialProductSelectionLimit} disabled={!selectedAgentPresetId} theme="dark" /> diff --git a/components/agents/AgentWorkflowMaterialPicker.tsx b/components/agents/AgentWorkflowMaterialPicker.tsx index 08b9cc5ac..76d4cafa8 100644 --- a/components/agents/AgentWorkflowMaterialPicker.tsx +++ b/components/agents/AgentWorkflowMaterialPicker.tsx @@ -2,6 +2,7 @@ import { useMemo } from "react"; import { Check, ImageIcon, Loader2, RefreshCw, Search } from "lucide-react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { cn } from "@/lib/utils"; @@ -24,6 +25,8 @@ interface AgentWorkflowMaterialPickerProps { errorText?: string | null; result?: AgentWorkflowMaterialSearchResult | null; selectedImageIds: string[]; + selectionLimit?: number | null; + selectionLimitLabel?: string; onValueChange: (value: string) => void; onQueryModeChange: (value: AgentWorkflowMaterialQueryType) => void; onSearch: () => void; @@ -46,6 +49,8 @@ export function AgentWorkflowMaterialPicker({ errorText, result, selectedImageIds, + selectionLimit = null, + selectionLimitLabel = "当前生图模式", onValueChange, onQueryModeChange, onSearch, @@ -61,26 +66,41 @@ export function AgentWorkflowMaterialPicker({ const selectedCount = result?.images ? result.images.filter((item) => selectedSet.has(item.id)).length : 0; + const resolvedSelectionLimit = + typeof selectionLimit === "number" && Number.isFinite(selectionLimit) && selectionLimit > 0 + ? Math.max(1, Math.round(selectionLimit)) + : null; const hasSelectableImages = Boolean( result?.images?.some((item) => Boolean(String(item.imageUrl || "").trim())) ); + const remainingSelectableCount = + resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0); const handleToggleImage = (imageId: string) => { const next = new Set(selectedImageIds); if (next.has(imageId)) { next.delete(imageId); } else { + if (resolvedSelectionLimit !== null && next.size >= resolvedSelectionLimit) { + toast.error(`${selectionLimitLabel}最多可选择 ${resolvedSelectionLimit} 张图片`); + return; + } next.add(imageId); } onSelectedImageIdsChange(Array.from(next)); }; const handleSelectAll = () => { - onSelectedImageIdsChange( + const allIds = result?.images ?.filter((item) => Boolean(String(item.imageUrl || "").trim())) - .map((item) => item.id) || [] - ); + .map((item) => item.id) || []; + const nextIds = + resolvedSelectionLimit !== null ? allIds.slice(0, resolvedSelectionLimit) : allIds; + onSelectedImageIdsChange(nextIds); + if (resolvedSelectionLimit !== null && allIds.length > resolvedSelectionLimit) { + toast.message(`${selectionLimitLabel}最多可选择 ${resolvedSelectionLimit} 张图片,已按上限选取`); + } }; return ( @@ -168,6 +188,19 @@ export function AgentWorkflowMaterialPicker({ {searchNote} ) : null} + {resolvedSelectionLimit !== null ? ( +