fix: refine detail replacement workflow
This commit is contained in:
parent
29f94248f4
commit
b98196b96e
@ -6135,6 +6135,19 @@ export async function POST(request: Request) {
|
||||
const replacementSourceType = isDetailReplacementPreset
|
||||
? "detail_replacement"
|
||||
: "sku_replacement";
|
||||
console.info("[Chat API] Replacement reference decision:", {
|
||||
mode: replacementSourceType,
|
||||
referenceIndex: referenceIndex + 1,
|
||||
totalReferences: replacementReferences.length,
|
||||
referenceLabel: reference.label,
|
||||
referenceImageUrl: reference.imageUrl,
|
||||
pairedProductIndex:
|
||||
typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null,
|
||||
shouldReplace: referenceDecision.shouldReplace,
|
||||
reason: referenceDecision.reason,
|
||||
detectionModelName: referenceDecision.detectionModelName,
|
||||
detectionSource: referenceDecision.detectionSource,
|
||||
});
|
||||
const referenceSourceImageUrls = Array.from(
|
||||
new Set([
|
||||
...sharedUploadedImageUrls,
|
||||
@ -6144,6 +6157,13 @@ export async function POST(request: Request) {
|
||||
);
|
||||
|
||||
if (!referenceDecision.shouldReplace) {
|
||||
console.info("[Chat API] Replacement reference skipped and reused:", {
|
||||
mode: replacementSourceType,
|
||||
referenceIndex: referenceIndex + 1,
|
||||
totalReferences: replacementReferences.length,
|
||||
referenceLabel: reference.label,
|
||||
reason: referenceDecision.reason,
|
||||
});
|
||||
await emitReplacementResultMessage({
|
||||
reference,
|
||||
bodyText: referenceDecision.reason,
|
||||
@ -6185,6 +6205,15 @@ export async function POST(request: Request) {
|
||||
hasModelImage: Boolean(modelInputImageUrl),
|
||||
pairedProductIndex,
|
||||
});
|
||||
console.info("[Chat API] Replacement image generation started:", {
|
||||
mode: replacementSourceType,
|
||||
referenceIndex: referenceIndex + 1,
|
||||
totalReferences: replacementReferences.length,
|
||||
referenceLabel: reference.label,
|
||||
pairedProductIndex:
|
||||
typeof pairedProductIndex === "number" ? pairedProductIndex + 1 : null,
|
||||
sourceImageCount: sourceImageUrls.length,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await generateCanvasImage({
|
||||
@ -6204,6 +6233,14 @@ export async function POST(request: Request) {
|
||||
if (!firstImage) {
|
||||
throw new Error("未返回结果图片");
|
||||
}
|
||||
console.info("[Chat API] Replacement image generation completed:", {
|
||||
mode: replacementSourceType,
|
||||
referenceIndex: referenceIndex + 1,
|
||||
totalReferences: replacementReferences.length,
|
||||
referenceLabel: reference.label,
|
||||
modelDisplayName: resolvedModelDisplayName,
|
||||
resultImageUrl: firstImage.originalUrl,
|
||||
});
|
||||
await emitReplacementResultMessage({
|
||||
reference,
|
||||
image: firstImage,
|
||||
|
||||
@ -130,11 +130,22 @@ function getCandidateTargetUrls(
|
||||
return target === "detail" ? candidate.detailImageUrls : candidate.skuImageUrls;
|
||||
}
|
||||
|
||||
function getRenderableCandidateTargetUrls(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
target: "main" | "detail" | "sku"
|
||||
) {
|
||||
const urls = getCandidateTargetUrls(candidate, target);
|
||||
if (target === "detail" && urls.length > 1) {
|
||||
return urls.slice(1);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
function buildReferenceItems(params: {
|
||||
candidate: AgentModeReferenceCandidate;
|
||||
target: "main" | "detail" | "sku";
|
||||
}): AgentDetailReplacementReferenceItem[] {
|
||||
const urls = getCandidateTargetUrls(params.candidate, params.target);
|
||||
const urls = getRenderableCandidateTargetUrls(params.candidate, params.target);
|
||||
const targetLabel =
|
||||
params.target === "main"
|
||||
? "主图参考图"
|
||||
@ -252,7 +263,7 @@ export async function GET(request: NextRequest) {
|
||||
(upstreamResult.data.candidates || []).map((item, index) => normalizeCandidate(item, index))
|
||||
)
|
||||
.filter((candidate) => isCandidateMatchedStore(candidate, requestedStoreName))
|
||||
.filter((candidate) => getCandidateTargetUrls(candidate, parsed.target).length > 0);
|
||||
.filter((candidate) => getRenderableCandidateTargetUrls(candidate, parsed.target).length > 0);
|
||||
|
||||
if (normalizedCandidates.length === 0) {
|
||||
return NextResponse.json(
|
||||
|
||||
@ -249,12 +249,14 @@ function buildReferenceCardsFromCandidate(params: {
|
||||
const candidate = params.candidate;
|
||||
if (!candidate) return [] as AgentModeWorkspaceState["detailReplacement"]["references"];
|
||||
|
||||
const urls =
|
||||
const rawUrls =
|
||||
params.target === "main"
|
||||
? candidate.mainImageUrls || []
|
||||
: params.target === "detail"
|
||||
? candidate.detailImageUrls || []
|
||||
: candidate.skuImageUrls || [];
|
||||
const urls =
|
||||
params.target === "detail" && rawUrls.length > 1 ? rawUrls.slice(1) : rawUrls;
|
||||
const targetLabel = `${getTaskDetailReferenceSourceLabel(params.target)}参考图`;
|
||||
const prefix =
|
||||
candidate.sceneName || candidate.taskNo || candidate.productModel || "候选整套";
|
||||
@ -273,12 +275,13 @@ function buildReferenceCardsFromCandidate(params: {
|
||||
}
|
||||
|
||||
function formatCandidateMeta(candidate: AgentModeReferenceCandidate, target: AgentTaskDetailReferenceSource) {
|
||||
const imageCount =
|
||||
const rawImageCount =
|
||||
target === "main"
|
||||
? candidate.mainImageUrls.length
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls.length
|
||||
: candidate.skuImageUrls.length;
|
||||
const imageCount = target === "detail" && rawImageCount > 1 ? rawImageCount - 1 : rawImageCount;
|
||||
const uploadedAt = candidate.uploadedAt ? new Date(candidate.uploadedAt) : null;
|
||||
const timeText =
|
||||
uploadedAt && !Number.isNaN(uploadedAt.getTime())
|
||||
@ -303,12 +306,14 @@ function getCandidatePreviewUrl(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
target: AgentTaskDetailReferenceSource
|
||||
) {
|
||||
const targetUrls =
|
||||
const rawTargetUrls =
|
||||
target === "main"
|
||||
? candidate.mainImageUrls || []
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls || []
|
||||
: candidate.skuImageUrls || [];
|
||||
const targetUrls =
|
||||
target === "detail" && rawTargetUrls.length > 1 ? rawTargetUrls.slice(1) : rawTargetUrls;
|
||||
return (
|
||||
targetUrls[0] ||
|
||||
candidate.mainImageUrls[0] ||
|
||||
@ -321,23 +326,56 @@ function getCandidateTargetCount(
|
||||
candidate: AgentModeReferenceCandidate,
|
||||
target: AgentTaskDetailReferenceSource
|
||||
) {
|
||||
return target === "main"
|
||||
? candidate.mainImageUrls.length
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls.length
|
||||
: candidate.skuImageUrls.length;
|
||||
const rawCount =
|
||||
target === "main"
|
||||
? candidate.mainImageUrls.length
|
||||
: target === "detail"
|
||||
? candidate.detailImageUrls.length
|
||||
: candidate.skuImageUrls.length;
|
||||
return target === "detail" && rawCount > 1 ? rawCount - 1 : rawCount;
|
||||
}
|
||||
|
||||
type ReplacementCardStatus = "waiting" | "queued" | "generating" | "completed" | "missing";
|
||||
type ReplacementCardStatus =
|
||||
| "waiting"
|
||||
| "queued"
|
||||
| "generating"
|
||||
| "completed"
|
||||
| "passthrough"
|
||||
| "fallback"
|
||||
| "missing";
|
||||
|
||||
function inferReplacementCardStatusFromText(text: string | null | undefined): ReplacementCardStatus | null {
|
||||
const normalized = normalizeAssistantText(String(text || ""))
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalized.includes("生成失败,当前先沿用参考图")) {
|
||||
return "fallback";
|
||||
}
|
||||
|
||||
if (
|
||||
normalized.includes("沿用参考图") ||
|
||||
normalized.includes("未检测到可替换的家具主体") ||
|
||||
normalized.includes("视觉判断失败")
|
||||
) {
|
||||
return "passthrough";
|
||||
}
|
||||
|
||||
return "completed";
|
||||
}
|
||||
|
||||
function getReplacementCardStatus(params: {
|
||||
index: number;
|
||||
resultUrl: string | null;
|
||||
resultText?: string | null;
|
||||
generatedCount: number;
|
||||
isGenerating: boolean;
|
||||
}) : ReplacementCardStatus {
|
||||
if (params.resultUrl) {
|
||||
return "completed";
|
||||
return inferReplacementCardStatusFromText(params.resultText) || "completed";
|
||||
}
|
||||
if (params.isGenerating) {
|
||||
return params.index === params.generatedCount ? "generating" : "queued";
|
||||
@ -350,6 +388,16 @@ function getReplacementCardStatus(params: {
|
||||
|
||||
function getReplacementCardStatusMeta(status: ReplacementCardStatus) {
|
||||
switch (status) {
|
||||
case "passthrough":
|
||||
return {
|
||||
label: "沿用参考图",
|
||||
className: "border-cyan-400/30 bg-cyan-500/[0.10] text-cyan-200",
|
||||
};
|
||||
case "fallback":
|
||||
return {
|
||||
label: "失败已沿用",
|
||||
className: "border-amber-400/30 bg-amber-500/[0.10] text-amber-200",
|
||||
};
|
||||
case "completed":
|
||||
return {
|
||||
label: "已生成",
|
||||
@ -619,7 +667,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const hasMaterialStep = Boolean(materialSelectionPanel);
|
||||
const hasWorkflowStyleStep = Boolean(workflowStylePanel) || hasBrandModelStep;
|
||||
|
||||
const { messages, setMessages, sendMessage, status } = useChat<ChatMessage>({
|
||||
const { messages, setMessages, sendMessage, status, stop } = useChat<ChatMessage>({
|
||||
id: chatId,
|
||||
messages: historyMessages,
|
||||
experimental_throttle: 100,
|
||||
@ -871,48 +919,78 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
}
|
||||
return generatedGroups[generatedGroups.length - 1] || null;
|
||||
}, [generatedGroups, messages]);
|
||||
const latestReplacementResults = useMemo(
|
||||
() =>
|
||||
replacementGeneratedGroupsAfterLastUser.map((group) => ({
|
||||
url: group.imageUrls[0] || null,
|
||||
text: group.text || "",
|
||||
})),
|
||||
[replacementGeneratedGroupsAfterLastUser]
|
||||
);
|
||||
const latestReplacementResultUrls = useMemo(
|
||||
() =>
|
||||
replacementGeneratedGroupsAfterLastUser.flatMap((group) => group.imageUrls),
|
||||
[replacementGeneratedGroupsAfterLastUser]
|
||||
latestReplacementResults
|
||||
.map((item) => item.url)
|
||||
.filter((url): url is string => Boolean(url)),
|
||||
[latestReplacementResults]
|
||||
);
|
||||
const displayedResultUrl = selectedResultUrl || latestGeneratedGroup?.imageUrls[0] || null;
|
||||
const isGenerating = status === "streaming" || status === "submitted";
|
||||
const detailResultCards = useMemo(
|
||||
() =>
|
||||
detailReplacementReferenceCards.map((card, index) => {
|
||||
const resultUrl = latestReplacementResultUrls[index] || null;
|
||||
const resultMeta = latestReplacementResults[index];
|
||||
const resultUrl = resultMeta?.url || null;
|
||||
return {
|
||||
...card,
|
||||
index,
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
status: getReplacementCardStatus({
|
||||
index,
|
||||
resultUrl,
|
||||
generatedCount: latestReplacementResultUrls.length,
|
||||
resultText: resultMeta?.text || "",
|
||||
generatedCount: latestReplacementResults.length,
|
||||
isGenerating,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[detailReplacementReferenceCards, isGenerating, latestReplacementResultUrls]
|
||||
[detailReplacementReferenceCards, isGenerating, latestReplacementResults]
|
||||
);
|
||||
const skuResultCards = useMemo(
|
||||
() =>
|
||||
skuReplacementReferenceCards.map((card, index) => {
|
||||
const resultUrl = latestReplacementResultUrls[index] || null;
|
||||
const resultMeta = latestReplacementResults[index];
|
||||
const resultUrl = resultMeta?.url || null;
|
||||
return {
|
||||
...card,
|
||||
index,
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
status: getReplacementCardStatus({
|
||||
index,
|
||||
resultUrl,
|
||||
generatedCount: latestReplacementResultUrls.length,
|
||||
resultText: resultMeta?.text || "",
|
||||
generatedCount: latestReplacementResults.length,
|
||||
isGenerating,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[isGenerating, latestReplacementResultUrls, skuReplacementReferenceCards]
|
||||
[isGenerating, latestReplacementResults, skuReplacementReferenceCards]
|
||||
);
|
||||
const detailProcessedCount = useMemo(
|
||||
() =>
|
||||
detailResultCards.filter((card) =>
|
||||
["completed", "passthrough", "fallback"].includes(card.status)
|
||||
).length,
|
||||
[detailResultCards]
|
||||
);
|
||||
const skuProcessedCount = useMemo(
|
||||
() =>
|
||||
skuResultCards.filter((card) =>
|
||||
["completed", "passthrough", "fallback"].includes(card.status)
|
||||
).length,
|
||||
[skuResultCards]
|
||||
);
|
||||
const sceneResultCardCount = Math.max(
|
||||
allGeneratedImages.length,
|
||||
@ -1376,6 +1454,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
workflowContext?.promptText,
|
||||
workflowContext?.summaryText,
|
||||
]);
|
||||
const handleStopGeneration = useCallback(() => {
|
||||
stop();
|
||||
setMessages((current) => current);
|
||||
toast.info("已停止当前生成任务");
|
||||
}, [setMessages, stop]);
|
||||
|
||||
const handleDownloadOriginalImage = useCallback(async (imageUrl: string) => {
|
||||
const raw = String(imageUrl || "").trim();
|
||||
@ -2088,34 +2171,47 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageGenConfig={currentModel?.imageGenConfig || undefined}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={
|
||||
isGenerating ||
|
||||
!selectedAgentPresetId ||
|
||||
(hasAgentUploadSlots && !hasSatisfiedSlotRequirement)
|
||||
}
|
||||
className={cn(
|
||||
"h-11 w-full gap-2 rounded-[24px]",
|
||||
"bg-gradient-to-r from-emerald-500 to-teal-500",
|
||||
"text-white font-medium",
|
||||
"hover:from-emerald-400 hover:to-teal-400",
|
||||
"shadow-[0_0_24px_rgba(16,185,129,0.24)]",
|
||||
"disabled:opacity-30"
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={
|
||||
isGenerating ||
|
||||
!selectedAgentPresetId ||
|
||||
(hasAgentUploadSlots && !hasSatisfiedSlotRequirement)
|
||||
}
|
||||
className={cn(
|
||||
"h-11 flex-1 gap-2 rounded-[24px]",
|
||||
"bg-gradient-to-r from-emerald-500 to-teal-500",
|
||||
"text-white font-medium",
|
||||
"hover:from-emerald-400 hover:to-teal-400",
|
||||
"shadow-[0_0_24px_rgba(16,185,129,0.24)]",
|
||||
"disabled:opacity-30"
|
||||
)}
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isReplacementPreset ? "批量替换中..." : "生成中..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
{isReplacementPreset ? "开始批量替换" : "开始生成"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isReplacementPreset ? "批量替换中..." : "生成中..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
{isReplacementPreset ? "开始批量替换" : "开始生成"}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleStopGeneration}
|
||||
className="h-11 shrink-0 rounded-[24px] border-white/12 bg-black/30 px-4 text-white hover:bg-white/[0.08]"
|
||||
>
|
||||
<X className="mr-2 h-4 w-4" />
|
||||
停止生成
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{slotRequirementMessage ? (
|
||||
<div className="rounded-[18px] border border-amber-400/20 bg-amber-500/[0.08] px-4 py-3 text-xs leading-6 text-amber-100/90">
|
||||
{slotRequirementMessage}
|
||||
@ -2213,7 +2309,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
|
||||
{isDetailReplacementPreset ? (
|
||||
<div className="grid flex-1 min-h-0 grid-cols-1 gap-0 xl:grid-cols-[320px_minmax(0,1fr)_360px]">
|
||||
<div className="grid flex-1 min-h-0 grid-cols-1 gap-0 xl:grid-cols-[320px_repeat(2,minmax(0,1fr))]">
|
||||
<div className="border-b border-white/10 p-4 xl:border-b-0 xl:border-r">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div>
|
||||
@ -2422,7 +2518,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="agent-dark-scrollbar max-h-[620px] space-y-3 overflow-y-auto pr-1">
|
||||
<div className="agent-dark-scrollbar grid max-h-[620px] grid-cols-1 gap-3 overflow-y-auto pr-1">
|
||||
{detailReplacementReferenceCards.length > 0 ? (
|
||||
detailReplacementReferenceCards.map((card, index) => (
|
||||
(() => {
|
||||
@ -2496,11 +2592,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-emerald-200/80">
|
||||
已接通
|
||||
已处理 {detailProcessedCount}/{detailResultCards.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="agent-dark-scrollbar max-h-[720px] space-y-3 overflow-y-auto pr-1">
|
||||
<div className="agent-dark-scrollbar grid max-h-[720px] grid-cols-1 gap-3 overflow-y-auto pr-1">
|
||||
{detailResultCards.length > 0 ? (
|
||||
detailResultCards.map((card) => (
|
||||
(() => {
|
||||
@ -2577,7 +2673,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
)}
|
||||
<div className="border-t border-white/10 px-3 py-2 text-[11px] text-white/45">
|
||||
{card.resultUrl
|
||||
? "已生成,可继续逐屏调整文字或重新执行。"
|
||||
? card.status === "passthrough"
|
||||
? "当前参考图未检测到可替换产品,本屏直接沿用参考图展示。"
|
||||
: card.status === "fallback"
|
||||
? "当前参考图替换失败,本屏先沿用参考图展示。"
|
||||
: "已生成,可继续逐屏调整文字或重新执行。"
|
||||
: card.status === "generating"
|
||||
? "当前正在生成这一屏,请稍候。"
|
||||
: card.status === "queued"
|
||||
@ -2948,7 +3048,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-full border border-white/10 bg-white/[0.03] px-3 py-1 text-xs text-emerald-200/80">
|
||||
已接通
|
||||
已处理 {skuProcessedCount}/{skuResultCards.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3022,7 +3122,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
)}
|
||||
<div className="border-t border-white/10 px-3 py-2 text-[11px] text-white/45">
|
||||
{card.resultUrl
|
||||
? "已生成,可继续逐张检查并后续批量下载。"
|
||||
? card.status === "passthrough"
|
||||
? "当前参考图未检测到可替换产品,这一张直接沿用参考图展示。"
|
||||
: card.status === "fallback"
|
||||
? "当前参考图替换失败,这一张先沿用参考图展示。"
|
||||
: "已生成,可继续逐张检查并后续批量下载。"
|
||||
: card.status === "generating"
|
||||
? "当前正在生成这一张,请稍候。"
|
||||
: card.status === "queued"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user