"use client"; import { memo, startTransition, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, type ReactNode, type SyntheticEvent, } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { motion } from "framer-motion"; import { ArrowRight, Check, Sparkles, ChevronDown, ChevronRight, ChevronUp, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { SearchableSelect } from "@/components/ui/searchable-select"; import { cn } from "@/lib/utils"; import { getAgentWorkflowCategoryLabels, defaultAgentWorkflowUiConfig, flattenAgentWorkflowSteps, flattenVisibleAgentWorkflowSteps, getAgentWorkflowBlockedOptionKeysForSelections, getAgentWorkflowBrandStoreEntries, getAgentWorkflowPageFeatureSettings, getAgentWorkflowOfficialSecondCategoryOptions, getAgentWorkflowOfficialThirdCategoryOptions, getAgentWorkflowFourthCategoryOptionsByBrandStoreThirdCategory, getAgentWorkflowPriceBandOptions, getAgentWorkflowThirdCategoryOptionsByBrandStore, getAgentWorkflowSelectedOptionPath, getAgentWorkflowStoreOptionsByBrand, getEnabledAgentWorkflowOptionTree, getEnabledAgentWorkflowStepTree, getRenderableAgentWorkflowOptionTree, getRenderableAgentWorkflowStepTree, filterAgentWorkflowStepTreeByOfficialCategory, isAgentWorkflowStepSelectionComplete, serializeAgentWorkflowSelectionPath, } from "@/lib/agents/workflow-ui-config"; import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark"; import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner"; import { getAgentWorkflowProgress } from "@/lib/agents/workflow-composer"; import { buildImageProxyUrl } from "@/lib/image-delivery"; import type { AgentWorkflowMaterialSelections, AgentWorkflowShowcaseConfig, AgentWorkflowStepKey, AgentWorkflowUiConfig, } from "@/types/agent"; export interface AgentWorkflowInputs { primarySku: string; secondarySku: string; brand: string; store: string; officialSecondCategory: string; officialThirdCategory: string; thirdCategory: string; fourthCategory: string; fifthCategory: string; priceBand: string; } interface AgentWorkflowPanelDarkV2Props { config?: AgentWorkflowUiConfig | null; inputs: AgentWorkflowInputs; onInputsChange: (next: AgentWorkflowInputs) => void; materials: AgentWorkflowMaterialSelections; onMaterialsChange: (next: AgentWorkflowMaterialSelections) => void; selections: Partial>; onSelectionsChange: ( next: Partial> ) => void; onReset?: () => void; onNextStep?: () => void; onSkipStep?: () => void; siteName?: string | null; siteLogo?: string | null; siteLogoDark?: string | null; showcaseConfig?: AgentWorkflowShowcaseConfig | null; topNavRightSlot?: ReactNode; embedded?: boolean; showBrandStoreSection?: boolean; } interface AgentWorkflowBrandStorePanelProps { config?: AgentWorkflowUiConfig | null; inputs: AgentWorkflowInputs; onInputsChange: (next: AgentWorkflowInputs) => void; theme?: "light" | "dark"; title?: string; description?: string; } type WorkflowSelectionState = Partial>; type WorkflowStep = AgentWorkflowUiConfig["steps"][number]; type WorkflowOption = WorkflowStep["options"][number]; const VIRTUAL_OPTION_GRID_GAP = 16; const VIRTUAL_OPTION_GRID_MAX_HEIGHT = 640; const VIRTUAL_OPTION_GRID_MIN_CARD_HEIGHT = 122; const VIRTUAL_OPTION_GRID_THRESHOLD = 12; const WORKFLOW_OPTION_PREVIEW_WIDTH = 230; const WORKFLOW_OPTION_PREVIEW_QUALITY = 56; const WORKFLOW_OPTION_PREVIEW_EAGER_COUNT = 2; const WORKFLOW_OPTION_NEXT_PREVIEW_PREFETCH_COUNT = 3; const WORKFLOW_OPTION_PREFETCH_CACHE_LIMIT = 30; const WORKFLOW_OPTION_PREFETCH_BATCH_SIZE = 1; function isLikelyStoredWorkflowPreview(url: string, sourceUrl?: string | null) { const source = String(sourceUrl || "").trim(); if (source && source !== url) { return true; } if (source && source === url && /\.webp(?:[?#]|$)/i.test(url)) { return true; } return /[?&]x-oss-process=/i.test(url); } function buildWorkflowOptionPreviewUrl(url?: string | null, sourceUrl?: string | null) { const source = String(url || "").trim(); if (!source) return ""; if (isLikelyStoredWorkflowPreview(source, sourceUrl)) { return source; } try { const target = new URL(source); const hostname = target.hostname.toLowerCase(); if ( hostname === "s3.jiansun.vip" || hostname.endsWith(".jiansun.vip") || hostname === "s3.1080p.hk" ) { return source; } } catch { return source; } return buildImageProxyUrl(source, { width: WORKFLOW_OPTION_PREVIEW_WIDTH, quality: WORKFLOW_OPTION_PREVIEW_QUALITY, format: "webp", delivery: "proxy", }); } function handleWorkflowOptionPreviewError( event: SyntheticEvent, originalUrl?: string | null ): boolean { const fallbackUrl = String(originalUrl || "").trim(); if (!fallbackUrl) return false; const image = event.currentTarget; if (image.dataset.workflowPreviewFallbackApplied === "1") { return false; } image.dataset.workflowPreviewFallbackApplied = "1"; image.src = fallbackUrl; return true; } function appendWorkflowOptionPreviewUrls( output: Set, options: WorkflowStep["options"] | undefined, limit: number ) { for (const option of (options || []).slice(0, limit)) { const previewUrl = buildWorkflowOptionPreviewUrl( option.previewImageUrl, option.previewImageSourceUrl ); if (previewUrl) { output.add(previewUrl); } } } function trimWorkflowPreviewImageCache( cache: Map, limit = WORKFLOW_OPTION_PREFETCH_CACHE_LIMIT ) { while (cache.size > limit) { const firstKey = cache.keys().next().value; if (!firstKey) return; const image = cache.get(firstKey); if (image) { image.src = ""; } cache.delete(firstKey); } } function shouldSkipWorkflowPreviewPrefetch() { if (typeof document !== "undefined" && document.visibilityState === "hidden") { return true; } const connection = (typeof navigator !== "undefined" ? (navigator as Navigator & { connection?: { saveData?: boolean }; }).connection : null); return Boolean(connection?.saveData); } function preloadWorkflowOptionPreviewImages(params: { urls: string[]; cache: Map; markPrefetched: Set; }) { if (typeof window === "undefined" || params.urls.length === 0) { return () => {}; } let cancelled = false; let cleanupIdleTask: (() => void) | null = null; const urls = params.urls.slice(); const preloadNextBatch = () => { if (cancelled || shouldSkipWorkflowPreviewPrefetch()) { return; } const batch = urls.splice(0, WORKFLOW_OPTION_PREFETCH_BATCH_SIZE); for (const src of batch) { if (cancelled || params.cache.has(src)) { continue; } const image = new Image(); image.decoding = "async"; ( image as HTMLImageElement & { fetchPriority?: "high" | "low" | "auto"; } ).fetchPriority = "low"; image.onerror = () => { params.cache.delete(src); params.markPrefetched.delete(src); }; image.src = src; params.cache.set(src, image); } trimWorkflowPreviewImageCache(params.cache); if (urls.length > 0) { cleanupIdleTask = scheduleWorkflowIdleTask(preloadNextBatch, 900); } }; cleanupIdleTask = scheduleWorkflowIdleTask(preloadNextBatch, 700); return () => { cancelled = true; cleanupIdleTask?.(); }; } interface WorkflowPanelDerivedState { progress: ReturnType; } function scheduleWorkflowIdleTask(callback: () => void, timeout = 450) { if (typeof window === "undefined") { callback(); return () => {}; } const idleWindow = window as Window & { requestIdleCallback?: ( callback: IdleRequestCallback, options?: IdleRequestOptions ) => number; cancelIdleCallback?: (handle: number) => void; }; if (typeof idleWindow.requestIdleCallback === "function") { const idleId = idleWindow.requestIdleCallback(callback, { timeout }); return () => idleWindow.cancelIdleCallback?.(idleId); } const timer = window.setTimeout(callback, Math.min(timeout, 120)); return () => window.clearTimeout(timer); } function buildWorkflowPanelDerivedState(params: { config: AgentWorkflowUiConfig; selections: WorkflowSelectionState; }): WorkflowPanelDerivedState { return { progress: getAgentWorkflowProgress({ config: params.config, selections: params.selections, }), }; } function areWorkflowSelectionStatesEqual( left?: WorkflowSelectionState | null, right?: WorkflowSelectionState | null ) { const leftEntries = Object.entries(left || {}).filter(([, value]) => Boolean(String(value || "").trim()) ); const rightEntries = Object.entries(right || {}).filter(([, value]) => Boolean(String(value || "").trim()) ); if (leftEntries.length !== rightEntries.length) { return false; } return leftEntries.every( ([key, value]) => String(right?.[key as AgentWorkflowStepKey] || "") === String(value || "") ); } function getWorkflowOptionGridColumnCount(width: number) { if (width >= 1280) return 5; if (width >= 1024) return 4; if (width >= 640) return 3; if (width >= 420) return 2; return 1; } function getWorkflowStepSelectionSummary(params: { step: WorkflowStep; selectionValue?: string; }) { const selectedOptions = getAgentWorkflowSelectedOptionPath({ step: params.step, selectionValue: params.selectionValue, }); if (selectedOptions.length === 0) { return ""; } return selectedOptions.map((option) => option.buttonName).join(" / "); } function getWorkflowStepOptionLevels(params: { step: WorkflowStep; selectionValue?: string; }) { const levels: Array<{ depth: number; options: WorkflowStep["options"]; selectedKey: string; selectedOption: WorkflowOption | null; }> = []; const selectedPath = getAgentWorkflowSelectedOptionPath({ step: params.step, selectionValue: params.selectionValue, }); let depth = 1; let currentOptions = params.step.options || []; while (currentOptions.length > 0) { const selectedKey = selectedPath[depth - 1]?.key || ""; const selectedOption = currentOptions.find((option) => option.key === selectedKey) || null; levels.push({ depth, options: currentOptions, selectedKey, selectedOption, }); const nextOptions = selectedOption?.children || []; if (!selectedOption || nextOptions.length === 0) { break; } currentOptions = nextOptions; depth += 1; } return levels; } function getWorkflowOptionDepthLabel(depth: number) { return depth === 1 ? "主卡片" : `${depth}级子卡片`; } function getWorkflowOptionDepthShortLabel(depth: number) { return depth === 1 ? "主" : `${depth}级`; } const StepOptionCard = memo(function StepOptionCard({ option, step, selected, disabled = false, disabledReason = null, onSelect, index, }: { option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null; previewImageSourceUrl?: string | null; }; step: { title: string }; selected: boolean; disabled?: boolean; disabledReason?: "blocked" | "developing" | null; onSelect: (optionKey: string) => void; index: number; }) { const handleClick = useCallback(() => { onSelect(option.key); }, [onSelect, option.key]); const previewUrl = useMemo( () => buildWorkflowOptionPreviewUrl(option.previewImageUrl, option.previewImageSourceUrl), [option.previewImageSourceUrl, option.previewImageUrl] ); const [previewFailed, setPreviewFailed] = useState(false); useEffect(() => { setPreviewFailed(false); }, [previewUrl]); const shouldLoadFast = selected || index < WORKFLOW_OPTION_PREVIEW_EAGER_COUNT; const isAutoConfiguredOption = /根据风格已配置/.test(option.buttonName); const hasPreview = Boolean(previewUrl && !previewFailed && !isAutoConfiguredOption); return ( ); }); interface VirtualizedOptionGridProps { options: WorkflowStep["options"]; step: WorkflowStep; selectedKey: string; blockedOptionKeys: Set; onSelect: (optionKey: string) => void; } const VirtualizedOptionGrid = memo(function VirtualizedOptionGrid({ options, step, selectedKey, blockedOptionKeys, onSelect, }: VirtualizedOptionGridProps) { const scrollParentRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const fallbackColumnCount = 5; const columnCount = containerWidth > 0 ? getWorkflowOptionGridColumnCount(containerWidth) : fallbackColumnCount; const rowCount = Math.ceil(options.length / columnCount); const cardWidth = containerWidth > 0 ? (containerWidth - VIRTUAL_OPTION_GRID_GAP * (columnCount - 1)) / columnCount : 220; const cardHeight = Math.max( VIRTUAL_OPTION_GRID_MIN_CARD_HEIGHT, Math.round(cardWidth * 0.625) ); const rowHeight = cardHeight + VIRTUAL_OPTION_GRID_GAP; const shouldVirtualize = options.length > VIRTUAL_OPTION_GRID_THRESHOLD; useEffect(() => { const element = scrollParentRef.current; if (!element || typeof ResizeObserver === "undefined") { return; } const updateWidth = () => { setContainerWidth(Math.max(0, Math.floor(element.clientWidth))); }; updateWidth(); const resizeObserver = new ResizeObserver(updateWidth); resizeObserver.observe(element); return () => resizeObserver.disconnect(); }, []); const rowVirtualizer = useVirtualizer({ count: rowCount, getScrollElement: () => scrollParentRef.current, estimateSize: () => rowHeight, overscan: 1, }); const renderCard = (option: WorkflowOption, optionIndex: number) => { const isDeveloping = option.enabled === false; const isBlocked = !isDeveloping && selectedKey !== option.key && blockedOptionKeys.has(option.key); const isDisabled = isDeveloping || isBlocked; return ( ); }; if (!shouldVirtualize) { return (
{options.map((option, optionIndex) => renderCard(option, optionIndex))}
); } return (
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const startIndex = virtualRow.index * columnCount; const rowOptions = options.slice( startIndex, startIndex + columnCount ); return (
{rowOptions.map((option, rowOptionIndex) => renderCard(option, startIndex + rowOptionIndex) )}
); })}
); }); export function AgentWorkflowBrandStorePanel({ config, inputs, onInputsChange, theme = "dark", title = "品牌层级选择", description = "先选择品牌、店铺、分类层级和价格带,后续品牌模特与风格步骤会按这里的选择联动。", }: AgentWorkflowBrandStorePanelProps) { const categoryLabels = useMemo( () => getAgentWorkflowCategoryLabels(config), [config] ); const firstCategoryLabel = categoryLabels.firstCategory; const secondCategoryLabel = categoryLabels.secondCategory; const thirdCategoryLabel = categoryLabels.thirdCategory; const fourthCategoryLabel = categoryLabels.fourthCategory; const resolvedTitle = title === "品牌层级选择" ? `${firstCategoryLabel}层级选择` : title; const resolvedDescription = description === "先选择品牌、店铺、分类层级和价格带,后续品牌模特与风格步骤会按这里的选择联动。" ? `先选择${firstCategoryLabel}、${secondCategoryLabel}、分类层级和价格带,后续品牌模特与风格步骤会按这里的选择联动。` : description; 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 officialSecondCategoryOptions = useMemo( () => getAgentWorkflowOfficialSecondCategoryOptions(config), [config] ); const officialThirdCategoryOptions = useMemo( () => getAgentWorkflowOfficialThirdCategoryOptions({ config, secondCategory: inputs.officialSecondCategory, }), [config, inputs.officialSecondCategory] ); const priceBandOptions = useMemo( () => getAgentWorkflowPriceBandOptions(config), [config] ); const hasThirdCategoryLevel = useMemo( () => brandStoreEntries.some((entry) => (entry.storeEntries || []).some( (storeEntry) => (storeEntry.thirdCategoryEntries || []).length > 0 || (storeEntry.thirdCategories || []).length > 0 ) ) || Boolean(config?.inputOptions?.thirdCategories?.length), [brandStoreEntries, config?.inputOptions?.thirdCategories] ); const hasFourthCategoryLevel = useMemo( () => hasThirdCategoryLevel && (brandStoreEntries.some((entry) => (entry.storeEntries || []).some((storeEntry) => (storeEntry.thirdCategoryEntries || []).some( (thirdEntry) => (thirdEntry.fourthCategoryEntries || []).length > 0 || (thirdEntry.fourthCategories || []).length > 0 ) ) ) || Boolean(config?.inputOptions?.fourthCategories?.length)), [ brandStoreEntries, config?.inputOptions?.fourthCategories, hasThirdCategoryLevel, ] ); const isDark = theme === "dark"; useEffect(() => { let nextInputs = inputs; let changed = false; if (!hasThirdCategoryLevel) { if (nextInputs.thirdCategory || nextInputs.fourthCategory) { nextInputs = { ...nextInputs, thirdCategory: "", fourthCategory: "", }; changed = true; } } else if (!hasFourthCategoryLevel) { if (nextInputs.fourthCategory) { nextInputs = { ...nextInputs, fourthCategory: "", }; changed = true; } } if ( nextInputs.officialSecondCategory && !officialSecondCategoryOptions.some( (option) => option.trim().toLowerCase() === nextInputs.officialSecondCategory.trim().toLowerCase() ) ) { nextInputs = { ...nextInputs, officialSecondCategory: "", officialThirdCategory: "", fifthCategory: "", }; changed = true; } if ( nextInputs.officialThirdCategory && !officialThirdCategoryOptions.some( (option) => option.trim().toLowerCase() === nextInputs.officialThirdCategory.trim().toLowerCase() ) ) { nextInputs = { ...nextInputs, officialThirdCategory: "", fifthCategory: "", }; changed = true; } else if ( nextInputs.officialThirdCategory && nextInputs.fifthCategory !== nextInputs.officialThirdCategory ) { nextInputs = { ...nextInputs, fifthCategory: nextInputs.officialThirdCategory, }; changed = true; } if (changed) { onInputsChange(nextInputs); } }, [ hasThirdCategoryLevel, hasFourthCategoryLevel, inputs, onInputsChange, officialSecondCategoryOptions, officialThirdCategoryOptions, ]); return (
{resolvedTitle}
{resolvedDescription}
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: inputs.officialThirdCategory || "", }) } /> 0 && storeOptions.length === 0) } onValueChange={(value) => onInputsChange({ ...inputs, store: value, thirdCategory: value.trim().toLowerCase() === inputs.store.trim().toLowerCase() ? inputs.thirdCategory : "", fourthCategory: "", }) } /> {hasThirdCategoryLevel ? ( onInputsChange({ ...inputs, thirdCategory: value, fourthCategory: value.trim().toLowerCase() === inputs.thirdCategory.trim().toLowerCase() ? inputs.fourthCategory : "", }) } /> ) : null} {hasFourthCategoryLevel ? ( onInputsChange({ ...inputs, fourthCategory: value, }) } /> ) : null} onInputsChange({ ...inputs, officialSecondCategory: value, officialThirdCategory: "", fifthCategory: "", }) } /> onInputsChange({ ...inputs, officialThirdCategory: value, fifthCategory: value, }) } /> onInputsChange({ ...inputs, priceBand: value }) } />
); } function AgentWorkflowPanelDarkV2Component({ config, inputs, onInputsChange, selections, onSelectionsChange, onNextStep, onSkipStep, siteName, siteLogo, siteLogoDark, showcaseConfig, topNavRightSlot, embedded = false, showBrandStoreSection = true, }: AgentWorkflowPanelDarkV2Props) { const categoryLabels = useMemo( () => getAgentWorkflowCategoryLabels(config), [config] ); const firstCategoryLabel = categoryLabels.firstCategory; const secondCategoryLabel = categoryLabels.secondCategory; const resolvedConfig = useMemo(() => { const source = config || defaultAgentWorkflowUiConfig; const filteredSteps = filterAgentWorkflowStepTreeByOfficialCategory({ steps: source.steps || [], officialSecondCategory: inputs.officialSecondCategory, officialThirdCategory: inputs.officialThirdCategory, }); return { ...source, steps: getEnabledAgentWorkflowStepTree(filteredSteps), }; }, [config, inputs.officialSecondCategory, inputs.officialThirdCategory]); const displayConfig = useMemo(() => { const source = config || defaultAgentWorkflowUiConfig; const filteredSteps = filterAgentWorkflowStepTreeByOfficialCategory({ steps: source.steps || [], officialSecondCategory: inputs.officialSecondCategory, officialThirdCategory: inputs.officialThirdCategory, }); return { ...source, steps: getRenderableAgentWorkflowStepTree(filteredSteps), }; }, [config, inputs.officialSecondCategory, inputs.officialThirdCategory]); const pageFeatureSettings = useMemo( () => getAgentWorkflowPageFeatureSettings(config), [config] ); const [optimisticSelections, setOptimisticSelections] = useState(selections); const deferredSelections = useDeferredValue(optimisticSelections); const latestExternalSelectionsRef = useRef(selections); const selectionCommitTimerRef = useRef(null); const pendingSelectionCommitRef = useRef(null); const workflowPreviewPrefetchedUrlsRef = useRef(new Set()); const workflowPreviewImageCacheRef = useRef( new Map() ); useEffect(() => { if (latestExternalSelectionsRef.current === selections) { return; } latestExternalSelectionsRef.current = selections; setOptimisticSelections((current) => areWorkflowSelectionStatesEqual(current, selections) ? current : selections ); }, [selections]); const flushSelectionCommit = useCallback(() => { const pendingSelections = pendingSelectionCommitRef.current; if (!pendingSelections) { return; } pendingSelectionCommitRef.current = null; if ( selectionCommitTimerRef.current !== null && typeof window !== "undefined" ) { window.clearTimeout(selectionCommitTimerRef.current); } selectionCommitTimerRef.current = null; startTransition(() => { onSelectionsChange(pendingSelections); }); }, [onSelectionsChange]); const emitSelectionsChange = useCallback( (nextSelections: WorkflowSelectionState) => { setOptimisticSelections(nextSelections); pendingSelectionCommitRef.current = nextSelections; if (typeof window === "undefined") { flushSelectionCommit(); return; } if (selectionCommitTimerRef.current !== null) { window.clearTimeout(selectionCommitTimerRef.current); } selectionCommitTimerRef.current = window.setTimeout(() => { flushSelectionCommit(); }, 90); }, [flushSelectionCommit] ); useEffect(() => { return () => { flushSelectionCommit(); }; }, [flushSelectionCommit]); const flattenedResolvedSteps = useMemo( () => flattenVisibleAgentWorkflowSteps( displayConfig.steps, optimisticSelections ).filter( ({ step }) => Array.isArray(step.options) && step.options.length > 0 ), [displayConfig.steps, optimisticSelections] ); const [workflowDerivedState, setWorkflowDerivedState] = useState(() => buildWorkflowPanelDerivedState({ config: resolvedConfig, selections: deferredSelections, }) ); const workflowDerivedVersionRef = useRef(0); useEffect(() => { const version = workflowDerivedVersionRef.current + 1; workflowDerivedVersionRef.current = version; return scheduleWorkflowIdleTask(() => { const nextDerivedState = buildWorkflowPanelDerivedState({ config: resolvedConfig, selections: deferredSelections, }); if (workflowDerivedVersionRef.current !== version) { return; } startTransition(() => { setWorkflowDerivedState(nextDerivedState); }); }); }, [deferredSelections, resolvedConfig]); const progress = workflowDerivedState.progress; const stepSelectionSummaryMap = useMemo(() => { const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map( ({ step }) => [ step.key, getWorkflowStepSelectionSummary({ step, selectionValue: deferredSelections[step.key], }), ] ); return Object.fromEntries(entries) as Record; }, [deferredSelections, resolvedConfig.steps]); const blockedOptionKeys = useMemo( () => new Set( getAgentWorkflowBlockedOptionKeysForSelections({ config: resolvedConfig, selections: deferredSelections, }) ), [deferredSelections, resolvedConfig] ); const [autoAdvanceNotice, setAutoAdvanceNotice] = useState<{ completedStepTitle: string; completedSummary: string; nextStepTitle: string; } | null>(null); const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState(""); const [activeEmbeddedOptionDepth, setActiveEmbeddedOptionDepth] = useState(1); const [isEmbeddedPathExpanded, setIsEmbeddedPathExpanded] = useState(false); const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState< string | null >(null); const activeEmbeddedStepIndex = Math.max( flattenedResolvedSteps.findIndex( ({ step }) => step.key === activeEmbeddedStepKey ), 0 ); const activeEmbeddedStepEntry = flattenedResolvedSteps[activeEmbeddedStepIndex] || flattenedResolvedSteps[0] || null; const activeEmbeddedOptionLevels = useMemo( () => activeEmbeddedStepEntry ? getWorkflowStepOptionLevels({ step: activeEmbeddedStepEntry.step, selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key], }) : [], [activeEmbeddedStepEntry, optimisticSelections] ); const activeEmbeddedSelectedPath = useMemo( () => activeEmbeddedStepEntry ? getAgentWorkflowSelectedOptionPath({ step: activeEmbeddedStepEntry.step, selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key], }) : [], [activeEmbeddedStepEntry, optimisticSelections] ); const activeEmbeddedOptionLevel = activeEmbeddedOptionLevels.find( (level) => level.depth === activeEmbeddedOptionDepth ) || activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] || activeEmbeddedOptionLevels[0] || null; const workflowOptionPreviewPrefetchUrls = useMemo(() => { const urls = new Set(); if (embedded) { const currentSelectedOption = activeEmbeddedOptionLevel?.selectedOption || null; appendWorkflowOptionPreviewUrls( urls, currentSelectedOption?.children, WORKFLOW_OPTION_NEXT_PREVIEW_PREFETCH_COUNT ); if (activeEmbeddedStepEntry) { const currentStepComplete = isAgentWorkflowStepSelectionComplete({ step: activeEmbeddedStepEntry.step, selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key], }); if (currentStepComplete) { const nextStepEntry = flattenedResolvedSteps .slice(activeEmbeddedStepIndex + 1) .find( ({ step }) => !isAgentWorkflowStepSelectionComplete({ step, selectionValue: optimisticSelections[step.key], }) ); if (nextStepEntry) { const nextStepLevels = getWorkflowStepOptionLevels({ step: nextStepEntry.step, selectionValue: optimisticSelections[nextStepEntry.step.key], }); const nextStepFirstPendingLevel = nextStepLevels.find((level) => !level.selectedKey) || nextStepLevels[nextStepLevels.length - 1] || null; appendWorkflowOptionPreviewUrls( urls, nextStepFirstPendingLevel?.options || nextStepEntry.step.options, WORKFLOW_OPTION_NEXT_PREVIEW_PREFETCH_COUNT ); } } } } else { appendWorkflowOptionPreviewUrls( urls, flattenedResolvedSteps[1]?.step.options, WORKFLOW_OPTION_NEXT_PREVIEW_PREFETCH_COUNT ); } return Array.from(urls); }, [ activeEmbeddedOptionLevel, activeEmbeddedStepEntry, activeEmbeddedStepIndex, embedded, flattenedResolvedSteps, optimisticSelections, ]); const activeEmbeddedDeepestSelectedOption = useMemo( () => [...activeEmbeddedOptionLevels] .reverse() .find((level) => level.selectedOption)?.selectedOption || null, [activeEmbeddedOptionLevels] ); const activeEmbeddedSelectedOptionHasDevelopingChildren = useMemo(() => { if (!activeEmbeddedDeepestSelectedOption) { return false; } const renderableChildren = activeEmbeddedDeepestSelectedOption.children || []; if (renderableChildren.length === 0) { return false; } return ( getEnabledAgentWorkflowOptionTree( activeEmbeddedDeepestSelectedOption.children || [] ).length === 0 ); }, [activeEmbeddedDeepestSelectedOption]); const activeEmbeddedPrimaryOption = activeEmbeddedSelectedPath[0] || null; const activeEmbeddedStepSummary = (activeEmbeddedStepEntry && stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key]) || ""; const activeEmbeddedPathStatusLabel = activeEmbeddedSelectedPath.length > 0 ? `已选 ${activeEmbeddedSelectedPath.length} 项` : "未选择"; const activeEmbeddedPathSummary = activeEmbeddedStepSummary || "先选择当前步骤的主卡片,完整路径会收纳在这里"; 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) { return; } if (!activeEmbeddedStepEntry) { if (activeEmbeddedOptionDepth !== 1) { setActiveEmbeddedOptionDepth(1); } return; } const firstIncompleteDepth = activeEmbeddedOptionLevels.find((level) => !level.selectedKey)?.depth || activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] ?.depth || 1; setActiveEmbeddedOptionDepth((current) => { const maxDepth = activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] ?.depth || 1; if (current < 1 || current > maxDepth) { return firstIncompleteDepth; } return current; }); }, [ activeEmbeddedOptionDepth, activeEmbeddedOptionLevels, activeEmbeddedStepEntry, embedded, ]); useEffect(() => { if ( typeof window === "undefined" || workflowOptionPreviewPrefetchUrls.length === 0 ) { return; } const urlsToPrefetch = workflowOptionPreviewPrefetchUrls.filter( (src) => !workflowPreviewPrefetchedUrlsRef.current.has(src) ); if (urlsToPrefetch.length === 0) { return; } urlsToPrefetch.forEach((src) => { workflowPreviewPrefetchedUrlsRef.current.add(src); }); return preloadWorkflowOptionPreviewImages({ urls: urlsToPrefetch, cache: workflowPreviewImageCacheRef.current, markPrefetched: workflowPreviewPrefetchedUrlsRef.current, }); }, [workflowOptionPreviewPrefetchUrls]); useEffect(() => { return () => { workflowPreviewImageCacheRef.current.forEach((image) => { image.src = ""; }); workflowPreviewImageCacheRef.current.clear(); workflowPreviewPrefetchedUrlsRef.current.clear(); }; }, []); useEffect(() => { if (!embedded || !activeEmbeddedStepEntry) { return; } if (pendingAutoAdvanceStepKey !== activeEmbeddedStepEntry.step.key) { return; } const currentStepComplete = isAgentWorkflowStepSelectionComplete({ step: activeEmbeddedStepEntry.step, selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key], }); if (!currentStepComplete) { return; } if (activeEmbeddedSelectedOptionHasDevelopingChildren) { setPendingAutoAdvanceStepKey(null); return; } const nextStep = flattenedResolvedSteps .slice(activeEmbeddedStepIndex + 1) .find( ({ step }) => !isAgentWorkflowStepSelectionComplete({ step, selectionValue: optimisticSelections[step.key], }) ); if (nextStep && nextStep.step.key !== activeEmbeddedStepKey) { setAutoAdvanceNotice({ completedStepTitle: activeEmbeddedStepEntry.step.title, completedSummary: stepSelectionSummaryMap[activeEmbeddedStepEntry.step.key] || "已完成当前步骤选择", nextStepTitle: nextStep.step.title, }); setActiveEmbeddedStepKey(nextStep.step.key); setActiveEmbeddedOptionDepth(1); } setPendingAutoAdvanceStepKey(null); }, [ activeEmbeddedStepEntry, activeEmbeddedStepIndex, activeEmbeddedStepKey, activeEmbeddedSelectedOptionHasDevelopingChildren, embedded, flattenedResolvedSteps, pendingAutoAdvanceStepKey, optimisticSelections, stepSelectionSummaryMap, ]); useEffect(() => { if (!autoAdvanceNotice) { return; } const timer = window.setTimeout(() => { setAutoAdvanceNotice(null); }, 3200); return () => window.clearTimeout(timer); }, [autoAdvanceNotice]); useEffect(() => { if (!embedded) { return; } setIsEmbeddedPathExpanded(false); }, [activeEmbeddedStepKey, embedded]); const renderOptionLevels = ( step: AgentWorkflowUiConfig["steps"][number], options: AgentWorkflowUiConfig["steps"][number]["options"], depth = 1, parentPath: string[] = [] ): ReactNode => { const selectedPathKeys = getAgentWorkflowSelectedOptionPath({ step, selectionValue: optimisticSelections[step.key], }).map((option) => option.key); const currentSelectedKey = selectedPathKeys[depth - 1] || ""; const currentSelectedOption = options.find((option) => option.key === currentSelectedKey) || null; const visibleChildren = currentSelectedOption?.children || []; const levelLabel = depth === 1 ? "主卡片" : depth === 2 ? "二级卡片" : "三级卡片"; const handleSelectOption = (optionKey: string) => { const option = options.find((item) => item.key === optionKey); if (!option) { return; } const isDeveloping = option.enabled === false; const isBlocked = !isDeveloping && currentSelectedKey !== option.key && blockedOptionKeys.has(option.key); if (isDeveloping || isBlocked) { return; } const prefix = parentPath.slice(0, depth - 1); const nextKeys = currentSelectedKey === option.key ? prefix : [...prefix, option.key]; const nextSelections = { ...optimisticSelections, [step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined, }; setPendingAutoAdvanceStepKey(step.key); emitSelectionsChange(nextSelections); }; return (
{levelLabel} {depth > 1 ? ( 该层卡片从上一级已选卡片中继续展开 ) : null}
{currentSelectedOption && visibleChildren.length > 0 ? renderOptionLevels(step, visibleChildren, depth + 1, [ ...parentPath.slice(0, depth - 1), currentSelectedOption.key, ]) : null}
); }; const handleEmbeddedOptionSelect = useCallback( (optionKey: string) => { if (!activeEmbeddedOptionLevel || !activeEmbeddedStepEntry) { return; } const option = activeEmbeddedOptionLevel.options.find( (item) => item.key === optionKey ); if (!option) { return; } const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey; const isDeveloping = option.enabled === false; const isBlocked = !isDeveloping && currentSelectedKey !== option.key && blockedOptionKeys.has(option.key); if (isDeveloping || isBlocked) { return; } const selectedPathKeys = activeEmbeddedOptionLevels .slice(0, activeEmbeddedOptionLevel.depth - 1) .map((level) => level.selectedKey) .filter(Boolean); const nextKeys = currentSelectedKey === option.key ? selectedPathKeys : [...selectedPathKeys, option.key]; const nextSelections = { ...optimisticSelections, [activeEmbeddedStepEntry.step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined, }; setPendingAutoAdvanceStepKey(activeEmbeddedStepEntry.step.key); emitSelectionsChange(nextSelections); if ( currentSelectedKey !== option.key && (option.children || []).length > 0 ) { setActiveEmbeddedOptionDepth(activeEmbeddedOptionLevel.depth + 1); } else { setActiveEmbeddedOptionDepth(activeEmbeddedOptionLevel.depth); } }, [ activeEmbeddedOptionLevel, activeEmbeddedOptionLevels, activeEmbeddedStepEntry, blockedOptionKeys, emitSelectionsChange, optimisticSelections, ] ); const workflowContent = (
{showBrandStoreSection ? (

1 {firstCategoryLabel}和{secondCategoryLabel}选择

) : null} {/* 工作流步骤 */} {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: optimisticSelections[step.key], }); const active = step.key === activeEmbeddedStepEntry.step.key; const stepSelectedPath = getAgentWorkflowSelectedOptionPath( { step, selectionValue: optimisticSelections[step.key], } ); const primarySelectedOption = stepSelectedPath[0] || null; const selectedCountLabel = stepSelectedPath.length > 0 ? `${stepSelectedPath.length} 项已选` : "待选择"; return (
); })}
{isEmbeddedPathExpanded ? (
{activeEmbeddedSelectedPath.length > 0 ? (
{activeEmbeddedSelectedPath.map( (option, optionIndex) => { const optionDepth = optionIndex + 1; const optionLevelActive = (activeEmbeddedOptionLevel?.depth || 1) === optionDepth; return ( ); } )}
) : (
先选择当前步骤的主卡片,完整子节点路径会收纳在这里,需要时再展开查看。
)}
) : null}
{activeEmbeddedOptionLevel ? (
{autoAdvanceNotice ? (
已完成「{autoAdvanceNotice.completedStepTitle}」 已自动切换到「{autoAdvanceNotice.nextStepTitle}」
刚刚完成的内容:{autoAdvanceNotice.completedSummary}
) : null}
{activeEmbeddedOptionLevel.depth === 1 ? "主卡片" : `${activeEmbeddedOptionLevel.depth}级子卡片`} {activeEmbeddedOptionLevel.depth === 1 ? "先选择当前步骤的主卡片" : "当前层级只展示这一层的子节点卡片,选完再进入下一层"}
) : null}
) : 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} {step.title} {depth > 1 ? ( {`${depth}级模块`} ) : null}

{stepSelectionSummaryMap[step.key] ? `已选:${stepSelectionSummaryMap[step.key]}` : "待选择:先确定这一组风格卡片,系统再继续组合 Prompt"}
{step.description && (

{step.description}

)} {renderOptionLevels(step, step.options || [])}
)) )}
); 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}
{onNextStep ? (
工作流进度
已完成 {progress.completed} / {progress.total} 个步骤 {progress.isComplete ? ",可以直接进入下一步" : ",也可以选择跳过直接进入下一步"}
{progress.total > 0 ? Math.round((progress.completed / progress.total) * 100) : 0} %
0 ? (progress.completed / progress.total) * 100 : 0}%`, }} transition={{ duration: 0.45 }} />
{onSkipStep ? ( ) : null}
) : null} {/* 页脚 */}
); } export const AgentWorkflowPanelDarkV2 = memo(AgentWorkflowPanelDarkV2Component);