fix agent workflow prompt cleanup and card blocking
This commit is contained in:
parent
fb2f3461c2
commit
785177bed4
@ -170,6 +170,7 @@ function createWorkflowOptionDraft(
|
||||
enabled: true,
|
||||
sortOrder: optionIndex,
|
||||
promptFragment: `请按选项${optionIndex + 1}执行。`,
|
||||
blockedOptionKeys: [],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@ -212,6 +213,27 @@ function collectWorkflowOptionKeys(options: AgentWorkflowOptionUiConfig[]): stri
|
||||
return flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key);
|
||||
}
|
||||
|
||||
function collectWorkflowOptionRelations(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
ancestors: string[] = []
|
||||
): Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
depth: number;
|
||||
}> {
|
||||
return options.flatMap((option) => {
|
||||
const currentPath = [...ancestors, option.buttonName || option.key];
|
||||
return [
|
||||
{
|
||||
key: option.key,
|
||||
label: currentPath.join(" / "),
|
||||
depth: currentPath.length,
|
||||
},
|
||||
...collectWorkflowOptionRelations(option.children || [], currentPath),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
function updateWorkflowOptionTree(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
optionKey: string,
|
||||
@ -1212,6 +1234,20 @@ export function AgentWorkflowSettingsClient({
|
||||
(left, right) => left.sortOrder - right.sortOrder
|
||||
);
|
||||
const canAddChild = depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH;
|
||||
const optionRelations = collectWorkflowOptionRelations(step.options || []);
|
||||
const optionLookup = new Map(optionRelations.map((item) => [item.key, item] as const));
|
||||
const blockedCandidates = optionRelations.filter((item) => item.key !== option.key);
|
||||
const selectedBlockedItems = (option.blockedOptionKeys || [])
|
||||
.map((blockedKey) => optionLookup.get(blockedKey))
|
||||
.filter(
|
||||
(
|
||||
item
|
||||
): item is {
|
||||
key: string;
|
||||
label: string;
|
||||
depth: number;
|
||||
} => Boolean(item)
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
|
||||
@ -1327,6 +1363,101 @@ export function AgentWorkflowSettingsClient({
|
||||
className="resize-none text-xs"
|
||||
/>
|
||||
</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">
|
||||
选中当前卡片后,下方指定的其它卡片会在前台变灰并不可选。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{selectedBlockedItems.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedBlockedItems.map((blockedItem) => (
|
||||
<span
|
||||
key={blockedItem.key}
|
||||
className="inline-flex items-center gap-2 rounded-full border bg-background px-3 py-1.5 text-xs"
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{getWorkflowLevelLabel(blockedItem.depth, "卡片")}
|
||||
</span>
|
||||
<span className="max-w-[220px] truncate">{blockedItem.label}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-0.5 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive"
|
||||
onClick={() =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
blockedOptionKeys: (currentOption.blockedOptionKeys || []).filter(
|
||||
(item) => item !== blockedItem.key
|
||||
),
|
||||
}))
|
||||
}
|
||||
aria-label={`移除互斥卡片 ${blockedItem.label}`}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-md border border-dashed bg-background/70 px-3 py-2 text-xs text-muted-foreground">
|
||||
当前还没有配置互斥卡片
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-h-44 space-y-2 overflow-y-auto rounded-md border bg-background/70 p-2">
|
||||
{blockedCandidates.length > 0 ? (
|
||||
blockedCandidates.map((candidate) => {
|
||||
const active = (option.blockedOptionKeys || []).includes(candidate.key);
|
||||
return (
|
||||
<button
|
||||
key={candidate.key}
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-md border px-3 py-2 text-left text-xs transition-colors",
|
||||
active
|
||||
? "border-amber-500/50 bg-amber-500/10 text-foreground"
|
||||
: "border-border/70 bg-background hover:border-foreground/20 hover:bg-muted/60"
|
||||
)}
|
||||
onClick={() =>
|
||||
updateOption(step.key, option.key, (currentOption) => {
|
||||
const currentBlocked = new Set(
|
||||
currentOption.blockedOptionKeys || []
|
||||
);
|
||||
if (currentBlocked.has(candidate.key)) {
|
||||
currentBlocked.delete(candidate.key);
|
||||
} else {
|
||||
currentBlocked.add(candidate.key);
|
||||
}
|
||||
return {
|
||||
...currentOption,
|
||||
blockedOptionKeys: Array.from(currentBlocked),
|
||||
};
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate font-medium">
|
||||
{candidate.label}
|
||||
</span>
|
||||
<span className="block text-[10px] text-muted-foreground">
|
||||
{getWorkflowLevelLabel(candidate.depth, "卡片")}
|
||||
</span>
|
||||
</span>
|
||||
<Badge variant={active ? "default" : "outline"}>
|
||||
{active ? "已限制" : "可限制"}
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
||||
当前步骤下没有其他可限制卡片
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2880,6 +2880,57 @@ const AGENT_PROMPT_STRUCTURAL_LABELS = new Set([
|
||||
"输出",
|
||||
]);
|
||||
|
||||
const AGENT_PROMPT_LABEL_SEMANTIC_ALIASES = new Map<string, string>([
|
||||
["角色", "role"],
|
||||
["技能", "skills"],
|
||||
["目标", "goal"],
|
||||
["约束条件", "constraints"],
|
||||
["流程", "process"],
|
||||
["输出", "output"],
|
||||
["人群选择", "audience"],
|
||||
["人物", "audience"],
|
||||
["主题风格", "style"],
|
||||
["整体风格", "style"],
|
||||
["空间选择", "space"],
|
||||
["空间布局", "space"],
|
||||
["主空间", "space"],
|
||||
["子空间选择", "subspace"],
|
||||
["子空间", "subspace"],
|
||||
["硬装方案", "hardDecoration"],
|
||||
["硬装", "hardDecoration"],
|
||||
["软装方案", "softDecoration"],
|
||||
["软装", "softDecoration"],
|
||||
["光氛选择", "lighting"],
|
||||
["光源布置", "lighting"],
|
||||
["灯具", "lighting"],
|
||||
["机位选择", "camera"],
|
||||
["拍摄角度", "camera"],
|
||||
["配饰陈列", "accessories"],
|
||||
["放在产品内的小件配饰", "accessories"],
|
||||
["前景", "sceneLayout"],
|
||||
["中景", "sceneLayout"],
|
||||
["背景", "sceneLayout"],
|
||||
]);
|
||||
|
||||
const AGENT_SCENE_WORKFLOW_OWNED_SEMANTICS = new Set([
|
||||
"role",
|
||||
"skills",
|
||||
"goal",
|
||||
"constraints",
|
||||
"process",
|
||||
"output",
|
||||
"audience",
|
||||
"style",
|
||||
"space",
|
||||
"subspace",
|
||||
"hardDecoration",
|
||||
"softDecoration",
|
||||
"lighting",
|
||||
"camera",
|
||||
"accessories",
|
||||
"sceneLayout",
|
||||
]);
|
||||
|
||||
function normalizePromptLineForCompare(line: string) {
|
||||
return String(line || "")
|
||||
.replace(/\s+/g, " ")
|
||||
@ -2893,6 +2944,14 @@ function parsePromptLineLabel(line: string) {
|
||||
const normalized = String(line || "").replace(/\s+/g, " ").trim();
|
||||
if (!normalized) return { label: "", value: "" };
|
||||
|
||||
const headerOnlyMatch = normalized.match(/^([^::]{1,20})[::]\s*$/);
|
||||
if (headerOnlyMatch) {
|
||||
return {
|
||||
label: headerOnlyMatch[1].trim(),
|
||||
value: "",
|
||||
};
|
||||
}
|
||||
|
||||
const colonMatch = normalized.match(/^([^::]{1,20})[::]\s*(.+)$/);
|
||||
if (colonMatch) {
|
||||
return {
|
||||
@ -2912,13 +2971,51 @@ function parsePromptLineLabel(line: string) {
|
||||
return { label: "", value: normalized };
|
||||
}
|
||||
|
||||
function normalizePromptLabel(label: string) {
|
||||
return String(label || "")
|
||||
.replace(/\s+/g, "")
|
||||
.replace(/[::]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function getPromptSemanticLabel(label: string) {
|
||||
const normalized = normalizePromptLabel(label);
|
||||
return AGENT_PROMPT_LABEL_SEMANTIC_ALIASES.get(normalized) || normalized;
|
||||
}
|
||||
|
||||
function shouldDropSceneSlotSemanticLine(
|
||||
slotSemanticLabel: string,
|
||||
workflowSemanticLabels: Set<string>,
|
||||
) {
|
||||
if (!slotSemanticLabel || !AGENT_SCENE_WORKFLOW_OWNED_SEMANTICS.has(slotSemanticLabel)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (slotSemanticLabel === "sceneLayout") {
|
||||
return [
|
||||
"style",
|
||||
"space",
|
||||
"subspace",
|
||||
"hardDecoration",
|
||||
"softDecoration",
|
||||
"accessories",
|
||||
"camera",
|
||||
"lighting",
|
||||
].some((semantic) => workflowSemanticLabels.has(semantic));
|
||||
}
|
||||
|
||||
return workflowSemanticLabels.has(slotSemanticLabel);
|
||||
}
|
||||
|
||||
function dedupeAgentSlotPromptAgainstWorkflow(params: {
|
||||
workflowPrompt?: string | null;
|
||||
slotPrompt?: string | null;
|
||||
modeType?: "scene_generation" | "detail_replacement" | "sku_replacement" | "generic";
|
||||
}) {
|
||||
const dedupedSlotLines: string[] = [];
|
||||
const seenExactLines = new Set<string>();
|
||||
const workflowStructuralValues = new Map<string, string>();
|
||||
const workflowSemanticLabels = new Set<string>();
|
||||
|
||||
const rememberLine = (line: string) => {
|
||||
const normalizedLine = normalizePromptLineForCompare(line);
|
||||
@ -2934,6 +3031,9 @@ function dedupeAgentSlotPromptAgainstWorkflow(params: {
|
||||
.filter(Boolean);
|
||||
normalizedWorkflowLines.forEach((line) => {
|
||||
const parsed = parsePromptLineLabel(line);
|
||||
if (parsed.label) {
|
||||
workflowSemanticLabels.add(getPromptSemanticLabel(parsed.label));
|
||||
}
|
||||
if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) {
|
||||
workflowStructuralValues.set(
|
||||
parsed.label,
|
||||
@ -2947,12 +3047,26 @@ function dedupeAgentSlotPromptAgainstWorkflow(params: {
|
||||
.split(/\n+/)
|
||||
.map((line) => line.replace(/\s+/g, " ").trim())
|
||||
.filter(Boolean);
|
||||
let activeSlotSemanticLabel = "";
|
||||
normalizedSlotLines.forEach((line) => {
|
||||
const normalizedLine = normalizePromptLineForCompare(line);
|
||||
if (!normalizedLine || seenExactLines.has(normalizedLine)) {
|
||||
return;
|
||||
}
|
||||
const parsed = parsePromptLineLabel(line);
|
||||
const slotSemanticLabel = parsed.label ? getPromptSemanticLabel(parsed.label) : "";
|
||||
if (slotSemanticLabel) {
|
||||
activeSlotSemanticLabel = slotSemanticLabel;
|
||||
}
|
||||
if (
|
||||
params.modeType === "scene_generation" &&
|
||||
shouldDropSceneSlotSemanticLine(
|
||||
slotSemanticLabel || activeSlotSemanticLabel,
|
||||
workflowSemanticLabels,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (parsed.label && AGENT_PROMPT_STRUCTURAL_LABELS.has(parsed.label)) {
|
||||
const normalizedValue = normalizePromptLineForCompare(parsed.value || "");
|
||||
const workflowValue = workflowStructuralValues.get(parsed.label);
|
||||
@ -2972,6 +3086,8 @@ function buildAgentSlotPrompt(params: {
|
||||
slotPrompt: string;
|
||||
slotTitle?: string | null;
|
||||
userSupplementPrompt?: string | null;
|
||||
includeSlotTitle?: boolean;
|
||||
modeType?: "scene_generation" | "detail_replacement" | "sku_replacement" | "generic";
|
||||
}) {
|
||||
const lines: string[] = [];
|
||||
const workflowPrompt = normalizeAgentWorkflowPrompt(params.workflowPrompt || "");
|
||||
@ -2979,12 +3095,13 @@ function buildAgentSlotPrompt(params: {
|
||||
lines.push(workflowPrompt);
|
||||
}
|
||||
const slotTitle = String(params.slotTitle || "").trim();
|
||||
if (slotTitle) {
|
||||
if (params.includeSlotTitle !== false && slotTitle) {
|
||||
lines.push(`【槽位】${slotTitle}`);
|
||||
}
|
||||
const dedupedSlotPrompt = dedupeAgentSlotPromptAgainstWorkflow({
|
||||
workflowPrompt,
|
||||
slotPrompt: params.slotPrompt,
|
||||
modeType: params.modeType || "generic",
|
||||
});
|
||||
if (dedupedSlotPrompt) {
|
||||
lines.push(dedupedSlotPrompt);
|
||||
@ -2996,6 +3113,22 @@ function buildAgentSlotPrompt(params: {
|
||||
return lines.join("\n\n").trim();
|
||||
}
|
||||
|
||||
function buildAgentSceneWorkflowPrompt(params: {
|
||||
workflowPrompt?: string | null;
|
||||
userSupplementPrompt?: string | null;
|
||||
}) {
|
||||
const lines: string[] = [];
|
||||
const workflowPrompt = String(params.workflowPrompt || "").trim();
|
||||
if (workflowPrompt) {
|
||||
lines.push(workflowPrompt);
|
||||
}
|
||||
const supplement = String(params.userSupplementPrompt || "").trim();
|
||||
if (supplement) {
|
||||
lines.push(`【补充要求】${supplement}`);
|
||||
}
|
||||
return lines.join("\n\n").trim();
|
||||
}
|
||||
|
||||
type AgentReplacementModePayload = NonNullable<PostRequestBody["agentModePayload"]>;
|
||||
|
||||
type ReplacementReferenceDecision = {
|
||||
@ -6633,6 +6766,12 @@ export async function POST(request: Request) {
|
||||
replacementSlot?.promptTemplate || selectedAgentPreset.promptTemplate,
|
||||
slotTitle: selectedAgentPreset.name,
|
||||
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
|
||||
includeSlotTitle: false,
|
||||
modeType: isDetailReplacementPreset
|
||||
? "detail_replacement"
|
||||
: isSkuReplacementPreset
|
||||
? "sku_replacement"
|
||||
: "generic",
|
||||
});
|
||||
|
||||
if (productInputImageUrls.length === 0) {
|
||||
@ -7082,12 +7221,18 @@ export async function POST(request: Request) {
|
||||
[...sharedUploadedAssetIds, ...(sceneSlot?.referenceAssetIds || [])].filter(Boolean),
|
||||
),
|
||||
);
|
||||
const scenePrompt = buildAgentSlotPrompt({
|
||||
const scenePrompt =
|
||||
buildAgentSceneWorkflowPrompt({
|
||||
workflowPrompt: hiddenPromptText || null,
|
||||
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
|
||||
}) ||
|
||||
buildAgentSlotPrompt({
|
||||
workflowPrompt: hiddenPromptText || null,
|
||||
slotPrompt: sceneSlot?.promptTemplate || selectedAgentPreset.promptTemplate,
|
||||
slotTitle: selectedAgentPreset.name,
|
||||
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
|
||||
});
|
||||
includeSlotTitle: false,
|
||||
modeType: "scene_generation",
|
||||
});
|
||||
const sceneGenerationStartedAt = Date.now();
|
||||
|
||||
try {
|
||||
@ -7113,6 +7258,9 @@ export async function POST(request: Request) {
|
||||
resolvedImageCount,
|
||||
sourceImageCount: sourceImageUrls.length,
|
||||
sourceAssetCount: sourceAssetIds.length,
|
||||
promptStrategy: hiddenPromptText || sanitizedUserSupplementPrompt
|
||||
? "workflow_only"
|
||||
: "legacy_template_fallback",
|
||||
promptLength: String(scenePrompt || `生成${selectedAgentPreset.name}`).length,
|
||||
aspectRatio:
|
||||
typeof mergedImageParams.aspectRatio === "string"
|
||||
@ -7326,6 +7474,7 @@ export async function POST(request: Request) {
|
||||
slotPrompt: slot.promptTemplate,
|
||||
slotTitle: slot.title || slot.uploadLabel || null,
|
||||
userSupplementPrompt: sanitizedUserSupplementPrompt || null,
|
||||
modeType: "generic",
|
||||
});
|
||||
const assistantMessageId = generateUUID();
|
||||
|
||||
|
||||
@ -724,6 +724,10 @@ const updateSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
promptFragment: z.string().trim().max(1000).nullable().optional(),
|
||||
blockedOptionKeys: z
|
||||
.array(z.string().trim().min(1).max(64))
|
||||
.max(100)
|
||||
.optional(),
|
||||
children: z.array(z.any()).max(100).optional(),
|
||||
})
|
||||
)
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenAgentWorkflowSteps,
|
||||
flattenVisibleAgentWorkflowSteps,
|
||||
getAgentWorkflowBlockedOptionKeys,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowPriceBandOptions,
|
||||
getAgentWorkflowSelectedOptionPath,
|
||||
@ -159,31 +160,37 @@ function StepOptionCard({
|
||||
option,
|
||||
step,
|
||||
selected,
|
||||
disabled = false,
|
||||
onClick,
|
||||
index,
|
||||
}: {
|
||||
option: { key: string; buttonName: string; description?: string | null; previewImageUrl?: string | null };
|
||||
step: { title: string };
|
||||
selected: boolean;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
index: number;
|
||||
}) {
|
||||
return (
|
||||
<motion.button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: index * 0.05 }}
|
||||
whileHover={{ y: -4, scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
whileHover={disabled ? undefined : { y: -4, scale: 1.02 }}
|
||||
whileTap={disabled ? undefined : { scale: 0.98 }}
|
||||
className={cn(
|
||||
"group relative overflow-hidden rounded-2xl text-left",
|
||||
"aspect-[16/10] w-full",
|
||||
"transition-all duration-500",
|
||||
disabled && "cursor-not-allowed opacity-45 saturate-0",
|
||||
selected
|
||||
? "ring-2 ring-white/30 shadow-[0_0_30px_rgba(255,255,255,0.1)]"
|
||||
: "ring-1 ring-white/12 hover:ring-white/24"
|
||||
: disabled
|
||||
? "ring-1 ring-white/8"
|
||||
: "ring-1 ring-white/12 hover:ring-white/24"
|
||||
)}
|
||||
>
|
||||
{/* 背景 */}
|
||||
@ -193,7 +200,10 @@ function StepOptionCard({
|
||||
<img
|
||||
src={option.previewImageUrl}
|
||||
alt={option.buttonName}
|
||||
className="h-full w-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
className={cn(
|
||||
"h-full w-full object-cover transition-transform duration-700",
|
||||
!disabled && "group-hover:scale-110"
|
||||
)}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent" />
|
||||
</>
|
||||
@ -215,6 +225,12 @@ function StepOptionCard({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{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>
|
||||
) : null}
|
||||
|
||||
{/* 步骤标签 */}
|
||||
<div className="absolute left-3 top-3">
|
||||
<span className="rounded-full bg-black/60 px-2.5 py-1 text-[10px] font-medium text-white/80 backdrop-blur-md">
|
||||
@ -389,6 +405,18 @@ export function AgentWorkflowPanelDarkV2({
|
||||
]);
|
||||
return Object.fromEntries(entries) as Record<string, string>;
|
||||
}, [resolvedConfig.steps, selections]);
|
||||
const blockedOptionKeyMap = useMemo(() => {
|
||||
const entries = flattenAgentWorkflowSteps(resolvedConfig.steps).map(({ step }) => [
|
||||
step.key,
|
||||
new Set(
|
||||
getAgentWorkflowBlockedOptionKeys({
|
||||
step,
|
||||
selectionValue: selections[step.key],
|
||||
})
|
||||
),
|
||||
] as const);
|
||||
return new Map(entries);
|
||||
}, [resolvedConfig.steps, selections]);
|
||||
const [activeEmbeddedStepKey, setActiveEmbeddedStepKey] = useState<string>("");
|
||||
const [activeEmbeddedOptionDepth, setActiveEmbeddedOptionDepth] = useState(1);
|
||||
const [pendingAutoAdvanceStepKey, setPendingAutoAdvanceStepKey] = useState<string | null>(null);
|
||||
@ -519,6 +547,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
|
||||
const currentSelectedOption =
|
||||
options.find((option) => option.key === currentSelectedKey) || null;
|
||||
const blockedKeys = blockedOptionKeyMap.get(step.key) || new Set<string>();
|
||||
const visibleChildren = getEnabledAgentWorkflowOptionTree(
|
||||
currentSelectedOption?.children || []
|
||||
);
|
||||
@ -538,25 +567,37 @@ export function AgentWorkflowPanelDarkV2({
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{options.map((option, optionIndex) => (
|
||||
<StepOptionCard
|
||||
key={`${option.key}-${depth}`}
|
||||
option={option}
|
||||
step={step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
onClick={() => {
|
||||
const prefix = parentPath.slice(0, depth - 1);
|
||||
const nextKeys =
|
||||
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
|
||||
setPendingAutoAdvanceStepKey(step.key);
|
||||
onSelectionsChange({
|
||||
...selections,
|
||||
[step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined,
|
||||
});
|
||||
}}
|
||||
index={optionIndex}
|
||||
/>
|
||||
))}
|
||||
{options.map((option, optionIndex) => {
|
||||
const isDisabled =
|
||||
currentSelectedKey !== option.key && blockedKeys.has(option.key);
|
||||
|
||||
return (
|
||||
<StepOptionCard
|
||||
key={`${option.key}-${depth}`}
|
||||
option={option}
|
||||
step={step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
const prefix = parentPath.slice(0, depth - 1);
|
||||
const nextKeys =
|
||||
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
|
||||
setPendingAutoAdvanceStepKey(step.key);
|
||||
onSelectionsChange({
|
||||
...selections,
|
||||
[step.key]:
|
||||
nextKeys.length > 0
|
||||
? serializeAgentWorkflowSelectionPath(nextKeys)
|
||||
: undefined,
|
||||
});
|
||||
}}
|
||||
index={optionIndex}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{currentSelectedOption && visibleChildren.length > 0
|
||||
@ -748,6 +789,11 @@ 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 blockedKeys =
|
||||
blockedOptionKeyMap.get(activeEmbeddedStepEntry.step.key) ||
|
||||
new Set<string>();
|
||||
const isDisabled =
|
||||
currentSelectedKey !== option.key && blockedKeys.has(option.key);
|
||||
const nextChildren = getEnabledAgentWorkflowOptionTree(
|
||||
option.children || []
|
||||
);
|
||||
@ -758,7 +804,11 @@ export function AgentWorkflowPanelDarkV2({
|
||||
option={option}
|
||||
step={activeEmbeddedStepEntry.step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
disabled={isDisabled}
|
||||
onClick={() => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
const selectedPathKeys = activeEmbeddedOptionLevels
|
||||
.slice(0, activeEmbeddedOptionLevel.depth - 1)
|
||||
.map((level) => level.selectedKey)
|
||||
|
||||
@ -23,6 +23,10 @@ function cloneOption(
|
||||
description: option.description || "",
|
||||
previewImageUrl: option.previewImageUrl || "",
|
||||
promptFragment: option.promptFragment || "",
|
||||
blockedOptionKeys: Array.from(new Set((option.blockedOptionKeys || []).filter(Boolean))).slice(
|
||||
0,
|
||||
100
|
||||
),
|
||||
children: (option.children || []).map(cloneOption),
|
||||
};
|
||||
}
|
||||
@ -67,6 +71,7 @@ function createOption(
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
promptFragment,
|
||||
blockedOptionKeys: [],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
@ -349,6 +354,37 @@ function normalizeNumber(value: unknown, fallback: number, max = 999) {
|
||||
return Math.max(0, Math.min(max, Math.round(parsed)));
|
||||
}
|
||||
|
||||
function normalizeKeyList(value: unknown, max = 100) {
|
||||
if (!Array.isArray(value)) return [] as string[];
|
||||
return Array.from(
|
||||
new Set(
|
||||
value
|
||||
.map((item, index) => normalizeKey(item, `blocked_option_${index + 1}`))
|
||||
.filter(Boolean)
|
||||
.slice(0, max)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeBlockedOptionTree(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
validKeys = new Set(
|
||||
flattenAgentWorkflowOptionTree(options).map(({ option }) => option.key)
|
||||
)
|
||||
): AgentWorkflowOptionUiConfig[] {
|
||||
return options.map((option) => ({
|
||||
...option,
|
||||
blockedOptionKeys: Array.from(
|
||||
new Set(
|
||||
(option.blockedOptionKeys || []).filter(
|
||||
(blockedKey) => blockedKey !== option.key && validKeys.has(blockedKey)
|
||||
)
|
||||
)
|
||||
).slice(0, 100),
|
||||
children: sanitizeBlockedOptionTree(option.children || [], validKeys),
|
||||
}));
|
||||
}
|
||||
|
||||
export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiConfig {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
@ -456,6 +492,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
normalizeText(optionRecord.promptFragment, 1000) ||
|
||||
defaultOption?.promptFragment ||
|
||||
buttonName;
|
||||
const hasRawBlockedOptionKeys = Array.isArray(optionRecord.blockedOptionKeys);
|
||||
const hasRawChildren = Array.isArray(optionRecord.children);
|
||||
const rawChildren = hasRawChildren ? (optionRecord.children as unknown[]) : [];
|
||||
const defaultChildren =
|
||||
@ -479,6 +516,9 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
defaultOption?.sortOrder ?? optionIndex
|
||||
),
|
||||
promptFragment,
|
||||
blockedOptionKeys: hasRawBlockedOptionKeys
|
||||
? normalizeKeyList(optionRecord.blockedOptionKeys, 100)
|
||||
: normalizeKeyList(defaultOption?.blockedOptionKeys, 100),
|
||||
children:
|
||||
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
|
||||
? childSource
|
||||
@ -529,6 +569,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
normalizeOption(option, optionIndex, stepKey, usedOptionKeys, defaultOptions, 1)
|
||||
)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
const sanitizedOptions = sanitizeBlockedOptionTree(options);
|
||||
|
||||
const hasRawChildren = Array.isArray(stepRecord?.children);
|
||||
const rawChildren = hasRawChildren ? (stepRecord?.children as unknown[]) : [];
|
||||
@ -547,7 +588,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
description: normalizeText(stepRecord?.description, 300) || fallbackDescription,
|
||||
enabled: normalizeBoolean(stepRecord?.enabled, defaultStep?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(stepRecord?.sortOrder, defaultStep?.sortOrder ?? stepIndex),
|
||||
options,
|
||||
options: sanitizedOptions,
|
||||
children:
|
||||
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
|
||||
? childSource
|
||||
@ -638,6 +679,18 @@ export function getAgentWorkflowSelectedOptionPath(params: {
|
||||
return selectedOptions;
|
||||
}
|
||||
|
||||
export function getAgentWorkflowBlockedOptionKeys(params: {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
selectionValue?: string | null;
|
||||
}) {
|
||||
const selectedOptions = getAgentWorkflowSelectedOptionPath(params);
|
||||
return Array.from(
|
||||
new Set(
|
||||
selectedOptions.flatMap((option) => (option.blockedOptionKeys || []).filter(Boolean))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function isAgentWorkflowStepSelectionComplete(params: {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
selectionValue?: string | null;
|
||||
|
||||
@ -87,6 +87,7 @@ export interface AgentWorkflowOptionUiConfig {
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
promptFragment?: string | null;
|
||||
blockedOptionKeys?: string[];
|
||||
children?: AgentWorkflowOptionUiConfig[];
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user