diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index 82a196bdf..1e7ee82dc 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -1464,41 +1464,6 @@ export default function CanvasEditorPage({ params }: PageProps) { ) : null; if (isAgentMode) { - // 深色主题智能体模式 V2 - 完整功能版本 - - // 第一步:工作流配置(品牌/店铺/步骤选择) - if (currentAgentStage === "workflow") { - return ( - setCurrentAgentStage("mode")} - onSkipStep={() => setCurrentAgentStage("mode")} - onReset={() => { - setWorkflowInputs({ primarySku: "", secondarySku: "", brand: "", store: "" }); - setWorkflowMaterials(createEmptyAgentWorkflowMaterialSelections()); - setWorkflowSelections({}); - setSelectedBrandModel(null); - setSelectedAgentPresetId(null); - setAgentModeState(createEmptyAgentModeWorkspaceState()); - setAgentWorkspaceState(createEmptyAgentGenerationProjectWorkspaceState()); - setCurrentAgentStage("workflow"); - }} - /> - ); - } - - // 第二步:智能体选择 + 生图工作台 return ( setCurrentAgentStage("workflow")} onOpenCanvasEditor={() => { - setCurrentAgentStage("mode"); updateCanvasSearchParams({ mode: null, }); }} onClearPreset={() => setSelectedAgentPresetId(null)} + workflowStylePanel={ + + } // 嵌入的智能体选择面板 presetPanel={ void; onResultPreviewUrlsChange?: (urls: string[]) => void; topNavRightSlot?: ReactNode; + showcaseConfig?: AgentWorkflowShowcaseConfig | null; + workflowStylePanel?: ReactNode; } interface PickedAttachment extends Attachment {} @@ -297,6 +301,8 @@ export function AgentGenerationWorkspaceDarkV2({ onWorkspaceStateChange, onResultPreviewUrlsChange, topNavRightSlot, + showcaseConfig, + workflowStylePanel, }: AgentGenerationWorkspaceDarkV2Props) { const darkSelectContentClass = "border-white/10 bg-zinc-950 text-white shadow-2xl [--popover:hsl(240_10%_3.9%)] [--popover-foreground:hsl(0_0%_98%)] [--accent:rgba(255,255,255,0.08)] [--accent-foreground:hsl(0_0%_98%)] [--muted:rgba(255,255,255,0.04)] [--muted-foreground:rgba(255,255,255,0.56)]"; @@ -521,6 +527,7 @@ export function AgentGenerationWorkspaceDarkV2({ const [activeToolStep, setActiveToolStep] = useState("preset"); const hasBrandModelStep = Boolean(brandModelPanel) && modelSlotEnabled; const hasMaterialStep = Boolean(materialSelectionPanel); + const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep; const { messages, setMessages, sendMessage, status } = useChat({ id: chatId, @@ -763,6 +770,20 @@ export function AgentGenerationWorkspaceDarkV2({ const latestReplacementResultUrls = latestGeneratedGroup?.imageUrls || []; const displayedResultUrl = selectedResultUrl || latestGeneratedGroup?.imageUrls[0] || null; const isGenerating = status === "streaming" || status === "submitted"; + const sceneResultCardCount = Math.max( + allGeneratedImages.length, + imageParams.imageCount || 1, + 1 + ); + const sceneResultCards = useMemo( + () => + Array.from({ length: sceneResultCardCount }, (_, index) => ({ + id: allGeneratedImages[index] || `scene-result-placeholder-${index}`, + index, + url: allGeneratedImages[index] || null, + })), + [allGeneratedImages, sceneResultCardCount] + ); const attachmentCount = attachments.filter(Boolean).length; const hasBrandModelSelection = Boolean( selectedBrandModel?.imageUrl || referenceImages.some((image) => image.source === "brandModel") @@ -775,6 +796,58 @@ export function AgentGenerationWorkspaceDarkV2({ const mergedSettingsStepDescription = hasMaterialStep ? "搜索素材、补充要求、确认槽位并开始生成" : "补充要求并开始生成"; + const styleStepBadge = hasMaterialStep ? "03" : "02"; + const generateStepBadge = + hasMaterialStep && hasWorkflowStyleStep + ? "04" + : hasMaterialStep || hasWorkflowStyleStep + ? "03" + : "02"; + const unifiedAnchorSteps = useMemo( + () => { + const steps = [ + { + id: "agent-anchor-mode", + badge: "01", + label: "选择生图模式", + description: "先确定本次要执行的生图模式", + }, + ]; + + if (hasMaterialStep) { + steps.push({ + id: "agent-anchor-materials", + badge: "02", + label: "选择 SPU / SKU", + description: "查询商品图并确定本次输入素材", + }); + } + + if (hasWorkflowStyleStep) { + steps.push({ + id: "agent-anchor-style", + badge: hasMaterialStep ? "03" : "02", + label: "风格步骤卡片", + description: "补齐风格、品牌和模特等工作流配置", + }); + } + + steps.push({ + id: "agent-anchor-generate", + badge: + hasMaterialStep && hasWorkflowStyleStep + ? "04" + : hasMaterialStep || hasWorkflowStyleStep + ? "03" + : "02", + label: "生图与结果预览", + description: "确认参数后直接生成,并在底部查看结果", + }); + + return steps; + }, + [hasMaterialStep, hasWorkflowStyleStep] + ); const toolSteps = useMemo( () => [ @@ -1250,6 +1323,16 @@ export function AgentGenerationWorkspaceDarkV2({ }, [toolSteps] ); + const scrollToSection = useCallback((sectionId: string) => { + if (typeof document === "undefined") { + return; + } + const target = document.getElementById(sectionId); + if (!target) { + return; + } + target.scrollIntoView({ behavior: "smooth", block: "start" }); + }, []); const updateDetailReplacementState = useCallback( ( updater: @@ -1645,66 +1728,113 @@ export function AgentGenerationWorkspaceDarkV2({ {/* Main Content */}
+ {showcaseConfig ? ( + + 0 ? "查看结果预览" : undefined} + onActionClick={() => scrollToSection("agent-anchor-mode")} + onSecondaryActionClick={() => scrollToSection("agent-anchor-results")} + /> + + ) : null} + -
-
-
Current Module
-

