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 { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
type ChangeEvent,
|
type ChangeEvent,
|
||||||
|
startTransition,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useDeferredValue,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@ -156,7 +158,6 @@ import {
|
|||||||
} from "@/lib/canvas-toolbar-config";
|
} from "@/lib/canvas-toolbar-config";
|
||||||
import {
|
import {
|
||||||
buildAgentWorkflowComposerContext,
|
buildAgentWorkflowComposerContext,
|
||||||
getAgentWorkflowProgress,
|
|
||||||
} from "@/lib/agents/workflow-composer";
|
} from "@/lib/agents/workflow-composer";
|
||||||
import {
|
import {
|
||||||
createEmptyAgentWorkflowMaterialSelections,
|
createEmptyAgentWorkflowMaterialSelections,
|
||||||
@ -1357,6 +1358,29 @@ function getAgentProjectDraftStorageKey(projectId: string) {
|
|||||||
return `${AGENT_PROJECT_DRAFT_STORAGE_KEY_PREFIX}${String(projectId || "").trim()}`;
|
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 {
|
interface AgentProjectDraftSnapshot {
|
||||||
savedAt: number;
|
savedAt: number;
|
||||||
title?: string | null;
|
title?: string | null;
|
||||||
@ -1832,6 +1856,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
}),
|
}),
|
||||||
[resolvedWorkflowUiConfig, workflowSelections]
|
[resolvedWorkflowUiConfig, workflowSelections]
|
||||||
);
|
);
|
||||||
|
const deferredSanitizedWorkflowSelections = useDeferredValue(
|
||||||
|
sanitizedWorkflowSelections
|
||||||
|
);
|
||||||
const resolvedBrandModelConfig = useMemo(
|
const resolvedBrandModelConfig = useMemo(
|
||||||
() =>
|
() =>
|
||||||
normalizeAgentWorkflowBrandModelConfig(
|
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 buildPersistedAgentProjectState = useCallback(() => {
|
||||||
const persistedMaterials = appearanceRecognitionEnabled
|
const persistedMaterials = appearanceRecognitionEnabled
|
||||||
? workflowMaterials
|
? workflowMaterials
|
||||||
@ -3157,58 +3227,14 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
return {
|
return {
|
||||||
workflowState,
|
workflowState,
|
||||||
workspaceState: agentWorkspaceState,
|
workspaceState: agentWorkspaceState,
|
||||||
resultCollections: {
|
...persistedAgentProjectResultState,
|
||||||
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
|
|
||||||
),
|
|
||||||
])
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
agentModeState,
|
agentModeState,
|
||||||
agentProjectDeletedResultCollections,
|
|
||||||
agentProjectResultCollections,
|
|
||||||
agentWorkspaceState,
|
agentWorkspaceState,
|
||||||
currentAgentStage,
|
currentAgentStage,
|
||||||
appearanceRecognitionEnabled,
|
appearanceRecognitionEnabled,
|
||||||
|
persistedAgentProjectResultState,
|
||||||
selectedAgentPresetId,
|
selectedAgentPresetId,
|
||||||
selectedBrandModel,
|
selectedBrandModel,
|
||||||
workflowInputs,
|
workflowInputs,
|
||||||
@ -3393,26 +3419,28 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
if (typeof window === "undefined" || workflowHydrationSource === "pending")
|
if (typeof window === "undefined" || workflowHydrationSource === "pending")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
return scheduleCanvasIdleTask(() => {
|
||||||
const persistedMaterials = appearanceRecognitionEnabled
|
try {
|
||||||
? workflowMaterials
|
const persistedMaterials = appearanceRecognitionEnabled
|
||||||
: clearAppearanceSummaryFromMaterials(workflowMaterials);
|
? workflowMaterials
|
||||||
window.localStorage.setItem(
|
: clearAppearanceSummaryFromMaterials(workflowMaterials);
|
||||||
workflowStorageKey,
|
window.localStorage.setItem(
|
||||||
JSON.stringify({
|
workflowStorageKey,
|
||||||
inputs: workflowInputs,
|
JSON.stringify({
|
||||||
materials: persistedMaterials,
|
inputs: workflowInputs,
|
||||||
appearanceRecognitionEnabled,
|
materials: persistedMaterials,
|
||||||
selections: sanitizedWorkflowSelections,
|
appearanceRecognitionEnabled,
|
||||||
selectedBrandModel,
|
selections: sanitizedWorkflowSelections,
|
||||||
selectedAgentPresetId,
|
selectedBrandModel,
|
||||||
currentAgentStage,
|
selectedAgentPresetId,
|
||||||
agentModeState,
|
currentAgentStage,
|
||||||
})
|
agentModeState,
|
||||||
);
|
})
|
||||||
} catch (error) {
|
);
|
||||||
console.warn("[Canvas] Failed to persist workflow state:", error);
|
} catch (error) {
|
||||||
}
|
console.warn("[Canvas] Failed to persist workflow state:", error);
|
||||||
|
}
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
isAgentMode,
|
isAgentMode,
|
||||||
workflowHydrationSource,
|
workflowHydrationSource,
|
||||||
@ -3432,39 +3460,30 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
return;
|
return;
|
||||||
if (typeof window === "undefined") return;
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
try {
|
return scheduleCanvasIdleTask(() => {
|
||||||
const draftSnapshot: AgentProjectDraftSnapshot = {
|
try {
|
||||||
savedAt: Date.now(),
|
const draftSnapshot: AgentProjectDraftSnapshot = {
|
||||||
title: agentProjectTitle,
|
savedAt: Date.now(),
|
||||||
thumbnail: agentProjectThumbnail,
|
title: agentProjectTitle,
|
||||||
projectState: buildPersistedAgentProjectState(),
|
thumbnail: agentProjectThumbnail,
|
||||||
};
|
projectState: buildPersistedAgentProjectState(),
|
||||||
|
};
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
getAgentProjectDraftStorageKey(agentProjectId),
|
getAgentProjectDraftStorageKey(agentProjectId),
|
||||||
JSON.stringify(draftSnapshot)
|
JSON.stringify(draftSnapshot)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn("[Agent Project] Failed to persist local draft:", error);
|
console.warn("[Agent Project] Failed to persist local draft:", error);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, [
|
}, [
|
||||||
agentModeState,
|
|
||||||
agentProjectDeletedResultCollections,
|
|
||||||
agentProjectId,
|
agentProjectId,
|
||||||
agentProjectResultCollections,
|
|
||||||
agentProjectThumbnail,
|
agentProjectThumbnail,
|
||||||
agentProjectTitle,
|
agentProjectTitle,
|
||||||
agentWorkspaceState,
|
|
||||||
appearanceRecognitionEnabled,
|
|
||||||
buildPersistedAgentProjectState,
|
buildPersistedAgentProjectState,
|
||||||
currentAgentStage,
|
|
||||||
hasLoadedCurrentAgentProject,
|
hasLoadedCurrentAgentProject,
|
||||||
isAgentMode,
|
isAgentMode,
|
||||||
selectedAgentPresetId,
|
|
||||||
selectedBrandModel,
|
|
||||||
workflowInputs,
|
|
||||||
workflowMaterials,
|
|
||||||
workflowSelections,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const persistAgentProject = useCallback(
|
const persistAgentProject = useCallback(
|
||||||
@ -8268,20 +8287,38 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
setWorkflowSelections(sanitizedWorkflowSelections);
|
setWorkflowSelections(sanitizedWorkflowSelections);
|
||||||
}, [sanitizedWorkflowSelections, workflowSelections]);
|
}, [sanitizedWorkflowSelections, workflowSelections]);
|
||||||
|
|
||||||
const workflowContext = useMemo<AgentWorkflowComposerContext | null>(() => {
|
const [workflowContext, setWorkflowContext] =
|
||||||
return buildAgentWorkflowComposerContext({
|
useState<AgentWorkflowComposerContext | null>(null);
|
||||||
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
useEffect(() => {
|
||||||
inputs: workflowInputs,
|
let canceled = false;
|
||||||
materials: workflowMaterials,
|
const cancelIdleTask = scheduleCanvasIdleTask(() => {
|
||||||
selectedBrandModel,
|
const nextWorkflowContext = buildAgentWorkflowComposerContext({
|
||||||
appearanceRecognitionEnabled,
|
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
||||||
selections: sanitizedWorkflowSelections,
|
inputs: workflowInputs,
|
||||||
});
|
materials: workflowMaterials,
|
||||||
|
selectedBrandModel,
|
||||||
|
appearanceRecognitionEnabled,
|
||||||
|
selections: deferredSanitizedWorkflowSelections,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
startTransition(() => {
|
||||||
|
setWorkflowContext(nextWorkflowContext);
|
||||||
|
});
|
||||||
|
}, 450);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
cancelIdleTask();
|
||||||
|
};
|
||||||
}, [
|
}, [
|
||||||
appearanceRecognitionEnabled,
|
appearanceRecognitionEnabled,
|
||||||
|
deferredSanitizedWorkflowSelections,
|
||||||
resolvedWorkflowUiConfig,
|
resolvedWorkflowUiConfig,
|
||||||
selectedBrandModel,
|
selectedBrandModel,
|
||||||
sanitizedWorkflowSelections,
|
|
||||||
workflowInputs,
|
workflowInputs,
|
||||||
workflowMaterials,
|
workflowMaterials,
|
||||||
]);
|
]);
|
||||||
@ -8378,7 +8415,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
workflowSelectionsRef.current = clearedSelections;
|
workflowSelectionsRef.current = clearedSelections;
|
||||||
setWorkflowSelections(clearedSelections);
|
startTransition(() => {
|
||||||
|
setWorkflowSelections(clearedSelections);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[resolvedWorkflowUiConfig]
|
[resolvedWorkflowUiConfig]
|
||||||
);
|
);
|
||||||
@ -8469,14 +8508,6 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
selectedAgentPresetData?.requiredUploadCount,
|
selectedAgentPresetData?.requiredUploadCount,
|
||||||
selectedAgentPresetData?.type,
|
selectedAgentPresetData?.type,
|
||||||
]);
|
]);
|
||||||
const workflowProgress = useMemo(
|
|
||||||
() =>
|
|
||||||
getAgentWorkflowProgress({
|
|
||||||
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
|
|
||||||
selections: sanitizedWorkflowSelections,
|
|
||||||
}),
|
|
||||||
[resolvedWorkflowUiConfig, sanitizedWorkflowSelections]
|
|
||||||
);
|
|
||||||
const resolvedAgentProjectTitle = hasActiveAgentProject
|
const resolvedAgentProjectTitle = hasActiveAgentProject
|
||||||
? String(agentProjectTitle || "").trim() || "未命名生图项目"
|
? String(agentProjectTitle || "").trim() || "未命名生图项目"
|
||||||
: "未选择项目";
|
: "未选择项目";
|
||||||
@ -8744,6 +8775,35 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null;
|
) : 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 (isAgentMode) {
|
||||||
if (!hasActiveAgentProject) {
|
if (!hasActiveAgentProject) {
|
||||||
return (
|
return (
|
||||||
@ -8826,22 +8886,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
|||||||
workflowFeatureSettings={workflowPageFeatureSettings}
|
workflowFeatureSettings={workflowPageFeatureSettings}
|
||||||
// 嵌入的风格化选择面板
|
// 嵌入的风格化选择面板
|
||||||
workflowPromptOptimizationEnabled={appearanceRecognitionEnabled}
|
workflowPromptOptimizationEnabled={appearanceRecognitionEnabled}
|
||||||
workflowStylePanel={
|
workflowStylePanel={workflowStylePanel}
|
||||||
<AgentWorkflowPanelDarkV2
|
|
||||||
config={resolvedWorkflowUiConfig}
|
|
||||||
embedded
|
|
||||||
inputs={workflowInputs}
|
|
||||||
materials={workflowMaterials}
|
|
||||||
onInputsChange={handleWorkflowInputsChange}
|
|
||||||
onMaterialsChange={handleWorkflowMaterialsChange}
|
|
||||||
onSelectionsChange={handleWorkflowSelectionsChange}
|
|
||||||
psdTemplateCategoryTagIndex={
|
|
||||||
siteSettingsData?.agentPsdTemplateCategoryTagIndex
|
|
||||||
}
|
|
||||||
selections={sanitizedWorkflowSelections}
|
|
||||||
showBrandStoreSection={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
workspaceState={agentWorkspaceState}
|
workspaceState={agentWorkspaceState}
|
||||||
/>
|
/>
|
||||||
{loginDialogElement}
|
{loginDialogElement}
|
||||||
|
|||||||
@ -3650,8 +3650,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
const detailPrompt =
|
const detailPrompt =
|
||||||
userSupplementPrompt ||
|
userSupplementPrompt ||
|
||||||
`${basePrompt},面向${detailGenerationBrief.targetAudience},在${detailGenerationBrief.usageScenario}场景解决${detailGenerationBrief.coreNeed}${demandHint}`;
|
`${basePrompt},面向${detailGenerationBrief.targetAudience},在${detailGenerationBrief.usageScenario}场景解决${detailGenerationBrief.coreNeed}${demandHint}`;
|
||||||
const workflowHint = workflowContext?.promptText
|
const latestWorkflowContext = getLatestWorkflowContext?.() ?? workflowContext;
|
||||||
? `\n\n【工作流风格要求】\n${workflowContext.promptText}`
|
const workflowHint = latestWorkflowContext?.promptText
|
||||||
|
? `\n\n【工作流风格要求】\n${latestWorkflowContext.promptText}`
|
||||||
: "";
|
: "";
|
||||||
const detailPromptWithWorkflow = `${detailPrompt}${workflowHint}`.trim();
|
const detailPromptWithWorkflow = `${detailPrompt}${workflowHint}`.trim();
|
||||||
const detailCount = extractAgentDetailFrameCount(detailPrompt);
|
const detailCount = extractAgentDetailFrameCount(detailPrompt);
|
||||||
|
|||||||
@ -76,6 +76,7 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
}: AgentWorkflowMaterialPickerProps) {
|
}: AgentWorkflowMaterialPickerProps) {
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
const hasUploadInFlight = isUploadingNewImage || Boolean(uploadingImageId);
|
const hasUploadInFlight = isUploadingNewImage || Boolean(uploadingImageId);
|
||||||
|
const canUploadMaterialImage = Boolean(onUploadImage);
|
||||||
const [previewDisplayMode, setPreviewDisplayMode] = useState<"selected" | "all">("all");
|
const [previewDisplayMode, setPreviewDisplayMode] = useState<"selected" | "all">("all");
|
||||||
const [hasManualSelectionInteraction, setHasManualSelectionInteraction] = useState(false);
|
const [hasManualSelectionInteraction, setHasManualSelectionInteraction] = useState(false);
|
||||||
const selectedSet = useMemo(
|
const selectedSet = useMemo(
|
||||||
@ -157,12 +158,14 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
const remainingSelectableCount =
|
const remainingSelectableCount =
|
||||||
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
|
resolvedSelectionLimit === null ? null : Math.max(resolvedSelectionLimit - selectedCount, 0);
|
||||||
const remainingUploadSlotCount =
|
const remainingUploadSlotCount =
|
||||||
resolvedSelectionLimit === null
|
!canUploadMaterialImage
|
||||||
? 0
|
? 0
|
||||||
: Math.max(
|
: resolvedSelectionLimit === null
|
||||||
resolvedSelectionLimit - selectedGalleryCount - uploadedImages.length,
|
? 1
|
||||||
0
|
: Math.max(
|
||||||
);
|
resolvedSelectionLimit - selectedGalleryCount - uploadedImages.length,
|
||||||
|
0
|
||||||
|
);
|
||||||
const totalImageCount = galleryImages.length;
|
const totalImageCount = galleryImages.length;
|
||||||
const hasSystemRecommendedSelection =
|
const hasSystemRecommendedSelection =
|
||||||
result?.queryType === "sku" &&
|
result?.queryType === "sku" &&
|
||||||
@ -430,7 +433,7 @@ export function AgentWorkflowMaterialPicker({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{!disabled && (resolvedSelectionLimit !== null || uploadedImages.length > 0) ? (
|
{!disabled && canUploadMaterialImage ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-3 rounded-[24px] border p-3",
|
"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"
|
isDark ? "bg-emerald-500/[0.12] text-emerald-200" : "bg-emerald-50 text-emerald-700"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
还可上传 {remainingUploadSlotCount} 张
|
{resolvedSelectionLimit === null
|
||||||
|
? "可继续上传"
|
||||||
|
: `还可上传 ${remainingUploadSlotCount} 张`}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
"use client";
|
"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 { motion } from "framer-motion";
|
||||||
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
|
import { ArrowRight, Check, Copy, Sparkles, ChevronRight } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@ -26,7 +37,6 @@ import {
|
|||||||
getRenderableAgentWorkflowOptionTree,
|
getRenderableAgentWorkflowOptionTree,
|
||||||
getRenderableAgentWorkflowStepTree,
|
getRenderableAgentWorkflowStepTree,
|
||||||
isAgentWorkflowStepSelectionComplete,
|
isAgentWorkflowStepSelectionComplete,
|
||||||
sanitizeAgentWorkflowSelectionsForBlockedOptions,
|
|
||||||
serializeAgentWorkflowSelectionPath,
|
serializeAgentWorkflowSelectionPath,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||||
@ -41,7 +51,6 @@ import {
|
|||||||
type AgentPsdTemplateCategoryTagIndex,
|
type AgentPsdTemplateCategoryTagIndex,
|
||||||
} from "@/lib/agents/psd-template-category-tag-config";
|
} from "@/lib/agents/psd-template-category-tag-config";
|
||||||
import type {
|
import type {
|
||||||
AgentWorkflowComposerSelection,
|
|
||||||
AgentWorkflowMaterialSelections,
|
AgentWorkflowMaterialSelections,
|
||||||
AgentWorkflowShowcaseConfig,
|
AgentWorkflowShowcaseConfig,
|
||||||
AgentWorkflowStepKey,
|
AgentWorkflowStepKey,
|
||||||
@ -90,31 +99,94 @@ interface AgentWorkflowBrandStorePanelProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildSelectionSummary(params: {
|
type WorkflowSelectionState = Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
config: AgentWorkflowUiConfig;
|
type WorkflowStep = AgentWorkflowUiConfig["steps"][number];
|
||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
type WorkflowOption = WorkflowStep["options"][number];
|
||||||
}) {
|
|
||||||
const result: AgentWorkflowComposerSelection[] = [];
|
const VIRTUAL_OPTION_GRID_GAP = 16;
|
||||||
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
|
const VIRTUAL_OPTION_GRID_MAX_HEIGHT = 640;
|
||||||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
const VIRTUAL_OPTION_GRID_MIN_CARD_HEIGHT = 122;
|
||||||
step,
|
const VIRTUAL_OPTION_GRID_THRESHOLD = 18;
|
||||||
selectionValue: params.selections[step.key],
|
|
||||||
});
|
interface WorkflowPanelDerivedState {
|
||||||
selectedOptions.forEach((option) => {
|
composerContext: ReturnType<typeof buildAgentWorkflowComposerContext>;
|
||||||
result.push({
|
progress: ReturnType<typeof getAgentWorkflowProgress>;
|
||||||
stepKey: step.key,
|
}
|
||||||
stepTitle: step.title,
|
|
||||||
optionKey: option.key,
|
function scheduleWorkflowIdleTask(callback: () => void, timeout = 450) {
|
||||||
optionName: option.buttonName,
|
if (typeof window === "undefined") {
|
||||||
promptFragment: option.promptFragment || "",
|
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: {
|
function getWorkflowStepSelectionSummary(params: {
|
||||||
step: AgentWorkflowUiConfig["steps"][number];
|
step: WorkflowStep;
|
||||||
selectionValue?: string;
|
selectionValue?: string;
|
||||||
}) {
|
}) {
|
||||||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||||
@ -130,14 +202,14 @@ function getWorkflowStepSelectionSummary(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWorkflowStepOptionLevels(params: {
|
function getWorkflowStepOptionLevels(params: {
|
||||||
step: AgentWorkflowUiConfig["steps"][number];
|
step: WorkflowStep;
|
||||||
selectionValue?: string;
|
selectionValue?: string;
|
||||||
}) {
|
}) {
|
||||||
const levels: Array<{
|
const levels: Array<{
|
||||||
depth: number;
|
depth: number;
|
||||||
options: AgentWorkflowUiConfig["steps"][number]["options"];
|
options: WorkflowStep["options"];
|
||||||
selectedKey: string;
|
selectedKey: string;
|
||||||
selectedOption: AgentWorkflowUiConfig["steps"][number]["options"][number] | null;
|
selectedOption: WorkflowOption | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
const selectedPath = getAgentWorkflowSelectedOptionPath({
|
const selectedPath = getAgentWorkflowSelectedOptionPath({
|
||||||
step: params.step,
|
step: params.step,
|
||||||
@ -145,7 +217,7 @@ function getWorkflowStepOptionLevels(params: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let depth = 1;
|
let depth = 1;
|
||||||
let currentOptions = getRenderableAgentWorkflowOptionTree(params.step.options || []);
|
let currentOptions = params.step.options || [];
|
||||||
|
|
||||||
while (currentOptions.length > 0) {
|
while (currentOptions.length > 0) {
|
||||||
const selectedKey = selectedPath[depth - 1]?.key || "";
|
const selectedKey = selectedPath[depth - 1]?.key || "";
|
||||||
@ -159,9 +231,7 @@ function getWorkflowStepOptionLevels(params: {
|
|||||||
selectedOption,
|
selectedOption,
|
||||||
});
|
});
|
||||||
|
|
||||||
const nextOptions = getRenderableAgentWorkflowOptionTree(
|
const nextOptions = selectedOption?.children || [];
|
||||||
selectedOption?.children || []
|
|
||||||
);
|
|
||||||
if (!selectedOption || nextOptions.length === 0) {
|
if (!selectedOption || nextOptions.length === 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -181,13 +251,13 @@ function getWorkflowOptionDepthShortLabel(depth: number) {
|
|||||||
return depth === 1 ? "主" : `${depth}级`;
|
return depth === 1 ? "主" : `${depth}级`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StepOptionCard({
|
const StepOptionCard = memo(function StepOptionCard({
|
||||||
option,
|
option,
|
||||||
step,
|
step,
|
||||||
selected,
|
selected,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
disabledReason = null,
|
disabledReason = null,
|
||||||
onClick,
|
onSelect,
|
||||||
index,
|
index,
|
||||||
}: {
|
}: {
|
||||||
option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null };
|
option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null };
|
||||||
@ -195,19 +265,18 @@ function StepOptionCard({
|
|||||||
selected: boolean;
|
selected: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
disabledReason?: "blocked" | "developing" | null;
|
disabledReason?: "blocked" | "developing" | null;
|
||||||
onClick: () => void;
|
onSelect: (optionKey: string) => void;
|
||||||
index: number;
|
index: number;
|
||||||
}) {
|
}) {
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onSelect(option.key);
|
||||||
|
}, [onSelect, option.key]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={handleClick}
|
||||||
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 }}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"group relative overflow-hidden rounded-2xl text-left",
|
"group relative overflow-hidden rounded-2xl text-left",
|
||||||
"aspect-[16/10] w-full",
|
"aspect-[16/10] w-full",
|
||||||
@ -227,6 +296,9 @@ function StepOptionCard({
|
|||||||
<img
|
<img
|
||||||
src={option.previewImageUrl}
|
src={option.previewImageUrl}
|
||||||
alt={option.buttonName}
|
alt={option.buttonName}
|
||||||
|
loading={selected || index < 4 ? "eager" : "lazy"}
|
||||||
|
decoding="async"
|
||||||
|
draggable={false}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full object-cover transition-transform duration-700",
|
"h-full w-full object-cover transition-transform duration-700",
|
||||||
!disabled && "group-hover:scale-110"
|
!disabled && "group-hover:scale-110"
|
||||||
@ -243,13 +315,9 @@ function StepOptionCard({
|
|||||||
|
|
||||||
{/* 选中标记 */}
|
{/* 选中标记 */}
|
||||||
{selected && (
|
{selected && (
|
||||||
<motion.div
|
<div className="absolute right-3 top-3 flex h-6 w-6 items-center justify-center rounded-full bg-white text-black">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Check className="h-4 w-4" />
|
<Check className="h-4 w-4" />
|
||||||
</motion.div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{disabled && !selected ? (
|
{disabled && !selected ? (
|
||||||
@ -281,10 +349,137 @@ function StepOptionCard({
|
|||||||
{selected && (
|
{selected && (
|
||||||
<div className="absolute inset-0 rounded-2xl ring-2 ring-inset ring-white/30" />
|
<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({
|
export function AgentWorkflowBrandStorePanel({
|
||||||
config,
|
config,
|
||||||
inputs,
|
inputs,
|
||||||
@ -571,7 +766,7 @@ export function AgentWorkflowBrandStorePanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentWorkflowPanelDarkV2({
|
function AgentWorkflowPanelDarkV2Component({
|
||||||
config,
|
config,
|
||||||
inputs,
|
inputs,
|
||||||
onInputsChange,
|
onInputsChange,
|
||||||
@ -614,53 +809,131 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
() => getAgentWorkflowPageFeatureSettings(config),
|
() => getAgentWorkflowPageFeatureSettings(config),
|
||||||
[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(
|
const flattenedResolvedSteps = useMemo(
|
||||||
() =>
|
() =>
|
||||||
flattenVisibleAgentWorkflowSteps(displayConfig.steps, selections).filter(
|
flattenVisibleAgentWorkflowSteps(displayConfig.steps, optimisticSelections).filter(
|
||||||
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
||||||
),
|
),
|
||||||
[displayConfig.steps, selections]
|
[displayConfig.steps, optimisticSelections]
|
||||||
);
|
);
|
||||||
|
|
||||||
const summary = useMemo(
|
const [workflowDerivedState, setWorkflowDerivedState] =
|
||||||
() => buildSelectionSummary({ config: resolvedConfig, selections }),
|
useState<WorkflowPanelDerivedState>(() =>
|
||||||
[resolvedConfig, selections]
|
buildWorkflowPanelDerivedState({
|
||||||
);
|
|
||||||
|
|
||||||
const composerContext = useMemo(
|
|
||||||
() =>
|
|
||||||
buildAgentWorkflowComposerContext({
|
|
||||||
config: resolvedConfig,
|
config: resolvedConfig,
|
||||||
inputs,
|
inputs,
|
||||||
materials,
|
materials,
|
||||||
selections,
|
selections: deferredSelections,
|
||||||
}),
|
})
|
||||||
[inputs, materials, resolvedConfig, selections]
|
);
|
||||||
);
|
const workflowDerivedVersionRef = useRef(0);
|
||||||
|
|
||||||
const progress = useMemo(
|
useEffect(
|
||||||
() => getAgentWorkflowProgress({ config: resolvedConfig, selections }),
|
() => {
|
||||||
[resolvedConfig, selections]
|
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 stepSelectionSummaryMap = useMemo(() => {
|
||||||
const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map(({ step }) => [
|
const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map(({ step }) => [
|
||||||
step.key,
|
step.key,
|
||||||
getWorkflowStepSelectionSummary({
|
getWorkflowStepSelectionSummary({
|
||||||
step,
|
step,
|
||||||
selectionValue: selections[step.key],
|
selectionValue: deferredSelections[step.key],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
return Object.fromEntries(entries) as Record<string, string>;
|
return Object.fromEntries(entries) as Record<string, string>;
|
||||||
}, [resolvedConfig.steps, selections]);
|
}, [deferredSelections, resolvedConfig.steps]);
|
||||||
const blockedOptionKeys = useMemo(
|
const blockedOptionKeys = useMemo(
|
||||||
() =>
|
() =>
|
||||||
new Set(
|
new Set(
|
||||||
getAgentWorkflowBlockedOptionKeysForSelections({
|
getAgentWorkflowBlockedOptionKeysForSelections({
|
||||||
config: resolvedConfig,
|
config: resolvedConfig,
|
||||||
selections,
|
selections: deferredSelections,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
[resolvedConfig, selections]
|
[deferredSelections, resolvedConfig]
|
||||||
);
|
);
|
||||||
const [autoAdvanceNotice, setAutoAdvanceNotice] = useState<{
|
const [autoAdvanceNotice, setAutoAdvanceNotice] = useState<{
|
||||||
completedStepTitle: string;
|
completedStepTitle: string;
|
||||||
@ -681,20 +954,20 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
activeEmbeddedStepEntry
|
activeEmbeddedStepEntry
|
||||||
? getWorkflowStepOptionLevels({
|
? getWorkflowStepOptionLevels({
|
||||||
step: activeEmbeddedStepEntry.step,
|
step: activeEmbeddedStepEntry.step,
|
||||||
selectionValue: selections[activeEmbeddedStepEntry.step.key],
|
selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key],
|
||||||
})
|
})
|
||||||
: [],
|
: [],
|
||||||
[activeEmbeddedStepEntry, selections]
|
[activeEmbeddedStepEntry, optimisticSelections]
|
||||||
);
|
);
|
||||||
const activeEmbeddedSelectedPath = useMemo(
|
const activeEmbeddedSelectedPath = useMemo(
|
||||||
() =>
|
() =>
|
||||||
activeEmbeddedStepEntry
|
activeEmbeddedStepEntry
|
||||||
? getAgentWorkflowSelectedOptionPath({
|
? getAgentWorkflowSelectedOptionPath({
|
||||||
step: activeEmbeddedStepEntry.step,
|
step: activeEmbeddedStepEntry.step,
|
||||||
selectionValue: selections[activeEmbeddedStepEntry.step.key],
|
selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key],
|
||||||
})
|
})
|
||||||
: [],
|
: [],
|
||||||
[activeEmbeddedStepEntry, selections]
|
[activeEmbeddedStepEntry, optimisticSelections]
|
||||||
);
|
);
|
||||||
const activeEmbeddedOptionLevel =
|
const activeEmbeddedOptionLevel =
|
||||||
activeEmbeddedOptionLevels.find((level) => level.depth === activeEmbeddedOptionDepth) ||
|
activeEmbeddedOptionLevels.find((level) => level.depth === activeEmbeddedOptionDepth) ||
|
||||||
@ -713,9 +986,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderableChildren = getRenderableAgentWorkflowOptionTree(
|
const renderableChildren = activeEmbeddedDeepestSelectedOption.children || [];
|
||||||
activeEmbeddedDeepestSelectedOption.children || []
|
|
||||||
);
|
|
||||||
if (renderableChildren.length === 0) {
|
if (renderableChildren.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -783,7 +1054,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
|
|
||||||
const currentStepComplete = isAgentWorkflowStepSelectionComplete({
|
const currentStepComplete = isAgentWorkflowStepSelectionComplete({
|
||||||
step: activeEmbeddedStepEntry.step,
|
step: activeEmbeddedStepEntry.step,
|
||||||
selectionValue: selections[activeEmbeddedStepEntry.step.key],
|
selectionValue: optimisticSelections[activeEmbeddedStepEntry.step.key],
|
||||||
});
|
});
|
||||||
if (!currentStepComplete) {
|
if (!currentStepComplete) {
|
||||||
return;
|
return;
|
||||||
@ -800,7 +1071,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
({ step }) =>
|
({ step }) =>
|
||||||
!isAgentWorkflowStepSelectionComplete({
|
!isAgentWorkflowStepSelectionComplete({
|
||||||
step,
|
step,
|
||||||
selectionValue: selections[step.key],
|
selectionValue: optimisticSelections[step.key],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -824,7 +1095,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
embedded,
|
embedded,
|
||||||
flattenedResolvedSteps,
|
flattenedResolvedSteps,
|
||||||
pendingAutoAdvanceStepKey,
|
pendingAutoAdvanceStepKey,
|
||||||
selections,
|
optimisticSelections,
|
||||||
stepSelectionSummaryMap,
|
stepSelectionSummaryMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -848,15 +1119,42 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
): ReactNode => {
|
): ReactNode => {
|
||||||
const selectedPathKeys = getAgentWorkflowSelectedOptionPath({
|
const selectedPathKeys = getAgentWorkflowSelectedOptionPath({
|
||||||
step,
|
step,
|
||||||
selectionValue: selections[step.key],
|
selectionValue: optimisticSelections[step.key],
|
||||||
}).map((option) => option.key);
|
}).map((option) => option.key);
|
||||||
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
|
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
|
||||||
const currentSelectedOption =
|
const currentSelectedOption =
|
||||||
options.find((option) => option.key === currentSelectedKey) || null;
|
options.find((option) => option.key === currentSelectedKey) || null;
|
||||||
const visibleChildren = getRenderableAgentWorkflowOptionTree(
|
const visibleChildren = currentSelectedOption?.children || [];
|
||||||
currentSelectedOption?.children || []
|
|
||||||
);
|
|
||||||
const levelLabel = depth === 1 ? "主卡片" : depth === 2 ? "二级卡片" : "三级卡片";
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@ -871,50 +1169,13 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
<VirtualizedOptionGrid
|
||||||
{options.map((option, optionIndex) => {
|
blockedOptionKeys={blockedOptionKeys}
|
||||||
const isDeveloping = option.enabled === false;
|
onSelect={handleSelectOption}
|
||||||
const isBlocked =
|
options={options}
|
||||||
!isDeveloping &&
|
selectedKey={currentSelectedKey}
|
||||||
currentSelectedKey !== option.key &&
|
step={step}
|
||||||
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>
|
|
||||||
|
|
||||||
{currentSelectedOption && visibleChildren.length > 0
|
{currentSelectedOption && visibleChildren.length > 0
|
||||||
? renderOptionLevels(step, visibleChildren, depth + 1, [
|
? 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 = (
|
const workflowContent = (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{showBrandStoreSection ? (
|
{showBrandStoreSection ? (
|
||||||
@ -979,17 +1296,17 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
>
|
>
|
||||||
<div className="mb-5 space-y-4">
|
<div className="mb-5 space-y-4">
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5">
|
||||||
{flattenedResolvedSteps.map(({ step }, stepIndex) => {
|
{flattenedResolvedSteps.map(({ step }, stepIndex) => {
|
||||||
const completed = isAgentWorkflowStepSelectionComplete({
|
const completed = isAgentWorkflowStepSelectionComplete({
|
||||||
step,
|
step,
|
||||||
selectionValue: selections[step.key],
|
selectionValue: optimisticSelections[step.key],
|
||||||
});
|
});
|
||||||
const active = step.key === activeEmbeddedStepEntry.step.key;
|
const active = step.key === activeEmbeddedStepEntry.step.key;
|
||||||
const stepSelectedPath = getAgentWorkflowSelectedOptionPath({
|
const stepSelectedPath = getAgentWorkflowSelectedOptionPath({
|
||||||
step,
|
step,
|
||||||
selectionValue: selections[step.key],
|
selectionValue: optimisticSelections[step.key],
|
||||||
});
|
});
|
||||||
const primarySelectedOption = stepSelectedPath[0] || null;
|
const primarySelectedOption = stepSelectedPath[0] || null;
|
||||||
const selectedCountLabel =
|
const selectedCountLabel =
|
||||||
stepSelectedPath.length > 0
|
stepSelectedPath.length > 0
|
||||||
@ -1186,72 +1503,13 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
<VirtualizedOptionGrid
|
||||||
{activeEmbeddedOptionLevel.options.map((option, optionIndex) => {
|
blockedOptionKeys={blockedOptionKeys}
|
||||||
const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey;
|
onSelect={handleEmbeddedOptionSelect}
|
||||||
const isDeveloping = option.enabled === false;
|
options={activeEmbeddedOptionLevel.options}
|
||||||
const isBlocked =
|
selectedKey={activeEmbeddedOptionLevel.selectedKey}
|
||||||
!isDeveloping &&
|
step={activeEmbeddedStepEntry.step}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</motion.section>
|
</motion.section>
|
||||||
@ -1287,7 +1545,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
{step.description && (
|
{step.description && (
|
||||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||||
)}
|
)}
|
||||||
{renderOptionLevels(step, getRenderableAgentWorkflowOptionTree(step.options || []))}
|
{renderOptionLevels(step, step.options || [])}
|
||||||
</motion.section>
|
</motion.section>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@ -1439,3 +1697,5 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
</div>
|
</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(
|
export function getEnabledAgentWorkflowStepTree(
|
||||||
steps: AgentWorkflowStepUiConfig[]
|
steps: AgentWorkflowStepUiConfig[]
|
||||||
): AgentWorkflowStepUiConfig[] {
|
): AgentWorkflowStepUiConfig[] {
|
||||||
return [...steps]
|
const cached = enabledStepTreeCache.get(steps);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSteps = [...steps]
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||||
.filter((step) => step.enabled !== false)
|
.filter((step) => step.enabled !== false)
|
||||||
.map((step) => ({
|
.map((step) => ({
|
||||||
@ -515,23 +566,37 @@ export function getEnabledAgentWorkflowStepTree(
|
|||||||
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
||||||
}))
|
}))
|
||||||
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
||||||
|
enabledStepTreeCache.set(steps, nextSteps);
|
||||||
|
return nextSteps;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRenderableAgentWorkflowOptionTree(
|
export function getRenderableAgentWorkflowOptionTree(
|
||||||
options: AgentWorkflowOptionUiConfig[]
|
options: AgentWorkflowOptionUiConfig[]
|
||||||
): AgentWorkflowOptionUiConfig[] {
|
): AgentWorkflowOptionUiConfig[] {
|
||||||
return [...options]
|
const cached = renderableOptionTreeCache.get(options);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextOptions = [...options]
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||||
.map((option) => ({
|
.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
children: getRenderableAgentWorkflowOptionTree(option.children || []),
|
children: getRenderableAgentWorkflowOptionTree(option.children || []),
|
||||||
}));
|
}));
|
||||||
|
renderableOptionTreeCache.set(options, nextOptions);
|
||||||
|
return nextOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRenderableAgentWorkflowStepTree(
|
export function getRenderableAgentWorkflowStepTree(
|
||||||
steps: AgentWorkflowStepUiConfig[]
|
steps: AgentWorkflowStepUiConfig[]
|
||||||
): AgentWorkflowStepUiConfig[] {
|
): AgentWorkflowStepUiConfig[] {
|
||||||
return [...steps]
|
const cached = renderableStepTreeCache.get(steps);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextSteps = [...steps]
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||||
.filter((step) => step.enabled !== false)
|
.filter((step) => step.enabled !== false)
|
||||||
.map((step) => ({
|
.map((step) => ({
|
||||||
@ -544,12 +609,8 @@ export function getRenderableAgentWorkflowStepTree(
|
|||||||
getEnabledAgentWorkflowOptionTree(step.options || []).length > 0 ||
|
getEnabledAgentWorkflowOptionTree(step.options || []).length > 0 ||
|
||||||
(step.children || []).length > 0
|
(step.children || []).length > 0
|
||||||
);
|
);
|
||||||
}
|
renderableStepTreeCache.set(steps, nextSteps);
|
||||||
|
return nextSteps;
|
||||||
export interface FlattenedAgentWorkflowOption {
|
|
||||||
option: AgentWorkflowOptionUiConfig;
|
|
||||||
depth: number;
|
|
||||||
parentKeys: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function flattenAgentWorkflowOptionTree(
|
export function flattenAgentWorkflowOptionTree(
|
||||||
@ -557,7 +618,15 @@ export function flattenAgentWorkflowOptionTree(
|
|||||||
depth = 1,
|
depth = 1,
|
||||||
parentKeys: string[] = []
|
parentKeys: string[] = []
|
||||||
): FlattenedAgentWorkflowOption[] {
|
): 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,
|
option,
|
||||||
depth,
|
depth,
|
||||||
@ -569,6 +638,10 @@ export function flattenAgentWorkflowOptionTree(
|
|||||||
[...parentKeys, option.key]
|
[...parentKeys, option.key]
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
if (canUseRootCache) {
|
||||||
|
flattenedOptionTreeCache.set(options, flattened);
|
||||||
|
}
|
||||||
|
return flattened;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function countAgentWorkflowOptionTree(
|
export function countAgentWorkflowOptionTree(
|
||||||
@ -600,13 +673,20 @@ export function normalizeAgentWorkflowOptionOrdering(
|
|||||||
export function getEnabledAgentWorkflowOptionTree(
|
export function getEnabledAgentWorkflowOptionTree(
|
||||||
options: AgentWorkflowOptionUiConfig[]
|
options: AgentWorkflowOptionUiConfig[]
|
||||||
): AgentWorkflowOptionUiConfig[] {
|
): AgentWorkflowOptionUiConfig[] {
|
||||||
return [...options]
|
const cached = enabledOptionTreeCache.get(options);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextOptions = [...options]
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||||
.filter((option) => option.enabled !== false)
|
.filter((option) => option.enabled !== false)
|
||||||
.map((option) => ({
|
.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
children: getEnabledAgentWorkflowOptionTree(option.children || []),
|
children: getEnabledAgentWorkflowOptionTree(option.children || []),
|
||||||
}));
|
}));
|
||||||
|
enabledOptionTreeCache.set(options, nextOptions);
|
||||||
|
return nextOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findAgentWorkflowOptionByKey(
|
export function findAgentWorkflowOptionByKey(
|
||||||
@ -1533,9 +1613,10 @@ export function getAgentWorkflowSelectedOptionPath(params: {
|
|||||||
let currentOptions = params.step.options || [];
|
let currentOptions = params.step.options || [];
|
||||||
|
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const matchedOption = currentOptions.find(
|
const matchedOption = getWorkflowOptionLookup(currentOptions).get(key);
|
||||||
(option) => option.key === key && option.enabled !== false
|
if (matchedOption?.enabled === false) {
|
||||||
);
|
break;
|
||||||
|
}
|
||||||
if (!matchedOption) break;
|
if (!matchedOption) break;
|
||||||
selectedOptions.push(matchedOption);
|
selectedOptions.push(matchedOption);
|
||||||
currentOptions = matchedOption.children || [];
|
currentOptions = matchedOption.children || [];
|
||||||
|
|||||||
@ -60,6 +60,7 @@
|
|||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.5.0",
|
||||||
"@sparkjsdev/spark": "^0.1.10",
|
"@sparkjsdev/spark": "^0.1.10",
|
||||||
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@types/form-data": "^2.5.2",
|
"@types/form-data": "^2.5.2",
|
||||||
"@vercel/analytics": "^1.3.1",
|
"@vercel/analytics": "^1.3.1",
|
||||||
"@vercel/blob": "^0.24.1",
|
"@vercel/blob": "^0.24.1",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -128,6 +128,9 @@ importers:
|
|||||||
'@sparkjsdev/spark':
|
'@sparkjsdev/spark':
|
||||||
specifier: ^0.1.10
|
specifier: ^0.1.10
|
||||||
version: 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':
|
'@types/form-data':
|
||||||
specifier: ^2.5.2
|
specifier: ^2.5.2
|
||||||
version: 2.5.2
|
version: 2.5.2
|
||||||
@ -3073,6 +3076,15 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
|
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':
|
'@tediousjs/connection-string@0.6.0':
|
||||||
resolution: {integrity: sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow==}
|
resolution: {integrity: sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow==}
|
||||||
|
|
||||||
@ -9450,6 +9462,14 @@ snapshots:
|
|||||||
postcss-selector-parser: 6.0.10
|
postcss-selector-parser: 6.0.10
|
||||||
tailwindcss: 4.1.16
|
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': {}
|
'@tediousjs/connection-string@0.6.0': {}
|
||||||
|
|
||||||
'@tokenlens/core@1.3.0': {}
|
'@tokenlens/core@1.3.0': {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user