feat: refine agent workflow settings and studio ui
This commit is contained in:
parent
a4abda497c
commit
7ebfcfd4eb
@ -26,7 +26,13 @@ import {
|
|||||||
Video,
|
Video,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
AGENT_WORKFLOW_MAX_TREE_DEPTH,
|
||||||
|
countAgentWorkflowOptions,
|
||||||
|
countAgentWorkflowSteps,
|
||||||
defaultAgentWorkflowUiConfig,
|
defaultAgentWorkflowUiConfig,
|
||||||
|
findAgentWorkflowStepByKey,
|
||||||
|
flattenAgentWorkflowSteps,
|
||||||
|
normalizeAgentWorkflowStepOrdering,
|
||||||
normalizeAgentWorkflowUiConfig,
|
normalizeAgentWorkflowUiConfig,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import {
|
import {
|
||||||
@ -78,17 +84,12 @@ const itemVariants = {
|
|||||||
visible: { opacity: 1, y: 0 }
|
visible: { opacity: 1, y: 0 }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WORKFLOW_LEVEL_LABELS = ["一级模块", "二级模块", "三级模块"];
|
||||||
|
|
||||||
function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig {
|
function normalizeConfigOrdering(config: AgentWorkflowUiConfig): AgentWorkflowUiConfig {
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
steps: config.steps.map((step, stepIndex) => ({
|
steps: normalizeAgentWorkflowStepOrdering(config.steps),
|
||||||
...step,
|
|
||||||
sortOrder: stepIndex,
|
|
||||||
options: step.options.map((option, optionIndex) => ({
|
|
||||||
...option,
|
|
||||||
sortOrder: optionIndex,
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,20 +141,59 @@ function createWorkflowOptionDraft(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createWorkflowStepDraft(
|
function createWorkflowStepDraft(
|
||||||
|
depth: number,
|
||||||
stepIndex: number,
|
stepIndex: number,
|
||||||
existingStepKeys: string[]
|
existingStepKeys: string[]
|
||||||
): AgentWorkflowStepUiConfig {
|
): 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 {
|
return {
|
||||||
key: stepKey,
|
key: stepKey,
|
||||||
title: `新步骤${stepIndex + 1}`,
|
title: `新${levelLabel}${stepIndex + 1}`,
|
||||||
description: "",
|
description: "",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sortOrder: stepIndex,
|
sortOrder: stepIndex,
|
||||||
options: [createWorkflowOptionDraft(stepKey, 0, [])],
|
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 {
|
interface AgentWorkflowSettingsClientProps {
|
||||||
backHref?: string;
|
backHref?: string;
|
||||||
backLabel?: string;
|
backLabel?: string;
|
||||||
@ -189,7 +229,8 @@ export function AgentWorkflowSettingsClient({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!data) return;
|
if (!data) return;
|
||||||
setConfig(normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig));
|
const normalizedUiConfig = normalizeAgentWorkflowUiConfig(data.agentWorkflowUiConfig);
|
||||||
|
setConfig(normalizedUiConfig);
|
||||||
setMaterialApiConfig(
|
setMaterialApiConfig(
|
||||||
normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig)
|
normalizeAgentWorkflowMaterialApiConfig(data.agentWorkflowMaterialApiConfig)
|
||||||
);
|
);
|
||||||
@ -199,9 +240,8 @@ export function AgentWorkflowSettingsClient({
|
|||||||
setBrandModelConfig(
|
setBrandModelConfig(
|
||||||
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
|
normalizeAgentWorkflowBrandModelConfig(data.agentWorkflowBrandModelConfig)
|
||||||
);
|
);
|
||||||
// Expand first step by default
|
if (normalizedUiConfig.steps[0]) {
|
||||||
if (data.agentWorkflowUiConfig?.steps?.[0]) {
|
setExpandedSteps(new Set([normalizedUiConfig.steps[0].key]));
|
||||||
setExpandedSteps(new Set([data.agentWorkflowUiConfig.steps[0].key]));
|
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
@ -278,11 +318,15 @@ export function AgentWorkflowSettingsClient({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const enabledStepsCount = useMemo(
|
const enabledStepsCount = useMemo(
|
||||||
() => sortedSteps.filter((s) => s.enabled).length,
|
() => flattenAgentWorkflowSteps(sortedSteps).filter(({ step }) => step.enabled).length,
|
||||||
[sortedSteps]
|
[sortedSteps]
|
||||||
);
|
);
|
||||||
const totalOptionsCount = useMemo(
|
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]
|
[sortedSteps]
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -305,7 +349,7 @@ export function AgentWorkflowSettingsClient({
|
|||||||
setConfig((prev) =>
|
setConfig((prev) =>
|
||||||
normalizeConfigOrdering({
|
normalizeConfigOrdering({
|
||||||
...prev,
|
...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) => {
|
setConfig((prev) => {
|
||||||
const newStep = createWorkflowStepDraft(prev.steps.length, prev.steps.map((step) => step.key));
|
const existingStepKeys = collectWorkflowStepKeys(prev.steps);
|
||||||
// Auto expand new step
|
|
||||||
setExpandedSteps((prevExpanded) => new Set([...prevExpanded, newStep.key]));
|
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({
|
return normalizeConfigOrdering({
|
||||||
...prev,
|
...prev,
|
||||||
steps: [...prev.steps, newStep],
|
steps: updateWorkflowStepTree(prev.steps, parentStepKey, (step) => ({
|
||||||
|
...step,
|
||||||
|
children: [...(step.children || []), newChildStep],
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// Switch to workflow tab
|
|
||||||
setActiveTab("workflow");
|
setActiveTab("workflow");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveStep = (stepKey: string) => {
|
const handleRemoveStep = (stepKey: string) => {
|
||||||
|
const targetStep = findAgentWorkflowStepByKey(config.steps, stepKey);
|
||||||
setConfig((prev) =>
|
setConfig((prev) =>
|
||||||
normalizeConfigOrdering({
|
normalizeConfigOrdering({
|
||||||
...prev,
|
...prev,
|
||||||
steps: prev.steps.filter((step) => step.key !== stepKey),
|
steps: removeWorkflowStepTree(prev.steps, stepKey),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
setExpandedSteps((prev) => {
|
setExpandedSteps((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.delete(stepKey);
|
for (const key of targetStep ? collectDescendantStepKeys(targetStep) : [stepKey]) {
|
||||||
|
next.delete(key);
|
||||||
|
}
|
||||||
return next;
|
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 = (
|
const updateBrandModelEntry = (
|
||||||
brandName: string,
|
brandName: string,
|
||||||
updater: (
|
updater: (
|
||||||
@ -1277,258 +1676,22 @@ export function AgentWorkflowSettingsClient({
|
|||||||
<motion.div variants={itemVariants} className="flex items-center justify-between">
|
<motion.div variants={itemVariants} className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="font-mono">
|
<Badge variant="outline" className="font-mono">
|
||||||
{sortedSteps.length} 步骤
|
{countAgentWorkflowSteps(sortedSteps)} 模块
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="font-mono">
|
<Badge variant="outline" className="font-mono">
|
||||||
{sortedSteps.reduce((acc, s) => acc + s.options.length, 0)} 选项
|
{countAgentWorkflowOptions(sortedSteps)} 选项
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleAddStep} size="sm">
|
<Button onClick={() => handleAddStep()} size="sm">
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
新增步骤
|
新增一级模块
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{sortedSteps.length > 0 ? (
|
{sortedSteps.length > 0 ? (
|
||||||
sortedSteps.map((step, stepIndex) => (
|
sortedSteps.map((step, stepIndex) =>
|
||||||
<motion.div key={step.key} variants={itemVariants}>
|
renderWorkflowStepCard(step, 1, [stepIndex + 1])
|
||||||
<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>
|
|
||||||
))
|
|
||||||
) : (
|
) : (
|
||||||
<motion.div variants={itemVariants}>
|
<motion.div variants={itemVariants}>
|
||||||
<Card>
|
<Card>
|
||||||
@ -1536,9 +1699,9 @@ export function AgentWorkflowSettingsClient({
|
|||||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-muted">
|
||||||
<Layers className="h-8 w-8 text-muted-foreground" />
|
<Layers className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
</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 className="text-sm text-muted-foreground">
|
||||||
点击上方按钮开始创建第一步
|
点击上方按钮开始创建第一个一级模块
|
||||||
</p>
|
</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1329,95 +1329,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 overflow-y-auto overscroll-contain pt-16">
|
<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">
|
<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
|
<motion.section
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|||||||
@ -503,27 +503,27 @@ export function AgentPresetDialog({
|
|||||||
|
|
||||||
<ScrollArea className="max-h-[72vh] pr-3">
|
<ScrollArea className="max-h-[72vh] pr-3">
|
||||||
<div className="space-y-4">
|
<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 ? (
|
{!isDetailReplacementModeBuilder ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>预览图</div>
|
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>预览图</div>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-center gap-3">
|
||||||
{avatarUrl ? (
|
{avatarUrl ? (
|
||||||
<img
|
<img
|
||||||
src={avatarUrl}
|
src={avatarUrl}
|
||||||
alt={`${entityLabel}预览图`}
|
alt={`${entityLabel}预览图`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-24 w-24 rounded-2xl border object-cover shadow-sm",
|
"h-20 w-20 rounded-xl border object-cover shadow-sm",
|
||||||
isDark ? "border-white/10" : ""
|
isDark ? "border-white/10" : "border-border"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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
|
isDark
|
||||||
? "border-white/10 bg-black/30 text-white/35"
|
? "border-white/10 bg-black/30 text-white/40"
|
||||||
: "bg-muted text-muted-foreground"
|
: "bg-muted text-muted-foreground border-border"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
未上传
|
未上传
|
||||||
@ -544,7 +544,7 @@ export function AgentPresetDialog({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-11 px-5 text-base",
|
"h-9 px-4 text-sm gap-1.5",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
? "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}
|
disabled={avatarUploading}
|
||||||
>
|
>
|
||||||
{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>
|
</Button>
|
||||||
@ -563,31 +563,33 @@ export function AgentPresetDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2.5">
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>
|
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||||
{entityNameLabel}
|
{entityNameLabel}
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(event) => setName(event.target.value)}
|
onChange={(event) => setName(event.target.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-10",
|
||||||
isDark
|
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>
|
</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="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
|
<Select
|
||||||
value={visibility}
|
value={visibility}
|
||||||
onValueChange={(value: "public" | "private") => setVisibility(value)}
|
onValueChange={(value: "public" | "private") => setVisibility(value)}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 text-sm",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||||
: ""
|
: ""
|
||||||
@ -603,7 +605,7 @@ export function AgentPresetDialog({
|
|||||||
</div>
|
</div>
|
||||||
{type === "image" ? (
|
{type === "image" ? (
|
||||||
<div className="space-y-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
|
<Select
|
||||||
value={modeConfig.modeType}
|
value={modeConfig.modeType}
|
||||||
onValueChange={(value: AgentPresetModeType) =>
|
onValueChange={(value: AgentPresetModeType) =>
|
||||||
@ -615,6 +617,7 @@ export function AgentPresetDialog({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 text-sm",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||||
: ""
|
: ""
|
||||||
@ -637,7 +640,7 @@ export function AgentPresetDialog({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="space-y-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
|
<Select
|
||||||
value={type}
|
value={type}
|
||||||
onValueChange={(value: AgentPresetType) => {
|
onValueChange={(value: AgentPresetType) => {
|
||||||
@ -647,6 +650,7 @@ export function AgentPresetDialog({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 text-sm",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||||
: ""
|
: ""
|
||||||
@ -662,10 +666,11 @@ export function AgentPresetDialog({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-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={defaultModelId} onValueChange={setDefaultModelId}>
|
<Select value={defaultModelId} onValueChange={setDefaultModelId}>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 text-sm",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||||
: ""
|
: ""
|
||||||
@ -691,14 +696,14 @@ export function AgentPresetDialog({
|
|||||||
{type === "image" ? (
|
{type === "image" ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl border p-3",
|
"rounded-xl border px-4 py-3.5",
|
||||||
isDark ? "border-white/10 bg-black/20" : "border-slate-200 bg-muted/20"
|
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}
|
当前模式:{modeConfig.modeType === "legacy" ? "旧版兼容" : selectedImageModeMeta.label}
|
||||||
</div>
|
</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"
|
{modeConfig.modeType === "legacy"
|
||||||
? "当前模板沿用旧版多槽位执行逻辑。切换到新模式后,将按新的业务流程配置。"
|
? "当前模板沿用旧版多槽位执行逻辑。切换到新模式后,将按新的业务流程配置。"
|
||||||
: selectedImageModeMeta.description}
|
: selectedImageModeMeta.description}
|
||||||
@ -709,15 +714,15 @@ export function AgentPresetDialog({
|
|||||||
{type === "image" && modeConfig.modeType !== "legacy" ? (
|
{type === "image" && modeConfig.modeType !== "legacy" ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl border p-4",
|
"rounded-xl border p-5",
|
||||||
isDark ? "border-white/10 bg-black/20" : "border-slate-200 bg-muted/20"
|
isDark ? "border-white/10 bg-white/[0.02]" : "border-slate-200 bg-muted/30"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mb-4">
|
<div className="mb-5">
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>
|
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||||
{selectedImageModeMeta.label}配置
|
{selectedImageModeMeta.label}配置
|
||||||
</div>
|
</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
|
{isSceneModeBuilder
|
||||||
? "第一步提示词组合 + 自定义提示词 + 输入图片 + 模板参考图,共同组成生图输入。"
|
? "第一步提示词组合 + 自定义提示词 + 输入图片 + 模板参考图,共同组成生图输入。"
|
||||||
: isDetailReplacementModeBuilder
|
: isDetailReplacementModeBuilder
|
||||||
@ -727,9 +732,9 @@ export function AgentPresetDialog({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSceneModeBuilder ? (
|
{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="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
|
<Select
|
||||||
value={String(modeConfig.scene.productImageSlotCount)}
|
value={String(modeConfig.scene.productImageSlotCount)}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
@ -745,6 +750,7 @@ export function AgentPresetDialog({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 text-sm",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||||
: ""
|
: ""
|
||||||
@ -766,11 +772,14 @@ export function AgentPresetDialog({
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
<div className={cn(
|
||||||
<div>
|
"flex items-center justify-between rounded-lg border px-3.5 py-3",
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>模特图槽位</div>
|
isDark ? "border-white/10 bg-white/[0.03]" : "border-border bg-background"
|
||||||
<div className={cn("text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
)}>
|
||||||
|
<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 个模特图槽位
|
开启后会额外保留 1 个模特图槽位
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -786,13 +795,17 @@ export function AgentPresetDialog({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
className="shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
<div className={cn(
|
||||||
<div>
|
"flex items-center justify-between rounded-lg border px-3.5 py-3",
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>允许模板参考图</div>
|
isDark ? "border-white/10 bg-white/[0.03]" : "border-border bg-background"
|
||||||
<div className={cn("text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
)}>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@ -808,13 +821,17 @@ export function AgentPresetDialog({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
className="shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
<div className={cn(
|
||||||
<div>
|
"flex items-center justify-between rounded-lg border px-3.5 py-3",
|
||||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>允许用户切换模型</div>
|
isDark ? "border-white/10 bg-white/[0.03]" : "border-border bg-background"
|
||||||
<div className={cn("text-xs", isDark ? "text-white/42" : "text-muted-foreground")}>
|
)}>
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@ -830,6 +847,7 @@ export function AgentPresetDialog({
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
className="shrink-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1247,11 +1265,12 @@ export function AgentPresetDialog({
|
|||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter className="gap-2 pt-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => onOpenChange(false)}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 px-5 text-sm",
|
||||||
isDark
|
isDark
|
||||||
? "border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
? "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}
|
onClick={handleCreate}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
"h-9 px-5 text-sm gap-1.5",
|
||||||
isDark
|
isDark
|
||||||
? "bg-gradient-to-r from-emerald-500 to-teal-500 text-white hover:from-emerald-400 hover:to-teal-400"
|
? "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"
|
: "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}
|
{submitLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { cn } from "@/lib/utils";
|
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 { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||||
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
||||||
import {
|
import {
|
||||||
@ -52,7 +56,7 @@ function buildSelectionSummary(params: {
|
|||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}) {
|
}) {
|
||||||
const result: AgentWorkflowComposerSelection[] = [];
|
const result: AgentWorkflowComposerSelection[] = [];
|
||||||
for (const step of params.config.steps) {
|
for (const { step } of flattenAgentWorkflowSteps(params.config.steps)) {
|
||||||
const selectedOptionKey = params.selections[step.key];
|
const selectedOptionKey = params.selections[step.key];
|
||||||
if (!selectedOptionKey) continue;
|
if (!selectedOptionKey) continue;
|
||||||
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
||||||
@ -183,18 +187,16 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
const resolvedConfig = useMemo(() => {
|
const resolvedConfig = useMemo(() => {
|
||||||
const source = config || defaultAgentWorkflowUiConfig;
|
const source = config || defaultAgentWorkflowUiConfig;
|
||||||
return {
|
return {
|
||||||
steps: [...source.steps]
|
steps: getEnabledAgentWorkflowStepTree(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),
|
|
||||||
};
|
};
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
const flattenedResolvedSteps = useMemo(
|
||||||
|
() =>
|
||||||
|
flattenAgentWorkflowSteps(resolvedConfig.steps).filter(
|
||||||
|
({ step }) => Array.isArray(step.options) && step.options.length > 0
|
||||||
|
),
|
||||||
|
[resolvedConfig.steps]
|
||||||
|
);
|
||||||
|
|
||||||
const summary = useMemo(
|
const summary = useMemo(
|
||||||
() => buildSelectionSummary({ config: resolvedConfig, selections }),
|
() => buildSelectionSummary({ config: resolvedConfig, selections }),
|
||||||
@ -300,10 +302,13 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
</motion.section>
|
</motion.section>
|
||||||
|
|
||||||
{/* 工作流步骤 */}
|
{/* 工作流步骤 */}
|
||||||
{resolvedConfig.steps.map((step, stepIndex) => (
|
{flattenedResolvedSteps.map(({ step, depth }, stepIndex) => (
|
||||||
<motion.section
|
<motion.section
|
||||||
key={step.key}
|
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 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.15 + stepIndex * 0.05 }}
|
transition={{ delay: 0.15 + stepIndex * 0.05 }}
|
||||||
@ -313,6 +318,11 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
{stepIndex + 2}
|
{stepIndex + 2}
|
||||||
</span>
|
</span>
|
||||||
{step.title}
|
{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>
|
</h2>
|
||||||
{step.description && (
|
{step.description && (
|
||||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
import {
|
||||||
|
defaultAgentWorkflowUiConfig,
|
||||||
|
flattenAgentWorkflowSteps,
|
||||||
|
getEnabledAgentWorkflowStepTree,
|
||||||
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import type {
|
import type {
|
||||||
AgentWorkflowComposerContext,
|
AgentWorkflowComposerContext,
|
||||||
AgentWorkflowMaterialSelections,
|
AgentWorkflowMaterialSelections,
|
||||||
@ -23,12 +27,12 @@ export function buildAgentWorkflowComposerContext(params: {
|
|||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}): AgentWorkflowComposerContext | null {
|
}): AgentWorkflowComposerContext | null {
|
||||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||||
const steps = (Array.isArray(config.steps) ? config.steps : []).filter(
|
const steps = flattenAgentWorkflowSteps(
|
||||||
(step) =>
|
getEnabledAgentWorkflowStepTree(Array.isArray(config.steps) ? config.steps : [])
|
||||||
step.enabled !== false && Array.isArray(step.options) && step.options.some((item) => item.enabled !== false)
|
)
|
||||||
);
|
.map(({ step }) => step)
|
||||||
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
const selections = steps
|
const selections = steps
|
||||||
.filter((step) => step.enabled !== false)
|
|
||||||
.map((step) => {
|
.map((step) => {
|
||||||
const selectedKey = params.selections[step.key];
|
const selectedKey = params.selections[step.key];
|
||||||
if (!selectedKey) return null;
|
if (!selectedKey) return null;
|
||||||
@ -219,10 +223,11 @@ export function getAgentWorkflowProgress(params: {
|
|||||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||||
}) {
|
}) {
|
||||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||||
const enabledSteps = (config.steps || []).filter(
|
const enabledSteps = flattenAgentWorkflowSteps(
|
||||||
(step) =>
|
getEnabledAgentWorkflowStepTree(config.steps || [])
|
||||||
step.enabled !== false && Array.isArray(step.options) && step.options.some((item) => item.enabled !== false)
|
)
|
||||||
);
|
.map(({ step }) => step)
|
||||||
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
const completedSteps = enabledSteps.filter((step) => {
|
const completedSteps = enabledSteps.filter((step) => {
|
||||||
const value = params.selections[step.key];
|
const value = params.selections[step.key];
|
||||||
return Boolean(String(value || "").trim());
|
return Boolean(String(value || "").trim());
|
||||||
|
|||||||
@ -5,6 +5,13 @@ import type {
|
|||||||
} from "@/types/agent";
|
} from "@/types/agent";
|
||||||
|
|
||||||
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
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(
|
function cloneOption(
|
||||||
option: AgentWorkflowOptionUiConfig
|
option: AgentWorkflowOptionUiConfig
|
||||||
@ -22,6 +29,7 @@ function cloneStep(step: AgentWorkflowStepUiConfig): AgentWorkflowStepUiConfig {
|
|||||||
...step,
|
...step,
|
||||||
description: step.description || "",
|
description: step.description || "",
|
||||||
options: step.options.map(cloneOption),
|
options: step.options.map(cloneOption),
|
||||||
|
children: (step.children || []).map(cloneStep),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +46,7 @@ function createStep(
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sortOrder: 0,
|
sortOrder: 0,
|
||||||
options,
|
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) {
|
function normalizeText(value: unknown, maxLength: number) {
|
||||||
return String(value || "").trim().slice(0, maxLength);
|
return String(value || "").trim().slice(0, maxLength);
|
||||||
}
|
}
|
||||||
@ -178,97 +259,116 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
|||||||
? (raw.inputOptions as Record<string, unknown>)
|
? (raw.inputOptions as Record<string, unknown>)
|
||||||
: {};
|
: {};
|
||||||
const defaultStepByKey = new Map(
|
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 sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
||||||
const usedStepKeys = new Set<string>();
|
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
|
const normalizedSteps = sourceSteps
|
||||||
.map((item, stepIndex) => {
|
.map((item, stepIndex) => normalizeStep(item, stepIndex, 1))
|
||||||
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,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -276,6 +376,6 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
|||||||
brands: normalizeStringList(rawInputOptions.brands),
|
brands: normalizeStringList(rawInputOptions.brands),
|
||||||
stores: normalizeStringList(rawInputOptions.stores),
|
stores: normalizeStringList(rawInputOptions.stores),
|
||||||
},
|
},
|
||||||
steps: normalizedSteps.sort((left, right) => left.sortOrder - right.sortOrder),
|
steps: normalizedSteps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export interface AgentWorkflowStepUiConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
options: AgentWorkflowOptionUiConfig[];
|
options: AgentWorkflowOptionUiConfig[];
|
||||||
|
children?: AgentWorkflowStepUiConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentWorkflowUiConfig {
|
export interface AgentWorkflowUiConfig {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user