feat: support configurable agent reference sources
This commit is contained in:
parent
8d602bf543
commit
da3f4fad64
@ -17,7 +17,7 @@ export const maxDuration = 180;
|
||||
const querySchema = z.object({
|
||||
keyword: z.string().trim().min(1).max(64),
|
||||
queryType: z.enum(["sku", "spu"]).default("spu"),
|
||||
target: z.enum(["detail", "sku"]),
|
||||
target: z.enum(["main", "detail", "sku"]),
|
||||
});
|
||||
|
||||
type ExternalTaskDetailCandidate = {
|
||||
@ -116,17 +116,23 @@ function normalizeCandidate(
|
||||
|
||||
function getCandidateTargetUrls(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
target: "detail" | "sku"
|
||||
target: "main" | "detail" | "sku"
|
||||
) {
|
||||
if (target === "main") return candidate.mainImageUrls;
|
||||
return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls;
|
||||
}
|
||||
|
||||
function buildReferenceItems(params: {
|
||||
candidate: AgentModeReferenceCandidate;
|
||||
target: "detail" | "sku";
|
||||
target: "main" | "detail" | "sku";
|
||||
}): AgentDetailReplacementReferenceItem[] {
|
||||
const urls = getCandidateTargetUrls(params.candidate, params.target);
|
||||
const targetLabel = params.target === "detail" ? "详情参考图" : "SKU参考图";
|
||||
const targetLabel =
|
||||
params.target === "main"
|
||||
? "主图参考图"
|
||||
: params.target === "detail"
|
||||
? "详情参考图"
|
||||
: "SKU参考图";
|
||||
const prefix =
|
||||
params.candidate.sceneName ||
|
||||
params.candidate.taskNo ||
|
||||
@ -222,11 +228,15 @@ export async function GET(request: NextRequest) {
|
||||
success: false,
|
||||
error: {
|
||||
code:
|
||||
parsed.target === "detail"
|
||||
parsed.target === "main"
|
||||
? "MAIN_REFERENCE_NOT_FOUND"
|
||||
: parsed.target === "detail"
|
||||
? "DETAIL_REFERENCE_NOT_FOUND"
|
||||
: "SKU_REFERENCE_NOT_FOUND",
|
||||
message:
|
||||
parsed.target === "detail"
|
||||
parsed.target === "main"
|
||||
? "当前整套素材中没有可用的主图参考图"
|
||||
: parsed.target === "detail"
|
||||
? "当前整套素材中没有可用的详情参考图"
|
||||
: "当前整套素材中没有可用的 SKU 参考图",
|
||||
},
|
||||
|
||||
@ -54,6 +54,7 @@ import type {
|
||||
AgentModeReferenceCandidate,
|
||||
AgentModeWorkspaceState,
|
||||
AgentPreset,
|
||||
AgentTaskDetailReferenceSource,
|
||||
AgentWorkflowShowcaseConfig,
|
||||
AgentWorkflowComposerContext,
|
||||
AgentWorkflowMaterialQueryType,
|
||||
@ -107,7 +108,7 @@ interface AgentModeReferenceSearchResponse {
|
||||
data?: {
|
||||
query: string;
|
||||
queryType: AgentWorkflowMaterialQueryType;
|
||||
target: "detail" | "sku";
|
||||
target: AgentTaskDetailReferenceSource;
|
||||
matchedSpu?: string | null;
|
||||
matchedSku?: string | null;
|
||||
categoryName?: string | null;
|
||||
@ -123,6 +124,10 @@ interface AgentModeReferenceSearchResponse {
|
||||
};
|
||||
}
|
||||
|
||||
function getTaskDetailReferenceSourceLabel(source: AgentTaskDetailReferenceSource) {
|
||||
return source === "main" ? "主图" : source === "sku" ? "SKU图" : "详情图";
|
||||
}
|
||||
|
||||
function areImageGenerationParamsEqual(
|
||||
left: ImageGenerationParams,
|
||||
right: ImageGenerationParams
|
||||
@ -237,15 +242,20 @@ function getImageDimensions(url: string): Promise<{
|
||||
|
||||
function buildReferenceCardsFromCandidate(params: {
|
||||
candidate: AgentModeReferenceCandidate | null | undefined;
|
||||
target: "detail" | "sku";
|
||||
target: AgentTaskDetailReferenceSource;
|
||||
productCount?: number;
|
||||
assignProductIndex?: boolean;
|
||||
}) {
|
||||
const candidate = params.candidate;
|
||||
if (!candidate) return [] as AgentModeWorkspaceState["detailReplacement"]["references"];
|
||||
|
||||
const urls =
|
||||
params.target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || [];
|
||||
const targetLabel = params.target === "detail" ? "详情参考图" : "SKU参考图";
|
||||
params.target === "main"
|
||||
? candidate.mainImageUrls || []
|
||||
: params.target === "detail"
|
||||
? candidate.detailImageUrls || []
|
||||
: candidate.skuImageUrls || [];
|
||||
const targetLabel = `${getTaskDetailReferenceSourceLabel(params.target)}参考图`;
|
||||
const prefix =
|
||||
candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套";
|
||||
|
||||
@ -256,17 +266,19 @@ function buildReferenceCardsFromCandidate(params: {
|
||||
candidateId: candidate.id,
|
||||
sortOrder: index,
|
||||
assignedProductIndex:
|
||||
params.target === "sku" && (params.productCount || 0) > 0
|
||||
params.assignProductIndex === true && (params.productCount || 0) > 0
|
||||
? Math.min(index, Math.max((params.productCount || 1) - 1, 0))
|
||||
: null,
|
||||
}));
|
||||
}
|
||||
|
||||
function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: "detail" | "sku") {
|
||||
function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) {
|
||||
const imageCount =
|
||||
target === "detail"
|
||||
? candidate.detailImageUrls.length
|
||||
: candidate.skuImageUrls.length;
|
||||
target === "main"
|
||||
? candidate.mainImageUrls.length
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls.length
|
||||
: candidate.skuImageUrls.length;
|
||||
const uploadedAt = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null;
|
||||
const timeText =
|
||||
uploadedAt && !Number.isNaN(uploadedAt.getTime())
|
||||
@ -289,10 +301,14 @@ function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: "de
|
||||
|
||||
function getCandidatePreviewUrl(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
target: "detail" | "sku"
|
||||
target: AgentTaskDetailReferenceSource
|
||||
) {
|
||||
const targetUrls =
|
||||
target === "detail" ? candidate.detailImageUrls || [] : candidate.skuImageUrls || [];
|
||||
target === "main"
|
||||
? candidate.mainImageUrls || []
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls || []
|
||||
: candidate.skuImageUrls || [];
|
||||
return (
|
||||
targetUrls[0] ||
|
||||
candidate.mainImageUrls[0] ||
|
||||
@ -301,6 +317,17 @@ function getCandidatePreviewUrl(
|
||||
);
|
||||
}
|
||||
|
||||
function getCandidateTargetCount(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
target: AgentTaskDetailReferenceSource
|
||||
) {
|
||||
return target === "main"
|
||||
? candidate.mainImageUrls.length
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls.length
|
||||
: candidate.skuImageUrls.length;
|
||||
}
|
||||
|
||||
type ReplacementCardStatus = "waiting" | "queued" | "generating" | "completed" | "missing";
|
||||
|
||||
function getReplacementCardStatus(params: {
|
||||
@ -535,6 +562,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
fallback: modelSlotEnabled ? 3 : productSlotCount,
|
||||
})
|
||||
: 0;
|
||||
const detailReferenceTarget = presetModeConfig.detail.referenceImageSource;
|
||||
const skuReferenceTarget = presetModeConfig.sku.referenceImageSource;
|
||||
const detailReferenceSourceLabel = getTaskDetailReferenceSourceLabel(detailReferenceTarget);
|
||||
const skuReferenceSourceLabel = getTaskDetailReferenceSourceLabel(skuReferenceTarget);
|
||||
const shouldAllowModelOverride = !isSceneGenerationPreset || presetModeConfig.scene.allowModelOverride;
|
||||
const selectedProductAttachments = attachments.filter((item, index) =>
|
||||
modelSlotIndex >= 0 ? index !== modelSlotIndex && Boolean(item) : Boolean(item)
|
||||
@ -1274,11 +1305,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
return;
|
||||
}
|
||||
if (isDetailReplacementPreset && detailReplacementReferenceCards.length === 0) {
|
||||
toast.error("请先查询并选择详情参考图");
|
||||
toast.error(`请先查询并选择${detailReferenceSourceLabel}参考图`);
|
||||
return;
|
||||
}
|
||||
if (isSkuReplacementPreset && skuReplacementReferenceCards.length === 0) {
|
||||
toast.error("请先查询并选择 SKU 参考图");
|
||||
toast.error(`请先查询并选择${skuReferenceSourceLabel}参考图`);
|
||||
return;
|
||||
}
|
||||
const uploadedAttachmentCount = attachments.filter(Boolean).length;
|
||||
@ -1431,7 +1462,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const handleDetailReplacementSearch = useCallback(async () => {
|
||||
const normalized = detailReplacementQueryInput.trim().toUpperCase();
|
||||
if (!normalized) {
|
||||
toast.error("请输入 SPU 后再查询详情图");
|
||||
toast.error(`请输入 SPU 后再查询${detailReferenceSourceLabel}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1444,13 +1475,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const response = await fetch(
|
||||
`/api/agent-workflow/reference-search?keyword=${encodeURIComponent(
|
||||
normalized
|
||||
)}&queryType=spu&target=detail`,
|
||||
)}&queryType=spu&target=${detailReferenceTarget}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
const result = (await response.json().catch(() => ({}))) as AgentModeReferenceSearchResponse;
|
||||
|
||||
if (!response.ok || !result?.success || !result?.data) {
|
||||
throw new Error(result?.error?.message || "详情参考图查询失败");
|
||||
throw new Error(result?.error?.message || `${detailReferenceSourceLabel}参考图查询失败`);
|
||||
}
|
||||
|
||||
updateDetailReplacementState({
|
||||
@ -1465,15 +1496,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
isLoading: false,
|
||||
lastQuery: normalized,
|
||||
});
|
||||
toast.success(`已载入 ${result.data.references?.length || 0} 张详情参考图`);
|
||||
toast.success(`已载入 ${result.data.references?.length || 0} 张${detailReferenceSourceLabel}参考图`);
|
||||
} catch (error) {
|
||||
setDetailReplacementSearchState({
|
||||
isLoading: false,
|
||||
lastQuery: normalized,
|
||||
});
|
||||
toast.error(error instanceof Error ? error.message : "详情参考图查询失败");
|
||||
toast.error(error instanceof Error ? error.message : `${detailReferenceSourceLabel}参考图查询失败`);
|
||||
}
|
||||
}, [detailReplacementQueryInput, updateDetailReplacementState]);
|
||||
}, [detailReferenceSourceLabel, detailReferenceTarget, detailReplacementQueryInput, updateDetailReplacementState]);
|
||||
const handleSelectDetailReplacementCandidate = useCallback((candidateId: string) => {
|
||||
updateDetailReplacementState((current) => {
|
||||
const nextCandidate =
|
||||
@ -1483,12 +1514,12 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
selectedCandidateId: candidateId,
|
||||
references: buildReferenceCardsFromCandidate({
|
||||
candidate: nextCandidate,
|
||||
target: "detail",
|
||||
target: detailReferenceTarget,
|
||||
productCount: selectedProductAttachments.length,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [selectedProductAttachments.length, updateDetailReplacementState]);
|
||||
}, [detailReferenceTarget, selectedProductAttachments.length, updateDetailReplacementState]);
|
||||
const handleRemoveDetailReferenceCard = useCallback((cardId: string) => {
|
||||
updateDetailReplacementState((current) => ({
|
||||
...current,
|
||||
@ -1506,7 +1537,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const handleSkuReplacementSearch = useCallback(async () => {
|
||||
const normalized = skuReplacementQueryInput.trim().toUpperCase();
|
||||
if (!normalized) {
|
||||
toast.error(`请输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"}后再查询 SKU 参考图`);
|
||||
toast.error(`请输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"}后再查询${skuReferenceSourceLabel}参考图`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1519,13 +1550,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const response = await fetch(
|
||||
`/api/agent-workflow/reference-search?keyword=${encodeURIComponent(
|
||||
normalized
|
||||
)}&queryType=${skuReplacementQueryType}&target=sku`,
|
||||
)}&queryType=${skuReplacementQueryType}&target=${skuReferenceTarget}`,
|
||||
{ cache: "no-store" }
|
||||
);
|
||||
const result = (await response.json().catch(() => ({}))) as AgentModeReferenceSearchResponse;
|
||||
|
||||
if (!response.ok || !result?.success || !result?.data) {
|
||||
throw new Error(result?.error?.message || "SKU 参考图查询失败");
|
||||
throw new Error(result?.error?.message || `${skuReferenceSourceLabel}参考图查询失败`);
|
||||
}
|
||||
|
||||
updateSkuReplacementState({
|
||||
@ -1547,16 +1578,18 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
isLoading: false,
|
||||
lastQuery: normalized,
|
||||
});
|
||||
toast.success(`已载入 ${result.data.references?.length || 0} 张 SKU 参考图`);
|
||||
toast.success(`已载入 ${result.data.references?.length || 0} 张${skuReferenceSourceLabel}参考图`);
|
||||
} catch (error) {
|
||||
setSkuReplacementSearchState({
|
||||
isLoading: false,
|
||||
lastQuery: normalized,
|
||||
});
|
||||
toast.error(error instanceof Error ? error.message : "SKU 参考图查询失败");
|
||||
toast.error(error instanceof Error ? error.message : `${skuReferenceSourceLabel}参考图查询失败`);
|
||||
}
|
||||
}, [
|
||||
selectedProductAttachments.length,
|
||||
skuReferenceSourceLabel,
|
||||
skuReferenceTarget,
|
||||
skuReplacementQueryInput,
|
||||
skuReplacementQueryType,
|
||||
updateSkuReplacementState,
|
||||
@ -1570,12 +1603,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
selectedCandidateId: candidateId,
|
||||
references: buildReferenceCardsFromCandidate({
|
||||
candidate: nextCandidate,
|
||||
target: "sku",
|
||||
target: skuReferenceTarget,
|
||||
productCount: selectedProductAttachments.length,
|
||||
assignProductIndex: true,
|
||||
}),
|
||||
};
|
||||
});
|
||||
}, [selectedProductAttachments.length, updateSkuReplacementState]);
|
||||
}, [selectedProductAttachments.length, skuReferenceTarget, updateSkuReplacementState]);
|
||||
const handleAssignSkuReferenceProduct = useCallback((cardId: string, productIndex: number) => {
|
||||
updateSkuReplacementState((current) => ({
|
||||
...current,
|
||||
@ -2191,9 +2225,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="min-h-0 border-b border-white/10 p-4 xl:border-b-0 xl:border-r">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">详情参考图</div>
|
||||
<div className="text-sm font-semibold text-white">{detailReferenceSourceLabel}参考图</div>
|
||||
<div className="text-xs text-white/40">
|
||||
仅支持按 SPU 查询任务-详情整套候选,并切换某一套的详情参考图。
|
||||
仅支持按 SPU 查询任务整套候选,并切换某一套的{detailReferenceSourceLabel}参考图。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
|
||||
@ -2216,7 +2250,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
queryType: "spu",
|
||||
})
|
||||
}
|
||||
placeholder="输入 SPU,查询任务-详情整套中的详情参考图"
|
||||
placeholder={`输入 SPU,查询任务整套中的${detailReferenceSourceLabel}参考图`}
|
||||
className="min-h-[68px] resize-none rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
|
||||
/>
|
||||
<Button
|
||||
@ -2231,7 +2265,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
查询中
|
||||
</>
|
||||
) : (
|
||||
"查询详情图"
|
||||
`查询${detailReferenceSourceLabel}`
|
||||
)}
|
||||
</Button>
|
||||
{detailReplacementReferenceCards.length > 0 ? (
|
||||
@ -2253,7 +2287,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{detailReplacementSearchState.lastQuery ||
|
||||
detailReplacementQueryInput.trim().toUpperCase()}
|
||||
<span className="ml-2 text-emerald-200/70">
|
||||
已接入真实详情参考图接口
|
||||
{`已接入真实${detailReferenceSourceLabel}参考图接口`}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
@ -2271,7 +2305,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="agent-dark-scrollbar flex gap-2 overflow-x-auto pb-1">
|
||||
{detailReplacementCandidates.map((candidate) => {
|
||||
const isSelected = detailReplacementSelectedCandidateId === candidate.id;
|
||||
const previewUrl = getCandidatePreviewUrl(candidate, "detail");
|
||||
const previewUrl = getCandidatePreviewUrl(candidate, detailReferenceTarget);
|
||||
return (
|
||||
<button
|
||||
key={candidate.id}
|
||||
@ -2293,7 +2327,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute left-3 top-3 rounded-full border border-white/10 bg-black/45 px-2.5 py-1 text-[11px] text-white/78">
|
||||
{candidate.detailImageUrls.length} 张详情图
|
||||
{getCandidateTargetCount(candidate, detailReferenceTarget)} 张{detailReferenceSourceLabel}
|
||||
</div>
|
||||
{isSelected ? (
|
||||
<div className="absolute right-3 top-3 rounded-full border border-emerald-300/35 bg-emerald-500/[0.14] px-2.5 py-1 text-[11px] text-emerald-100">
|
||||
@ -2306,7 +2340,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
|
||||
</div>
|
||||
<div className="line-clamp-2 text-[11px] text-white/42">
|
||||
{formatCandidateMeta(candidate, "detail") || "点击切换这套详情参考图"}
|
||||
{formatCandidateMeta(candidate, detailReferenceTarget) || `点击切换这套${detailReferenceSourceLabel}参考图`}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 text-[11px] text-white/48">
|
||||
{candidate.shopName ? (
|
||||
@ -2370,7 +2404,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent px-4 py-3">
|
||||
<div className="text-sm font-medium text-white/88">{card.label}</div>
|
||||
<div className="text-xs text-white/45">
|
||||
第 {index + 1} 张详情参考图
|
||||
第 {index + 1} 张{detailReferenceSourceLabel}参考图
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2383,9 +2417,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
))
|
||||
) : (
|
||||
<div className="flex min-h-[220px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-5 text-center">
|
||||
<div className="text-sm font-medium text-white/72">等待详情参考图</div>
|
||||
<div className="text-sm font-medium text-white/72">等待{detailReferenceSourceLabel}参考图</div>
|
||||
<div className="mt-2 text-xs leading-6 text-white/40">
|
||||
输入 SPU 后,这里会展示任务-详情整套里的真实详情参考图。
|
||||
输入 SPU 后,这里会展示任务整套里的真实{detailReferenceSourceLabel}参考图。
|
||||
删除不需要的参考图后,列表会自动上移补位。
|
||||
</div>
|
||||
</div>
|
||||
@ -2398,7 +2432,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">替换结果</div>
|
||||
<div className="text-xs text-white/40">
|
||||
每张详情参考图将对应生成一张替换结果,并支持逐屏文字编辑。
|
||||
每张{detailReferenceSourceLabel}参考图将对应生成一张替换结果,并支持逐屏文字编辑。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-emerald-200/80">
|
||||
@ -2476,7 +2510,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
结果图 {card.index + 1}
|
||||
</div>
|
||||
<div className="text-xs text-white/35">
|
||||
将按“输入图组 + 第 {card.index + 1} 张详情参考图”生成
|
||||
将按“输入图组 + 第 {card.index + 1} 张{detailReferenceSourceLabel}参考图”生成
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2500,7 +2534,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="flex min-h-[220px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-5 text-center">
|
||||
<div className="text-sm font-medium text-white/72">等待替换结果</div>
|
||||
<div className="mt-2 text-xs leading-6 text-white/40">
|
||||
详情参考图加载后,右侧会按相同顺序生成对应结果图,并逐屏支持文字编辑。
|
||||
{detailReferenceSourceLabel}参考图加载后,右侧会按相同顺序生成对应结果图,并逐屏支持文字编辑。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -2514,7 +2548,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">输入图</div>
|
||||
<div className="text-xs text-white/40">
|
||||
选中的白底图会和 SKU 参考图建立一对一替换关系
|
||||
选中的白底图会和{skuReferenceSourceLabel}参考图建立一对一替换关系
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
|
||||
@ -2566,9 +2600,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="min-h-0 border-b border-white/10 p-4 xl:border-b-0 xl:border-r">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">SKU 参考图</div>
|
||||
<div className="text-sm font-semibold text-white">{skuReferenceSourceLabel}参考图</div>
|
||||
<div className="text-xs text-white/40">
|
||||
支持按 SKU 或 SPU 查询任务-详情整套候选,并切换对应的 SKU 参考图。
|
||||
支持按 SKU 或 SPU 查询任务整套候选,并切换对应的{skuReferenceSourceLabel}参考图。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-white/55">
|
||||
@ -2613,7 +2647,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
query: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={`输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"},查询任务-详情整套中的 SKU 参考图`}
|
||||
placeholder={`输入${skuReplacementQueryType === "spu" ? "SPU" : "SKU"},查询任务整套中的${skuReferenceSourceLabel}参考图`}
|
||||
className="min-h-[68px] resize-none rounded-[22px] border-white/10 bg-black/20 text-sm text-white placeholder:text-white/22 focus:border-emerald-400/30"
|
||||
/>
|
||||
<Button
|
||||
@ -2628,7 +2662,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
查询中
|
||||
</>
|
||||
) : (
|
||||
"查询 SKU 图"
|
||||
`查询${skuReferenceSourceLabel}`
|
||||
)}
|
||||
</Button>
|
||||
{skuReplacementReferenceCards.length > 0 ? (
|
||||
@ -2650,7 +2684,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{skuReplacementSearchState.lastQuery ||
|
||||
skuReplacementQueryInput.trim().toUpperCase()}
|
||||
<span className="ml-2 text-emerald-200/70">
|
||||
已接入真实 SKU 参考图接口
|
||||
{`已接入真实${skuReferenceSourceLabel}参考图接口`}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
@ -2668,7 +2702,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="agent-dark-scrollbar flex gap-2 overflow-x-auto pb-1">
|
||||
{skuReplacementCandidates.map((candidate) => {
|
||||
const isSelected = skuReplacementSelectedCandidateId === candidate.id;
|
||||
const previewUrl = getCandidatePreviewUrl(candidate, "sku");
|
||||
const previewUrl = getCandidatePreviewUrl(candidate, skuReferenceTarget);
|
||||
return (
|
||||
<button
|
||||
key={candidate.id}
|
||||
@ -2690,7 +2724,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute left-3 top-3 rounded-full border border-white/10 bg-black/45 px-2.5 py-1 text-[11px] text-white/78">
|
||||
{candidate.skuImageUrls.length} 张SKU图
|
||||
{getCandidateTargetCount(candidate, skuReferenceTarget)} 张{skuReferenceSourceLabel}
|
||||
</div>
|
||||
{isSelected ? (
|
||||
<div className="absolute right-3 top-3 rounded-full border border-emerald-300/35 bg-emerald-500/[0.14] px-2.5 py-1 text-[11px] text-emerald-100">
|
||||
@ -2703,7 +2737,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
{candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套"}
|
||||
</div>
|
||||
<div className="mt-1 line-clamp-2 text-[11px] text-white/42">
|
||||
{formatCandidateMeta(candidate, "sku") || "点击切换这套 SKU 参考图"}
|
||||
{formatCandidateMeta(candidate, skuReferenceTarget) || `点击切换这套${skuReferenceSourceLabel}参考图`}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 text-[11px] text-white/48">
|
||||
{candidate.shopName ? (
|
||||
@ -2776,7 +2810,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/85 via-black/35 to-transparent px-4 py-3">
|
||||
<div className="text-sm font-medium text-white/88">{card.label}</div>
|
||||
<div className="text-xs text-white/45">
|
||||
第 {index + 1} 张 SKU 参考图
|
||||
第 {index + 1} 张{skuReferenceSourceLabel}参考图
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2821,9 +2855,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
))
|
||||
) : (
|
||||
<div className="flex min-h-[220px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-5 text-center">
|
||||
<div className="text-sm font-medium text-white/72">等待 SKU 参考图</div>
|
||||
<div className="text-sm font-medium text-white/72">等待{skuReferenceSourceLabel}参考图</div>
|
||||
<div className="mt-2 text-xs leading-6 text-white/40">
|
||||
输入 SKU 或 SPU 后,这里会展示任务-详情整套里的真实 SKU 参考图。
|
||||
输入 SKU 或 SPU 后,这里会展示任务整套里的真实{skuReferenceSourceLabel}参考图。
|
||||
选择多少张图,就会按相同数量进入替换流程。
|
||||
</div>
|
||||
</div>
|
||||
@ -2836,7 +2870,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">替换结果</div>
|
||||
<div className="text-xs text-white/40">
|
||||
每张 SKU 参考图会对应一张结果图,形成一对一批量替换结果。
|
||||
每张{skuReferenceSourceLabel}参考图会对应一张结果图,形成一对一批量替换结果。
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-emerald-200/80">
|
||||
@ -2907,7 +2941,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
结果图 {card.index + 1}
|
||||
</div>
|
||||
<div className="text-xs text-white/35">
|
||||
将按“输入图 + 第 {card.index + 1} 张 SKU 参考图”生成
|
||||
将按“输入图 + 第 {card.index + 1} 张{skuReferenceSourceLabel}参考图”生成
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -2930,7 +2964,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<div className="flex min-h-[220px] flex-col items-center justify-center rounded-[22px] border border-dashed border-white/12 bg-black/20 px-5 text-center">
|
||||
<div className="text-sm font-medium text-white/72">等待替换结果</div>
|
||||
<div className="mt-2 text-xs leading-6 text-white/40">
|
||||
SKU 参考图加载后,右侧会按相同顺序生成一对一替换结果,并支持批量下载。
|
||||
{skuReferenceSourceLabel}参考图加载后,右侧会按相同顺序生成一对一替换结果,并支持批量下载。
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -44,6 +44,7 @@ import type {
|
||||
AgentCreatePayload,
|
||||
AgentPresetModeType,
|
||||
AgentPresetModeConfig,
|
||||
AgentTaskDetailReferenceSource,
|
||||
AgentPresetType,
|
||||
AgentPreset,
|
||||
AgentPresetSlot,
|
||||
@ -102,6 +103,28 @@ const IMAGE_MODE_OPTIONS: Array<{
|
||||
},
|
||||
];
|
||||
|
||||
const TASK_DETAIL_REFERENCE_SOURCE_OPTIONS: Array<{
|
||||
value: AgentTaskDetailReferenceSource;
|
||||
label: string;
|
||||
description: string;
|
||||
}> = [
|
||||
{
|
||||
value: "main",
|
||||
label: "主图",
|
||||
description: "获取任务整套里的主图作为参考图来源。",
|
||||
},
|
||||
{
|
||||
value: "sku",
|
||||
label: "SKU图",
|
||||
description: "获取任务整套里的 SKU 图作为参考图来源。",
|
||||
},
|
||||
{
|
||||
value: "detail",
|
||||
label: "详情图",
|
||||
description: "获取任务整套里的详情图作为参考图来源。",
|
||||
},
|
||||
];
|
||||
|
||||
export function AgentPresetDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@ -1209,6 +1232,69 @@ export function AgentPresetDialog({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>
|
||||
参考图来源
|
||||
</div>
|
||||
<Select
|
||||
value={
|
||||
isDetailReplacementModeBuilder
|
||||
? modeConfig.detail.referenceImageSource
|
||||
: modeConfig.sku.referenceImageSource
|
||||
}
|
||||
onValueChange={(value) =>
|
||||
setModeConfig((prev) => ({
|
||||
...prev,
|
||||
modeType: isDetailReplacementModeBuilder
|
||||
? "detail_replacement"
|
||||
: "sku_replacement",
|
||||
detail: {
|
||||
...prev.detail,
|
||||
referenceImageSource: isDetailReplacementModeBuilder
|
||||
? (value as AgentTaskDetailReferenceSource)
|
||||
: prev.detail.referenceImageSource,
|
||||
},
|
||||
sku: {
|
||||
...prev.sku,
|
||||
referenceImageSource: isSkuReplacementModeBuilder
|
||||
? (value as AgentTaskDetailReferenceSource)
|
||||
: prev.sku.referenceImageSource,
|
||||
},
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
isDark
|
||||
? "border-white/10 bg-black/30 text-white focus:ring-emerald-400/35"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className={darkSelectContentClass}>
|
||||
{TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={cn(isDark ? "text-white focus:bg-white/10 focus:text-white" : "")}
|
||||
>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className={cn("text-xs leading-5", isDark ? "text-white/42" : "text-muted-foreground")}>
|
||||
{TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.find(
|
||||
(option) =>
|
||||
option.value ===
|
||||
(isDetailReplacementModeBuilder
|
||||
? modeConfig.detail.referenceImageSource
|
||||
: modeConfig.sku.referenceImageSource)
|
||||
)?.description || "根据模式配置自动获取对应参考图"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2.5">
|
||||
<div>
|
||||
<div className={cn("text-sm font-medium", isDark ? "text-white/88" : "")}>模特图槽位</div>
|
||||
@ -1284,8 +1370,8 @@ export function AgentPresetDialog({
|
||||
|
||||
<div className={cn("rounded-xl border border-dashed px-3 py-3 text-xs", isDark ? "border-white/10 text-white/45" : "text-muted-foreground")}>
|
||||
{isDetailReplacementModeBuilder
|
||||
? "详情替换模式不需要上传模板参考图,第二步会通过 SPU 获取详情参考图,再把输入图逐张替换到详情参考图中。"
|
||||
: "SKU替换模式会把用户选中的白底图与参考图建立一对一关系,后续在第二步专用工作台中执行替换。"}
|
||||
? `详情替换模式不需要上传模板参考图,第二步会通过 SPU 获取${TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.find((option) => option.value === modeConfig.detail.referenceImageSource)?.label || "详情图"},再把输入图逐张替换到对应参考图中。`
|
||||
: `SKU替换模式会把用户选中的白底图与${TASK_DETAIL_REFERENCE_SOURCE_OPTIONS.find((option) => option.value === modeConfig.sku.referenceImageSource)?.label || "SKU图"}建立一对一关系,后续在第二步专用工作台中执行替换。`}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type {
|
||||
AgentPresetModeConfig,
|
||||
AgentPresetModeType,
|
||||
AgentTaskDetailReferenceSource,
|
||||
} from "@/types/agent";
|
||||
|
||||
export const defaultAgentSceneGenerationModeConfig = {
|
||||
@ -14,12 +15,14 @@ export const defaultAgentDetailReplacementModeConfig = {
|
||||
enableTextEditing: true,
|
||||
productImageSlotCount: 2,
|
||||
includeModelSlot: true,
|
||||
referenceImageSource: "detail",
|
||||
} as const;
|
||||
|
||||
export const defaultAgentSkuReplacementModeConfig = {
|
||||
enableTextEditing: true,
|
||||
productImageSlotCount: 2,
|
||||
includeModelSlot: true,
|
||||
referenceImageSource: "sku",
|
||||
} as const;
|
||||
|
||||
export const defaultAgentPresetModeConfig: AgentPresetModeConfig = {
|
||||
@ -39,6 +42,13 @@ function normalizeInteger(value: unknown, fallback: number, min: number, max: nu
|
||||
return Math.min(max, Math.max(min, Math.round(normalized)));
|
||||
}
|
||||
|
||||
function normalizeReferenceSource(
|
||||
value: unknown,
|
||||
fallback: AgentTaskDetailReferenceSource
|
||||
): AgentTaskDetailReferenceSource {
|
||||
return value === "main" || value === "sku" || value === "detail" ? value : fallback;
|
||||
}
|
||||
|
||||
export function normalizeAgentPresetModeConfig(value: unknown): AgentPresetModeConfig {
|
||||
const raw =
|
||||
value && typeof value === "object" && !Array.isArray(value)
|
||||
@ -108,6 +118,10 @@ export function normalizeAgentPresetModeConfig(value: unknown): AgentPresetModeC
|
||||
detailRaw.includeModelSlot,
|
||||
defaultAgentDetailReplacementModeConfig.includeModelSlot
|
||||
),
|
||||
referenceImageSource: normalizeReferenceSource(
|
||||
detailRaw.referenceImageSource,
|
||||
defaultAgentDetailReplacementModeConfig.referenceImageSource
|
||||
),
|
||||
},
|
||||
sku: {
|
||||
enableTextEditing: normalizeBoolean(
|
||||
@ -124,6 +138,10 @@ export function normalizeAgentPresetModeConfig(value: unknown): AgentPresetModeC
|
||||
skuRaw.includeModelSlot,
|
||||
defaultAgentSkuReplacementModeConfig.includeModelSlot
|
||||
),
|
||||
referenceImageSource: normalizeReferenceSource(
|
||||
skuRaw.referenceImageSource,
|
||||
defaultAgentSkuReplacementModeConfig.referenceImageSource
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -8,6 +8,8 @@ export type AgentPresetModeType =
|
||||
| "detail_replacement"
|
||||
| "sku_replacement";
|
||||
|
||||
export type AgentTaskDetailReferenceSource = "main" | "sku" | "detail";
|
||||
|
||||
export interface AgentSceneGenerationModeConfig {
|
||||
productImageSlotCount: number;
|
||||
includeModelSlot: boolean;
|
||||
@ -19,12 +21,14 @@ export interface AgentDetailReplacementModeConfig {
|
||||
enableTextEditing: boolean;
|
||||
productImageSlotCount: number;
|
||||
includeModelSlot: boolean;
|
||||
referenceImageSource: AgentTaskDetailReferenceSource;
|
||||
}
|
||||
|
||||
export interface AgentSkuReplacementModeConfig {
|
||||
enableTextEditing: boolean;
|
||||
productImageSlotCount: number;
|
||||
includeModelSlot: boolean;
|
||||
referenceImageSource: AgentTaskDetailReferenceSource;
|
||||
}
|
||||
|
||||
export interface AgentPresetModeConfig {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user