diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 1a3a5aaf3..2aade6f65 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -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 ( +
+ +
+ +
+ + + +
+
+ + {params.badge} + + {params.showSpinner ? ( + + ) : null} +
+
{params.title}
+
{params.description}
+
+
+ ); +} + 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(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({ - {isGenerating ? ( + {isAnyGenerationRunning ? (
@@ -3081,20 +3250,16 @@ export function AgentGenerationWorkspaceDarkV2({ ) : (
setActiveDetailReferenceCardId(card.id)} > -
-
- {statusMeta.label} -
-
- 结果图 {card.index + 1} -
-
- 将按“输入图组 + 第 {card.index + 1} 张{detailReferenceSourceLabel}参考图”生成 -
-
+
)}
@@ -3530,20 +3695,16 @@ export function AgentGenerationWorkspaceDarkV2({
) : (
setActiveSkuReferenceCardId(card.id)} > -
-
- {statusMeta.label} -
-
- 结果图 {card.index + 1} -
-
- 将按“输入图 + 第 {card.index + 1} 张{skuReferenceSourceLabel}参考图”生成 -
-
+
)}
@@ -3628,13 +3789,24 @@ export function AgentGenerationWorkspaceDarkV2({ loading="lazy" /> ) : ( -
)}