import { type Dispatch, type MutableRefObject, type SetStateAction, useCallback, useMemo, } from 'react'; import { resolveEditorImageReferenceDataUrl, resolveEditorImageReferenceDataUrlForGeneration, } from '../../services/image-editor/editorImageReference'; import { editEditorImage, extractEditorUiDesignAssets, generateEditorBackgroundMusic, generateEditorCharacterAnimation, generateEditorIconSpritesheet, generateEditorImage, generateEditorSoundEffect, generateEditorVideo, } from '../../services/image-editor/editorProjectClient'; import type { CanvasGenerationDialogState, CanvasGenerationInputs, CanvasLayer, CanvasTool, CanvasViewport, CharacterAnimationPanelState, GenerateDialogState, QuickEditPanelState, SidebarPanel, } from './ImageCanvasEditorTypes'; import { createAudioResultLayer, createCharacterAnimationResultLayer, createGeneratedResultLayer, createIconSpritesheetResultLayers, createQuickEditResultLayer, createVideoResultLayer, } from './ImageCanvasGenerationLayerModel'; import { buildCharacterAnimationGenerationInputs, buildEditGenerationInputs, buildQuickEditGenerationInputs, buildUiDesignAssetExtractionGenerationInputs, ICON_FRAME_DISPLAY_SIZE, ICON_FRAME_ORIGINAL_SIZE, isCanvasGenerationDialog, normalizeEditorImageModel, normalizeQuickEditPromptSourceReference, resolveImageGenerationErrorMessage, } from './ImageCanvasGenerationModel'; import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel'; import { buildCharacterAnimationSubmissionPlan, buildIconSpritesheetGenerationSubmissionPlan, buildImageGenerationSubmissionPlan, } from './ImageCanvasGenerationSubmissionModel'; type CanvasSize = { width: number; height: number }; type CanvasGenerationDialogUpdater = ( dialog: CanvasGenerationDialogState, ) => CanvasGenerationDialogState | null; type GenerationSubmissionWorkflowOptions = { layers: CanvasLayer[]; canvasSize: CanvasSize; viewport: CanvasViewport; layerCounterRef: MutableRefObject; quickEditPanel: QuickEditPanelState | null; quickEditSourceLayer: CanvasLayer | null; setQuickEditPanel: Dispatch>; characterAnimationPanel: CharacterAnimationPanelState | null; characterAnimationDialog: CanvasGenerationDialogState | null; characterAnimationSourceLayer: CanvasLayer | null; setCharacterAnimationPanel: Dispatch< SetStateAction >; setGenerateDialog: Dispatch>; openCanvasGenerationDialog: ( dialog: Omit, ) => 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>; setActiveSidebarPanel: Dispatch>; rememberImageModel: (imageModel: string) => void; persistGeneratedAsset?: (layer: CanvasLayer) => void; }; async function normalizePublicationReferenceImages( input: Parameters[0], ) { if ( input.kind !== 'publication-material' || !input.referenceImageSrcs?.length ) { return input; } return { ...input, referenceImageSrcs: await Promise.all( input.referenceImageSrcs.map((referenceImageSrc) => resolveEditorImageReferenceDataUrlForGeneration(referenceImageSrc), ), ), }; } export function useImageCanvasGenerationSubmissionWorkflow({ layers, canvasSize, viewport, layerCounterRef, quickEditPanel, quickEditSourceLayer, setQuickEditPanel, characterAnimationPanel, characterAnimationDialog, characterAnimationSourceLayer, setCharacterAnimationPanel, setGenerateDialog, openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, selectSingleLayer, fitLayers, setActiveTool, setActiveSidebarPanel, rememberImageModel, persistGeneratedAsset, }: GenerationSubmissionWorkflowOptions) { const addGeneratedLayersToCanvas = useCallback( (nextLayers: CanvasLayer[]) => { appendCanvasLayersWithResources(nextLayers); nextLayers.forEach((layer) => persistGeneratedAsset?.(layer)); }, [appendCanvasLayersWithResources, persistGeneratedAsset], ); const addGeneratedResultLayer = useCallback( ( generated: Parameters[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[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[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 = createVideoResultLayer({ 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 addAudioResultLayer = useCallback( ( generated: Parameters[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, ) => { if (dialogId && !hasCanvasGenerationDialogById(dialogId)) { return; } layerCounterRef.current += 1; const generatedIndex = layerCounterRef.current; const nextLayer = createCharacterAnimationResultLayer({ generated, generatedIndex, title, canvasSize, viewport, generationInputs, frame, }); 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) => { if (sourceLayer.assetKind !== 'ui-design') { return; } try { const sourceImageSrc = await resolveEditorImageReferenceDataUrl( sourceLayer.objectKey?.trim() || sourceLayer.src, ); const generated = await extractEditorUiDesignAssets({ sourceImageSrc, }); 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 nextLayers = addIconSpritesheetResultLayers( generated, generated.iconImageSrcs, buildUiDesignAssetExtractionGenerationInputs(sourceLayer), extractionFrame, undefined, { spritesheetTitle: `${sourceLayer.title} 素材图集`, }, ); if (nextLayers.length) { fitLayers([sourceLayer, ...nextLayers]); } } catch (error) { window.alert( error instanceof Error && error.message.trim() ? error.message : '提取素材失败', ); } }, [addIconSpritesheetResultLayers, fitLayers], ); 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 generated = await generateEditorIconSpritesheet( submissionPlan.input, ); rememberImageModel(submissionPlan.rememberImageModel); addIconSpritesheetResultLayers( generated, generated.iconImageSrcs, submissionPlan.generationInputs, getGeneratingDialogPlaceholder(dialog), canvasDialog.id, ); } catch (error) { setSubmittingIconDialog({ ...canvasDialog, iconDescriptions: submissionPlan.iconDescriptions, status: 'failed', composerOpen: true, errorMessage: resolveImageGenerationErrorMessage(error), }); } }, [ addIconSpritesheetResultLayers, getGeneratingDialogPlaceholder, rememberImageModel, updateCanvasGenerationDialogById, ], ); 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 basePrompt = quickEditPanel.prompt.trim() || (panelMode === 'redraw' ? '重绘图片' : '快速编辑图片'); const normalizedPrompt = panelMode === 'redraw' ? basePrompt : normalizeQuickEditPromptSourceReference( basePrompt, sourceReferenceIndex, ); let quickEditDialogId: string | undefined; if (panelMode === 'quick-edit') { quickEditDialogId = openCanvasGenerationDialog( createQuickEditGenerationDialogDraft({ sourceLayer: quickEditSourceLayer, prompt: normalizedPrompt, status: 'generating', references: quickEditReferences, }), ); setQuickEditPanel(null); selectSingleLayer(null); } else { setQuickEditPanel({ ...quickEditPanel, prompt: normalizedPrompt, status: 'generating', errorMessage: undefined, }); } try { const extraReferenceImageSrcs = await Promise.all( quickEditReferences.map((reference) => resolveEditorImageReferenceDataUrl( reference.objectKey?.trim() || reference.src, ), ), ); const sourceReferenceImageSrc = await resolveEditorImageReferenceDataUrl( quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src, ); const generated = await generateEditorImage({ prompt: normalizedPrompt, size: quickEditPanel.size, kind: 'quick-edit', model: normalizeEditorImageModel(quickEditPanel.model), referenceImageSrcs: [ ...extraReferenceImageSrcs, sourceReferenceImageSrc, ], }); addQuickEditResultLayer( generated, quickEditSourceLayer, panelMode === 'redraw' ? buildEditGenerationInputs( '重绘提示词', normalizedPrompt, quickEditSourceLayer, ) : buildQuickEditGenerationInputs( '快速编辑提示词', normalizedPrompt, quickEditSourceLayer, quickEditReferences, ), panelMode, quickEditDialogId ? { frame: getGeneratingDialogPlaceholder({ id: quickEditDialogId, mode: 'quick-edit', prompt: normalizedPrompt, status: 'generating', }), dialogId: quickEditDialogId, } : {}, ); } catch (error) { if (quickEditDialogId) { updateCanvasGenerationDialogById(quickEditDialogId, (currentDialog) => ({ ...currentDialog, prompt: normalizedPrompt, status: 'failed', composerOpen: false, errorMessage: resolveImageGenerationErrorMessage(error), })); } setQuickEditPanel({ ...quickEditPanel, prompt: normalizedPrompt, status: 'failed', errorMessage: resolveImageGenerationErrorMessage(error), }); } }, [ addQuickEditResultLayer, getGeneratingDialogPlaceholder, openCanvasGenerationDialog, quickEditPanel, quickEditSourceLayer, setQuickEditPanel, updateCanvasGenerationDialogById, ]); 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.src, ); const generated = await editEditorImage({ prompt: submissionPlan.normalizedPrompt, sourceImageSrc: referenceImageSrc, }); addGeneratedResultLayer(generated, { sourceLayer: submissionPlan.sourceLayer, generationInputs: submissionPlan.generationInputs, }); } else if (submissionPlan.kind === 'video') { const generated = await generateEditorVideo(submissionPlan.input); addVideoResultLayer( generated, submissionPlan.result.title, submissionPlan.result.generationInputs, getGeneratingDialogPlaceholder(dialog), canvasDialog?.id, ); } else if (submissionPlan.kind === 'audio') { const generated = submissionPlan.audioKind === 'sound-effect' ? await generateEditorSoundEffect(submissionPlan.input) : await generateEditorBackgroundMusic(submissionPlan.input); addAudioResultLayer( generated, submissionPlan.result.title, submissionPlan.result.generationInputs, getGeneratingDialogPlaceholder(dialog), canvasDialog?.id, ); } else { const imageGenerationInput = submissionPlan.input.kind === 'publication-material' ? await normalizePublicationReferenceImages(submissionPlan.input) : submissionPlan.input; const generated = await generateEditorImage(imageGenerationInput); if (submissionPlan.rememberImageModel) { rememberImageModel(submissionPlan.rememberImageModel); } addGeneratedResultLayer(generated, { frame: getGeneratingDialogPlaceholder(dialog), assetKind: submissionPlan.result.assetKind, title: submissionPlan.result.title, 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, setGenerateDialog, updateCanvasGenerationDialogById, ], ); const submitCharacterAnimation = useCallback(async () => { if (!characterAnimationPanel || !characterAnimationSourceLayer) { 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 result = await generateEditorCharacterAnimation( submissionPlan.input, ); if (canvasDialog) { addCharacterAnimationResultLayer( result, '角色动作', buildCharacterAnimationGenerationInputs( submissionPlan.promptText, characterAnimationSourceLayer, ), getGeneratingDialogPlaceholder(canvasDialog), canvasDialog.id, ); 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: error instanceof Error && error.message.trim() ? error.message : '生成角色动画失败', })); return; } setCharacterAnimationPanel((currentPanel) => currentPanel ? { ...currentPanel, status: 'failed', errorMessage: error instanceof Error && error.message.trim() ? error.message : '生成角色动画失败', } : currentPanel, ); } }, [ addCharacterAnimationResultLayer, characterAnimationDialog, characterAnimationPanel, characterAnimationSourceLayer, getGeneratingDialogPlaceholder, setCharacterAnimationPanel, updateCanvasGenerationDialogById, ]); return useMemo( () => ({ submitIconSpritesheetGeneration, extractUiDesignAssets, submitQuickEdit, submitImageGeneration, submitCharacterAnimation, }), [ submitCharacterAnimation, extractUiDesignAssets, submitIconSpritesheetGeneration, submitImageGeneration, submitQuickEdit, ], ); }