fix agent workflow prompt state sync
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-03-26 19:00:37 +08:00
parent 243e12cc0e
commit c16862bd90
4 changed files with 223 additions and 209 deletions

View File

@ -74,6 +74,7 @@ import {
AgentWorkflowBrandStorePanel,
AgentWorkflowPanelDarkV2,
} from "@/components/agents/AgentWorkflowPanelDarkV2";
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
import { AgentGenerationWorkspaceDarkV2 } from "@/components/agents/AgentGenerationWorkspaceDarkV2";
import { AgentProjectListSheet } from "@/components/agents/AgentProjectListSheet";
import { CanvasWorkspace } from "@/components/canvas";
@ -86,7 +87,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { cn, fetcher } from "@/lib/utils";
import {
defaultAgentWorkflowUiConfig,
flattenVisibleAgentWorkflowSteps,
normalizeAgentWorkflowUiConfig,
sanitizeAgentWorkflowSelections,
} from "@/lib/agents/workflow-ui-config";
import {
createEmptyAgentWorkflowMaterialSelections,
@ -411,6 +414,18 @@ export default function CanvasEditorPage({ params }: PageProps) {
isAgentWorkspaceEnabled && requestedAgentMode;
const hasLoadedCurrentAgentProject =
!requestedAgentMode || !agentProjectId || loadedAgentProjectId === agentProjectId;
const resolvedWorkflowUiConfig = useMemo(
() => normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig),
[siteSettingsData?.agentWorkflowUiConfig]
);
const sanitizedWorkflowSelections = useMemo(
() =>
sanitizeAgentWorkflowSelections({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
selections: workflowSelections,
}),
[resolvedWorkflowUiConfig, workflowSelections]
);
const resolvedBrandModelConfig = useMemo(
() => normalizeAgentWorkflowBrandModelConfig(siteSettingsData?.agentWorkflowBrandModelConfig),
[siteSettingsData?.agentWorkflowBrandModelConfig]
@ -474,7 +489,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
selections: workflowSelections,
selections: sanitizedWorkflowSelections,
selectedBrandModel,
selectedAgentPresetId: selectedAgentPresetId || null,
currentStage: currentAgentStage,
@ -487,7 +502,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
selectedBrandModel,
workflowInputs,
workflowMaterials,
workflowSelections,
sanitizedWorkflowSelections,
]);
const currentWorkflowPersistedStateRef = useRef<AgentWorkflowPersistedState>(
buildCurrentWorkflowPersistedState()
@ -658,7 +673,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
priceBand: workflowInputs.priceBand.trim(),
},
materials: workflowMaterials,
selections: workflowSelections,
selections: sanitizedWorkflowSelections,
selectedBrandModel,
selectedAgentPresetId: selectedAgentPresetId || null,
currentStage: currentAgentStage,
@ -715,7 +730,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
selectedBrandModel,
workflowInputs,
workflowMaterials,
workflowSelections,
sanitizedWorkflowSelections,
]);
const persistedAgentProjectStateRef = useRef(buildPersistedAgentProjectState());
@ -781,7 +796,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
JSON.stringify({
inputs: workflowInputs,
materials: workflowMaterials,
selections: workflowSelections,
selections: sanitizedWorkflowSelections,
selectedBrandModel,
selectedAgentPresetId,
currentAgentStage,
@ -796,7 +811,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
workflowHydrationSource,
workflowInputs,
workflowMaterials,
workflowSelections,
sanitizedWorkflowSelections,
workflowStorageKey,
selectedBrandModel,
selectedAgentPresetId,
@ -841,48 +856,6 @@ export default function CanvasEditorPage({ params }: PageProps) {
]
);
useEffect(() => {
if (!requestedAgentMode || !isAgentWorkspaceEnabled || agentProjectId) return;
let cancelled = false;
const createAgentProject = async () => {
setIsAgentProjectLoading(true);
try {
const result = await agentProjectService.createProject({
title: buildDefaultAgentProjectTitle(),
projectState: createEmptyAgentGenerationProjectPersistedState(),
});
if (!result.success || !result.data) {
throw new Error(result.error || "创建生图项目失败");
}
if (cancelled) return;
toast.success("已为你创建新项目,可直接开始生图");
updateCanvasSearchParams({
mode: "agent",
agentProjectId: result.data.id,
});
} catch (error) {
if (!cancelled) {
toast.error(error instanceof Error ? error.message : "创建生图项目失败");
}
} finally {
if (!cancelled) {
setIsAgentProjectLoading(false);
}
}
};
void createAgentProject();
return () => {
cancelled = true;
};
}, [
agentProjectId,
isAgentWorkspaceEnabled,
requestedAgentMode,
updateCanvasSearchParams,
]);
useEffect(() => {
if (!requestedAgentMode || !agentProjectId) return;
@ -1132,16 +1105,11 @@ export default function CanvasEditorPage({ params }: PageProps) {
agentProjectId: nextProject.id,
});
} else {
const createResult = await agentProjectService.createProject({
title: buildDefaultAgentProjectTitle(),
projectState: createEmptyAgentGenerationProjectPersistedState(),
updateCanvasSearchParams({
mode: "agent",
agentProjectId: null,
});
if (createResult.success && createResult.data) {
updateCanvasSearchParams({
mode: "agent",
agentProjectId: createResult.data.id,
});
}
setIsAgentProjectSheetOpen(true);
}
}
@ -1705,10 +1673,16 @@ export default function CanvasEditorPage({ params }: PageProps) {
};
}, []);
const resolvedWorkflowUiConfig = useMemo(
() => normalizeAgentWorkflowUiConfig(siteSettingsData?.agentWorkflowUiConfig),
[siteSettingsData?.agentWorkflowUiConfig]
);
useEffect(() => {
const currentSerialized = JSON.stringify(workflowSelections);
const sanitizedSerialized = JSON.stringify(sanitizedWorkflowSelections);
if (currentSerialized === sanitizedSerialized) {
return;
}
workflowSelectionsRef.current = sanitizedWorkflowSelections;
setWorkflowSelections(sanitizedWorkflowSelections);
}, [sanitizedWorkflowSelections, workflowSelections]);
const workflowContext = useMemo<AgentWorkflowComposerContext | null>(() => {
return buildAgentWorkflowComposerContext({
@ -1716,14 +1690,14 @@ export default function CanvasEditorPage({ params }: PageProps) {
inputs: workflowInputs,
materials: workflowMaterials,
selectedBrandModel,
selections: workflowSelections,
selections: sanitizedWorkflowSelections,
});
}, [
resolvedWorkflowUiConfig,
selectedBrandModel,
sanitizedWorkflowSelections,
workflowInputs,
workflowMaterials,
workflowSelections,
]);
useEffect(() => {
workflowInputsRef.current = workflowInputs;
@ -1732,8 +1706,8 @@ export default function CanvasEditorPage({ params }: PageProps) {
workflowMaterialsRef.current = workflowMaterials;
}, [workflowMaterials]);
useEffect(() => {
workflowSelectionsRef.current = workflowSelections;
}, [workflowSelections]);
workflowSelectionsRef.current = sanitizedWorkflowSelections;
}, [sanitizedWorkflowSelections]);
useEffect(() => {
selectedBrandModelRef.current = selectedBrandModel;
}, [selectedBrandModel]);
@ -1747,10 +1721,39 @@ export default function CanvasEditorPage({ params }: PageProps) {
}, []);
const handleWorkflowSelectionsChange = useCallback(
(next: Partial<Record<AgentWorkflowStepKey, string>>) => {
workflowSelectionsRef.current = next;
setWorkflowSelections(next);
const normalizedCurrentSelections = sanitizeAgentWorkflowSelections({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
selections: workflowSelectionsRef.current,
});
const normalizedNextSelections = sanitizeAgentWorkflowSelections({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
selections: next,
});
const orderedStepKeys = flattenVisibleAgentWorkflowSteps(
(resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig).steps,
normalizedCurrentSelections
).map(({ step }) => step.key);
const earliestChangedIndex = orderedStepKeys.findIndex((stepKey) => {
return String(normalizedCurrentSelections[stepKey] || "") !== String(normalizedNextSelections[stepKey] || "");
});
let clearedSelections = { ...normalizedNextSelections };
if (earliestChangedIndex >= 0) {
orderedStepKeys.slice(earliestChangedIndex + 1).forEach((stepKey) => {
delete clearedSelections[stepKey];
});
}
clearedSelections = sanitizeAgentWorkflowSelections({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
selections: clearedSelections,
});
workflowSelectionsRef.current = clearedSelections;
setWorkflowSelections(clearedSelections);
},
[]
[resolvedWorkflowUiConfig]
);
const handleSelectedBrandModelChange = useCallback(
(next: AgentWorkflowSelectedBrandModel | null) => {
@ -1802,12 +1805,14 @@ export default function CanvasEditorPage({ params }: PageProps) {
() =>
getAgentWorkflowProgress({
config: resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig,
selections: workflowSelections,
selections: sanitizedWorkflowSelections,
}),
[resolvedWorkflowUiConfig, workflowSelections]
[resolvedWorkflowUiConfig, sanitizedWorkflowSelections]
);
const resolvedAgentProjectTitle =
String(agentProjectTitle || "").trim() || "未命名生图项目";
hasActiveAgentProject
? String(agentProjectTitle || "").trim() || "未命名生图项目"
: "未选择项目";
if (isLoading) {
return (
@ -1949,11 +1954,74 @@ export default function CanvasEditorPage({ params }: PageProps) {
</div>
) : null;
const agentProjectSelectionScreen = isAgentMode && !hasActiveAgentProject ? (
<div className="min-h-screen bg-[#050607] text-white">
<AgentCanvasTopNavDark
siteName={siteSettingsData?.siteName}
siteLogo={siteSettingsData?.siteLogo}
siteLogoDark={siteSettingsData?.siteLogoDark}
rightSlot={agentProjectNavSlot}
activeKey="agent"
/>
<main className="flex min-h-screen items-center justify-center px-6 pt-24 pb-8">
<div className="w-full max-w-[860px] rounded-[36px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.02)_100%)] p-8 shadow-[0_24px_80px_rgba(0,0,0,0.35)] md:p-10">
<div className="mx-auto max-w-[620px] text-center">
<div className="mx-auto flex h-16 w-16 items-center justify-center rounded-3xl border border-emerald-400/20 bg-emerald-500/[0.10] text-emerald-200">
<FolderOpen className="h-8 w-8" />
</div>
<h1 className="mt-6 text-3xl font-semibold tracking-[0.02em] text-white md:text-4xl">
先选择一个生图项目
</h1>
<p className="mt-4 text-sm leading-7 text-white/55 md:text-base">
打开历史项目可以继续沿用之前的生图结果、模式和工作流。只有在你明确点击新建项目时,系统才会创建一个全新的智能体项目。
</p>
</div>
<div className="mt-8 grid gap-4 md:grid-cols-2">
<button
type="button"
onClick={() => setIsAgentProjectSheetOpen(true)}
className="rounded-[28px] border border-white/10 bg-black/30 p-5 text-left transition-colors hover:border-white/18 hover:bg-white/[0.04]"
>
<div className="flex items-center gap-3 text-emerald-200">
<Grid3X3 className="h-5 w-5" />
<span className="text-sm font-medium">打开历史项目</span>
</div>
<div className="mt-3 text-sm leading-6 text-white/55">
从项目列表里继续昨天或今天的智能体项目,避免生成结果分散在多个空项目中。
</div>
</button>
<button
type="button"
onClick={() => {
void handleCreateAgentProject();
}}
className="rounded-[28px] border border-emerald-400/20 bg-emerald-500/[0.10] p-5 text-left transition-colors hover:border-emerald-300/35 hover:bg-emerald-500/[0.16]"
>
<div className="flex items-center gap-3 text-emerald-100">
<Plus className="h-5 w-5" />
<span className="text-sm font-medium">新建空白项目</span>
</div>
<div className="mt-3 text-sm leading-6 text-emerald-50/70">
仅在你要开始一条全新的场景生图流程时再创建,避免项目列表里自动冒出一堆无效项目。
</div>
</button>
</div>
</div>
</main>
</div>
) : null;
if (isAgentMode) {
if (!hasActiveAgentProject) {
return agentProjectSelectionScreen;
}
return (
<AgentGenerationWorkspaceDarkV2
chatId={agentModeChatId}
projectId={agentProjectId || id}
projectId={agentProjectId}
selectedAgentPresetId={isAgentWorkspaceEnabled ? selectedAgentPresetId : null}
selectedBrandModel={selectedBrandModel}
workflowContext={workflowContext}
@ -1994,7 +2062,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
onInputsChange={handleWorkflowInputsChange}
materials={workflowMaterials}
onMaterialsChange={handleWorkflowMaterialsChange}
selections={workflowSelections}
selections={sanitizedWorkflowSelections}
onSelectionsChange={handleWorkflowSelectionsChange}
showBrandStoreSection={false}
/>

View File

@ -22,6 +22,7 @@ import {
Download,
ImagePlus,
Loader2,
Maximize2,
Sparkles,
Upload,
UserRound,
@ -144,11 +145,6 @@ interface GeneratedImageGroup {
text: string;
}
interface SceneResultToolbarTarget {
url: string;
index: number;
}
interface AgentModeReferenceSearchResponse {
success?: boolean;
data?: {
@ -730,6 +726,7 @@ interface LocalGenerationPendingState {
const AGENT_PENDING_GENERATION_STORAGE_KEY = "ai-chatbot-agent-pending-generations";
const AGENT_PENDING_GENERATION_EXPIRY_MS = 30 * 60 * 1000;
const AGENT_SCENE_PENDING_FAILURE_GRACE_MS = 3 * 60 * 1000;
function getStoredAgentPendingGenerations(): LocalGenerationPendingState[] {
if (typeof window === "undefined") {
@ -942,8 +939,6 @@ export function AgentGenerationWorkspaceDarkV2({
const [activeResultModeType, setActiveResultModeType] =
useState<AgentGenerationResultModeType>("scene_generation");
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
const [sceneResultToolbarTarget, setSceneResultToolbarTarget] =
useState<SceneResultToolbarTarget | null>(null);
const [sceneResizeEditorTargetUrl, setSceneResizeEditorTargetUrl] = useState<string | null>(null);
const [batchDownloadState, setBatchDownloadState] = useState<null | "scene" | "detail" | "sku">(null);
const [detailReplacementSearchState, setDetailReplacementSearchState] = useState<{
@ -2194,6 +2189,14 @@ export function AgentGenerationWorkspaceDarkV2({
? detailGenerationRunResults.length > 0
: latestReplacementResults.length > 0;
if (
pendingGeneration.modeType === "scene_generation" &&
!hasProgress &&
Date.now() - pendingGeneration.timestamp < AGENT_SCENE_PENDING_FAILURE_GRACE_MS
) {
return;
}
if (!hasProgress) {
setPendingGeneration(null);
}
@ -2226,17 +2229,6 @@ export function AgentGenerationWorkspaceDarkV2({
}
}, [activeResultModeUrls, selectedResultUrl]);
useEffect(() => {
if (
sceneResultToolbarTarget?.url &&
!normalizeResultCollectionUrls(mergedResultCollections.scene_generation).includes(
sceneResultToolbarTarget.url
)
) {
setSceneResultToolbarTarget(null);
}
}, [mergedResultCollections.scene_generation, sceneResultToolbarTarget]);
useEffect(() => {
if (!selectedAgentPresetId || !hasAgentUploadSlots) {
setIsAttachmentSlotExpanded(false);
@ -2642,7 +2634,6 @@ export function AgentGenerationWorkspaceDarkV2({
setActiveResultModeType("scene_generation");
setSelectedResultUrl(finalUrl);
setSceneResizeEditorTargetUrl(null);
setSceneResultToolbarTarget(null);
toast.success("尺寸调整结果已加入场景结果区");
},
[mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage]
@ -2660,9 +2651,6 @@ export function AgentGenerationWorkspaceDarkV2({
if (fullscreenPreview === normalizedUrl) {
setFullscreenPreview(null);
}
if (sceneResultToolbarTarget?.url === normalizedUrl) {
setSceneResultToolbarTarget(null);
}
if (sceneResizeEditorTargetUrl === normalizedUrl) {
setSceneResizeEditorTargetUrl(null);
}
@ -2670,13 +2658,7 @@ export function AgentGenerationWorkspaceDarkV2({
onDeleteResultImage(modeType, normalizedUrl);
toast.success("结果图已删除");
},
[
fullscreenPreview,
onDeleteResultImage,
sceneResizeEditorTargetUrl,
sceneResultToolbarTarget,
selectedResultUrl,
]
[fullscreenPreview, onDeleteResultImage, sceneResizeEditorTargetUrl, selectedResultUrl]
);
const handleToolStepChange = useCallback(
@ -5481,12 +5463,6 @@ export function AgentGenerationWorkspaceDarkV2({
onClick={() => {
if (!card.url) return;
setSelectedResultUrl(card.url);
if (isSceneResultCard) {
setSceneResultToolbarTarget({
url: card.url,
index: card.index,
});
}
}}
onDoubleClick={() => {
if (!card.url) return;
@ -5498,12 +5474,6 @@ export function AgentGenerationWorkspaceDarkV2({
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
setSelectedResultUrl(card.url);
if (isSceneResultCard) {
setSceneResultToolbarTarget({
url: card.url,
index: card.index,
});
}
}
}}
className={cn(
@ -5524,9 +5494,23 @@ export function AgentGenerationWorkspaceDarkV2({
loading="lazy"
/>
{isSceneResultCard ? (
<div className="pointer-events-none absolute inset-x-3 bottom-3 rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-[11px] text-white/78 backdrop-blur">
单击打开工具栏,双击查看大图
</div>
<>
<button
type="button"
onClick={(event) => {
event.stopPropagation();
setSelectedResultUrl(card.url!);
setSceneResizeEditorTargetUrl(card.url!);
}}
className="absolute left-3 top-3 flex h-9 w-9 items-center justify-center rounded-full border border-emerald-400/28 bg-black/60 text-emerald-100 transition-colors hover:border-emerald-300/45 hover:bg-emerald-500/[0.16]"
aria-label="调整尺寸"
>
<Maximize2 className="h-4 w-4" />
</button>
<div className="pointer-events-none absolute inset-x-3 bottom-3 rounded-2xl border border-white/10 bg-black/55 px-3 py-2 text-[11px] text-white/78 backdrop-blur">
双击查看大图
</div>
</>
) : null}
<button
type="button"
@ -5622,97 +5606,6 @@ export function AgentGenerationWorkspaceDarkV2({
</div>
</main>
<AnimatePresence>
{sceneResultToolbarTarget && activeResultModeType === "scene_generation" ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[9998] flex items-center justify-center bg-black/75 px-4 backdrop-blur-sm"
onClick={() => setSceneResultToolbarTarget(null)}
>
<motion.div
initial={{ opacity: 0, scale: 0.96, y: 16 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.96, y: 16 }}
transition={{ duration: 0.18 }}
className="w-full max-w-[760px] rounded-[30px] border border-white/12 bg-[#090c0d]/96 p-5 shadow-[0_30px_80px_rgba(0,0,0,0.45)]"
onClick={(event) => event.stopPropagation()}
>
<div className="flex items-start justify-between gap-4">
<div>
<div className="text-xs uppercase tracking-[0.28em] text-emerald-200/60">
场景图工具栏
</div>
<div className="mt-2 text-lg font-semibold text-white">
场景结果图 {sceneResultToolbarTarget.index + 1}
</div>
<div className="mt-1 text-sm text-white/42">
这里可以快速预览、下载,或直接进入尺寸调整弹窗。
</div>
</div>
<button
type="button"
onClick={() => setSceneResultToolbarTarget(null)}
className="flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/[0.03] text-white/72 transition-colors hover:border-white/20 hover:bg-white/[0.06]"
aria-label="关闭工具栏"
>
<X className="h-4 w-4" />
</button>
</div>
<div className="mt-5 overflow-hidden rounded-[26px] border border-white/10 bg-[linear-gradient(180deg,#050607_0%,#101417_100%)]">
<img
src={buildPreviewImageUrl(sceneResultToolbarTarget.url, 1280)}
alt={`场景结果图 ${sceneResultToolbarTarget.index + 1}`}
className="max-h-[58vh] w-full object-contain"
onError={(event) =>
handlePreviewImageLoadError(event, sceneResultToolbarTarget.url)
}
/>
</div>
<div className="mt-5 grid gap-3 sm:grid-cols-3">
<button
type="button"
onClick={() => {
setSelectedResultUrl(sceneResultToolbarTarget.url);
setSceneResultToolbarTarget(null);
setFullscreenPreview(sceneResultToolbarTarget.url);
}}
className="flex min-h-[56px] items-center justify-center gap-2 rounded-[20px] border border-white/10 bg-white/[0.04] px-4 text-sm font-medium text-white transition-colors hover:border-white/20 hover:bg-white/[0.07]"
>
<ImagePlus className="h-4 w-4" />
查看大图
</button>
<button
type="button"
onClick={() => {
void handleDownloadOriginalImage(sceneResultToolbarTarget.url);
}}
className="flex min-h-[56px] items-center justify-center gap-2 rounded-[20px] border border-white/10 bg-white/[0.04] px-4 text-sm font-medium text-white transition-colors hover:border-white/20 hover:bg-white/[0.07]"
>
<Download className="h-4 w-4" />
下载原图
</button>
<button
type="button"
onClick={() => {
setSelectedResultUrl(sceneResultToolbarTarget.url);
setSceneResizeEditorTargetUrl(sceneResultToolbarTarget.url);
setSceneResultToolbarTarget(null);
}}
className="flex min-h-[56px] items-center justify-center gap-2 rounded-[20px] border border-emerald-400/28 bg-emerald-500/[0.10] px-4 text-sm font-medium text-emerald-50 transition-colors hover:border-emerald-300/45 hover:bg-emerald-500/[0.16]"
>
<Settings2 className="h-4 w-4" />
尺寸调整
</button>
</div>
</motion.div>
</motion.div>
) : null}
</AnimatePresence>
{sceneResizeEditorTargetUrl ? (
<ImageAnnotator
key={`agent-scene-resize-${sceneResizeEditorTargetUrl}`}

View File

@ -93,6 +93,8 @@ function buildGroupedSelectionLines(params: {
selections: AgentWorkflowComposerSelection[];
valueKey: "optionName" | "promptFragment";
separator: string;
leafOnly?: boolean;
dedupeAcrossSteps?: boolean;
}) {
const grouped = new Map<
string,
@ -102,8 +104,16 @@ function buildGroupedSelectionLines(params: {
valueSet: Set<string>;
}
>();
const globalValueSet = new Set<string>();
params.selections.forEach((item) => {
const sourceSelections = params.leafOnly
? params.selections.filter((item, index, items) => {
const nextItem = items[index + 1];
return !nextItem || nextItem.stepKey !== item.stepKey;
})
: params.selections;
sourceSelections.forEach((item) => {
const rawValue =
params.valueKey === "promptFragment"
? item.promptFragment || item.optionName
@ -121,8 +131,14 @@ function buildGroupedSelectionLines(params: {
};
const dedupeKey = normalizedValue.toLowerCase();
if (params.dedupeAcrossSteps && globalValueSet.has(dedupeKey)) {
return;
}
if (!currentGroup.valueSet.has(dedupeKey)) {
currentGroup.valueSet.add(dedupeKey);
if (params.dedupeAcrossSteps) {
globalValueSet.add(dedupeKey);
}
currentGroup.values.push(normalizedValue);
}
@ -312,6 +328,7 @@ export function buildAgentWorkflowComposerContext(params: {
selections,
valueKey: "promptFragment",
separator: ";",
dedupeAcrossSteps: true,
})
);

View File

@ -701,6 +701,42 @@ export function getAgentWorkflowSelectedOptionPath(params: {
return selectedOptions;
}
export function sanitizeAgentWorkflowSelections(params: {
config?: AgentWorkflowUiConfig | null;
selections: Partial<Record<AgentWorkflowStepKey, string>>;
}) {
const config = params.config || defaultAgentWorkflowUiConfig;
const enabledSteps = flattenAgentWorkflowSteps(
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
).map(({ step }) => step);
const sanitized: Partial<Record<AgentWorkflowStepKey, string>> = {};
enabledSteps.forEach((step) => {
const rawSelectionValue = String(params.selections[step.key] || "").trim();
if (!rawSelectionValue) {
return;
}
const selectedOptions = getAgentWorkflowSelectedOptionPath({
step,
selectionValue: rawSelectionValue,
});
if (selectedOptions.length === 0) {
return;
}
const serialized = serializeAgentWorkflowSelectionPath(
selectedOptions.map((option) => option.key)
);
if (serialized) {
sanitized[step.key] = serialized;
}
});
return sanitized;
}
export function getAgentWorkflowBlockedOptionKeys(params: {
step: AgentWorkflowStepUiConfig;
selectionValue?: string | null;