feat: polish canvas and workflow settings
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-24 17:30:11 +08:00
parent f946d9c1f0
commit b4529c860c
9 changed files with 737 additions and 145 deletions

View File

@ -58,8 +58,10 @@ import {
getAgentWorkflowSpecialModuleRootOptions,
getEnabledAgentWorkflowOptionTree,
getAgentWorkflowCategoryLabels,
getAgentWorkflowBusinessModeSettings,
getAgentWorkflowConstraintSettings,
getAgentWorkflowPageFeatureSettings,
normalizeAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowOptionOrdering,
flattenAgentWorkflowSteps,
normalizeAgentWorkflowStepOrdering,
@ -129,6 +131,8 @@ import type {
AgentPresetLibraryConfig,
AgentPsdTextColorBrandConfig,
AgentPresetLibrarySection,
AgentWorkflowBusinessModeConfig,
AgentWorkflowBusinessModeKey,
AgentWorkflowBrandModelConfig,
AgentWorkflowBrandStoreEntry,
AgentWorkflowCategoryLabels,
@ -2046,6 +2050,10 @@ export function AgentWorkflowSettingsClient({
() => getAgentWorkflowPageFeatureSettings(config),
[config]
);
const workflowBusinessModeSettings = useMemo(
() => getAgentWorkflowBusinessModeSettings(config),
[config]
);
const firstCategoryLabel = categoryLabels.firstCategory;
const secondCategoryLabel = categoryLabels.secondCategory;
const thirdCategoryLabel = categoryLabels.thirdCategory;
@ -3535,6 +3543,61 @@ export function AgentWorkflowSettingsClient({
}
};
const updateBusinessMode = (
modeKey: AgentWorkflowBusinessModeKey,
updater: (current: AgentWorkflowBusinessModeConfig) => AgentWorkflowBusinessModeConfig
) => {
setConfig((prev) => {
const currentSettings = normalizeAgentWorkflowBusinessModeSettings(
prev.businessModeSettings
);
return {
...prev,
businessModeSettings: {
modes: currentSettings.modes.map((mode) =>
mode.key === modeKey ? updater(mode) : mode
),
},
};
});
};
const handleUploadBusinessModeSvg = async (
modeKey: AgentWorkflowBusinessModeKey,
file: File
) => {
const isSvg =
file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
if (!isSvg) {
toast.error("请上传 SVG 图标");
return;
}
const refKey = `business-mode:${modeKey}`;
try {
setUploadingKey(refKey);
const formData = new FormData();
formData.append("file", file);
const response = await fetchUploadWithRetry(formData);
const result = await response.json();
if (!response.ok || !result?.url) {
throw new Error(result?.error || "上传失败");
}
updateBusinessMode(modeKey, (mode) => ({
...mode,
customSvgUrl: result.url,
}));
toast.success("SVG 图标上传成功");
} catch (error) {
toast.error(error instanceof Error ? error.message : "SVG 图标上传失败");
} finally {
setUploadingKey(null);
}
};
const updateSelectedBrandPsdTextColorConfig = (
updater: (current: AgentPsdTextColorBrandConfig | AgentPsdTextColorConfig) => {
enabled: boolean;
@ -5654,7 +5717,7 @@ export function AgentWorkflowSettingsClient({
onChange={(e) =>
setShowcaseConfig((prev) => ({ ...prev, subtitle: e.target.value }))
}
placeholder="选择生图模式、查询商品图、完成风格配置并实时查看结果"
placeholder="选择品牌、配置风格化提示词、选择生图模式并查看结果"
/>
</div>
</div>
@ -5866,7 +5929,7 @@ export function AgentWorkflowSettingsClient({
<div>
<CardTitle className="text-base">前台流程功能开关</CardTitle>
<CardDescription>
控制风格化页面是否展示选择生图模式、选择 SPU / SKU 和套 PSD 模板入口
控制风格化页面是否展示固定生图模式、素材选择和套 PSD 模板入口
</CardDescription>
</div>
</div>
@ -5874,9 +5937,9 @@ export function AgentWorkflowSettingsClient({
<CardContent className="grid gap-3 md:grid-cols-4">
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
<div className="pr-4">
<div className="text-sm font-medium">开启第一步:选择生图模式</div>
<div className="text-sm font-medium">开启固定生图模式</div>
<div className="text-xs text-muted-foreground">
关闭后前台不再要求选择生图模式,直接进入后续配置并选择模型生图。
关闭后前台不展示主图/SKU图、详情和买家秀模式入口,直接进入后续配置并选择模型生图。
</div>
</div>
<Switch
@ -5896,7 +5959,7 @@ export function AgentWorkflowSettingsClient({
<div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
<div className="pr-4">
<div className="text-sm font-medium">开启第二步:选择 SPU / SKU</div>
<div className="text-sm font-medium">开启素材选择</div>
<div className="text-xs text-muted-foreground">
关闭后前台跳过商品素材搜索,不再强制补齐 SPU / SKU 输入图槽位。
</div>
@ -5963,6 +6026,177 @@ export function AgentWorkflowSettingsClient({
</Card>
</motion.div>
<motion.div variants={itemVariants} className="lg:col-span-2">
<Card>
<CardHeader className="pb-4">
<div className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-cyan-500/10">
<Layers className="h-4 w-4 text-cyan-600" />
</div>
<div>
<CardTitle className="text-base">风格化固定模式配置</CardTitle>
<CardDescription>
前台固定展示主图/SKU图、详情和买家秀三个业务模式;图标优先使用管理员上传的 SVG。
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="grid gap-4 lg:grid-cols-3">
{workflowBusinessModeSettings.modes.map((mode) => {
const refKey = `business-mode:${mode.key}`;
const isUploading = uploadingKey === refKey;
const Icon =
mode.key === "detail"
? Layers
: mode.key === "buyer_show"
? UserCircle
: Package;
return (
<div key={mode.key} className="space-y-4 rounded-xl border bg-muted/20 p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-xl border bg-background">
{mode.customSvgUrl ? (
<img
src={mode.customSvgUrl}
alt={mode.title}
className="h-7 w-7 object-contain"
/>
) : (
<Icon className="h-6 w-6 text-muted-foreground" />
)}
</div>
<div>
<div className="text-sm font-semibold">{mode.title}</div>
<div className="text-xs text-muted-foreground">
{mode.key === "buyer_show" ? "预留模块" : "已接入前台入口"}
</div>
</div>
</div>
<Switch
checked={mode.enabled}
onCheckedChange={(checked) =>
updateBusinessMode(mode.key, (current) => ({
...current,
enabled: checked,
}))
}
/>
</div>
<div className="space-y-2">
<Label>显示名称</Label>
<Input
value={mode.title}
onChange={(event) =>
updateBusinessMode(mode.key, (current) => ({
...current,
title: event.target.value,
}))
}
/>
</div>
<div className="space-y-2">
<Label>说明文案</Label>
<Textarea
value={mode.description || ""}
onChange={(event) =>
updateBusinessMode(mode.key, (current) => ({
...current,
description: event.target.value,
}))
}
className="min-h-[90px]"
/>
</div>
{mode.key === "detail" ? (
<div className="grid gap-2 rounded-lg border bg-background/60 p-3">
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">详情图替换</span>
<Switch
checked={mode.referenceReplaceEnabled !== false}
onCheckedChange={(checked) =>
updateBusinessMode(mode.key, (current) => ({
...current,
referenceReplaceEnabled: checked,
}))
}
/>
</div>
<div className="flex items-center justify-between gap-3">
<span className="text-xs text-muted-foreground">详情图生成</span>
<Switch
checked={mode.aiDetailGenerateEnabled !== false}
onCheckedChange={(checked) =>
updateBusinessMode(mode.key, (current) => ({
...current,
aiDetailGenerateEnabled: checked,
}))
}
/>
</div>
</div>
) : null}
<div className="space-y-2">
<Label>自定义 SVG 图标</Label>
<input
ref={(el) => {
fileInputRefs.current[refKey] = el;
}}
type="file"
accept=".svg,image/svg+xml"
className="hidden"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) {
void handleUploadBusinessModeSvg(mode.key, file);
}
event.target.value = "";
}}
/>
<div className="flex gap-2">
<Button
type="button"
variant="outline"
size="sm"
disabled={isUploading}
onClick={() => fileInputRefs.current[refKey]?.click()}
>
{isUploading ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Upload className="mr-2 h-4 w-4" />
)}
上传 SVG
</Button>
{mode.customSvgUrl ? (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() =>
updateBusinessMode(mode.key, (current) => ({
...current,
customSvgUrl: "",
}))
}
>
清除
</Button>
) : null}
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
</motion.div>
<motion.div variants={itemVariants} className="lg:col-span-2">
<Card>
<CardHeader className="pb-4">

View File

@ -1170,6 +1170,26 @@ const updateSchema = z.object({
batchProcessingEnabled: z.boolean().optional(),
})
.optional(),
businessModeSettings: z
.object({
modes: z
.array(
z.object({
key: z.enum(["main_sku", "detail", "buyer_show"]),
title: z.string().trim().min(1).max(40).optional(),
description: z.string().trim().max(220).nullable().optional(),
enabled: z.boolean().optional(),
iconKey: z.enum(["main-sku", "detail", "buyer-show"]).optional(),
customSvgUrl: z.string().trim().max(2000).nullable().optional(),
sortOrder: z.number().min(0).max(99).optional(),
referenceReplaceEnabled: z.boolean().optional(),
aiDetailGenerateEnabled: z.boolean().optional(),
})
)
.max(3)
.optional(),
})
.optional(),
steps: z
.array(agentWorkflowStepSchema)
.max(20),

View File

@ -186,6 +186,7 @@ import {
defaultAgentWorkflowUiConfig,
flattenVisibleAgentWorkflowSteps,
getEnabledAgentWorkflowStepTree,
getAgentWorkflowBusinessModeSettings,
getAgentWorkflowPageFeatureSettings,
normalizeAgentWorkflowUiConfig,
sanitizeAgentWorkflowSelections,
@ -1690,6 +1691,10 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
() => getAgentWorkflowPageFeatureSettings(resolvedWorkflowUiConfig),
[resolvedWorkflowUiConfig]
);
const workflowBusinessModeSettings = useMemo(
() => getAgentWorkflowBusinessModeSettings(resolvedWorkflowUiConfig),
[resolvedWorkflowUiConfig]
);
const isWorkflowPresetStepEnabled =
workflowPageFeatureSettings.presetStepEnabled !== false;
const isWorkflowMaterialStepEnabled =
@ -8635,17 +8640,15 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
/>
}
brandStorePanel={
isWorkflowPresetStepEnabled ? (
<AgentWorkflowBrandStorePanel
config={resolvedWorkflowUiConfig}
inputs={workflowInputs}
onInputsChange={handleWorkflowInputsChange}
psdTemplateCategoryTagIndex={
siteSettingsData?.agentPsdTemplateCategoryTagIndex
}
theme="dark"
/>
) : null
<AgentWorkflowBrandStorePanel
config={resolvedWorkflowUiConfig}
inputs={workflowInputs}
onInputsChange={handleWorkflowInputsChange}
psdTemplateCategoryTagIndex={
siteSettingsData?.agentPsdTemplateCategoryTagIndex
}
theme="dark"
/>
}
chatId={agentModeChatId}
deletedResultCollections={agentProjectDeletedResultCollections}
@ -8683,7 +8686,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
isWorkflowPresetStepEnabled ? (
<AgentPresetPanelDarkV2
embedded
embeddedEntityLabel="生图模式"
embeddedEntityLabel="风格化模板"
onSelectPreset={setSelectedAgentPresetId}
selectedPresetId={selectedAgentPresetId}
/>
@ -8702,6 +8705,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
syncGeneratedResultsToCanvas={false}
topNavRightSlot={agentProjectNavSlot}
workflowContext={workflowContext}
workflowBusinessModeSettings={workflowBusinessModeSettings}
workflowFeatureSettings={workflowPageFeatureSettings}
// 嵌入的风格化选择面板
workflowPromptOptimizationEnabled={appearanceRecognitionEnabled}

View File

@ -83,6 +83,8 @@ import {
normalizeAgentPresetModeConfig,
} from "@/lib/agents/preset-mode-config";
import {
defaultAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowBusinessModeSettings,
normalizeAgentWorkflowPageFeatureSettings,
} from "@/lib/agents/workflow-ui-config";
import { buildAgentSceneImageRolePrompt } from "@/lib/agents/scene-image-role-prompt";
@ -102,6 +104,8 @@ import type {
AgentTaskDetailReferenceSource,
AgentWorkflowShowcaseConfig,
AgentWorkflowComposerContext,
AgentWorkflowBusinessModeKey,
AgentWorkflowBusinessModeSettings,
AgentWorkflowMaterialQueryType,
AgentWorkflowPageFeatureSettings,
AgentWorkflowSelectedBrandModel,
@ -120,6 +124,7 @@ interface AgentGenerationWorkspaceDarkV2Props {
selectedAgentPresetId?: string | null;
selectedBrandModel?: AgentWorkflowSelectedBrandModel | null;
workflowContext?: AgentWorkflowComposerContext | null;
workflowBusinessModeSettings?: AgentWorkflowBusinessModeSettings | null;
workflowFeatureSettings?: AgentWorkflowPageFeatureSettings | null;
workflowPromptOptimizationEnabled?: boolean;
getLatestWorkflowContext?: () => AgentWorkflowComposerContext | null;
@ -257,6 +262,15 @@ function getTaskDetailReferenceSourceLabel(source: AgentTaskDetailReferenceSourc
return source === "main" ? "主图" : source === "sku" ? "SKU图" : "详情图";
}
function getBusinessModeKeyByResultMode(
modeType: AgentGenerationResultModeType
): AgentWorkflowBusinessModeKey {
if (modeType === "detail_generation" || modeType === "detail_replacement") {
return "detail";
}
return "main_sku";
}
function normalizeResultCollectionUrls(urls?: string[] | null) {
return Array.from(
new Set(
@ -961,6 +975,7 @@ export function AgentGenerationWorkspaceDarkV2({
selectedAgentPresetId,
selectedBrandModel = null,
workflowContext = null,
workflowBusinessModeSettings = null,
workflowFeatureSettings = null,
workflowPromptOptimizationEnabled = true,
getLatestWorkflowContext,
@ -1029,6 +1044,10 @@ export function AgentGenerationWorkspaceDarkV2({
() => normalizeAgentWorkflowPageFeatureSettings(workflowFeatureSettings),
[workflowFeatureSettings]
);
const resolvedBusinessModeSettings = useMemo(
() => normalizeAgentWorkflowBusinessModeSettings(workflowBusinessModeSettings),
[workflowBusinessModeSettings]
);
const isPresetStepEnabled = resolvedWorkflowFeatureSettings.presetStepEnabled !== false;
const isMaterialStepEnabled = resolvedWorkflowFeatureSettings.materialStepEnabled !== false;
const isPsdTemplateEnabled = resolvedWorkflowFeatureSettings.psdTemplateEnabled !== false;
@ -1056,6 +1075,8 @@ export function AgentGenerationWorkspaceDarkV2({
normalizedIncomingWorkspaceState.selectedModelId || "auto"
);
const [selectedResultUrl, setSelectedResultUrl] = useState<string | null>(null);
const [activeBusinessModeKey, setActiveBusinessModeKey] =
useState<AgentWorkflowBusinessModeKey>("main_sku");
const [activeResultModeType, setActiveResultModeType] =
useState<AgentGenerationResultModeType>("scene_generation");
const [fullscreenPreview, setFullscreenPreview] = useState<string | null>(null);
@ -1234,6 +1255,18 @@ export function AgentGenerationWorkspaceDarkV2({
: isSkuReplacementPreset
? "sku_replacement"
: "scene_generation";
const businessModeCards = resolvedBusinessModeSettings.modes.length > 0
? resolvedBusinessModeSettings.modes
: defaultAgentWorkflowBusinessModeSettings.modes;
const activeBusinessMode = businessModeCards.find((mode) => mode.key === activeBusinessModeKey) ||
businessModeCards[0] ||
defaultAgentWorkflowBusinessModeSettings.modes[0];
const currentPresetBusinessModeKey = getBusinessModeKeyByResultMode(currentResultModeType);
const isSelectedBusinessModeUnavailable = activeBusinessMode?.enabled === false;
const isBusinessModePresetMismatch =
Boolean(selectedAgentPresetId) &&
activeBusinessModeKey !== "buyer_show" &&
activeBusinessModeKey !== currentPresetBusinessModeKey;
const productSlotCount =
selectedAgentPreset?.type === "image"
? isSceneGenerationPreset
@ -1659,20 +1692,22 @@ export function AgentGenerationWorkspaceDarkV2({
);
const [activeToolStep, setActiveToolStep] = useState<ToolStepKey>("preset");
const hasBrandModelStep = Boolean(brandModelPanel) && modelSlotEnabled;
const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep;
const hasBrandStoreStep = Boolean(brandStorePanel);
const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep || Boolean(presetPanel);
const shouldShowDirectUploadInStyleStep =
isDirectReferenceUploadEnabled && hasWorkflowStyleStep;
const visibleWorkflowSectionKeys = useMemo(
() => [
...(hasBrandStoreStep ? ["brand"] : []),
...(hasWorkflowStyleStep ? ["style"] : []),
...(isPresetStepEnabled ? ["mode"] : []),
...(hasMaterialStep ? ["materials"] : []),
...(hasWorkflowStyleStep ? ["style"] : []),
"generate",
],
[hasMaterialStep, hasWorkflowStyleStep, isPresetStepEnabled]
[hasBrandStoreStep, hasMaterialStep, hasWorkflowStyleStep, isPresetStepEnabled]
);
const getWorkflowSectionBadge = useCallback(
(key: "mode" | "materials" | "style" | "generate") => {
(key: "brand" | "mode" | "materials" | "style" | "generate") => {
const index = visibleWorkflowSectionKeys.indexOf(key);
return String(index >= 0 ? index + 1 : visibleWorkflowSectionKeys.length + 1).padStart(
2,
@ -1681,13 +1716,15 @@ export function AgentGenerationWorkspaceDarkV2({
},
[visibleWorkflowSectionKeys]
);
const firstWorkflowSectionAnchor = isPresetStepEnabled
? "agent-anchor-mode"
: hasMaterialStep
? "agent-anchor-materials"
: hasWorkflowStyleStep
? "agent-anchor-style"
: "agent-anchor-generate";
const firstWorkflowSectionAnchor = hasBrandStoreStep
? "agent-anchor-brand"
: hasWorkflowStyleStep
? "agent-anchor-style"
: isPresetStepEnabled
? "agent-anchor-mode"
: hasMaterialStep
? "agent-anchor-materials"
: "agent-anchor-generate";
const { messages, setMessages, sendMessage, status, stop } = useChat<ChatMessage>({
id: chatId,
@ -2383,6 +2420,7 @@ export function AgentGenerationWorkspaceDarkV2({
const mergedSettingsStepDescription = hasMaterialStep
? "搜索素材、补充要求、确认槽位并开始生成"
: "补充要求并开始生成";
const brandStepBadge = getWorkflowSectionBadge("brand");
const modeStepBadge = getWorkflowSectionBadge("mode");
const materialStepBadge = getWorkflowSectionBadge("materials");
const styleStepBadge = getWorkflowSectionBadge("style");
@ -2394,8 +2432,8 @@ export function AgentGenerationWorkspaceDarkV2({
? [
{
key: "preset" as const,
label: "生图模式",
description: "先选择一个模式,再继续后续配置",
label: "风格模板",
description: "先确定通用风格化模板,再选择输出模式",
icon: Wand2,
disabled: false,
completed: Boolean(selectedAgentPresetId),
@ -2537,6 +2575,13 @@ export function AgentGenerationWorkspaceDarkV2({
setActiveResultModeType(currentResultModeType);
}, [currentResultModeType]);
useEffect(() => {
if (!selectedAgentPresetId) {
return;
}
setActiveBusinessModeKey(currentPresetBusinessModeKey);
}, [currentPresetBusinessModeKey, selectedAgentPresetId]);
useEffect(() => {
if (!pendingGeneration) {
return;
@ -2824,12 +2869,20 @@ export function AgentGenerationWorkspaceDarkV2({
}, [addNode, nodes.length, onOpenCanvasEditor, requestImageNodeAction, selectNodes]);
const handleSend = useCallback(() => {
if (isSelectedBusinessModeUnavailable || activeBusinessModeKey === "buyer_show") {
toast.error("当前生图模式暂未开放");
return;
}
if (isBusinessModePresetMismatch) {
toast.error("当前风格化模板与生图模式不匹配,请先切换对应模板");
return;
}
if (isDetailReplacementPreset && isDetailAiGenerationMethod) {
void handleRunAiDetailGeneration();
return;
}
if (requiresPresetSelection && !selectedAgentPresetId) {
toast.error("请先选择生图模式");
toast.error("请先选择风格化模板");
return;
}
if (isDetailReplacementPreset && detailReplacementReferenceCards.length === 0) {
@ -2960,6 +3013,7 @@ export function AgentGenerationWorkspaceDarkV2({
}
}, [
attachments,
activeBusinessModeKey,
detailReplacementReferenceCards.length,
handleRunAiDetailGeneration,
currentResultModeType,
@ -2968,6 +3022,8 @@ export function AgentGenerationWorkspaceDarkV2({
input,
isDetailAiGenerationMethod,
isDetailReplacementPreset,
isBusinessModePresetMismatch,
isSelectedBusinessModeUnavailable,
isSkuReplacementPreset,
modelsData?.defaultModelId,
requiredProductUploadCount,
@ -3147,7 +3203,7 @@ export function AgentGenerationWorkspaceDarkV2({
const nextStep = toolSteps.find((step) => step.key === nextKey);
if (!nextStep) return;
if (nextStep.disabled) {
toast.error(requiresPresetSelection ? "请先选择生图模式" : "当前步骤暂不可用");
toast.error(requiresPresetSelection ? "请先选择风格化模板" : "当前步骤暂不可用");
return;
}
setActiveToolStep(nextKey);
@ -3483,7 +3539,7 @@ export function AgentGenerationWorkspaceDarkV2({
);
async function handleRunAiDetailGeneration() {
if (!selectedAgentPresetId) {
toast.error("请先选择生图模式");
toast.error("请先选择风格化模板");
return;
}
if (detailGenerationMode !== "classic") {
@ -4330,7 +4386,7 @@ export function AgentGenerationWorkspaceDarkV2({
</>
) : (
<p className="text-xs leading-6 text-white/45">
先选择一个生图模式,系统会按模板配置展示对应的产品图和模特图槽位。
先选择风格化模板,系统会按模板配置展示对应的产品图和模特图槽位。
</p>
)}
</div>
@ -4440,7 +4496,7 @@ export function AgentGenerationWorkspaceDarkV2({
) : (
<div className="mt-4 flex min-h-[132px] flex-col items-center justify-center rounded-[24px] border border-dashed border-white/12 bg-black/20 px-4 text-center text-sm text-white/45">
<ImagePlus className="mb-2 h-5 w-5 opacity-60" />
先选择一个生图模式,系统会按模板配置展示对应的产品图和模特图槽位。
先选择风格化模板,系统会按模板配置展示对应的产品图和模特图槽位。
</div>
)}
</div>
@ -4470,7 +4526,7 @@ export function AgentGenerationWorkspaceDarkV2({
>
<AgentWorkflowShowcaseBanner
config={showcaseConfig}
actionLabel={isPresetStepEnabled ? "开始选择模式" : "开始配置"}
actionLabel="开始配置"
secondaryActionLabel={allGeneratedImages.length > 0 ? "查看结果预览" : undefined}
onActionClick={() => scrollToSection(firstWorkflowSectionAnchor)}
onSecondaryActionClick={() => scrollToSection("agent-anchor-results")}
@ -4485,78 +4541,203 @@ export function AgentGenerationWorkspaceDarkV2({
className="rounded-[28px] border border-white/12 bg-white/[0.04] p-5 shadow-[inset_0_1px_0_rgba(255,255,255,0.03)]"
>
<div className="space-y-6">
{isPresetStepEnabled ? (
<section id="agent-anchor-mode" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
{hasBrandStoreStep ? (
<section id="agent-anchor-brand" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 flex flex-col gap-4 border-b border-white/10 pb-4 lg:flex-row lg:items-start lg:justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
{modeStepBadge}
{brandStepBadge}
</span>
<span className="text-sm font-semibold text-white">选择生图模式</span>
<span className="text-sm font-semibold text-white">选择品牌上下文</span>
</div>
<p className="text-sm text-white/45">
先确定本次使用的模式,并完成品牌和店铺选择,后续的商品图、品牌模特、参考图和结果展示都会随之联动。
先确定品牌、店铺和类目,让后续风格化提示词、素材搜索和 PSD 模板都能拿到同一套业务上下文。
</p>
{isDetailReplacementPreset || isSkuReplacementPreset ? (
<p className="text-xs text-amber-300/80">
{isDetailReplacementPreset
? isDetailAiGenerationMethod
? "当前模板已切换到 AI详情生成,系统会直接复用智能规划详情链路生成整套详情图。"
: "当前模板属于参考图替换模式,结果区会按参考图顺序逐屏展示。"
: "当前模板属于 SKU 替换模式,结果区会按参考图顺序一对一展示。"}
</p>
) : null}
</div>
<div className="flex flex-wrap items-center gap-2">
{selectedAgentPreset ? (
<div className="flex items-center gap-3 rounded-[20px] border border-white/10 bg-black/20 px-3 py-2">
{selectedAgentPreset.avatarUrl ? (
<img
src={selectedAgentPreset.avatarUrl}
alt={selectedAgentPreset.name}
className="h-10 w-10 rounded-xl object-cover"
/>
) : (
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-500/18">
<Wand2 className="h-5 w-5 text-emerald-300" />
</div>
)}
<div>
<div className="text-sm font-medium text-white">{selectedAgentPreset.name}</div>
<div className="text-xs text-white/40">
{selectedAgentPreset.type === "image" ? "生图模式" : "视频模式"}
</div>
</div>
</div>
) : null}
{selectedAgentPreset && onClearPreset ? (
<button
type="button"
onClick={onClearPreset}
className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-emerald-300 transition-colors hover:border-emerald-300/35 hover:text-emerald-200"
>
更换生图模式
</button>
) : null}
</div>
</div>
{brandStorePanel}
</section>
) : null}
{presetPanel ? (
<div className="agent-dark-scrollbar max-h-[760px] overflow-y-auto pr-1">
{presetPanel}
{hasWorkflowStyleStep ? (
<section id="agent-anchor-style" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
{styleStepBadge}
</span>
<span className="text-sm font-semibold text-white">风格化提示词配置</span>
</div>
) : (
<div className="flex min-h-[220px] items-center justify-center rounded-[24px] border border-dashed border-white/12 bg-black/20 text-sm text-white/45">
暂无可用的生图模式配置。
</div>
)}
<p className="text-sm text-white/45">
先完成风格、空间、光线、机位、人群等通用提示词配置;这些配置会被后面的不同生图模式共用。
</p>
</div>
{brandStorePanel ? (
<div className="mt-4 border-t border-white/10 pt-4">
{brandStorePanel}
<div className="space-y-4">
{shouldShowDirectUploadInStyleStep ? attachmentSlotPanel : null}
{workflowStylePanel}
{isPresetStepEnabled ? (
<div className="rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 flex flex-col gap-3 border-b border-white/10 pb-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h3 className="text-sm font-semibold text-white">风格化模板</h3>
<p className="mt-1 text-xs text-white/42">
这里沿用现有模板库作为生成执行配置;后续固定业务模式会接管前台入口展示。
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
{selectedAgentPreset ? (
<div className="flex items-center gap-3 rounded-[18px] border border-white/10 bg-white/[0.03] px-3 py-2">
{selectedAgentPreset.avatarUrl ? (
<img
src={selectedAgentPreset.avatarUrl}
alt={selectedAgentPreset.name}
className="h-9 w-9 rounded-xl object-cover"
/>
) : (
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-emerald-500/18">
<Wand2 className="h-4 w-4 text-emerald-300" />
</div>
)}
<div>
<div className="text-xs font-medium text-white">{selectedAgentPreset.name}</div>
<div className="text-[11px] text-white/40">
{currentPresetBusinessModeKey === "detail"
? "详情执行模板"
: "主图/SKU执行模板"}
</div>
</div>
</div>
) : null}
{selectedAgentPreset && onClearPreset ? (
<button
type="button"
onClick={onClearPreset}
className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-emerald-300 transition-colors hover:border-emerald-300/35 hover:text-emerald-200"
>
更换模板
</button>
) : null}
</div>
</div>
{presetPanel ? (
<div className="agent-dark-scrollbar max-h-[520px] overflow-y-auto pr-1">
{presetPanel}
</div>
) : (
<div className="flex min-h-[180px] items-center justify-center rounded-[24px] border border-dashed border-white/12 bg-black/20 text-sm text-white/45">
暂无可用的风格化模板配置。
</div>
)}
</div>
) : null}
{hasBrandModelStep ? brandModelPanel : null}
</div>
</section>
) : null}
{isPresetStepEnabled ? (
<section id="agent-anchor-mode" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
{modeStepBadge}
</span>
<span className="text-sm font-semibold text-white">选择生图模式</span>
</div>
) : null}
<p className="text-sm text-white/45">
生图模式只决定输出链路和素材规则,前面配置好的风格化提示词会在不同模式中共用。
</p>
{isBusinessModePresetMismatch ? (
<p className="text-xs text-amber-300/80">
当前风格化模板与所选生图模式不匹配,请在上方模板库切换到对应类型后再生成。
</p>
) : null}
</div>
<div className="grid gap-3 lg:grid-cols-3">
{businessModeCards.map((mode) => {
const isActive = activeBusinessModeKey === mode.key;
const Icon =
mode.key === "detail"
? Settings2
: mode.key === "buyer_show"
? UserRound
: ImagePlus;
const modeStatusText =
mode.key === "buyer_show" && mode.enabled === false
? "预留中"
: mode.enabled === false
? "已关闭"
: mode.key === currentPresetBusinessModeKey
? "当前模板匹配"
: "可选择";
return (
<button
key={mode.key}
type="button"
onClick={() => {
setActiveBusinessModeKey(mode.key);
if (mode.enabled === false || mode.key === "buyer_show") {
toast.info("当前生图模式已预留,暂未开放生成");
}
}}
className={cn(
"group min-h-[148px] rounded-[22px] border p-4 text-left transition-all",
isActive
? "border-emerald-300/55 bg-emerald-500/[0.12] shadow-[0_0_0_1px_rgba(16,185,129,0.18)]"
: "border-white/10 bg-white/[0.03] hover:border-white/20 hover:bg-white/[0.06]",
mode.enabled === false ? "opacity-70" : ""
)}
>
<div className="flex items-start justify-between gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-black/25">
{mode.customSvgUrl ? (
<img
src={mode.customSvgUrl}
alt={mode.title}
className="h-7 w-7 object-contain"
/>
) : (
<Icon className="h-6 w-6 text-emerald-200/80" />
)}
</div>
<span
className={cn(
"rounded-full border px-2 py-1 text-[11px]",
isActive
? "border-emerald-300/35 bg-emerald-400/10 text-emerald-100"
: "border-white/10 bg-black/20 text-white/45"
)}
>
{modeStatusText}
</span>
</div>
<div className="mt-4 text-sm font-semibold text-white">{mode.title}</div>
<p className="mt-2 line-clamp-3 text-xs leading-5 text-white/46">
{mode.description || "当前模式暂无说明。"}
</p>
{mode.key === "detail" ? (
<div className="mt-3 flex flex-wrap gap-2 text-[11px]">
{mode.referenceReplaceEnabled !== false ? (
<span className="rounded-full border border-white/10 bg-black/20 px-2 py-1 text-white/55">
详情图替换
</span>
) : null}
{mode.aiDetailGenerateEnabled !== false ? (
<span className="rounded-full border border-white/10 bg-black/20 px-2 py-1 text-white/55">
详情图生成
</span>
) : null}
</div>
) : null}
</button>
);
})}
</div>
</section>
) : null}
@ -4570,35 +4751,13 @@ export function AgentGenerationWorkspaceDarkV2({
<span className="text-sm font-semibold text-white">选择 SPU / SKU</span>
</div>
<p className="text-sm text-white/45">
先搜索商品白底图与搭配图,再继续完成风格步骤和生成参数配置。
根据当前生图模式选择商品白底图、SKU 图、详情参考图或手动上传素材,素材确认后再进入结果生成。
</p>
</div>
{materialSelectionPanel}
</section>
) : null}
{hasWorkflowStyleStep ? (
<section id="agent-anchor-style" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
<div className="flex items-center gap-2">
<span className="rounded-full bg-emerald-500/18 px-2.5 py-1 text-[11px] font-semibold text-emerald-200">
{styleStepBadge}
</span>
<span className="text-sm font-semibold text-white">风格步骤卡片</span>
</div>
<p className="text-sm text-white/45">
先完成风格步骤卡片,再继续选择品牌模特,方便后续做人群联动,不再跳转第二个页面。
</p>
</div>
<div className="space-y-4">
{shouldShowDirectUploadInStyleStep ? attachmentSlotPanel : null}
{workflowStylePanel}
{hasBrandModelStep ? brandModelPanel : null}
</div>
</section>
) : null}
<section id="agent-anchor-generate" className="scroll-mt-24 rounded-[24px] border border-white/10 bg-black/20 p-4">
<div className="mb-4 space-y-1 border-b border-white/10 pb-4">
<div className="flex items-center gap-2">
@ -6190,12 +6349,12 @@ export function AgentGenerationWorkspaceDarkV2({
<p className="text-base font-medium text-white/60">
{!requiresPresetSelection || selectedAgentPresetId
? `${getResultModeLabel(activeResultModeType)}结果暂为空`
: "请选择生图模式"}
: "请选择风格化模板"}
</p>
<p className="mt-2 text-sm text-white/30">
{!requiresPresetSelection || selectedAgentPresetId
? "选择模型并开始生成后,结果会保留在当前项目的对应分类中。"
: "请先从顶部流程线选择一个生图模式"}
: "请先完成上方风格化模板配置"}
</p>
{isPsdTemplateEnabled ? (
<>

View File

@ -512,7 +512,7 @@ export function AgentPresetPanelDarkV2({
<div className="relative flex-1 max-w-md">
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
<Input
placeholder="搜索生图模式..."
placeholder="搜索风格化模板..."
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
className={cn(
@ -639,7 +639,7 @@ export function AgentPresetPanelDarkV2({
setShowCreateDialog(true);
}}
initialPreset={editingPreset}
entityLabel="生图模式"
entityLabel={embeddedEntityLabel}
theme="dark"
onSaved={async (preset) => {
await mutate();

View File

@ -975,7 +975,7 @@ export function AgentWorkflowMaterialPicker({
{disabled ? (
<>
<ImageIcon className="mb-2 h-5 w-5 opacity-60" />
先选择生图模式,再搜索并选择素材图。
先选择风格化模板,再搜索并选择素材图。
</>
) : isLoading ? (
<>

View File

@ -2606,7 +2606,7 @@ export function CanvasChatPanel({
tagSpan.contentEditable = 'false';
tagSpan.setAttribute('data-attachment-index', String(index));
tagSpan.setAttribute('data-attachment-url', url);
tagSpan.className = 'inline-flex items-center gap-1 bg-muted/60 hover:bg-muted rounded-full pl-1 pr-2 py-0.5 mx-0.5 align-middle cursor-default select-none';
tagSpan.className = 'inline-flex items-center gap-1 bg-muted/60 hover:bg-muted dark:bg-white/10 dark:hover:bg-white/15 dark:text-zinc-100 rounded-full pl-1 pr-2 py-0.5 mx-0.5 align-middle cursor-default select-none';
const shortName = name.length > 10 ? name.substring(0, 8) + '...' : name;
tagSpan.innerHTML = `
<img src="${url}" alt="" class="h-5 w-5 object-cover rounded-full border border-border/50" />
@ -2661,7 +2661,7 @@ export function CanvasChatPanel({
tagSpan.setAttribute('data-object-name', objectName);
tagSpan.setAttribute('data-object-position', objectPosition);
// 样式与普通图片标签一致
tagSpan.className = 'inline-flex items-center gap-1 bg-muted/60 hover:bg-muted rounded-full pl-1 pr-2 py-0.5 mx-0.5 align-middle cursor-default select-none';
tagSpan.className = 'inline-flex items-center gap-1 bg-muted/60 hover:bg-muted dark:bg-white/10 dark:hover:bg-white/15 dark:text-zinc-100 rounded-full pl-1 pr-2 py-0.5 mx-0.5 align-middle cursor-default select-none';
// 使用图片缩略图,不使用 emoji 或图标
tagSpan.innerHTML = `
<img src="${url}" alt="" class="h-5 w-5 object-cover rounded-full border border-border/50" />
@ -9040,7 +9040,7 @@ export function CanvasChatPanel({
"h-5 w-5 overflow-hidden rounded-full",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-[#d4d9df] bg-white dark:border-white/10 dark:bg-white/10"
: "border border-[#d4d9df] bg-white dark:border-white/20 dark:bg-white"
)}
>
{model.avatarUrl ? (
@ -9182,7 +9182,7 @@ export function CanvasChatPanel({
"inline-flex h-5 w-5 items-center justify-center rounded-md",
isVideoWorkspace
? "border border-border/60 bg-background/80 text-foreground/70"
: "bg-[#eef2f7] text-[#6b7280]"
: "bg-[#eef2f7] text-[#6b7280] dark:bg-white/12 dark:text-zinc-100 dark:ring-1 dark:ring-white/10"
)}
>
<MessageCircle className="h-3.5 w-3.5" />
@ -9190,7 +9190,7 @@ export function CanvasChatPanel({
<h2
className={cn(
"min-w-0 flex-1 truncate text-[14px] font-semibold leading-tight tracking-tight",
isVideoWorkspace ? "text-foreground" : "text-[#111827]"
isVideoWorkspace ? "text-foreground" : "text-[#111827] dark:text-zinc-100"
)}
title={displayCanvasName}
>
@ -9206,7 +9206,7 @@ export function CanvasChatPanel({
"h-8 w-8 rounded-full",
isVideoWorkspace
? "border border-border/60 bg-background/72 hover:bg-background"
: "hover:bg-muted"
: "text-zinc-600 hover:bg-muted dark:text-zinc-100 dark:hover:bg-white/10"
)}
onClick={createNewChatSession}
title="新建聊天窗口"
@ -9221,8 +9221,8 @@ export function CanvasChatPanel({
"h-8 w-8 rounded-full",
isVideoWorkspace
? "border border-border/60 bg-background/72 hover:bg-background"
: "hover:bg-muted",
isSessionMenuOpen && (isVideoWorkspace ? "bg-background" : "bg-muted")
: "text-zinc-600 hover:bg-muted dark:text-zinc-100 dark:hover:bg-white/10",
isSessionMenuOpen && (isVideoWorkspace ? "bg-background" : "bg-muted dark:bg-white/10")
)}
onClick={() => setIsSessionMenuOpen((open) => !open)}
title="聊天窗口记录"
@ -9235,7 +9235,7 @@ export function CanvasChatPanel({
"absolute right-0 top-10 w-64 p-2 shadow-xl",
isVideoWorkspace
? "rounded-2xl border border-border/60 bg-background/95 backdrop-blur-xl"
: "rounded-xl border bg-white dark:bg-zinc-950"
: "rounded-xl border bg-white dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-100"
)}
>
<div className="px-2 py-1 text-xs text-muted-foreground">聊天窗口记录</div>
@ -9261,7 +9261,7 @@ export function CanvasChatPanel({
: "bg-primary/10 text-primary"
: isVideoWorkspace
? "hover:bg-background"
: "hover:bg-muted"
: "hover:bg-muted dark:hover:bg-white/10"
)}
onClick={() => switchToSession(session.id)}
>
@ -9286,7 +9286,7 @@ export function CanvasChatPanel({
"h-8 w-8 rounded-full",
isVideoWorkspace
? "border border-border/60 bg-background/72 hover:bg-background"
: "hover:bg-muted"
: "text-zinc-600 hover:bg-muted dark:text-zinc-100 dark:hover:bg-white/10"
)}
onClick={(e) => {
e.preventDefault();
@ -9307,7 +9307,7 @@ export function CanvasChatPanel({
"h-8 w-8 rounded-full",
isVideoWorkspace
? "border border-border/60 bg-background/72 hover:bg-background"
: "hover:bg-muted"
: "text-zinc-600 hover:bg-muted dark:text-zinc-100 dark:hover:bg-white/10"
)}
onClick={onClose}
disabled={!onClose}
@ -10100,7 +10100,7 @@ export function CanvasChatPanel({
)}
>
<div
className="relative flex flex-col bg-card rounded-3xl border transition-all duration-200 focus-within:border-primary/50 focus-within:bg-background"
className="relative flex flex-col rounded-3xl border bg-card transition-all duration-200 focus-within:border-primary/50 focus-within:bg-background dark:border-white/12 dark:bg-zinc-950 dark:focus-within:border-white/25 dark:focus-within:bg-zinc-950"
data-canvas-chat-input-anchor="true"
>
{isWorkspacePresentation && !isDetailGenerationMode ? (
@ -11191,7 +11191,7 @@ export function CanvasChatPanel({
suppressContentEditableWarning
className={cn(
"min-h-[40px] max-h-[120px] overflow-y-auto text-sm outline-none empty:before:content-[attr(data-placeholder)]",
isVideoWorkspace ? "empty:before:text-muted-foreground/70" : "empty:before:text-[#9aa1ad]"
isVideoWorkspace ? "empty:before:text-muted-foreground/70" : "text-[#111827] empty:before:text-[#9aa1ad] dark:text-zinc-100 dark:empty:before:text-zinc-500"
)}
data-placeholder={composerPlaceholder}
onInput={(e) => {
@ -11630,7 +11630,7 @@ export function CanvasChatPanel({
"h-8 w-8 rounded-full text-muted-foreground transition-colors hover:text-foreground mr-1",
isVideoWorkspace
? "border border-border/60 bg-background hover:bg-background/90"
: "hover:bg-muted"
: "hover:bg-muted dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
)}
onClick={() => fileInputRef.current?.click()}
disabled={isSubmitting}
@ -11658,7 +11658,7 @@ export function CanvasChatPanel({
</div>
</div>
) : (
<div className="min-w-0 shrink pl-1 border-l border-border/40 dark:border-border/60 ml-1">
<div className="min-w-0 shrink pl-1 border-l border-border/40 dark:border-white/10 ml-1">
<ImageParamsBar
modelName={activeComposerMode === "agent" ? agentDisplayName : currentModel?.displayName}
initialParams={imageParams}
@ -11673,7 +11673,7 @@ export function CanvasChatPanel({
)}
</div>
<div className="flex shrink-0 items-center gap-2 pl-2 border-l border-border/40 dark:border-border/60">
<div className="flex shrink-0 items-center gap-2 pl-2 border-l border-border/40 dark:border-white/10">
{!isWorkspacePresentation && activeComposerMode === "image" ? (
<Button
variant="ghost"
@ -11687,7 +11687,7 @@ export function CanvasChatPanel({
: "text-muted-foreground hover:bg-muted/70"
: deepThinkingEnabled
? "bg-[#eef4ff] text-[#3b5bcc] hover:bg-[#e5efff] dark:bg-blue-500/15 dark:text-blue-200 dark:hover:bg-blue-500/20"
: "text-[#6b7280] hover:bg-[#f4f6f8] dark:text-zinc-400 dark:hover:bg-white/8"
: "text-[#6b7280] hover:bg-[#f4f6f8] dark:bg-white/8 dark:text-zinc-200 dark:hover:bg-white/12 dark:hover:text-white"
)}
onClick={() => setDeepThinkingEnabled((prev) => !prev)}
disabled={isSubmitting}
@ -11726,14 +11726,14 @@ export function CanvasChatPanel({
"h-8 w-8 rounded-full p-0 justify-center [&>svg]:hidden",
isVideoWorkspace
? "border border-border/60 bg-background"
: "border border-[#cfd4dc] bg-white dark:border-white/10 dark:bg-white/8"
: "border border-[#cfd4dc] bg-white dark:border-white/20 dark:bg-white dark:text-zinc-950 dark:hover:bg-zinc-100"
)}
title={getToolbarItem("chatModelSelector").label}
>
<span
className={cn(
"flex h-6 w-6 items-center justify-center overflow-hidden rounded-full",
isVideoWorkspace ? "bg-background" : "bg-white dark:bg-transparent"
isVideoWorkspace ? "bg-background" : "bg-white dark:bg-white"
)}
>
{getToolbarItem("chatModelSelector").iconUrl ? (
@ -11759,7 +11759,7 @@ export function CanvasChatPanel({
<span
className={cn(
"flex h-full w-full items-center justify-center text-[10px] font-medium",
isVideoWorkspace ? "text-foreground/70" : "text-[#4b5563]"
isVideoWorkspace ? "text-foreground/70" : "text-[#4b5563] dark:text-zinc-950"
)}
>
{(selectDisplayModel?.displayName || "A").slice(0, 1)}
@ -11827,9 +11827,16 @@ export function CanvasChatPanel({
) : isDetailGenerationMode ? (
<>
<CanvasToolbarIcon
className="h-4 w-4"
className={cn(
"h-4 w-4",
canSend &&
(isVideoWorkspace || isDetailGenerationMode
? "opacity-95"
: "opacity-95 invert dark:invert-0")
)}
fallback={Sparkles}
item={getToolbarItem("chatStartGeneration")}
active={canSend && (isVideoWorkspace || isDetailGenerationMode)}
/>
{isWorkspacePresentation ? (
<span className="ml-2">
@ -11840,13 +11847,20 @@ export function CanvasChatPanel({
) : (
<>
<CanvasToolbarIcon
className="h-4 w-4"
className={cn(
"h-4 w-4",
canSend &&
(isVideoWorkspace || isDetailGenerationMode
? "opacity-95"
: "opacity-95 invert dark:invert-0")
)}
fallback={ArrowUp}
item={getToolbarItem(
isWorkspacePresentation
? "chatStartGeneration"
: "chatSend"
)}
active={canSend && (isVideoWorkspace || isDetailGenerationMode)}
/>
{isWorkspacePresentation ? (
<span className="ml-2">

View File

@ -1,4 +1,8 @@
import type {
AgentWorkflowBusinessModeConfig,
AgentWorkflowBusinessModeIconKey,
AgentWorkflowBusinessModeKey,
AgentWorkflowBusinessModeSettings,
AgentWorkflowBrandStoreEntry,
AgentWorkflowCategoryLabels,
AgentWorkflowConstraintSettings,
@ -34,6 +38,39 @@ export const defaultAgentWorkflowPageFeatureSettings: AgentWorkflowPageFeatureSe
psdTemplateEnabled: true,
batchProcessingEnabled: true,
};
export const defaultAgentWorkflowBusinessModeSettings: AgentWorkflowBusinessModeSettings = {
modes: [
{
key: "main_sku",
title: "主图/SKU图模式",
description: "用于主图、SKU 图和场景化商品图输出,复用当前风格化提示词配置。",
enabled: true,
iconKey: "main-sku",
customSvgUrl: "",
sortOrder: 0,
},
{
key: "detail",
title: "详情模式",
description: "包含详情图替换和详情图生成,适合做商品详情页内容。",
enabled: true,
iconKey: "detail",
customSvgUrl: "",
sortOrder: 1,
referenceReplaceEnabled: true,
aiDetailGenerateEnabled: true,
},
{
key: "buyer_show",
title: "买家秀模式",
description: "买家秀业务模块已预留,后续可在这里接入专用素材和提示词规则。",
enabled: false,
iconKey: "buyer-show",
customSvgUrl: "",
sortOrder: 2,
},
],
};
export interface AgentWorkflowBrandCategoryOption {
value: string;
@ -189,6 +226,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
},
constraintSettings: defaultAgentWorkflowConstraintSettings,
pageFeatureSettings: defaultAgentWorkflowPageFeatureSettings,
businessModeSettings: defaultAgentWorkflowBusinessModeSettings,
steps: defaultSteps.map((step, index) =>
cloneStep({
...step,
@ -278,6 +316,97 @@ export function normalizeAgentWorkflowPageFeatureSettings(
};
}
function normalizeAgentWorkflowBusinessModeKey(
value: unknown
): AgentWorkflowBusinessModeKey | null {
return value === "main_sku" || value === "detail" || value === "buyer_show"
? value
: null;
}
function normalizeAgentWorkflowBusinessModeIconKey(
value: unknown,
fallback: AgentWorkflowBusinessModeIconKey
): AgentWorkflowBusinessModeIconKey {
return value === "main-sku" || value === "detail" || value === "buyer-show"
? value
: fallback;
}
function normalizeAgentWorkflowBusinessModeConfig(
value: unknown,
fallback: AgentWorkflowBusinessModeConfig
): AgentWorkflowBusinessModeConfig {
const record =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const title = normalizeText(record.title ?? record.label ?? record.name, 40);
const description = normalizeText(record.description, 220);
const normalized: AgentWorkflowBusinessModeConfig = {
key: fallback.key,
title: title || fallback.title,
description: description || fallback.description || "",
enabled: normalizeBoolean(record.enabled, fallback.enabled),
iconKey: normalizeAgentWorkflowBusinessModeIconKey(record.iconKey, fallback.iconKey),
customSvgUrl: normalizeText(record.customSvgUrl ?? record.iconSvgUrl, 2000),
sortOrder: normalizeNumber(record.sortOrder, fallback.sortOrder, 99),
};
if (
fallback.referenceReplaceEnabled !== undefined ||
record.referenceReplaceEnabled !== undefined
) {
normalized.referenceReplaceEnabled = normalizeBoolean(
record.referenceReplaceEnabled,
fallback.referenceReplaceEnabled !== false
);
}
if (
fallback.aiDetailGenerateEnabled !== undefined ||
record.aiDetailGenerateEnabled !== undefined
) {
normalized.aiDetailGenerateEnabled = normalizeBoolean(
record.aiDetailGenerateEnabled,
fallback.aiDetailGenerateEnabled !== false
);
}
return normalized;
}
export function normalizeAgentWorkflowBusinessModeSettings(
value: unknown
): AgentWorkflowBusinessModeSettings {
const record =
value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: {};
const rawModes = Array.isArray(record.modes) ? record.modes : [];
const modeByKey = new Map<AgentWorkflowBusinessModeKey, unknown>();
rawModes.forEach((item) => {
const itemRecord =
item && typeof item === "object" && !Array.isArray(item)
? (item as Record<string, unknown>)
: {};
const key = normalizeAgentWorkflowBusinessModeKey(itemRecord.key);
if (key && !modeByKey.has(key)) {
modeByKey.set(key, item);
}
});
return {
modes: defaultAgentWorkflowBusinessModeSettings.modes
.map((fallback) =>
normalizeAgentWorkflowBusinessModeConfig(modeByKey.get(fallback.key), fallback)
)
.sort((left, right) => left.sortOrder - right.sortOrder),
};
}
export function flattenAgentWorkflowSteps(
steps: AgentWorkflowStepUiConfig[],
depth = 1,
@ -1149,6 +1278,9 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
batchProcessingEnabled: raw.batchProcessingEnabled,
}
),
businessModeSettings: normalizeAgentWorkflowBusinessModeSettings(
raw.businessModeSettings ?? raw.fixedModeSettings ?? raw.businessModes
),
steps: sanitizedSteps,
};
}
@ -1175,6 +1307,12 @@ export function getAgentWorkflowPageFeatureSettings(
return normalizeAgentWorkflowPageFeatureSettings(config?.pageFeatureSettings);
}
export function getAgentWorkflowBusinessModeSettings(
config?: AgentWorkflowUiConfig | null
) {
return normalizeAgentWorkflowBusinessModeSettings(config?.businessModeSettings);
}
export function getAgentWorkflowCategoryOptionsByBrand(params: {
config?: AgentWorkflowUiConfig | null;
brand?: string | null;

View File

@ -116,6 +116,28 @@ export interface AgentWorkflowPageFeatureSettings {
batchProcessingEnabled: boolean;
}
export type AgentWorkflowBusinessModeKey = "main_sku" | "detail" | "buyer_show";
export type AgentWorkflowBusinessModeIconKey =
| "main-sku"
| "detail"
| "buyer-show";
export interface AgentWorkflowBusinessModeConfig {
key: AgentWorkflowBusinessModeKey;
title: string;
description?: string | null;
enabled: boolean;
iconKey: AgentWorkflowBusinessModeIconKey;
customSvgUrl?: string | null;
sortOrder: number;
referenceReplaceEnabled?: boolean;
aiDetailGenerateEnabled?: boolean;
}
export interface AgentWorkflowBusinessModeSettings {
modes: AgentWorkflowBusinessModeConfig[];
}
export interface AgentWorkflowUiConfig {
inputOptions?: {
brands: string[];
@ -129,6 +151,7 @@ export interface AgentWorkflowUiConfig {
};
constraintSettings?: AgentWorkflowConstraintSettings;
pageFeatureSettings?: AgentWorkflowPageFeatureSettings;
businessModeSettings?: AgentWorkflowBusinessModeSettings;
steps: AgentWorkflowStepUiConfig[];
}