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,
|
getSystemSetting,
|
||||||
updateAgentPreset,
|
updateAgentPreset,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
|
import { isAgentWorkflowAdminType } from "@/lib/middleware/admin";
|
||||||
import {
|
import {
|
||||||
AGENT_SECONDARY_TYPE_CONFIG_KEY,
|
AGENT_SECONDARY_TYPE_CONFIG_KEY,
|
||||||
isValidSecondaryTypeForPresetType,
|
isValidSecondaryTypeForPresetType,
|
||||||
@ -31,6 +32,14 @@ const updateAgentSchema = z
|
|||||||
visibility: z.enum(["public", "private"]).optional(),
|
visibility: z.enum(["public", "private"]).optional(),
|
||||||
categoryPath: z
|
categoryPath: z
|
||||||
.object({
|
.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(),
|
productCategory: z.string().trim().max(64).optional(),
|
||||||
audienceTag: z.string().trim().max(64).optional(),
|
audienceTag: z.string().trim().max(64).optional(),
|
||||||
usageScenario: z.string().trim().max(64).optional(),
|
usageScenario: z.string().trim().max(64).optional(),
|
||||||
@ -99,6 +108,11 @@ export async function PUT(
|
|||||||
if (!existingPreset) {
|
if (!existingPreset) {
|
||||||
return NextResponse.json({ success: false, error: "智能体不存在" }, { status: 404 });
|
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 effectiveType = data.type || existingPreset.type;
|
||||||
const effectiveSecondaryType =
|
const effectiveSecondaryType =
|
||||||
@ -124,7 +138,7 @@ export async function PUT(
|
|||||||
|
|
||||||
const preset = await updateAgentPreset({
|
const preset = await updateAgentPreset({
|
||||||
id,
|
id,
|
||||||
userId: session.user.id,
|
userId: canManagePublicPreset ? existingPreset.ownerUserId : session.user.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
secondaryType: data.secondaryType ?? undefined,
|
secondaryType: data.secondaryType ?? undefined,
|
||||||
@ -167,11 +181,25 @@ export async function DELETE(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
const deleted = await deleteAgentPresetById({
|
const existingPreset = await getAccessibleAgentPresetById({
|
||||||
id,
|
id,
|
||||||
userId: session.user.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) {
|
if (!deleted) {
|
||||||
return NextResponse.json({ success: false, error: "智能体不存在" }, { status: 404 });
|
return NextResponse.json({ success: false, error: "智能体不存在" }, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,10 @@ import {
|
|||||||
const listQuerySchema = z.object({
|
const listQuerySchema = z.object({
|
||||||
scope: z.enum(["all", "public", "private"]).default("all"),
|
scope: z.enum(["all", "public", "private"]).default("all"),
|
||||||
type: z.enum(["image", "video", "text"]).optional(),
|
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(),
|
productCategory: z.string().trim().max(64).optional(),
|
||||||
audienceTag: z.string().trim().max(64).optional(),
|
audienceTag: z.string().trim().max(64).optional(),
|
||||||
usageScenario: 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"),
|
visibility: z.enum(["public", "private"]).default("private"),
|
||||||
categoryPath: z
|
categoryPath: z
|
||||||
.object({
|
.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(),
|
productCategory: z.string().trim().max(64).optional(),
|
||||||
audienceTag: z.string().trim().max(64).optional(),
|
audienceTag: z.string().trim().max(64).optional(),
|
||||||
usageScenario: 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({
|
const query = listQuerySchema.parse({
|
||||||
scope: searchParams.get("scope") || "all",
|
scope: searchParams.get("scope") || "all",
|
||||||
type: searchParams.get("type") || undefined,
|
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,
|
productCategory: searchParams.get("productCategory") || undefined,
|
||||||
audienceTag: searchParams.get("audienceTag") || undefined,
|
audienceTag: searchParams.get("audienceTag") || undefined,
|
||||||
usageScenario: searchParams.get("usageScenario") || undefined,
|
usageScenario: searchParams.get("usageScenario") || undefined,
|
||||||
@ -84,6 +100,10 @@ export async function GET(request: NextRequest) {
|
|||||||
const filterByPath = (items: typeof presets) =>
|
const filterByPath = (items: typeof presets) =>
|
||||||
items.filter((item) => {
|
items.filter((item) => {
|
||||||
const path = item.categoryPath || {};
|
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.productCategory && path.productCategory !== query.productCategory) return false;
|
||||||
if (query.audienceTag && path.audienceTag !== query.audienceTag) return false;
|
if (query.audienceTag && path.audienceTag !== query.audienceTag) return false;
|
||||||
if (query.usageScenario && path.usageScenario !== query.usageScenario) return false;
|
if (query.usageScenario && path.usageScenario !== query.usageScenario) return false;
|
||||||
|
|||||||
@ -51,6 +51,11 @@ import {
|
|||||||
defaultAgentWorkflowShowcaseConfig,
|
defaultAgentWorkflowShowcaseConfig,
|
||||||
normalizeAgentWorkflowShowcaseConfig,
|
normalizeAgentWorkflowShowcaseConfig,
|
||||||
} from "@/lib/agents/workflow-showcase-config";
|
} from "@/lib/agents/workflow-showcase-config";
|
||||||
|
import {
|
||||||
|
AGENT_PRESET_LIBRARY_CONFIG_KEY,
|
||||||
|
defaultAgentPresetLibraryConfig,
|
||||||
|
normalizeAgentPresetLibraryConfig,
|
||||||
|
} from "@/lib/agents/preset-library-config";
|
||||||
import {
|
import {
|
||||||
defaultOssImageMigrationConfig,
|
defaultOssImageMigrationConfig,
|
||||||
normalizeOssImageMigrationConfig,
|
normalizeOssImageMigrationConfig,
|
||||||
@ -235,6 +240,7 @@ export async function GET() {
|
|||||||
realtimeCanvasConfigSetting,
|
realtimeCanvasConfigSetting,
|
||||||
storageFailoverConfigSetting,
|
storageFailoverConfigSetting,
|
||||||
ossImageMigrationConfigSetting,
|
ossImageMigrationConfigSetting,
|
||||||
|
agentPresetLibraryConfigSetting,
|
||||||
agentCategoryTreeConfigSetting,
|
agentCategoryTreeConfigSetting,
|
||||||
agentSecondaryTypeConfigSetting,
|
agentSecondaryTypeConfigSetting,
|
||||||
agentWorkflowUiConfigSetting,
|
agentWorkflowUiConfigSetting,
|
||||||
@ -339,6 +345,12 @@ export async function GET() {
|
|||||||
.where(eq(systemSettings.key, OSS_MIGRATION_CONFIG_KEY))
|
.where(eq(systemSettings.key, OSS_MIGRATION_CONFIG_KEY))
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.then(rows => rows[0]),
|
.then(rows => rows[0]),
|
||||||
|
db
|
||||||
|
.select()
|
||||||
|
.from(systemSettings)
|
||||||
|
.where(eq(systemSettings.key, AGENT_PRESET_LIBRARY_CONFIG_KEY))
|
||||||
|
.limit(1)
|
||||||
|
.then(rows => rows[0]),
|
||||||
db
|
db
|
||||||
.select()
|
.select()
|
||||||
.from(systemSettings)
|
.from(systemSettings)
|
||||||
@ -410,6 +422,9 @@ export async function GET() {
|
|||||||
const ossImageMigrationConfig = normalizeOssImageMigrationConfig(
|
const ossImageMigrationConfig = normalizeOssImageMigrationConfig(
|
||||||
ossImageMigrationConfigSetting?.value
|
ossImageMigrationConfigSetting?.value
|
||||||
);
|
);
|
||||||
|
const agentPresetLibraryConfig = normalizeAgentPresetLibraryConfig(
|
||||||
|
agentPresetLibraryConfigSetting?.value
|
||||||
|
);
|
||||||
const agentCategoryTreeConfig = normalizeAgentCategoryTreeConfig(
|
const agentCategoryTreeConfig = normalizeAgentCategoryTreeConfig(
|
||||||
agentCategoryTreeConfigSetting?.value
|
agentCategoryTreeConfigSetting?.value
|
||||||
);
|
);
|
||||||
@ -455,6 +470,7 @@ export async function GET() {
|
|||||||
rankingEnabled: true,
|
rankingEnabled: true,
|
||||||
canvasEnabled: true,
|
canvasEnabled: true,
|
||||||
agentEnabled: true,
|
agentEnabled: true,
|
||||||
|
agentPresetLibraryConfig: defaultAgentPresetLibraryConfig,
|
||||||
agentCategoryTreeConfig: normalizeAgentCategoryTreeConfig(undefined),
|
agentCategoryTreeConfig: normalizeAgentCategoryTreeConfig(undefined),
|
||||||
agentSecondaryTypeConfig: defaultAgentSecondaryTypeConfig,
|
agentSecondaryTypeConfig: defaultAgentSecondaryTypeConfig,
|
||||||
agentWorkflowUiConfig: defaultAgentWorkflowUiConfig,
|
agentWorkflowUiConfig: defaultAgentWorkflowUiConfig,
|
||||||
@ -502,6 +518,7 @@ export async function GET() {
|
|||||||
lovartComposerPreviewTexts,
|
lovartComposerPreviewTexts,
|
||||||
lovartPromptTagVisibility,
|
lovartPromptTagVisibility,
|
||||||
contactConfig,
|
contactConfig,
|
||||||
|
agentPresetLibraryConfig,
|
||||||
agentCategoryTreeConfig,
|
agentCategoryTreeConfig,
|
||||||
agentSecondaryTypeConfig,
|
agentSecondaryTypeConfig,
|
||||||
agentWorkflowUiConfig,
|
agentWorkflowUiConfig,
|
||||||
@ -556,6 +573,59 @@ const updateSchema = z.object({
|
|||||||
rankingEnabled: z.boolean().optional(),
|
rankingEnabled: z.boolean().optional(),
|
||||||
canvasEnabled: z.boolean().optional(),
|
canvasEnabled: z.boolean().optional(),
|
||||||
agentEnabled: 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
|
agentCategoryTreeConfig: z
|
||||||
.object({
|
.object({
|
||||||
productCategories: z.array(z.string().trim().min(1).max(64)).max(200).optional(),
|
productCategories: z.array(z.string().trim().min(1).max(64)).max(200).optional(),
|
||||||
@ -577,6 +647,16 @@ const updateSchema = z.object({
|
|||||||
.object({
|
.object({
|
||||||
brands: z.array(z.string().trim().min(1).max(64)).max(300).optional(),
|
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(),
|
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(),
|
.optional(),
|
||||||
steps: z
|
steps: z
|
||||||
@ -589,15 +669,18 @@ const updateSchema = z.object({
|
|||||||
sortOrder: z.number().min(0).max(999).optional(),
|
sortOrder: z.number().min(0).max(999).optional(),
|
||||||
options: z
|
options: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.lazy(() =>
|
||||||
key: z.string().trim().min(1).max(64),
|
z.object({
|
||||||
buttonName: z.string().trim().min(1).max(64),
|
key: z.string().trim().min(1).max(64),
|
||||||
description: z.string().trim().max(300).nullable().optional(),
|
buttonName: z.string().trim().min(1).max(64),
|
||||||
previewImageUrl: z.string().trim().max(1000).nullable().optional(),
|
description: z.string().trim().max(300).nullable().optional(),
|
||||||
enabled: z.boolean().optional(),
|
previewImageUrl: z.string().trim().max(1000).nullable().optional(),
|
||||||
sortOrder: z.number().min(0).max(999).optional(),
|
enabled: z.boolean().optional(),
|
||||||
promptFragment: z.string().trim().max(1000).nullable().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),
|
.max(100),
|
||||||
})
|
})
|
||||||
@ -719,6 +802,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
const allowedKeys = new Set([
|
const allowedKeys = new Set([
|
||||||
|
"agentPresetLibraryConfig",
|
||||||
"agentWorkflowUiConfig",
|
"agentWorkflowUiConfig",
|
||||||
"agentWorkflowMaterialApiConfig",
|
"agentWorkflowMaterialApiConfig",
|
||||||
"agentWorkflowBrandModelConfig",
|
"agentWorkflowBrandModelConfig",
|
||||||
@ -738,6 +822,7 @@ export async function PUT(request: NextRequest) {
|
|||||||
lovartComposerPreviewTexts,
|
lovartComposerPreviewTexts,
|
||||||
lovartPromptTagVisibility,
|
lovartPromptTagVisibility,
|
||||||
contactConfig,
|
contactConfig,
|
||||||
|
agentPresetLibraryConfig,
|
||||||
agentCategoryTreeConfig,
|
agentCategoryTreeConfig,
|
||||||
agentSecondaryTypeConfig,
|
agentSecondaryTypeConfig,
|
||||||
agentWorkflowUiConfig,
|
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) {
|
if (agentCategoryTreeConfig !== undefined) {
|
||||||
const normalized = normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig);
|
const normalized = normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig);
|
||||||
await db
|
await db
|
||||||
@ -1196,6 +1301,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
contactConfig !== undefined
|
contactConfig !== undefined
|
||||||
? normalizeContactConfig(contactConfig)
|
? normalizeContactConfig(contactConfig)
|
||||||
: normalizeContactConfig(undefined),
|
: normalizeContactConfig(undefined),
|
||||||
|
agentPresetLibraryConfig:
|
||||||
|
agentPresetLibraryConfig !== undefined
|
||||||
|
? normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig)
|
||||||
|
: defaultAgentPresetLibraryConfig,
|
||||||
agentCategoryTreeConfig:
|
agentCategoryTreeConfig:
|
||||||
agentCategoryTreeConfig !== undefined
|
agentCategoryTreeConfig !== undefined
|
||||||
? normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig)
|
? 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) {
|
if (agentCategoryTreeConfig !== undefined) {
|
||||||
const normalized = normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig);
|
const normalized = normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig);
|
||||||
await db
|
await db
|
||||||
@ -1716,6 +1845,10 @@ export async function PUT(request: NextRequest) {
|
|||||||
contactConfig !== undefined
|
contactConfig !== undefined
|
||||||
? normalizeContactConfig(contactConfig)
|
? normalizeContactConfig(contactConfig)
|
||||||
: normalizeContactConfig(undefined),
|
: normalizeContactConfig(undefined),
|
||||||
|
agentPresetLibraryConfig:
|
||||||
|
agentPresetLibraryConfig !== undefined
|
||||||
|
? normalizeAgentPresetLibraryConfig(agentPresetLibraryConfig)
|
||||||
|
: defaultAgentPresetLibraryConfig,
|
||||||
agentCategoryTreeConfig:
|
agentCategoryTreeConfig:
|
||||||
agentCategoryTreeConfig !== undefined
|
agentCategoryTreeConfig !== undefined
|
||||||
? normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig)
|
? normalizeAgentCategoryTreeConfig(agentCategoryTreeConfig)
|
||||||
|
|||||||
@ -249,6 +249,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
const [agentProjectResultUrls, setAgentProjectResultUrls] = useState<string[]>([]);
|
const [agentProjectResultUrls, setAgentProjectResultUrls] = useState<string[]>([]);
|
||||||
const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false);
|
const [isAgentProjectLoading, setIsAgentProjectLoading] = useState(false);
|
||||||
const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false);
|
const [isAgentProjectSaving, setIsAgentProjectSaving] = useState(false);
|
||||||
|
const [isAgentProjectSheetOpen, setIsAgentProjectSheetOpen] = useState(false);
|
||||||
const [workflowHydrationSource, setWorkflowHydrationSource] = useState<
|
const [workflowHydrationSource, setWorkflowHydrationSource] = useState<
|
||||||
"pending" | "project" | "local" | "empty"
|
"pending" | "project" | "local" | "empty"
|
||||||
>("pending");
|
>("pending");
|
||||||
@ -597,11 +598,17 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const persistAgentProject = useCallback(
|
const persistAgentProject = useCallback(
|
||||||
async (reason: "autosave" | "manual" | "switch") => {
|
async (
|
||||||
|
reason: "autosave" | "manual" | "switch",
|
||||||
|
overrides?: { title?: string | null }
|
||||||
|
) => {
|
||||||
if (!isAgentMode || !agentProjectId) return false;
|
if (!isAgentMode || !agentProjectId) return false;
|
||||||
|
const nextTitle =
|
||||||
|
String(overrides?.title ?? agentProjectTitle ?? "").trim() ||
|
||||||
|
buildDefaultAgentProjectTitle();
|
||||||
|
|
||||||
const result = await agentProjectService.updateProject(agentProjectId, {
|
const result = await agentProjectService.updateProject(agentProjectId, {
|
||||||
title: agentProjectTitle || buildDefaultAgentProjectTitle(),
|
title: nextTitle,
|
||||||
thumbnail: agentProjectThumbnail,
|
thumbnail: agentProjectThumbnail,
|
||||||
projectState: buildPersistedAgentProjectState(),
|
projectState: buildPersistedAgentProjectState(),
|
||||||
});
|
});
|
||||||
@ -752,9 +759,22 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
toast.error("当前还没有可保存的生图项目");
|
toast.error("当前还没有可保存的生图项目");
|
||||||
return;
|
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);
|
setIsAgentProjectSaving(true);
|
||||||
try {
|
try {
|
||||||
const success = await persistAgentProject("manual");
|
const success = await persistAgentProject("manual", { title: nextTitle });
|
||||||
if (success) {
|
if (success) {
|
||||||
toast.success("生图项目已保存");
|
toast.success("生图项目已保存");
|
||||||
} else {
|
} else {
|
||||||
@ -765,6 +785,36 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
}
|
}
|
||||||
}, [agentProjectId, persistAgentProject]);
|
}, [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 () => {
|
const handleCreateAgentProject = useCallback(async () => {
|
||||||
if (agentProjectId) {
|
if (agentProjectId) {
|
||||||
await persistAgentProject("switch");
|
await persistAgentProject("switch");
|
||||||
@ -1357,28 +1407,58 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
|
|
||||||
const agentProjectNavSlot = isAgentMode ? (
|
const agentProjectNavSlot = isAgentMode ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<DropdownMenu>
|
||||||
type="button"
|
<DropdownMenuTrigger asChild>
|
||||||
variant="outline"
|
<Button
|
||||||
onClick={() => {
|
type="button"
|
||||||
void handleManualSaveAgentProject();
|
variant="outline"
|
||||||
}}
|
disabled={isAgentProjectLoading}
|
||||||
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"
|
||||||
className="h-9 gap-2 border-white/10 bg-white/[0.03] text-white hover:bg-white/[0.08] hover:text-white"
|
>
|
||||||
>
|
{isAgentProjectSaving ? (
|
||||||
{isAgentProjectSaving ? (
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
) : (
|
||||||
) : (
|
<Save className="h-4 w-4" />
|
||||||
<Save className="h-4 w-4" />
|
)}
|
||||||
)}
|
项目管理
|
||||||
保存项目
|
</Button>
|
||||||
</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
|
<AgentProjectListSheet
|
||||||
|
open={isAgentProjectSheetOpen}
|
||||||
|
onOpenChange={setIsAgentProjectSheetOpen}
|
||||||
currentProjectId={agentProjectId || null}
|
currentProjectId={agentProjectId || null}
|
||||||
currentProjectTitle={agentProjectTitle}
|
|
||||||
onCreateProject={handleCreateAgentProject}
|
onCreateProject={handleCreateAgentProject}
|
||||||
onSelectProject={handleSelectAgentProject}
|
onSelectProject={handleSelectAgentProject}
|
||||||
onDeleteProject={handleDeleteAgentProject}
|
onDeleteProject={handleDeleteAgentProject}
|
||||||
|
onRenameProject={handleRenameAgentProject}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|||||||
@ -156,7 +156,6 @@ type ToolStepKey =
|
|||||||
| "preset"
|
| "preset"
|
||||||
| "brand-model"
|
| "brand-model"
|
||||||
| "materials"
|
| "materials"
|
||||||
| "slots"
|
|
||||||
| "prompt"
|
| "prompt"
|
||||||
| "settings";
|
| "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,
|
key: "prompt" as const,
|
||||||
label: "补充要求",
|
label: "补充要求",
|
||||||
@ -956,11 +946,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
if (current !== "preset") return current;
|
if (current !== "preset") return current;
|
||||||
if (isDetailReplacementPreset || isSkuReplacementPreset) {
|
if (isDetailReplacementPreset || isSkuReplacementPreset) {
|
||||||
if (hasMaterialStep) return "materials";
|
if (hasMaterialStep) return "materials";
|
||||||
return "slots";
|
return "prompt";
|
||||||
}
|
}
|
||||||
if (hasBrandModelStep) return "brand-model";
|
if (hasBrandModelStep) return "brand-model";
|
||||||
if (hasMaterialStep) return "materials";
|
if (hasMaterialStep) return "materials";
|
||||||
return "slots";
|
return "prompt";
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
hasBrandModelStep,
|
hasBrandModelStep,
|
||||||
@ -1481,6 +1471,179 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
}));
|
}));
|
||||||
}, [updateSkuReplacementState]);
|
}, [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 (
|
return (
|
||||||
<div className="flex h-dvh flex-col overflow-hidden bg-black">
|
<div className="flex h-dvh flex-col overflow-hidden bg-black">
|
||||||
<AgentCanvasTopNavDark
|
<AgentCanvasTopNavDark
|
||||||
@ -1576,176 +1739,6 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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={cn(activeToolStep === "prompt" ? "block" : "hidden")}>
|
||||||
<div className="rounded-[28px] border border-white/12 bg-white/[0.02] p-4">
|
<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">
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||||
@ -1762,7 +1755,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(activeToolStep === "settings" ? "block" : "hidden")}>
|
<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">
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-semibold text-white">
|
||||||
<Settings2 className="h-4 w-4 text-emerald-300" />
|
<Settings2 className="h-4 w-4 text-emerald-300" />
|
||||||
生成参数
|
生成参数
|
||||||
@ -1835,6 +1830,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</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 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>
|
</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">
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,9 @@ export function AgentPresetCardDark({
|
|||||||
: imageModeType === "scene_generation"
|
: imageModeType === "scene_generation"
|
||||||
? "场景图"
|
? "场景图"
|
||||||
: null;
|
: null;
|
||||||
|
const showEdit = Boolean(onEdit);
|
||||||
|
const showDelete = Boolean(onDelete);
|
||||||
|
const showFork = Boolean(onFork) && !showEdit && !showDelete;
|
||||||
|
|
||||||
if (compact) {
|
if (compact) {
|
||||||
return (
|
return (
|
||||||
@ -103,6 +106,45 @@ export function AgentPresetCardDark({
|
|||||||
)}
|
)}
|
||||||
onClick={() => onSelect(preset.id)}
|
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">
|
<div className="relative mx-auto h-[70px] w-[70px] overflow-hidden rounded-2xl border border-white/10 bg-black/30">
|
||||||
{previewUrl ? (
|
{previewUrl ? (
|
||||||
<img
|
<img
|
||||||
@ -237,36 +279,36 @@ export function AgentPresetCardDark({
|
|||||||
whileHover={{ opacity: 1 }}
|
whileHover={{ opacity: 1 }}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
<div className="flex flex-col gap-2 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||||
{preset.visibility === "private" && onEdit && (
|
{showEdit && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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"
|
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" />
|
<Pencil className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{preset.visibility === "private" && onDelete && (
|
{showDelete && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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"
|
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" />
|
<Trash2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{preset.visibility === "public" && onFork && (
|
{showFork && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
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"
|
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,
|
normalizeAgentPresetModeConfig,
|
||||||
patchAgentDefaultParamsWithModeConfig,
|
patchAgentDefaultParamsWithModeConfig,
|
||||||
} from "@/lib/agents/preset-mode-config";
|
} from "@/lib/agents/preset-mode-config";
|
||||||
|
import {
|
||||||
|
buildAgentPresetCategoryPath,
|
||||||
|
getEnabledAgentPresetLibraryNodes,
|
||||||
|
getEnabledAgentPresetLibrarySections,
|
||||||
|
normalizeAgentPresetLibraryConfig,
|
||||||
|
resolveAgentPresetCategoryPath,
|
||||||
|
} from "@/lib/agents/preset-library-config";
|
||||||
import { imageGenerationService } from "@/services/canvas";
|
import { imageGenerationService } from "@/services/canvas";
|
||||||
import { agentService } from "@/services/agents";
|
import { agentService } from "@/services/agents";
|
||||||
import type {
|
import type {
|
||||||
|
AgentPresetLibraryConfig,
|
||||||
AgentCreatePayload,
|
AgentCreatePayload,
|
||||||
AgentPresetModeType,
|
AgentPresetModeType,
|
||||||
AgentPresetModeConfig,
|
AgentPresetModeConfig,
|
||||||
@ -111,6 +119,9 @@ export function AgentPresetDialog({
|
|||||||
isMultimodal?: boolean;
|
isMultimodal?: boolean;
|
||||||
}>;
|
}>;
|
||||||
}>("/api/models", fetcher);
|
}>("/api/models", fetcher);
|
||||||
|
const { data: siteSettingsData } = useSWR<{
|
||||||
|
agentPresetLibraryConfig?: AgentPresetLibraryConfig;
|
||||||
|
}>("/api/site-settings", fetcher);
|
||||||
|
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [type, setType] = useState<"image" | "video" | "text">("image");
|
const [type, setType] = useState<"image" | "video" | "text">("image");
|
||||||
@ -127,6 +138,10 @@ export function AgentPresetDialog({
|
|||||||
const [uploadingSlotIndex, setUploadingSlotIndex] = useState<number | null>(null);
|
const [uploadingSlotIndex, setUploadingSlotIndex] = useState<number | null>(null);
|
||||||
const [avatarUploading, setAvatarUploading] = useState(false);
|
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||||
const [slots, setSlots] = useState<DialogSlotState[]>([createDefaultDialogSlot()]);
|
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 fileInputRefs = useRef<Array<HTMLInputElement | null>>([]);
|
||||||
const avatarInputRef = useRef<HTMLInputElement | null>(null);
|
const avatarInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
@ -153,6 +168,50 @@ export function AgentPresetDialog({
|
|||||||
...IMAGE_MODE_OPTIONS,
|
...IMAGE_MODE_OPTIONS,
|
||||||
]
|
]
|
||||||
: 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(
|
const videoProviderTypes = useMemo(
|
||||||
() => new Set(["volcano-video", "doubao-video", "qianwen-video", "fal-video", "ruxa-video"]),
|
() => new Set(["volcano-video", "doubao-video", "qianwen-video", "fal-video", "ruxa-video"]),
|
||||||
@ -212,6 +271,10 @@ export function AgentPresetDialog({
|
|||||||
modeType: "scene_generation",
|
modeType: "scene_generation",
|
||||||
});
|
});
|
||||||
setSlots([createDefaultDialogSlot()]);
|
setSlots([createDefaultDialogSlot()]);
|
||||||
|
setSelectedPresetSectionId("");
|
||||||
|
setSelectedPresetLevel1Id("");
|
||||||
|
setSelectedPresetLevel2Id("");
|
||||||
|
setSelectedPresetLevel3Id("");
|
||||||
setUploadingSlotIndex(null);
|
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(() => {
|
useEffect(() => {
|
||||||
if (type !== "image" || modeConfig.modeType === "legacy") {
|
if (type !== "image" || modeConfig.modeType === "legacy") {
|
||||||
@ -281,6 +352,48 @@ export function AgentPresetDialog({
|
|||||||
});
|
});
|
||||||
}, [modeConfig.modeType, type]);
|
}, [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>) => {
|
const handleSlotChange = (slotIndex: number, updates: Partial<DialogSlotState>) => {
|
||||||
setSlots((current) =>
|
setSlots((current) =>
|
||||||
current.map((slot, index) =>
|
current.map((slot, index) =>
|
||||||
@ -432,10 +545,17 @@ export function AgentPresetDialog({
|
|||||||
const nextDefaultParams = isStructuredImageModeBuilder
|
const nextDefaultParams = isStructuredImageModeBuilder
|
||||||
? patchAgentDefaultParamsWithModeConfig(initialPreset?.defaultParams || null, modeConfig)
|
? patchAgentDefaultParamsWithModeConfig(initialPreset?.defaultParams || null, modeConfig)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const nextCategoryPath = buildAgentPresetCategoryPath({
|
||||||
|
section: selectedPresetSection,
|
||||||
|
level1: selectedPresetLevel1,
|
||||||
|
level2: selectedPresetLevel2,
|
||||||
|
level3: selectedPresetLevel3,
|
||||||
|
});
|
||||||
const basePayload: AgentCreatePayload = {
|
const basePayload: AgentCreatePayload = {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
type,
|
type,
|
||||||
visibility,
|
visibility,
|
||||||
|
categoryPath: nextCategoryPath,
|
||||||
description: description.trim() || undefined,
|
description: description.trim() || undefined,
|
||||||
defaultModelId,
|
defaultModelId,
|
||||||
defaultParams: nextDefaultParams || undefined,
|
defaultParams: nextDefaultParams || undefined,
|
||||||
@ -455,7 +575,6 @@ export function AgentPresetDialog({
|
|||||||
? await agentService.updatePreset(initialPreset.id, {
|
? await agentService.updatePreset(initialPreset.id, {
|
||||||
...basePayload,
|
...basePayload,
|
||||||
secondaryType: null,
|
secondaryType: null,
|
||||||
categoryPath: null,
|
|
||||||
} satisfies AgentUpdatePayload)
|
} satisfies AgentUpdatePayload)
|
||||||
: await agentService.createPreset(basePayload);
|
: await agentService.createPreset(basePayload);
|
||||||
|
|
||||||
@ -693,6 +812,184 @@ export function AgentPresetDialog({
|
|||||||
</div>
|
</div>
|
||||||
</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" ? (
|
{type === "image" ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@ -1,36 +1,39 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import {
|
import {
|
||||||
Loader2,
|
Loader2,
|
||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
User,
|
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
Wand2,
|
Wand2,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Building2,
|
|
||||||
Menu
|
Menu
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { agentService } from "@/services/agents";
|
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 { 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 { AgentWorkflowShowcaseBanner } from "./AgentWorkflowShowcaseBanner";
|
||||||
import { AgentPresetCardDark } from "./AgentPresetCardDark";
|
import { AgentPresetCardDark } from "./AgentPresetCardDark";
|
||||||
import { AgentPresetDialog } from "./AgentPresetDialog";
|
import { AgentPresetDialog } from "./AgentPresetDialog";
|
||||||
|
|
||||||
// 导航标签类型
|
|
||||||
type NavTab = "cases" | "my-works" | "personal-assets" | "company-assets";
|
|
||||||
|
|
||||||
interface AgentPresetPanelDarkV2Props {
|
interface AgentPresetPanelDarkV2Props {
|
||||||
selectedPresetId?: string | null;
|
selectedPresetId?: string | null;
|
||||||
onSelectPreset: (presetId: string | null) => void;
|
onSelectPreset: (presetId: string | null) => void;
|
||||||
@ -46,12 +49,19 @@ export function AgentPresetPanelDarkV2({
|
|||||||
embedded = false,
|
embedded = false,
|
||||||
embeddedEntityLabel = "智能体",
|
embeddedEntityLabel = "智能体",
|
||||||
}: AgentPresetPanelDarkV2Props) {
|
}: 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 [keyword, setKeyword] = useState("");
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
||||||
const [editingPreset, setEditingPreset] = useState<AgentPreset | null>(null);
|
const [editingPreset, setEditingPreset] = useState<AgentPreset | null>(null);
|
||||||
const { data: siteSettingsData } = useSWR<{ agentWorkflowShowcaseConfig?: unknown }>(
|
const [selectedLevel1Id, setSelectedLevel1Id] = useState("");
|
||||||
!embedded ? "/api/site-settings" : null,
|
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())
|
(url: string) => fetch(url, { cache: "no-store" }).then((res) => res.json())
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -68,28 +78,124 @@ export function AgentPresetPanelDarkV2({
|
|||||||
return result.data;
|
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(() => {
|
const filteredPresets = useMemo(() => {
|
||||||
let presets: AgentPreset[] = [];
|
let presets: AgentPreset[] = isMyWorksTab ? data?.private || [] : data?.public || [];
|
||||||
|
|
||||||
switch (activeTab) {
|
if (!isMyWorksTab) {
|
||||||
case "cases":
|
const fallbackSectionId =
|
||||||
presets = data?.public || [];
|
presetLibrarySections.find((section) => section.id === AGENT_PRESET_LIBRARY_DEFAULT_SECTION_ID)?.id ||
|
||||||
break;
|
presetLibrarySections[0]?.id ||
|
||||||
case "my-works":
|
null;
|
||||||
presets = data?.private || [];
|
|
||||||
break;
|
presets = presets.filter((preset) => {
|
||||||
case "personal-assets":
|
const resolvedPath = resolveAgentPresetCategoryPath({
|
||||||
// 这里可以接入个人资产库数据
|
categoryPath: preset.categoryPath || null,
|
||||||
presets = data?.private || [];
|
config: presetLibraryConfig,
|
||||||
break;
|
});
|
||||||
case "company-assets":
|
const presetSectionId = resolvedPath.sectionId || fallbackSectionId;
|
||||||
// 这里可以接入公司资产库数据
|
if (!presetSectionId || presetSectionId !== activeTab) {
|
||||||
presets = data?.public || [];
|
return false;
|
||||||
break;
|
}
|
||||||
|
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()) {
|
if (keyword.trim()) {
|
||||||
const normalizedKeyword = keyword.trim().toLowerCase();
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||||
@ -101,7 +207,17 @@ export function AgentPresetPanelDarkV2({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return presets;
|
return presets;
|
||||||
}, [data, activeTab, keyword]);
|
}, [
|
||||||
|
activeTab,
|
||||||
|
data,
|
||||||
|
isMyWorksTab,
|
||||||
|
keyword,
|
||||||
|
presetLibraryConfig,
|
||||||
|
presetLibrarySections,
|
||||||
|
selectedLevel1Id,
|
||||||
|
selectedLevel2Id,
|
||||||
|
selectedLevel3Id,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleForkPreset = async (preset: AgentPreset) => {
|
const handleForkPreset = async (preset: AgentPreset) => {
|
||||||
const result = await agentService.forkPreset(preset.id, `${preset.name}(副本)`);
|
const result = await agentService.forkPreset(preset.id, `${preset.name}(副本)`);
|
||||||
@ -130,22 +246,10 @@ export function AgentPresetPanelDarkV2({
|
|||||||
await mutate();
|
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 = () => {
|
const getCurrentTitle = () => {
|
||||||
switch (activeTab) {
|
if (isMyWorksTab) return "我的作品";
|
||||||
case "cases": return "公共智能体";
|
return activeSection?.label || "生图模式";
|
||||||
case "my-works": return "我的智能体";
|
|
||||||
case "personal-assets": return "个人资产库";
|
|
||||||
case "company-assets": return "公司资产库";
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 嵌入模式只渲染卡片网格
|
// 嵌入模式只渲染卡片网格
|
||||||
@ -183,10 +287,10 @@ export function AgentPresetPanelDarkV2({
|
|||||||
|
|
||||||
{/* 导航标签(简化版) */}
|
{/* 导航标签(简化版) */}
|
||||||
<div className="mb-4 flex flex-wrap gap-1">
|
<div className="mb-4 flex flex-wrap gap-1">
|
||||||
{navTabs.slice(0, 2).map((tab) => (
|
{navTabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => setActiveTab(tab.id as NavTab)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
"rounded-full px-3 py-1 text-xs font-medium transition-colors",
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
@ -199,6 +303,40 @@ export function AgentPresetPanelDarkV2({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-2.5">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -213,9 +351,17 @@ export function AgentPresetPanelDarkV2({
|
|||||||
preset={preset}
|
preset={preset}
|
||||||
selected={selectedPresetId === preset.id}
|
selected={selectedPresetId === preset.id}
|
||||||
onSelect={onSelectPreset}
|
onSelect={onSelectPreset}
|
||||||
onFork={handleForkPreset}
|
onFork={preset.visibility === "public" && !canManagePublicPresets ? handleForkPreset : undefined}
|
||||||
onEdit={setEditingPreset}
|
onEdit={
|
||||||
onDelete={handleDeletePreset}
|
preset.visibility === "private" || canManagePublicPresets
|
||||||
|
? setEditingPreset
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onDelete={
|
||||||
|
preset.visibility === "private" || canManagePublicPresets
|
||||||
|
? handleDeletePreset
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
index={index}
|
index={index}
|
||||||
compact
|
compact
|
||||||
/>
|
/>
|
||||||
@ -352,7 +498,7 @@ export function AgentPresetPanelDarkV2({
|
|||||||
<div className="relative flex-1 max-w-md">
|
<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" />
|
<Search className="absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 text-white/40" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索智能体..."
|
placeholder="搜索生图模式..."
|
||||||
value={keyword}
|
value={keyword}
|
||||||
onChange={(e) => setKeyword(e.target.value)}
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -363,11 +509,50 @@ export function AgentPresetPanelDarkV2({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-sm text-white/50">
|
<div className="flex items-center gap-2 text-sm text-white/50">
|
||||||
<Grid3X3 className="h-4 w-4" />
|
<Grid3X3 className="h-4 w-4" />
|
||||||
<span>共 {filteredPresets.length} 个</span>
|
<span>共 {filteredPresets.length} 个</span>
|
||||||
</div>
|
</div>
|
||||||
</motion.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
|
<motion.div
|
||||||
@ -396,9 +581,17 @@ export function AgentPresetPanelDarkV2({
|
|||||||
preset={preset}
|
preset={preset}
|
||||||
selected={selectedPresetId === preset.id}
|
selected={selectedPresetId === preset.id}
|
||||||
onSelect={onSelectPreset}
|
onSelect={onSelectPreset}
|
||||||
onFork={handleForkPreset}
|
onFork={preset.visibility === "public" && !canManagePublicPresets ? handleForkPreset : undefined}
|
||||||
onEdit={setEditingPreset}
|
onEdit={
|
||||||
onDelete={handleDeletePreset}
|
preset.visibility === "private" || canManagePublicPresets
|
||||||
|
? setEditingPreset
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onDelete={
|
||||||
|
preset.visibility === "private" || canManagePublicPresets
|
||||||
|
? handleDeletePreset
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
index={index}
|
index={index}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
@ -432,7 +625,7 @@ export function AgentPresetPanelDarkV2({
|
|||||||
setShowCreateDialog(true);
|
setShowCreateDialog(true);
|
||||||
}}
|
}}
|
||||||
initialPreset={editingPreset}
|
initialPreset={editingPreset}
|
||||||
entityLabel="智能体"
|
entityLabel="生图模式"
|
||||||
theme="dark"
|
theme="dark"
|
||||||
onSaved={async (preset) => {
|
onSaved={async (preset) => {
|
||||||
await mutate();
|
await mutate();
|
||||||
@ -459,3 +652,62 @@ function EmptyState({ message }: { message: string }) {
|
|||||||
</motion.div>
|
</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 { useEffect, useState } from "react";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { zhCN } from "date-fns/locale";
|
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 { toast } from "sonner";
|
||||||
import { agentProjectService } from "@/services/agent-projects";
|
import { agentProjectService } from "@/services/agent-projects";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -14,31 +14,34 @@ import {
|
|||||||
SheetDescription,
|
SheetDescription,
|
||||||
SheetHeader,
|
SheetHeader,
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
import { buildImageProxyUrl } from "@/lib/image-delivery";
|
||||||
import type { AgentGenerationProject } from "@/types/agent";
|
import type { AgentGenerationProject } from "@/types/agent";
|
||||||
|
|
||||||
interface AgentProjectListSheetProps {
|
interface AgentProjectListSheetProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
currentProjectId?: string | null;
|
currentProjectId?: string | null;
|
||||||
currentProjectTitle?: string | null;
|
|
||||||
onCreateProject: () => Promise<void> | void;
|
onCreateProject: () => Promise<void> | void;
|
||||||
onSelectProject: (projectId: string) => Promise<void> | void;
|
onSelectProject: (projectId: string) => Promise<void> | void;
|
||||||
onDeleteProject: (projectId: string) => Promise<void> | void;
|
onDeleteProject: (projectId: string) => Promise<void> | void;
|
||||||
|
onRenameProject: (projectId: string, currentTitle?: string | null) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AgentProjectListSheet({
|
export function AgentProjectListSheet({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
currentProjectId,
|
currentProjectId,
|
||||||
currentProjectTitle,
|
|
||||||
onCreateProject,
|
onCreateProject,
|
||||||
onSelectProject,
|
onSelectProject,
|
||||||
onDeleteProject,
|
onDeleteProject,
|
||||||
|
onRenameProject,
|
||||||
}: AgentProjectListSheetProps) {
|
}: AgentProjectListSheetProps) {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [projects, setProjects] = useState<AgentGenerationProject[]>([]);
|
const [projects, setProjects] = useState<AgentGenerationProject[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||||
|
const [pendingRenameId, setPendingRenameId] = useState<string | null>(null);
|
||||||
|
|
||||||
const loadProjects = async () => {
|
const loadProjects = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -61,17 +64,7 @@ export function AgentProjectListSheet({
|
|||||||
}, [open, currentProjectId]);
|
}, [open, currentProjectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<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>
|
|
||||||
<SheetContent
|
<SheetContent
|
||||||
side="right"
|
side="right"
|
||||||
className="w-[380px] border-white/10 bg-[#0b0d0f] p-0 text-white sm:max-w-[380px]"
|
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 () => {
|
onClick={async () => {
|
||||||
await onCreateProject();
|
await onCreateProject();
|
||||||
await loadProjects();
|
await loadProjects();
|
||||||
setOpen(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
className="h-10 w-full gap-2 bg-emerald-500 text-white hover:bg-emerald-400"
|
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"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await onSelectProject(project.id);
|
await onSelectProject(project.id);
|
||||||
setOpen(false);
|
onOpenChange(false);
|
||||||
}}
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex w-full items-center gap-3 rounded-2xl border p-3 text-left transition-colors",
|
"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>
|
||||||
</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
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0 text-white/45 opacity-0 transition-opacity hover:text-rose-300 group-hover:opacity-100"
|
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) => {
|
onClick={async (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
if (!window.confirm("确定删除这个生图项目吗?")) return;
|
if (!window.confirm("确定删除这个生图项目吗?")) return;
|
||||||
|
|||||||
@ -8,7 +8,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
import {
|
||||||
|
defaultAgentWorkflowUiConfig,
|
||||||
|
getAgentWorkflowBrandStoreEntries,
|
||||||
|
getAgentWorkflowStoreOptionsByBrand,
|
||||||
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||||
import {
|
import {
|
||||||
buildAgentWorkflowComposerContext,
|
buildAgentWorkflowComposerContext,
|
||||||
@ -84,9 +88,13 @@ export function AgentWorkflowPanel({
|
|||||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||||
[config?.inputOptions?.brands]
|
[config?.inputOptions?.brands]
|
||||||
);
|
);
|
||||||
|
const brandStoreEntries = useMemo(
|
||||||
|
() => getAgentWorkflowBrandStoreEntries(config),
|
||||||
|
[config]
|
||||||
|
);
|
||||||
const storeOptions = useMemo(
|
const storeOptions = useMemo(
|
||||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||||
[config?.inputOptions?.stores]
|
[config, inputs.brand]
|
||||||
);
|
);
|
||||||
const resolvedConfig = useMemo(() => {
|
const resolvedConfig = useMemo(() => {
|
||||||
const source = config || defaultAgentWorkflowUiConfig;
|
const source = config || defaultAgentWorkflowUiConfig;
|
||||||
@ -197,14 +205,24 @@ export function AgentWorkflowPanel({
|
|||||||
placeholder="品牌"
|
placeholder="品牌"
|
||||||
searchPlaceholder="输入品牌名筛选"
|
searchPlaceholder="输入品牌名筛选"
|
||||||
emptyText="暂无品牌可选,请先到系统设置配置"
|
emptyText="暂无品牌可选,请先到系统设置配置"
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
onValueChange={(value) =>
|
||||||
|
onInputsChange({
|
||||||
|
...inputs,
|
||||||
|
brand: value,
|
||||||
|
store:
|
||||||
|
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||||
|
? inputs.store
|
||||||
|
: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
value={inputs.store}
|
value={inputs.store}
|
||||||
options={storeOptions}
|
options={storeOptions}
|
||||||
placeholder="店铺"
|
placeholder="店铺"
|
||||||
searchPlaceholder="输入店铺名筛选"
|
searchPlaceholder="输入店铺名筛选"
|
||||||
emptyText="暂无店铺可选,请先到系统设置配置"
|
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||||
|
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,7 +9,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
import {
|
||||||
|
defaultAgentWorkflowUiConfig,
|
||||||
|
getAgentWorkflowBrandStoreEntries,
|
||||||
|
getAgentWorkflowStoreOptionsByBrand,
|
||||||
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||||
import {
|
import {
|
||||||
buildAgentWorkflowComposerContext,
|
buildAgentWorkflowComposerContext,
|
||||||
@ -166,10 +170,13 @@ export function AgentWorkflowPanelDark({
|
|||||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||||
[config?.inputOptions?.brands]
|
[config?.inputOptions?.brands]
|
||||||
);
|
);
|
||||||
|
const brandStoreEntries = useMemo(
|
||||||
|
() => getAgentWorkflowBrandStoreEntries(config),
|
||||||
|
[config]
|
||||||
|
);
|
||||||
const storeOptions = useMemo(
|
const storeOptions = useMemo(
|
||||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||||
[config?.inputOptions?.stores]
|
[config, inputs.brand]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedConfig = useMemo(() => {
|
const resolvedConfig = useMemo(() => {
|
||||||
@ -305,14 +312,24 @@ export function AgentWorkflowPanelDark({
|
|||||||
placeholder="选择品牌"
|
placeholder="选择品牌"
|
||||||
searchPlaceholder="输入品牌名筛选"
|
searchPlaceholder="输入品牌名筛选"
|
||||||
emptyText="暂无品牌可选"
|
emptyText="暂无品牌可选"
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
onValueChange={(value) =>
|
||||||
|
onInputsChange({
|
||||||
|
...inputs,
|
||||||
|
brand: value,
|
||||||
|
store:
|
||||||
|
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||||
|
? inputs.store
|
||||||
|
: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
value={inputs.store}
|
value={inputs.store}
|
||||||
options={storeOptions}
|
options={storeOptions}
|
||||||
placeholder="选择店铺"
|
placeholder="选择店铺"
|
||||||
searchPlaceholder="输入店铺名筛选"
|
searchPlaceholder="输入店铺名筛选"
|
||||||
emptyText="暂无店铺可选"
|
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||||
|
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@ -12,7 +12,12 @@ import {
|
|||||||
defaultAgentWorkflowUiConfig,
|
defaultAgentWorkflowUiConfig,
|
||||||
flattenAgentWorkflowSteps,
|
flattenAgentWorkflowSteps,
|
||||||
flattenVisibleAgentWorkflowSteps,
|
flattenVisibleAgentWorkflowSteps,
|
||||||
|
getAgentWorkflowBrandStoreEntries,
|
||||||
|
getAgentWorkflowSelectedOptionPath,
|
||||||
|
getAgentWorkflowStoreOptionsByBrand,
|
||||||
|
getEnabledAgentWorkflowOptionTree,
|
||||||
getEnabledAgentWorkflowStepTree,
|
getEnabledAgentWorkflowStepTree,
|
||||||
|
serializeAgentWorkflowSelectionPath,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
import { AgentCanvasTopNavDark } from "@/components/agents/AgentCanvasTopNavDark";
|
||||||
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner";
|
||||||
@ -59,16 +64,18 @@ function buildSelectionSummary(params: {
|
|||||||
}) {
|
}) {
|
||||||
const result: AgentWorkflowComposerSelection[] = [];
|
const result: AgentWorkflowComposerSelection[] = [];
|
||||||
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
|
for (const { step } of flattenVisibleAgentWorkflowSteps(params.config.steps, params.selections)) {
|
||||||
const selectedOptionKey = params.selections[step.key];
|
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||||
if (!selectedOptionKey) continue;
|
step,
|
||||||
const option = step.options.find((item) => item.key === selectedOptionKey && item.enabled);
|
selectionValue: params.selections[step.key],
|
||||||
if (!option) continue;
|
});
|
||||||
result.push({
|
selectedOptions.forEach((option) => {
|
||||||
stepKey: step.key,
|
result.push({
|
||||||
stepTitle: step.title,
|
stepKey: step.key,
|
||||||
optionKey: option.key,
|
stepTitle: step.title,
|
||||||
optionName: option.buttonName,
|
optionKey: option.key,
|
||||||
promptFragment: option.promptFragment || "",
|
optionName: option.buttonName,
|
||||||
|
promptFragment: option.promptFragment || "",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@ -181,10 +188,13 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||||
[config?.inputOptions?.brands]
|
[config?.inputOptions?.brands]
|
||||||
);
|
);
|
||||||
|
const brandStoreEntries = useMemo(
|
||||||
|
() => getAgentWorkflowBrandStoreEntries(config),
|
||||||
|
[config]
|
||||||
|
);
|
||||||
const storeOptions = useMemo(
|
const storeOptions = useMemo(
|
||||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||||
[config?.inputOptions?.stores]
|
[config, inputs.brand]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedConfig = useMemo(() => {
|
const resolvedConfig = useMemo(() => {
|
||||||
@ -222,6 +232,68 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
[resolvedConfig, selections]
|
[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 () => {
|
const handleCopyPrompt = async () => {
|
||||||
if (!composerContext?.promptText) {
|
if (!composerContext?.promptText) {
|
||||||
toast.error("当前还没有可复制的 Prompt");
|
toast.error("当前还没有可复制的 Prompt");
|
||||||
@ -291,15 +363,29 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
searchPlaceholder="输入品牌名筛选"
|
searchPlaceholder="输入品牌名筛选"
|
||||||
emptyText="暂无品牌可选"
|
emptyText="暂无品牌可选"
|
||||||
theme="dark"
|
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
|
<SearchableSelect
|
||||||
value={inputs.store}
|
value={inputs.store}
|
||||||
options={storeOptions}
|
options={storeOptions}
|
||||||
placeholder="选择店铺"
|
placeholder="选择店铺"
|
||||||
searchPlaceholder="输入店铺名筛选"
|
searchPlaceholder="输入店铺名筛选"
|
||||||
emptyText="暂无店铺可选"
|
emptyText={
|
||||||
|
inputs.brand
|
||||||
|
? "当前品牌下暂无店铺可选"
|
||||||
|
: "请先选择品牌"
|
||||||
|
}
|
||||||
theme="dark"
|
theme="dark"
|
||||||
|
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -331,23 +417,7 @@ export function AgentWorkflowPanelDarkV2({
|
|||||||
{step.description && (
|
{step.description && (
|
||||||
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
<p className="mb-4 text-sm text-white/40">{step.description}</p>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
|
{renderOptionLevels(step, getEnabledAgentWorkflowOptionTree(step.options || []))}
|
||||||
{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>
|
|
||||||
</motion.section>
|
</motion.section>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { defaultAgentWorkflowUiConfig } from "@/lib/agents/workflow-ui-config";
|
import {
|
||||||
|
defaultAgentWorkflowUiConfig,
|
||||||
|
getAgentWorkflowBrandStoreEntries,
|
||||||
|
getAgentWorkflowStoreOptionsByBrand,
|
||||||
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
import { createEmptyAgentWorkflowMaterialSelection } from "@/lib/agents/workflow-material-config";
|
||||||
import {
|
import {
|
||||||
buildAgentWorkflowComposerContext,
|
buildAgentWorkflowComposerContext,
|
||||||
@ -178,10 +182,13 @@ export function AgentWorkflowPanelNew({
|
|||||||
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
() => config?.inputOptions?.brands || defaultAgentWorkflowUiConfig.inputOptions?.brands || [],
|
||||||
[config?.inputOptions?.brands]
|
[config?.inputOptions?.brands]
|
||||||
);
|
);
|
||||||
|
const brandStoreEntries = useMemo(
|
||||||
|
() => getAgentWorkflowBrandStoreEntries(config),
|
||||||
|
[config]
|
||||||
|
);
|
||||||
const storeOptions = useMemo(
|
const storeOptions = useMemo(
|
||||||
() => config?.inputOptions?.stores || defaultAgentWorkflowUiConfig.inputOptions?.stores || [],
|
() => getAgentWorkflowStoreOptionsByBrand({ config, brand: inputs.brand }),
|
||||||
[config?.inputOptions?.stores]
|
[config, inputs.brand]
|
||||||
);
|
);
|
||||||
|
|
||||||
const resolvedConfig = useMemo(() => {
|
const resolvedConfig = useMemo(() => {
|
||||||
@ -340,14 +347,24 @@ export function AgentWorkflowPanelNew({
|
|||||||
placeholder="选择品牌"
|
placeholder="选择品牌"
|
||||||
searchPlaceholder="输入品牌名筛选"
|
searchPlaceholder="输入品牌名筛选"
|
||||||
emptyText="暂无品牌可选"
|
emptyText="暂无品牌可选"
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, brand: value })}
|
onValueChange={(value) =>
|
||||||
|
onInputsChange({
|
||||||
|
...inputs,
|
||||||
|
brand: value,
|
||||||
|
store:
|
||||||
|
value.trim().toLowerCase() === inputs.brand.trim().toLowerCase()
|
||||||
|
? inputs.store
|
||||||
|
: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<SearchableSelect
|
<SearchableSelect
|
||||||
value={inputs.store}
|
value={inputs.store}
|
||||||
options={storeOptions}
|
options={storeOptions}
|
||||||
placeholder="选择店铺"
|
placeholder="选择店铺"
|
||||||
searchPlaceholder="输入店铺名筛选"
|
searchPlaceholder="输入店铺名筛选"
|
||||||
emptyText="暂无店铺可选"
|
emptyText={inputs.brand ? "当前品牌下暂无店铺可选" : "请先选择品牌"}
|
||||||
|
disabled={!inputs.brand || (brandStoreEntries.length > 0 && storeOptions.length === 0)}
|
||||||
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
onValueChange={(value) => onInputsChange({ ...inputs, store: value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1411,7 +1411,7 @@ export function SceneFusionView({
|
|||||||
{selectedModeOption?.label || "请选择处理模式"}
|
{selectedModeOption?.label || "请选择处理模式"}
|
||||||
</span>
|
</span>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="z-[30000] bg-background">
|
<SelectContent className="z-[30000] !bg-white dark:!bg-zinc-950">
|
||||||
{modeOptions.map((option) => (
|
{modeOptions.map((option) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
className="items-start py-2"
|
className="items-start py-2"
|
||||||
|
|||||||
@ -16,6 +16,7 @@ interface SearchableSelectProps {
|
|||||||
emptyText?: string;
|
emptyText?: string;
|
||||||
onValueChange: (value: string) => void;
|
onValueChange: (value: string) => void;
|
||||||
theme?: "light" | "dark";
|
theme?: "light" | "dark";
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchableSelect({
|
export function SearchableSelect({
|
||||||
@ -26,6 +27,7 @@ export function SearchableSelect({
|
|||||||
emptyText = "暂无可选项",
|
emptyText = "暂无可选项",
|
||||||
onValueChange,
|
onValueChange,
|
||||||
theme = "light",
|
theme = "light",
|
||||||
|
disabled = false,
|
||||||
}: SearchableSelectProps) {
|
}: SearchableSelectProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
@ -51,9 +53,11 @@ export function SearchableSelect({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-10 w-full justify-between px-3 text-left font-normal",
|
"h-10 w-full justify-between px-3 text-left font-normal",
|
||||||
|
disabled && "cursor-not-allowed opacity-60",
|
||||||
isDark &&
|
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"
|
"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
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -87,6 +91,7 @@ export function SearchableSelect({
|
|||||||
value={query}
|
value={query}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
|
disabled={disabled}
|
||||||
className={cn(
|
className={cn(
|
||||||
isDark &&
|
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"
|
"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
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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" &&
|
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",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
style={style}
|
style={{
|
||||||
|
backgroundColor: "var(--popover)",
|
||||||
|
color: "var(--popover-foreground)",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
position={position}
|
position={position}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<SelectScrollUpButton />
|
<SelectScrollUpButton />
|
||||||
<SelectPrimitive.Viewport
|
<SelectPrimitive.Viewport
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-[inherit] bg-inherit p-1",
|
"rounded-[inherit] bg-inherit p-1 !bg-white dark:!bg-zinc-950",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"w-full min-w-[var(--radix-select-trigger-width)]"
|
"w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "inherit",
|
backgroundColor: "var(--popover)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{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 {
|
import {
|
||||||
defaultAgentWorkflowUiConfig,
|
defaultAgentWorkflowUiConfig,
|
||||||
flattenVisibleAgentWorkflowSteps,
|
flattenVisibleAgentWorkflowSteps,
|
||||||
|
getAgentWorkflowSelectedOptionPath,
|
||||||
getEnabledAgentWorkflowStepTree,
|
getEnabledAgentWorkflowStepTree,
|
||||||
|
isAgentWorkflowStepSelectionComplete,
|
||||||
} from "@/lib/agents/workflow-ui-config";
|
} from "@/lib/agents/workflow-ui-config";
|
||||||
import type {
|
import type {
|
||||||
AgentWorkflowComposerContext,
|
AgentWorkflowComposerContext,
|
||||||
@ -35,21 +37,20 @@ export function buildAgentWorkflowComposerContext(params: {
|
|||||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
const selections = steps
|
const selections = steps
|
||||||
.map((step) => {
|
.map((step) => {
|
||||||
const selectedKey = params.selections[step.key];
|
const selectedOptions = getAgentWorkflowSelectedOptionPath({
|
||||||
if (!selectedKey) return null;
|
step,
|
||||||
const option = (step.options || []).find(
|
selectionValue: params.selections[step.key],
|
||||||
(item) => item.key === selectedKey && item.enabled !== false
|
});
|
||||||
);
|
if (selectedOptions.length === 0) return [];
|
||||||
if (!option) return null;
|
return selectedOptions.map((option) => ({
|
||||||
return {
|
|
||||||
stepKey: step.key,
|
stepKey: step.key,
|
||||||
stepTitle: step.title,
|
stepTitle: step.title,
|
||||||
optionKey: option.key,
|
optionKey: option.key,
|
||||||
optionName: option.buttonName,
|
optionName: option.buttonName,
|
||||||
promptFragment: option.promptFragment || option.buttonName,
|
promptFragment: option.promptFragment || option.buttonName,
|
||||||
};
|
}));
|
||||||
})
|
})
|
||||||
.filter(Boolean) as AgentWorkflowComposerContext["selections"];
|
.flat() as AgentWorkflowComposerContext["selections"];
|
||||||
const referenceImages = getSelectedWorkflowReferenceImages({
|
const referenceImages = getSelectedWorkflowReferenceImages({
|
||||||
inputs: params.inputs,
|
inputs: params.inputs,
|
||||||
materials: params.materials,
|
materials: params.materials,
|
||||||
@ -231,8 +232,10 @@ export function getAgentWorkflowProgress(params: {
|
|||||||
.map(({ step }) => step)
|
.map(({ step }) => step)
|
||||||
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
.filter((step) => Array.isArray(step.options) && step.options.length > 0);
|
||||||
const completedSteps = enabledSteps.filter((step) => {
|
const completedSteps = enabledSteps.filter((step) => {
|
||||||
const value = params.selections[step.key];
|
return isAgentWorkflowStepSelectionComplete({
|
||||||
return Boolean(String(value || "").trim());
|
step,
|
||||||
|
selectionValue: params.selections[step.key],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
|
AgentWorkflowBrandStoreEntry,
|
||||||
AgentWorkflowOptionUiConfig,
|
AgentWorkflowOptionUiConfig,
|
||||||
AgentWorkflowStepUiConfig,
|
AgentWorkflowStepUiConfig,
|
||||||
AgentWorkflowUiConfig,
|
AgentWorkflowUiConfig,
|
||||||
@ -6,6 +7,7 @@ import type {
|
|||||||
|
|
||||||
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
export const AGENT_WORKFLOW_UI_CONFIG_KEY = "agent_workflow_ui_config";
|
||||||
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 3;
|
export const AGENT_WORKFLOW_MAX_TREE_DEPTH = 3;
|
||||||
|
export const AGENT_WORKFLOW_MAX_OPTION_DEPTH = 3;
|
||||||
|
|
||||||
export interface FlattenedAgentWorkflowStep {
|
export interface FlattenedAgentWorkflowStep {
|
||||||
step: AgentWorkflowStepUiConfig;
|
step: AgentWorkflowStepUiConfig;
|
||||||
@ -21,6 +23,7 @@ function cloneOption(
|
|||||||
description: option.description || "",
|
description: option.description || "",
|
||||||
previewImageUrl: option.previewImageUrl || "",
|
previewImageUrl: option.previewImageUrl || "",
|
||||||
promptFragment: option.promptFragment || "",
|
promptFragment: option.promptFragment || "",
|
||||||
|
children: (option.children || []).map(cloneOption),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,7 @@ function createOption(
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
promptFragment,
|
promptFragment,
|
||||||
|
children: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +133,7 @@ export const defaultAgentWorkflowUiConfig: AgentWorkflowUiConfig = {
|
|||||||
inputOptions: {
|
inputOptions: {
|
||||||
brands: [],
|
brands: [],
|
||||||
stores: [],
|
stores: [],
|
||||||
|
brandStoreEntries: [],
|
||||||
},
|
},
|
||||||
steps: defaultSteps.map((step, index) =>
|
steps: defaultSteps.map((step, index) =>
|
||||||
cloneStep({
|
cloneStep({
|
||||||
@ -216,7 +221,9 @@ export function countAgentWorkflowSteps(steps: AgentWorkflowStepUiConfig[]): num
|
|||||||
|
|
||||||
export function countAgentWorkflowOptions(steps: AgentWorkflowStepUiConfig[]): number {
|
export function countAgentWorkflowOptions(steps: AgentWorkflowStepUiConfig[]): number {
|
||||||
return flattenAgentWorkflowSteps(steps).reduce(
|
return flattenAgentWorkflowSteps(steps).reduce(
|
||||||
(total, item) => total + (item.step.options?.length || 0),
|
(total, item) =>
|
||||||
|
total +
|
||||||
|
flattenAgentWorkflowOptionTree(item.step.options || []).length,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -227,10 +234,7 @@ export function normalizeAgentWorkflowStepOrdering(
|
|||||||
return steps.map((step, stepIndex) => ({
|
return steps.map((step, stepIndex) => ({
|
||||||
...step,
|
...step,
|
||||||
sortOrder: stepIndex,
|
sortOrder: stepIndex,
|
||||||
options: (step.options || []).map((option, optionIndex) => ({
|
options: normalizeAgentWorkflowOptionOrdering(step.options || []),
|
||||||
...option,
|
|
||||||
sortOrder: optionIndex,
|
|
||||||
})),
|
|
||||||
children: normalizeAgentWorkflowStepOrdering(step.children || []),
|
children: normalizeAgentWorkflowStepOrdering(step.children || []),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -243,14 +247,81 @@ export function getEnabledAgentWorkflowStepTree(
|
|||||||
.filter((step) => step.enabled !== false)
|
.filter((step) => step.enabled !== false)
|
||||||
.map((step) => ({
|
.map((step) => ({
|
||||||
...step,
|
...step,
|
||||||
options: [...(step.options || [])]
|
options: getEnabledAgentWorkflowOptionTree(step.options || []),
|
||||||
.filter((option) => option.enabled !== false)
|
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder),
|
|
||||||
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
children: getEnabledAgentWorkflowStepTree(step.children || []),
|
||||||
}))
|
}))
|
||||||
.filter((step) => step.options.length > 0 || (step.children || []).length > 0);
|
.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) {
|
function normalizeText(value: unknown, maxLength: number) {
|
||||||
return String(value || "").trim().slice(0, maxLength);
|
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 && typeof raw.inputOptions === "object" && !Array.isArray(raw.inputOptions)
|
||||||
? (raw.inputOptions as Record<string, unknown>)
|
? (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(
|
const defaultStepByKey = new Map(
|
||||||
flattenAgentWorkflowSteps(defaultAgentWorkflowUiConfig.steps).map(({ step }) => [
|
flattenAgentWorkflowSteps(defaultAgentWorkflowUiConfig.steps).map(({ step }) => [
|
||||||
step.key,
|
step.key,
|
||||||
@ -308,6 +419,82 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
|||||||
const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
const sourceSteps = hasRawSteps ? rawSteps : (defaultAgentWorkflowUiConfig.steps as unknown[]);
|
||||||
const usedStepKeys = new Set<string>();
|
const usedStepKeys = new Set<string>();
|
||||||
|
|
||||||
|
const 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 = (
|
const normalizeStep = (
|
||||||
item: unknown,
|
item: unknown,
|
||||||
stepIndex: number,
|
stepIndex: number,
|
||||||
@ -336,49 +523,9 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
|||||||
const usedOptionKeys = new Set<string>();
|
const usedOptionKeys = new Set<string>();
|
||||||
|
|
||||||
const options = optionSource
|
const options = optionSource
|
||||||
.map((option, optionIndex) => {
|
.map((option, optionIndex) =>
|
||||||
const optionRecord =
|
normalizeOption(option, optionIndex, stepKey, usedOptionKeys, defaultOptions, 1)
|
||||||
option && typeof option === "object" && !Array.isArray(option)
|
)
|
||||||
? (option as Record<string, unknown>)
|
|
||||||
: null;
|
|
||||||
const defaultOption = defaultStep?.options.find(
|
|
||||||
(defaultItem) => defaultItem.key === normalizeText(optionRecord?.key, 64)
|
|
||||||
);
|
|
||||||
const optionBaseKey = normalizeKey(
|
|
||||||
optionRecord?.key,
|
|
||||||
defaultOption?.key || `${stepKey}_option_${optionIndex + 1}`
|
|
||||||
);
|
|
||||||
let optionKey = optionBaseKey;
|
|
||||||
let optionDuplicateIndex = 2;
|
|
||||||
while (usedOptionKeys.has(optionKey)) {
|
|
||||||
optionKey = `${optionBaseKey}_${optionDuplicateIndex}`;
|
|
||||||
optionDuplicateIndex += 1;
|
|
||||||
}
|
|
||||||
usedOptionKeys.add(optionKey);
|
|
||||||
|
|
||||||
const buttonName =
|
|
||||||
normalizeText(optionRecord?.buttonName, 64) ||
|
|
||||||
defaultOption?.buttonName ||
|
|
||||||
`选项${optionIndex + 1}`;
|
|
||||||
const promptFragment =
|
|
||||||
normalizeText(optionRecord?.promptFragment, 1000) ||
|
|
||||||
defaultOption?.promptFragment ||
|
|
||||||
buttonName;
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: optionKey,
|
|
||||||
buttonName,
|
|
||||||
description:
|
|
||||||
normalizeText(optionRecord?.description, 300) || defaultOption?.description || "",
|
|
||||||
previewImageUrl: normalizeText(optionRecord?.previewImageUrl, 1000),
|
|
||||||
enabled: normalizeBoolean(optionRecord?.enabled, defaultOption?.enabled ?? true),
|
|
||||||
sortOrder: normalizeNumber(
|
|
||||||
optionRecord?.sortOrder,
|
|
||||||
defaultOption?.sortOrder ?? optionIndex
|
|
||||||
),
|
|
||||||
promptFragment,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.sort((left, right) => left.sortOrder - right.sortOrder);
|
.sort((left, right) => left.sortOrder - right.sortOrder);
|
||||||
|
|
||||||
const hasRawChildren = Array.isArray(stepRecord?.children);
|
const hasRawChildren = Array.isArray(stepRecord?.children);
|
||||||
@ -414,9 +561,82 @@ export function normalizeAgentWorkflowUiConfig(value: unknown): AgentWorkflowUiC
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
inputOptions: {
|
inputOptions: {
|
||||||
brands: normalizeStringList(rawInputOptions.brands),
|
brands:
|
||||||
stores: normalizeStringList(rawInputOptions.stores),
|
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,
|
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;
|
secondaryType?: string | null;
|
||||||
visibility: AgentPresetVisibility;
|
visibility: AgentPresetVisibility;
|
||||||
categoryPath?: {
|
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;
|
productCategory?: string | null;
|
||||||
audienceTag?: string | null;
|
audienceTag?: string | null;
|
||||||
usageScenario?: string | null;
|
usageScenario?: string | null;
|
||||||
@ -2613,6 +2621,14 @@ export async function updateAgentPreset(params: {
|
|||||||
secondaryType?: string | null;
|
secondaryType?: string | null;
|
||||||
visibility?: AgentPresetVisibility;
|
visibility?: AgentPresetVisibility;
|
||||||
categoryPath?: {
|
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;
|
productCategory?: string | null;
|
||||||
audienceTag?: string | null;
|
audienceTag?: string | null;
|
||||||
usageScenario?: string | null;
|
usageScenario?: string | null;
|
||||||
|
|||||||
@ -532,6 +532,14 @@ export const agentPreset = pgTable("AgentPreset", {
|
|||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
categoryPath: jsonb("categoryPath").$type<{
|
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;
|
productCategory?: string | null;
|
||||||
audienceTag?: string | null;
|
audienceTag?: string | null;
|
||||||
usageScenario?: string | null;
|
usageScenario?: string | null;
|
||||||
|
|||||||
@ -35,12 +35,41 @@ export interface AgentPresetModeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentCategoryPath {
|
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;
|
productCategory?: string | null;
|
||||||
audienceTag?: string | null;
|
audienceTag?: string | null;
|
||||||
usageScenario?: string | null;
|
usageScenario?: string | null;
|
||||||
brand?: 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 type AgentWorkflowStepKey = string;
|
||||||
|
|
||||||
export interface AgentWorkflowOptionUiConfig {
|
export interface AgentWorkflowOptionUiConfig {
|
||||||
@ -51,6 +80,7 @@ export interface AgentWorkflowOptionUiConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
promptFragment?: string | null;
|
promptFragment?: string | null;
|
||||||
|
children?: AgentWorkflowOptionUiConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AgentWorkflowStepUiConfig {
|
export interface AgentWorkflowStepUiConfig {
|
||||||
@ -67,10 +97,17 @@ export interface AgentWorkflowUiConfig {
|
|||||||
inputOptions?: {
|
inputOptions?: {
|
||||||
brands: string[];
|
brands: string[];
|
||||||
stores: string[];
|
stores: string[];
|
||||||
|
brandStoreEntries?: AgentWorkflowBrandStoreEntry[];
|
||||||
};
|
};
|
||||||
steps: AgentWorkflowStepUiConfig[];
|
steps: AgentWorkflowStepUiConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AgentWorkflowBrandStoreEntry {
|
||||||
|
brandName: string;
|
||||||
|
stores: string[];
|
||||||
|
sortOrder?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentWorkflowMaterialApiConfig {
|
export interface AgentWorkflowMaterialApiConfig {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user