feat: support dynamic agent workflow steps
This commit is contained in:
parent
dae5f59417
commit
67da32512a
@ -8,8 +8,10 @@ import {
|
||||
ArrowLeft,
|
||||
Image as ImageIcon,
|
||||
Loader2,
|
||||
Plus,
|
||||
Save,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@ -34,11 +36,89 @@ import { Switch } from "@/components/ui/switch";
|
||||
import type {
|
||||
AgentWorkflowBrandModelConfig,
|
||||
AgentWorkflowMaterialApiConfig,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentWorkflowStepUiConfig,
|
||||
AgentWorkflowUiConfig,
|
||||
} from "@/types/agent";
|
||||
|
||||
const fetcher = (url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json());
|
||||
|
||||
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,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkflowSlug(value: string, fallback: string) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^\w-]/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function ensureUniqueWorkflowKey(baseKey: string, existingKeys: string[]) {
|
||||
const normalizedBase = createWorkflowSlug(baseKey, "item");
|
||||
const keySet = new Set(existingKeys);
|
||||
if (!keySet.has(normalizedBase)) {
|
||||
return normalizedBase;
|
||||
}
|
||||
|
||||
let suffix = 2;
|
||||
let candidate = `${normalizedBase}_${suffix}`;
|
||||
while (keySet.has(candidate)) {
|
||||
suffix += 1;
|
||||
candidate = `${normalizedBase}_${suffix}`;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function createWorkflowOptionDraft(
|
||||
stepKey: string,
|
||||
optionIndex: number,
|
||||
existingOptionKeys: string[]
|
||||
): AgentWorkflowOptionUiConfig {
|
||||
const optionKey = ensureUniqueWorkflowKey(
|
||||
`${stepKey}_option_${optionIndex + 1}`,
|
||||
existingOptionKeys
|
||||
);
|
||||
return {
|
||||
key: optionKey,
|
||||
buttonName: `选项${optionIndex + 1}`,
|
||||
description: "",
|
||||
previewImageUrl: "",
|
||||
enabled: true,
|
||||
sortOrder: optionIndex,
|
||||
promptFragment: `请按选项${optionIndex + 1}执行。`,
|
||||
};
|
||||
}
|
||||
|
||||
function createWorkflowStepDraft(
|
||||
stepIndex: number,
|
||||
existingStepKeys: string[]
|
||||
): AgentWorkflowStepUiConfig {
|
||||
const stepKey = ensureUniqueWorkflowKey(`step_${stepIndex + 1}`, existingStepKeys);
|
||||
return {
|
||||
key: stepKey,
|
||||
title: `新步骤${stepIndex + 1}`,
|
||||
description: "",
|
||||
enabled: true,
|
||||
sortOrder: stepIndex,
|
||||
options: [createWorkflowOptionDraft(stepKey, 0, [])],
|
||||
};
|
||||
}
|
||||
|
||||
export function AgentWorkflowSettingsClient() {
|
||||
const { data, isLoading, mutate } = useSWR<{
|
||||
agentWorkflowUiConfig?: AgentWorkflowUiConfig;
|
||||
@ -89,10 +169,12 @@ export function AgentWorkflowSettingsClient() {
|
||||
stepKey: string,
|
||||
updater: (step: AgentWorkflowUiConfig["steps"][number]) => AgentWorkflowUiConfig["steps"][number]
|
||||
) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
steps: prev.steps.map((step) => (step.key === stepKey ? updater(step) : step)),
|
||||
}));
|
||||
setConfig((prev) =>
|
||||
normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: prev.steps.map((step) => (step.key === stepKey ? updater(step) : step)),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const updateOption = (
|
||||
@ -110,6 +192,44 @@ export function AgentWorkflowSettingsClient() {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddStep = () => {
|
||||
setConfig((prev) =>
|
||||
normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: [
|
||||
...prev.steps,
|
||||
createWorkflowStepDraft(prev.steps.length, prev.steps.map((step) => step.key)),
|
||||
],
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveStep = (stepKey: string) => {
|
||||
setConfig((prev) =>
|
||||
normalizeConfigOrdering({
|
||||
...prev,
|
||||
steps: prev.steps.filter((step) => step.key !== stepKey),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleAddOption = (stepKey: string) => {
|
||||
updateStep(stepKey, (step) => ({
|
||||
...step,
|
||||
options: [
|
||||
...step.options,
|
||||
createWorkflowOptionDraft(step.key, step.options.length, step.options.map((item) => item.key)),
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveOption = (stepKey: string, optionKey: string) => {
|
||||
updateStep(stepKey, (step) => ({
|
||||
...step,
|
||||
options: step.options.filter((option) => option.key !== optionKey),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleUploadPreview = async (stepKey: string, optionKey: string, file: File) => {
|
||||
if (!file.type.startsWith("image/")) {
|
||||
toast.error("请上传图片文件");
|
||||
@ -534,11 +654,27 @@ export function AgentWorkflowSettingsClient() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{sortedSteps.map((step) => (
|
||||
<div className="flex justify-end">
|
||||
<Button type="button" variant="outline" onClick={handleAddStep}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新增步骤
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{sortedSteps.length > 0 ? (
|
||||
sortedSteps.map((step) => (
|
||||
<Card key={step.key}>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
步骤 Key:{step.key}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
新增步骤后系统会自动生成唯一 Key,用于前台保存和读取选择结果。
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>步骤标题</Label>
|
||||
<Input
|
||||
@ -568,7 +704,21 @@ export function AgentWorkflowSettingsClient() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 pt-8">
|
||||
<div className="flex flex-wrap items-center justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => handleAddOption(step.key)}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
新增选项
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={() => handleRemoveStep(step.key)}
|
||||
>
|
||||
<Trash2 className="mr-1 h-4 w-4" />
|
||||
删除步骤
|
||||
</Button>
|
||||
<Label htmlFor={`workflow-step-${step.key}`}>启用</Label>
|
||||
<Switch
|
||||
id={`workflow-step-${step.key}`}
|
||||
@ -585,7 +735,8 @@ export function AgentWorkflowSettingsClient() {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
{step.options.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-2">
|
||||
{step.options
|
||||
.slice()
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
@ -603,6 +754,16 @@ export function AgentWorkflowSettingsClient() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 px-2 text-xs text-destructive hover:text-destructive"
|
||||
onClick={() => handleRemoveOption(step.key, option.key)}
|
||||
>
|
||||
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
||||
删除选项
|
||||
</Button>
|
||||
<Label
|
||||
htmlFor={`workflow-option-${step.key}-${option.key}`}
|
||||
className="text-xs text-muted-foreground"
|
||||
@ -745,10 +906,22 @@ export function AgentWorkflowSettingsClient() {
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
当前步骤还没有选项,请先新增一个选项。
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="rounded-2xl border border-dashed px-4 py-10 text-center text-sm text-muted-foreground">
|
||||
还没有配置工作流步骤,点击上方“新增步骤”开始搭建第一步页面。
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -89,7 +89,7 @@ export function AgentWorkflowPanel({
|
||||
[config?.inputOptions?.stores]
|
||||
);
|
||||
const resolvedConfig = useMemo(() => {
|
||||
const source = config?.steps?.length ? config : defaultAgentWorkflowUiConfig;
|
||||
const source = config || defaultAgentWorkflowUiConfig;
|
||||
return {
|
||||
steps: [...source.steps]
|
||||
.filter((step) => step.enabled)
|
||||
@ -99,6 +99,7 @@ export function AgentWorkflowPanel({
|
||||
.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]);
|
||||
@ -217,7 +218,8 @@ export function AgentWorkflowPanel({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{resolvedConfig.steps.map((step) => (
|
||||
{resolvedConfig.steps.length > 0 ? (
|
||||
resolvedConfig.steps.map((step) => (
|
||||
<section key={step.key} className="space-y-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium">{step.title}</div>
|
||||
@ -273,7 +275,12 @@ export function AgentWorkflowPanel({
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
))
|
||||
) : (
|
||||
<section className="rounded-2xl border border-dashed bg-muted/10 px-4 py-8 text-center text-sm text-muted-foreground">
|
||||
当前还没有启用的工作流步骤,请先到系统设置新增步骤或启用现有步骤。
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="space-y-3 rounded-2xl border bg-muted/20 p-4">
|
||||
<div className="text-sm font-medium">当前工作流摘要</div>
|
||||
@ -367,7 +374,11 @@ export function AgentWorkflowPanel({
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
<Button type="button" onClick={onNextStep} disabled={!progress.isComplete}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onNextStep}
|
||||
disabled={progress.total > 0 && !progress.isComplete}
|
||||
>
|
||||
下一步:选择生图模式
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@ -23,7 +23,10 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}): AgentWorkflowComposerContext | null {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const steps = Array.isArray(config.steps) ? config.steps : [];
|
||||
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 selections = steps
|
||||
.filter((step) => step.enabled !== false)
|
||||
.map((step) => {
|
||||
@ -216,7 +219,10 @@ export function getAgentWorkflowProgress(params: {
|
||||
selections: Partial<Record<AgentWorkflowStepKey, string>>;
|
||||
}) {
|
||||
const config = params.config || defaultAgentWorkflowUiConfig;
|
||||
const enabledSteps = (config.steps || []).filter((step) => step.enabled !== false);
|
||||
const enabledSteps = (config.steps || []).filter(
|
||||
(step) =>
|
||||
step.enabled !== false && Array.isArray(step.options) && step.options.some((item) => item.enabled !== false)
|
||||
);
|
||||
const completedSteps = enabledSteps.filter((step) => {
|
||||
const value = params.selections[step.key];
|
||||
return Boolean(String(value || "").trim());
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type {
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentWorkflowStepKey,
|
||||
AgentWorkflowStepUiConfig,
|
||||
AgentWorkflowUiConfig,
|
||||
} from "@/types/agent";
|
||||
@ -27,7 +26,7 @@ function cloneStep(step: AgentWorkflowStepUiConfig): AgentWorkflowStepUiConfig {
|
||||
}
|
||||
|
||||
function createStep(
|
||||
key: AgentWorkflowStepKey,
|
||||
key: string,
|
||||
title: string,
|
||||
description: string,
|
||||
options: AgentWorkflowOptionUiConfig[]
|
||||
@ -134,6 +133,18 @@ function normalizeText(value: unknown, maxLength: number) {
|
||||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function normalizeKey(value: unknown, fallback: string) {
|
||||
const normalized = String(value || "")
|
||||
.trim()
|
||||
.slice(0, 64)
|
||||
.replace(/\s+/g, "_")
|
||||
.replace(/[^\w-]/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toLowerCase();
|
||||
return normalized || fallback;
|
||||
}
|
||||
|
||||
function normalizeBoolean(value: unknown, fallback: boolean) {
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
@ -149,6 +160,7 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
const hasRawSteps = Array.isArray(raw.steps);
|
||||
const rawSteps = Array.isArray(raw.steps) ? raw.steps : [];
|
||||
const normalizeStringList = (input: unknown) => {
|
||||
if (!Array.isArray(input)) return [] as string[];
|
||||
@ -165,68 +177,99 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
raw.inputOptions && typeof raw.inputOptions === "object" && !Array.isArray(raw.inputOptions)
|
||||
? (raw.inputOptions as Record<string, unknown>)
|
||||
: {};
|
||||
const defaultStepByKey = new Map(
|
||||
defaultAgentWorkflowUiConfig.steps.map((step) => [step.key, step] as const)
|
||||
);
|
||||
const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
||||
const usedStepKeys = new Set<string>();
|
||||
|
||||
const normalizedSteps = defaultAgentWorkflowUiConfig.steps.map((defaultStep, stepIndex) => {
|
||||
const matchedStep =
|
||||
rawSteps.find(
|
||||
(item) =>
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
normalizeText((item as Record<string, unknown>).key, 64) === defaultStep.key
|
||||
) || null;
|
||||
const matchedStepRecord =
|
||||
matchedStep && typeof matchedStep === "object"
|
||||
? (matchedStep as Record<string, unknown>)
|
||||
: null;
|
||||
const rawOptions = Array.isArray(matchedStepRecord?.options)
|
||||
? (matchedStepRecord?.options as unknown[])
|
||||
: [];
|
||||
|
||||
const options = defaultStep.options.map((defaultOption, optionIndex) => {
|
||||
const matchedOption =
|
||||
rawOptions.find(
|
||||
(item) =>
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
normalizeText((item as Record<string, unknown>).key, 64) === defaultOption.key
|
||||
) || null;
|
||||
const matchedOptionRecord =
|
||||
matchedOption && typeof matchedOption === "object"
|
||||
? (matchedOption as Record<string, unknown>)
|
||||
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: defaultOption.key,
|
||||
buttonName:
|
||||
normalizeText(matchedOptionRecord?.buttonName, 64) || defaultOption.buttonName,
|
||||
key: stepKey,
|
||||
title: normalizeText(stepRecord?.title, 64) || fallbackTitle,
|
||||
description:
|
||||
normalizeText(matchedOptionRecord?.description, 300) || defaultOption.description || "",
|
||||
previewImageUrl: normalizeText(matchedOptionRecord?.previewImageUrl, 1000),
|
||||
enabled: normalizeBoolean(matchedOptionRecord?.enabled, defaultOption.enabled),
|
||||
normalizeText(stepRecord?.description, 300) || fallbackDescription,
|
||||
enabled: normalizeBoolean(stepRecord?.enabled, defaultStep?.enabled ?? true),
|
||||
sortOrder: normalizeNumber(
|
||||
matchedOptionRecord?.sortOrder,
|
||||
defaultOption.sortOrder ?? optionIndex
|
||||
stepRecord?.sortOrder,
|
||||
defaultStep?.sortOrder ?? stepIndex
|
||||
),
|
||||
promptFragment:
|
||||
normalizeText(matchedOptionRecord?.promptFragment, 1000) ||
|
||||
defaultOption.promptFragment ||
|
||||
"",
|
||||
options,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
key: defaultStep.key,
|
||||
title: normalizeText(matchedStepRecord?.title, 64) || defaultStep.title,
|
||||
description:
|
||||
normalizeText(matchedStepRecord?.description, 300) || defaultStep.description || "",
|
||||
enabled: normalizeBoolean(matchedStepRecord?.enabled, defaultStep.enabled),
|
||||
sortOrder: normalizeNumber(
|
||||
matchedStepRecord?.sortOrder,
|
||||
defaultStep.sortOrder ?? stepIndex
|
||||
),
|
||||
options,
|
||||
};
|
||||
});
|
||||
})
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
|
||||
return {
|
||||
inputOptions: {
|
||||
|
||||
@ -10,15 +10,7 @@ export interface AgentCategoryPath {
|
||||
brand?: string | null;
|
||||
}
|
||||
|
||||
export type AgentWorkflowStepKey =
|
||||
| "audience"
|
||||
| "style"
|
||||
| "space"
|
||||
| "subSpace"
|
||||
| "hardDecoration"
|
||||
| "softDecoration"
|
||||
| "lighting"
|
||||
| "accessories";
|
||||
export type AgentWorkflowStepKey = string;
|
||||
|
||||
export interface AgentWorkflowOptionUiConfig {
|
||||
key: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user