fix: keep generation placeholders active during processing
This commit is contained in:
parent
bb86d1fb1a
commit
5b30c2616b
@ -517,6 +517,69 @@ function getReplacementCardStatusMeta(status: ReplacementCardStatus) {
|
||||
}
|
||||
}
|
||||
|
||||
interface LocalGenerationPendingState {
|
||||
modeType: AgentGenerationResultModeType;
|
||||
expectedCount: number;
|
||||
userTurnCountAtStart: number;
|
||||
}
|
||||
|
||||
function AnimatedGenerationPlaceholderCard(params: {
|
||||
title: string;
|
||||
description: string;
|
||||
badge: string;
|
||||
badgeClassName?: string;
|
||||
showSpinner?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative h-full w-full overflow-hidden bg-[linear-gradient(180deg,#060809_0%,#101417_100%)]">
|
||||
<motion.div
|
||||
aria-hidden
|
||||
className="absolute inset-[-42%] origin-center"
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 5.2, repeat: Infinity, ease: "linear" }}
|
||||
>
|
||||
<div className="h-full w-full bg-[conic-gradient(from_0deg,rgba(16,185,129,0)_0deg,rgba(16,185,129,0.96)_42deg,rgba(34,211,238,0.86)_120deg,rgba(251,191,36,0.72)_198deg,rgba(168,85,247,0.88)_272deg,rgba(16,185,129,0)_360deg)] opacity-95" />
|
||||
</motion.div>
|
||||
<div className="absolute inset-[1.5px] rounded-[20px] bg-[linear-gradient(180deg,#080a0c_0%,#12171a_100%)]" />
|
||||
<motion.div
|
||||
aria-hidden
|
||||
className="absolute inset-[1.5px] rounded-[20px] bg-[radial-gradient(circle_at_top,rgba(16,185,129,0.16),transparent_58%)]"
|
||||
animate={{ opacity: [0.42, 0.72, 0.42] }}
|
||||
transition={{ duration: 2.3, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
aria-hidden
|
||||
className="absolute inset-[1.5px] rounded-[20px] opacity-70"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(118deg, transparent 8%, rgba(255,255,255,0.08) 36%, transparent 62%)",
|
||||
backgroundSize: "220% 100%",
|
||||
}}
|
||||
animate={{ backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"] }}
|
||||
transition={{ duration: 3.6, repeat: Infinity, ease: "linear" }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 flex h-full flex-col items-center justify-center gap-3 px-5 text-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex rounded-full border px-3 py-1 text-[11px]",
|
||||
params.badgeClassName || "border-white/12 bg-white/[0.05] text-white/65"
|
||||
)}
|
||||
>
|
||||
{params.badge}
|
||||
</span>
|
||||
{params.showSpinner ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-emerald-200/80" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-sm font-medium tracking-[0.02em] text-white/82">{params.title}</div>
|
||||
<div className="max-w-[220px] text-xs leading-6 text-white/42">{params.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentGenerationWorkspaceDarkV2({
|
||||
chatId,
|
||||
projectId,
|
||||
@ -618,6 +681,8 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
imageSize: normalizedIncomingWorkspaceState.imageParams?.imageSize || "2K",
|
||||
imageCount: normalizedIncomingWorkspaceState.imageParams?.imageCount || 1,
|
||||
});
|
||||
const [pendingGeneration, setPendingGeneration] =
|
||||
useState<LocalGenerationPendingState | null>(null);
|
||||
|
||||
const { data: historyData } = useSWR<{ messages: ChatMessage[] }>(
|
||||
rawChatId ? `/api/messages?chatId=${rawChatId}` : null,
|
||||
@ -869,6 +934,9 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
messages: historyMessages,
|
||||
experimental_throttle: 100,
|
||||
generateId: generateUUID,
|
||||
onError: () => {
|
||||
setPendingGeneration(null);
|
||||
},
|
||||
transport: new DefaultChatTransport({
|
||||
api: "/api/chat",
|
||||
fetch: fetchWithErrorHandlers,
|
||||
@ -1099,6 +1167,10 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
})
|
||||
.filter((group) => group.imageUrls.length > 0);
|
||||
}, [messages]);
|
||||
const userMessageCount = useMemo(
|
||||
() => messages.filter((message) => message.role === "user").length,
|
||||
[messages]
|
||||
);
|
||||
|
||||
const generatedGroupsAfterLastUser = useMemo(() => {
|
||||
const lastUserIndex = [...messages]
|
||||
@ -1178,6 +1250,40 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
const currentLiveResultUrls = isReplacementPreset
|
||||
? latestReplacementResultUrls
|
||||
: currentSceneGeneratedUrls;
|
||||
const transportIsGenerating = status === "streaming" || status === "submitted";
|
||||
const pendingGenerationProgressCount = useMemo(() => {
|
||||
if (!pendingGeneration) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (userMessageCount <= pendingGeneration.userTurnCountAtStart) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (pendingGeneration.modeType === "scene_generation") {
|
||||
return currentSceneGeneratedUrls.length;
|
||||
}
|
||||
|
||||
return latestReplacementResults.length;
|
||||
}, [
|
||||
currentSceneGeneratedUrls.length,
|
||||
latestReplacementResults.length,
|
||||
pendingGeneration,
|
||||
userMessageCount,
|
||||
]);
|
||||
const isAnyGenerationRunning = transportIsGenerating || Boolean(pendingGeneration);
|
||||
const isSceneResultModeGenerating =
|
||||
(activeResultModeType === "scene_generation" &&
|
||||
pendingGeneration?.modeType === "scene_generation") ||
|
||||
(activeResultModeType === currentResultModeType &&
|
||||
currentResultModeType === "scene_generation" &&
|
||||
transportIsGenerating);
|
||||
const isDetailResultModeGenerating =
|
||||
pendingGeneration?.modeType === "detail_replacement" ||
|
||||
(currentResultModeType === "detail_replacement" && transportIsGenerating);
|
||||
const isSkuResultModeGenerating =
|
||||
pendingGeneration?.modeType === "sku_replacement" ||
|
||||
(currentResultModeType === "sku_replacement" && transportIsGenerating);
|
||||
const mergedResultCollections = useMemo(
|
||||
() => {
|
||||
const baseCollections = {
|
||||
@ -1204,13 +1310,13 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
.filter(
|
||||
(modeType) =>
|
||||
modeType === currentResultModeType ||
|
||||
modeType === pendingGeneration?.modeType ||
|
||||
(mergedResultCollections[modeType] || []).length > 0
|
||||
),
|
||||
[currentResultModeType, mergedResultCollections]
|
||||
[currentResultModeType, mergedResultCollections, pendingGeneration?.modeType]
|
||||
);
|
||||
const activeResultModeUrls = mergedResultCollections[activeResultModeType] || [];
|
||||
const displayedResultUrl = selectedResultUrl || activeResultModeUrls[0] || null;
|
||||
const isGenerating = status === "streaming" || status === "submitted";
|
||||
const detailResultCards = useMemo(
|
||||
() =>
|
||||
detailReplacementReferenceCards.map((card, index) => {
|
||||
@ -1226,11 +1332,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
generatedCount: latestReplacementResults.length,
|
||||
isGenerating,
|
||||
isGenerating: isDetailResultModeGenerating,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[detailReplacementReferenceCards, isGenerating, latestReplacementResults]
|
||||
[detailReplacementReferenceCards, isDetailResultModeGenerating, latestReplacementResults]
|
||||
);
|
||||
const skuResultCards = useMemo(
|
||||
() =>
|
||||
@ -1247,11 +1353,11 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
resultUrl,
|
||||
resultText: resultMeta?.text || "",
|
||||
generatedCount: latestReplacementResults.length,
|
||||
isGenerating,
|
||||
isGenerating: isSkuResultModeGenerating,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[isGenerating, latestReplacementResults, skuReplacementReferenceCards]
|
||||
[isSkuResultModeGenerating, latestReplacementResults, skuReplacementReferenceCards]
|
||||
);
|
||||
const detailProcessedCount = useMemo(
|
||||
() =>
|
||||
@ -1268,27 +1374,48 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
[skuResultCards]
|
||||
);
|
||||
const sceneRequestedImageCount = Math.max(1, imageParams.imageCount || 1);
|
||||
const scenePendingCardCount =
|
||||
activeResultModeType === currentResultModeType && !isReplacementPreset && isGenerating
|
||||
? sceneRequestedImageCount
|
||||
const scenePendingRemainingCount =
|
||||
activeResultModeType === "scene_generation" &&
|
||||
pendingGeneration?.modeType === "scene_generation"
|
||||
? Math.max(pendingGeneration.expectedCount - currentSceneGeneratedUrls.length, 0)
|
||||
: 0;
|
||||
const scenePendingCardCount =
|
||||
activeResultModeType === "scene_generation" ? scenePendingRemainingCount : 0;
|
||||
const sceneResultCardCount = Math.max(
|
||||
activeResultModeUrls.length + scenePendingCardCount,
|
||||
activeResultModeType === currentResultModeType ? sceneRequestedImageCount : activeResultModeUrls.length,
|
||||
activeResultModeType === "scene_generation" &&
|
||||
pendingGeneration?.modeType === "scene_generation"
|
||||
? activeResultModeUrls.length + scenePendingRemainingCount
|
||||
: activeResultModeType === currentResultModeType
|
||||
? sceneRequestedImageCount
|
||||
: activeResultModeUrls.length,
|
||||
1
|
||||
);
|
||||
const sceneResultCards = useMemo(
|
||||
() =>
|
||||
Array.from({ length: sceneResultCardCount }, (_, index) => ({
|
||||
id: activeResultModeUrls[index] || `scene-result-placeholder-${activeResultModeType}-${index}`,
|
||||
index,
|
||||
url: activeResultModeUrls[index] || null,
|
||||
})),
|
||||
[activeResultModeType, activeResultModeUrls, sceneResultCardCount]
|
||||
() => {
|
||||
const placeholderStartIndex = activeResultModeUrls.length;
|
||||
return Array.from({ length: sceneResultCardCount }, (_, index) => {
|
||||
const url = activeResultModeUrls[index] || null;
|
||||
const placeholderOffset = Math.max(index - placeholderStartIndex, 0);
|
||||
return {
|
||||
id: url || `scene-result-placeholder-${activeResultModeType}-${index}`,
|
||||
index,
|
||||
url,
|
||||
status: url
|
||||
? ("completed" as const)
|
||||
: scenePendingRemainingCount > 0
|
||||
? placeholderOffset === 0
|
||||
? ("generating" as const)
|
||||
: ("queued" as const)
|
||||
: ("waiting" as const),
|
||||
};
|
||||
});
|
||||
},
|
||||
[activeResultModeType, activeResultModeUrls, scenePendingRemainingCount, sceneResultCardCount]
|
||||
);
|
||||
const shouldShowSceneResultGrid =
|
||||
activeResultModeUrls.length > 0 ||
|
||||
(activeResultModeType === currentResultModeType && !isReplacementPreset && isGenerating);
|
||||
(activeResultModeType === "scene_generation" && isSceneResultModeGenerating);
|
||||
const attachmentCount = attachments.filter(Boolean).length;
|
||||
const hasBrandModelSelection = Boolean(
|
||||
selectedBrandModel?.imageUrl || referenceImages.some((image) => image.source === "brandModel")
|
||||
@ -1456,6 +1583,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
setActiveResultModeType(currentResultModeType);
|
||||
}, [currentResultModeType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pendingGenerationProgressCount >= pendingGeneration.expectedCount) {
|
||||
setPendingGeneration(null);
|
||||
}
|
||||
}, [pendingGeneration, pendingGenerationProgressCount]);
|
||||
|
||||
useEffect(() => {
|
||||
onResultPreviewUrlsChange?.(
|
||||
currentResultModeType,
|
||||
@ -1731,26 +1868,47 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
hiddenPromptRef.current = workflowContext?.promptText
|
||||
? `【工作流设定】\n${workflowContext.promptText}\n\n请严格遵循以上工作流设定输出内容,并保持整体结果一致。`
|
||||
: null;
|
||||
sendMessage({ role: "user", parts });
|
||||
setInput("");
|
||||
setActiveResultModeType(currentResultModeType);
|
||||
setSelectedResultUrl(null);
|
||||
setPendingGeneration({
|
||||
modeType: currentResultModeType,
|
||||
expectedCount:
|
||||
currentResultModeType === "scene_generation"
|
||||
? Math.max(1, imageParams.imageCount || 1)
|
||||
: currentResultModeType === "detail_replacement"
|
||||
? detailReplacementReferenceCards.length
|
||||
: skuReplacementReferenceCards.length,
|
||||
userTurnCountAtStart: userMessageCount,
|
||||
});
|
||||
try {
|
||||
sendMessage({ role: "user", parts });
|
||||
setInput("");
|
||||
} catch (error) {
|
||||
setPendingGeneration(null);
|
||||
toast.error(error instanceof Error ? error.message : "发送生成请求失败");
|
||||
}
|
||||
}, [
|
||||
attachments,
|
||||
detailReplacementReferenceCards.length,
|
||||
currentResultModeType,
|
||||
hasAgentUploadSlots,
|
||||
imageParams.imageCount,
|
||||
input,
|
||||
isDetailReplacementPreset,
|
||||
isReplacementPreset,
|
||||
isSkuReplacementPreset,
|
||||
requiredUploadCount,
|
||||
selectedAgentPreset,
|
||||
selectedAgentPresetId,
|
||||
sendMessage,
|
||||
setPendingGeneration,
|
||||
skuReplacementReferenceCards.length,
|
||||
userMessageCount,
|
||||
workflowContext?.promptText,
|
||||
workflowContext?.summaryText,
|
||||
]);
|
||||
const handleStopGeneration = useCallback(() => {
|
||||
stop();
|
||||
setPendingGeneration(null);
|
||||
setMessages((current) => current);
|
||||
toast.info("已停止当前生成任务");
|
||||
}, [setMessages, stop]);
|
||||
@ -2479,7 +2637,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
disabled={
|
||||
isGenerating ||
|
||||
isAnyGenerationRunning ||
|
||||
!selectedAgentPresetId ||
|
||||
(hasAgentUploadSlots && !hasSatisfiedSlotRequirement)
|
||||
}
|
||||
@ -2492,10 +2650,15 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
"disabled:opacity-30"
|
||||
)}
|
||||
>
|
||||
{isGenerating ? (
|
||||
{isAnyGenerationRunning ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{isReplacementPreset ? "批量替换中..." : "生成中..."}
|
||||
{pendingGeneration?.modeType === currentResultModeType
|
||||
? `${isReplacementPreset ? "批量处理中" : "生成中"} ${Math.min(
|
||||
pendingGenerationProgressCount,
|
||||
pendingGeneration.expectedCount
|
||||
)}/${pendingGeneration.expectedCount}`
|
||||
: "任务处理中..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -2504,7 +2667,7 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{isGenerating ? (
|
||||
{isAnyGenerationRunning ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@ -2850,6 +3013,12 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
共 {activeResultModeUrls.length} 张
|
||||
</div>
|
||||
) : null}
|
||||
{pendingGeneration?.modeType === activeResultModeType ? (
|
||||
<div className="rounded-full border border-sky-400/25 bg-sky-500/[0.10] px-3 py-1 text-xs text-sky-100">
|
||||
处理中 {Math.min(pendingGenerationProgressCount, pendingGeneration.expectedCount)}/
|
||||
{pendingGeneration.expectedCount}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3081,20 +3250,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex aspect-[5/3] items-center justify-center bg-white/[0.02]"
|
||||
className="aspect-[5/3]"
|
||||
onClick={() => setActiveDetailReferenceCardId(card.id)}
|
||||
>
|
||||
<div className="space-y-2 px-4 text-center">
|
||||
<div className={cn("mx-auto inline-flex rounded-full border px-2.5 py-1 text-[11px]", statusMeta.className)}>
|
||||
{statusMeta.label}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-white/70">
|
||||
结果图 {card.index + 1}
|
||||
</div>
|
||||
<div className="text-xs text-white/35">
|
||||
将按“输入图组 + 第 {card.index + 1} 张{detailReferenceSourceLabel}参考图”生成
|
||||
</div>
|
||||
</div>
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={statusMeta.label}
|
||||
badgeClassName={statusMeta.className}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`结果图 ${card.index + 1}`}
|
||||
description={`将按“输入图组 + 第 ${card.index + 1} 张${detailReferenceSourceLabel}参考图”生成`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-white/10 px-3 py-2 text-[11px] text-white/45">
|
||||
@ -3530,20 +3695,16 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex aspect-[5/3] items-center justify-center bg-white/[0.02]"
|
||||
className="aspect-[5/3]"
|
||||
onClick={() => setActiveSkuReferenceCardId(card.id)}
|
||||
>
|
||||
<div className="space-y-2 px-4 text-center">
|
||||
<div className={cn("mx-auto inline-flex rounded-full border px-2.5 py-1 text-[11px]", statusMeta.className)}>
|
||||
{statusMeta.label}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-white/70">
|
||||
结果图 {card.index + 1}
|
||||
</div>
|
||||
<div className="text-xs text-white/35">
|
||||
将按“输入图 + 第 {card.index + 1} 张{skuReferenceSourceLabel}参考图”生成
|
||||
</div>
|
||||
</div>
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={statusMeta.label}
|
||||
badgeClassName={statusMeta.className}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`结果图 ${card.index + 1}`}
|
||||
description={`将按“输入图 + 第 ${card.index + 1} 张${skuReferenceSourceLabel}参考图”生成`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-white/10 px-3 py-2 text-[11px] text-white/45">
|
||||
@ -3628,13 +3789,24 @@ export function AgentGenerationWorkspaceDarkV2({
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-full bg-[linear-gradient(135deg,rgba(255,255,255,0.03),rgba(255,255,255,0.01))]",
|
||||
activeResultModeType === currentResultModeType && isGenerating
|
||||
? "animate-pulse"
|
||||
: ""
|
||||
)}
|
||||
<AnimatedGenerationPlaceholderCard
|
||||
badge={card.status === "generating" ? "生成中" : card.status === "queued" ? "排队中" : "待生成"}
|
||||
badgeClassName={
|
||||
card.status === "generating"
|
||||
? "border-sky-400/30 bg-sky-500/[0.10] text-sky-100"
|
||||
: card.status === "queued"
|
||||
? "border-white/12 bg-white/[0.05] text-white/62"
|
||||
: "border-white/10 bg-white/[0.04] text-white/58"
|
||||
}
|
||||
showSpinner={card.status === "generating"}
|
||||
title={`${getResultModeLabel(activeResultModeType)} 结果图 ${card.index + 1}`}
|
||||
description={
|
||||
card.status === "generating"
|
||||
? "当前图片正在生成中,结果返回后会自动替换这张占位卡。"
|
||||
: card.status === "queued"
|
||||
? "前面的结果完成后会继续处理这一张,请保持当前页面即可。"
|
||||
: "开始生成后,这里会出现对应数量的结果预览卡片。"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user