"use client"; import { useMemo } from "react"; import { ArrowRight, Copy, RotateCcw, Sparkles } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { SearchableSelect } from "@/components/ui/searchable-select"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; import { defaultAgentWorkflowUiConfig, getAgentWorkflowCategoryLabels, getAgentWorkflowBrandStoreEntries, getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth, getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory, getAgentWorkflowPriceBandOptions, getAgentWorkflowThirdCategoryOptionsByBrandStore, getAgentWorkflowStoreOptionsByBrand, } from "@/lib/agents/workflow-ui-config"; import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config"; import { buildAgentWorkflowComposerContext, getAgentWorkflowProgress, } from "@/lib/agents/workflow-composer"; import type { AgentWorkflowComposerSelection, AgentWorkflowMaterialSelections, AgentWorkflowStepKey, AgentWorkflowUiConfig, } from "@/types/agent"; export interface AgentWorkflowInputs { primarySku: string; secondarySku: string; brand: string; store: string; thirdCategory: string; fourthCategory: string; fifthCategory: string; priceBand: string; } interface AgentWorkflowPanelProps { config?: AgentWorkflowUiConfig | null; inputs: AgentWorkflowInputs; onInputsChange: (next: AgentWorkflowInputs) => void; materials: AgentWorkflowMaterialSelections; onMaterialsChange: (next: AgentWorkflowMaterialSelections) => void; selections: Partial>; onSelectionsChange: (next: Partial>) => void; onReset?: () => void; embedded?: boolean; onNextStep?: () => void; onSkipStep?: () => void; } function buildSelectionSummary(params: { config: AgentWorkflowUiConfig; selections: Partial>; }) { const result: AgentWorkflowComposerSelection[] = []; for (const step of params.config.steps) { const selectedOptionKey = params.selections[step.key]; if (!selectedOptionKey) continue; const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled); if (!option) continue; result.push({ stepKey: step.key, stepTitle: step.title, optionKey: option.key, optionName: option.buttonName, promptFragment: option.promptFragment || "", }); } return result; } export function AgentWorkflowPanel({ config, inputs, onInputsChange, materials, onMaterialsChange, selections, onSelectionsChange, onReset, embedded = false, onNextStep, onSkipStep, }: AgentWorkflowPanelProps) { const categoryLabels = useMemo( () => getAgentWorkflowCategoryLabels(config), [config] ); const firstCategoryLabel = categoryLabels.firstCategory; const secondCategoryLabel = categoryLabels.secondCategory; const thirdCategoryLabel = categoryLabels.thirdCategory; const fourthCategoryLabel = categoryLabels.fourthCategory; const fifthCategoryLabel = categoryLabels.fifthCategory; const brandOptions = useMemo( () => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [], [config?.inputOptions?.brands] ); const brandStoreEntries = useMemo( () => getAgentWorkflowBrandStoreEntries(config), [config] ); const storeOptions = useMemo( () => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }), [config, inputs.brand] ); const thirdCategoryOptions = useMemo( () => getAgentWorkflowThirdCategoryOptionsByBrandStore({ config, brand: inputs.brand, store: inputs.store, }), [config, inputs.brand, inputs.store] ); const fourthCategoryOptions = useMemo( () => getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory({ config, brand: inputs.brand, store: inputs.store, thirdCategory: inputs.thirdCategory, }), [config, inputs.brand, inputs.store, inputs.thirdCategory] ); const fifthCategoryOptions = useMemo( () => getAgentWorkflowFifthCategoryOptionsByBrandStoreThirdFourth({ config, brand: inputs.brand, store: inputs.store, thirdCategory: inputs.thirdCategory, fourthCategory: inputs.fourthCategory, }), [config, inputs.brand, inputs.store, inputs.thirdCategory, inputs.fourthCategory] ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), [config] ); const resolvedConfig = useMemo(() => { const source = config || defaultAgentWorkflowUiConfig; return { steps: [...source.steps] .filter((step) => step.enabled) .map((step) => ({ ...step, options: [...step.options] .filter((option) => option.enabled) .sort((left, right) => left.sortOrder - right.sortOrder), })) .filter((step) => step.options.length > 0) .sort((left, right) => left.sortOrder - right.sortOrder), }; }, [config]); const summary = useMemo( () => buildSelectionSummary({ config: resolvedConfig, selections }), [resolvedConfig, selections] ); const composerContext = useMemo( () => buildAgentWorkflowComposerContext({ config: resolvedConfig, inputs, materials, selections, }), [inputs, materials, resolvedConfig, selections] ); const progress = useMemo( () => getAgentWorkflowProgress({ config: resolvedConfig, selections }), [resolvedConfig, selections] ); const handleCopyPrompt = async () => { if (!composerContext?.promptText) { toast.error("当前还没有可复制的 Prompt"); return; } try { await navigator.clipboard.writeText(composerContext.promptText); toast.success("Prompt 已复制"); } catch { toast.error("复制失败,请手动复制"); } }; return (
风格化工作流
先完成工作流设定,下一步再进入生图模式执行。
已完成 {progress.completed}/{progress.total} 个环节
基础配置
onInputsChange({ ...inputs, brand: value, store: value.trim().toLowerCase() === inputs.brand.trim().toLowerCase() ? inputs.store : "", thirdCategory: value.trim().toLowerCase() === inputs.brand.trim().toLowerCase() ? inputs.thirdCategory : "", fourthCategory: "", fifthCategory: "", }) } /> 0 && storeOptions.length === 0)} onValueChange={(value) => onInputsChange({ ...inputs, store: value, thirdCategory: value.trim().toLowerCase() === inputs.store.trim().toLowerCase() ? inputs.thirdCategory : "", fourthCategory: "", fifthCategory: "", }) } /> onInputsChange({ ...inputs, thirdCategory: value, fourthCategory: value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() ? inputs.fourthCategory : "", fifthCategory: "", }) } /> onInputsChange({ ...inputs, fourthCategory: value, fifthCategory: value.trim().toLowerCase() === inputs.fourthCategory.trim().toLowerCase() ? inputs.fifthCategory : "", }) } /> onInputsChange({ ...inputs, fifthCategory: value })} /> onInputsChange({ ...inputs, priceBand: value })} />
{(brandOptions.length > 0 || storeOptions.length > 0) && (
{firstCategoryLabel}和{secondCategoryLabel}支持输入关键字匹配下拉选择,候选列表由系统设置配置。
)}
SKU / SPU 搜图已移动到下一步“生图模式”中,选中的图片会直接进入对应生图槽位。
{resolvedConfig.steps.length > 0 ? ( resolvedConfig.steps.map((step) => (
{step.title}
{step.description ? (
{step.description}
) : null}
{step.options.map((option) => { const selected = selections[step.key] === option.key; return ( ); })}
)) ) : (
当前还没有启用的工作流步骤,请先到系统设置新增步骤或启用现有步骤。
)}
当前工作流摘要
{inputs.primarySku || inputs.secondarySku || inputs.brand || inputs.store || inputs.thirdCategory || inputs.fourthCategory || inputs.fifthCategory || inputs.priceBand || materials.primary.selectedImageIds.length > 0 || materials.secondary.selectedImageIds.length > 0 ? (
{inputs.primarySku ? ( 主SKU:{inputs.primarySku} ) : null} {inputs.secondarySku ? ( 搭配SKU:{inputs.secondarySku} ) : null} {inputs.brand ? ( {firstCategoryLabel}:{inputs.brand} ) : null} {inputs.store ? ( {secondCategoryLabel}:{inputs.store} ) : null} {inputs.thirdCategory ? ( {thirdCategoryLabel}:{inputs.thirdCategory} ) : null} {inputs.fourthCategory ? ( {fourthCategoryLabel}:{inputs.fourthCategory} ) : null} {inputs.fifthCategory ? ( {fifthCategoryLabel}:{inputs.fifthCategory} ) : null} {inputs.priceBand ? ( 价格带:{inputs.priceBand} ) : null} {materials.primary.selectedImageIds.length > 0 ? ( 主SPU图:已选 {materials.primary.selectedImageIds.length} 张 ) : null} {materials.secondary.selectedImageIds.length > 0 ? ( 搭配SKU图:已选 {materials.secondary.selectedImageIds.length} 张 ) : null}
) : null} {summary.length > 0 ? (
{summary.map((item) => (
{item.stepTitle} {item.optionName}
))}
) : (
还没有选择工作流方案,先从上面的卡片开始。
)} {composerContext?.referenceImages.length ? (
已选素材图
{composerContext.referenceImages.map((image) => ( {image.sourceLabel} · {image.label} ))}
) : null}
Prompt 预览
右侧发送时,会把下面这份工作流设定作为隐藏上下文一起注入。