diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 0d2413fe6..bfc29ab96 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -2,7 +2,7 @@ import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; -import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import useSWR from "swr"; import { motion, AnimatePresence } from "framer-motion"; import { @@ -12,6 +12,7 @@ import { Loader2, Sparkles, Upload, + UserRound, X, Wand2, Settings2, @@ -65,6 +66,14 @@ interface GeneratedImageGroup { text: string; } +type ToolStepKey = + | "preset" + | "brand-model" + | "materials" + | "slots" + | "prompt" + | "settings"; + function buildPreviewImageUrl(url: string, width: number) { return buildImageProxyUrl(url, { width, quality: 86, format: "webp" }); } @@ -228,7 +237,9 @@ export function AgentGenerationWorkspaceDarkV2({ () => selectableModels.find((model) => model.id === selectedModelId) || selectableModels[0], [selectableModels, selectedModelId] ); - const shouldShowFullWidthPresetPanel = Boolean(!selectedAgentPreset && presetPanel); + const [activeToolStep, setActiveToolStep] = useState("preset"); + const hasBrandModelStep = Boolean(brandModelPanel); + const hasMaterialStep = Boolean(materialSelectionPanel); const { messages, setMessages, sendMessage, status } = useChat({ id: chatId, @@ -376,6 +387,98 @@ export function AgentGenerationWorkspaceDarkV2({ const latestGeneratedGroup = generatedGroups[generatedGroups.length - 1] || null; const displayedResultUrl = selectedResultUrl || latestGeneratedGroup?.imageUrls[0] || null; const isGenerating = status === "streaming" || status === "submitted"; + const attachmentCount = attachments.filter(Boolean).length; + const hasBrandModelSelection = Boolean( + selectedBrandModel?.imageUrl || referenceImages.some((image) => image.source === "brandModel") + ); + const hasMaterialSelections = referenceImages.some( + (image) => image.source === "primary" || image.source === "secondary" + ); + const hasSatisfiedSlotRequirement = !hasAgentUploadSlots || attachmentCount >= requiredUploadCount; + + const toolSteps = useMemo( + () => [ + { + key: "preset" as const, + label: "生图模式", + description: "先选择一个模式,再继续后续配置", + icon: Wand2, + disabled: false, + completed: Boolean(selectedAgentPresetId), + optional: false, + }, + ...(hasBrandModelStep + ? [ + { + key: "brand-model" as const, + label: "品牌模特", + description: "根据品牌选择模特图", + icon: UserRound, + disabled: !selectedAgentPresetId, + completed: hasBrandModelSelection, + optional: true, + }, + ] + : []), + ...(hasMaterialStep + ? [ + { + key: "materials" as const, + label: "SKU / SPU 选图", + description: "搜索主图和搭配图", + icon: Upload, + disabled: !selectedAgentPresetId, + completed: hasMaterialSelections, + optional: true, + }, + ] + : []), + { + key: "slots" as const, + label: "图片槽位", + description: "检查和补齐参考图槽位", + icon: ImagePlus, + disabled: !selectedAgentPresetId, + completed: hasSatisfiedSlotRequirement, + optional: false, + }, + { + key: "prompt" as const, + label: "补充要求", + description: "补充风格、构图和细节要求", + icon: Sparkles, + disabled: !selectedAgentPresetId, + completed: Boolean(input.trim()), + optional: true, + }, + { + key: "settings" as const, + label: "生成参数", + description: "选择模型并开始生成", + icon: Settings2, + disabled: !selectedAgentPresetId, + completed: Boolean(selectedAgentPresetId), + optional: false, + }, + ], + [ + hasBrandModelSelection, + hasBrandModelStep, + hasMaterialSelections, + hasMaterialStep, + hasSatisfiedSlotRequirement, + input, + selectedAgentPresetId, + ] + ); + const activeToolStepIndex = Math.max( + toolSteps.findIndex((step) => step.key === activeToolStep), + 0 + ); + const activeToolStepMeta = toolSteps[activeToolStepIndex] || toolSteps[0]; + const previousToolStep = activeToolStepIndex > 0 ? toolSteps[activeToolStepIndex - 1] : null; + const nextToolStep = + activeToolStepIndex < toolSteps.length - 1 ? toolSteps[activeToolStepIndex + 1] : null; useEffect(() => { if (!selectedResultUrl && latestGeneratedGroup?.imageUrls[0]) { @@ -383,6 +486,26 @@ export function AgentGenerationWorkspaceDarkV2({ } }, [latestGeneratedGroup?.imageUrls, selectedResultUrl]); + useEffect(() => { + if (!selectedAgentPresetId) { + setActiveToolStep("preset"); + return; + } + + setActiveToolStep((current) => { + if (current !== "preset") return current; + if (hasBrandModelStep) return "brand-model"; + if (hasMaterialStep) return "materials"; + return "slots"; + }); + }, [hasBrandModelStep, hasMaterialStep, selectedAgentPresetId]); + + useEffect(() => { + if (!toolSteps.some((step) => step.key === activeToolStep)) { + setActiveToolStep("preset"); + } + }, [activeToolStep, toolSteps]); + useEffect(() => { const latestUrls = latestGeneratedGroup?.imageUrls || []; if (latestUrls.length === 0) return; @@ -537,6 +660,19 @@ export function AgentGenerationWorkspaceDarkV2({ } }, []); + const handleToolStepChange = useCallback( + (nextKey: ToolStepKey) => { + const nextStep = toolSteps.find((step) => step.key === nextKey); + if (!nextStep) return; + if (nextStep.disabled) { + toast.error("请先选择生图模式"); + return; + } + setActiveToolStep(nextKey); + }, + [toolSteps] + ); + return (
- {shouldShowFullWidthPresetPanel ? ( - -
-
-

选择生图模式

-

- 先选一个模式,再继续配置图片槽位、素材图和生成参数。 -

+ +
+
+
Step Flow
+

第二步:生图模式执行流程

+

+ 将原来的侧边栏工具整合为顶部流程切换,按步骤逐项完成配置后再生成。 +

+
+
+
+ 第一步:风格选择 + → + 第二步:生图模式
-
- 支持搜索、预览与新建模式 +
+ 当前步骤 {activeToolStepIndex + 1} / {toolSteps.length}
-
- {presetPanel} +
+ +
+
+ {toolSteps.map((step, index) => { + const Icon = step.icon; + const isActive = activeToolStep === step.key; + return ( + + + {index < toolSteps.length - 1 ? ( +
+ ) : null} + + ); + })}
- - ) : null} +
+ -
- {/* Left Panel - Controls */} -
- {/* 嵌入的智能体选择面板 */} - {!shouldShowFullWidthPresetPanel && !selectedAgentPreset && presetPanel && ( - -

选择生图模式

-
- {presetPanel} -
-
- )} - - {/* 已选智能体信息 */} - {selectedAgentPreset && ( - -
当前生图模式
-
+ +
+
+
Current Module
+

{activeToolStepMeta.label}

+

{activeToolStepMeta.description}

+
+
+ {selectedAgentPreset ? ( +
{selectedAgentPreset.avatarUrl ? ( - {selectedAgentPreset.name} @@ -612,328 +799,352 @@ export function AgentGenerationWorkspaceDarkV2({
)}
-
{selectedAgentPreset.name}
-
{selectedAgentPreset.type === "image" ? "生图" : "视频"}
+
{selectedAgentPreset.name}
+
+ {selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"} +
- {presetPanel && onClearPreset && ( - - )} - - )} + ) : null} + {selectedAgentPreset && onClearPreset ? ( + + ) : null} +
+
- {brandModelPanel ? ( - - {brandModelPanel} - - ) : null} - - {materialSelectionPanel ? ( - - {materialSelectionPanel} - - ) : null} - - {/* Image Slots */} - -

- - 图片槽位 -

- {agentUploadSlots.length > 0 ? ( -
- {agentUploadSlots.map((slot, index) => ( - fileInputRefs.current[index]?.click()} - whileHover={{ scale: 1.03 }} - whileTap={{ scale: 0.97 }} - className={cn( - "group relative aspect-square overflow-hidden rounded-[24px] transition-all", - attachments[index] - ? "ring-2 ring-emerald-400/45" - : "border-2 border-dashed border-white/15 bg-black/25 hover:border-emerald-400/35" - )} - > - { - fileInputRefs.current[index] = el; - }} - type="file" - accept="image/*" - className="hidden" - onChange={(e) => handleSlotFileChange(index, e)} - /> - {attachments[index] ? ( - <> - {attachments[index]!.name -
- {slot.label} -
- - - ) : ( -
- {slotUploading === index ? ( - - ) : ( - <> - - {slot.label} - - )} -
- )} -
- ))} +
+
+ {presetPanel ? ( +
+ {presetPanel}
) : ( -
- - 先选择一个生图模式,系统会自动展示产品图1、产品图2和模特图槽位。 +
+ 暂无可用的生图模式配置。
)} - +
- {/* Prompt Input */} - -

- - 补充要求 -

-