feat: expand agent workflow and project management
This commit is contained in:
parent
af7e98a4dc
commit
8d2066a3bf
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import {
|
||||
getSystemSetting,
|
||||
updateAgentPreset,
|
||||
} from "@/lib/db/queries";
|
||||
import { isAgentWorkflowAdminType } from "@/lib/middleware/admin";
|
||||
import {
|
||||
AGENT_SECONDARY_TYPE_CONFIG_KEY,
|
||||
isValidSecondaryTypeForPresetType,
|
||||
@ -31,6 +32,14 @@ const updateAgentSchema = z
|
||||
visibility: z.enum(["public", "private"]).optional(),
|
||||
categoryPath: z
|
||||
.object({
|
||||
sectionId: z.string().trim().max(64).optional(),
|
||||
sectionName: z.string().trim().max(64).optional(),
|
||||
level1Id: z.string().trim().max(64).optional(),
|
||||
level1Name: z.string().trim().max(64).optional(),
|
||||
level2Id: z.string().trim().max(64).optional(),
|
||||
level2Name: z.string().trim().max(64).optional(),
|
||||
level3Id: z.string().trim().max(64).optional(),
|
||||
level3Name: z.string().trim().max(64).optional(),
|
||||
productCategory: z.string().trim().max(64).optional(),
|
||||
audienceTag: z.string().trim().max(64).optional(),
|
||||
usageScenario: z.string().trim().max(64).optional(),
|
||||
@ -99,6 +108,11 @@ export async function PUT(
|
||||
if (!existingPreset) {
|
||||
return NextResponse.json({ success: false, error: "智能体不存在" }, { status: 404 });
|
||||
}
|
||||
const canManagePublicPreset =
|
||||
isAgentWorkflowAdminType(session.user.type) && existingPreset.visibility === "public";
|
||||
if (!canManagePublicPreset && existingPreset.ownerUserId !== session.user.id) {
|
||||
return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const effectiveType = data.type || existingPreset.type;
|
||||
const effectiveSecondaryType =
|
||||
@ -124,7 +138,7 @@ export async function PUT(
|
||||
|
||||
const preset = await updateAgentPreset({
|
||||
id,
|
||||
userId: session.user.id,
|
||||
userId: canManagePublicPreset ? existingPreset.ownerUserId : session.user.id,
|
||||
name: data.name,
|
||||
type: data.type,
|
||||
secondaryType: data.secondaryType ?? undefined,
|
||||
@ -167,11 +181,25 @@ export async function DELETE(
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const deleted = await deleteAgentPresetById({
|
||||
const existingPreset = await getAccessibleAgentPresetById({
|
||||
id,
|
||||
userId: session.user.id,
|
||||
});
|
||||
|
||||
if (!existingPreset) {
|
||||
return NextResponse.json({ success: false, error: "智能体不存在" }, { status: 404 });
|
||||
}
|
||||
const canManagePublicPreset =
|
||||
isAgentWorkflowAdminType(session.user.type) && existingPreset.visibility === "public";
|
||||
if (!canManagePublicPreset && existingPreset.ownerUserId !== session.user.id) {
|
||||
return NextResponse.json({ success: false, error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const deleted = await deleteAgentPresetById({
|
||||
id,
|
||||
userId: canManagePublicPreset ? existingPreset.ownerUserId : session.user.id,
|
||||
});
|
||||
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ success: false, error: "智能体不存在" }, { status: 404 });
|
||||
}
|
||||
|
||||
@ -16,6 +16,10 @@ import {
|
||||
const listQuerySchema = z.object({
|
||||
scope: z.enum(["all", "public", "private"]).default("all"),
|
||||
type: z.enum(["image", "video", "text"]).optional(),
|
||||
sectionId: z.string().trim().max(64).optional(),
|
||||
level1Id: z.string().trim().max(64).optional(),
|
||||
level2Id: z.string().trim().max(64).optional(),
|
||||
level3Id: z.string().trim().max(64).optional(),
|
||||
productCategory: z.string().trim().max(64).optional(),
|
||||
audienceTag: z.string().trim().max(64).optional(),
|
||||
usageScenario: z.string().trim().max(64).optional(),
|
||||
@ -41,6 +45,14 @@ const createAgentSchema = z
|
||||
visibility: z.enum(["public", "private"]).default("private"),
|
||||
categoryPath: z
|
||||
.object({
|
||||
sectionId: z.string().trim().max(64).optional(),
|
||||
sectionName: z.string().trim().max(64).optional(),
|
||||
level1Id: z.string().trim().max(64).optional(),
|
||||
level1Name: z.string().trim().max(64).optional(),
|
||||
level2Id: z.string().trim().max(64).optional(),
|
||||
level2Name: z.string().trim().max(64).optional(),
|
||||
level3Id: z.string().trim().max(64).optional(),
|
||||
level3Name: z.string().trim().max(64).optional(),
|
||||
productCategory: z.string().trim().max(64).optional(),
|
||||
audienceTag: z.string().trim().max(64).optional(),
|
||||
usageScenario: z.string().trim().max(64).optional(),
|
||||
@ -65,6 +77,10 @@ export async function GET(request: NextRequest) {
|
||||
const query = listQuerySchema.parse({
|
||||
scope: searchParams.get("scope") || "all",
|
||||
type: searchParams.get("type") || undefined,
|
||||
sectionId: searchParams.get("sectionId") || undefined,
|
||||
level1Id: searchParams.get("level1Id") || undefined,
|
||||
level2Id: searchParams.get("level2Id") || undefined,
|
||||
level3Id: searchParams.get("level3Id") || undefined,
|
||||
productCategory: searchParams.get("productCategory") || undefined,
|
||||
audienceTag: searchParams.get("audienceTag") || undefined,
|
||||
usageScenario: searchParams.get("usageScenario") || undefined,
|
||||
@ -84,6 +100,10 @@ export async function GET(request: NextRequest) {
|
||||
const filterByPath = (items: typeof presets) =>
|
||||
items.filter((item) => {
|
||||
const path = item.categoryPath || {};
|
||||
if (query.sectionId && path.sectionId !== query.sectionId) return false;
|
||||
if (query.level1Id && path.level1Id !== query.level1Id) return false;
|
||||
if (query.level2Id && path.level2Id !== query.level2Id) return false;
|
||||
if (query.level3Id && path.level3Id !== query.level3Id) return false;
|
||||
if (query.productCategory && path.productCategory !== query.productCategory) return false;
|
||||
if (query.audienceTag && path.audienceTag !== query.audienceTag) return false;
|
||||
if (query.usageScenario && path.usageScenario !== query.usageScenario) return false;
|
||||
|
||||
@ -51,6 +51,11 @@ import {
|
||||
defaultAgentWorkflowShowcaseConfig,
|
||||
normalizeAgentWorkflowShowcaseConfig,
|
||||
} from "@/lib/agents/workflow-showcase-config";
|
||||
import {
|
||||
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
defaultAgentPresetLibraryConfig,
|
||||
normalizeAgentPresetLibraryConfig,
|
||||
} from "@/lib/agents/preset-library-config";
|
||||
import {
|
||||
defaultOssImageMigrationConfig,
|
||||
normalizeOssImageMigrationConfig,
|
||||
@ -235,6 +240,7 @@ export async function GET() {
|
||||
realtimeCanvasConfigSetting,
|
||||
storageFailoverConfigSetting,
|
||||
ossImageMigrationConfigSetting,
|
||||
agentPresetLibraryConfigSetting,
|
||||
agentCategoryTreeConfigSetting,
|
||||
agentSecondaryTypeConfigSetting,
|
||||
agentWorkflowUiConfigSetting,
|
||||
@ -339,6 +345,12 @@ export async function GET() {
|
||||
.where(eq(systemSettings.key, OSS_MIGRATION_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
.where(eq(systemSettings.key, AGENT_PRESET_LIBRARY_CONFIG_KEY))
|
||||
.limit(1)
|
||||
.then(rows => rows[0]),
|
||||
db
|
||||
.select()
|
||||
.from(systemSettings)
|
||||
@ -410,6 +422,9 @@ export async function GET() {
|
||||
const ossImageMigrationConfig = normalizeOssImageMigrationConfig(
|
||||
ossImageMigrationConfigSetting?.value
|
||||
);
|
||||
const agentPresetLibraryConfig = normalizeAgentPresetLibraryConfig(
|
||||
agentPresetLibraryConfigSetting?.value
|
||||
);
|
||||
const agentCategoryTreeConfig = normalizeAgentCategoryTreeConfig(
|
||||
agentCategoryTreeConfigSetting?.value
|
||||
);
|
||||
@ -455,6 +470,7 @@ export async function GET() {
|
||||
rankingEnabled: true,
|
||||
canvasEnabled: true,
|
||||
agentEnabled: true,
|
||||
agentPresetLibraryConfig: defaultAgentPresetLibraryConfig,
|
||||
agentCategoryTreeConfig: normalizeAgentCategoryTreeConfig(undefined),
|
||||
agentSecondaryTypeConfig: defaultAgentSecondaryTypeConfig,
|
||||
agentWorkflowUiConfig: defaultAgentWorkflowUiConfig,
|
||||
@ -502,6 +518,7 @@ export async function GET() {
|
||||
lovartComposerPreviewTexts,
|
||||
lovartPromptTagVisibility,
|
||||
contactConfig,
|
||||
agentPresetLibraryConfig,
|
||||
agentCategoryTreeConfig,
|
||||
agentSecondaryTypeConfig,
|
||||
agentWorkflowUiConfig,
|
||||
@ -556,6 +573,59 @@ const updateSchema = z.object({
|
||||
rankingEnabled: z.boolean().optional(),
|
||||
canvasEnabled: z.boolean().optional(),
|
||||
agentEnabled: z.boolean().optional(),
|
||||
agentPresetLibraryConfig: z
|
||||
.object({
|
||||
sections: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().trim().min(1).max(64).optional(),
|
||||
label: z.string().trim().min(1).max(64),
|
||||
description: z.string().trim().max(200).nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
categories: z
|
||||
.array(
|
||||
z.lazy(() =>
|
||||
z.object({
|
||||
id: z.string().trim().min(1).max(64).optional(),
|
||||
name: z.string().trim().min(1).max(64),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
children: z
|
||||
.array(
|
||||
z.lazy(() =>
|
||||
z.object({
|
||||
id: z.string().trim().min(1).max(64).optional(),
|
||||
name: z.string().trim().min(1).max(64),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
children: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().trim().min(1).max(64).optional(),
|
||||
name: z.string().trim().min(1).max(64),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
children: z.array(z.any()).optional(),
|
||||
})
|
||||
)
|
||||
.max(100)
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
)
|
||||
.max(100)
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
)
|
||||
.max(100)
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
.max(30),
|
||||
})
|
||||
.optional(),
|
||||
agentCategoryTreeConfig: z
|
||||
.object({
|
||||
productCategories: z.array(z.string().trim().min(1).max(64)).max(200).optional(),
|
||||
@ -577,6 +647,16 @@ const updateSchema = z.object({
|
||||
.object({
|
||||
brands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
brandStoreEntries: z
|
||||
.array(
|
||||
z.object({
|
||||
brandName: z.string().trim().min(1).max(64),
|
||||
stores: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
})
|
||||
)
|
||||
.max(300)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
steps: z
|
||||
@ -589,15 +669,18 @@ const updateSchema = z.object({
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
options: z
|
||||
.array(
|
||||
z.object({
|
||||
key: z.string().trim().min(1).max(64),
|
||||
buttonName: z.string().trim().min(1).max(64),
|
||||
description: z.string().trim().max(300).nullable().optional(),
|
||||
previewImageUrl: z.string().trim().max(1000).nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
promptFragment: z.string().trim().max(1000).nullable().optional(),
|
||||
})
|
||||
z.lazy(() =>
|
||||
z.object({
|
||||
key: z.string().trim().min(1).max(64),
|
||||
buttonName: z.string().trim().min(1).max(64),
|
||||
description: z.string().trim().max(300).nullable().optional(),
|
||||
previewImageUrl: z.string().trim().max(1000).nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
sortOrder: z.number().min(0).max(999).optional(),
|
||||
promptFragment: z.string().trim().max(1000).nullable().optional(),
|
||||
children: z.array(z.any()).max(100).optional(),
|
||||
})
|
||||
)
|
||||
)
|
||||
.max(100),
|
||||
})
|
||||
@ -719,6 +802,7 @@ export async function PUT(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
if (!isAdmin) {
|
||||
const allowedKeys = new Set([
|
||||
"agentPresetLibraryConfig",
|
||||
"agentWorkflowUiConfig",
|
||||
"agentWorkflowMaterialApiConfig",
|
||||
"agentWorkflowBrandModelConfig",
|
||||
@ -738,6 +822,7 @@ export async function PUT(request: NextRequest) {
|
||||
lovartComposerPreviewTexts,
|
||||
lovartPromptTagVisibility,
|
||||
contactConfig,
|
||||
agentPresetLibraryConfig,
|
||||
agentCategoryTreeConfig,
|
||||
agentSecondaryTypeConfig,
|
||||
agentWorkflowUiConfig,
|
||||
@ -908,6 +993,26 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (agentPresetLibraryConfig !== undefined) {
|
||||
const normalized = normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (agentCategoryTreeConfig !== undefined) {
|
||||
const normalized = normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig);
|
||||
await db
|
||||
@ -1196,6 +1301,10 @@ export async function PUT(request: NextRequest) {
|
||||
contactConfig !== undefined
|
||||
? normalizeContactConfig(contactConfig)
|
||||
: normalizeContactConfig(undefined),
|
||||
agentPresetLibraryConfig:
|
||||
agentPresetLibraryConfig !== undefined
|
||||
? normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig)
|
||||
: defaultAgentPresetLibraryConfig,
|
||||
agentCategoryTreeConfig:
|
||||
agentCategoryTreeConfig !== undefined
|
||||
? normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig)
|
||||
@ -1428,6 +1537,26 @@ export async function PUT(request: NextRequest) {
|
||||
});
|
||||
}
|
||||
|
||||
if (agentPresetLibraryConfig !== undefined) {
|
||||
const normalized = normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig);
|
||||
await db
|
||||
.insert(systemSettings)
|
||||
.values({
|
||||
key: AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: systemSettings.key,
|
||||
set: {
|
||||
value: normalized,
|
||||
updatedAt: new Date(),
|
||||
updatedBy: session.user.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (agentCategoryTreeConfig !== undefined) {
|
||||
const normalized = normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig);
|
||||
await db
|
||||
@ -1716,6 +1845,10 @@ export async function PUT(request: NextRequest) {
|
||||
contactConfig !== undefined
|
||||
? normalizeContactConfig(contactConfig)
|
||||
: normalizeContactConfig(undefined),
|
||||
agentPresetLibraryConfig:
|
||||
agentPresetLibraryConfig !== undefined
|
||||
? normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig)
|
||||
: defaultAgentPresetLibraryConfig,
|
||||
agentCategoryTreeConfig:
|
||||
agentCategoryTreeConfig !== undefined
|
||||
? normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig)
|
||||
|
||||
@ -249,6 +249,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
const [agentProjectResultUrls, setAgentProjectResultUrls] = useState<string[]>([]);
|
||||
const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false);
|
||||
const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false);
|
||||
const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false);
|
||||
const [workflowHydrationSource, setWorkflowHydrationSource] = useState<
|
||||
"pending" | "project" | "local" | "empty"
|
||||
>("pending");
|
||||
@ -597,11 +598,17 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
]);
|
||||
|
||||
const persistAgentProject = useCallback(
|
||||
async (reason: "autosave" | "manual" | "switch") => {
|
||||
async (
|
||||
reason: "autosave" | "manual" | "switch",
|
||||
overrides?: { title?: string | null }
|
||||
) => {
|
||||
if (!isAgentMode || !agentProjectId) return false;
|
||||
const nextTitle =
|
||||
String(overrides?.title ?? agentProjectTitle ?? "").trim() ||
|
||||
buildDefaultAgentProjectTitle();
|
||||
|
||||
const result = await agentProjectService.updateProject(agentProjectId, {
|
||||
title: agentProjectTitle || buildDefaultAgentProjectTitle(),
|
||||
title: nextTitle,
|
||||
thumbnail: agentProjectThumbnail,
|
||||
projectState: buildPersistedAgentProjectState(),
|
||||
});
|
||||
@ -752,9 +759,22 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
toast.error("当前还没有可保存的生图项目");
|
||||
return;
|
||||
}
|
||||
|
||||
const suggestedTitle =
|
||||
String(agentProjectTitle || "").trim() || buildDefaultAgentProjectTitle();
|
||||
const promptedTitle = window.prompt("请输入要保存的项目名称", suggestedTitle);
|
||||
if (promptedTitle === null) {
|
||||
return;
|
||||
}
|
||||
const nextTitle = promptedTitle.trim();
|
||||
if (!nextTitle) {
|
||||
toast.error("项目名称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsAgentProjectSaving(true);
|
||||
try {
|
||||
const success = await persistAgentProject("manual");
|
||||
const success = await persistAgentProject("manual", { title: nextTitle });
|
||||
if (success) {
|
||||
toast.success("生图项目已保存");
|
||||
} else {
|
||||
@ -765,6 +785,36 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
}
|
||||
}, [agentProjectId, persistAgentProject]);
|
||||
|
||||
const handleRenameAgentProject = useCallback(
|
||||
async (targetProjectId: string, currentTitle?: string | null) => {
|
||||
const suggestedTitle =
|
||||
String(currentTitle || "").trim() || "未命名生图项目";
|
||||
const promptedTitle = window.prompt("请输入新的项目名称", suggestedTitle);
|
||||
if (promptedTitle === null) {
|
||||
return;
|
||||
}
|
||||
const nextTitle = promptedTitle.trim();
|
||||
if (!nextTitle) {
|
||||
toast.error("项目名称不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentProjectService.updateProject(targetProjectId, {
|
||||
title: nextTitle,
|
||||
});
|
||||
if (!result.success || !result.data) {
|
||||
toast.error(result.error || "更新生图项目名称失败");
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetProjectId === agentProjectId) {
|
||||
setAgentProjectTitle(result.data.title || nextTitle);
|
||||
}
|
||||
toast.success("项目名称已更新");
|
||||
},
|
||||
[agentProjectId]
|
||||
);
|
||||
|
||||
const handleCreateAgentProject = useCallback(async () => {
|
||||
if (agentProjectId) {
|
||||
await persistAgentProject("switch");
|
||||
@ -1357,28 +1407,58 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
||||
|
||||
const agentProjectNavSlot = isAgentMode ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
void handleManualSaveAgentProject();
|
||||
}}
|
||||
disabled={!hasActiveAgentProject || isAgentProjectLoading || isAgentProjectSaving}
|
||||
className="h-9 gap-2 border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||
>
|
||||
{isAgentProjectSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
保存项目
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isAgentProjectLoading}
|
||||
className="h-9 gap-2 border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||
>
|
||||
{isAgentProjectSaving ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="h-4 w-4" />
|
||||
)}
|
||||
项目管理
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-44 border-white/10 bg-[#0b0d0f] text-white"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
disabled={!hasActiveAgentProject || isAgentProjectLoading || isAgentProjectSaving}
|
||||
className="cursor-pointer text-white focus:bg-white/10 focus:text-white"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
void handleManualSaveAgentProject();
|
||||
}}
|
||||
>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
保存项目
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="bg-white/10" />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer text-white focus:bg-white/10 focus:text-white"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault();
|
||||
setIsAgentProjectSheetOpen(true);
|
||||
}}
|
||||
>
|
||||
<Grid3X3 className="mr-2 h-4 w-4" />
|
||||
项目列表
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<AgentProjectListSheet
|
||||
open={isAgentProjectSheetOpen}
|
||||
onOpenChange={setIsAgentProjectSheetOpen}
|
||||
currentProjectId={agentProjectId || null}
|
||||
currentProjectTitle={agentProjectTitle}
|
||||
onCreateProject={handleCreateAgentProject}
|
||||
onSelectProject={handleSelectAgentProject}
|
||||
onDeleteProject={handleDeleteAgentProject}
|
||||
onRenameProject={handleRenameAgentProject}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@ -156,7 +156,6 @@ type ToolStepKey =
|
||||
| "preset"
|
||||
| "brand-model"
|
||||
| "materials"
|
||||
| "slots"
|
||||
| "prompt"
|
||||
| "settings";
|
||||
|
||||
@ -808,15 +807,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "slots" as const,
|
||||
label: "图片槽位",
|
||||
description: "检查和补齐参考图槽位",
|
||||
icon: ImagePlus,
|
||||
disabled: !selectedAgentPresetId,
|
||||
completed: hasSatisfiedSlotRequirement,
|
||||
optional: false,
|
||||
},
|
||||
{
|
||||
key: "prompt" as const,
|
||||
label: "补充要求",
|
||||
@ -956,11 +946,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
if (current !== "preset") return current;
|
||||
if (isDetailReplacementPreset || isSkuReplacementPreset) {
|
||||
if (hasMaterialStep) return "materials";
|
||||
return "slots";
|
||||
return "prompt";
|
||||
}
|
||||
if (hasBrandModelStep) return "brand-model";
|
||||
if (hasMaterialStep) return "materials";
|
||||
return "slots";
|
||||
return "prompt";
|
||||
});
|
||||
}, [
|
||||
hasBrandModelStep,
|
||||
@ -1481,6 +1471,179 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}));
|
||||
}, [updateSkuReplacementState]);
|
||||
|
||||
const attachmentSlotPanel = (
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<ImagePlus className="h-4 w-4 text-emerald-300" />
|
||||
输入图槽位
|
||||
</h3>
|
||||
<div className="mb-4 rounded-[20px] border border-emerald-400/15 bg-emerald-500/[0.06] px-4 py-3 text-xs leading-6 text-emerald-100/80">
|
||||
上一步已选的白底图会自动带入对应产品槽位,这里只作为“检查 / 替换 / 微调映射”的调整区,不再单独占一个步骤页。
|
||||
</div>
|
||||
{agentUploadSlots.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{agentUploadSlots.map((slot, index) => {
|
||||
const isActiveAssignmentSlot = activeAssignmentSlotIndex === index;
|
||||
return (
|
||||
<motion.div
|
||||
key={slot.label}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className={cn(
|
||||
"group relative aspect-square overflow-hidden rounded-[24px] transition-all",
|
||||
attachments[index]
|
||||
? "ring-2 ring-emerald-400/45"
|
||||
: "border-2 border-dashed border-white/15 bg-black/25 hover:border-emerald-400/35",
|
||||
isActiveAssignmentSlot && "ring-2 ring-sky-400/65 ring-offset-2 ring-offset-black"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={(el) => {
|
||||
fileInputRefs.current[index] = el;
|
||||
}}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => handleSlotFileChange(index, e)}
|
||||
/>
|
||||
{attachments[index] ? (
|
||||
<>
|
||||
<img
|
||||
src={buildPreviewImageUrl(attachments[index]!.url, 256)}
|
||||
alt={attachments[index]!.name || slot.label}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-black/60 px-2 py-1 text-[10px] text-white">
|
||||
{slot.label}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-white transition-opacity hover:bg-white/30"
|
||||
onClick={() => updateAttachmentAtSlot(index, null)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-1 text-white/30">
|
||||
{slotUploading === index ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin text-emerald-400" />
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-5 w-5" />
|
||||
<span className="text-[10px]">{slot.label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-x-0 top-0 flex items-center justify-between gap-2 p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRefs.current[index]?.click()}
|
||||
className="rounded-full border border-white/10 bg-black/45 px-2.5 py-1 text-[11px] text-white/80 transition-colors hover:border-white/20 hover:text-white"
|
||||
>
|
||||
{attachments[index] ? "更换" : "上传"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveAssignmentSlotIndex(index)}
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-[11px] transition-colors",
|
||||
isActiveAssignmentSlot
|
||||
? "border-sky-400/45 bg-sky-500/[0.18] text-sky-100"
|
||||
: "border-white/10 bg-black/45 text-white/70 hover:border-white/20 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{isActiveAssignmentSlot ? "当前目标" : "设为目标"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{slotSourceLibrary.length > 0 ? (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/20 p-3">
|
||||
<div className="mb-3 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">从已选素材快速替换槽位</div>
|
||||
<div className="text-xs text-white/42">
|
||||
先点击上方槽位的“设为目标”,再点击下方素材图,将它放入对应槽位。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/60">
|
||||
当前目标:
|
||||
{activeAssignmentSlotIndex !== null
|
||||
? agentUploadSlots[activeAssignmentSlotIndex]?.label || "未选择"
|
||||
: "未选择"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{assignableSourceLibrary.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
|
||||
{assignableSourceLibrary.map((source) => {
|
||||
const assignedToActiveSlot =
|
||||
activeAssignmentSlotIndex !== null &&
|
||||
attachments[activeAssignmentSlotIndex]?.url === source.url;
|
||||
return (
|
||||
<button
|
||||
key={source.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (activeAssignmentSlotIndex === null) return;
|
||||
handleAssignSourceToSlot(activeAssignmentSlotIndex, source);
|
||||
}}
|
||||
disabled={activeAssignmentSlotIndex === null}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-[20px] border text-left transition-all",
|
||||
assignedToActiveSlot
|
||||
? "border-emerald-400/55 bg-emerald-500/[0.08]"
|
||||
: "border-white/10 bg-white/[0.03] hover:border-white/18",
|
||||
activeAssignmentSlotIndex === null && "cursor-not-allowed opacity-45"
|
||||
)}
|
||||
>
|
||||
<div className="relative aspect-square overflow-hidden bg-black/30">
|
||||
<img
|
||||
src={buildPreviewImageUrl(source.url, 320)}
|
||||
alt={source.label}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute left-2 top-2 rounded-full bg-black/60 px-2 py-1 text-[10px] text-white">
|
||||
{source.sourceLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 px-3 py-2">
|
||||
<div className="truncate text-xs font-medium text-white/88">
|
||||
{source.label}
|
||||
</div>
|
||||
<div className="truncate text-[11px] text-white/42">
|
||||
{source.name}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[92px] items-center justify-center rounded-[20px] border border-dashed border-white/10 bg-white/[0.02] px-4 text-center text-xs text-white/40">
|
||||
{activeAssignmentIsModelSlot
|
||||
? "当前模特槽位还没有可分配的品牌模特图,请先在“品牌模特”步骤里选择。"
|
||||
: "当前产品槽位还没有来自已选素材区的图片,请先到“SKU / SPU 选图”里完成选择。"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="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>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-dvh flex-col overflow-hidden bg-black">
|
||||
<AgentCanvasTopNavDark
|
||||
@ -1576,176 +1739,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className={cn(activeToolStep === "slots" ? "block" : "hidden")}>
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<ImagePlus className="h-4 w-4 text-emerald-300" />
|
||||
图片槽位
|
||||
</h3>
|
||||
{agentUploadSlots.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{agentUploadSlots.map((slot, index) => {
|
||||
const isActiveAssignmentSlot = activeAssignmentSlotIndex === index;
|
||||
return (
|
||||
<motion.div
|
||||
key={slot.label}
|
||||
whileHover={{ scale: 1.02 }}
|
||||
className={cn(
|
||||
"group relative aspect-square overflow-hidden rounded-[24px] transition-all",
|
||||
attachments[index]
|
||||
? "ring-2 ring-emerald-400/45"
|
||||
: "border-2 border-dashed border-white/15 bg-black/25 hover:border-emerald-400/35",
|
||||
isActiveAssignmentSlot && "ring-2 ring-sky-400/65 ring-offset-2 ring-offset-black"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={(el) => {
|
||||
fileInputRefs.current[index] = el;
|
||||
}}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => handleSlotFileChange(index, e)}
|
||||
/>
|
||||
{attachments[index] ? (
|
||||
<>
|
||||
<img
|
||||
src={buildPreviewImageUrl(attachments[index]!.url, 256)}
|
||||
alt={attachments[index]!.name || slot.label}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-x-0 bottom-0 bg-black/60 px-2 py-1 text-[10px] text-white">
|
||||
{slot.label}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-white/20 text-white transition-opacity hover:bg-white/30"
|
||||
onClick={() => updateAttachmentAtSlot(index, null)}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-1 text-white/30">
|
||||
{slotUploading === index ? (
|
||||
<Loader2 className="h-5 w-5 animate-spin text-emerald-400" />
|
||||
) : (
|
||||
<>
|
||||
<Upload className="h-5 w-5" />
|
||||
<span className="text-[10px]">{slot.label}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute inset-x-0 top-0 flex items-center justify-between gap-2 p-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRefs.current[index]?.click()}
|
||||
className="rounded-full border border-white/10 bg-black/45 px-2.5 py-1 text-[11px] text-white/80 transition-colors hover:border-white/20 hover:text-white"
|
||||
>
|
||||
{attachments[index] ? "更换" : "上传"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveAssignmentSlotIndex(index)}
|
||||
className={cn(
|
||||
"rounded-full border px-2.5 py-1 text-[11px] transition-colors",
|
||||
isActiveAssignmentSlot
|
||||
? "border-sky-400/45 bg-sky-500/[0.18] text-sky-100"
|
||||
: "border-white/10 bg-black/45 text-white/70 hover:border-white/20 hover:text-white"
|
||||
)}
|
||||
>
|
||||
{isActiveAssignmentSlot ? "当前目标" : "设为目标"}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{slotSourceLibrary.length > 0 ? (
|
||||
<div className="rounded-[24px] border border-white/10 bg-black/20 p-3">
|
||||
<div className="mb-3 flex flex-col gap-2 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-white">从第一步已选素材填入槽位</div>
|
||||
<div className="text-xs text-white/42">
|
||||
先点击上方槽位的“设为目标”,再点击下方素材图,将它放入对应槽位。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/60">
|
||||
当前目标:
|
||||
{activeAssignmentSlotIndex !== null
|
||||
? agentUploadSlots[activeAssignmentSlotIndex]?.label || "未选择"
|
||||
: "未选择"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{assignableSourceLibrary.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-3 xl:grid-cols-4">
|
||||
{assignableSourceLibrary.map((source) => {
|
||||
const assignedToActiveSlot =
|
||||
activeAssignmentSlotIndex !== null &&
|
||||
attachments[activeAssignmentSlotIndex]?.url === source.url;
|
||||
return (
|
||||
<button
|
||||
key={source.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (activeAssignmentSlotIndex === null) return;
|
||||
handleAssignSourceToSlot(activeAssignmentSlotIndex, source);
|
||||
}}
|
||||
disabled={activeAssignmentSlotIndex === null}
|
||||
className={cn(
|
||||
"overflow-hidden rounded-[20px] border text-left transition-all",
|
||||
assignedToActiveSlot
|
||||
? "border-emerald-400/55 bg-emerald-500/[0.08]"
|
||||
: "border-white/10 bg-white/[0.03] hover:border-white/18",
|
||||
activeAssignmentSlotIndex === null && "cursor-not-allowed opacity-45"
|
||||
)}
|
||||
>
|
||||
<div className="relative aspect-square overflow-hidden bg-black/30">
|
||||
<img
|
||||
src={buildPreviewImageUrl(source.url, 320)}
|
||||
alt={source.label}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute left-2 top-2 rounded-full bg-black/60 px-2 py-1 text-[10px] text-white">
|
||||
{source.sourceLabel}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1 px-3 py-2">
|
||||
<div className="truncate text-xs font-medium text-white/88">
|
||||
{source.label}
|
||||
</div>
|
||||
<div className="truncate text-[11px] text-white/42">
|
||||
{source.name}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex min-h-[92px] items-center justify-center rounded-[20px] border border-dashed border-white/10 bg-white/[0.02] px-4 text-center text-xs text-white/40">
|
||||
{activeAssignmentIsModelSlot
|
||||
? "当前模特槽位还没有可分配的品牌模特图,请先在“品牌模特”步骤里选择。"
|
||||
: "当前产品槽位还没有来自第一步的已选商品图,请先到“SKU / SPU 选图”里完成选择。"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="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>
|
||||
</div>
|
||||
|
||||
<div className={cn(activeToolStep === "prompt" ? "block" : "hidden")}>
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
@ -1762,7 +1755,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
|
||||
<div className={cn(activeToolStep === "settings" ? "block" : "hidden")}>
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<div className="space-y-4">
|
||||
{attachmentSlotPanel}
|
||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
||||
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||
<Settings2 className="h-4 w-4 text-emerald-300" />
|
||||
生成参数
|
||||
@ -1835,6 +1830,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1997,7 +1993,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
))
|
||||
) : (
|
||||
<div className="flex min-h-[160px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-4 text-center text-sm text-white/42">
|
||||
先在上方“图片槽位”中选择用于详情替换的白底输入图。
|
||||
请先在“生成参数”里的输入图槽位中确认用于详情替换的白底输入图。
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -2304,7 +2300,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
))
|
||||
) : (
|
||||
<div className="flex min-h-[160px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-4 text-center text-sm text-white/42">
|
||||
先在上方“图片槽位”中选择用于 SKU 替换的白底输入图。
|
||||
请先在“生成参数”里的输入图槽位中确认用于 SKU 替换的白底输入图。
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -82,6 +82,9 @@ export function AgentPresetCardDark({
|
||||
: imageModeType === "scene_generation"
|
||||
? "场景图"
|
||||
: null;
|
||||
const showEdit = Boolean(onEdit);
|
||||
const showDelete = Boolean(onDelete);
|
||||
const showFork = Boolean(onFork) && !showEdit && !showDelete;
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
@ -103,6 +106,45 @@ export function AgentPresetCardDark({
|
||||
)}
|
||||
onClick={() => onSelect(preset.id)}
|
||||
>
|
||||
<div className="absolute right-2 top-2 z-10 flex gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
{showEdit ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit?.(preset);
|
||||
}}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full bg-black/65 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<Pencil className="h-3 w-3" />
|
||||
</button>
|
||||
) : null}
|
||||
{showDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete?.(preset);
|
||||
}}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full bg-black/65 text-white/80 backdrop-blur-md transition-all hover:bg-red-500/80 hover:text-white"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
</button>
|
||||
) : null}
|
||||
{showFork ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFork?.(preset);
|
||||
}}
|
||||
className="flex h-6 w-6 items-center justify-center rounded-full bg-black/65 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto h-[70px] w-[70px] overflow-hidden rounded-2xl border border-white/10 bg-black/30">
|
||||
{previewUrl ? (
|
||||
<img
|
||||
@ -237,36 +279,36 @@ export function AgentPresetCardDark({
|
||||
whileHover={{ opacity: 1 }}
|
||||
>
|
||||
<div className="flex flex-col gap-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
{preset.visibility === "private" && onEdit && (
|
||||
{showEdit && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit(preset);
|
||||
onEdit?.(preset);
|
||||
}}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{preset.visibility === "private" && onDelete && (
|
||||
{showDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDelete(preset);
|
||||
onDelete?.(preset);
|
||||
}}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white/80 backdrop-blur-md transition-all hover:bg-red-500/80 hover:text-white"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
{preset.visibility === "public" && onFork && (
|
||||
{showFork && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFork(preset);
|
||||
onFork?.(preset);
|
||||
}}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full bg-black/60 text-white/80 backdrop-blur-md transition-all hover:bg-white/20 hover:text-white"
|
||||
>
|
||||
|
||||
@ -30,9 +30,17 @@ import {
|
||||
normalizeAgentPresetModeConfig,
|
||||
patchAgentDefaultParamsWithModeConfig,
|
||||
} from "@/lib/agents/preset-mode-config";
|
||||
import {
|
||||
buildAgentPresetCategoryPath,
|
||||
getEnabledAgentPresetLibraryNodes,
|
||||
getEnabledAgentPresetLibrarySections,
|
||||
normalizeAgentPresetLibraryConfig,
|
||||
resolveAgentPresetCategoryPath,
|
||||
} from "@/lib/agents/preset-library-config";
|
||||
import { imageGenerationService } from "@/services/canvas";
|
||||
import { agentService } from "@/services/agents";
|
||||
import type {
|
||||
AgentPresetLibraryConfig,
|
||||
AgentCreatePayload,
|
||||
AgentPresetModeType,
|
||||
AgentPresetModeConfig,
|
||||
@ -111,6 +119,9 @@ export function AgentPresetDialog({
|
||||
isMultimodal?: boolean;
|
||||
}>;
|
||||
}>("/api/models", fetcher);
|
||||
const { data: siteSettingsData } = useSWR<{
|
||||
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
|
||||
}>("/api/site-settings", fetcher);
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [type, setType] = useState<"image" | "video" | "text">("image");
|
||||
@ -127,6 +138,10 @@ export function AgentPresetDialog({
|
||||
const [uploadingSlotIndex, setUploadingSlotIndex] = useState<number | null>(null);
|
||||
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||
const [slots, setSlots] = useState<DialogSlotState[]>([createDefaultDialogSlot()]);
|
||||
const [selectedPresetSectionId, setSelectedPresetSectionId] = useState<string>("");
|
||||
const [selectedPresetLevel1Id, setSelectedPresetLevel1Id] = useState<string>("");
|
||||
const [selectedPresetLevel2Id, setSelectedPresetLevel2Id] = useState<string>("");
|
||||
const [selectedPresetLevel3Id, setSelectedPresetLevel3Id] = useState<string>("");
|
||||
const fileInputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||
const avatarInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const isDark = theme === "dark";
|
||||
@ -153,6 +168,50 @@ export function AgentPresetDialog({
|
||||
...IMAGE_MODE_OPTIONS,
|
||||
]
|
||||
: IMAGE_MODE_OPTIONS;
|
||||
const presetLibraryConfig = useMemo(
|
||||
() => normalizeAgentPresetLibraryConfig(siteSettingsData?.agentPresetLibraryConfig),
|
||||
[siteSettingsData?.agentPresetLibraryConfig]
|
||||
);
|
||||
const presetLibrarySections = useMemo(
|
||||
() => getEnabledAgentPresetLibrarySections(presetLibraryConfig),
|
||||
[presetLibraryConfig]
|
||||
);
|
||||
const selectedPresetSection = useMemo(
|
||||
() =>
|
||||
presetLibrarySections.find((section) => section.id === selectedPresetSectionId) ||
|
||||
null,
|
||||
[presetLibrarySections, selectedPresetSectionId]
|
||||
);
|
||||
const presetLevel1Options = useMemo(
|
||||
() => getEnabledAgentPresetLibraryNodes(selectedPresetSection?.categories || []),
|
||||
[selectedPresetSection]
|
||||
);
|
||||
const selectedPresetLevel1 = useMemo(
|
||||
() =>
|
||||
presetLevel1Options.find((node) => node.id === selectedPresetLevel1Id) ||
|
||||
null,
|
||||
[presetLevel1Options, selectedPresetLevel1Id]
|
||||
);
|
||||
const presetLevel2Options = useMemo(
|
||||
() => getEnabledAgentPresetLibraryNodes(selectedPresetLevel1?.children || []),
|
||||
[selectedPresetLevel1]
|
||||
);
|
||||
const selectedPresetLevel2 = useMemo(
|
||||
() =>
|
||||
presetLevel2Options.find((node) => node.id === selectedPresetLevel2Id) ||
|
||||
null,
|
||||
[presetLevel2Options, selectedPresetLevel2Id]
|
||||
);
|
||||
const presetLevel3Options = useMemo(
|
||||
() => getEnabledAgentPresetLibraryNodes(selectedPresetLevel2?.children || []),
|
||||
[selectedPresetLevel2]
|
||||
);
|
||||
const selectedPresetLevel3 = useMemo(
|
||||
() =>
|
||||
presetLevel3Options.find((node) => node.id === selectedPresetLevel3Id) ||
|
||||
null,
|
||||
[presetLevel3Options, selectedPresetLevel3Id]
|
||||
);
|
||||
|
||||
const videoProviderTypes = useMemo(
|
||||
() => new Set(["volcano-video", "doubao-video", "qianwen-video", "fal-video", "ruxa-video"]),
|
||||
@ -212,6 +271,10 @@ export function AgentPresetDialog({
|
||||
modeType: "scene_generation",
|
||||
});
|
||||
setSlots([createDefaultDialogSlot()]);
|
||||
setSelectedPresetSectionId("");
|
||||
setSelectedPresetLevel1Id("");
|
||||
setSelectedPresetLevel2Id("");
|
||||
setSelectedPresetLevel3Id("");
|
||||
setUploadingSlotIndex(null);
|
||||
};
|
||||
|
||||
@ -246,7 +309,15 @@ export function AgentPresetDialog({
|
||||
})),
|
||||
}))
|
||||
);
|
||||
}, [initialPreset, open]);
|
||||
const resolvedCategoryPath = resolveAgentPresetCategoryPath({
|
||||
categoryPath: initialPreset.categoryPath || null,
|
||||
config: presetLibraryConfig,
|
||||
});
|
||||
setSelectedPresetSectionId(resolvedCategoryPath.sectionId || "");
|
||||
setSelectedPresetLevel1Id(resolvedCategoryPath.level1Id || "");
|
||||
setSelectedPresetLevel2Id(resolvedCategoryPath.level2Id || "");
|
||||
setSelectedPresetLevel3Id(resolvedCategoryPath.level3Id || "");
|
||||
}, [initialPreset, open, presetLibraryConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
if (type !== "image" || modeConfig.modeType === "legacy") {
|
||||
@ -281,6 +352,48 @@ export function AgentPresetDialog({
|
||||
});
|
||||
}, [modeConfig.modeType, type]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedPresetSectionId &&
|
||||
!presetLibrarySections.some((section) => section.id === selectedPresetSectionId)
|
||||
) {
|
||||
setSelectedPresetSectionId("");
|
||||
setSelectedPresetLevel1Id("");
|
||||
setSelectedPresetLevel2Id("");
|
||||
setSelectedPresetLevel3Id("");
|
||||
}
|
||||
}, [presetLibrarySections, selectedPresetSectionId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedPresetLevel1Id &&
|
||||
!presetLevel1Options.some((node) => node.id === selectedPresetLevel1Id)
|
||||
) {
|
||||
setSelectedPresetLevel1Id("");
|
||||
setSelectedPresetLevel2Id("");
|
||||
setSelectedPresetLevel3Id("");
|
||||
}
|
||||
}, [presetLevel1Options, selectedPresetLevel1Id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedPresetLevel2Id &&
|
||||
!presetLevel2Options.some((node) => node.id === selectedPresetLevel2Id)
|
||||
) {
|
||||
setSelectedPresetLevel2Id("");
|
||||
setSelectedPresetLevel3Id("");
|
||||
}
|
||||
}, [presetLevel2Options, selectedPresetLevel2Id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
selectedPresetLevel3Id &&
|
||||
!presetLevel3Options.some((node) => node.id === selectedPresetLevel3Id)
|
||||
) {
|
||||
setSelectedPresetLevel3Id("");
|
||||
}
|
||||
}, [presetLevel3Options, selectedPresetLevel3Id]);
|
||||
|
||||
const handleSlotChange = (slotIndex: number, updates: Partial<DialogSlotState>) => {
|
||||
setSlots((current) =>
|
||||
current.map((slot, index) =>
|
||||
@ -432,10 +545,17 @@ export function AgentPresetDialog({
|
||||
const nextDefaultParams = isStructuredImageModeBuilder
|
||||
? patchAgentDefaultParamsWithModeConfig(initialPreset?.defaultParams || null, modeConfig)
|
||||
: undefined;
|
||||
const nextCategoryPath = buildAgentPresetCategoryPath({
|
||||
section: selectedPresetSection,
|
||||
level1: selectedPresetLevel1,
|
||||
level2: selectedPresetLevel2,
|
||||
level3: selectedPresetLevel3,
|
||||
});
|
||||
const basePayload: AgentCreatePayload = {
|
||||
name: name.trim(),
|
||||
type,
|
||||
visibility,
|
||||
categoryPath: nextCategoryPath,
|
||||
description: description.trim() || undefined,
|
||||
defaultModelId,
|
||||
defaultParams: nextDefaultParams || undefined,
|
||||
@ -455,7 +575,6 @@ export function AgentPresetDialog({
|
||||
? await agentService.updatePreset(initialPreset.id, {
|
||||
...basePayload,
|
||||
secondaryType: null,
|
||||
categoryPath: null,
|
||||
} satisfies AgentUpdatePayload)
|
||||
: await agentService.createPreset(basePayload);
|
||||
|
||||
@ -693,6 +812,184 @@ export function AgentPresetDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{type === "image" ? (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-xl border p-4",
|
||||
isDark ? "border-white/10 bg-white/[0.02]" : "border-slate-200 bg-muted/30"
|
||||
)}
|
||||
>
|
||||
<div className="mb-4">
|
||||
<div className={cn("text-sm font-semibold", isDark ? "text-white" : "text-foreground")}>
|
||||
生图模式分类
|
||||
</div>
|
||||
<div className={cn("mt-1.5 text-sm leading-relaxed", isDark ? "text-white/50" : "text-muted-foreground")}>
|
||||
选择该生图模式显示在哪个分类页,以及对应的一级、二级、三级分类。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{presetLibrarySections.length > 0 ? (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>顶部分类页</div>
|
||||
<Select
|
||||
value={selectedPresetSectionId || "__none__"}
|
||||
onValueChange={(value) => {
|
||||
const nextSectionId = value === "__none__" ? "" : value;
|
||||
setSelectedPresetSectionId(nextSectionId);
|
||||
setSelectedPresetLevel1Id("");
|
||||
setSelectedPresetLevel2Id("");
|
||||
setSelectedPresetLevel3Id("");
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="选择顶部分类页" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={cn(isDark ? "border-white/10 bg-zinc-950 text-white" : "")}>
|
||||
<SelectItem value="__none__" className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}>
|
||||
不分类
|
||||
</SelectItem>
|
||||
{presetLibrarySections.map((section) => (
|
||||
<SelectItem
|
||||
key={section.id}
|
||||
value={section.id}
|
||||
className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}
|
||||
>
|
||||
{section.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>一级分类</div>
|
||||
<Select
|
||||
value={selectedPresetLevel1Id || "__none__"}
|
||||
onValueChange={(value) => {
|
||||
const nextValue = value === "__none__" ? "" : value;
|
||||
setSelectedPresetLevel1Id(nextValue);
|
||||
setSelectedPresetLevel2Id("");
|
||||
setSelectedPresetLevel3Id("");
|
||||
}}
|
||||
disabled={!selectedPresetSection || presetLevel1Options.length === 0}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="选择一级分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={cn(isDark ? "border-white/10 bg-zinc-950 text-white" : "")}>
|
||||
<SelectItem value="__none__" className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}>
|
||||
不选择
|
||||
</SelectItem>
|
||||
{presetLevel1Options.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={node.id}
|
||||
className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}
|
||||
>
|
||||
{node.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>二级分类</div>
|
||||
<Select
|
||||
value={selectedPresetLevel2Id || "__none__"}
|
||||
onValueChange={(value) => {
|
||||
const nextValue = value === "__none__" ? "" : value;
|
||||
setSelectedPresetLevel2Id(nextValue);
|
||||
setSelectedPresetLevel3Id("");
|
||||
}}
|
||||
disabled={!selectedPresetLevel1 || presetLevel2Options.length === 0}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="选择二级分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={cn(isDark ? "border-white/10 bg-zinc-950 text-white" : "")}>
|
||||
<SelectItem value="__none__" className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}>
|
||||
不选择
|
||||
</SelectItem>
|
||||
{presetLevel2Options.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={node.id}
|
||||
className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}
|
||||
>
|
||||
{node.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/90" : "text-foreground")}>三级分类</div>
|
||||
<Select
|
||||
value={selectedPresetLevel3Id || "__none__"}
|
||||
onValueChange={(value) => {
|
||||
setSelectedPresetLevel3Id(value === "__none__" ? "" : value);
|
||||
}}
|
||||
disabled={!selectedPresetLevel2 || presetLevel3Options.length === 0}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-9 text-sm",
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="选择三级分类" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={cn(isDark ? "border-white/10 bg-zinc-950 text-white" : "")}>
|
||||
<SelectItem value="__none__" className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}>
|
||||
不选择
|
||||
</SelectItem>
|
||||
{presetLevel3Options.map((node) => (
|
||||
<SelectItem
|
||||
key={node.id}
|
||||
value={node.id}
|
||||
className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}
|
||||
>
|
||||
{node.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn("rounded-lg border border-dashed px-4 py-6 text-sm", isDark ? "border-white/10 bg-black/20 text-white/55" : "text-muted-foreground")}>
|
||||
还没有配置任何生图分类,请先到“智能体工作流配置 - 生图分类”中新增顶部分类页和三级分类树。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{type === "image" ? (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -1,36 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useSession } from "next-auth/react";
|
||||
import {
|
||||
Loader2,
|
||||
Plus,
|
||||
Search,
|
||||
Sparkles,
|
||||
User,
|
||||
Grid3X3,
|
||||
Wand2,
|
||||
LayoutGrid,
|
||||
FolderOpen,
|
||||
Building2,
|
||||
Menu
|
||||
} from "lucide-react";
|
||||
import useSWR from "swr";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { agentService } from "@/services/agents";
|
||||
import type { AgentPreset } from "@/types/agent";
|
||||
import type { AgentPreset, AgentPresetLibraryCategoryNode, AgentPresetLibraryConfig } from "@/types/agent";
|
||||
import { normalizeAgentWorkflowShowcaseConfig } from "@/lib/agents/workflow-showcase-config";
|
||||
import {
|
||||
AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID,
|
||||
AGENT_PRESET_LIBRARY_MY_WORKS_ID,
|
||||
getEnabledAgentPresetLibraryNodes,
|
||||
getEnabledAgentPresetLibrarySections,
|
||||
normalizeAgentPresetLibraryConfig,
|
||||
resolveAgentPresetCategoryPath,
|
||||
} from "@/lib/agents/preset-library-config";
|
||||
import { AgentWorkflowShowcaseBanner } from "./AgentWorkflowShowcaseBanner";
|
||||
import { AgentPresetCardDark } from "./AgentPresetCardDark";
|
||||
import { AgentPresetDialog } from "./AgentPresetDialog";
|
||||
|
||||
// 导航标签类型
|
||||
type NavTab = "cases" | "my-works" | "personal-assets" | "company-assets";
|
||||
|
||||
interface AgentPresetPanelDarkV2Props {
|
||||
selectedPresetId?: string | null;
|
||||
onSelectPreset: (presetId: string | null) => void;
|
||||
@ -46,12 +49,19 @@ export function AgentPresetPanelDarkV2({
|
||||
embedded = false,
|
||||
embeddedEntityLabel = "智能体",
|
||||
}: AgentPresetPanelDarkV2Props) {
|
||||
const [activeTab, setActiveTab] = useState<NavTab>("cases");
|
||||
const { data: session } = useSession();
|
||||
const [activeTab, setActiveTab] = useState<string>(AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID);
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||
const [editingPreset, setEditingPreset] = useState<AgentPreset | null>(null);
|
||||
const { data: siteSettingsData } = useSWR<{ agentWorkflowShowcaseConfig?: unknown }>(
|
||||
!embedded ? "/api/site-settings" : null,
|
||||
const [selectedLevel1Id, setSelectedLevel1Id] = useState("");
|
||||
const [selectedLevel2Id, setSelectedLevel2Id] = useState("");
|
||||
const [selectedLevel3Id, setSelectedLevel3Id] = useState("");
|
||||
const { data: siteSettingsData } = useSWR<{
|
||||
agentWorkflowShowcaseConfig?: unknown;
|
||||
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
|
||||
}>(
|
||||
"/api/site-settings",
|
||||
(url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json())
|
||||
);
|
||||
|
||||
@ -68,28 +78,124 @@ export function AgentPresetPanelDarkV2({
|
||||
return result.data;
|
||||
}
|
||||
);
|
||||
const presetLibraryConfig = useMemo(
|
||||
() => normalizeAgentPresetLibraryConfig(siteSettingsData?.agentPresetLibraryConfig),
|
||||
[siteSettingsData?.agentPresetLibraryConfig]
|
||||
);
|
||||
const presetLibrarySections = useMemo(
|
||||
() => getEnabledAgentPresetLibrarySections(presetLibraryConfig),
|
||||
[presetLibraryConfig]
|
||||
);
|
||||
const navTabs = useMemo(() => {
|
||||
const dynamicTabs = presetLibrarySections.map((section) => ({
|
||||
id: section.id,
|
||||
label: section.label,
|
||||
icon: LayoutGrid,
|
||||
isMyWorks: false,
|
||||
}));
|
||||
const myWorksTab = {
|
||||
id: AGENT_PRESET_LIBRARY_MY_WORKS_ID,
|
||||
label: "我的作品",
|
||||
icon: FolderOpen,
|
||||
isMyWorks: true,
|
||||
};
|
||||
|
||||
if (dynamicTabs.length === 0) {
|
||||
return [myWorksTab];
|
||||
}
|
||||
|
||||
return [dynamicTabs[0], myWorksTab, ...dynamicTabs.slice(1)];
|
||||
}, [presetLibrarySections]);
|
||||
const activeSection = useMemo(
|
||||
() => presetLibrarySections.find((section) => section.id === activeTab) || null,
|
||||
[activeTab, presetLibrarySections]
|
||||
);
|
||||
const level1Options = useMemo(
|
||||
() => getEnabledAgentPresetLibraryNodes(activeSection?.categories || []),
|
||||
[activeSection]
|
||||
);
|
||||
const selectedLevel1 = useMemo(
|
||||
() => level1Options.find((node) => node.id === selectedLevel1Id) || null,
|
||||
[level1Options, selectedLevel1Id]
|
||||
);
|
||||
const level2Options = useMemo(
|
||||
() => getEnabledAgentPresetLibraryNodes(selectedLevel1?.children || []),
|
||||
[selectedLevel1]
|
||||
);
|
||||
const selectedLevel2 = useMemo(
|
||||
() => level2Options.find((node) => node.id === selectedLevel2Id) || null,
|
||||
[level2Options, selectedLevel2Id]
|
||||
);
|
||||
const level3Options = useMemo(
|
||||
() => getEnabledAgentPresetLibraryNodes(selectedLevel2?.children || []),
|
||||
[selectedLevel2]
|
||||
);
|
||||
const isMyWorksTab = activeTab === AGENT_PRESET_LIBRARY_MY_WORKS_ID;
|
||||
const canManagePublicPresets =
|
||||
session?.user?.type === "admin" || session?.user?.type === "agent_admin";
|
||||
|
||||
useEffect(() => {
|
||||
if (navTabs.some((tab) => tab.id === activeTab)) return;
|
||||
const nextTabId = navTabs[0]?.id || AGENT_PRESET_LIBRARY_MY_WORKS_ID;
|
||||
setActiveTab(nextTabId);
|
||||
}, [activeTab, navTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedLevel1Id("");
|
||||
setSelectedLevel2Id("");
|
||||
setSelectedLevel3Id("");
|
||||
}, [activeTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedLevel1Id || level1Options.some((node) => node.id === selectedLevel1Id)) return;
|
||||
setSelectedLevel1Id("");
|
||||
setSelectedLevel2Id("");
|
||||
setSelectedLevel3Id("");
|
||||
}, [level1Options, selectedLevel1Id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedLevel2Id || level2Options.some((node) => node.id === selectedLevel2Id)) return;
|
||||
setSelectedLevel2Id("");
|
||||
setSelectedLevel3Id("");
|
||||
}, [level2Options, selectedLevel2Id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedLevel3Id || level3Options.some((node) => node.id === selectedLevel3Id)) return;
|
||||
setSelectedLevel3Id("");
|
||||
}, [level3Options, selectedLevel3Id]);
|
||||
|
||||
// 根据当前标签过滤数据
|
||||
const filteredPresets = useMemo(() => {
|
||||
let presets: AgentPreset[] = [];
|
||||
|
||||
switch (activeTab) {
|
||||
case "cases":
|
||||
presets = data?.public || [];
|
||||
break;
|
||||
case "my-works":
|
||||
presets = data?.private || [];
|
||||
break;
|
||||
case "personal-assets":
|
||||
// 这里可以接入个人资产库数据
|
||||
presets = data?.private || [];
|
||||
break;
|
||||
case "company-assets":
|
||||
// 这里可以接入公司资产库数据
|
||||
presets = data?.public || [];
|
||||
break;
|
||||
let presets: AgentPreset[] = isMyWorksTab ? data?.private || [] : data?.public || [];
|
||||
|
||||
if (!isMyWorksTab) {
|
||||
const fallbackSectionId =
|
||||
presetLibrarySections.find((section) => section.id === AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID)?.id ||
|
||||
presetLibrarySections[0]?.id ||
|
||||
null;
|
||||
|
||||
presets = presets.filter((preset) => {
|
||||
const resolvedPath = resolveAgentPresetCategoryPath({
|
||||
categoryPath: preset.categoryPath || null,
|
||||
config: presetLibraryConfig,
|
||||
});
|
||||
const presetSectionId = resolvedPath.sectionId || fallbackSectionId;
|
||||
if (!presetSectionId || presetSectionId !== activeTab) {
|
||||
return false;
|
||||
}
|
||||
if (selectedLevel1Id && resolvedPath.level1Id !== selectedLevel1Id) {
|
||||
return false;
|
||||
}
|
||||
if (selectedLevel2Id && resolvedPath.level2Id !== selectedLevel2Id) {
|
||||
return false;
|
||||
}
|
||||
if (selectedLevel3Id && resolvedPath.level3Id !== selectedLevel3Id) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 搜索过滤
|
||||
if (keyword.trim()) {
|
||||
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||
@ -101,7 +207,17 @@ export function AgentPresetPanelDarkV2({
|
||||
}
|
||||
|
||||
return presets;
|
||||
}, [data, activeTab, keyword]);
|
||||
}, [
|
||||
activeTab,
|
||||
data,
|
||||
isMyWorksTab,
|
||||
keyword,
|
||||
presetLibraryConfig,
|
||||
presetLibrarySections,
|
||||
selectedLevel1Id,
|
||||
selectedLevel2Id,
|
||||
selectedLevel3Id,
|
||||
]);
|
||||
|
||||
const handleForkPreset = async (preset: AgentPreset) => {
|
||||
const result = await agentService.forkPreset(preset.id, `${preset.name}(副本)`);
|
||||
@ -130,22 +246,10 @@ export function AgentPresetPanelDarkV2({
|
||||
await mutate();
|
||||
};
|
||||
|
||||
// 导航标签配置
|
||||
const navTabs = [
|
||||
{ id: "cases" as NavTab, label: "案例广场", icon: LayoutGrid },
|
||||
{ id: "my-works" as NavTab, label: "我的作品", icon: FolderOpen },
|
||||
{ id: "personal-assets" as NavTab, label: "个人资产库", icon: User },
|
||||
{ id: "company-assets" as NavTab, label: "公司资产库", icon: Building2 },
|
||||
];
|
||||
|
||||
// 获取当前标签标题
|
||||
const getCurrentTitle = () => {
|
||||
switch (activeTab) {
|
||||
case "cases": return "公共智能体";
|
||||
case "my-works": return "我的智能体";
|
||||
case "personal-assets": return "个人资产库";
|
||||
case "company-assets": return "公司资产库";
|
||||
}
|
||||
if (isMyWorksTab) return "我的作品";
|
||||
return activeSection?.label || "生图模式";
|
||||
};
|
||||
|
||||
// 嵌入模式只渲染卡片网格
|
||||
@ -183,10 +287,10 @@ export function AgentPresetPanelDarkV2({
|
||||
|
||||
{/* 导航标签(简化版) */}
|
||||
<div className="mb-4 flex flex-wrap gap-1">
|
||||
{navTabs.slice(0, 2).map((tab) => (
|
||||
{navTabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as NavTab)}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={cn(
|
||||
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
||||
activeTab === tab.id
|
||||
@ -199,6 +303,40 @@ export function AgentPresetPanelDarkV2({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isMyWorksTab ? (
|
||||
<div className="mb-4 space-y-2 rounded-2xl border border-white/8 bg-white/[0.02] p-3">
|
||||
<CategoryFilterRow
|
||||
label="一级"
|
||||
options={level1Options}
|
||||
selectedId={selectedLevel1Id}
|
||||
onSelect={(value) => {
|
||||
setSelectedLevel1Id(value);
|
||||
setSelectedLevel2Id("");
|
||||
setSelectedLevel3Id("");
|
||||
}}
|
||||
/>
|
||||
{selectedLevel1 ? (
|
||||
<CategoryFilterRow
|
||||
label="二级"
|
||||
options={level2Options}
|
||||
selectedId={selectedLevel2Id}
|
||||
onSelect={(value) => {
|
||||
setSelectedLevel2Id(value);
|
||||
setSelectedLevel3Id("");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{selectedLevel2 ? (
|
||||
<CategoryFilterRow
|
||||
label="三级"
|
||||
options={level3Options}
|
||||
selectedId={selectedLevel3Id}
|
||||
onSelect={setSelectedLevel3Id}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 卡片网格 */}
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2.5">
|
||||
{isLoading ? (
|
||||
@ -213,9 +351,17 @@ export function AgentPresetPanelDarkV2({
|
||||
preset={preset}
|
||||
selected={selectedPresetId === preset.id}
|
||||
onSelect={onSelectPreset}
|
||||
onFork={handleForkPreset}
|
||||
onEdit={setEditingPreset}
|
||||
onDelete={handleDeletePreset}
|
||||
onFork={preset.visibility === "public" && !canManagePublicPresets ? handleForkPreset : undefined}
|
||||
onEdit={
|
||||
preset.visibility === "private" || canManagePublicPresets
|
||||
? setEditingPreset
|
||||
: undefined
|
||||
}
|
||||
onDelete={
|
||||
preset.visibility === "private" || canManagePublicPresets
|
||||
? handleDeletePreset
|
||||
: undefined
|
||||
}
|
||||
index={index}
|
||||
compact
|
||||
/>
|
||||
@ -352,7 +498,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(
|
||||
@ -363,11 +509,50 @@ export function AgentPresetPanelDarkV2({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-white/50">
|
||||
<Grid3X3 className="h-4 w-4" />
|
||||
<span>共 {filteredPresets.length} 个</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
<div className="flex items-center gap-2 text-sm text-white/50">
|
||||
<Grid3X3 className="h-4 w-4" />
|
||||
<span>共 {filteredPresets.length} 个</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{!isMyWorksTab ? (
|
||||
<motion.div
|
||||
className="mb-6 space-y-2 rounded-2xl border border-white/8 bg-white/[0.02] p-4"
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, delay: 0.35 }}
|
||||
>
|
||||
<CategoryFilterRow
|
||||
label="一级分类"
|
||||
options={level1Options}
|
||||
selectedId={selectedLevel1Id}
|
||||
onSelect={(value) => {
|
||||
setSelectedLevel1Id(value);
|
||||
setSelectedLevel2Id("");
|
||||
setSelectedLevel3Id("");
|
||||
}}
|
||||
/>
|
||||
{selectedLevel1 ? (
|
||||
<CategoryFilterRow
|
||||
label="二级分类"
|
||||
options={level2Options}
|
||||
selectedId={selectedLevel2Id}
|
||||
onSelect={(value) => {
|
||||
setSelectedLevel2Id(value);
|
||||
setSelectedLevel3Id("");
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{selectedLevel2 ? (
|
||||
<CategoryFilterRow
|
||||
label="三级分类"
|
||||
options={level3Options}
|
||||
selectedId={selectedLevel3Id}
|
||||
onSelect={setSelectedLevel3Id}
|
||||
/>
|
||||
) : null}
|
||||
</motion.div>
|
||||
) : null}
|
||||
|
||||
{/* 区域标题 */}
|
||||
<motion.div
|
||||
@ -396,9 +581,17 @@ export function AgentPresetPanelDarkV2({
|
||||
preset={preset}
|
||||
selected={selectedPresetId === preset.id}
|
||||
onSelect={onSelectPreset}
|
||||
onFork={handleForkPreset}
|
||||
onEdit={setEditingPreset}
|
||||
onDelete={handleDeletePreset}
|
||||
onFork={preset.visibility === "public" && !canManagePublicPresets ? handleForkPreset : undefined}
|
||||
onEdit={
|
||||
preset.visibility === "private" || canManagePublicPresets
|
||||
? setEditingPreset
|
||||
: undefined
|
||||
}
|
||||
onDelete={
|
||||
preset.visibility === "private" || canManagePublicPresets
|
||||
? handleDeletePreset
|
||||
: undefined
|
||||
}
|
||||
index={index}
|
||||
/>
|
||||
))
|
||||
@ -432,7 +625,7 @@ export function AgentPresetPanelDarkV2({
|
||||
setShowCreateDialog(true);
|
||||
}}
|
||||
initialPreset={editingPreset}
|
||||
entityLabel="智能体"
|
||||
entityLabel="生图模式"
|
||||
theme="dark"
|
||||
onSaved={async (preset) => {
|
||||
await mutate();
|
||||
@ -459,3 +652,62 @@ function EmptyState({ message }: { message: string }) {
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryFilterRow({
|
||||
label,
|
||||
options,
|
||||
selectedId,
|
||||
onSelect,
|
||||
dark = true,
|
||||
}: {
|
||||
label: string;
|
||||
options: AgentPresetLibraryCategoryNode[];
|
||||
selectedId?: string;
|
||||
onSelect: (value: string) => void;
|
||||
dark?: boolean;
|
||||
}) {
|
||||
if (!options.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={cn("text-xs", dark ? "text-white/38" : "text-muted-foreground")}>
|
||||
{label}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect("")}
|
||||
className={cn(
|
||||
"rounded-full border px-3 py-1 text-xs transition-colors",
|
||||
!selectedId
|
||||
? dark
|
||||
? "border-white/30 bg-white text-black"
|
||||
: "border-primary bg-primary/10 text-primary"
|
||||
: dark
|
||||
? "border-white/10 bg-white/[0.03] text-white/62 hover:border-white/20 hover:text-white"
|
||||
: "border-border bg-background text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
全部
|
||||
</button>
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(option.id)}
|
||||
className={cn(
|
||||
"rounded-full border px-3 py-1 text-xs transition-colors",
|
||||
selectedId === option.id
|
||||
? dark
|
||||
? "border-emerald-300/35 bg-emerald-300/90 text-black"
|
||||
: "border-primary bg-primary/10 text-primary"
|
||||
: dark
|
||||
? "border-white/10 bg-white/[0.03] text-white/62 hover:border-white/20 hover:text-white"
|
||||
: "border-border bg-background text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{option.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { zhCN } from "date-fns/locale";
|
||||
import { FolderOpen, Loader2, Plus, Trash2 } from "lucide-react";
|
||||
import { FolderOpen, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { agentProjectService } from "@/services/agent-projects";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@ -14,31 +14,34 @@ import {
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||
import type { AgentGenerationProject } from "@/types/agent";
|
||||
|
||||
interface AgentProjectListSheetProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
currentProjectId?: string | null;
|
||||
currentProjectTitle?: string | null;
|
||||
onCreateProject: () => Promise<void> | void;
|
||||
onSelectProject: (projectId: string) => Promise<void> | void;
|
||||
onDeleteProject: (projectId: string) => Promise<void> | void;
|
||||
onRenameProject: (projectId: string, currentTitle?: string | null) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export function AgentProjectListSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
currentProjectId,
|
||||
currentProjectTitle,
|
||||
onCreateProject,
|
||||
onSelectProject,
|
||||
onDeleteProject,
|
||||
onRenameProject,
|
||||
}: AgentProjectListSheetProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [projects, setProjects] = useState<AgentGenerationProject[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||
const [pendingRenameId, setPendingRenameId] = useState<string | null>(null);
|
||||
|
||||
const loadProjects = async () => {
|
||||
setIsLoading(true);
|
||||
@ -61,17 +64,7 @@ export function AgentProjectListSheet({
|
||||
}, [open, currentProjectId]);
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="h-9 max-w-[260px] gap-2 border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">{currentProjectTitle || "生图项目列表"}</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-[380px] border-white/10 bg-[#0b0d0f] p-0 text-white sm:max-w-[380px]"
|
||||
@ -89,7 +82,7 @@ export function AgentProjectListSheet({
|
||||
onClick={async () => {
|
||||
await onCreateProject();
|
||||
await loadProjects();
|
||||
setOpen(false);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
className="h-10 w-full gap-2 bg-emerald-500 text-white hover:bg-emerald-400"
|
||||
>
|
||||
@ -111,7 +104,7 @@ export function AgentProjectListSheet({
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await onSelectProject(project.id);
|
||||
setOpen(false);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
className={cn(
|
||||
"group flex w-full items-center gap-3 rounded-2xl border p-3 text-left transition-colors",
|
||||
@ -155,12 +148,36 @@ export function AgentProjectListSheet({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-white/45 opacity-0 transition-opacity hover:text-sky-300 group-hover:opacity-100"
|
||||
disabled={pendingRenameId === project.id}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
setPendingRenameId(project.id);
|
||||
try {
|
||||
await onRenameProject(project.id, project.title || "");
|
||||
await loadProjects();
|
||||
} finally {
|
||||
setPendingRenameId(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pendingRenameId === project.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Pencil className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-white/45 opacity-0 transition-opacity hover:text-rose-300 group-hover:opacity-100"
|
||||
disabled={pendingDeleteId === project.id}
|
||||
disabled={pendingDeleteId === project.id || pendingRenameId === project.id}
|
||||
onClick={async (event) => {
|
||||
event.stopPropagation();
|
||||
if (!window.confirm("确定删除这个生图项目吗?")) return;
|
||||
|
||||
@ -8,7 +8,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
import {
|
||||
buildAgentWorkflowComposerContext,
|
||||
@ -84,9 +88,13 @@ export function AgentWorkflowPanel({
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
);
|
||||
const brandStoreEntries = useMemo(
|
||||
() => getAgentWorkflowBrandStoreEntries(config),
|
||||
[config]
|
||||
);
|
||||
const storeOptions = useMemo(
|
||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
||||
[config?.inputOptions?.stores]
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
const resolvedConfig = useMemo(() => {
|
||||
const source = config || defaultAgentWorkflowUiConfig;
|
||||
@ -197,14 +205,24 @@ export function AgentWorkflowPanel({
|
||||
placeholder="品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选,请先到系统设置配置"
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
brand: value,
|
||||
store:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText="暂无店铺可选,请先到系统设置配置"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -9,7 +9,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
import {
|
||||
buildAgentWorkflowComposerContext,
|
||||
@ -166,10 +170,13 @@ export function AgentWorkflowPanelDark({
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
);
|
||||
|
||||
const brandStoreEntries = useMemo(
|
||||
() => getAgentWorkflowBrandStoreEntries(config),
|
||||
[config]
|
||||
);
|
||||
const storeOptions = useMemo(
|
||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
||||
[config?.inputOptions?.stores]
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
|
||||
const resolvedConfig = useMemo(() => {
|
||||
@ -305,14 +312,24 @@ export function AgentWorkflowPanelDark({
|
||||
placeholder="选择品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
brand: value,
|
||||
store:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="选择店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText="暂无店铺可选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
@ -12,7 +12,12 @@ import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenAgentWorkflowSteps,
|
||||
flattenVisibleAgentWorkflowSteps,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowSelectedOptionPath,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
getEnabledAgentWorkflowOptionTree,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
serializeAgentWorkflowSelectionPath,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
||||
@ -59,16 +64,18 @@ function buildSelectionSummary(params: {
|
||||
}) {
|
||||
const result: AgentWorkflowComposerSelection[] = [];
|
||||
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
|
||||
const selectedOptionKey = params.selections[step.key];
|
||||
if (!selectedOptionKey) continue;
|
||||
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
||||
if (!option) continue;
|
||||
result.push({
|
||||
stepKey: step.key,
|
||||
stepTitle: step.title,
|
||||
optionKey: option.key,
|
||||
optionName: option.buttonName,
|
||||
promptFragment: option.promptFragment || "",
|
||||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||
step,
|
||||
selectionValue: params.selections[step.key],
|
||||
});
|
||||
selectedOptions.forEach((option) => {
|
||||
result.push({
|
||||
stepKey: step.key,
|
||||
stepTitle: step.title,
|
||||
optionKey: option.key,
|
||||
optionName: option.buttonName,
|
||||
promptFragment: option.promptFragment || "",
|
||||
});
|
||||
});
|
||||
}
|
||||
return result;
|
||||
@ -181,10 +188,13 @@ export function AgentWorkflowPanelDarkV2({
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
);
|
||||
|
||||
const brandStoreEntries = useMemo(
|
||||
() => getAgentWorkflowBrandStoreEntries(config),
|
||||
[config]
|
||||
);
|
||||
const storeOptions = useMemo(
|
||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
||||
[config?.inputOptions?.stores]
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
|
||||
const resolvedConfig = useMemo(() => {
|
||||
@ -222,6 +232,68 @@ export function AgentWorkflowPanelDarkV2({
|
||||
[resolvedConfig, selections]
|
||||
);
|
||||
|
||||
const renderOptionLevels = (
|
||||
step: AgentWorkflowUiConfig["steps"][number],
|
||||
options: AgentWorkflowUiConfig["steps"][number]["options"],
|
||||
depth = 1,
|
||||
parentPath: string[] = []
|
||||
): ReactNode => {
|
||||
const selectedPathKeys = getAgentWorkflowSelectedOptionPath({
|
||||
step,
|
||||
selectionValue: selections[step.key],
|
||||
}).map((option) => option.key);
|
||||
const currentSelectedKey = selectedPathKeys[depth - 1] || "";
|
||||
const currentSelectedOption =
|
||||
options.find((option) => option.key === currentSelectedKey) || null;
|
||||
const visibleChildren = getEnabledAgentWorkflowOptionTree(
|
||||
currentSelectedOption?.children || []
|
||||
);
|
||||
const levelLabel = depth === 1 ? "主卡片" : depth === 2 ? "二级卡片" : "三级卡片";
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded-full border border-white/12 px-2.5 py-1 text-[11px] font-medium text-white/45">
|
||||
{levelLabel}
|
||||
</span>
|
||||
{depth > 1 ? (
|
||||
<span className="text-xs text-white/35">
|
||||
该层卡片从上一级已选卡片中继续展开
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{options.map((option, optionIndex) => (
|
||||
<StepOptionCard
|
||||
key={`${option.key}-${depth}`}
|
||||
option={option}
|
||||
step={step}
|
||||
selected={currentSelectedKey === option.key}
|
||||
onClick={() => {
|
||||
const prefix = parentPath.slice(0, depth - 1);
|
||||
const nextKeys =
|
||||
currentSelectedKey === option.key ? prefix : [...prefix, option.key];
|
||||
onSelectionsChange({
|
||||
...selections,
|
||||
[step.key]: nextKeys.length > 0 ? serializeAgentWorkflowSelectionPath(nextKeys) : undefined,
|
||||
});
|
||||
}}
|
||||
index={optionIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{currentSelectedOption && visibleChildren.length > 0
|
||||
? renderOptionLevels(step, visibleChildren, depth + 1, [
|
||||
...parentPath.slice(0, depth - 1),
|
||||
currentSelectedOption.key,
|
||||
])
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCopyPrompt = async () => {
|
||||
if (!composerContext?.promptText) {
|
||||
toast.error("当前还没有可复制的 Prompt");
|
||||
@ -291,15 +363,29 @@ export function AgentWorkflowPanelDarkV2({
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
theme="dark"
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
brand: value,
|
||||
store:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="选择店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText="暂无店铺可选"
|
||||
emptyText={
|
||||
inputs.brand
|
||||
? "当前品牌下暂无店铺可选"
|
||||
: "请先选择品牌"
|
||||
}
|
||||
theme="dark"
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
/>
|
||||
</div>
|
||||
@ -331,23 +417,7 @@ export function AgentWorkflowPanelDarkV2({
|
||||
{step.description && (
|
||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
||||
{step.options.map((option, optionIndex) => (
|
||||
<StepOptionCard
|
||||
key={option.key}
|
||||
option={option}
|
||||
step={step}
|
||||
selected={selections[step.key] === option.key}
|
||||
onClick={() =>
|
||||
onSelectionsChange({
|
||||
...selections,
|
||||
[step.key]: selections[step.key] === option.key ? undefined : option.key,
|
||||
})
|
||||
}
|
||||
index={optionIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
|
||||
</motion.section>
|
||||
))}
|
||||
|
||||
|
||||
@ -9,7 +9,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
getAgentWorkflowBrandStoreEntries,
|
||||
getAgentWorkflowStoreOptionsByBrand,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||
import {
|
||||
buildAgentWorkflowComposerContext,
|
||||
@ -178,10 +182,13 @@ export function AgentWorkflowPanelNew({
|
||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||
[config?.inputOptions?.brands]
|
||||
);
|
||||
|
||||
const brandStoreEntries = useMemo(
|
||||
() => getAgentWorkflowBrandStoreEntries(config),
|
||||
[config]
|
||||
);
|
||||
const storeOptions = useMemo(
|
||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
||||
[config?.inputOptions?.stores]
|
||||
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||
[config, inputs.brand]
|
||||
);
|
||||
|
||||
const resolvedConfig = useMemo(() => {
|
||||
@ -340,14 +347,24 @@ export function AgentWorkflowPanelNew({
|
||||
placeholder="选择品牌"
|
||||
searchPlaceholder="输入品牌名筛选"
|
||||
emptyText="暂无品牌可选"
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
||||
onValueChange={(value) =>
|
||||
onInputsChange({
|
||||
...inputs,
|
||||
brand: value,
|
||||
store:
|
||||
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||
? inputs.store
|
||||
: "",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<SearchableSelect
|
||||
value={inputs.store}
|
||||
options={storeOptions}
|
||||
placeholder="选择店铺"
|
||||
searchPlaceholder="输入店铺名筛选"
|
||||
emptyText="暂无店铺可选"
|
||||
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1411,7 +1411,7 @@ export function SceneFusionView({
|
||||
{selectedModeOption?.label || "请选择处理模式"}
|
||||
</span>
|
||||
</SelectTrigger>
|
||||
<SelectContent className="z-[30000] bg-background">
|
||||
<SelectContent className="z-[30000] !bg-white dark:!bg-zinc-950">
|
||||
{modeOptions.map((option) => (
|
||||
<SelectItem
|
||||
className="items-start py-2"
|
||||
|
||||
@ -16,6 +16,7 @@ interface SearchableSelectProps {
|
||||
emptyText?: string;
|
||||
onValueChange: (value: string) => void;
|
||||
theme?: "light" | "dark";
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function SearchableSelect({
|
||||
@ -26,6 +27,7 @@ export function SearchableSelect({
|
||||
emptyText = "暂无可选项",
|
||||
onValueChange,
|
||||
theme = "light",
|
||||
disabled = false,
|
||||
}: SearchableSelectProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
@ -51,9 +53,11 @@ export function SearchableSelect({
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"h-10 w-full justify-between px-3 text-left font-normal",
|
||||
disabled && "cursor-not-allowed opacity-60",
|
||||
isDark &&
|
||||
"h-12 rounded-2xl border-white/14 bg-white/[0.05] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:bg-white/[0.08] hover:text-white"
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
@ -87,6 +91,7 @@ export function SearchableSelect({
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder={searchPlaceholder}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
isDark &&
|
||||
"border-white/12 bg-white/[0.04] text-white placeholder:text-white/28 focus-visible:ring-emerald-400/30 focus-visible:ring-offset-0"
|
||||
|
||||
@ -75,24 +75,28 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-[10000] isolate max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md ring-1 ring-border/50 !opacity-100 backdrop-blur-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
|
||||
"relative z-[10000] isolate max-h-[var(--radix-select-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md ring-1 ring-border/50 !opacity-100 backdrop-blur-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin] !bg-white dark:!bg-zinc-950",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
style={style}
|
||||
style={{
|
||||
backgroundColor: "var(--popover)",
|
||||
color: "var(--popover-foreground)",
|
||||
...style,
|
||||
}}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"rounded-[inherit] bg-inherit p-1",
|
||||
"rounded-[inherit] bg-inherit p-1 !bg-white dark:!bg-zinc-950",
|
||||
position === "popper" &&
|
||||
"w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: "inherit",
|
||||
backgroundColor: "var(--popover)",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
335
lib/agents/preset-library-config.ts
Normal file
335
lib/agents/preset-library-config.ts
Normal file
@ -0,0 +1,335 @@
|
||||
import type {
|
||||
AgentCategoryPath,
|
||||
AgentPresetLibraryCategoryNode,
|
||||
AgentPresetLibraryConfig,
|
||||
AgentPresetLibrarySection,
|
||||
} from "@/types/agent";
|
||||
|
||||
export const AGENT_PRESET_LIBRARY_CONFIG_KEY = "agent_preset_library_config";
|
||||
export const AGENT_PRESET_LIBRARY_MAX_DEPTH = 3;
|
||||
export const AGENT_PRESET_LIBRARY_MY_WORKS_ID = "my-works";
|
||||
export const AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID = "cases";
|
||||
|
||||
function normalizeText(value: unknown, maxLength: number) {
|
||||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
|
||||
function normalizeBoolean(value: unknown, fallback: boolean) {
|
||||
return typeof value === "boolean" ? value : fallback;
|
||||
}
|
||||
|
||||
function normalizeNumber(value: unknown, fallback: number, max = 999) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return fallback;
|
||||
return Math.max(0, Math.min(max, Math.round(parsed)));
|
||||
}
|
||||
|
||||
function normalizeId(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 createDefaultSection(
|
||||
id: string,
|
||||
label: string,
|
||||
sortOrder: number
|
||||
): AgentPresetLibrarySection {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
description: "",
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
categories: [],
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultAgentPresetLibraryConfig: AgentPresetLibraryConfig = {
|
||||
sections: [
|
||||
createDefaultSection("cases", "案例广场", 0),
|
||||
createDefaultSection("personal-assets", "个人资产库", 1),
|
||||
createDefaultSection("company-assets", "公司资产库", 2),
|
||||
],
|
||||
};
|
||||
|
||||
function normalizeNodeArray(
|
||||
input: unknown,
|
||||
depth: number,
|
||||
usedIds: Set<string>
|
||||
): AgentPresetLibraryCategoryNode[] {
|
||||
if (!Array.isArray(input) || depth > AGENT_PRESET_LIBRARY_MAX_DEPTH) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return input
|
||||
.map((item, index) => {
|
||||
const raw =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: {};
|
||||
const name = normalizeText(raw.name, 64) || `分类${index + 1}`;
|
||||
const baseId = normalizeId(raw.id, `category_${depth}_${index + 1}`);
|
||||
let id = baseId;
|
||||
let duplicateIndex = 2;
|
||||
while (usedIds.has(id)) {
|
||||
id = `${baseId}_${duplicateIndex}`;
|
||||
duplicateIndex += 1;
|
||||
}
|
||||
usedIds.add(id);
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
enabled: normalizeBoolean(raw.enabled, true),
|
||||
sortOrder: normalizeNumber(raw.sortOrder, index),
|
||||
children:
|
||||
depth < AGENT_PRESET_LIBRARY_MAX_DEPTH
|
||||
? normalizeNodeArray(raw.children, depth + 1, usedIds)
|
||||
: [],
|
||||
};
|
||||
})
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
}
|
||||
|
||||
export function normalizeAgentPresetLibraryConfig(
|
||||
value: unknown
|
||||
): AgentPresetLibraryConfig {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
const hasRawSections = Array.isArray(raw.sections);
|
||||
const rawSections: unknown[] = hasRawSections
|
||||
? (raw.sections as unknown[])
|
||||
: defaultAgentPresetLibraryConfig.sections;
|
||||
const usedSectionIds = new Set<string>();
|
||||
|
||||
const sections = rawSections
|
||||
.map((item: unknown, index: number) => {
|
||||
const record =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: {};
|
||||
const fallbackSection =
|
||||
defaultAgentPresetLibraryConfig.sections[index] ||
|
||||
createDefaultSection(`section_${index + 1}`, `分类页${index + 1}`, index);
|
||||
const baseId = normalizeId(record.id, fallbackSection.id);
|
||||
let id = baseId;
|
||||
let duplicateIndex = 2;
|
||||
while (usedSectionIds.has(id)) {
|
||||
id = `${baseId}_${duplicateIndex}`;
|
||||
duplicateIndex += 1;
|
||||
}
|
||||
usedSectionIds.add(id);
|
||||
|
||||
const usedNodeIds = new Set<string>();
|
||||
|
||||
return {
|
||||
id,
|
||||
label: normalizeText(record.label, 64) || fallbackSection.label,
|
||||
description: normalizeText(record.description, 200) || "",
|
||||
enabled: normalizeBoolean(record.enabled, true),
|
||||
sortOrder: normalizeNumber(record.sortOrder, fallbackSection.sortOrder ?? index),
|
||||
categories: normalizeNodeArray(record.categories, 1, usedNodeIds),
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
(left: AgentPresetLibrarySection, right: AgentPresetLibrarySection) =>
|
||||
left.sortOrder - right.sortOrder
|
||||
);
|
||||
|
||||
return {
|
||||
sections: hasRawSections
|
||||
? sections
|
||||
: sections.length > 0
|
||||
? sections
|
||||
: defaultAgentPresetLibraryConfig.sections,
|
||||
};
|
||||
}
|
||||
|
||||
export function getEnabledAgentPresetLibrarySections(
|
||||
config: AgentPresetLibraryConfig
|
||||
) {
|
||||
return [...config.sections]
|
||||
.filter((section) => section.enabled !== false)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
}
|
||||
|
||||
export function getEnabledAgentPresetLibraryNodes(
|
||||
nodes: AgentPresetLibraryCategoryNode[]
|
||||
): AgentPresetLibraryCategoryNode[] {
|
||||
return [...nodes]
|
||||
.filter((node) => node.enabled !== false)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.map((node) => ({
|
||||
...node,
|
||||
children: getEnabledAgentPresetLibraryNodes(node.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
function findNodeByMatcher(
|
||||
nodes: AgentPresetLibraryCategoryNode[],
|
||||
matcher: (node: AgentPresetLibraryCategoryNode) => boolean
|
||||
): AgentPresetLibraryCategoryNode | null {
|
||||
for (const node of nodes) {
|
||||
if (matcher(node)) {
|
||||
return node;
|
||||
}
|
||||
const foundChild = findNodeByMatcher(node.children || [], matcher);
|
||||
if (foundChild) {
|
||||
return foundChild;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function normalizeAgentCategoryPath(value: unknown): AgentCategoryPath {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: {};
|
||||
|
||||
return {
|
||||
sectionId: normalizeText(raw.sectionId, 64) || null,
|
||||
sectionName: normalizeText(raw.sectionName, 64) || null,
|
||||
level1Id: normalizeText(raw.level1Id, 64) || null,
|
||||
level1Name: normalizeText(raw.level1Name, 64) || normalizeText(raw.productCategory, 64) || null,
|
||||
level2Id: normalizeText(raw.level2Id, 64) || null,
|
||||
level2Name: normalizeText(raw.level2Name, 64) || normalizeText(raw.audienceTag, 64) || null,
|
||||
level3Id: normalizeText(raw.level3Id, 64) || null,
|
||||
level3Name: normalizeText(raw.level3Name, 64) || normalizeText(raw.usageScenario, 64) || null,
|
||||
productCategory: normalizeText(raw.productCategory, 64) || null,
|
||||
audienceTag: normalizeText(raw.audienceTag, 64) || null,
|
||||
usageScenario: normalizeText(raw.usageScenario, 64) || null,
|
||||
brand: normalizeText(raw.brand, 64) || null,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResolvedAgentPresetCategoryPath {
|
||||
section: AgentPresetLibrarySection | null;
|
||||
level1: AgentPresetLibraryCategoryNode | null;
|
||||
level2: AgentPresetLibraryCategoryNode | null;
|
||||
level3: AgentPresetLibraryCategoryNode | null;
|
||||
sectionId: string | null;
|
||||
sectionName: string | null;
|
||||
level1Id: string | null;
|
||||
level1Name: string | null;
|
||||
level2Id: string | null;
|
||||
level2Name: string | null;
|
||||
level3Id: string | null;
|
||||
level3Name: string | null;
|
||||
}
|
||||
|
||||
export function resolveAgentPresetCategoryPath(params: {
|
||||
categoryPath?: AgentCategoryPath | null;
|
||||
config?: AgentPresetLibraryConfig | null;
|
||||
}) : ResolvedAgentPresetCategoryPath {
|
||||
const normalizedPath = normalizeAgentCategoryPath(params.categoryPath);
|
||||
const config = normalizeAgentPresetLibraryConfig(
|
||||
params.config || defaultAgentPresetLibraryConfig
|
||||
);
|
||||
const sections = getEnabledAgentPresetLibrarySections(config);
|
||||
const fallbackSection =
|
||||
sections.find((section) => section.id === AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID) ||
|
||||
sections[0] ||
|
||||
null;
|
||||
|
||||
const section =
|
||||
sections.find((item) => item.id === normalizedPath.sectionId) ||
|
||||
sections.find((item) => item.label === normalizedPath.sectionName) ||
|
||||
((!normalizedPath.sectionId &&
|
||||
!normalizedPath.sectionName &&
|
||||
(normalizedPath.level1Name || normalizedPath.level2Name || normalizedPath.level3Name)) ||
|
||||
normalizedPath.productCategory ||
|
||||
normalizedPath.audienceTag ||
|
||||
normalizedPath.usageScenario
|
||||
? fallbackSection
|
||||
: null);
|
||||
|
||||
const level1 =
|
||||
section
|
||||
? findNodeByMatcher(getEnabledAgentPresetLibraryNodes(section.categories), (node) => {
|
||||
return (
|
||||
node.id === normalizedPath.level1Id ||
|
||||
node.name === normalizedPath.level1Name ||
|
||||
node.name === normalizedPath.productCategory
|
||||
);
|
||||
})
|
||||
: null;
|
||||
const level2 =
|
||||
level1
|
||||
? findNodeByMatcher(getEnabledAgentPresetLibraryNodes(level1.children || []), (node) => {
|
||||
return (
|
||||
node.id === normalizedPath.level2Id ||
|
||||
node.name === normalizedPath.level2Name ||
|
||||
node.name === normalizedPath.audienceTag
|
||||
);
|
||||
})
|
||||
: null;
|
||||
const level3 =
|
||||
level2
|
||||
? findNodeByMatcher(getEnabledAgentPresetLibraryNodes(level2.children || []), (node) => {
|
||||
return (
|
||||
node.id === normalizedPath.level3Id ||
|
||||
node.name === normalizedPath.level3Name ||
|
||||
node.name === normalizedPath.usageScenario
|
||||
);
|
||||
})
|
||||
: null;
|
||||
|
||||
return {
|
||||
section,
|
||||
level1,
|
||||
level2,
|
||||
level3,
|
||||
sectionId: section?.id || normalizedPath.sectionId || null,
|
||||
sectionName: section?.label || normalizedPath.sectionName || null,
|
||||
level1Id: level1?.id || normalizedPath.level1Id || null,
|
||||
level1Name:
|
||||
level1?.name || normalizedPath.level1Name || normalizedPath.productCategory || null,
|
||||
level2Id: level2?.id || normalizedPath.level2Id || null,
|
||||
level2Name:
|
||||
level2?.name || normalizedPath.level2Name || normalizedPath.audienceTag || null,
|
||||
level3Id: level3?.id || normalizedPath.level3Id || null,
|
||||
level3Name:
|
||||
level3?.name || normalizedPath.level3Name || normalizedPath.usageScenario || null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAgentPresetCategoryPath(params: {
|
||||
section?: AgentPresetLibrarySection | null;
|
||||
level1?: AgentPresetLibraryCategoryNode | null;
|
||||
level2?: AgentPresetLibraryCategoryNode | null;
|
||||
level3?: AgentPresetLibraryCategoryNode | null;
|
||||
}): AgentCategoryPath | null {
|
||||
const section = params.section || null;
|
||||
const level1 = params.level1 || null;
|
||||
const level2 = params.level2 || null;
|
||||
const level3 = params.level3 || null;
|
||||
|
||||
if (!section && !level1 && !level2 && !level3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
sectionId: section?.id || null,
|
||||
sectionName: section?.label || null,
|
||||
level1Id: level1?.id || null,
|
||||
level1Name: level1?.name || null,
|
||||
level2Id: level2?.id || null,
|
||||
level2Name: level2?.name || null,
|
||||
level3Id: level3?.id || null,
|
||||
level3Name: level3?.name || null,
|
||||
productCategory: level1?.name || null,
|
||||
audienceTag: level2?.name || null,
|
||||
usageScenario: level3?.name || null,
|
||||
brand: null,
|
||||
};
|
||||
}
|
||||
@ -1,7 +1,9 @@
|
||||
import {
|
||||
defaultAgentWorkflowUiConfig,
|
||||
flattenVisibleAgentWorkflowSteps,
|
||||
getAgentWorkflowSelectedOptionPath,
|
||||
getEnabledAgentWorkflowStepTree,
|
||||
isAgentWorkflowStepSelectionComplete,
|
||||
} from "@/lib/agents/workflow-ui-config";
|
||||
import type {
|
||||
AgentWorkflowComposerContext,
|
||||
@ -35,21 +37,20 @@ export function buildAgentWorkflowComposerContext(params: {
|
||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||
const selections = steps
|
||||
.map((step) => {
|
||||
const selectedKey = params.selections[step.key];
|
||||
if (!selectedKey) return null;
|
||||
const option = (step.options || []).find(
|
||||
(item) => item.key === selectedKey && item.enabled !== false
|
||||
);
|
||||
if (!option) return null;
|
||||
return {
|
||||
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||
step,
|
||||
selectionValue: params.selections[step.key],
|
||||
});
|
||||
if (selectedOptions.length === 0) return [];
|
||||
return selectedOptions.map((option) => ({
|
||||
stepKey: step.key,
|
||||
stepTitle: step.title,
|
||||
optionKey: option.key,
|
||||
optionName: option.buttonName,
|
||||
promptFragment: option.promptFragment || option.buttonName,
|
||||
};
|
||||
}));
|
||||
})
|
||||
.filter(Boolean) as AgentWorkflowComposerContext["selections"];
|
||||
.flat() as AgentWorkflowComposerContext["selections"];
|
||||
const referenceImages = getSelectedWorkflowReferenceImages({
|
||||
inputs: params.inputs,
|
||||
materials: params.materials,
|
||||
@ -231,8 +232,10 @@ export function getAgentWorkflowProgress(params: {
|
||||
.map(({ step }) => step)
|
||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||
const completedSteps = enabledSteps.filter((step) => {
|
||||
const value = params.selections[step.key];
|
||||
return Boolean(String(value || "").trim());
|
||||
return isAgentWorkflowStepSelectionComplete({
|
||||
step,
|
||||
selectionValue: params.selections[step.key],
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
AgentWorkflowBrandStoreEntry,
|
||||
AgentWorkflowOptionUiConfig,
|
||||
AgentWorkflowStepUiConfig,
|
||||
AgentWorkflowUiConfig,
|
||||
@ -6,6 +7,7 @@ import type {
|
||||
|
||||
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 3;
|
||||
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 3;
|
||||
|
||||
export interface FlattenedAgentWorkflowStep {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
@ -21,6 +23,7 @@ function cloneOption(
|
||||
description: option.description || "",
|
||||
previewImageUrl: option.previewImageUrl || "",
|
||||
promptFragment: option.promptFragment || "",
|
||||
children: (option.children || []).map(cloneOption),
|
||||
};
|
||||
}
|
||||
|
||||
@ -64,6 +67,7 @@ function createOption(
|
||||
enabled: true,
|
||||
sortOrder,
|
||||
promptFragment,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -129,6 +133,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
|
||||
inputOptions: {
|
||||
brands: [],
|
||||
stores: [],
|
||||
brandStoreEntries: [],
|
||||
},
|
||||
steps: defaultSteps.map((step, index) =>
|
||||
cloneStep({
|
||||
@ -216,7 +221,9 @@ export function countAgentWorkflowSteps(steps: AgentWorkflowStepUiConfig[]): num
|
||||
|
||||
export function countAgentWorkflowOptions(steps: AgentWorkflowStepUiConfig[]): number {
|
||||
return flattenAgentWorkflowSteps(steps).reduce(
|
||||
(total, item) => total + (item.step.options?.length || 0),
|
||||
(total, item) =>
|
||||
total +
|
||||
flattenAgentWorkflowOptionTree(item.step.options || []).length,
|
||||
0
|
||||
);
|
||||
}
|
||||
@ -227,10 +234,7 @@ export function normalizeAgentWorkflowStepOrdering(
|
||||
return steps.map((step, stepIndex) => ({
|
||||
...step,
|
||||
sortOrder: stepIndex,
|
||||
options: (step.options || []).map((option, optionIndex) => ({
|
||||
...option,
|
||||
sortOrder: optionIndex,
|
||||
})),
|
||||
options: normalizeAgentWorkflowOptionOrdering(step.options || []),
|
||||
children: normalizeAgentWorkflowStepOrdering(step.children || []),
|
||||
}));
|
||||
}
|
||||
@ -243,14 +247,81 @@ export function getEnabledAgentWorkflowStepTree(
|
||||
.filter((step) => step.enabled !== false)
|
||||
.map((step) => ({
|
||||
...step,
|
||||
options: [...(step.options || [])]
|
||||
.filter((option) => option.enabled !== false)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder),
|
||||
options: getEnabledAgentWorkflowOptionTree(step.options || []),
|
||||
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
||||
}))
|
||||
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
||||
}
|
||||
|
||||
export interface FlattenedAgentWorkflowOption {
|
||||
option: AgentWorkflowOptionUiConfig;
|
||||
depth: number;
|
||||
parentKeys: string[];
|
||||
}
|
||||
|
||||
export function flattenAgentWorkflowOptionTree(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
depth = 1,
|
||||
parentKeys: string[] = []
|
||||
): FlattenedAgentWorkflowOption[] {
|
||||
return options.flatMap((option) => [
|
||||
{
|
||||
option,
|
||||
depth,
|
||||
parentKeys,
|
||||
},
|
||||
...flattenAgentWorkflowOptionTree(
|
||||
option.children || [],
|
||||
depth + 1,
|
||||
[...parentKeys, option.key]
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
export function countAgentWorkflowOptionTree(
|
||||
options: AgentWorkflowOptionUiConfig[]
|
||||
): number {
|
||||
return flattenAgentWorkflowOptionTree(options).length;
|
||||
}
|
||||
|
||||
export function normalizeAgentWorkflowOptionOrdering(
|
||||
options: AgentWorkflowOptionUiConfig[]
|
||||
): AgentWorkflowOptionUiConfig[] {
|
||||
return options.map((option, optionIndex) => ({
|
||||
...option,
|
||||
sortOrder: optionIndex,
|
||||
children: normalizeAgentWorkflowOptionOrdering(option.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
export function getEnabledAgentWorkflowOptionTree(
|
||||
options: AgentWorkflowOptionUiConfig[]
|
||||
): AgentWorkflowOptionUiConfig[] {
|
||||
return [...options]
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
.filter((option) => option.enabled !== false)
|
||||
.map((option) => ({
|
||||
...option,
|
||||
children: getEnabledAgentWorkflowOptionTree(option.children || []),
|
||||
}));
|
||||
}
|
||||
|
||||
export function findAgentWorkflowOptionByKey(
|
||||
options: AgentWorkflowOptionUiConfig[],
|
||||
optionKey: string
|
||||
): AgentWorkflowOptionUiConfig | null {
|
||||
for (const option of options) {
|
||||
if (option.key === optionKey) {
|
||||
return option;
|
||||
}
|
||||
const foundChild = findAgentWorkflowOptionByKey(option.children || [], optionKey);
|
||||
if (foundChild) {
|
||||
return foundChild;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown, maxLength: number) {
|
||||
return String(value || "").trim().slice(0, maxLength);
|
||||
}
|
||||
@ -299,6 +370,46 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
raw.inputOptions && typeof raw.inputOptions === "object" && !Array.isArray(raw.inputOptions)
|
||||
? (raw.inputOptions as Record<string, unknown>)
|
||||
: {};
|
||||
const normalizeBrandStoreEntries = (input: unknown) => {
|
||||
if (!Array.isArray(input)) return [] as AgentWorkflowBrandStoreEntry[];
|
||||
|
||||
const mappedEntries: Array<AgentWorkflowBrandStoreEntry | null> = input.map((item, index) => {
|
||||
const record =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: {};
|
||||
const brandName = normalizeText(record.brandName, 64);
|
||||
if (!brandName) return null;
|
||||
const stores = normalizeStringList(record.stores).slice(0, 300);
|
||||
|
||||
return {
|
||||
brandName,
|
||||
stores,
|
||||
sortOrder: normalizeNumber(record.sortOrder, index),
|
||||
};
|
||||
});
|
||||
|
||||
return mappedEntries
|
||||
.filter((item): item is AgentWorkflowBrandStoreEntry => item !== null)
|
||||
.sort((left, right) => (left.sortOrder || 0) - (right.sortOrder || 0))
|
||||
.map((entry, index) => ({
|
||||
...entry,
|
||||
sortOrder: index,
|
||||
}));
|
||||
};
|
||||
const normalizedBrandStoreEntries = normalizeBrandStoreEntries(
|
||||
rawInputOptions.brandStoreEntries
|
||||
);
|
||||
const normalizedBrands = normalizeStringList(rawInputOptions.brands);
|
||||
const normalizedStores = normalizeStringList(rawInputOptions.stores);
|
||||
const fallbackBrandStoreEntries: AgentWorkflowBrandStoreEntry[] =
|
||||
normalizedBrandStoreEntries.length > 0
|
||||
? normalizedBrandStoreEntries
|
||||
: normalizedBrands.map((brandName, index) => ({
|
||||
brandName,
|
||||
stores: normalizedBrands.length === 1 ? normalizedStores : [],
|
||||
sortOrder: index,
|
||||
}));
|
||||
const defaultStepByKey = new Map(
|
||||
flattenAgentWorkflowSteps(defaultAgentWorkflowUiConfig.steps).map(({ step }) => [
|
||||
step.key,
|
||||
@ -308,6 +419,82 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
||||
const usedStepKeys = new Set<string>();
|
||||
|
||||
const normalizeOption = (
|
||||
item: unknown,
|
||||
optionIndex: number,
|
||||
stepKey: string,
|
||||
usedOptionKeys: Set<string>,
|
||||
defaultOptions: AgentWorkflowOptionUiConfig[],
|
||||
depth: number
|
||||
): AgentWorkflowOptionUiConfig => {
|
||||
const optionRecord =
|
||||
item && typeof item === "object" && !Array.isArray(item)
|
||||
? (item as Record<string, unknown>)
|
||||
: {};
|
||||
const defaultOption = defaultOptions.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;
|
||||
const hasRawChildren = Array.isArray(optionRecord.children);
|
||||
const rawChildren = hasRawChildren ? (optionRecord.children as unknown[]) : [];
|
||||
const defaultChildren =
|
||||
!hasRawChildren && defaultOption?.children ? defaultOption.children : [];
|
||||
const childSource =
|
||||
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH && (rawChildren.length > 0 || hasRawChildren)
|
||||
? rawChildren
|
||||
: depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
|
||||
? defaultChildren
|
||||
: [];
|
||||
|
||||
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,
|
||||
children:
|
||||
depth < AGENT_WORKFLOW_MAX_OPTION_DEPTH
|
||||
? childSource
|
||||
.map((child, childIndex) =>
|
||||
normalizeOption(
|
||||
child,
|
||||
childIndex,
|
||||
optionKey,
|
||||
usedOptionKeys,
|
||||
defaultOption?.children || [],
|
||||
depth + 1
|
||||
)
|
||||
)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder)
|
||||
: [],
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeStep = (
|
||||
item: unknown,
|
||||
stepIndex: number,
|
||||
@ -336,49 +523,9 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
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,
|
||||
};
|
||||
})
|
||||
.map((option, optionIndex) =>
|
||||
normalizeOption(option, optionIndex, stepKey, usedOptionKeys, defaultOptions, 1)
|
||||
)
|
||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||
|
||||
const hasRawChildren = Array.isArray(stepRecord?.children);
|
||||
@ -414,9 +561,82 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
||||
|
||||
return {
|
||||
inputOptions: {
|
||||
brands: normalizeStringList(rawInputOptions.brands),
|
||||
stores: normalizeStringList(rawInputOptions.stores),
|
||||
brands:
|
||||
fallbackBrandStoreEntries.length > 0
|
||||
? fallbackBrandStoreEntries.map((entry) => entry.brandName)
|
||||
: normalizedBrands,
|
||||
stores:
|
||||
fallbackBrandStoreEntries.length > 0
|
||||
? Array.from(
|
||||
new Set(fallbackBrandStoreEntries.flatMap((entry) => entry.stores))
|
||||
)
|
||||
: normalizedStores,
|
||||
brandStoreEntries: fallbackBrandStoreEntries,
|
||||
},
|
||||
steps: normalizedSteps,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAgentWorkflowBrandStoreEntries(config?: AgentWorkflowUiConfig | null) {
|
||||
return config?.inputOptions?.brandStoreEntries || defaultAgentWorkflowUiConfig.inputOptions?.brandStoreEntries || [];
|
||||
}
|
||||
|
||||
export function getAgentWorkflowStoreOptionsByBrand(params: {
|
||||
config?: AgentWorkflowUiConfig | null;
|
||||
brand?: string | null;
|
||||
}) {
|
||||
const entries = getAgentWorkflowBrandStoreEntries(params.config);
|
||||
const normalizedBrand = String(params.brand || "").trim().toLowerCase();
|
||||
if (!normalizedBrand) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matchedEntry = entries.find(
|
||||
(entry) => entry.brandName.trim().toLowerCase() === normalizedBrand
|
||||
);
|
||||
if (matchedEntry && matchedEntry.stores.length > 0) {
|
||||
return matchedEntry.stores;
|
||||
}
|
||||
|
||||
return params.config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [];
|
||||
}
|
||||
|
||||
export function parseAgentWorkflowSelectionPath(value?: string | null) {
|
||||
return String(value || "")
|
||||
.split(">")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, AGENT_WORKFLOW_MAX_OPTION_DEPTH);
|
||||
}
|
||||
|
||||
export function serializeAgentWorkflowSelectionPath(optionKeys: string[]) {
|
||||
return optionKeys.filter(Boolean).slice(0, AGENT_WORKFLOW_MAX_OPTION_DEPTH).join(">");
|
||||
}
|
||||
|
||||
export function getAgentWorkflowSelectedOptionPath(params: {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
selectionValue?: string | null;
|
||||
}) {
|
||||
const keys = parseAgentWorkflowSelectionPath(params.selectionValue);
|
||||
const selectedOptions: AgentWorkflowOptionUiConfig[] = [];
|
||||
let currentOptions = params.step.options || [];
|
||||
|
||||
for (const key of keys) {
|
||||
const matchedOption = currentOptions.find((option) => option.key === key);
|
||||
if (!matchedOption) break;
|
||||
selectedOptions.push(matchedOption);
|
||||
currentOptions = matchedOption.children || [];
|
||||
}
|
||||
|
||||
return selectedOptions;
|
||||
}
|
||||
|
||||
export function isAgentWorkflowStepSelectionComplete(params: {
|
||||
step: AgentWorkflowStepUiConfig;
|
||||
selectionValue?: string | null;
|
||||
}) {
|
||||
const selectedPath = getAgentWorkflowSelectedOptionPath(params);
|
||||
if (selectedPath.length === 0) return false;
|
||||
const lastOption = selectedPath[selectedPath.length - 1];
|
||||
return getEnabledAgentWorkflowOptionTree(lastOption.children || []).length === 0;
|
||||
}
|
||||
|
||||
@ -2493,6 +2493,14 @@ export async function createAgentPreset(params: {
|
||||
secondaryType?: string | null;
|
||||
visibility: AgentPresetVisibility;
|
||||
categoryPath?: {
|
||||
sectionId?: string | null;
|
||||
sectionName?: string | null;
|
||||
level1Id?: string | null;
|
||||
level1Name?: string | null;
|
||||
level2Id?: string | null;
|
||||
level2Name?: string | null;
|
||||
level3Id?: string | null;
|
||||
level3Name?: string | null;
|
||||
productCategory?: string | null;
|
||||
audienceTag?: string | null;
|
||||
usageScenario?: string | null;
|
||||
@ -2613,6 +2621,14 @@ export async function updateAgentPreset(params: {
|
||||
secondaryType?: string | null;
|
||||
visibility?: AgentPresetVisibility;
|
||||
categoryPath?: {
|
||||
sectionId?: string | null;
|
||||
sectionName?: string | null;
|
||||
level1Id?: string | null;
|
||||
level1Name?: string | null;
|
||||
level2Id?: string | null;
|
||||
level2Name?: string | null;
|
||||
level3Id?: string | null;
|
||||
level3Name?: string | null;
|
||||
productCategory?: string | null;
|
||||
audienceTag?: string | null;
|
||||
usageScenario?: string | null;
|
||||
|
||||
@ -532,6 +532,14 @@ export const agentPreset = pgTable("AgentPreset", {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
categoryPath: jsonb("categoryPath").$type<{
|
||||
sectionId?: string | null;
|
||||
sectionName?: string | null;
|
||||
level1Id?: string | null;
|
||||
level1Name?: string | null;
|
||||
level2Id?: string | null;
|
||||
level2Name?: string | null;
|
||||
level3Id?: string | null;
|
||||
level3Name?: string | null;
|
||||
productCategory?: string | null;
|
||||
audienceTag?: string | null;
|
||||
usageScenario?: string | null;
|
||||
|
||||
@ -35,12 +35,41 @@ export interface AgentPresetModeConfig {
|
||||
}
|
||||
|
||||
export interface AgentCategoryPath {
|
||||
sectionId?: string | null;
|
||||
sectionName?: string | null;
|
||||
level1Id?: string | null;
|
||||
level1Name?: string | null;
|
||||
level2Id?: string | null;
|
||||
level2Name?: string | null;
|
||||
level3Id?: string | null;
|
||||
level3Name?: string | null;
|
||||
productCategory?: string | null;
|
||||
audienceTag?: string | null;
|
||||
usageScenario?: string | null;
|
||||
brand?: string | null;
|
||||
}
|
||||
|
||||
export interface AgentPresetLibraryCategoryNode {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
children: AgentPresetLibraryCategoryNode[];
|
||||
}
|
||||
|
||||
export interface AgentPresetLibrarySection {
|
||||
id: string;
|
||||
label: string;
|
||||
description?: string | null;
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
categories: AgentPresetLibraryCategoryNode[];
|
||||
}
|
||||
|
||||
export interface AgentPresetLibraryConfig {
|
||||
sections: AgentPresetLibrarySection[];
|
||||
}
|
||||
|
||||
export type AgentWorkflowStepKey = string;
|
||||
|
||||
export interface AgentWorkflowOptionUiConfig {
|
||||
@ -51,6 +80,7 @@ export interface AgentWorkflowOptionUiConfig {
|
||||
enabled: boolean;
|
||||
sortOrder: number;
|
||||
promptFragment?: string | null;
|
||||
children?: AgentWorkflowOptionUiConfig[];
|
||||
}
|
||||
|
||||
export interface AgentWorkflowStepUiConfig {
|
||||
@ -67,10 +97,17 @@ export interface AgentWorkflowUiConfig {
|
||||
inputOptions?: {
|
||||
brands: string[];
|
||||
stores: string[];
|
||||
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
|
||||
};
|
||||
steps: AgentWorkflowStepUiConfig[];
|
||||
}
|
||||
|
||||
export interface AgentWorkflowBrandStoreEntry {
|
||||
brandName: string;
|
||||
stores: string[];
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface AgentWorkflowMaterialApiConfig {
|
||||
enabled: boolean;
|
||||
baseUrl: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user