{activeToolStepMeta.label}

-

{activeToolStepMeta.description}

- {isReplacementPreset ? ( -

- {isDetailReplacementPreset - ? "当前模板属于详情替换模式,下一步将切入专用工作台进行逐屏替换。" - : "当前模板属于 SKU 替换模式,下一步将切入专用工作台进行一对一替换。"} -

- ) : null} +
+
+
Workflow Anchors
+

单页生图流程

+

+ 在同一页面完成生图模式选择、SPU / SKU 选图、风格步骤配置、生成和结果预览。 +

-
- {selectedAgentPreset ? ( -
- {selectedAgentPreset.avatarUrl ? ( - {selectedAgentPreset.name} - ) : ( -
- -
- )} -
-
{selectedAgentPreset.name}
-
- {selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"} -
-
-
- ) : null} - {selectedAgentPreset && onClearPreset ? ( +
+ {unifiedAnchorSteps.map((step) => ( - ) : null} + ))}
-
-
+
+
+
+
+
+ + 01 + + 选择生图模式 +
+

+ 先确定本次使用的模式,后续的商品图、参考图和结果展示都会随模式切换。 +

+ {isReplacementPreset ? ( +

+ {isDetailReplacementPreset + ? "当前模板属于详情替换模式,结果区会按参考图顺序逐屏展示。" + : "当前模板属于 SKU 替换模式,结果区会按参考图顺序一对一展示。"} +

+ ) : null} +
+
+ {selectedAgentPreset ? ( +
+ {selectedAgentPreset.avatarUrl ? ( + {selectedAgentPreset.name} + ) : ( +
+ +
+ )} +
+
{selectedAgentPreset.name}
+
+ {selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"} +
+
+
+ ) : null} + {selectedAgentPreset && onClearPreset ? ( + + ) : null} +
+
+ {presetPanel ? ( -
+
{presetPanel}
) : ( @@ -1712,17 +1842,60 @@ export function AgentGenerationWorkspaceDarkV2({ 暂无可用的生图模式配置。
)} -
+ - {hasBrandModelStep ? ( -
- {brandModelPanel} -
+ {hasMaterialStep ? ( +
+
+
+ + 02 + + 选择 SPU / SKU +
+

+ 先搜索商品白底图与搭配图,再继续完成风格步骤和生成参数配置。 +

+
+ {materialSelectionPanel} +
) : null} -
+ {hasWorkflowStyleStep ? ( +
+
+
+ + {styleStepBadge} + + 风格步骤卡片 +
+

+ 在同一页完成品牌、店铺、风格卡片、品牌模特和 Prompt 组合,不再跳转第二个页面。 +

+
+ +
+ {hasBrandModelStep ? brandModelPanel : null} + {workflowStylePanel} +
+
+ ) : null} + +
+
+
+ + {generateStepBadge} + + 生图与结果预览 +
+

+ 确认补充要求、输入图槽位和生成参数后直接开始生成,结果会显示在当前页底部预览区。 +

+
+
- {hasMaterialStep ? materialSelectionPanel : null}

@@ -1735,126 +1908,93 @@ export function AgentGenerationWorkspaceDarkV2({ className="min-h-[160px] resize-none rounded-[24px] border-white/12 bg-black text-sm text-white placeholder:text-white/20 focus:border-emerald-400/30" />

+ {attachmentSlotPanel} +
-

- - 生成参数 -

-
- {isReplacementPreset ? ( -
- {isDetailReplacementPreset - ? "详情替换会按当前参考图顺序逐张执行,结果将在下方结果区一一回显。" - : "SKU替换会按当前参考图顺序执行一对一替换,结果将在下方结果区一一回显。"} -
- ) : null} +

+ + 生成参数 +

+
+ {isReplacementPreset ? ( +
+ {isDetailReplacementPreset + ? "详情替换会按当前参考图顺序逐张执行,结果将在下方结果区一一回显。" + : "SKU替换会按当前参考图顺序执行一对一替换,结果将在下方结果区一一回显。"} +
+ ) : null} - {shouldAllowModelOverride ? ( - - ) : ( -
- 当前模板已锁定模型: - - {selectedAgentPreset?.defaultModel?.displayName || currentModel?.displayName || "未配置"} - -
- )} - - - - + + + + +
-
-
-
- -
-
- {hasMaterialStep - ? "当前页已合并 SKU / SPU 选图、补充要求、槽位确认和生成参数,可在这里直接完成后开始生成。" - : "当前页已集中补充要求与生成参数,确认后即可直接开始生成。"} -
-
- - -
+
- ) : displayedResultUrl ? ( + ) : allGeneratedImages.length > 0 ? ( - 生成结果 setFullscreenPreview(displayedResultUrl)} - /> - +
+
+ 单击卡片高亮,双击放大预览 +
+ {selectedResultUrl ? ( + + ) : null} +
+ +
+ {sceneResultCards.map((card) => { + const isActive = card.url && displayedResultUrl === card.url; + + return ( +
{ + if (!card.url) { + return; + } + setSelectedResultUrl(card.url); + }} + onDoubleClick={() => { + if (!card.url) { + return; + } + setSelectedResultUrl(card.url); + setFullscreenPreview(card.url); + }} + onKeyDown={(event) => { + if (!card.url) { + return; + } + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + setSelectedResultUrl(card.url); + } + }} + className={cn( + "group overflow-hidden rounded-[22px] border text-left transition-all", + card.url ? "cursor-pointer" : "cursor-default", + isActive + ? "border-emerald-400/55 bg-emerald-500/[0.08] ring-1 ring-emerald-400/35" + : "border-white/10 bg-black/20 hover:border-white/18" + )} + > +
+ {card.url ? ( + <> + {`结果图 + +
+
+ 结果图 {card.index + 1} +
+
+ 双击可放大查看 +
+
+ + ) : ( +
+
+ +
+
+ 结果图 {card.index + 1} +
+
+ {isGenerating + ? "正在生成中,结果会按卡片顺序依次填充。" + : "开始生成后,这里会显示固定尺寸预览卡片。"} +
+
+ )} +
+
+ ); + })} +
) : ( )} - - {allGeneratedImages.length > 0 ? ( - -
- {allGeneratedImages.map((url) => ( - - ))} -
-
- ) : null} )}
diff --git a/components/agents/AgentWorkflowPanelDarkV2.tsx b/components/agents/AgentWorkflowPanelDarkV2.tsx index 91aacb4ac..2c3dcfb4f 100644 --- a/components/agents/AgentWorkflowPanelDarkV2.tsx +++ b/components/agents/AgentWorkflowPanelDarkV2.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, type ReactNode } from "react"; +import { useEffect, useMemo, useState, type ReactNode } from "react"; import { motion } from "framer-motion"; import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react"; import { toast } from "sonner"; @@ -17,6 +17,7 @@ import { getAgentWorkflowStoreOptionsByBrand, getEnabledAgentWorkflowOptionTree, getEnabledAgentWorkflowStepTree, + isAgentWorkflowStepSelectionComplete, serializeAgentWorkflowSelectionPath, } from "@/lib/agents/workflow-ui-config"; import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; @@ -56,6 +57,7 @@ interface AgentWorkflowPanelDarkV2Props { siteLogoDark?: string | null; showcaseConfig?: AgentWorkflowShowcaseConfig | null; topNavRightSlot?: ReactNode; + embedded?: boolean; } function buildSelectionSummary(params: { @@ -183,6 +185,7 @@ export function AgentWorkflowPanelDarkV2({ siteLogoDark, showcaseConfig, topNavRightSlot, + embedded = false, }: AgentWorkflowPanelDarkV2Props) { const brandOptions = useMemo( () => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [], @@ -231,6 +234,70 @@ export function AgentWorkflowPanelDarkV2({ () => getAgentWorkflowProgress({ config: resolvedConfig, selections }), [resolvedConfig, selections] ); + const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState(""); + const activeEmbeddedStepIndex = Math.max( + flattenedResolvedSteps.findIndex(({ step }) => step.key === activeEmbeddedStepKey), + 0 + ); + const activeEmbeddedStepEntry = + flattenedResolvedSteps[activeEmbeddedStepIndex] || flattenedResolvedSteps[0] || null; + + useEffect(() => { + if (!embedded) { + return; + } + + if (flattenedResolvedSteps.length === 0) { + if (activeEmbeddedStepKey) { + setActiveEmbeddedStepKey(""); + } + return; + } + + if ( + activeEmbeddedStepKey && + flattenedResolvedSteps.some(({ step }) => step.key === activeEmbeddedStepKey) + ) { + return; + } + + setActiveEmbeddedStepKey(flattenedResolvedSteps[0]?.step.key || ""); + }, [activeEmbeddedStepKey, embedded, flattenedResolvedSteps]); + + useEffect(() => { + if (!embedded || !activeEmbeddedStepEntry) { + return; + } + + const currentStepComplete = isAgentWorkflowStepSelectionComplete({ + step: activeEmbeddedStepEntry.step, + selectionValue: selections[activeEmbeddedStepEntry.step.key], + }); + if (!currentStepComplete) { + return; + } + + const nextStep = flattenedResolvedSteps + .slice(activeEmbeddedStepIndex + 1) + .find( + ({ step }) => + !isAgentWorkflowStepSelectionComplete({ + step, + selectionValue: selections[step.key], + }) + ); + + if (nextStep && nextStep.step.key !== activeEmbeddedStepKey) { + setActiveEmbeddedStepKey(nextStep.step.key); + } + }, [ + activeEmbeddedStepEntry, + activeEmbeddedStepIndex, + activeEmbeddedStepKey, + embedded, + flattenedResolvedSteps, + selections, + ]); const renderOptionLevels = ( step: AgentWorkflowUiConfig["steps"][number], @@ -307,41 +374,8 @@ export function AgentWorkflowPanelDarkV2({ } }; - return ( -
- - -
- {/* Hero Section */} -
-
- { - const target = document.getElementById("agent-workflow-content"); - if (!target) return; - target.scrollIntoView({ behavior: "smooth", block: "start" }); - }} - onSecondaryActionClick={onSkipStep} - /> -
-
- - {/* Main Content */} -
-
-
+ const workflowContent = ( +
{/* 基础配置 */} {/* 工作流步骤 */} - {flattenedResolvedSteps.map(({ step, depth }, stepIndex) => ( - 1 && "ml-3 border-l-4 border-l-emerald-500/30 sm:ml-6" - )} - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ delay: 0.15 + stepIndex * 0.05 }} - > -

- - {stepIndex + 2} - - {step.title} - {depth > 1 ? ( - - {depth === 2 ? "二级模块" : "三级模块"} + {embedded ? ( + activeEmbeddedStepEntry ? ( + 1 && + "border-l-4 border-l-emerald-500/30" + )} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + > +
+
+ {flattenedResolvedSteps.map(({ step }, stepIndex) => { + const completed = isAgentWorkflowStepSelectionComplete({ + step, + selectionValue: selections[step.key], + }); + const active = step.key === activeEmbeddedStepEntry.step.key; + + return ( + + ); + })} +
+ +
+
+ + {activeEmbeddedStepIndex + 2} + + {activeEmbeddedStepEntry.step.title} + {activeEmbeddedStepEntry.depth > 1 ? ( + + {activeEmbeddedStepEntry.depth === 2 ? "二级模块" : "三级模块"} + + ) : null} +
+ {activeEmbeddedStepEntry.step.description ? ( +

+ {activeEmbeddedStepEntry.step.description} +

+ ) : null} +
+
+ + {renderOptionLevels( + activeEmbeddedStepEntry.step, + getEnabledAgentWorkflowOptionTree(activeEmbeddedStepEntry.step.options || []) + )} +
+ ) : null + ) : ( + flattenedResolvedSteps.map(({ step, depth }, stepIndex) => ( + 1 && "ml-3 border-l-4 border-l-emerald-500/30 sm:ml-6" + )} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: 0.15 + stepIndex * 0.05 }} + > +

+ + {stepIndex + 2} - ) : null} -

- {step.description && ( -

{step.description}

- )} - {renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))} -
- ))} + {step.title} + {depth > 1 ? ( + + {depth === 2 ? "二级模块" : "三级模块"} + + ) : null} +

+ {step.description && ( +

{step.description}

+ )} + {renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))} +
+ )) + )} {/* 当前选择摘要 */} {summary.length > 0 && ( @@ -470,6 +585,47 @@ export function AgentWorkflowPanelDarkV2({ />
+ ); + + if (embedded) { + return workflowContent; + } + + return ( +
+ + +
+ {/* Hero Section */} +
+
+ { + const target = document.getElementById("agent-workflow-content"); + if (!target) return; + target.scrollIntoView({ behavior: "smooth", block: "start" }); + }} + onSecondaryActionClick={onSkipStep} + /> +
+
+ + {/* Main Content */} +
+
+ {workflowContent}
diff --git a/components/agents/AgentWorkflowShowcaseBanner.tsx b/components/agents/AgentWorkflowShowcaseBanner.tsx index 5df467541..bd188ff6d 100644 --- a/components/agents/AgentWorkflowShowcaseBanner.tsx +++ b/components/agents/AgentWorkflowShowcaseBanner.tsx @@ -44,7 +44,7 @@ export function AgentWorkflowShowcaseBanner({ animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }} > - +