Files
Genarrative/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts
T
kdletters 8eecd038f5 允许UI素材提取选择模型
为UI素材提取面板接入图片模型选择器,默认使用nanobanana模型。

提取提交链路传递选中模型并按模型计算泥点价格。

后端UI素材提取请求支持模型字段,队列、执行、持久化和响应统一使用选中模型。

同步外部OpenAPI模型字段并补充前端工作流、客户端请求和后端默认模型测试。
2026-06-27 17:51:41 +08:00

1802 lines
56 KiB
TypeScript

import {
type Dispatch,
type MutableRefObject,
type SetStateAction,
useCallback,
useMemo,
} from 'react';
import {
resolveEditorImageReferenceDataUrl,
resolveEditorImageReferenceDataUrlForGeneration,
} from '../../services/image-editor/editorImageReference';
import type {
EditorCanvasGenerationCompletionInput,
EditorAssetSnapshot,
EditorProjectSnapshot,
} from '../../services/image-editor/editorProjectClient';
import {
editEditorImage,
extractEditorUiDesignAssets,
generateEditorBackgroundMusic,
generateEditorCharacterAnimation,
generateEditorIconSpritesheet,
generateEditorImage,
generateEditorSoundEffect,
generateEditorVideo,
loadEditorProject,
} from '../../services/image-editor/editorProjectClient';
import { getExternalGenerationJobStatus } from '../../services/external-generation';
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
import type {
CanvasGenerationDialogState,
CanvasGenerationInputs,
CanvasLayer,
CanvasTool,
CanvasViewport,
CharacterAnimationPanelState,
GenerateDialogState,
QuickEditPanelState,
SidebarPanel,
} from './ImageCanvasEditorTypes';
import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
import {
createAudioResultLayer,
createCharacterAnimationResultLayer,
createGeneratedResultLayer,
createIconSpritesheetResultLayers,
createQuickEditResultLayer,
createVideoResultLayer,
} from './ImageCanvasGenerationLayerModel';
import { chooseGenerationPlacement } from './ImageCanvasGenerationPlacementModel';
import {
buildCharacterAnimationGenerationInputs,
buildEditGenerationInputs,
buildQuickEditGenerationInputs,
buildUiDesignAssetExtractionGenerationInputs,
buildVideoGenerationInputs,
CHARACTER_ANIMATION_MODEL,
DEFAULT_IMAGE_MODEL,
DEFAULT_VIDEO_MODEL,
ICON_FRAME_DISPLAY_SIZE,
ICON_FRAME_ORIGINAL_SIZE,
inferEditorImageAspectRatio,
inferEditorImageSizeLabel,
isCanvasGenerationDialog,
normalizeEditorImageModel,
normalizeQuickEditPromptSourceReference,
resolveCharacterAnimationSourceImageSrc,
resolveEditorImageGenerationPixelSize,
resolveImageGenerationErrorMessage,
} from './ImageCanvasGenerationModel';
import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
import type {
UiAssetExtractionMark,
UiAssetExtractionState,
} from './ImageCanvasUiAssetExtractionModel';
import { resolveUiAssetExtractionGenerationPlan } from './ImageCanvasUiAssetExtractionModel';
import { renderUiDesignAssetExtractionMarkedImage } from './ImageCanvasUiAssetExtractionRasterModel';
type CanvasSize = { width: number; height: number };
type CanvasGenerationDialogUpdater = (
dialog: CanvasGenerationDialogState,
) => CanvasGenerationDialogState | null;
type UiDesignAssetExtractionOptions = {
marks?: UiAssetExtractionMark[];
model?: string;
suppressAlert?: boolean;
};
const EDITOR_GENERATION_QUEUE_POLL_INTERVAL_MS = 1600;
const EDITOR_GENERATION_QUEUE_TIMEOUT_MS = 20 * 60 * 1000;
type GenerationSubmissionWorkflowOptions = {
layers: CanvasLayer[];
canvasSize: CanvasSize;
viewport: CanvasViewport;
layerCounterRef: MutableRefObject<number>;
quickEditPanel: QuickEditPanelState | null;
quickEditSourceLayer: CanvasLayer | null;
quickEditSelectionState: UiAssetExtractionState | null;
canvasGenerationDialogs: CanvasGenerationDialogState[];
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
characterAnimationPanel: CharacterAnimationPanelState | null;
characterAnimationDialog: CanvasGenerationDialogState | null;
characterAnimationSourceLayer: CanvasLayer | null;
setCharacterAnimationPanel: Dispatch<
SetStateAction<CharacterAnimationPanelState | null>
>;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
openCanvasGenerationDialog: (
dialog: Omit<CanvasGenerationDialogState, 'id'>,
) => string;
updateCanvasGenerationDialogById: (
dialogId: string,
updater: CanvasGenerationDialogUpdater,
) => void;
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
getGeneratingDialogPlaceholder: (
dialog: GenerateDialogState,
) => GenerateDialogState['placeholder'];
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
fitLayers: (targetLayers?: CanvasLayer[]) => void;
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
rememberImageModel: (imageModel: string) => void;
projectId?: string | null;
assetFolderId?: string | null;
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
applyProjectSnapshot?: (project: EditorProjectSnapshot) => void;
onWalletBalanceMayHaveChanged?: () => void;
};
async function normalizeImageGenerationReferenceImages(
input: Parameters<typeof generateEditorImage>[0],
) {
if (!input.referenceImageSrcs?.length) {
return input;
}
return {
...input,
referenceImageSrcs: await Promise.all(
input.referenceImageSrcs.map((referenceImageSrc) =>
resolveEditorImageReferenceDataUrlForGeneration(referenceImageSrc),
),
),
};
}
async function normalizeIconSpritesheetReferenceImage(
input: Parameters<typeof generateEditorIconSpritesheet>[0],
) {
return {
...input,
referenceImageSrc: await resolveEditorImageReferenceDataUrlForGeneration(
input.referenceImageSrc,
),
};
}
function delay(ms: number) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function queuedStateFromResponse(
response: { queueState?: ExternalGenerationJobStatusRecord | null } | null,
) {
return response?.queueState ?? null;
}
function notifyWalletBalanceMayHaveChanged(callback?: () => void) {
callback?.();
}
async function runEditorGenerationWithWalletRefresh<T>(
operation: Promise<T>,
onWalletBalanceMayHaveChanged?: () => void,
): Promise<T> {
try {
const response = await operation;
if (
!queuedStateFromResponse(
response as { queueState?: ExternalGenerationJobStatusRecord | null },
)
) {
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
}
return response;
} catch (error) {
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
throw error;
}
}
async function waitForEditorGenerationQueue(
queueState: ExternalGenerationJobStatusRecord,
): Promise<ExternalGenerationJobStatusRecord | null> {
const startedAt = Date.now();
let current = queueState;
while (current.status === 'queued' || current.status === 'running') {
if (Date.now() - startedAt > EDITOR_GENERATION_QUEUE_TIMEOUT_MS) {
return null;
}
await delay(EDITOR_GENERATION_QUEUE_POLL_INTERVAL_MS);
current = (
await getExternalGenerationJobStatus(current.operationId)
).job;
}
if (current.status === 'failed') {
throw new Error(current.error?.trim() || '生成任务失败');
}
return current;
}
async function applyQueuedEditorGenerationProject(
response: { queueState?: ExternalGenerationJobStatusRecord | null },
projectId: string | null | undefined,
applyProjectSnapshot: ((project: EditorProjectSnapshot) => void) | undefined,
onWalletBalanceMayHaveChanged?: () => void,
) {
const queueState = queuedStateFromResponse(response);
if (!queueState) {
return false;
}
let terminalState: ExternalGenerationJobStatusRecord | null = null;
try {
terminalState = await waitForEditorGenerationQueue(queueState);
} finally {
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
}
if (!terminalState) {
return true;
}
if (projectId && applyProjectSnapshot) {
applyProjectSnapshot(await loadEditorProject(projectId));
}
return true;
}
function positiveCanvasSize(value: number | null | undefined, fallback = 1) {
const resolved = Number.isFinite(value) && (value ?? 0) > 0 ? value : fallback;
return Math.max(1, Math.round(resolved ?? 1));
}
function buildRightSideCanvasCompletionPlaceholder(
sourceLayer: CanvasLayer,
size?: {
width?: number;
height?: number;
originalWidth?: number;
originalHeight?: number;
},
): EditorCanvasGenerationCompletionInput['placeholder'] {
const originalWidth = positiveCanvasSize(
size?.originalWidth ?? size?.width,
sourceLayer.originalWidth || sourceLayer.width || 1,
);
const originalHeight = positiveCanvasSize(
size?.originalHeight ?? size?.height,
sourceLayer.originalHeight || sourceLayer.height || 1,
);
const width = positiveCanvasSize(size?.width, originalWidth);
const height = positiveCanvasSize(size?.height, originalHeight);
return {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width,
height,
originalWidth,
originalHeight,
};
}
function isVideoQuickEditSource(layer: CanvasLayer) {
return layer.mediaType === 'video' || layer.assetKind === 'video';
}
function isCharacterAnimationQuickEditSource(layer: CanvasLayer) {
return layer.assetKind === 'character-animation';
}
export function useImageCanvasGenerationSubmissionWorkflow({
layers,
canvasSize,
viewport,
layerCounterRef,
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
canvasGenerationDialogs,
setQuickEditPanel,
characterAnimationPanel,
characterAnimationDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
selectSingleLayer,
fitLayers,
setActiveTool,
setActiveSidebarPanel,
rememberImageModel,
projectId,
assetFolderId,
upsertGeneratedAsset,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
}: GenerationSubmissionWorkflowOptions) {
const addGeneratedLayersToCanvas = useCallback(
(nextLayers: CanvasLayer[]) => {
appendCanvasLayersWithResources(nextLayers);
nextLayers.forEach((layer) => {
const asset = layer.generatedAssetSnapshot;
if (asset) {
upsertGeneratedAsset?.(asset);
}
});
},
[appendCanvasLayersWithResources, upsertGeneratedAsset],
);
const addGeneratedResultLayer = useCallback(
(
generated: Parameters<typeof createGeneratedResultLayer>[0]['generated'],
options: {
sourceLayer?: CanvasLayer;
frame?: GenerateDialogState['placeholder'];
assetKind?: CanvasLayer['assetKind'];
title?: string;
dialogId?: string;
generationInputs?: CanvasGenerationInputs;
} = {},
) => {
if (
options.dialogId &&
!hasCanvasGenerationDialogById(options.dialogId)
) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createGeneratedResultLayer({
generated,
generatedIndex,
canvasSize,
viewport,
sourceLayer: options.sourceLayer,
frame: options.frame,
assetKind: options.assetKind,
title: options.title,
generationInputs: options.generationInputs,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (options.sourceLayer) {
setGenerateDialog(null);
setActiveTool('select');
} else if (options.dialogId) {
updateCanvasGenerationDialogById(options.dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
if (options.sourceLayer) {
fitLayers([options.sourceLayer, nextLayer]);
}
},
[
addGeneratedLayersToCanvas,
canvasSize,
fitLayers,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
setGenerateDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addQuickEditResultLayer = useCallback(
(
generated: Parameters<typeof createQuickEditResultLayer>[0]['generated'],
sourceLayer: CanvasLayer,
generationInputs: CanvasGenerationInputs,
mode: QuickEditPanelState['mode'] = 'quick-edit',
options: {
frame?: GenerateDialogState['placeholder'];
dialogId?: string;
} = {},
) => {
if (
options.dialogId &&
!hasCanvasGenerationDialogById(options.dialogId)
) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createQuickEditResultLayer({
generated,
generatedIndex,
sourceLayer,
generationInputs,
mode,
frame: options.frame,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(mode === 'redraw' ? sourceLayer.id : nextLayer.id);
setActiveSidebarPanel('layers');
if (mode === 'redraw') {
setQuickEditPanel((currentPanel) =>
currentPanel?.sourceLayerId === sourceLayer.id
? {
...currentPanel,
mode: 'redraw',
status: 'idle',
errorMessage: undefined,
}
: currentPanel,
);
} else {
setQuickEditPanel(null);
setActiveTool('select');
if (options.dialogId) {
updateCanvasGenerationDialogById(
options.dialogId,
(currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: false,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}),
);
}
}
fitLayers([sourceLayer, nextLayer]);
},
[
addGeneratedLayersToCanvas,
fitLayers,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
setQuickEditPanel,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
],
);
const addIconSpritesheetResultLayers = useCallback(
(
generated: Parameters<
typeof createIconSpritesheetResultLayers
>[0]['generated'],
iconResults: Parameters<
typeof createIconSpritesheetResultLayers
>[0]['iconResults'],
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
options: {
spritesheetTitle?: string;
} = {},
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return [];
}
const startIndex = layerCounterRef.current + 1;
const nextLayers = createIconSpritesheetResultLayers({
generated,
iconResults,
startIndex,
canvasSize,
viewport,
generationInputs,
frame,
spritesheetTitle: options.spritesheetTitle,
});
if (!nextLayers.length) {
return [];
}
layerCounterRef.current += nextLayers.length;
addGeneratedLayersToCanvas(nextLayers);
selectSingleLayer(nextLayers[0]?.id ?? null);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayers[0]?.id,
errorMessage: undefined,
}));
}
setActiveTool('select');
return nextLayers;
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addVideoResultLayer = useCallback(
(
generated: Parameters<typeof createVideoResultLayer>[0]['generated'],
title: string,
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
options: {
sourceLayer?: CanvasLayer;
assetKind?: CanvasLayer['assetKind'];
} = {},
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createVideoResultLayer({
generated,
generatedIndex,
title,
canvasSize,
viewport,
generationInputs,
frame,
sourceLayer: options.sourceLayer,
assetKind: options.assetKind,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
setActiveTool('select');
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addAudioResultLayer = useCallback(
(
generated: Parameters<typeof createAudioResultLayer>[0]['generated'],
title: string,
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createAudioResultLayer({
generated,
generatedIndex,
title,
canvasSize,
viewport,
generationInputs,
frame,
});
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
errorMessage: undefined,
}));
}
setActiveTool('select');
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const addCharacterAnimationResultLayer = useCallback(
(
generated: Parameters<
typeof createCharacterAnimationResultLayer
>[0]['generated'],
title: string,
generationInputs: CanvasGenerationInputs,
frame?: GenerateDialogState['placeholder'],
dialogId?: string,
options: {
sourceLayer?: CanvasLayer;
} = {},
) => {
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
return;
}
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const nextLayer = createCharacterAnimationResultLayer({
generated,
generatedIndex,
title,
canvasSize,
viewport,
generationInputs,
frame,
sourceLayer: options.sourceLayer,
});
if (!nextLayer) {
return;
}
addGeneratedLayersToCanvas([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (dialogId) {
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
...currentDialog,
status: 'idle',
composerOpen: true,
generatedLayerId: nextLayer.id,
characterAnimationResult: generated,
errorMessage: undefined,
}));
}
setActiveTool('select');
},
[
addGeneratedLayersToCanvas,
canvasSize,
layerCounterRef,
selectSingleLayer,
setActiveSidebarPanel,
setActiveTool,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
viewport,
],
);
const extractUiDesignAssets = useCallback(
async (
sourceLayer: CanvasLayer,
options: UiDesignAssetExtractionOptions = {},
) => {
if (sourceLayer.assetKind !== 'ui-design') {
return;
}
try {
let sourceImageSrc = await resolveEditorImageReferenceDataUrl(
sourceLayer.objectKey?.trim() || sourceLayer.src,
);
if (options.marks?.length) {
sourceImageSrc = await renderUiDesignAssetExtractionMarkedImage({
source: sourceImageSrc,
marks: options.marks,
});
}
const generationInputs =
buildUiDesignAssetExtractionGenerationInputs(sourceLayer);
const extractionPlan = resolveUiAssetExtractionGenerationPlan(
options.marks?.length ?? 0,
);
const extractionFrame = {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width: ICON_FRAME_DISPLAY_SIZE.width,
height: ICON_FRAME_DISPLAY_SIZE.height,
originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
};
const generated = await runEditorGenerationWithWalletRefresh(
extractEditorUiDesignAssets({
sourceImageSrc,
model: normalizeEditorImageModel(
options.model ?? DEFAULT_IMAGE_MODEL,
),
projectId,
generationInputs,
assetFolderId,
spritesheetLabel: `${sourceLayer.title} 素材图集`,
aspectRatio: extractionPlan.aspectRatio,
imageSize: extractionPlan.imageSize,
...(projectId
? {
canvasCompletion: {
title: `${sourceLayer.title} 素材图集`,
placeholder: extractionFrame,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
const nextLayers = addIconSpritesheetResultLayers(
generated,
generated.iconImageSrcs ?? [],
generationInputs,
extractionFrame,
undefined,
{
spritesheetTitle: `${sourceLayer.title} 素材图集`,
},
);
if (nextLayers.length) {
fitLayers([sourceLayer, ...nextLayers]);
}
} catch (error) {
if (options.suppressAlert) {
throw error;
}
window.alert(
error instanceof Error && error.message.trim()
? error.message
: '提取素材失败',
);
}
},
[
addIconSpritesheetResultLayers,
applyProjectSnapshot,
fitLayers,
projectId,
assetFolderId,
onWalletBalanceMayHaveChanged,
],
);
const submitIconSpritesheetGeneration = useCallback(
async (dialog: GenerateDialogState) => {
if (dialog.mode !== 'icon') {
return;
}
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
const setSubmittingIconDialog = (
nextDialog: CanvasGenerationDialogState,
) => {
updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
};
const submissionPlan =
buildIconSpritesheetGenerationSubmissionPlan(dialog);
if (submissionPlan.ok === false) {
if (canvasDialog) {
setSubmittingIconDialog({
...canvasDialog,
status: 'failed',
composerOpen: true,
errorMessage: submissionPlan.errorMessage,
});
}
return;
}
if (!canvasDialog) {
return;
}
setSubmittingIconDialog({
...canvasDialog,
iconDescriptions: submissionPlan.iconDescriptions,
status: 'generating',
composerOpen: false,
errorMessage: undefined,
});
try {
const iconGenerationInput =
await normalizeIconSpritesheetReferenceImage(submissionPlan.input);
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorIconSpritesheet({
...iconGenerationInput,
projectId,
generationInputs: submissionPlan.generationInputs,
assetFolderId,
...(projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog.id,
title: '图标素材图集',
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
rememberImageModel(submissionPlan.rememberImageModel);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addIconSpritesheetResultLayers(
generated,
generated.iconImageSrcs ?? [],
submissionPlan.generationInputs,
canvasCompletionPlaceholder,
canvasDialog.id,
);
} catch (error) {
setSubmittingIconDialog({
...canvasDialog,
iconDescriptions: submissionPlan.iconDescriptions,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
},
[
addIconSpritesheetResultLayers,
applyProjectSnapshot,
getGeneratingDialogPlaceholder,
rememberImageModel,
projectId,
assetFolderId,
updateCanvasGenerationDialogById,
onWalletBalanceMayHaveChanged,
],
);
const submitQuickEdit = useCallback(async () => {
if (!quickEditPanel || !quickEditSourceLayer) {
return;
}
const panelMode = quickEditPanel.mode ?? 'quick-edit';
const quickEditReferences =
panelMode === 'redraw' ? [] : (quickEditPanel.quickEditReferences ?? []);
const sourceReferenceIndex = quickEditReferences.length + 1;
const quickEditAspectRatio =
quickEditPanel.aspectRatio ??
inferEditorImageAspectRatio(
quickEditSourceLayer.originalWidth,
quickEditSourceLayer.originalHeight,
);
const quickEditImageSize =
quickEditPanel.imageSize ??
inferEditorImageSizeLabel(
quickEditSourceLayer.originalWidth,
quickEditSourceLayer.originalHeight,
);
const quickEditOutputSize = resolveEditorImageGenerationPixelSize({
model: quickEditPanel.model,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
});
const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`;
const basePrompt =
quickEditPanel.prompt.trim() ||
(panelMode === 'redraw' ? '重绘图片' : '快速编辑图片');
const normalizedPrompt =
panelMode === 'redraw'
? basePrompt
: normalizeQuickEditPromptSourceReference(
basePrompt,
sourceReferenceIndex,
);
let quickEditDialogId: string | undefined;
if (panelMode === 'quick-edit') {
const quickEditDraft = createQuickEditGenerationDialogDraft({
sourceLayer: quickEditSourceLayer,
prompt: normalizedPrompt,
status: 'generating',
references: quickEditReferences,
model: quickEditPanel.model,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
frame: {
width: quickEditOutputSize.width,
height: quickEditOutputSize.height,
},
});
const quickEditPlacement = quickEditDraft.placeholder
? chooseGenerationPlacement({
canvasSize,
viewport,
frame: quickEditDraft.placeholder,
layers,
generationDialogs: canvasGenerationDialogs,
})
: quickEditDraft.placeholder;
quickEditDialogId = openCanvasGenerationDialog({
...quickEditDraft,
// 中文注释:快速编辑生成结果复用新建图片的避让落点,避免覆盖已有素材。
placeholder: quickEditPlacement,
});
setQuickEditPanel(null);
selectSingleLayer(null);
} else {
setQuickEditPanel({
...quickEditPanel,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
size: quickEditSize,
prompt: normalizedPrompt,
status: 'generating',
errorMessage: undefined,
});
}
try {
const extraReferenceImageSrcs = await Promise.all(
quickEditReferences.map((reference) =>
resolveEditorImageReferenceDataUrl(
reference.objectKey?.trim() || reference.src,
),
),
);
const quickEditFrame = quickEditDialogId
? getGeneratingDialogPlaceholder({
id: quickEditDialogId,
mode: 'quick-edit',
prompt: normalizedPrompt,
status: 'generating',
})
: undefined;
const quickEditCanvasCompletionPlaceholder =
quickEditFrame ??
buildRightSideCanvasCompletionPlaceholder(quickEditSourceLayer, {
width: quickEditOutputSize.width,
height: quickEditOutputSize.height,
originalWidth: quickEditOutputSize.width,
originalHeight: quickEditOutputSize.height,
});
if (
panelMode === 'quick-edit' &&
isCharacterAnimationQuickEditSource(quickEditSourceLayer)
) {
const animationResolution = '480p' as const;
const animationDurationSeconds = 4 as const;
const animationFrameCount = 32 as const;
const generationInputs = buildCharacterAnimationGenerationInputs(
normalizedPrompt,
quickEditSourceLayer,
);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorCharacterAnimation({
sourceLayerId: quickEditSourceLayer.id,
sourceImageSrc:
resolveCharacterAnimationSourceImageSrc(quickEditSourceLayer),
sourceWidth: quickEditSourceLayer.originalWidth,
sourceHeight: quickEditSourceLayer.originalHeight,
promptText: normalizedPrompt,
resolution: animationResolution,
ratio: 'same',
frameCount: animationFrameCount,
durationSeconds: animationDurationSeconds,
model: CHARACTER_ANIMATION_MODEL,
projectId,
generationInputs,
sourceResourceId: quickEditSourceLayer.resourceId,
...(projectId && quickEditCanvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: quickEditDialogId,
title: `${quickEditSourceLayer.title} 快速编辑`,
placeholder: quickEditCanvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addCharacterAnimationResultLayer(
generated,
`${quickEditSourceLayer.title} 快速编辑`,
generationInputs,
quickEditFrame,
quickEditDialogId,
{
sourceLayer: quickEditSourceLayer,
},
);
return;
}
if (
panelMode === 'quick-edit' &&
isVideoQuickEditSource(quickEditSourceLayer)
) {
const videoModel = DEFAULT_VIDEO_MODEL;
const videoResolution = '480p' as const;
const videoDurationSeconds = 4 as const;
const sourceVideoSrc =
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src;
const generationInputs = buildVideoGenerationInputs(
normalizedPrompt,
quickEditReferences,
);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorVideo({
prompt: normalizedPrompt,
model: videoModel,
aspectRatio: '16:9',
durationSeconds: videoDurationSeconds,
resolution: videoResolution,
mode: 'std',
sound: 'off',
...(extraReferenceImageSrcs.length
? { referenceImageSrcs: extraReferenceImageSrcs }
: {}),
referenceVideoSrcs: [sourceVideoSrc],
projectId,
generationInputs,
sourceResourceId: quickEditSourceLayer.resourceId,
assetKind: quickEditSourceLayer.assetKind,
...(projectId && quickEditCanvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: quickEditDialogId,
title: `${quickEditSourceLayer.title} 快速编辑`,
placeholder: quickEditCanvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addVideoResultLayer(
generated,
`${quickEditSourceLayer.title} 快速编辑`,
generationInputs,
quickEditFrame,
quickEditDialogId,
{
sourceLayer: quickEditSourceLayer,
assetKind: quickEditSourceLayer.assetKind,
},
);
return;
}
let sourceReferenceImageSrc = await resolveEditorImageReferenceDataUrl(
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src,
);
const quickEditSelectionMarks =
panelMode === 'quick-edit' &&
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
? quickEditSelectionState.marks
: [];
if (quickEditSelectionMarks.length > 0) {
sourceReferenceImageSrc =
await renderUiDesignAssetExtractionMarkedImage({
source: sourceReferenceImageSrc,
marks: quickEditSelectionMarks,
showIndexLabels: true,
});
}
const generationInputs =
panelMode === 'redraw'
? buildEditGenerationInputs(
'重绘提示词',
normalizedPrompt,
quickEditSourceLayer,
)
: buildQuickEditGenerationInputs(
'快速编辑提示词',
normalizedPrompt,
quickEditSourceLayer,
quickEditReferences,
);
const isCharacterRedraw =
panelMode === 'redraw' &&
quickEditSourceLayer.assetKind === 'character';
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorImage({
prompt: normalizedPrompt,
size: quickEditSize,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
kind: isCharacterRedraw ? 'character' : 'quick-edit',
model: normalizeEditorImageModel(quickEditPanel.model),
referenceImageSrcs: [
...extraReferenceImageSrcs,
sourceReferenceImageSrc,
],
projectId,
assetKind: quickEditSourceLayer.assetKind,
generationInputs,
assetFolderId,
assetLabel: `${quickEditSourceLayer.title} ${
panelMode === 'redraw' ? '重绘' : '快速编辑'
}`,
sourceResourceId: quickEditSourceLayer.resourceId,
...(projectId && quickEditCanvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: quickEditDialogId,
title: `${quickEditSourceLayer.title} ${
panelMode === 'redraw' ? '重绘' : '快速编辑'
}`,
placeholder: quickEditCanvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addQuickEditResultLayer(
generated,
quickEditSourceLayer,
generationInputs,
panelMode,
quickEditDialogId
? {
frame: quickEditFrame,
dialogId: quickEditDialogId,
}
: {},
);
} catch (error) {
if (quickEditDialogId) {
updateCanvasGenerationDialogById(
quickEditDialogId,
(currentDialog) => ({
...currentDialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: false,
errorMessage: resolveImageGenerationErrorMessage(error),
}),
);
}
setQuickEditPanel({
...quickEditPanel,
aspectRatio: quickEditAspectRatio,
imageSize: quickEditImageSize,
size: quickEditSize,
prompt: normalizedPrompt,
status: 'failed',
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
}, [
addCharacterAnimationResultLayer,
addQuickEditResultLayer,
addVideoResultLayer,
applyProjectSnapshot,
canvasGenerationDialogs,
canvasSize,
getGeneratingDialogPlaceholder,
layers,
openCanvasGenerationDialog,
projectId,
assetFolderId,
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
selectSingleLayer,
setQuickEditPanel,
updateCanvasGenerationDialogById,
viewport,
onWalletBalanceMayHaveChanged,
]);
const submitImageGeneration = useCallback(
async (dialog: GenerateDialogState) => {
const normalizedPrompt =
dialog.prompt.trim() ||
(dialog.mode === 'edit'
? '修改当前图片'
: dialog.mode === 'audio-sound-effect'
? '游戏音效'
: dialog.mode === 'audio-background-music'
? '游戏背景音乐'
: 'AI 生成图片');
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: normalizedPrompt,
status: 'generating',
composerOpen: false,
}));
} else {
setGenerateDialog({
...dialog,
prompt: normalizedPrompt,
status: 'generating',
composerOpen: dialog.mode === 'edit',
});
}
try {
const submissionPlan = buildImageGenerationSubmissionPlan({
dialog,
layers,
nextGeneratedIndex: layerCounterRef.current + 1,
});
if (submissionPlan.kind === 'edit') {
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
submissionPlan.sourceLayer.objectKey?.trim() ||
submissionPlan.sourceLayer.src,
);
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const editCanvasCompletionPlaceholder =
canvasCompletionPlaceholder ??
buildRightSideCanvasCompletionPlaceholder(
submissionPlan.sourceLayer,
{
width: 1024,
height: 1024,
originalWidth: 1024,
originalHeight: 1024,
},
);
const generated = await runEditorGenerationWithWalletRefresh(
editEditorImage({
prompt: submissionPlan.normalizedPrompt,
sourceImageSrc: referenceImageSrc,
projectId,
assetKind: submissionPlan.sourceLayer.assetKind,
generationInputs: submissionPlan.generationInputs,
assetFolderId,
assetLabel: `${submissionPlan.sourceLayer.title} 修改结果`,
sourceResourceId: submissionPlan.sourceLayer.resourceId,
...(projectId && editCanvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog?.id,
title: `${submissionPlan.sourceLayer.title} 修改结果`,
placeholder: editCanvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addGeneratedResultLayer(generated, {
sourceLayer: submissionPlan.sourceLayer,
generationInputs: submissionPlan.generationInputs,
});
} else if (submissionPlan.kind === 'quick-edit') {
const imageGenerationInput =
await normalizeImageGenerationReferenceImages(submissionPlan.input);
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorImage({
...imageGenerationInput,
projectId,
assetKind: submissionPlan.result.assetKind,
generationInputs: submissionPlan.result.generationInputs,
assetFolderId,
assetLabel: submissionPlan.result.title,
sourceResourceId: submissionPlan.sourceLayer.resourceId,
...(projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog?.id,
title: submissionPlan.result.title,
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (submissionPlan.rememberImageModel) {
rememberImageModel(submissionPlan.rememberImageModel);
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
if (generated.asset) {
upsertGeneratedAsset?.(generated.asset);
}
return;
}
addQuickEditResultLayer(
generated,
submissionPlan.sourceLayer,
submissionPlan.result.generationInputs,
'quick-edit',
{
frame: canvasCompletionPlaceholder,
dialogId: canvasDialog?.id,
},
);
} else if (submissionPlan.kind === 'video') {
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorVideo({
...submissionPlan.input,
projectId,
generationInputs: submissionPlan.result.generationInputs,
...(projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog?.id,
title: submissionPlan.result.title,
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addVideoResultLayer(
generated,
submissionPlan.result.title,
submissionPlan.result.generationInputs,
canvasCompletionPlaceholder,
canvasDialog?.id,
);
} else if (submissionPlan.kind === 'audio') {
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const generated = await runEditorGenerationWithWalletRefresh(
submissionPlan.audioKind === 'sound-effect'
? generateEditorSoundEffect({
...submissionPlan.input,
projectId,
generationInputs: submissionPlan.result.generationInputs,
...(projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog?.id,
title: submissionPlan.result.title,
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
})
: generateEditorBackgroundMusic({
...submissionPlan.input,
projectId,
generationInputs: submissionPlan.result.generationInputs,
...(projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog?.id,
title: submissionPlan.result.title,
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addAudioResultLayer(
generated,
submissionPlan.result.title,
submissionPlan.result.generationInputs,
canvasCompletionPlaceholder,
canvasDialog?.id,
);
} else {
const imageGenerationInput =
await normalizeImageGenerationReferenceImages(submissionPlan.input);
const resultTitle =
submissionPlan.result.title ??
`生成图片 ${layerCounterRef.current + 1}`;
const canvasCompletionPlaceholder =
getGeneratingDialogPlaceholder(dialog);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorImage({
...imageGenerationInput,
projectId,
assetKind: submissionPlan.result.assetKind,
generationInputs: submissionPlan.result.generationInputs,
assetFolderId,
assetLabel: resultTitle,
...(projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog?.id,
title: resultTitle,
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (submissionPlan.rememberImageModel) {
rememberImageModel(submissionPlan.rememberImageModel);
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
if (generated.asset) {
upsertGeneratedAsset?.(generated.asset);
}
return;
}
if (canvasDialog && projectId) {
updateCanvasGenerationDialogById(
canvasDialog.id,
(currentDialog) =>
currentDialog.generatedLayerId
? currentDialog
: {
...currentDialog,
status: 'idle',
composerOpen: true,
errorMessage: undefined,
},
);
return;
}
addGeneratedResultLayer(generated, {
frame: canvasCompletionPlaceholder,
assetKind: submissionPlan.result.assetKind,
title: resultTitle,
dialogId: canvasDialog?.id,
generationInputs: submissionPlan.result.generationInputs,
});
}
} catch (error) {
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, () => ({
...canvasDialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
}));
} else {
setGenerateDialog({
...dialog,
prompt: normalizedPrompt,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error),
});
}
}
},
[
addGeneratedResultLayer,
addAudioResultLayer,
addVideoResultLayer,
getGeneratingDialogPlaceholder,
layerCounterRef,
layers,
rememberImageModel,
projectId,
assetFolderId,
applyProjectSnapshot,
setGenerateDialog,
updateCanvasGenerationDialogById,
upsertGeneratedAsset,
onWalletBalanceMayHaveChanged,
],
);
const submitCharacterAnimation = useCallback(async () => {
if (!characterAnimationPanel || !characterAnimationSourceLayer) {
return;
}
if (
characterAnimationPanel.status === 'generating' ||
(characterAnimationDialog?.mode === 'character-animation' &&
characterAnimationDialog.status === 'generating')
) {
return;
}
const submissionPlan = buildCharacterAnimationSubmissionPlan({
panel: characterAnimationPanel,
sourceLayer: characterAnimationSourceLayer,
});
const canvasDialog = characterAnimationDialog;
if (canvasDialog?.mode === 'character-animation') {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: submissionPlan.promptText,
characterAnimationResolution: characterAnimationPanel.resolution,
characterAnimationRatio: characterAnimationPanel.ratio,
characterAnimationFrameCount: characterAnimationPanel.frameCount,
characterAnimationDurationSeconds:
characterAnimationPanel.durationSeconds,
characterAnimationResult: undefined,
status: 'generating',
composerOpen: false,
errorMessage: undefined,
}));
}
const nextPanel = {
...characterAnimationPanel,
promptText: submissionPlan.promptText,
status: 'generating' as const,
errorMessage: undefined,
result: undefined,
};
if (!canvasDialog) {
setCharacterAnimationPanel(nextPanel);
}
try {
const generationInputs = buildCharacterAnimationGenerationInputs(
submissionPlan.promptText,
characterAnimationSourceLayer,
);
const canvasCompletionPlaceholder = canvasDialog
? getGeneratingDialogPlaceholder(canvasDialog)
: undefined;
const result = await runEditorGenerationWithWalletRefresh(
generateEditorCharacterAnimation({
...submissionPlan.input,
projectId,
generationInputs,
sourceResourceId: characterAnimationSourceLayer.resourceId,
...(canvasDialog && projectId && canvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: canvasDialog.id,
title: '角色动作',
placeholder: canvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
result,
projectId,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (result.project && applyProjectSnapshot) {
applyProjectSnapshot(result.project);
return;
}
if (canvasDialog) {
addCharacterAnimationResultLayer(
result,
'角色动作',
generationInputs,
canvasCompletionPlaceholder,
canvasDialog.id,
{
sourceLayer: characterAnimationSourceLayer,
},
);
return;
}
setCharacterAnimationPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
status: 'completed',
result,
}
: currentPanel,
);
} catch (error) {
if (canvasDialog) {
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
...currentDialog,
prompt: submissionPlan.promptText,
characterAnimationResolution: characterAnimationPanel.resolution,
characterAnimationRatio: characterAnimationPanel.ratio,
characterAnimationFrameCount: characterAnimationPanel.frameCount,
characterAnimationDurationSeconds:
characterAnimationPanel.durationSeconds,
status: 'failed',
composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(
error,
'生成角色动画失败',
),
}));
return;
}
setCharacterAnimationPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
status: 'failed',
errorMessage: resolveImageGenerationErrorMessage(
error,
'生成角色动画失败',
),
}
: currentPanel,
);
}
}, [
addCharacterAnimationResultLayer,
characterAnimationDialog,
characterAnimationPanel,
characterAnimationSourceLayer,
getGeneratingDialogPlaceholder,
applyProjectSnapshot,
projectId,
setCharacterAnimationPanel,
updateCanvasGenerationDialogById,
onWalletBalanceMayHaveChanged,
]);
return useMemo(
() => ({
submitIconSpritesheetGeneration,
extractUiDesignAssets,
submitQuickEdit,
submitImageGeneration,
submitCharacterAnimation,
}),
[
submitCharacterAnimation,
extractUiDesignAssets,
submitIconSpritesheetGeneration,
submitImageGeneration,
submitQuickEdit,
],
);
}