Optimize stylization workflow selection performance
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-27 16:49:02 +08:00
parent 4eaab49776
commit 1870f87beb
7 changed files with 769 additions and 356 deletions

View File

@ -60,7 +60,9 @@ import dynamic from "next/dynamic";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import {
type ChangeEvent,
startTransition,
useCallback,
useDeferredValue,
useEffect,
useMemo,
useRef,
@ -156,7 +158,6 @@ import {
} from "@/lib/canvas-toolbar-config";
import {
buildAgentWorkflowComposerContext,
getAgentWorkflowProgress,
} from "@/lib/agents/workflow-composer";
import {
createEmptyAgentWorkflowMaterialSelections,
@ -1357,6 +1358,29 @@ function getAgentProjectDraftStorageKey(projectId: string) {
return `${AGENT_PROJECT_DRAFT_STORAGE_KEY_PREFIX}${String(projectId || "").trim()}`;
}
function scheduleCanvasIdleTask(callback: () => void, timeout = 1200) {
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, 300));
return () => window.clearTimeout(timer);
}
interface AgentProjectDraftSnapshot {
savedAt: number;
title?: string | null;
@ -1832,6 +1856,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
}),
[resolvedWorkflowUiConfig, workflowSelections]
);
const deferredSanitizedWorkflowSelections = useDeferredValue(
sanitizedWorkflowSelections
);
const resolvedBrandModelConfig = useMemo(
() =>
normalizeAgentWorkflowBrandModelConfig(
@ -3129,6 +3156,49 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
]
);
const persistedAgentProjectResultState = useMemo(() => {
const resultCollections = {
scene_generation: normalizeResultCollectionUrls(
agentProjectResultCollections.scene_generation
),
detail_generation: normalizeResultCollectionUrls(
agentProjectResultCollections.detail_generation
),
detail_replacement: normalizeResultCollectionUrls(
agentProjectResultCollections.detail_replacement
),
sku_replacement: normalizeResultCollectionUrls(
agentProjectResultCollections.sku_replacement
),
};
return {
resultCollections,
deletedResultCollections: {
scene_generation: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.scene_generation
),
detail_generation: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.detail_generation
),
detail_replacement: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.detail_replacement
),
sku_replacement: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.sku_replacement
),
},
resultImageUrls: Array.from(
new Set([
...resultCollections.scene_generation,
...resultCollections.detail_generation,
...resultCollections.detail_replacement,
...resultCollections.sku_replacement,
])
),
};
}, [agentProjectDeletedResultCollections, agentProjectResultCollections]);
const buildPersistedAgentProjectState = useCallback(() => {
const persistedMaterials = appearanceRecognitionEnabled
? workflowMaterials
@ -3157,58 +3227,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
return {
workflowState,
workspaceState: agentWorkspaceState,
resultCollections: {
scene_generation: normalizeResultCollectionUrls(
agentProjectResultCollections.scene_generation
),
detail_generation: normalizeResultCollectionUrls(
agentProjectResultCollections.detail_generation
),
detail_replacement: normalizeResultCollectionUrls(
agentProjectResultCollections.detail_replacement
),
sku_replacement: normalizeResultCollectionUrls(
agentProjectResultCollections.sku_replacement
),
},
deletedResultCollections: {
scene_generation: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.scene_generation
),
detail_generation: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.detail_generation
),
detail_replacement: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.detail_replacement
),
sku_replacement: normalizeResultCollectionUrls(
agentProjectDeletedResultCollections.sku_replacement
),
},
resultImageUrls: Array.from(
new Set([
...normalizeResultCollectionUrls(
agentProjectResultCollections.scene_generation
),
...normalizeResultCollectionUrls(
agentProjectResultCollections.detail_generation
),
...normalizeResultCollectionUrls(
agentProjectResultCollections.detail_replacement
),
...normalizeResultCollectionUrls(
agentProjectResultCollections.sku_replacement
),
])
),
...persistedAgentProjectResultState,
};
}, [
agentModeState,
agentProjectDeletedResultCollections,
agentProjectResultCollections,
agentWorkspaceState,
currentAgentStage,
appearanceRecognitionEnabled,
persistedAgentProjectResultState,
selectedAgentPresetId,
selectedBrandModel,
workflowInputs,
@ -3393,26 +3419,28 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
if (typeof window === "undefined" || workflowHydrationSource === "pending")
return;
try {
const persistedMaterials = appearanceRecognitionEnabled
? workflowMaterials
: clearAppearanceSummaryFromMaterials(workflowMaterials);
window.localStorage.setItem(
workflowStorageKey,
JSON.stringify({
inputs: workflowInputs,
materials: persistedMaterials,
appearanceRecognitionEnabled,
selections: sanitizedWorkflowSelections,
selectedBrandModel,
selectedAgentPresetId,
currentAgentStage,
agentModeState,
})
);
} catch (error) {
console.warn("[Canvas] Failed to persist workflow state:", error);
}
return scheduleCanvasIdleTask(() => {
try {
const persistedMaterials = appearanceRecognitionEnabled
? workflowMaterials
: clearAppearanceSummaryFromMaterials(workflowMaterials);
window.localStorage.setItem(
workflowStorageKey,
JSON.stringify({
inputs: workflowInputs,
materials: persistedMaterials,
appearanceRecognitionEnabled,
selections: sanitizedWorkflowSelections,
selectedBrandModel,
selectedAgentPresetId,
currentAgentStage,
agentModeState,
})
);
} catch (error) {
console.warn("[Canvas] Failed to persist workflow state:", error);
}
});
}, [
isAgentMode,
workflowHydrationSource,
@ -3432,39 +3460,30 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
return;
if (typeof window === "undefined") return;
try {
const draftSnapshot: AgentProjectDraftSnapshot = {
savedAt: Date.now(),
title: agentProjectTitle,
thumbnail: agentProjectThumbnail,
projectState: buildPersistedAgentProjectState(),
};
return scheduleCanvasIdleTask(() => {
try {
const draftSnapshot: AgentProjectDraftSnapshot = {
savedAt: Date.now(),
title: agentProjectTitle,
thumbnail: agentProjectThumbnail,
projectState: buildPersistedAgentProjectState(),
};
window.localStorage.setItem(
getAgentProjectDraftStorageKey(agentProjectId),
JSON.stringify(draftSnapshot)
);
} catch (error) {
console.warn("[Agent Project] Failed to persist local draft:", error);
}
window.localStorage.setItem(
getAgentProjectDraftStorageKey(agentProjectId),
JSON.stringify(draftSnapshot)
);
} catch (error) {
console.warn("[Agent Project] Failed to persist local draft:", error);
}
});
}, [
agentModeState,
agentProjectDeletedResultCollections,
agentProjectId,
agentProjectResultCollections,
agentProjectThumbnail,
agentProjectTitle,
agentWorkspaceState,
appearanceRecognitionEnabled,
buildPersistedAgentProjectState,
currentAgentStage,
hasLoadedCurrentAgentProject,
isAgentMode,
selectedAgentPresetId,
selectedBrandModel,
workflowInputs,
workflowMaterials,
workflowSelections,
]);
const persistAgentProject = useCallback(
@ -8268,20 +8287,38 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
setWorkflowSelections(sanitizedWorkflowSelections);
}, [sanitizedWorkflowSelections, workflowSelections]);
const workflowContext = useMemo<AgentWorkflowComposerContext | null>(() => {
return buildAgentWorkflowComposerContext({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
inputs: workflowInputs,
materials: workflowMaterials,
selectedBrandModel,
appearanceRecognitionEnabled,
selections: sanitizedWorkflowSelections,
});
const [workflowContext, setWorkflowContext] =
useState<AgentWorkflowComposerContext | null>(null);
useEffect(() => {
let canceled = false;
const cancelIdleTask = scheduleCanvasIdleTask(() => {
const nextWorkflowContext = buildAgentWorkflowComposerContext({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
inputs: workflowInputs,
materials: workflowMaterials,
selectedBrandModel,
appearanceRecognitionEnabled,
selections: deferredSanitizedWorkflowSelections,
});
if (canceled) {
return;
}
startTransition(() => {
setWorkflowContext(nextWorkflowContext);
});
}, 450);
return () => {
canceled = true;
cancelIdleTask();
};
}, [
appearanceRecognitionEnabled,
deferredSanitizedWorkflowSelections,
resolvedWorkflowUiConfig,
selectedBrandModel,
sanitizedWorkflowSelections,
workflowInputs,
workflowMaterials,
]);
@ -8378,7 +8415,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
});
workflowSelectionsRef.current = clearedSelections;
setWorkflowSelections(clearedSelections);
startTransition(() => {
setWorkflowSelections(clearedSelections);
});
},
[resolvedWorkflowUiConfig]
);
@ -8469,14 +8508,6 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
selectedAgentPresetData?.requiredUploadCount,
selectedAgentPresetData?.type,
]);
const workflowProgress = useMemo(
() =>
getAgentWorkflowProgress({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
selections: sanitizedWorkflowSelections,
}),
[resolvedWorkflowUiConfig, sanitizedWorkflowSelections]
);
const resolvedAgentProjectTitle = hasActiveAgentProject
? String(agentProjectTitle || "").trim() || "未命名生图项目"
: "未选择项目";
@ -8744,6 +8775,35 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
</div>
) : null;
const workflowStylePanel = useMemo(
() => (
<AgentWorkflowPanelDarkV2
config={resolvedWorkflowUiConfig}
embedded
inputs={workflowInputs}
materials={workflowMaterials}
onInputsChange={handleWorkflowInputsChange}
onMaterialsChange={handleWorkflowMaterialsChange}
onSelectionsChange={handleWorkflowSelectionsChange}
psdTemplateCategoryTagIndex={
siteSettingsData?.agentPsdTemplateCategoryTagIndex
}
selections={sanitizedWorkflowSelections}
showBrandStoreSection={false}
/>
),
[
handleWorkflowInputsChange,
handleWorkflowMaterialsChange,
handleWorkflowSelectionsChange,
resolvedWorkflowUiConfig,
sanitizedWorkflowSelections,
siteSettingsData?.agentPsdTemplateCategoryTagIndex,
workflowInputs,
workflowMaterials,
]
);
if (isAgentMode) {
if (!hasActiveAgentProject) {
return (
@ -8826,22 +8886,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
workflowFeatureSettings={workflowPageFeatureSettings}
// 嵌入的风格化选择面板
workflowPromptOptimizationEnabled={appearanceRecognitionEnabled}
workflowStylePanel={
<AgentWorkflowPanelDarkV2
config={resolvedWorkflowUiConfig}
embedded
inputs={workflowInputs}
materials={workflowMaterials}
onInputsChange={handleWorkflowInputsChange}
onMaterialsChange={handleWorkflowMaterialsChange}
onSelectionsChange={handleWorkflowSelectionsChange}
psdTemplateCategoryTagIndex={
siteSettingsData?.agentPsdTemplateCategoryTagIndex
}
selections={sanitizedWorkflowSelections}
showBrandStoreSection={false}
/>
}
workflowStylePanel={workflowStylePanel}
workspaceState={agentWorkspaceState}
/>
{loginDialogElement}

View File

@ -3650,8 +3650,9 @@ export function AgentGenerationWorkspaceDarkV2({
const detailPrompt =
userSupplementPrompt ||
`${basePrompt},面向${detailGenerationBrief.targetAudience},在${detailGenerationBrief.usageScenario}场景解决${detailGenerationBrief.coreNeed}${demandHint}`;
const workflowHint = workflowContext?.promptText
? `\n\n【工作流风格要求】\n${workflowContext.promptText}`
const latestWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext;
const workflowHint = latestWorkflowContext?.promptText
? `\n\n【工作流风格要求】\n${latestWorkflowContext.promptText}`
: "";
const detailPromptWithWorkflow = `${detailPrompt}${workflowHint}`.trim();
const detailCount = extractAgentDetailFrameCount(detailPrompt);

View File

@ -76,6 +76,7 @@ export function AgentWorkflowMaterialPicker({
}: AgentWorkflowMaterialPickerProps) {
const isDark = theme === "dark";
const hasUploadInFlight = isUploadingNewImage || Boolean(uploadingImageId);
const canUploadMaterialImage = Boolean(onUploadImage);
const [previewDisplayMode, setPreviewDisplayMode] = useState<"selected" | "all">("all");
const [hasManualSelectionInteraction, setHasManualSelectionInteraction] = useState(false);
const selectedSet = useMemo(
@ -157,12 +158,14 @@ export function AgentWorkflowMaterialPicker({
const remainingSelectableCount =
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
const remainingUploadSlotCount =
resolvedSelectionLimit === null
!canUploadMaterialImage
? 0
: Math.max(
resolvedSelectionLimit - selectedGalleryCount - uploadedImages.length,
0
);
: resolvedSelectionLimit === null
? 1
: Math.max(
resolvedSelectionLimit - selectedGalleryCount - uploadedImages.length,
0
);
const totalImageCount = galleryImages.length;
const hasSystemRecommendedSelection =
result?.queryType === "sku" &&
@ -430,7 +433,7 @@ export function AgentWorkflowMaterialPicker({
</div>
) : null}
{!disabled && (resolvedSelectionLimit !== null || uploadedImages.length > 0) ? (
{!disabled && canUploadMaterialImage ? (
<div
className={cn(
"space-y-3 rounded-[24px] border p-3",
@ -472,7 +475,9 @@ export function AgentWorkflowMaterialPicker({
isDark ? "bg-emerald-500/[0.12] text-emerald-200" : "bg-emerald-50 text-emerald-700"
)}
>
还可上传 {remainingUploadSlotCount} 张
{resolvedSelectionLimit === null
? "可继续上传"
: `还可上传 ${remainingUploadSlotCount} 张`}
</span>
) : null}
</div>

View File

@ -1,6 +1,17 @@
"use client";
import { useEffect, useMemo, useState, type ReactNode } from "react";
import {
memo,
startTransition,
useCallback,
useDeferredValue,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { motion } from "framer-motion";
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
import { toast } from "sonner";
@ -26,7 +37,6 @@ import {
getRenderableAgentWorkflowOptionTree,
getRenderableAgentWorkflowStepTree,
isAgentWorkflowStepSelectionComplete,
sanitizeAgentWorkflowSelectionsForBlockedOptions,
serializeAgentWorkflowSelectionPath,
} from "@/lib/agents/workflow-ui-config";
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
@ -41,7 +51,6 @@ import {
type AgentPsdTemplateCategoryTagIndex,
} from "@/lib/agents/psd-template-category-tag-config";
import type {
AgentWorkflowComposerSelection,
AgentWorkflowMaterialSelections,
AgentWorkflowShowcaseConfig,
AgentWorkflowStepKey,
@ -90,31 +99,94 @@ interface AgentWorkflowBrandStorePanelProps {
description?: string;
}
function buildSelectionSummary(params: {
config: AgentWorkflowUiConfig;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) {
const result: AgentWorkflowComposerSelection[] = [];
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
const selectedOptions = getAgentWorkflowSelectedOptionPath({
step,
selectionValue: params.selections[step.key],
});
selectedOptions.forEach((option) => {
result.push({
stepKey: step.key,
stepTitle: step.title,
optionKey: option.key,
optionName: option.buttonName,
promptFragment: option.promptFragment || "",
});
});
type WorkflowSelectionState = Partial<Record<AgentWorkflowStepKey, string>>;
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 = 18;
interface WorkflowPanelDerivedState {
composerContext: ReturnType<typeof buildAgentWorkflowComposerContext>;
progress: ReturnType<typeof getAgentWorkflowProgress>;
}
function scheduleWorkflowIdleTask(callback: () => void, timeout = 450) {
if (typeof window === "undefined") {
callback();
return () => {};
}
return result;
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;
inputs: AgentWorkflowInputs;
materials: AgentWorkflowMaterialSelections;
selections: WorkflowSelectionState;
}): WorkflowPanelDerivedState {
return {
composerContext: buildAgentWorkflowComposerContext({
config: params.config,
inputs: params.inputs,
materials: params.materials,
selections: params.selections,
}),
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: AgentWorkflowUiConfig["steps"][number];
step: WorkflowStep;
selectionValue?: string;
}) {
const selectedOptions = getAgentWorkflowSelectedOptionPath({
@ -130,14 +202,14 @@ function getWorkflowStepSelectionSummary(params: {
}
function getWorkflowStepOptionLevels(params: {
step: AgentWorkflowUiConfig["steps"][number];
step: WorkflowStep;
selectionValue?: string;
}) {
const levels: Array<{
depth: number;
options: AgentWorkflowUiConfig["steps"][number]["options"];
options: WorkflowStep["options"];
selectedKey: string;
selectedOption: AgentWorkflowUiConfig["steps"][number]["options"][number] | null;
selectedOption: WorkflowOption | null;
}> = [];
const selectedPath = getAgentWorkflowSelectedOptionPath({
step: params.step,
@ -145,7 +217,7 @@ function getWorkflowStepOptionLevels(params: {
});
let depth = 1;
let currentOptions = getRenderableAgentWorkflowOptionTree(params.step.options || []);
let currentOptions = params.step.options || [];
while (currentOptions.length > 0) {
const selectedKey = selectedPath[depth - 1]?.key || "";
@ -159,9 +231,7 @@ function getWorkflowStepOptionLevels(params: {
selectedOption,
});
const nextOptions = getRenderableAgentWorkflowOptionTree(
selectedOption?.children || []
);
const nextOptions = selectedOption?.children || [];
if (!selectedOption || nextOptions.length === 0) {
break;
}
@ -181,13 +251,13 @@ function getWorkflowOptionDepthShortLabel(depth: number) {
return depth === 1 ? "主" : `${depth}级`;
}
function StepOptionCard({
const StepOptionCard = memo(function StepOptionCard({
option,
step,
selected,
disabled = false,
disabledReason = null,
onClick,
onSelect,
index,
}: {
option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null };
@ -195,19 +265,18 @@ function StepOptionCard({
selected: boolean;
disabled?: boolean;
disabledReason?: "blocked" | "developing" | null;
onClick: () => void;
onSelect: (optionKey: string) => void;
index: number;
}) {
const handleClick = useCallback(() => {
onSelect(option.key);
}, [onSelect, option.key]);
return (
<motion.button
<button
type="button"
disabled={disabled}
onClick={onClick}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
whileHover={disabled ? undefined : { y: -4, scale: 1.02 }}
whileTap={disabled ? undefined : { scale: 0.98 }}
onClick={handleClick}
className={cn(
"group relative overflow-hidden rounded-2xl text-left",
"aspect-[16/10] w-full",
@ -227,6 +296,9 @@ function StepOptionCard({
<img
src={option.previewImageUrl}
alt={option.buttonName}
loading={selected || index < 4 ? "eager" : "lazy"}
decoding="async"
draggable={false}
className={cn(
"h-full w-full object-cover transition-transform duration-700",
!disabled && "group-hover:scale-110"
@ -243,13 +315,9 @@ function StepOptionCard({
{/* 选中标记 */}
{selected && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="absolute right-3 top-3 flex h-6 w-6 items-center justify-center rounded-full bg-white text-black"
>
<div className="absolute right-3 top-3 flex h-6 w-6 items-center justify-center rounded-full bg-white text-black">
<Check className="h-4 w-4" />
</motion.div>
</div>
)}
{disabled && !selected ? (
@ -281,10 +349,137 @@ function StepOptionCard({
{selected && (
<div className="absolute inset-0 rounded-2xl ring-2 ring-inset ring-white/30" />
)}
</motion.button>
</button>
);
});
interface VirtualizedOptionGridProps {
options: WorkflowStep["options"];
step: WorkflowStep;
selectedKey: string;
blockedOptionKeys: Set<string>;
onSelect: (optionKey: string) => void;
}
const VirtualizedOptionGrid = memo(function VirtualizedOptionGrid({
options,
step,
selectedKey,
blockedOptionKeys,
onSelect,
}: VirtualizedOptionGridProps) {
const scrollParentRef = useRef<HTMLDivElement | null>(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<HTMLDivElement, HTMLDivElement>({
count: rowCount,
getScrollElement: () => scrollParentRef.current,
estimateSize: () => rowHeight,
overscan: 3,
});
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 (
<StepOptionCard
key={option.key}
disabled={isDisabled}
disabledReason={isDeveloping ? "developing" : isBlocked ? "blocked" : null}
index={optionIndex}
onSelect={onSelect}
option={option}
selected={selectedKey === option.key}
step={step}
/>
);
};
if (!shouldVirtualize) {
return (
<div
className="grid grid-cols-1 gap-4 min-[420px]:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
ref={scrollParentRef}
>
{options.map((option, optionIndex) => renderCard(option, optionIndex))}
</div>
);
}
return (
<div
className="overflow-y-auto overscroll-contain pr-1"
ref={scrollParentRef}
style={{ maxHeight: VIRTUAL_OPTION_GRID_MAX_HEIGHT }}
>
<div
className="relative w-full"
style={{
height: rowVirtualizer.getTotalSize(),
}}
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const startIndex = virtualRow.index * columnCount;
const rowOptions = options.slice(startIndex, startIndex + columnCount);
return (
<div
className="absolute left-0 top-0 grid w-full gap-4"
data-index={virtualRow.index}
key={virtualRow.key}
ref={rowVirtualizer.measureElement}
style={{
gridTemplateColumns: `repeat(${columnCount}, minmax(0, 1fr))`,
minHeight: cardHeight,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{rowOptions.map((option, rowOptionIndex) =>
renderCard(option, startIndex + rowOptionIndex)
)}
</div>
);
})}
</div>
</div>
);
});
export function AgentWorkflowBrandStorePanel({
config,
inputs,
@ -571,7 +766,7 @@ export function AgentWorkflowBrandStorePanel({
);
}
export function AgentWorkflowPanelDarkV2({
function AgentWorkflowPanelDarkV2Component({
config,
inputs,
onInputsChange,
@ -614,53 +809,131 @@ export function AgentWorkflowPanelDarkV2({
() => getAgentWorkflowPageFeatureSettings(config),
[config]
);
const [optimisticSelections, setOptimisticSelections] =
useState<WorkflowSelectionState>(selections);
const deferredSelections = useDeferredValue(optimisticSelections);
const latestExternalSelectionsRef = useRef(selections);
const selectionCommitTimerRef = useRef<number | null>(null);
const pendingSelectionCommitRef = useRef<WorkflowSelectionState | null>(null);
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, selections).filter(
flattenVisibleAgentWorkflowSteps(displayConfig.steps, optimisticSelections).filter(
({ step }) => Array.isArray(step.options) && step.options.length > 0
),
[displayConfig.steps, selections]
[displayConfig.steps, optimisticSelections]
);
const summary = useMemo(
() => buildSelectionSummary({ config: resolvedConfig, selections }),
[resolvedConfig, selections]
);
const composerContext = useMemo(
() =>
buildAgentWorkflowComposerContext({
const [workflowDerivedState, setWorkflowDerivedState] =
useState<WorkflowPanelDerivedState>(() =>
buildWorkflowPanelDerivedState({
config: resolvedConfig,
inputs,
materials,
selections,
}),
[inputs, materials, resolvedConfig, selections]
);
selections: deferredSelections,
})
);
const workflowDerivedVersionRef = useRef(0);
const progress = useMemo(
() => getAgentWorkflowProgress({ config: resolvedConfig, selections }),
[resolvedConfig, selections]
useEffect(
() => {
const version = workflowDerivedVersionRef.current + 1;
workflowDerivedVersionRef.current = version;
return scheduleWorkflowIdleTask(() => {
const nextDerivedState = buildWorkflowPanelDerivedState({
config: resolvedConfig,
inputs,
materials,
selections: deferredSelections,
});
if (workflowDerivedVersionRef.current !== version) {
return;
}
startTransition(() => {
setWorkflowDerivedState(nextDerivedState);
});
});
},
[deferredSelections, inputs, materials, resolvedConfig]
);
const composerContext = workflowDerivedState.composerContext;
const progress = workflowDerivedState.progress;
const stepSelectionSummaryMap = useMemo(() => {
const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map(({ step }) => [
step.key,
getWorkflowStepSelectionSummary({
step,
selectionValue: selections[step.key],
selectionValue: deferredSelections[step.key],
}),
]);
return Object.fromEntries(entries) as Record<string, string>;
}, [resolvedConfig.steps, selections]);
}, [deferredSelections, resolvedConfig.steps]);
const blockedOptionKeys = useMemo(
() =>
new Set(
getAgentWorkflowBlockedOptionKeysForSelections({
config: resolvedConfig,
selections,
selections: deferredSelections,
})
),
[resolvedConfig, selections]
[deferredSelections, resolvedConfig]
);
const [autoAdvanceNotice, setAutoAdvanceNotice] = useState<{
completedStepTitle: string;
@ -681,20 +954,20 @@ export function AgentWorkflowPanelDarkV2({
activeEmbeddedStepEntry
? getWorkflowStepOptionLevels({
step: activeEmbeddedStepEntry.step,
selectionValue: selections[activeEmbeddedStepEntry.step.key],
selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key],
})
: [],
[activeEmbeddedStepEntry, selections]
[activeEmbeddedStepEntry, optimisticSelections]
);
const activeEmbeddedSelectedPath = useMemo(
() =>
activeEmbeddedStepEntry
? getAgentWorkflowSelectedOptionPath({
step: activeEmbeddedStepEntry.step,
selectionValue: selections[activeEmbeddedStepEntry.step.key],
selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key],
})
: [],
[activeEmbeddedStepEntry, selections]
[activeEmbeddedStepEntry, optimisticSelections]
);
const activeEmbeddedOptionLevel =
activeEmbeddedOptionLevels.find((level) => level.depth === activeEmbeddedOptionDepth) ||
@ -713,9 +986,7 @@ export function AgentWorkflowPanelDarkV2({
return false;
}
const renderableChildren = getRenderableAgentWorkflowOptionTree(
activeEmbeddedDeepestSelectedOption.children || []
);
const renderableChildren = activeEmbeddedDeepestSelectedOption.children || [];
if (renderableChildren.length === 0) {
return false;
}
@ -783,7 +1054,7 @@ export function AgentWorkflowPanelDarkV2({
const currentStepComplete = isAgentWorkflowStepSelectionComplete({
step: activeEmbeddedStepEntry.step,
selectionValue: selections[activeEmbeddedStepEntry.step.key],
selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key],
});
if (!currentStepComplete) {
return;
@ -800,7 +1071,7 @@ export function AgentWorkflowPanelDarkV2({
({ step }) =>
!isAgentWorkflowStepSelectionComplete({
step,
selectionValue: selections[step.key],
selectionValue: optimisticSelections[step.key],
})
);
@ -824,7 +1095,7 @@ export function AgentWorkflowPanelDarkV2({
embedded,
flattenedResolvedSteps,
pendingAutoAdvanceStepKey,
selections,
optimisticSelections,
stepSelectionSummaryMap,
]);
@ -848,15 +1119,42 @@ export function AgentWorkflowPanelDarkV2({
): ReactNode => {
const selectedPathKeys = getAgentWorkflowSelectedOptionPath({
step,
selectionValue: selections[step.key],
selectionValue: optimisticSelections[step.key],
}).map((option) => option.key);
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
const currentSelectedOption =
options.find((option) => option.key === currentSelectedKey) || null;
const visibleChildren = getRenderableAgentWorkflowOptionTree(
currentSelectedOption?.children || []
);
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 (
<div className="space-y-4">
@ -871,50 +1169,13 @@ export function AgentWorkflowPanelDarkV2({
) : null}
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{options.map((option, optionIndex) => {
const isDeveloping = option.enabled === false;
const isBlocked =
!isDeveloping &&
currentSelectedKey !== option.key &&
blockedOptionKeys.has(option.key);
const isDisabled = isDeveloping || isBlocked;
return (
<StepOptionCard
key={`${option.key}-${depth}`}
option={option}
step={step}
selected={currentSelectedKey === option.key}
disabled={isDisabled}
disabledReason={isDeveloping ? "developing" : isBlocked ? "blocked" : null}
onClick={() => {
if (isDisabled) {
return;
}
const prefix = parentPath.slice(0, depth - 1);
const nextKeys =
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
const nextSelections = {
...selections,
[step.key]:
nextKeys.length > 0
? serializeAgentWorkflowSelectionPath(nextKeys)
: undefined,
};
setPendingAutoAdvanceStepKey(step.key);
onSelectionsChange(
sanitizeAgentWorkflowSelectionsForBlockedOptions({
config: resolvedConfig,
selections: nextSelections,
})
);
}}
index={optionIndex}
/>
);
})}
</div>
<VirtualizedOptionGrid
blockedOptionKeys={blockedOptionKeys}
onSelect={handleSelectOption}
options={options}
selectedKey={currentSelectedKey}
step={step}
/>
{currentSelectedOption && visibleChildren.length > 0
? renderOptionLevels(step, visibleChildren, depth + 1, [
@ -939,6 +1200,62 @@ export function AgentWorkflowPanelDarkV2({
}
};
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 = (
<div className="space-y-8">
{showBrandStoreSection ? (
@ -979,17 +1296,17 @@ export function AgentWorkflowPanelDarkV2({
>
<div className="mb-5 space-y-4">
<div className="mt-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5">
{flattenedResolvedSteps.map(({ step }, stepIndex) => {
const completed = isAgentWorkflowStepSelectionComplete({
step,
selectionValue: selections[step.key],
});
const active = step.key === activeEmbeddedStepEntry.step.key;
const stepSelectedPath = getAgentWorkflowSelectedOptionPath({
step,
selectionValue: selections[step.key],
});
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5">
{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
@ -1186,72 +1503,13 @@ export function AgentWorkflowPanelDarkV2({
</span>
</div>
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{activeEmbeddedOptionLevel.options.map((option, optionIndex) => {
const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey;
const isDeveloping = option.enabled === false;
const isBlocked =
!isDeveloping &&
currentSelectedKey !== option.key &&
blockedOptionKeys.has(option.key);
const isDisabled = isDeveloping || isBlocked;
const nextChildren = getRenderableAgentWorkflowOptionTree(
option.children || []
);
return (
<StepOptionCard
key={`${option.key}-${activeEmbeddedOptionLevel.depth}`}
option={option}
step={activeEmbeddedStepEntry.step}
selected={currentSelectedKey === option.key}
disabled={isDisabled}
disabledReason={
isDeveloping ? "developing" : isBlocked ? "blocked" : null
}
onClick={() => {
if (isDisabled) {
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 = {
...selections,
[activeEmbeddedStepEntry.step.key]:
nextKeys.length > 0
? serializeAgentWorkflowSelectionPath(nextKeys)
: undefined,
};
setPendingAutoAdvanceStepKey(activeEmbeddedStepEntry.step.key);
onSelectionsChange(
sanitizeAgentWorkflowSelectionsForBlockedOptions({
config: resolvedConfig,
selections: nextSelections,
})
);
if (currentSelectedKey !== option.key && nextChildren.length > 0) {
setActiveEmbeddedOptionDepth(
activeEmbeddedOptionLevel.depth + 1
);
} else {
setActiveEmbeddedOptionDepth(
activeEmbeddedOptionLevel.depth
);
}
}}
index={optionIndex}
/>
);
})}
</div>
<VirtualizedOptionGrid
blockedOptionKeys={blockedOptionKeys}
onSelect={handleEmbeddedOptionSelect}
options={activeEmbeddedOptionLevel.options}
selectedKey={activeEmbeddedOptionLevel.selectedKey}
step={activeEmbeddedStepEntry.step}
/>
</div>
) : null}
</motion.section>
@ -1287,7 +1545,7 @@ export function AgentWorkflowPanelDarkV2({
{step.description && (
<p className="mb-4 text-sm text-white/40">{step.description}</p>
)}
{renderOptionLevels(step, getRenderableAgentWorkflowOptionTree(step.options || []))}
{renderOptionLevels(step, step.options || [])}
</motion.section>
))
)}
@ -1439,3 +1697,5 @@ export function AgentWorkflowPanelDarkV2({
</div>
);
}
export const AgentWorkflowPanelDarkV2 = memo(AgentWorkflowPanelDarkV2Component);

View File

@ -503,10 +503,61 @@ export function normalizeAgentWorkflowStepOrdering(
}));
}
export interface FlattenedAgentWorkflowOption {
option: AgentWorkflowOptionUiConfig;
depth: number;
parentKeys: string[];
}
const enabledStepTreeCache = new WeakMap<
AgentWorkflowStepUiConfig[],
AgentWorkflowStepUiConfig[]
>();
const renderableStepTreeCache = new WeakMap<
AgentWorkflowStepUiConfig[],
AgentWorkflowStepUiConfig[]
>();
const enabledOptionTreeCache = new WeakMap<
AgentWorkflowOptionUiConfig[],
AgentWorkflowOptionUiConfig[]
>();
const renderableOptionTreeCache = new WeakMap<
AgentWorkflowOptionUiConfig[],
AgentWorkflowOptionUiConfig[]
>();
const flattenedOptionTreeCache = new WeakMap<
AgentWorkflowOptionUiConfig[],
FlattenedAgentWorkflowOption[]
>();
const optionLookupCache = new WeakMap<
AgentWorkflowOptionUiConfig[],
Map<string, AgentWorkflowOptionUiConfig>
>();
function getWorkflowOptionLookup(options: AgentWorkflowOptionUiConfig[]) {
const cached = optionLookupCache.get(options);
if (cached) {
return cached;
}
const lookup = new Map(
options
.map((option) => [option.key, option] as const)
.filter(([key]) => Boolean(String(key || "").trim()))
);
optionLookupCache.set(options, lookup);
return lookup;
}
export function getEnabledAgentWorkflowStepTree(
steps: AgentWorkflowStepUiConfig[]
): AgentWorkflowStepUiConfig[] {
return [...steps]
const cached = enabledStepTreeCache.get(steps);
if (cached) {
return cached;
}
const nextSteps = [...steps]
.sort((left, right) => left.sortOrder - right.sortOrder)
.filter((step) => step.enabled !== false)
.map((step) => ({
@ -515,23 +566,37 @@ export function getEnabledAgentWorkflowStepTree(
children: getEnabledAgentWorkflowStepTree(step.children || []),
}))
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
enabledStepTreeCache.set(steps, nextSteps);
return nextSteps;
}
export function getRenderableAgentWorkflowOptionTree(
options: AgentWorkflowOptionUiConfig[]
): AgentWorkflowOptionUiConfig[] {
return [...options]
const cached = renderableOptionTreeCache.get(options);
if (cached) {
return cached;
}
const nextOptions = [...options]
.sort((left, right) => left.sortOrder - right.sortOrder)
.map((option) => ({
...option,
children: getRenderableAgentWorkflowOptionTree(option.children || []),
}));
renderableOptionTreeCache.set(options, nextOptions);
return nextOptions;
}
export function getRenderableAgentWorkflowStepTree(
steps: AgentWorkflowStepUiConfig[]
): AgentWorkflowStepUiConfig[] {
return [...steps]
const cached = renderableStepTreeCache.get(steps);
if (cached) {
return cached;
}
const nextSteps = [...steps]
.sort((left, right) => left.sortOrder - right.sortOrder)
.filter((step) => step.enabled !== false)
.map((step) => ({
@ -544,12 +609,8 @@ export function getRenderableAgentWorkflowStepTree(
getEnabledAgentWorkflowOptionTree(step.options || []).length > 0 ||
(step.children || []).length > 0
);
}
export interface FlattenedAgentWorkflowOption {
option: AgentWorkflowOptionUiConfig;
depth: number;
parentKeys: string[];
renderableStepTreeCache.set(steps, nextSteps);
return nextSteps;
}
export function flattenAgentWorkflowOptionTree(
@ -557,7 +618,15 @@ export function flattenAgentWorkflowOptionTree(
depth = 1,
parentKeys: string[] = []
): FlattenedAgentWorkflowOption[] {
return options.flatMap((option) => [
const canUseRootCache = depth === 1 && parentKeys.length === 0;
if (canUseRootCache) {
const cached = flattenedOptionTreeCache.get(options);
if (cached) {
return cached;
}
}
const flattened = options.flatMap((option) => [
{
option,
depth,
@ -569,6 +638,10 @@ export function flattenAgentWorkflowOptionTree(
[...parentKeys, option.key]
),
]);
if (canUseRootCache) {
flattenedOptionTreeCache.set(options, flattened);
}
return flattened;
}
export function countAgentWorkflowOptionTree(
@ -600,13 +673,20 @@ export function normalizeAgentWorkflowOptionOrdering(
export function getEnabledAgentWorkflowOptionTree(
options: AgentWorkflowOptionUiConfig[]
): AgentWorkflowOptionUiConfig[] {
return [...options]
const cached = enabledOptionTreeCache.get(options);
if (cached) {
return cached;
}
const nextOptions = [...options]
.sort((left, right) => left.sortOrder - right.sortOrder)
.filter((option) => option.enabled !== false)
.map((option) => ({
...option,
children: getEnabledAgentWorkflowOptionTree(option.children || []),
}));
enabledOptionTreeCache.set(options, nextOptions);
return nextOptions;
}
export function findAgentWorkflowOptionByKey(
@ -1533,9 +1613,10 @@ export function getAgentWorkflowSelectedOptionPath(params: {
let currentOptions = params.step.options || [];
for (const key of keys) {
const matchedOption = currentOptions.find(
(option) => option.key === key && option.enabled !== false
);
const matchedOption = getWorkflowOptionLookup(currentOptions).get(key);
if (matchedOption?.enabled === false) {
break;
}
if (!matchedOption) break;
selectedOptions.push(matchedOption);
currentOptions = matchedOption.children || [];

View File

@ -60,6 +60,7 @@
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"@sparkjsdev/spark": "^0.1.10",
"@tanstack/react-virtual": "^3.13.24",
"@types/form-data": "^2.5.2",
"@vercel/analytics": "^1.3.1",
"@vercel/blob": "^0.24.1",

20
pnpm-lock.yaml generated
View File

@ -128,6 +128,9 @@ importers:
'@sparkjsdev/spark':
specifier: ^0.1.10
version: 0.1.10
'@tanstack/react-virtual':
specifier: ^3.13.24
version: 3.13.24(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)
'@types/form-data':
specifier: ^2.5.2
version: 2.5.2
@ -3073,6 +3076,15 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tanstack/react-virtual@3.13.24':
resolution: {integrity: sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@tanstack/virtual-core@3.14.0':
resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==}
'@tediousjs/connection-string@0.6.0':
resolution: {integrity: sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow==}
@ -9450,6 +9462,14 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 4.1.16
'@tanstack/react-virtual@3.13.24(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)':
dependencies:
'@tanstack/virtual-core': 3.14.0
react: 19.0.0-rc-45804af1-20241021
react-dom: 19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021)
'@tanstack/virtual-core@3.14.0': {}
'@tediousjs/connection-string@0.6.0': {}
'@tokenlens/core@1.3.0': {}