feat: add collapsible workflow child settings
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 17:12:16 +08:00
parent 478562c312
commit 19c76950db

View File

@ -487,6 +487,12 @@ export function AgentWorkflowSettingsClient({
const [isSaving, setIsSaving] = useState(false);
const [uploadingKey, setUploadingKey] = useState<string | null>(null);
const [expandedSteps, setExpandedSteps] = useState<Set<string>>(new Set());
const [expandedWorkflowOptions, setExpandedWorkflowOptions] = useState<Record<string, boolean>>(
{}
);
const [expandedPresetLibraryNodes, setExpandedPresetLibraryNodes] = useState<
Record<string, boolean>
>({});
const [activeTab, setActiveTab] = useState("showcase");
const [brandDraft, setBrandDraft] = useState("");
const [priceBandDraft, setPriceBandDraft] = useState("");
@ -520,6 +526,8 @@ export function AgentWorkflowSettingsClient({
setBrandModelConfig(
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
);
setExpandedWorkflowOptions({});
setExpandedPresetLibraryNodes({});
if (flattenedUiConfig.steps[0]) {
setExpandedSteps(new Set([flattenedUiConfig.steps[0].key]));
}
@ -862,6 +870,26 @@ export function AgentWorkflowSettingsClient({
});
};
const isWorkflowOptionExpanded = (optionKey: string, depth: number) =>
expandedWorkflowOptions[optionKey] ?? depth === 1;
const toggleWorkflowOptionExpansion = (optionKey: string, depth: number) => {
setExpandedWorkflowOptions((prev) => ({
...prev,
[optionKey]: !(prev[optionKey] ?? depth === 1),
}));
};
const isPresetLibraryNodeExpanded = (nodeId: string, depth: number) =>
expandedPresetLibraryNodes[nodeId] ?? depth === 1;
const togglePresetLibraryNodeExpansion = (nodeId: string, depth: number) => {
setExpandedPresetLibraryNodes((prev) => ({
...prev,
[nodeId]: !(prev[nodeId] ?? depth === 1),
}));
};
const updateStep = (
stepKey: string,
updater: (step: AgentWorkflowUiConfig["steps"][number]) => AgentWorkflowUiConfig["steps"][number]
@ -1037,91 +1065,112 @@ export function AgentWorkflowSettingsClient({
const sortedChildren = [...(node.children || [])].sort(
(left, right) => left.sortOrder - right.sortOrder
);
const isExpanded = isPresetLibraryNodeExpanded(node.id, depth);
return (
<div key={node.id} className={cn("space-y-3", depth > 1 && "pl-4 sm:pl-6")}>
<Card className={cn(!node.enabled && "opacity-60", depth > 1 && "border-l-4 border-l-sky-500/30")}>
<CardHeader className="py-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-9 min-w-9 items-center justify-center rounded-md bg-sky-500/10 px-2 text-xs font-medium text-sky-600">
{indexPath.join(".")}
</div>
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Input
value={node.name}
onChange={(e) =>
updatePresetLibraryNode(sectionId, node.id, (currentNode) => ({
...currentNode,
name: e.target.value,
}))
}
className="h-8 min-w-[200px] max-w-[320px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0"
placeholder="分类名称"
/>
<Badge variant="outline">{levelLabel}</Badge>
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
{node.id}
</code>
<Collapsible
open={isExpanded}
onOpenChange={() => togglePresetLibraryNodeExpansion(node.id, depth)}
>
<Card className={cn(!node.enabled && "opacity-60", depth > 1 && "border-l-4 border-l-sky-500/30")}>
<CardHeader className="py-4">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="flex min-w-0 items-center gap-3">
<CollapsibleTrigger asChild>
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-muted"
aria-label={isExpanded ? "收起分类配置" : "展开分类配置"}
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
</CollapsibleTrigger>
<div className="flex h-9 min-w-9 items-center justify-center rounded-md bg-sky-500/10 px-2 text-xs font-medium text-sky-600">
{indexPath.join(".")}
</div>
<div className="min-w-0 flex-1 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Input
value={node.name}
onChange={(e) =>
updatePresetLibraryNode(sectionId, node.id, (currentNode) => ({
...currentNode,
name: e.target.value,
}))
}
className="h-8 min-w-[200px] max-w-[320px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0"
placeholder="分类名称"
/>
<Badge variant="outline">{levelLabel}</Badge>
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
{node.id}
</code>
</div>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{sortedChildren.length} 子分类</Badge>
{canAddChild ? (
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">{sortedChildren.length} 子分类</Badge>
{canAddChild ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleAddPresetLibraryNode(sectionId, node.id)}
>
<Plus className="mr-2 h-4 w-4" />
新增子级
</Button>
) : (
<Badge variant="outline">已达三级</Badge>
)}
<Switch
checked={node.enabled}
onCheckedChange={(checked) =>
updatePresetLibraryNode(sectionId, node.id, (currentNode) => ({
...currentNode,
enabled: checked,
}))
}
/>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleAddPresetLibraryNode(sectionId, node.id)}
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
onClick={() => handleRemovePresetLibraryNode(sectionId, node.id)}
>
<Plus className="mr-2 h-4 w-4" />
新增子级
<Trash2 className="h-4 w-4" />
</Button>
) : (
<Badge variant="outline">已达三级</Badge>
)}
<Switch
checked={node.enabled}
onCheckedChange={(checked) =>
updatePresetLibraryNode(sectionId, node.id, (currentNode) => ({
...currentNode,
enabled: checked,
}))
}
/>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
onClick={() => handleRemovePresetLibraryNode(sectionId, node.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
</CardHeader>
</CardHeader>
<CardContent className="border-t bg-muted/20 pt-4">
{sortedChildren.length > 0 ? (
<div className="space-y-4 border-l border-dashed border-border/80 pl-3 sm:pl-4">
{sortedChildren.map((childNode, childIndex) =>
renderPresetLibraryNodeCard(sectionId, childNode, depth + 1, [
...indexPath,
childIndex + 1,
])
<CollapsibleContent>
<CardContent className="border-t bg-muted/20 pt-4">
{sortedChildren.length > 0 ? (
<div className="space-y-4 border-l border-dashed border-border/80 pl-3 sm:pl-4">
{sortedChildren.map((childNode, childIndex) =>
renderPresetLibraryNodeCard(sectionId, childNode, depth + 1, [
...indexPath,
childIndex + 1,
])
)}
</div>
) : (
<div className="rounded-lg border border-dashed bg-background/60 px-4 py-6 text-center text-sm text-muted-foreground">
暂无子分类
</div>
)}
</div>
) : (
<div className="rounded-lg border border-dashed bg-background/60 px-4 py-6 text-center text-sm text-muted-foreground">
暂无子分类
</div>
)}
</CardContent>
</Card>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
</div>
);
};
@ -1261,30 +1310,94 @@ export function AgentWorkflowSettingsClient({
const selectedBlockedItems = (option.blockedOptionKeys || [])
.map((blockedKey) => optionLookup.get(blockedKey))
.filter((item): item is WorkflowOptionRelation => Boolean(item));
const isExpanded = isWorkflowOptionExpanded(option.key, depth);
return (
<div key={option.key} className={cn("space-y-4", depth > 1 && "pl-4 sm:pl-6")}>
<div
className={cn(
"rounded-lg border bg-card p-4",
!option.enabled && "opacity-60",
depth > 1 && "border-l-4 border-l-sky-500/30"
)}
<Collapsible
open={isExpanded}
onOpenChange={() => toggleWorkflowOptionExpansion(option.key, depth)}
>
<div className="mb-4 flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 flex-1">
<div className="mb-3 flex flex-wrap items-center gap-2">
<span className="flex h-8 min-w-8 items-center justify-center rounded-md bg-sky-500/10 px-2 text-xs font-medium text-sky-600">
{indexPath.join(".")}
</span>
<Badge variant="outline">{levelLabel}</Badge>
<Badge variant="secondary">{sortedChildren.length} 子卡片</Badge>
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
{option.key}
</code>
<div
className={cn(
"rounded-lg border bg-card p-4",
!option.enabled && "opacity-60",
depth > 1 && "border-l-4 border-l-sky-500/30"
)}
>
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
<div className="min-w-0 flex-1">
<div className="mb-2 flex flex-wrap items-center gap-2">
<CollapsibleTrigger asChild>
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-muted"
aria-label={isExpanded ? "收起卡片配置" : "展开卡片配置"}
>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
</CollapsibleTrigger>
<span className="flex h-8 min-w-8 items-center justify-center rounded-md bg-sky-500/10 px-2 text-xs font-medium text-sky-600">
{indexPath.join(".")}
</span>
<Badge variant="outline">{levelLabel}</Badge>
<Badge variant="secondary">{sortedChildren.length} 子卡片</Badge>
<span className="max-w-[240px] truncate text-sm font-medium">
{option.buttonName || "未命名卡片"}
</span>
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
{option.key}
</code>
</div>
{depth > 1 ? (
<div className="text-xs text-muted-foreground">
{isExpanded ? "已展开当前子卡片配置" : "当前子卡片已折叠,点击左侧箭头展开详细设置"}
</div>
) : null}
</div>
<div className="grid grid-cols-[100px_1fr] gap-4">
<div className="flex flex-wrap items-center gap-1">
{canAddChild ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleAddOption(step.key, option.key)}
>
<Plus className="mr-2 h-4 w-4" />
新增子卡片
</Button>
) : (
<Badge variant="outline">{`已达 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级`}</Badge>
)}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive"
onClick={() => handleRemoveOption(step.key, option.key)}
>
<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>
<CollapsibleContent>
<div className="mt-4 grid grid-cols-[100px_1fr] gap-4">
<div className="space-y-2">
<div className="aspect-square overflow-hidden rounded-lg border bg-muted">
{option.previewImageUrl ? (
@ -1473,70 +1586,34 @@ export function AgentWorkflowSettingsClient({
</div>
</div>
</div>
</div>
<div className="flex flex-wrap items-center gap-1">
{canAddChild ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleAddOption(step.key, option.key)}
>
<Plus className="mr-2 h-4 w-4" />
新增子卡片
</Button>
) : (
<Badge variant="outline">{`已达 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级`}</Badge>
)}
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-destructive"
onClick={() => handleRemoveOption(step.key, option.key)}
>
<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>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">子卡片配置</div>
<div className="text-xs text-muted-foreground">
{`当前卡片下可继续建立最多 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级卡片母子关系。`}
<div className="mt-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium">子卡片配置</div>
<div className="text-xs text-muted-foreground">
{`当前卡片下可继续建立最多 ${AGENT_WORKFLOW_MAX_OPTION_DEPTH} 级卡片母子关系。`}
</div>
</div>
</div>
</div>
</div>
{sortedChildren.length > 0 ? (
<div className="space-y-4 border-l border-dashed border-border/80 pl-3 sm:pl-4">
{sortedChildren.map((childOption, childIndex) =>
renderWorkflowOptionCard(step, childOption, depth + 1, [
...indexPath,
childIndex + 1,
])
{sortedChildren.length > 0 ? (
<div className="space-y-4 border-l border-dashed border-border/80 pl-3 sm:pl-4">
{sortedChildren.map((childOption, childIndex) =>
renderWorkflowOptionCard(step, childOption, depth + 1, [
...indexPath,
childIndex + 1,
])
)}
</div>
) : (
<div className="rounded-lg border border-dashed bg-background/60 px-4 py-6 text-center text-sm text-muted-foreground">
暂无子卡片
</div>
)}
</div>
) : (
<div className="rounded-lg border border-dashed bg-background/60 px-4 py-6 text-center text-sm text-muted-foreground">
暂无子卡片
</div>
)}
</CollapsibleContent>
</div>
</div>
</Collapsible>
</div>
);
};