feat: 支持工作流跨级互斥与开发中卡片态
This commit is contained in:
parent
d7d33ebc4a
commit
a197a21337
@ -28,6 +28,7 @@ import {
|
||||
Video,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
defaultAgentWorkflowConstraintSettings,
|
||||
AGENT_WORKFLOW_MAX_OPTION_DEPTH,
|
||||
AGENT_WORKFLOW_MAX_TREE_DEPTH,
|
||||
countAgentWorkflowOptions,
|
||||
@ -40,6 +41,7 @@ import {
|
||||
flattenAgentWorkflowOptionTree,
|
||||
getEnabledAgentWorkflowOptionTree,
|
||||
getAgentWorkflowCategoryLabels,
|
||||
getAgentWorkflowConstraintSettings,
|
||||
normalizeAgentWorkflowOptionOrdering,
|
||||
flattenAgentWorkflowSteps,
|
||||
normalizeAgentWorkflowStepOrdering,
|
||||
@ -913,6 +915,10 @@ export function AgentWorkflowSettingsClient({
|
||||
() => getAgentWorkflowCategoryLabels(config),
|
||||
[config]
|
||||
);
|
||||
const workflowConstraintSettings = useMemo(
|
||||
() => getAgentWorkflowConstraintSettings(config),
|
||||
[config]
|
||||
);
|
||||
const firstCategoryLabel = categoryLabels.firstCategory;
|
||||
const secondCategoryLabel = categoryLabels.secondCategory;
|
||||
const thirdCategoryLabel = categoryLabels.thirdCategory;
|
||||
@ -2339,7 +2345,9 @@ export function AgentWorkflowSettingsClient({
|
||||
);
|
||||
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
|
||||
const blockedCandidates = workflowOptionRelations.filter(
|
||||
(item) => item.key !== option.key && item.depth === depth
|
||||
(item) =>
|
||||
item.key !== option.key &&
|
||||
(workflowConstraintSettings.crossLevelMutualExclusionEnabled || item.depth === depth)
|
||||
);
|
||||
const selectedBlockedItems = (option.blockedOptionKeys || [])
|
||||
.map((blockedKey) => workflowOptionRelationLookup.get(blockedKey))
|
||||
@ -2393,6 +2401,7 @@ export function AgentWorkflowSettingsClient({
|
||||
</span>
|
||||
<Badge variant="outline">{levelLabel}</Badge>
|
||||
<Badge variant="secondary">{sortedChildren.length} 子卡片</Badge>
|
||||
{!option.enabled ? <Badge variant="secondary">前台显示开发中</Badge> : null}
|
||||
<span className="max-w-[240px] truncate text-sm font-medium">
|
||||
{option.buttonName || "未命名卡片"}
|
||||
</span>
|
||||
@ -2454,16 +2463,6 @@ export function AgentWorkflowSettingsClient({
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Switch
|
||||
checked={option.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
enabled: checked,
|
||||
}))
|
||||
}
|
||||
className="scale-75"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2560,11 +2559,31 @@ export function AgentWorkflowSettingsClient({
|
||||
className="resize-none text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start justify-between gap-4 rounded-lg border border-dashed bg-muted/20 p-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">前台禁用状态</Label>
|
||||
<p className="text-[11px] leading-5 text-muted-foreground">
|
||||
关闭后前端依然会展示这张卡片,但会标记为“开发中”且不可点击。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={option.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
enabled: checked,
|
||||
}))
|
||||
}
|
||||
className="mt-1 scale-90"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 rounded-lg border border-dashed bg-muted/20 p-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">互斥卡片限制</Label>
|
||||
<p className="text-[11px] leading-5 text-muted-foreground">
|
||||
当前只展示同级卡片供你配置互斥。一级只与所有一级卡片互斥,二级只与所有二级卡片互斥;生效时会自动覆盖目标卡片下的全部子级。
|
||||
{workflowConstraintSettings.crossLevelMutualExclusionEnabled
|
||||
? "当前已开启跨级互斥,可直接限制任意层级卡片;生效时会自动覆盖目标卡片下的全部子级。"
|
||||
: "当前仅展示同级卡片供你配置互斥。一级只与所有一级卡片互斥,二级只与所有二级卡片互斥;生效时会自动覆盖目标卡片下的全部子级。"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -2614,7 +2633,7 @@ export function AgentWorkflowSettingsClient({
|
||||
[refKey]: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={`搜索同级卡片(${blockedCandidates.length} 个候选)`}
|
||||
placeholder={`${workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "搜索所有层级卡片" : "搜索同级卡片"}(${blockedCandidates.length} 个候选)`}
|
||||
className="h-8 pl-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
@ -2667,7 +2686,9 @@ export function AgentWorkflowSettingsClient({
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
{blockedCandidates.length > 0
|
||||
? "没有命中搜索结果,换个关键词试试"
|
||||
: "当前工作流下没有其他可限制的同级卡片"}
|
||||
: workflowConstraintSettings.crossLevelMutualExclusionEnabled
|
||||
? "当前工作流下没有其他可限制的卡片"
|
||||
: "当前工作流下没有其他可限制的同级卡片"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -4421,19 +4442,47 @@ export function AgentWorkflowSettingsClient({
|
||||
variants={containerVariants}
|
||||
className="space-y-4"
|
||||
>
|
||||
<motion.div variants={itemVariants} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{countAgentWorkflowSteps(sortedSteps)} 模块
|
||||
</Badge>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{countAgentWorkflowOptions(sortedSteps)} 选项
|
||||
</Badge>
|
||||
<Badge variant={workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "default" : "secondary"}>
|
||||
{workflowConstraintSettings.crossLevelMutualExclusionEnabled ? "跨级互斥已开启" : "跨级互斥已关闭"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<div className="flex items-start justify-between gap-4 rounded-lg border bg-background px-4 py-3 sm:min-w-[380px]">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">跨级互斥</div>
|
||||
<div className="text-xs leading-5 text-muted-foreground">
|
||||
关闭时仅允许同级卡片互斥;开启后任意级别卡片都可以互相限制。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
checked={workflowConstraintSettings.crossLevelMutualExclusionEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
constraintSettings: {
|
||||
...(prev.constraintSettings || defaultAgentWorkflowConstraintSettings),
|
||||
crossLevelMutualExclusionEnabled: checked,
|
||||
},
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={() => handleAddStep()} size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增一级模块
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={() => handleAddStep()} size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增一级模块
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{sortedSteps.length > 0 ? (
|
||||
|
||||
@ -817,6 +817,11 @@ const updateSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
constraintSettings: z
|
||||
.object({
|
||||
crossLevelMutualExclusionEnabled: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
steps: z
|
||||
.array(
|
||||
z.object({
|
||||
|
||||
@ -88,6 +88,7 @@ import { cn, fetcher } from "@/lib/utils";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenVisibleAgentWorkflowSteps,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
normalizeAgentWorkflowUiConfig,
|
||||
sanitizeAgentWorkflowSelections,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
@ -1755,7 +1756,9 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
});
|
||||
|
||||
const orderedStepKeys = flattenVisibleAgentWorkflowSteps(
|
||||
(resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig).steps,
|
||||
getEnabledAgentWorkflowStepTree(
|
||||
(resolvedWorkflowUiConfig || defaultAgentWorkflowUiConfig).steps
|
||||
),
|
||||
normalizedCurrentSelections
|
||||
).map(({ step }) => step.key);
|
||||
const earliestChangedIndex = orderedStepKeys.findIndex((stepKey) => {
|
||||
|
||||
@ -23,6 +23,8 @@ import {
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
getEnabledAgentWorkflowOptionTree,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
getRenderableAgentWorkflowOptionTree,
|
||||
getRenderableAgentWorkflowStepTree,
|
||||
isAgentWorkflowStepSelectionComplete,
|
||||
serializeAgentWorkflowSelectionPath,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
@ -135,7 +137,7 @@ function getWorkflowStepOptionLevels(params: {
|
||||
});
|
||||
|
||||
let depth = 1;
|
||||
let currentOptions = getEnabledAgentWorkflowOptionTree(params.step.options || []);
|
||||
let currentOptions = getRenderableAgentWorkflowOptionTree(params.step.options || []);
|
||||
|
||||
while (currentOptions.length > 0) {
|
||||
const selectedKey = selectedPath[depth - 1]?.key || "";
|
||||
@ -149,7 +151,7 @@ function getWorkflowStepOptionLevels(params: {
|
||||
selectedOption,
|
||||
});
|
||||
|
||||
const nextOptions = getEnabledAgentWorkflowOptionTree(
|
||||
const nextOptions = getRenderableAgentWorkflowOptionTree(
|
||||
selectedOption?.children || []
|
||||
);
|
||||
if (!selectedOption || nextOptions.length === 0) {
|
||||
@ -176,6 +178,7 @@ function StepOptionCard({
|
||||
step,
|
||||
selected,
|
||||
disabled = false,
|
||||
disabledReason = null,
|
||||
onClick,
|
||||
index,
|
||||
}: {
|
||||
@ -183,6 +186,7 @@ function StepOptionCard({
|
||||
step: { title: string };
|
||||
selected: boolean;
|
||||
disabled?: boolean;
|
||||
disabledReason?: "blocked" | "developing" | null;
|
||||
onClick: () => void;
|
||||
index: number;
|
||||
}) {
|
||||
@ -242,7 +246,7 @@ function StepOptionCard({
|
||||
|
||||
{disabled && !selected ? (
|
||||
<div className="absolute right-3 top-3 rounded-full bg-black/65 px-2 py-1 text-[10px] font-medium text-white/80 backdrop-blur-md">
|
||||
已限制
|
||||
{disabledReason === "developing" ? "开发中" : "已限制"}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -595,15 +599,23 @@ export function AgentWorkflowPanelDarkV2({
|
||||
const resolvedConfig = useMemo(() => {
|
||||
const source = config || defaultAgentWorkflowUiConfig;
|
||||
return {
|
||||
...source,
|
||||
steps: getEnabledAgentWorkflowStepTree(source.steps || []),
|
||||
};
|
||||
}, [config]);
|
||||
const displayConfig = useMemo(() => {
|
||||
const source = config || defaultAgentWorkflowUiConfig;
|
||||
return {
|
||||
...source,
|
||||
steps: getRenderableAgentWorkflowStepTree(source.steps || []),
|
||||
};
|
||||
}, [config]);
|
||||
const flattenedResolvedSteps = useMemo(
|
||||
() =>
|
||||
flattenVisibleAgentWorkflowSteps(resolvedConfig.steps, selections).filter(
|
||||
flattenVisibleAgentWorkflowSteps(displayConfig.steps, selections).filter(
|
||||
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
||||
),
|
||||
[resolvedConfig.steps, selections]
|
||||
[displayConfig.steps, selections]
|
||||
);
|
||||
|
||||
const summary = useMemo(
|
||||
@ -675,6 +687,27 @@ export function AgentWorkflowPanelDarkV2({
|
||||
activeEmbeddedOptionLevels[activeEmbeddedOptionLevels.length - 1] ||
|
||||
activeEmbeddedOptionLevels[0] ||
|
||||
null;
|
||||
const activeEmbeddedDeepestSelectedOption = useMemo(
|
||||
() =>
|
||||
[...activeEmbeddedOptionLevels]
|
||||
.reverse()
|
||||
.find((level) => level.selectedOption)?.selectedOption || null,
|
||||
[activeEmbeddedOptionLevels]
|
||||
);
|
||||
const activeEmbeddedSelectedOptionHasDevelopingChildren = useMemo(() => {
|
||||
if (!activeEmbeddedDeepestSelectedOption) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const renderableChildren = getRenderableAgentWorkflowOptionTree(
|
||||
activeEmbeddedDeepestSelectedOption.children || []
|
||||
);
|
||||
if (renderableChildren.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getEnabledAgentWorkflowOptionTree(activeEmbeddedDeepestSelectedOption.children || []).length === 0;
|
||||
}, [activeEmbeddedDeepestSelectedOption]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!embedded) {
|
||||
@ -742,6 +775,11 @@ export function AgentWorkflowPanelDarkV2({
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeEmbeddedSelectedOptionHasDevelopingChildren) {
|
||||
setPendingAutoAdvanceStepKey(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextStep = flattenedResolvedSteps
|
||||
.slice(activeEmbeddedStepIndex + 1)
|
||||
.find(
|
||||
@ -768,6 +806,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
activeEmbeddedStepEntry,
|
||||
activeEmbeddedStepIndex,
|
||||
activeEmbeddedStepKey,
|
||||
activeEmbeddedSelectedOptionHasDevelopingChildren,
|
||||
embedded,
|
||||
flattenedResolvedSteps,
|
||||
pendingAutoAdvanceStepKey,
|
||||
@ -800,7 +839,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
|
||||
const currentSelectedOption =
|
||||
options.find((option) => option.key === currentSelectedKey) || null;
|
||||
const visibleChildren = getEnabledAgentWorkflowOptionTree(
|
||||
const visibleChildren = getRenderableAgentWorkflowOptionTree(
|
||||
currentSelectedOption?.children || []
|
||||
);
|
||||
const levelLabel = depth === 1 ? "主卡片" : depth === 2 ? "二级卡片" : "三级卡片";
|
||||
@ -820,8 +859,12 @@ export function AgentWorkflowPanelDarkV2({
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{options.map((option, optionIndex) => {
|
||||
const isDisabled =
|
||||
currentSelectedKey !== option.key && blockedOptionKeys.has(option.key);
|
||||
const isDeveloping = option.enabled === false;
|
||||
const isBlocked =
|
||||
!isDeveloping &&
|
||||
currentSelectedKey !== option.key &&
|
||||
blockedOptionKeys.has(option.key);
|
||||
const isDisabled = isDeveloping || isBlocked;
|
||||
|
||||
return (
|
||||
<StepOptionCard
|
||||
@ -830,6 +873,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
step={step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
disabled={isDisabled}
|
||||
disabledReason={isDeveloping ? "developing" : isBlocked ? "blocked" : null}
|
||||
onClick={() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
@ -1077,10 +1121,13 @@ export function AgentWorkflowPanelDarkV2({
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{activeEmbeddedOptionLevel.options.map((option, optionIndex) => {
|
||||
const currentSelectedKey = activeEmbeddedOptionLevel.selectedKey;
|
||||
const isDisabled =
|
||||
const isDeveloping = option.enabled === false;
|
||||
const isBlocked =
|
||||
!isDeveloping &&
|
||||
currentSelectedKey !== option.key &&
|
||||
blockedOptionKeys.has(option.key);
|
||||
const nextChildren = getEnabledAgentWorkflowOptionTree(
|
||||
const isDisabled = isDeveloping || isBlocked;
|
||||
const nextChildren = getRenderableAgentWorkflowOptionTree(
|
||||
option.children || []
|
||||
);
|
||||
|
||||
@ -1091,6 +1138,9 @@ export function AgentWorkflowPanelDarkV2({
|
||||
step={activeEmbeddedStepEntry.step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
disabled={isDisabled}
|
||||
disabledReason={
|
||||
isDeveloping ? "developing" : isBlocked ? "blocked" : null
|
||||
}
|
||||
onClick={() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
@ -1163,7 +1213,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
{step.description && (
|
||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||
)}
|
||||
{renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
|
||||
{renderOptionLevels(step, getRenderableAgentWorkflowOptionTree(step.options || []))}
|
||||
</motion.section>
|
||||
))
|
||||
)}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AgentWorkflowBrandStoreEntry,
|
||||
AgentWorkflowCategoryLabels,
|
||||
AgentWorkflowConstraintSettings,
|
||||
AgentWorkflowFourthCategoryEntry,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentWorkflowStoreEntry,
|
||||
@ -20,6 +21,9 @@ export const defaultAgentWorkflowCategoryLabels: AgentWorkflowCategoryLabels = {
|
||||
fourthCategory: "四级分类",
|
||||
fifthCategory: "五级分类",
|
||||
};
|
||||
export const defaultAgentWorkflowConstraintSettings: AgentWorkflowConstraintSettings = {
|
||||
crossLevelMutualExclusionEnabled: false,
|
||||
};
|
||||
|
||||
export interface AgentWorkflowBrandCategoryOption {
|
||||
value: string;
|
||||
@ -163,6 +167,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
|
||||
priceBands: [],
|
||||
brandStoreEntries: [],
|
||||
},
|
||||
constraintSettings: defaultAgentWorkflowConstraintSettings,
|
||||
steps: defaultSteps.map((step, index) =>
|
||||
cloneStep({
|
||||
...step,
|
||||
@ -208,6 +213,22 @@ export function normalizeAgentWorkflowCategoryLabels(
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeAgentWorkflowConstraintSettings(
|
||||
value: unknown
|
||||
): AgentWorkflowConstraintSettings {
|
||||
const record =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
crossLevelMutualExclusionEnabled: normalizeBoolean(
|
||||
record.crossLevelMutualExclusionEnabled ?? record.enableCrossLevelMutualExclusion,
|
||||
defaultAgentWorkflowConstraintSettings.crossLevelMutualExclusionEnabled
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function flattenAgentWorkflowSteps(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
depth = 1,
|
||||
@ -318,6 +339,35 @@ export function getEnabledAgentWorkflowStepTree(
|
||||
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
||||
}
|
||||
|
||||
export function getRenderableAgentWorkflowOptionTree(
|
||||
options: AgentWorkflowOptionUiConfig[]
|
||||
): AgentWorkflowOptionUiConfig[] {
|
||||
return [...options]
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
children: getRenderableAgentWorkflowOptionTree(option.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getRenderableAgentWorkflowStepTree(
|
||||
steps: AgentWorkflowStepUiConfig[]
|
||||
): AgentWorkflowStepUiConfig[] {
|
||||
return [...steps]
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.filter((step) => step.enabled !== false)
|
||||
.map((step) => ({
|
||||
...step,
|
||||
options: getRenderableAgentWorkflowOptionTree(step.options || []),
|
||||
children: getRenderableAgentWorkflowStepTree(step.children || []),
|
||||
}))
|
||||
.filter(
|
||||
(step) =>
|
||||
getEnabledAgentWorkflowOptionTree(step.options || []).length > 0 ||
|
||||
(step.children || []).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export interface FlattenedAgentWorkflowOption {
|
||||
option: AgentWorkflowOptionUiConfig;
|
||||
depth: number;
|
||||
@ -980,6 +1030,11 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
priceBands: normalizedPriceBands,
|
||||
brandStoreEntries: fallbackBrandStoreEntries,
|
||||
},
|
||||
constraintSettings: normalizeAgentWorkflowConstraintSettings(
|
||||
raw.constraintSettings ?? {
|
||||
crossLevelMutualExclusionEnabled: raw.crossLevelMutualExclusionEnabled,
|
||||
}
|
||||
),
|
||||
steps: sanitizedSteps,
|
||||
};
|
||||
}
|
||||
@ -994,6 +1049,12 @@ export function getAgentWorkflowCategoryLabels(
|
||||
return normalizeAgentWorkflowCategoryLabels(config?.inputOptions?.categoryLabels);
|
||||
}
|
||||
|
||||
export function getAgentWorkflowConstraintSettings(
|
||||
config?: AgentWorkflowUiConfig | null
|
||||
) {
|
||||
return normalizeAgentWorkflowConstraintSettings(config?.constraintSettings);
|
||||
}
|
||||
|
||||
export function getAgentWorkflowCategoryOptionsByBrand(params: {
|
||||
config?: AgentWorkflowUiConfig | null;
|
||||
brand?: string | null;
|
||||
@ -1214,7 +1275,9 @@ export function getAgentWorkflowSelectedOptionPath(params: {
|
||||
let currentOptions = params.step.options || [];
|
||||
|
||||
for (const key of keys) {
|
||||
const matchedOption = currentOptions.find((option) => option.key === key);
|
||||
const matchedOption = currentOptions.find(
|
||||
(option) => option.key === key && option.enabled !== false
|
||||
);
|
||||
if (!matchedOption) break;
|
||||
selectedOptions.push(matchedOption);
|
||||
currentOptions = matchedOption.children || [];
|
||||
@ -1262,11 +1325,35 @@ export function sanitizeAgentWorkflowSelections(params: {
|
||||
export function getAgentWorkflowBlockedOptionKeys(params: {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
selectionValue?: string | null;
|
||||
allowCrossLevel?: boolean;
|
||||
optionDepthLookup?: Map<string, number>;
|
||||
}) {
|
||||
const selectedOptions = getAgentWorkflowSelectedOptionPath(params);
|
||||
const optionDepthLookup =
|
||||
params.optionDepthLookup ||
|
||||
new Map(
|
||||
flattenAgentWorkflowOptionTree(params.step.options || []).map(({ option, depth }) => [
|
||||
option.key,
|
||||
depth,
|
||||
] as const)
|
||||
);
|
||||
return Array.from(
|
||||
new Set(
|
||||
selectedOptions.flatMap((option) => (option.blockedOptionKeys || []).filter(Boolean))
|
||||
selectedOptions.flatMap((option) => {
|
||||
const currentDepth = optionDepthLookup.get(option.key);
|
||||
return (option.blockedOptionKeys || []).filter((blockedKey) => {
|
||||
if (!blockedKey) {
|
||||
return false;
|
||||
}
|
||||
if (params.allowCrossLevel) {
|
||||
return true;
|
||||
}
|
||||
if (typeof currentDepth !== "number") {
|
||||
return false;
|
||||
}
|
||||
return optionDepthLookup.get(blockedKey) === currentDepth;
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -1294,10 +1381,19 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}) {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const constraintSettings = getAgentWorkflowConstraintSettings(config);
|
||||
const visibleSteps = flattenVisibleAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : []),
|
||||
params.selections
|
||||
).map(({ step }) => step);
|
||||
const visibleOptionDepthLookup = new Map(
|
||||
visibleSteps.flatMap((step) =>
|
||||
flattenAgentWorkflowOptionTree(step.options || []).map(({ option, depth }) => [
|
||||
option.key,
|
||||
depth,
|
||||
] as const)
|
||||
)
|
||||
);
|
||||
|
||||
const directlyBlockedKeys = Array.from(
|
||||
new Set(
|
||||
@ -1305,6 +1401,8 @@ export function getAgentWorkflowBlockedOptionKeysForSelections(params: {
|
||||
getAgentWorkflowBlockedOptionKeys({
|
||||
step,
|
||||
selectionValue: params.selections[step.key],
|
||||
allowCrossLevel: constraintSettings.crossLevelMutualExclusionEnabled,
|
||||
optionDepthLookup: visibleOptionDepthLookup,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
@ -101,6 +101,10 @@ export interface AgentWorkflowStepUiConfig {
|
||||
children?: AgentWorkflowStepUiConfig[];
|
||||
}
|
||||
|
||||
export interface AgentWorkflowConstraintSettings {
|
||||
crossLevelMutualExclusionEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface AgentWorkflowUiConfig {
|
||||
inputOptions?: {
|
||||
brands: string[];
|
||||
@ -112,6 +116,7 @@ export interface AgentWorkflowUiConfig {
|
||||
priceBands?: string[];
|
||||
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
|
||||
};
|
||||
constraintSettings?: AgentWorkflowConstraintSettings;
|
||||
steps: AgentWorkflowStepUiConfig[];
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user