Files
Genarrative/src/components/image-editor/useCanvasGenerationDialogs.ts
T
lhk229 281c84b7bf
Project CI / Frontend tests (push) Failing after 2m11s
Project CI / Backend tests (push) Successful in 3m33s
Project CI / Native shell tests (push) Successful in 13m57s
Project CI / Repository checks (push) Successful in 1m15s
BGM生成路径优化 (#142)
Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/142
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
2026-08-06 17:54:40 +08:00

462 lines
16 KiB
TypeScript

import {
type Dispatch,
type SetStateAction,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import type {
CanvasGenerationDialogState,
GenerateDialogState,
} from './ImageCanvasEditorTypes';
import { isCanvasGenerationDialog } from './ImageCanvasGenerationModel';
type CanvasGenerationDialogUpdater = (
dialog: CanvasGenerationDialogState,
) => CanvasGenerationDialogState | null;
export type CanvasGenerationDialogDraft = Omit<
CanvasGenerationDialogState,
'id'
> & {
id?: string;
};
/**
* 中文注释:删除生成占位前是否需要二次确认。
*
* 确认弹窗讲的是「已消耗的泥点不会返还」,只对计费生成成立。完美像素
* `generation_cost_mud_points = 0`,删除占位也不撤销任何在途请求——它没有取消接口,结果照常
* 落库并进素材库,服务端 completion 发现 dialog 已不在会返回 DialogMissing 并由客户端提示。
* 所以完美像素占位在任何状态都直接删,不额外解释。
*
* 判据必须看 `perfectPixelOperationId` 而**不是** `perfectPixelOperation`:后者要到源图解析 /
* 直传完成后才写入,那一段预算最长 90 秒(未登记的本地图片要走 ticket → PUT → confirm),
* 期间占位是 `generating` 且没有账本,按账本判会让用户删一个免费操作时看到「已消耗的泥点
* 不会返还」。marker 在占位创建那一刻就写上,覆盖完整生命周期。
*
* 更一般地:这里问的是「**这次生成计不计费**」,账本的有无只是它在某一段时间内的代理。
* 用短寿命字段的存在性去判断长期属性,正是本仓库反复出错的形状。
*/
export function requiresGenerationDeleteConfirmation(
dialog: CanvasGenerationDialogState,
) {
return dialog.status === 'generating' && !dialog.perfectPixelOperationId;
}
function withGenerationTimestamps<T extends GenerateDialogState | null>(
nextDialog: T,
previousDialog?: GenerateDialogState | null,
): T {
if (!nextDialog) {
return nextDialog;
}
if (nextDialog.status !== 'generating') {
const shouldMarkFinished =
nextDialog.status === 'failed' || Boolean(nextDialog.generatedLayerId);
return {
...nextDialog,
generationFinishedAt: shouldMarkFinished
? (nextDialog.generationFinishedAt ??
previousDialog?.generationFinishedAt ??
Date.now())
: nextDialog.generationFinishedAt,
};
}
return {
...nextDialog,
generationStartedAt:
nextDialog.generationStartedAt ??
previousDialog?.generationStartedAt ??
Date.now(),
generationFinishedAt: undefined,
};
}
function generationDialogContextId(dialog: GenerateDialogState | null) {
if (!dialog) {
return null;
}
return isCanvasGenerationDialog(dialog)
? dialog.id
: `${dialog.mode}:${dialog.sourceLayerId ?? 'draft'}`;
}
export function useCanvasGenerationDialogs({
onActivate,
isContextMutationLocked,
onContextMutationRejected,
}: {
onActivate?: () => void;
isContextMutationLocked?: () => boolean;
onContextMutationRejected?: () => void;
} = {}) {
const generationDialogCounterRef = useRef(0);
const generateDialogRef = useRef<GenerateDialogState | null>(null);
const inactiveGenerateDialogsRef = useRef<CanvasGenerationDialogState[]>([]);
const [generateDialog, setGenerateDialogState] =
useState<GenerateDialogState | null>(null);
const [inactiveGenerateDialogs, setInactiveGenerateDialogs] = useState<
CanvasGenerationDialogState[]
>([]);
generateDialogRef.current = generateDialog;
inactiveGenerateDialogsRef.current = inactiveGenerateDialogs;
const activeCanvasGenerationDialog = isCanvasGenerationDialog(generateDialog)
? generateDialog
: null;
const rejectContextMutation = useCallback(() => {
if (!isContextMutationLocked?.()) {
return false;
}
onContextMutationRejected?.();
return true;
}, [isContextMutationLocked, onContextMutationRejected]);
const setGenerateDialog = useCallback<
Dispatch<SetStateAction<GenerateDialogState | null>>
>(
(nextDialogOrUpdater) => {
const currentDialog = generateDialogRef.current;
const nextDialog =
typeof nextDialogOrUpdater === 'function'
? nextDialogOrUpdater(currentDialog)
: nextDialogOrUpdater;
const nextDialogWithTimestamps = withGenerationTimestamps(
nextDialog,
currentDialog,
);
const currentContextId = generationDialogContextId(currentDialog);
const nextContextId = generationDialogContextId(nextDialogWithTimestamps);
if (currentContextId !== nextContextId && rejectContextMutation()) {
return;
}
generateDialogRef.current = nextDialogWithTimestamps;
setGenerateDialogState(nextDialogWithTimestamps);
},
[rejectContextMutation],
);
const canvasGenerationDialogs = useMemo(
() =>
activeCanvasGenerationDialog
? [...inactiveGenerateDialogs, activeCanvasGenerationDialog]
: inactiveGenerateDialogs,
[activeCanvasGenerationDialog, inactiveGenerateDialogs],
);
const getCanvasGenerationDialogsSnapshot = useCallback(() => {
const currentDialog = generateDialogRef.current;
return [
...inactiveGenerateDialogsRef.current,
...(isCanvasGenerationDialog(currentDialog) ? [currentDialog] : []),
];
}, []);
const createGenerationDialogId = useCallback(() => {
generationDialogCounterRef.current += 1;
return `generation-dialog-${generationDialogCounterRef.current}`;
}, []);
const archiveActiveCanvasGenerationDialog = useCallback(() => {
const currentDialog = generateDialogRef.current;
if (!isCanvasGenerationDialog(currentDialog)) {
return;
}
if (rejectContextMutation()) {
return;
}
const nextInactiveDialogs = inactiveGenerateDialogsRef.current.some(
(dialog) => dialog.id === currentDialog.id,
)
? inactiveGenerateDialogsRef.current
: [
...inactiveGenerateDialogsRef.current,
{
...currentDialog,
composerOpen: false,
},
];
inactiveGenerateDialogsRef.current = nextInactiveDialogs;
setInactiveGenerateDialogs(nextInactiveDialogs);
}, [rejectContextMutation]);
const openCanvasGenerationDialog = useCallback(
(dialog: CanvasGenerationDialogDraft) => {
const currentDialog = generateDialogRef.current;
if (rejectContextMutation()) {
return isCanvasGenerationDialog(currentDialog) ? currentDialog.id : '';
}
if (isCanvasGenerationDialog(currentDialog)) {
inactiveGenerateDialogsRef.current =
inactiveGenerateDialogsRef.current.some(
(existingDialog) => existingDialog.id === currentDialog.id,
)
? inactiveGenerateDialogsRef.current
: [
...inactiveGenerateDialogsRef.current,
{
...currentDialog,
composerOpen: false,
},
];
}
archiveActiveCanvasGenerationDialog();
const requestedId = dialog.id?.trim();
const requestedIdAlreadyExists =
Boolean(requestedId) &&
getCanvasGenerationDialogsSnapshot().some(
(existingDialog) => existingDialog.id === requestedId,
);
const id =
requestedId && !requestedIdAlreadyExists
? requestedId
: createGenerationDialogId();
const { id: _requestedId, ...dialogWithoutId } = dialog;
const nextDialog = withGenerationTimestamps({
...dialogWithoutId,
id,
});
generateDialogRef.current = nextDialog;
setGenerateDialogState(nextDialog);
return id;
},
[
archiveActiveCanvasGenerationDialog,
createGenerationDialogId,
getCanvasGenerationDialogsSnapshot,
rejectContextMutation,
],
);
const updateCanvasGenerationDialogById = useCallback(
(dialogId: string, updater: CanvasGenerationDialogUpdater) => {
const currentDialogRef = generateDialogRef.current;
if (
isCanvasGenerationDialog(currentDialogRef) &&
currentDialogRef.id === dialogId &&
updater(currentDialogRef) === null &&
rejectContextMutation()
) {
return;
}
if (
isCanvasGenerationDialog(currentDialogRef) &&
currentDialogRef.id === dialogId
) {
generateDialogRef.current = withGenerationTimestamps(
updater(currentDialogRef),
currentDialogRef,
);
}
inactiveGenerateDialogsRef.current =
inactiveGenerateDialogsRef.current.flatMap((dialog) => {
if (dialog.id !== dialogId) {
return [dialog];
}
const nextDialog = withGenerationTimestamps(updater(dialog), dialog);
return nextDialog ? [nextDialog] : [];
});
setGenerateDialogState((currentDialog) => {
if (
!isCanvasGenerationDialog(currentDialog) ||
currentDialog.id !== dialogId
) {
return currentDialog;
}
return withGenerationTimestamps(updater(currentDialog), currentDialog);
});
setInactiveGenerateDialogs((currentDialogs) =>
currentDialogs.flatMap((dialog) => {
if (dialog.id !== dialogId) {
return [dialog];
}
const nextDialog = withGenerationTimestamps(updater(dialog), dialog);
return nextDialog ? [nextDialog] : [];
}),
);
},
[rejectContextMutation],
);
// 中文注释:低层删除不再对未收口的完美像素 operation 抗命。删除占位不撤销任何在途请求
// ——完美像素没有取消接口,结果照常落库并进素材库,服务端 completion 发现 dialog 已不在
// 会返回 DialogMissing,客户端有对应提示。封锁换来的只是「结果自动回填画布」这一便利,
// 代价却是用户画布上出现删不掉的对象;低层偷偷保留还会让上层写出伪历史。
const removeCanvasGenerationDialogById = useCallback(
(dialogId: string) => {
updateCanvasGenerationDialogById(dialogId, () => null);
},
[updateCanvasGenerationDialogById],
);
const hasCanvasGenerationDialogById = useCallback((dialogId: string) => {
const currentDialog = generateDialogRef.current;
return [
...(isCanvasGenerationDialog(currentDialog) ? [currentDialog] : []),
...inactiveGenerateDialogsRef.current,
].some((dialog) => dialog.id === dialogId);
}, []);
const getCanvasGenerationDialogById = useCallback((dialogId: string) => {
const currentDialog = generateDialogRef.current;
if (
isCanvasGenerationDialog(currentDialog) &&
currentDialog.id === dialogId
) {
return currentDialog;
}
return inactiveGenerateDialogsRef.current.find(
(dialog) => dialog.id === dialogId,
);
}, []);
const activateCanvasGenerationDialog = useCallback(
(targetDialog: CanvasGenerationDialogState) => {
const currentDialog = generateDialogRef.current;
if (
(!isCanvasGenerationDialog(currentDialog) ||
currentDialog.id !== targetDialog.id) &&
rejectContextMutation()
) {
return;
}
const nextInactiveDialogs = inactiveGenerateDialogsRef.current.filter(
(dialog) => dialog.id !== targetDialog.id,
);
if (
isCanvasGenerationDialog(currentDialog) &&
currentDialog.id !== targetDialog.id
) {
nextInactiveDialogs.push({
...currentDialog,
composerOpen: false,
});
}
const nextActiveDialog = {
...targetDialog,
composerOpen: true,
};
inactiveGenerateDialogsRef.current = nextInactiveDialogs;
generateDialogRef.current = nextActiveDialog;
setInactiveGenerateDialogs(nextInactiveDialogs);
setGenerateDialogState(nextActiveDialog);
onActivate?.();
},
[onActivate, rejectContextMutation],
);
const restoreCanvasGenerationDialogs = useCallback(
(dialogs: CanvasGenerationDialogState[]) => {
const nextCounter = dialogs.reduce((maxCounter, dialog) => {
const match = /^generation-dialog-(\d+)$/u.exec(dialog.id);
const numericId = match ? Number.parseInt(match[1] ?? '0', 10) : 0;
return Math.max(maxCounter, Number.isFinite(numericId) ? numericId : 0);
}, 0);
generationDialogCounterRef.current = Math.max(
generationDialogCounterRef.current,
nextCounter,
);
const activeDialog =
[...dialogs]
.reverse()
.find((dialog) => dialog.composerOpen !== false) ??
dialogs[dialogs.length - 1] ??
null;
const nextActiveDialog = activeDialog
? {
...activeDialog,
composerOpen: activeDialog.composerOpen !== false,
}
: null;
const nextInactiveDialogs = dialogs
.filter((dialog) => dialog.id !== activeDialog?.id)
.map((dialog) => ({
...dialog,
composerOpen: false,
}));
generateDialogRef.current = nextActiveDialog;
inactiveGenerateDialogsRef.current = nextInactiveDialogs;
setGenerateDialogState(nextActiveDialog);
setInactiveGenerateDialogs(nextInactiveDialogs);
},
[],
);
const removeCanvasGenerationDialogsByLayerId = useCallback(
(targetLayerId: string) => {
const keepDialog = (dialog: CanvasGenerationDialogState) =>
dialog.sourceLayerId !== targetLayerId &&
dialog.generatedLayerId !== targetLayerId;
const currentDialog = generateDialogRef.current;
if (
isCanvasGenerationDialog(currentDialog) &&
!keepDialog(currentDialog) &&
rejectContextMutation()
) {
return;
}
const nextActiveDialog =
isCanvasGenerationDialog(currentDialog) && !keepDialog(currentDialog)
? null
: currentDialog;
const nextInactiveDialogs =
inactiveGenerateDialogsRef.current.filter(keepDialog);
generateDialogRef.current = nextActiveDialog;
inactiveGenerateDialogsRef.current = nextInactiveDialogs;
setGenerateDialogState(nextActiveDialog);
setInactiveGenerateDialogs(nextInactiveDialogs);
},
[rejectContextMutation],
);
const getGeneratingDialogPlaceholder = useCallback(
(dialog: GenerateDialogState) => {
const currentDialog = generateDialogRef.current;
if (dialog.id) {
const latestDialog = [
...(isCanvasGenerationDialog(currentDialog) ? [currentDialog] : []),
...inactiveGenerateDialogsRef.current,
].find((candidateDialog) => candidateDialog.id === dialog.id);
if (latestDialog?.status === 'generating') {
return latestDialog.placeholder ?? dialog.placeholder;
}
}
if (
currentDialog?.mode === dialog.mode &&
(!dialog.id || currentDialog.id === dialog.id) &&
currentDialog.status === 'generating'
) {
return currentDialog.placeholder ?? dialog.placeholder;
}
return dialog.placeholder;
},
[],
);
return {
generateDialog,
setGenerateDialog,
generateDialogRef,
inactiveGenerateDialogs,
setInactiveGenerateDialogs,
inactiveGenerateDialogsRef,
activeCanvasGenerationDialog,
canvasGenerationDialogs,
getCanvasGenerationDialogsSnapshot,
archiveActiveCanvasGenerationDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
removeCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getCanvasGenerationDialogById,
activateCanvasGenerationDialog,
restoreCanvasGenerationDialogs,
removeCanvasGenerationDialogsByLayerId,
getGeneratingDialogPlaceholder,
};
}