From f3869f5e63cb2e45e42cd79afac8304fbdc0ee86 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 19 Apr 2026 10:22:52 +0800 Subject: [PATCH] Add canvas generation pending placeholders --- .../canvas/LiblibVideoCanvasReplica.tsx | 337 ++++++++++++++---- 1 file changed, 273 insertions(+), 64 deletions(-) diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index cb533a2ac..d0d26e23e 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -1116,6 +1116,12 @@ function getDraftString(node: CloneNode | null | undefined, key: string) { return typeof value === "string" ? value : ""; } +function getDraftNumber(node: CloneNode | null | undefined, key: string, fallback = 0) { + const value = node?.draftValues?.[key]; + const numericValue = typeof value === "number" ? value : Number(value); + return Number.isFinite(numericValue) ? numericValue : fallback; +} + function getNodeMediaUrl(node: CloneNode | null | undefined) { const direct = getDraftString(node, "mediaUrl") || getDraftString(node, "thumbnailUrl") || getDraftString(node, "resultImage"); if (direct) return direct; @@ -1846,6 +1852,39 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib if (last) setSelectedNodeId(last.id); }, []); + useEffect(() => { + const timer = window.setInterval(() => { + setNodes((current) => { + let changed = false; + const nextNodes = current.map((node) => { + if (getDraftString(node, "generationStatus") !== "pending") return node; + const currentProgress = getDraftNumber(node, "generationProgress", 2); + const step = currentProgress < 24 ? 2 : currentProgress < 68 ? 1 : 0.35; + const nextProgress = Math.min(96, Number((currentProgress + step).toFixed(1))); + if (nextProgress === currentProgress) return node; + changed = true; + return { + ...node, + draftValues: { + ...(node.draftValues || {}), + generationProgress: nextProgress, + }, + }; + }); + return changed ? nextNodes : current; + }); + }, 850); + + return () => window.clearInterval(timer); + }, []); + + const cancelGenerationNode = useCallback((nodeId: string) => { + setNodes((current) => current.filter((node) => node.id !== nodeId)); + setEdges((current) => current.filter((edge) => edge.source !== nodeId && edge.target !== nodeId)); + setSelectedNodeId((current) => (current === nodeId ? null : current)); + toast.success("已取消生成"); + }, []); + const addWorkflowPreset = useCallback( (workflow: WorkflowPreset, index = 0) => { const title = String(workflow.name || workflow.title || "工具箱预设"); @@ -2489,7 +2528,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib if (action === "grid") { const { columns, rows } = normalizeGridOption(option || "9宫格"); const gridDefinition = getConfiguredFunction(videoCanvasFunctionConfig, "multi-view-nine-grid"); - let gridNode = createNineGridCompositeNode({ + const gridNode = createNineGridCompositeNode({ anchor, columns, option, @@ -2498,6 +2537,30 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib x: rightX, y: centerY(347), }); + const shouldGenerate = videoCanvasFunctionConfig.enabled && gridDefinition?.enabled; + const pendingGridNode: CloneNode = { + ...gridNode, + draftValues: { + ...(gridNode.draftValues || {}), + generated: !shouldGenerate, + generationProgress: shouldGenerate ? 2 : 100, + generationStatus: shouldGenerate ? "pending" : "complete", + mediaUrl: shouldGenerate ? "" : getDraftString(gridNode, "mediaUrl"), + pendingLabel: "生成中", + sourceNodeId: anchor.id, + sourceUrl, + url: shouldGenerate ? [] : gridNode.draftValues?.url, + }, + }; + + setNodes((current) => [...current, pendingGridNode]); + setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: pendingGridNode.id }]); + setSelectedNodeId(pendingGridNode.id); + setCanvasCommand(null); + setNodeContextMenu(null); + setConnectionMenu(null); + setActivePanel(null); + if (videoCanvasFunctionConfig.enabled && gridDefinition?.enabled) { const prompt = composeFunctionPrompt(gridDefinition, option); const response = await imageGenerationService.generateImage({ @@ -2509,30 +2572,46 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib }); const generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; if (generatedUrl) { - gridNode = { - ...gridNode, - draftValues: { - ...(gridNode.draftValues || {}), - generated: true, - mediaUrl: generatedUrl, - prompt, - sourceNodeId: anchor.id, - sourceUrl, - url: [generatedUrl], - }, - }; - } else if (!response.success) { - toast.error(response.error || "九宫格生成失败,已保留可切分占位节点"); + setNodes((current) => + current.map((node) => + node.id === pendingGridNode.id && getDraftString(node, "generationStatus") === "pending" + ? { + ...node, + draftValues: { + ...(node.draftValues || {}), + generated: true, + generationProgress: 100, + generationStatus: "complete", + mediaUrl: generatedUrl, + prompt, + sourceNodeId: anchor.id, + sourceUrl, + url: [generatedUrl], + }, + } + : node + ) + ); + } else { + const errorMessage = response.error || "九宫格生成未返回结果"; + setNodes((current) => + current.map((node) => + node.id === pendingGridNode.id && getDraftString(node, "generationStatus") === "pending" + ? { + ...node, + draftValues: { + ...(node.draftValues || {}), + generationStatus: "failed", + generationError: errorMessage, + }, + } + : node + ) + ); + toast.error(`${errorMessage},已保留可切分占位节点`); } } - setNodes((current) => [...current, gridNode]); - setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: gridNode.id }]); - setSelectedNodeId(gridNode.id); - setCanvasCommand(null); - setNodeContextMenu(null); - setConnectionMenu(null); - setActivePanel(null); - toast.success("已按预设提示词生成多机位九宫格结果图"); + toast.success(shouldGenerate ? "已创建生成中占位节点" : "已按预设提示词生成多机位九宫格结果图"); return; } @@ -2554,6 +2633,45 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib const prompt = composeFunctionPrompt(definition, option?.trim() || title); if (videoCanvasFunctionConfig.enabled && definition?.enabled) { + const pendingResultNode = createMediaResourceNode({ + action: "IMAGE_RESOURCE", + height: 280, + mediaType: "image", + mediaUrl: "", + prompt, + title: definition.label || title, + width: 420, + x: rightX, + y: centerY(280), + }); + const pendingNode: CloneNode = { + ...pendingResultNode, + draftValues: { + ...(pendingResultNode.draftValues || {}), + generated: false, + generationProgress: 2, + generationStatus: "pending", + params: { + mode: action, + prompt, + sourceNodeId: anchor.id, + sourceUrl, + }, + pendingLabel: "生成中", + sourceNodeId: anchor.id, + sourceUrl, + toolbarOption: option || "", + url: [], + }, + }; + setNodes((current) => [...current, pendingNode]); + setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: pendingNode.id }]); + setSelectedNodeId(pendingNode.id); + setCanvasCommand(null); + setNodeContextMenu(null); + setConnectionMenu(null); + setActivePanel(null); + let generatedUrl = ""; if (definition.route === "enhance") { const response = await imageGenerationService.enhanceImage(sourceUrl); @@ -2580,45 +2698,50 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib } if (generatedUrl) { - const resultNode = createMediaResourceNode({ - action: "IMAGE_RESOURCE", - height: 280, - mediaType: "image", - mediaUrl: generatedUrl, - prompt, - title: definition.label || title, - width: 420, - x: rightX, - y: centerY(280), - }); - setNodes((current) => [ - ...current, - { - ...resultNode, - draftValues: { - ...(resultNode.draftValues || {}), - generated: true, - params: { - mode: action, - prompt, - sourceNodeId: anchor.id, - sourceUrl, - }, - sourceNodeId: anchor.id, - sourceUrl, - toolbarOption: option || "", - }, - }, - ]); - setEdges((current) => [...current, { id: makeId("edge"), source: anchor.id, target: resultNode.id }]); - setSelectedNodeId(resultNode.id); - setCanvasCommand(null); - setNodeContextMenu(null); - setConnectionMenu(null); - setActivePanel(null); + setNodes((current) => + current.map((node) => + node.id === pendingNode.id && getDraftString(node, "generationStatus") === "pending" + ? { + ...node, + draftValues: { + ...(node.draftValues || {}), + generated: true, + generationProgress: 100, + generationStatus: "complete", + mediaUrl: generatedUrl, + params: { + mode: action, + prompt, + sourceNodeId: anchor.id, + sourceUrl, + }, + sourceNodeId: anchor.id, + sourceUrl, + toolbarOption: option || "", + url: [generatedUrl], + }, + } + : node + ) + ); toast.success(`已生成「${definition.label || title}」结果`); - return; + } else { + setNodes((current) => + current.map((node) => + node.id === pendingNode.id && getDraftString(node, "generationStatus") === "pending" + ? { + ...node, + draftValues: { + ...(node.draftValues || {}), + generationStatus: "failed", + generationError: `${definition.label || title}处理失败`, + }, + } + : node + ) + ); } + return; } const created = createTargetModuleNode("image", rightX, 0, nodes.length + 1); @@ -3360,6 +3483,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib node={node} selected={node.id === selectedNodeId} onContextMenu={(event) => openNodeContext(event, node)} + onCancelGeneration={() => cancelGenerationNode(node.id)} onModuleAction={(actionLabel) => runTargetModuleAction(node.id, actionLabel)} onOpenConnectionMenu={(event, side) => openConnectionMenu(event, node, side)} onPointerDown={(event) => startNodeDrag(event, node)} @@ -6342,6 +6466,7 @@ function EdgeFlowSegments({ function CloneCanvasNode({ node, + onCancelGeneration, onContextMenu, onModuleAction, onOpenConnectionMenu, @@ -6350,6 +6475,7 @@ function CloneCanvasNode({ selected, }: { node: CloneNode; + onCancelGeneration: () => void; onContextMenu: (event: ReactMouseEvent) => void; onModuleAction: (actionLabel: TargetModuleActionLabel) => void; onOpenConnectionMenu: (event: ReactMouseEvent, side: ConnectionSide) => void; @@ -6377,7 +6503,7 @@ function CloneCanvasNode({ if (node.kind === "media") { return ( - + ); } @@ -6835,11 +6961,22 @@ function CanvasNodeShell({ ); } -function PresetMediaCanvasNode({ node, selected }: { node: CloneNode; selected: boolean }) { +function PresetMediaCanvasNode({ + node, + onCancelGeneration, + selected, +}: { + node: CloneNode; + onCancelGeneration: () => void; + selected: boolean; +}) { const mediaType = getDraftString(node, "mediaType") || node.inputType || "image"; const mediaUrl = getNodeMediaUrl(node); const sourceUrl = getNodeSourceMediaUrl(node); const generated = Boolean(node.draftValues?.generated); + const generationStatus = getDraftString(node, "generationStatus"); + const isGenerating = generationStatus === "pending"; + const isGenerationFailed = generationStatus === "failed"; const annotationMode = Boolean(node.draftValues?.annotationMode); const rotation = Number(node.draftValues?.rotation || 0); const outputSizeLabel = getDraftString(node, "outputSizeLabel"); @@ -6897,7 +7034,14 @@ function PresetMediaCanvasNode({ node, selected }: { node: CloneNode; selected: selected && "outline outline-2 outline-[#8b96ff]" )} > - {isNineGridComposite ? ( + {isGenerating || isGenerationFailed ? ( + + ) : isNineGridComposite ? ( ) : cropTile && mediaUrl && gridCellVisual ? ( @@ -6921,7 +7065,7 @@ function PresetMediaCanvasNode({ node, selected }: { node: CloneNode; selected: 主体 ) : null} - {!generated ? ( + {!generated && !isGenerating && !isGenerationFailed ? ( + + ) : ( + <> + 生成中 {displayProgress}%... + + + )} + + + ); +} + function PureTextResourceCanvasNode({ node, onTextResourceChange,