Add canvas generation pending placeholders
This commit is contained in:
parent
e21f99eaca
commit
f3869f5e63
@ -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<HTMLDivElement>) => void;
|
||||
onModuleAction: (actionLabel: TargetModuleActionLabel) => void;
|
||||
onOpenConnectionMenu: (event: ReactMouseEvent<HTMLButtonElement>, side: ConnectionSide) => void;
|
||||
@ -6377,7 +6503,7 @@ function CloneCanvasNode({
|
||||
if (node.kind === "media") {
|
||||
return (
|
||||
<CanvasNodeShell node={node} onContextMenu={onContextMenu} onOpenConnectionMenu={onOpenConnectionMenu} onPointerDown={onPointerDown} selected={selected}>
|
||||
<PresetMediaCanvasNode node={node} selected={selected} />
|
||||
<PresetMediaCanvasNode node={node} onCancelGeneration={onCancelGeneration} selected={selected} />
|
||||
</CanvasNodeShell>
|
||||
);
|
||||
}
|
||||
@ -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 ? (
|
||||
<GenerationPendingPreview
|
||||
errorMessage={getDraftString(node, "generationError")}
|
||||
onCancel={onCancelGeneration}
|
||||
progress={getDraftNumber(node, "generationProgress", 2)}
|
||||
status={generationStatus}
|
||||
/>
|
||||
) : isNineGridComposite ? (
|
||||
<NineGridCompositePreview node={node} sourceUrl={sourceUrl || mediaUrl} />
|
||||
) : cropTile && mediaUrl && gridCellVisual ? (
|
||||
<GridCellImage alt={node.title} src={mediaUrl} visual={gridCellVisual} />
|
||||
@ -6921,7 +7065,7 @@ function PresetMediaCanvasNode({ node, selected }: { node: CloneNode; selected:
|
||||
<span className="absolute left-[38%] top-[calc(32%-26px)] rounded-[7px] bg-black/68 px-2 py-1 text-[11px] text-white">主体</span>
|
||||
</div>
|
||||
) : null}
|
||||
{!generated ? (
|
||||
{!generated && !isGenerating && !isGenerationFailed ? (
|
||||
<button
|
||||
className="absolute right-3 top-3 flex h-10 w-10 items-center justify-center rounded-[8px] bg-black/56 text-white/90"
|
||||
onClick={() => toast.info(`${node.title}素材替换接口待接入。`)}
|
||||
@ -6936,6 +7080,71 @@ function PresetMediaCanvasNode({ node, selected }: { node: CloneNode; selected:
|
||||
);
|
||||
}
|
||||
|
||||
function GenerationPendingPreview({
|
||||
errorMessage,
|
||||
onCancel,
|
||||
progress,
|
||||
status,
|
||||
}: {
|
||||
errorMessage?: string;
|
||||
onCancel: () => void;
|
||||
progress: number;
|
||||
status: string;
|
||||
}) {
|
||||
const displayProgress = Math.max(2, Math.min(99, Math.round(progress)));
|
||||
const failed = status === "failed";
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center overflow-hidden bg-[#303030]">
|
||||
<style>{`
|
||||
@keyframes libtv-loading-sweep {
|
||||
0% { transform: translateX(0); opacity: 0; }
|
||||
18% { opacity: 1; }
|
||||
100% { transform: translateX(300%); opacity: 0; }
|
||||
}
|
||||
`}</style>
|
||||
{!failed ? (
|
||||
<div className="absolute inset-0 opacity-45">
|
||||
<span className="absolute left-[-30%] top-0 h-full w-[44%] animate-[libtv-loading-sweep_1.8s_ease-in-out_infinite] bg-gradient-to-r from-transparent via-white/8 to-transparent" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="relative flex h-[34px] min-w-[164px] items-center justify-center gap-3 rounded-[7px] border border-white/14 bg-[#1f1f1f]/86 px-5 text-[14px] font-semibold text-white shadow-[0_10px_28px_rgba(0,0,0,0.34)]">
|
||||
{failed ? (
|
||||
<>
|
||||
<span className="text-white/78">{errorMessage || "生成失败"}</span>
|
||||
<button
|
||||
className="text-white/44 transition hover:text-white/80"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
移除
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span>生成中 {displayProgress}%...</span>
|
||||
<button
|
||||
className="font-medium text-white/42 transition hover:text-white/80"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
type="button"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PureTextResourceCanvasNode({
|
||||
node,
|
||||
onTextResourceChange,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user