Optimize stylization workflow selection performance
This commit is contained in:
parent
4eaab49776
commit
1870f87beb
@ -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}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 || [];
|
||||
|
||||
@ -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
20
pnpm-lock.yaml
generated
@ -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': {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user