feat: refine agent workflow settings and studio ui
This commit is contained in:
parent
a4abda497c
commit
7ebfcfd4eb
@ -26,7 +26,13 @@ import {
|
||||
Video,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AGENT_WORKFLOW_MAX_TREE_DEPTH,
|
||||
countAgentWorkflowOptions,
|
||||
countAgentWorkflowSteps,
|
||||
defaultAgentWorkflowUiConfig,
|
||||
findAgentWorkflowStepByKey,
|
||||
flattenAgentWorkflowSteps,
|
||||
normalizeAgentWorkflowStepOrdering,
|
||||
normalizeAgentWorkflowUiConfig,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
@ -78,17 +84,12 @@ const itemVariants = {
|
||||
visible: { opacity: 1, y: 0 }
|
||||
};
|
||||
|
||||
const WORKFLOW_LEVEL_LABELS = ["一级模块", "二级模块", "三级模块"];
|
||||
|
||||
function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig {
|
||||
return {
|
||||
...config,
|
||||
steps: config.steps.map((step, stepIndex) => ({
|
||||
...step,
|
||||
sortOrder: stepIndex,
|
||||
options: step.options.map((option, optionIndex) => ({
|
||||
...option,
|
||||
sortOrder: optionIndex,
|
||||
})),
|
||||
})),
|
||||
steps: normalizeAgentWorkflowStepOrdering(config.steps),
|
||||
};
|
||||
}
|
||||
|
||||
@ -140,20 +141,59 @@ function createWorkflowOptionDraft(
|
||||
}
|
||||
|
||||
function createWorkflowStepDraft(
|
||||
depth: number,
|
||||
stepIndex: number,
|
||||
existingStepKeys: string[]
|
||||
): AgentWorkflowStepUiConfig {
|
||||
const stepKey = ensureUniqueWorkflowKey(`step_${stepIndex + 1}`, existingStepKeys);
|
||||
const levelLabel = WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, 2))] || "模块";
|
||||
const stepKey = ensureUniqueWorkflowKey(`step_${depth}_${stepIndex + 1}`, existingStepKeys);
|
||||
return {
|
||||
key: stepKey,
|
||||
title: `新步骤${stepIndex + 1}`,
|
||||
title: `新${levelLabel}${stepIndex + 1}`,
|
||||
description: "",
|
||||
enabled: true,
|
||||
sortOrder: stepIndex,
|
||||
options: [createWorkflowOptionDraft(stepKey, 0, [])],
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
function updateWorkflowStepTree(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
stepKey: string,
|
||||
updater: (step: AgentWorkflowStepUiConfig) => AgentWorkflowStepUiConfig
|
||||
): AgentWorkflowStepUiConfig[] {
|
||||
return steps.map((step) => {
|
||||
if (step.key === stepKey) {
|
||||
return updater(step);
|
||||
}
|
||||
return {
|
||||
...step,
|
||||
children: updateWorkflowStepTree(step.children || [], stepKey, updater),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function removeWorkflowStepTree(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
stepKey: string
|
||||
): AgentWorkflowStepUiConfig[] {
|
||||
return steps
|
||||
.filter((step) => step.key !== stepKey)
|
||||
.map((step) => ({
|
||||
...step,
|
||||
children: removeWorkflowStepTree(step.children || [], stepKey),
|
||||
}));
|
||||
}
|
||||
|
||||
function collectWorkflowStepKeys(steps: AgentWorkflowStepUiConfig[]): string[] {
|
||||
return flattenAgentWorkflowSteps(steps).map(({ step }) => step.key);
|
||||
}
|
||||
|
||||
function collectDescendantStepKeys(step: AgentWorkflowStepUiConfig): string[] {
|
||||
return [step.key, ...collectWorkflowStepKeys(step.children || [])];
|
||||
}
|
||||
|
||||
interface AgentWorkflowSettingsClientProps {
|
||||
backHref?: string;
|
||||
backLabel?: string;
|
||||
@ -189,7 +229,8 @@ export function AgentWorkflowSettingsClient({
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
setConfig(normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig));
|
||||
const normalizedUiConfig = normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig);
|
||||
setConfig(normalizedUiConfig);
|
||||
setMaterialApiConfig(
|
||||
normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig)
|
||||
);
|
||||
@ -199,9 +240,8 @@ export function AgentWorkflowSettingsClient({
|
||||
setBrandModelConfig(
|
||||
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
|
||||
);
|
||||
// Expand first step by default
|
||||
if (data.agentWorkflowUiConfig?.steps?.[0]) {
|
||||
setExpandedSteps(new Set([data.agentWorkflowUiConfig.steps[0].key]));
|
||||
if (normalizedUiConfig.steps[0]) {
|
||||
setExpandedSteps(new Set([normalizedUiConfig.steps[0].key]));
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
@ -278,11 +318,15 @@ export function AgentWorkflowSettingsClient({
|
||||
};
|
||||
|
||||
const enabledStepsCount = useMemo(
|
||||
() => sortedSteps.filter((s) => s.enabled).length,
|
||||
() => flattenAgentWorkflowSteps(sortedSteps).filter(({ step }) => step.enabled).length,
|
||||
[sortedSteps]
|
||||
);
|
||||
const totalOptionsCount = useMemo(
|
||||
() => sortedSteps.reduce((acc, s) => acc + s.options.filter((o) => o.enabled).length, 0),
|
||||
() =>
|
||||
flattenAgentWorkflowSteps(sortedSteps).reduce(
|
||||
(acc, { step }) => acc + step.options.filter((option) => option.enabled).length,
|
||||
0
|
||||
),
|
||||
[sortedSteps]
|
||||
);
|
||||
|
||||
@ -305,7 +349,7 @@ export function AgentWorkflowSettingsClient({
|
||||
setConfig((prev) =>
|
||||
normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: prev.steps.map((step) => (step.key === stepKey ? updater(step) : step)),
|
||||
steps: updateWorkflowStepTree(prev.steps, stepKey, updater),
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -325,30 +369,59 @@ export function AgentWorkflowSettingsClient({
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddStep = () => {
|
||||
const handleAddStep = (parentStepKey?: string) => {
|
||||
setConfig((prev) => {
|
||||
const newStep = createWorkflowStepDraft(prev.steps.length, prev.steps.map((step) => step.key));
|
||||
// Auto expand new step
|
||||
setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key]));
|
||||
const existingStepKeys = collectWorkflowStepKeys(prev.steps);
|
||||
|
||||
if (!parentStepKey) {
|
||||
const newStep = createWorkflowStepDraft(1, prev.steps.length, existingStepKeys);
|
||||
setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key]));
|
||||
return normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: [...prev.steps, newStep],
|
||||
});
|
||||
}
|
||||
|
||||
const parentStep = findAgentWorkflowStepByKey(prev.steps, parentStepKey);
|
||||
const parentDepth = flattenAgentWorkflowSteps(prev.steps).find(
|
||||
({ step }) => step.key === parentStepKey
|
||||
)?.depth;
|
||||
if (!parentStep || !parentDepth || parentDepth >= AGENT_WORKFLOW_MAX_TREE_DEPTH) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const newChildStep = createWorkflowStepDraft(
|
||||
parentDepth + 1,
|
||||
(parentStep.children || []).length,
|
||||
existingStepKeys
|
||||
);
|
||||
setExpandedSteps(
|
||||
(prevExpanded) => new Set([...prevExpanded, parentStepKey, newChildStep.key])
|
||||
);
|
||||
return normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: [...prev.steps, newStep],
|
||||
steps: updateWorkflowStepTree(prev.steps, parentStepKey, (step) => ({
|
||||
...step,
|
||||
children: [...(step.children || []), newChildStep],
|
||||
})),
|
||||
});
|
||||
});
|
||||
// Switch to workflow tab
|
||||
setActiveTab("workflow");
|
||||
};
|
||||
|
||||
const handleRemoveStep = (stepKey: string) => {
|
||||
const targetStep = findAgentWorkflowStepByKey(config.steps, stepKey);
|
||||
setConfig((prev) =>
|
||||
normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: prev.steps.filter((step) => step.key !== stepKey),
|
||||
steps: removeWorkflowStepTree(prev.steps, stepKey),
|
||||
})
|
||||
);
|
||||
setExpandedSteps((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(stepKey);
|
||||
for (const key of targetStep ? collectDescendantStepKeys(targetStep) : [stepKey]) {
|
||||
next.delete(key);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
@ -428,6 +501,332 @@ export function AgentWorkflowSettingsClient({
|
||||
}
|
||||
};
|
||||
|
||||
const renderWorkflowStepCard = (
|
||||
step: AgentWorkflowStepUiConfig,
|
||||
depth: number,
|
||||
indexPath: number[]
|
||||
) => {
|
||||
const stepLabel = indexPath.join(".");
|
||||
const levelLabel =
|
||||
WORKFLOW_LEVEL_LABELS[Math.max(0, Math.min(depth - 1, WORKFLOW_LEVEL_LABELS.length - 1))] ||
|
||||
`第 ${depth} 级模块`;
|
||||
const childSteps = [...(step.children || [])].sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
const directEnabledOptionCount = step.options.filter((option) => option.enabled).length;
|
||||
const childStepCount = countAgentWorkflowSteps(childSteps);
|
||||
const canAddChild = depth < AGENT_WORKFLOW_MAX_TREE_DEPTH;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={step.key}
|
||||
variants={itemVariants}
|
||||
className={cn(depth > 1 && "pl-4 sm:pl-6")}
|
||||
>
|
||||
<Collapsible
|
||||
open={expandedSteps.has(step.key)}
|
||||
onOpenChange={() => toggleStepExpansion(step.key)}
|
||||
>
|
||||
<Card
|
||||
className={cn(
|
||||
!step.enabled && "opacity-60",
|
||||
depth > 1 && "border-l-4 border-l-emerald-500/30"
|
||||
)}
|
||||
>
|
||||
<CardHeader className="py-4">
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-muted">
|
||||
{expandedSteps.has(step.key) ? (
|
||||
<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-emerald-500/10 px-2 text-xs font-medium text-emerald-600">
|
||||
{stepLabel}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
value={step.title}
|
||||
onChange={(e) =>
|
||||
updateStep(step.key, (currentStep) => ({
|
||||
...currentStep,
|
||||
title: e.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-8 min-w-[180px] max-w-[260px] 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">
|
||||
{step.key}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary">{directEnabledOptionCount} 选项</Badge>
|
||||
<Badge variant="outline">{childStepCount} 子模块</Badge>
|
||||
{canAddChild ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddStep(step.key)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增子级
|
||||
</Button>
|
||||
) : (
|
||||
<Badge variant="outline">已达三级</Badge>
|
||||
)}
|
||||
<Switch
|
||||
checked={step.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateStep(step.key, (currentStep) => ({
|
||||
...currentStep,
|
||||
enabled: checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
onClick={() => handleRemoveStep(step.key)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CollapsibleContent>
|
||||
<CardContent className="border-t bg-muted/20 pt-4">
|
||||
<div className="mb-4 space-y-2">
|
||||
<Label className="text-xs">模块说明</Label>
|
||||
<Textarea
|
||||
value={step.description || ""}
|
||||
onChange={(e) =>
|
||||
updateStep(step.key, (currentStep) => ({
|
||||
...currentStep,
|
||||
description: e.target.value,
|
||||
}))
|
||||
}
|
||||
rows={2}
|
||||
placeholder="描述此模块的用途..."
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">选项配置</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddOption(step.key)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增选项
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{step.options.length > 0 ? (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{step.options
|
||||
.slice()
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((option) => {
|
||||
const refKey = `${step.key}:${option.key}`;
|
||||
const isUploading = uploadingKey === refKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-4",
|
||||
!option.enabled && "opacity-60"
|
||||
)}
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between gap-2">
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
||||
{option.key}
|
||||
</code>
|
||||
<div className="flex items-center gap-1">
|
||||
<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="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 ? (
|
||||
<img
|
||||
src={option.previewImageUrl}
|
||||
alt={option.buttonName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-1 text-muted-foreground">
|
||||
<ImageIcon className="h-6 w-6 opacity-40" />
|
||||
<span className="text-[10px]">无图片</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={(el) => {
|
||||
fileInputRefs.current[refKey] = el;
|
||||
}}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
void handleUploadPreview(step.key, option.key, file);
|
||||
}
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full text-xs"
|
||||
disabled={isUploading}
|
||||
onClick={() => fileInputRefs.current[refKey]?.click()}
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
上传
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">按钮名称</Label>
|
||||
<Input
|
||||
value={option.buttonName}
|
||||
onChange={(e) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
buttonName: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="选项名称"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">说明文案</Label>
|
||||
<Input
|
||||
value={option.description || ""}
|
||||
onChange={(e) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
description: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="简短描述"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Prompt 片段</Label>
|
||||
<Textarea
|
||||
value={option.promptFragment || ""}
|
||||
onChange={(e) =>
|
||||
updateOption(step.key, option.key, (currentOption) => ({
|
||||
...currentOption,
|
||||
promptFragment: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="注入的提示词片段"
|
||||
rows={2}
|
||||
className="resize-none text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed py-8 text-center text-sm text-muted-foreground">
|
||||
暂无选项,点击上方按钮添加
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-6 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">
|
||||
每个模块最多支持三级树形结构,可继续向下建立母子模块关系。
|
||||
</div>
|
||||
</div>
|
||||
{canAddChild ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddStep(step.key)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增子级
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{childSteps.length > 0 ? (
|
||||
<div className="space-y-4 border-l border-dashed border-border/80 pl-3 sm:pl-4">
|
||||
{childSteps.map((childStep, childIndex) =>
|
||||
renderWorkflowStepCard(childStep, 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>
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const updateBrandModelEntry = (
|
||||
brandName: string,
|
||||
updater: (
|
||||
@ -1277,258 +1676,22 @@ export function AgentWorkflowSettingsClient({
|
||||
<motion.div variants={itemVariants} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{sortedSteps.length} 步骤
|
||||
{countAgentWorkflowSteps(sortedSteps)} 模块
|
||||
</Badge>
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{sortedSteps.reduce((acc, s) => acc + s.options.length, 0)} 选项
|
||||
{countAgentWorkflowOptions(sortedSteps)} 选项
|
||||
</Badge>
|
||||
</div>
|
||||
<Button onClick={handleAddStep} size="sm">
|
||||
<Button onClick={() => handleAddStep()} size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增步骤
|
||||
新增一级模块
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
{sortedSteps.length > 0 ? (
|
||||
sortedSteps.map((step, stepIndex) => (
|
||||
<motion.div key={step.key} variants={itemVariants}>
|
||||
<Collapsible
|
||||
open={expandedSteps.has(step.key)}
|
||||
onOpenChange={() => toggleStepExpansion(step.key)}
|
||||
>
|
||||
<Card className={cn(!step.enabled && "opacity-60")}>
|
||||
<CardHeader className="py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<CollapsibleTrigger asChild>
|
||||
<button className="flex h-8 w-8 items-center justify-center rounded-md hover:bg-muted">
|
||||
{expandedSteps.has(step.key) ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-violet-500/10 text-xs font-medium text-violet-600">
|
||||
{stepIndex + 1}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={step.title}
|
||||
onChange={(e) =>
|
||||
updateStep(step.key, (s) => ({ ...s, title: e.target.value }))
|
||||
}
|
||||
className="h-8 w-[200px] border-0 bg-transparent px-0 text-base font-medium shadow-none focus-visible:ring-0"
|
||||
placeholder="步骤标题"
|
||||
/>
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
||||
{step.key}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{step.options.filter((o) => o.enabled).length} 选项
|
||||
</Badge>
|
||||
<Switch
|
||||
checked={step.enabled}
|
||||
onCheckedChange={(checked) =>
|
||||
updateStep(step.key, (s) => ({ ...s, enabled: checked }))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
onClick={() => handleRemoveStep(step.key)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CollapsibleContent>
|
||||
<CardContent className="border-t bg-muted/20 pt-4">
|
||||
<div className="mb-4 space-y-2">
|
||||
<Label className="text-xs">步骤说明</Label>
|
||||
<Textarea
|
||||
value={step.description || ""}
|
||||
onChange={(e) =>
|
||||
updateStep(step.key, (s) => ({ ...s, description: e.target.value }))
|
||||
}
|
||||
rows={2}
|
||||
placeholder="描述此步骤的用途..."
|
||||
className="bg-background"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<span className="text-sm font-medium">选项配置</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleAddOption(step.key)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增选项
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{step.options.length > 0 ? (
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{step.options
|
||||
.slice()
|
||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||
.map((option) => {
|
||||
const refKey = `${step.key}:${option.key}`;
|
||||
const isUploading = uploadingKey === refKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={option.key}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card p-4",
|
||||
!option.enabled && "opacity-60"
|
||||
)}
|
||||
>
|
||||
<div className="mb-4 flex items-start justify-between gap-2">
|
||||
<code className="rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
|
||||
{option.key}
|
||||
</code>
|
||||
<div className="flex items-center gap-1">
|
||||
<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, (o) => ({
|
||||
...o,
|
||||
enabled: checked,
|
||||
}))
|
||||
}
|
||||
className="scale-75"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="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 ? (
|
||||
<img
|
||||
src={option.previewImageUrl}
|
||||
alt={option.buttonName}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-1 text-muted-foreground">
|
||||
<ImageIcon className="h-6 w-6 opacity-40" />
|
||||
<span className="text-[10px]">无图片</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
ref={(el) => { fileInputRefs.current[refKey] = el; }}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) void handleUploadPreview(step.key, option.key, file);
|
||||
e.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="w-full text-xs"
|
||||
disabled={isUploading}
|
||||
onClick={() => fileInputRefs.current[refKey]?.click()}
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
上传
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">按钮名称</Label>
|
||||
<Input
|
||||
value={option.buttonName}
|
||||
onChange={(e) =>
|
||||
updateOption(step.key, option.key, (o) => ({
|
||||
...o,
|
||||
buttonName: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="选项名称"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">说明文案</Label>
|
||||
<Input
|
||||
value={option.description || ""}
|
||||
onChange={(e) =>
|
||||
updateOption(step.key, option.key, (o) => ({
|
||||
...o,
|
||||
description: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="简短描述"
|
||||
className="h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Prompt 片段</Label>
|
||||
<Textarea
|
||||
value={option.promptFragment || ""}
|
||||
onChange={(e) =>
|
||||
updateOption(step.key, option.key, (o) => ({
|
||||
...o,
|
||||
promptFragment: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="注入的提示词片段"
|
||||
rows={2}
|
||||
className="resize-none text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-dashed py-8 text-center text-sm text-muted-foreground">
|
||||
暂无选项,点击上方按钮添加
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</CollapsibleContent>
|
||||
</Card>
|
||||
</Collapsible>
|
||||
</motion.div>
|
||||
))
|
||||
sortedSteps.map((step, stepIndex) =>
|
||||
renderWorkflowStepCard(step, 1, [stepIndex + 1])
|
||||
)
|
||||
) : (
|
||||
<motion.div variants={itemVariants}>
|
||||
<Card>
|
||||
@ -1536,9 +1699,9 @@ export function AgentWorkflowSettingsClient({
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||
<Layers className="h-8 w-8 text-muted-foreground" />
|
||||
</div>
|
||||
<p className="mt-4 text-lg font-medium">还没有配置工作流步骤</p>
|
||||
<p className="mt-4 text-lg font-medium">还没有配置工作流模块</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
点击上方按钮开始创建第一步
|
||||
点击上方按钮开始创建第一个一级模块
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1329,95 +1329,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
||||
<div className="mx-auto flex w-full max-w-[1480px] flex-col gap-6 px-6 py-6 pb-24 lg:px-8 xl:px-10">
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 18 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="rounded-[26px] border border-white/12 bg-white/[0.04] px-4 py-2.5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)] lg:px-5"
|
||||
>
|
||||
<div className="flex flex-col gap-2 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="rounded-full border border-white/10 bg-white/[0.03] px-2.5 py-1 text-[10px] uppercase tracking-[0.22em] text-white/40">
|
||||
Step Flow
|
||||
</span>
|
||||
<h2 className="text-[15px] font-semibold leading-none text-white lg:text-[18px]">
|
||||
第二步:生图模式执行流程
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-[11px] text-white/58">
|
||||
第一步:风格选择
|
||||
<span className="mx-1.5 text-white/22">→</span>
|
||||
<span className="font-medium text-emerald-300">第二步:生图模式</span>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-[11px] text-white/58">
|
||||
当前步骤 {activeToolStepIndex + 1} / {toolSteps.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-dark-scrollbar mt-2 overflow-x-auto pb-0.5">
|
||||
<div className="flex min-w-max items-center gap-2">
|
||||
{toolSteps.map((step, index) => {
|
||||
const Icon = step.icon;
|
||||
const isActive = activeToolStep === step.key;
|
||||
return (
|
||||
<Fragment key={step.key}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleToolStepChange(step.key)}
|
||||
disabled={step.disabled}
|
||||
className={cn(
|
||||
"flex h-[58px] min-w-[148px] items-center gap-2 rounded-[20px] border px-2.5 py-2 text-left transition-all lg:min-w-[158px]",
|
||||
isActive
|
||||
? "border-emerald-400/55 bg-emerald-500/[0.10] shadow-[0_0_0_1px_rgba(16,185,129,0.2)]"
|
||||
: step.completed
|
||||
? "border-emerald-400/30 bg-white/[0.03] hover:border-emerald-400/45"
|
||||
: "border-white/10 bg-black/20 hover:border-white/20",
|
||||
step.disabled && "cursor-not-allowed opacity-45 hover:border-white/10"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-8 w-8 shrink-0 items-center justify-center rounded-[14px] border",
|
||||
isActive
|
||||
? "border-emerald-300/60 bg-emerald-400/20 text-emerald-200"
|
||||
: step.completed
|
||||
? "border-emerald-400/35 bg-emerald-500/10 text-emerald-300"
|
||||
: "border-white/12 bg-white/[0.03] text-white/55"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-[18px] w-[18px]" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate text-sm font-medium leading-none text-white">
|
||||
{step.label}
|
||||
</span>
|
||||
{step.optional ? (
|
||||
<span className="rounded-full bg-white/8 px-1.5 py-0.5 text-[9px] font-normal leading-none text-white/45">
|
||||
可选
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{index < toolSteps.length - 1 ? (
|
||||
<div
|
||||
className={cn(
|
||||
"h-px w-4 shrink-0 rounded-full lg:w-5",
|
||||
step.completed ? "bg-emerald-400/45" : "bg-white/10"
|
||||
)}
|
||||
/>
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</motion.section>
|
||||
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
||||
@ -503,27 +503,27 @@ export function AgentPresetDialog({
|
||||
|
||||
<ScrollArea className="max-h-[72vh] pr-3">
|
||||
<div className="space-y-4">
|
||||
<div className={cn("grid grid-cols-1 gap-3", isDetailReplacementModeBuilder ? "md:grid-cols-1" : "md:grid-cols-2")}>
|
||||
<div className={cn("grid grid-cols-1 gap-5", isDetailReplacementModeBuilder ? "md:grid-cols-1" : "md:grid-cols-2")}>
|
||||
{!isDetailReplacementModeBuilder ? (
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>预览图</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="space-y-2.5">
|
||||
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>预览图</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{avatarUrl ? (
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={`${entityLabel}预览图`}
|
||||
className={cn(
|
||||
"h-24 w-24 rounded-2xl border object-cover shadow-sm",
|
||||
isDark ? "border-white/10" : ""
|
||||
"h-20 w-20 rounded-xl border object-cover shadow-sm",
|
||||
isDark ? "border-white/10" : "border-border"
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-24 w-24 items-center justify-center rounded-2xl border text-sm shadow-sm",
|
||||
"flex h-20 w-20 items-center justify-center rounded-xl border text-xs shadow-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white/35"
|
||||
: "bg-muted text-muted-foreground"
|
||||
? "border-white/10 bg-black/30 text-white/40"
|
||||
: "bg-muted text-muted-foreground border-border"
|
||||
)}
|
||||
>
|
||||
未上传
|
||||
@ -544,7 +544,7 @@ export function AgentPresetDialog({
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"h-11 px-5 text-base",
|
||||
"h-9 px-4 text-sm gap-1.5",
|
||||
isDark
|
||||
? "border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||
: ""
|
||||
@ -553,9 +553,9 @@ export function AgentPresetDialog({
|
||||
disabled={avatarUploading}
|
||||
>
|
||||
{avatarUploading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
<Upload className="h-3.5 w-3.5" />
|
||||
)}
|
||||
上传预览图
|
||||
</Button>
|
||||
@ -563,31 +563,33 @@ export function AgentPresetDialog({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>
|
||||
<div className="space-y-2.5">
|
||||
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||
{entityNameLabel}
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
className={cn(
|
||||
"h-10",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white placeholder:text-white/22 focus-visible:ring-emerald-400/35"
|
||||
? "border-white/10 bg-black/30 text-white placeholder:text-white/25 focus-visible:ring-emerald-400/35"
|
||||
: ""
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("grid grid-cols-1 gap-3", type === "image" ? "md:grid-cols-4" : "md:grid-cols-3")}>
|
||||
<div className={cn("grid grid-cols-1 gap-4", type === "image" ? "md:grid-cols-4" : "md:grid-cols-3")}>
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>可见性</div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>可见性</div>
|
||||
<Select
|
||||
value={visibility}
|
||||
onValueChange={(value: "public" | "private") => setVisibility(value)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
@ -603,7 +605,7 @@ export function AgentPresetDialog({
|
||||
</div>
|
||||
{type === "image" ? (
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>模式类型</div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>模式类型</div>
|
||||
<Select
|
||||
value={modeConfig.modeType}
|
||||
onValueChange={(value: AgentPresetModeType) =>
|
||||
@ -615,6 +617,7 @@ export function AgentPresetDialog({
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
@ -637,7 +640,7 @@ export function AgentPresetDialog({
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>类型</div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>类型</div>
|
||||
<Select
|
||||
value={type}
|
||||
onValueChange={(value: AgentPresetType) => {
|
||||
@ -647,6 +650,7 @@ export function AgentPresetDialog({
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
@ -662,10 +666,11 @@ export function AgentPresetDialog({
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>对应模型</div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>对应模型</div>
|
||||
<Select value={defaultModelId} onValueChange={setDefaultModelId}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
@ -691,14 +696,14 @@ export function AgentPresetDialog({
|
||||
{type === "image" ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl border p-3",
|
||||
isDark ? "border-white/10 bg-black/20" : "border-slate-200 bg-muted/20"
|
||||
"rounded-xl border px-4 py-3.5",
|
||||
isDark ? "border-white/10 bg-white/[0.02]" : "border-slate-200 bg-muted/30"
|
||||
)}
|
||||
>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>
|
||||
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||
当前模式:{modeConfig.modeType === "legacy" ? "旧版兼容" : selectedImageModeMeta.label}
|
||||
</div>
|
||||
<div className={cn("mt-1 text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
||||
<div className={cn("mt-1.5 text-sm leading-relaxed", isDark ? "text-white/50" : "text-muted-foreground")}>
|
||||
{modeConfig.modeType === "legacy"
|
||||
? "当前模板沿用旧版多槽位执行逻辑。切换到新模式后,将按新的业务流程配置。"
|
||||
: selectedImageModeMeta.description}
|
||||
@ -709,15 +714,15 @@ export function AgentPresetDialog({
|
||||
{type === "image" && modeConfig.modeType !== "legacy" ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-2xl border p-4",
|
||||
isDark ? "border-white/10 bg-black/20" : "border-slate-200 bg-muted/20"
|
||||
"rounded-xl border p-5",
|
||||
isDark ? "border-white/10 bg-white/[0.02]" : "border-slate-200 bg-muted/30"
|
||||
)}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>
|
||||
<div className="mb-5">
|
||||
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||
{selectedImageModeMeta.label}配置
|
||||
</div>
|
||||
<div className={cn("mt-1 text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
||||
<div className={cn("mt-1.5 text-sm leading-relaxed", isDark ? "text-white/50" : "text-muted-foreground")}>
|
||||
{isSceneModeBuilder
|
||||
? "第一步提示词组合 + 自定义提示词 + 输入图片 + 模板参考图,共同组成生图输入。"
|
||||
: isDetailReplacementModeBuilder
|
||||
@ -727,9 +732,9 @@ export function AgentPresetDialog({
|
||||
</div>
|
||||
|
||||
{isSceneModeBuilder ? (
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>产品图槽位数量</div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>产品图槽位数量</div>
|
||||
<Select
|
||||
value={String(modeConfig.scene.productImageSlotCount)}
|
||||
onValueChange={(value) =>
|
||||
@ -745,6 +750,7 @@ export function AgentPresetDialog({
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
@ -766,11 +772,14 @@ export function AgentPresetDialog({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
||||
<div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>模特图槽位</div>
|
||||
<div className={cn("text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className={cn(
|
||||
"flex items-center justify-between rounded-lg border px-3.5 py-3",
|
||||
isDark ? "border-white/10 bg-white/[0.03]" : "border-border bg-background"
|
||||
)}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white" : "text-foreground")}>模特图槽位</div>
|
||||
<div className={cn("text-xs", isDark ? "text-white/45" : "text-muted-foreground")}>
|
||||
开启后会额外保留 1 个模特图槽位
|
||||
</div>
|
||||
</div>
|
||||
@ -786,13 +795,17 @@ export function AgentPresetDialog({
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
||||
<div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>允许模板参考图</div>
|
||||
<div className={cn("text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
||||
<div className={cn(
|
||||
"flex items-center justify-between rounded-lg border px-3.5 py-3",
|
||||
isDark ? "border-white/10 bg-white/[0.03]" : "border-border bg-background"
|
||||
)}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white" : "text-foreground")}>允许模板参考图</div>
|
||||
<div className={cn("text-xs", isDark ? "text-white/45" : "text-muted-foreground")}>
|
||||
不强制,但可以预置风格参考图
|
||||
</div>
|
||||
</div>
|
||||
@ -808,13 +821,17 @@ export function AgentPresetDialog({
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
||||
<div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>允许用户切换模型</div>
|
||||
<div className={cn("text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
||||
<div className={cn(
|
||||
"flex items-center justify-between rounded-lg border px-3.5 py-3",
|
||||
isDark ? "border-white/10 bg-white/[0.03]" : "border-border bg-background"
|
||||
)}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white" : "text-foreground")}>允许用户切换模型</div>
|
||||
<div className={cn("text-xs", isDark ? "text-white/45" : "text-muted-foreground")}>
|
||||
关闭后第二步直接锁定模板模型
|
||||
</div>
|
||||
</div>
|
||||
@ -830,6 +847,7 @@ export function AgentPresetDialog({
|
||||
},
|
||||
}))
|
||||
}
|
||||
className="shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -1247,11 +1265,12 @@ export function AgentPresetDialog({
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<DialogFooter className="gap-2 pt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
className={cn(
|
||||
"h-9 px-5 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||
: ""
|
||||
@ -1263,12 +1282,13 @@ export function AgentPresetDialog({
|
||||
onClick={handleCreate}
|
||||
disabled={isSubmitting}
|
||||
className={cn(
|
||||
"h-9 px-5 text-sm gap-1.5",
|
||||
isDark
|
||||
? "bg-gradient-to-r from-emerald-500 to-teal-500 text-white hover:from-emerald-400 hover:to-teal-400"
|
||||
: "bg-black text-white hover:bg-black/90 dark:bg-white dark:text-black dark:hover:bg-white/90"
|
||||
)}
|
||||
>
|
||||
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
{isSubmitting ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@ -8,7 +8,11 @@ import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenAgentWorkflowSteps,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
||||
import {
|
||||
@ -52,7 +56,7 @@ function buildSelectionSummary(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}) {
|
||||
const result: AgentWorkflowComposerSelection[] = [];
|
||||
for (const step of params.config.steps) {
|
||||
for (const { step } of flattenAgentWorkflowSteps(params.config.steps)) {
|
||||
const selectedOptionKey = params.selections[step.key];
|
||||
if (!selectedOptionKey) continue;
|
||||
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
||||
@ -183,18 +187,16 @@ export function AgentWorkflowPanelDarkV2({
|
||||
const resolvedConfig = useMemo(() => {
|
||||
const source = config || defaultAgentWorkflowUiConfig;
|
||||
return {
|
||||
steps: [...source.steps]
|
||||
.filter((step) => step.enabled)
|
||||
.map((step) => ({
|
||||
...step,
|
||||
options: [...step.options]
|
||||
.filter((option) => option.enabled)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
}))
|
||||
.filter((step) => step.options.length > 0)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
steps: getEnabledAgentWorkflowStepTree(source.steps || []),
|
||||
};
|
||||
}, [config]);
|
||||
const flattenedResolvedSteps = useMemo(
|
||||
() =>
|
||||
flattenAgentWorkflowSteps(resolvedConfig.steps).filter(
|
||||
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
||||
),
|
||||
[resolvedConfig.steps]
|
||||
);
|
||||
|
||||
const summary = useMemo(
|
||||
() => buildSelectionSummary({ config: resolvedConfig, selections }),
|
||||
@ -300,10 +302,13 @@ export function AgentWorkflowPanelDarkV2({
|
||||
</motion.section>
|
||||
|
||||
{/* 工作流步骤 */}
|
||||
{resolvedConfig.steps.map((step, stepIndex) => (
|
||||
{flattenedResolvedSteps.map(({ step, depth }, stepIndex) => (
|
||||
<motion.section
|
||||
key={step.key}
|
||||
className="rounded-2xl border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
|
||||
className={cn(
|
||||
"rounded-2xl border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]",
|
||||
depth > 1 && "ml-3 border-l-4 border-l-emerald-500/30 sm:ml-6"
|
||||
)}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.15 + stepIndex * 0.05 }}
|
||||
@ -313,6 +318,11 @@ export function AgentWorkflowPanelDarkV2({
|
||||
{stepIndex + 2}
|
||||
</span>
|
||||
{step.title}
|
||||
{depth > 1 ? (
|
||||
<span className="rounded-full border border-white/12 px-2 py-0.5 text-[11px] font-medium text-white/45">
|
||||
{depth === 2 ? "二级模块" : "三级模块"}
|
||||
</span>
|
||||
) : null}
|
||||
</h2>
|
||||
{step.description && (
|
||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenAgentWorkflowSteps,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import type {
|
||||
AgentWorkflowComposerContext,
|
||||
AgentWorkflowMaterialSelections,
|
||||
@ -23,12 +27,12 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}): AgentWorkflowComposerContext | null {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const steps = (Array.isArray(config.steps) ? config.steps : []).filter(
|
||||
(step) =>
|
||||
step.enabled !== false && Array.isArray(step.options) && step.options.some((item) => item.enabled !== false)
|
||||
);
|
||||
const steps = flattenAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
|
||||
)
|
||||
.map(({ step }) => step)
|
||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||
const selections = steps
|
||||
.filter((step) => step.enabled !== false)
|
||||
.map((step) => {
|
||||
const selectedKey = params.selections[step.key];
|
||||
if (!selectedKey) return null;
|
||||
@ -219,10 +223,11 @@ export function getAgentWorkflowProgress(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}) {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const enabledSteps = (config.steps || []).filter(
|
||||
(step) =>
|
||||
step.enabled !== false && Array.isArray(step.options) && step.options.some((item) => item.enabled !== false)
|
||||
);
|
||||
const enabledSteps = flattenAgentWorkflowSteps(
|
||||
getEnabledAgentWorkflowStepTree(config.steps || [])
|
||||
)
|
||||
.map(({ step }) => step)
|
||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||
const completedSteps = enabledSteps.filter((step) => {
|
||||
const value = params.selections[step.key];
|
||||
return Boolean(String(value || "").trim());
|
||||
|
||||
@ -5,6 +5,13 @@ import type {
|
||||
} from "@/types/agent";
|
||||
|
||||
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 3;
|
||||
|
||||
export interface FlattenedAgentWorkflowStep {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
depth: number;
|
||||
parentKeys: string[];
|
||||
}
|
||||
|
||||
function cloneOption(
|
||||
option: AgentWorkflowOptionUiConfig
|
||||
@ -22,6 +29,7 @@ function cloneStep(step: AgentWorkflowStepUiConfig): AgentWorkflowStepUiConfig {
|
||||
...step,
|
||||
description: step.description || "",
|
||||
options: step.options.map(cloneOption),
|
||||
children: (step.children || []).map(cloneStep),
|
||||
};
|
||||
}
|
||||
|
||||
@ -38,6 +46,7 @@ function createStep(
|
||||
enabled: true,
|
||||
sortOrder: 0,
|
||||
options,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -129,6 +138,78 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
|
||||
),
|
||||
};
|
||||
|
||||
export function flattenAgentWorkflowSteps(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
depth = 1,
|
||||
parentKeys: string[] = []
|
||||
): FlattenedAgentWorkflowStep[] {
|
||||
return steps.flatMap((step) => [
|
||||
{
|
||||
step,
|
||||
depth,
|
||||
parentKeys,
|
||||
},
|
||||
...flattenAgentWorkflowSteps(step.children || [], depth + 1, [...parentKeys, step.key]),
|
||||
]);
|
||||
}
|
||||
|
||||
export function findAgentWorkflowStepByKey(
|
||||
steps: AgentWorkflowStepUiConfig[],
|
||||
stepKey: string
|
||||
): AgentWorkflowStepUiConfig | null {
|
||||
for (const step of steps) {
|
||||
if (step.key === stepKey) {
|
||||
return step;
|
||||
}
|
||||
const foundChild = findAgentWorkflowStepByKey(step.children || [], stepKey);
|
||||
if (foundChild) {
|
||||
return foundChild;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function countAgentWorkflowSteps(steps: AgentWorkflowStepUiConfig[]): number {
|
||||
return flattenAgentWorkflowSteps(steps).length;
|
||||
}
|
||||
|
||||
export function countAgentWorkflowOptions(steps: AgentWorkflowStepUiConfig[]): number {
|
||||
return flattenAgentWorkflowSteps(steps).reduce(
|
||||
(total, item) => total + (item.step.options?.length || 0),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeAgentWorkflowStepOrdering(
|
||||
steps: AgentWorkflowStepUiConfig[]
|
||||
): AgentWorkflowStepUiConfig[] {
|
||||
return steps.map((step, stepIndex) => ({
|
||||
...step,
|
||||
sortOrder: stepIndex,
|
||||
options: (step.options || []).map((option, optionIndex) => ({
|
||||
...option,
|
||||
sortOrder: optionIndex,
|
||||
})),
|
||||
children: normalizeAgentWorkflowStepOrdering(step.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getEnabledAgentWorkflowStepTree(
|
||||
steps: AgentWorkflowStepUiConfig[]
|
||||
): AgentWorkflowStepUiConfig[] {
|
||||
return [...steps]
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.filter((step) => step.enabled !== false)
|
||||
.map((step) => ({
|
||||
...step,
|
||||
options: [...(step.options || [])]
|
||||
.filter((option) => option.enabled !== false)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
||||
}))
|
||||
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown, maxLength: number) {
|
||||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
@ -178,97 +259,116 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
? (raw.inputOptions as Record<string, unknown>)
|
||||
: {};
|
||||
const defaultStepByKey = new Map(
|
||||
defaultAgentWorkflowUiConfig.steps.map((step) => [step.key, step] as const)
|
||||
flattenAgentWorkflowSteps(defaultAgentWorkflowUiConfig.steps).map(({ step }) => [
|
||||
step.key,
|
||||
step,
|
||||
] as const)
|
||||
);
|
||||
const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
||||
const usedStepKeys = new Set<string>();
|
||||
|
||||
const normalizeStep = (
|
||||
item: unknown,
|
||||
stepIndex: number,
|
||||
depth: number
|
||||
): AgentWorkflowStepUiConfig => {
|
||||
const stepRecord =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: null;
|
||||
const candidateKey = normalizeKey(stepRecord?.key, `step_${stepIndex + 1}`);
|
||||
let stepKey = candidateKey;
|
||||
let duplicateIndex = 2;
|
||||
while (usedStepKeys.has(stepKey)) {
|
||||
stepKey = `${candidateKey}_${duplicateIndex}`;
|
||||
duplicateIndex += 1;
|
||||
}
|
||||
usedStepKeys.add(stepKey);
|
||||
|
||||
const defaultStep = defaultStepByKey.get(stepKey);
|
||||
const fallbackTitle = defaultStep?.title || `步骤${stepIndex + 1}`;
|
||||
const fallbackDescription = defaultStep?.description || "";
|
||||
const hasRawOptions = Array.isArray(stepRecord?.options);
|
||||
const rawOptions = hasRawOptions ? (stepRecord?.options as unknown[]) : [];
|
||||
const defaultOptions = !hasRawOptions && defaultStep?.options ? defaultStep.options : [];
|
||||
const optionSource = rawOptions.length > 0 || hasRawOptions ? rawOptions : defaultOptions;
|
||||
const usedOptionKeys = new Set<string>();
|
||||
|
||||
const options = optionSource
|
||||
.map((option, optionIndex) => {
|
||||
const optionRecord =
|
||||
option && typeof option === "object" && !Array.isArray(option)
|
||||
? (option as Record<string, unknown>)
|
||||
: null;
|
||||
const defaultOption = defaultStep?.options.find(
|
||||
(defaultItem) => defaultItem.key === normalizeText(optionRecord?.key, 64)
|
||||
);
|
||||
const optionBaseKey = normalizeKey(
|
||||
optionRecord?.key,
|
||||
defaultOption?.key || `${stepKey}_option_${optionIndex + 1}`
|
||||
);
|
||||
let optionKey = optionBaseKey;
|
||||
let optionDuplicateIndex = 2;
|
||||
while (usedOptionKeys.has(optionKey)) {
|
||||
optionKey = `${optionBaseKey}_${optionDuplicateIndex}`;
|
||||
optionDuplicateIndex += 1;
|
||||
}
|
||||
usedOptionKeys.add(optionKey);
|
||||
|
||||
const buttonName =
|
||||
normalizeText(optionRecord?.buttonName, 64) ||
|
||||
defaultOption?.buttonName ||
|
||||
`选项${optionIndex + 1}`;
|
||||
const promptFragment =
|
||||
normalizeText(optionRecord?.promptFragment, 1000) ||
|
||||
defaultOption?.promptFragment ||
|
||||
buttonName;
|
||||
|
||||
return {
|
||||
key: optionKey,
|
||||
buttonName,
|
||||
description:
|
||||
normalizeText(optionRecord?.description, 300) || defaultOption?.description || "",
|
||||
previewImageUrl: normalizeText(optionRecord?.previewImageUrl, 1000),
|
||||
enabled: normalizeBoolean(optionRecord?.enabled, defaultOption?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(
|
||||
optionRecord?.sortOrder,
|
||||
defaultOption?.sortOrder ?? optionIndex
|
||||
),
|
||||
promptFragment,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
|
||||
const hasRawChildren = Array.isArray(stepRecord?.children);
|
||||
const rawChildren = hasRawChildren ? (stepRecord?.children as unknown[]) : [];
|
||||
const defaultChildren =
|
||||
!hasRawChildren && defaultStep?.children ? defaultStep.children : [];
|
||||
const childSource =
|
||||
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH && (rawChildren.length > 0 || hasRawChildren)
|
||||
? rawChildren
|
||||
: depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
|
||||
? defaultChildren
|
||||
: [];
|
||||
|
||||
return {
|
||||
key: stepKey,
|
||||
title: normalizeText(stepRecord?.title, 64) || fallbackTitle,
|
||||
description: normalizeText(stepRecord?.description, 300) || fallbackDescription,
|
||||
enabled: normalizeBoolean(stepRecord?.enabled, defaultStep?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(stepRecord?.sortOrder, defaultStep?.sortOrder ?? stepIndex),
|
||||
options,
|
||||
children:
|
||||
depth < AGENT_WORKFLOW_MAX_TREE_DEPTH
|
||||
? childSource
|
||||
.map((child, childIndex) => normalizeStep(child, childIndex, depth + 1))
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
||||
const normalizedSteps = sourceSteps
|
||||
.map((item, stepIndex) => {
|
||||
const stepRecord =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: null;
|
||||
const candidateKey = normalizeKey(stepRecord?.key, `step_${stepIndex + 1}`);
|
||||
let stepKey = candidateKey;
|
||||
let duplicateIndex = 2;
|
||||
while (usedStepKeys.has(stepKey)) {
|
||||
stepKey = `${candidateKey}_${duplicateIndex}`;
|
||||
duplicateIndex += 1;
|
||||
}
|
||||
usedStepKeys.add(stepKey);
|
||||
|
||||
const defaultStep = defaultStepByKey.get(stepKey);
|
||||
const fallbackTitle = defaultStep?.title || `步骤${stepIndex + 1}`;
|
||||
const fallbackDescription = defaultStep?.description || "";
|
||||
const hasRawOptions = Array.isArray(stepRecord?.options);
|
||||
const rawOptions = hasRawOptions ? (stepRecord?.options as unknown[]) : [];
|
||||
const defaultOptions =
|
||||
!hasRawOptions && defaultStep?.options ? defaultStep.options : [];
|
||||
const optionSource = rawOptions.length > 0 || hasRawOptions ? rawOptions : defaultOptions;
|
||||
const usedOptionKeys = new Set<string>();
|
||||
|
||||
const options = optionSource
|
||||
.map((option, optionIndex) => {
|
||||
const optionRecord =
|
||||
option && typeof option === "object" && !Array.isArray(option)
|
||||
? (option as Record<string, unknown>)
|
||||
: null;
|
||||
const defaultOption = defaultStep?.options.find(
|
||||
(item) => item.key === normalizeText(optionRecord?.key, 64)
|
||||
);
|
||||
const optionBaseKey = normalizeKey(
|
||||
optionRecord?.key,
|
||||
defaultOption?.key || `${stepKey}_option_${optionIndex + 1}`
|
||||
);
|
||||
let optionKey = optionBaseKey;
|
||||
let optionDuplicateIndex = 2;
|
||||
while (usedOptionKeys.has(optionKey)) {
|
||||
optionKey = `${optionBaseKey}_${optionDuplicateIndex}`;
|
||||
optionDuplicateIndex += 1;
|
||||
}
|
||||
usedOptionKeys.add(optionKey);
|
||||
|
||||
const buttonName =
|
||||
normalizeText(optionRecord?.buttonName, 64) ||
|
||||
defaultOption?.buttonName ||
|
||||
`选项${optionIndex + 1}`;
|
||||
const promptFragment =
|
||||
normalizeText(optionRecord?.promptFragment, 1000) ||
|
||||
defaultOption?.promptFragment ||
|
||||
buttonName;
|
||||
|
||||
return {
|
||||
key: optionKey,
|
||||
buttonName,
|
||||
description:
|
||||
normalizeText(optionRecord?.description, 300) ||
|
||||
defaultOption?.description ||
|
||||
"",
|
||||
previewImageUrl: normalizeText(optionRecord?.previewImageUrl, 1000),
|
||||
enabled: normalizeBoolean(optionRecord?.enabled, defaultOption?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(
|
||||
optionRecord?.sortOrder,
|
||||
defaultOption?.sortOrder ?? optionIndex
|
||||
),
|
||||
promptFragment,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
|
||||
return {
|
||||
key: stepKey,
|
||||
title: normalizeText(stepRecord?.title, 64) || fallbackTitle,
|
||||
description:
|
||||
normalizeText(stepRecord?.description, 300) || fallbackDescription,
|
||||
enabled: normalizeBoolean(stepRecord?.enabled, defaultStep?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(
|
||||
stepRecord?.sortOrder,
|
||||
defaultStep?.sortOrder ?? stepIndex
|
||||
),
|
||||
options,
|
||||
};
|
||||
})
|
||||
.map((item, stepIndex) => normalizeStep(item, stepIndex, 1))
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
|
||||
return {
|
||||
@ -276,6 +376,6 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
brands: normalizeStringList(rawInputOptions.brands),
|
||||
stores: normalizeStringList(rawInputOptions.stores),
|
||||
},
|
||||
steps: normalizedSteps.sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
steps: normalizedSteps,
|
||||
};
|
||||
}
|
||||
|
||||
@ -60,6 +60,7 @@ export interface AgentWorkflowStepUiConfig {
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
options: AgentWorkflowOptionUiConfig[];
|
||||
children?: AgentWorkflowStepUiConfig[];
|
||||
}
|
||||
|
||||
export interface AgentWorkflowUiConfig {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user