feat: add collapsible workflow child settings
This commit is contained in:
parent
478562c312
commit
19c76950db
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user