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