281c84b7bf
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>
462 lines
16 KiB
TypeScript
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,
|
|
};
|
|
}
|