ac180ecad9
新增画板底部生成音乐入口,支持游戏音效和游戏背景音乐生成面板。 接入编辑器音频生成前后端契约、VectorEngine 请求与 BFF 路由。 补齐音频结果图层、元数据、画布恢复和客户端提交流程。 统一生成类面板 Lovart 式参数浮层、参考素材入口与相关测试。 更新编辑器音乐生成设计文档和项目长期记忆。
247 lines
7.9 KiB
TypeScript
247 lines
7.9 KiB
TypeScript
import { ImageIcon, X } from 'lucide-react';
|
|
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import { PlatformTextField } from '../common/PlatformTextField';
|
|
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
|
import type {
|
|
CharacterReferenceImage,
|
|
GenerateDialogState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
import {
|
|
EDITOR_GENERATION_MUD_POINT_CONFIG,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
|
|
|
type ImageCanvasBasicGenerationComposerViewProps = {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
generationReferenceButtonRef?: RefObject<HTMLButtonElement | null>;
|
|
isGenerationReferenceMenuOpen?: boolean;
|
|
setIsGenerationReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingGenerationReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
|
|
renderEditorPortal?: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle?: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onRequestUpload: (target: 'generation-reference') => void;
|
|
onToggleReferenceMenu?: () => void;
|
|
onRememberImageModel?: (model: string) => void;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
onClose: () => void;
|
|
};
|
|
function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
function ReferenceChip({
|
|
reference,
|
|
index,
|
|
onRemove,
|
|
}: {
|
|
reference: CharacterReferenceImage;
|
|
index: number;
|
|
onRemove: () => void;
|
|
}) {
|
|
const label = reference.label || `参考图${index + 1}`;
|
|
return (
|
|
<ImageCanvasReferenceSlot
|
|
tone="default"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
imageSrc={reference.src}
|
|
label={label}
|
|
ariaLabel={label}
|
|
title={reference.label}
|
|
onRemove={onRemove}
|
|
removeLabel={`删除${label}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function ImageCanvasBasicGenerationComposerView({
|
|
dialog,
|
|
style,
|
|
setGenerateDialog,
|
|
generationReferenceButtonRef,
|
|
isGenerationReferenceMenuOpen = false,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
renderEditorPortal = (node) => node,
|
|
buildPortalMenuStyle = () => ({}),
|
|
onRequestUpload,
|
|
onToggleReferenceMenu,
|
|
onRememberImageModel = () => {},
|
|
onSubmit,
|
|
onClose,
|
|
}: ImageCanvasBasicGenerationComposerViewProps) {
|
|
const references = dialog.generationReferences ?? [];
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: isGenerationReferenceMenuOpen,
|
|
boundaryRefs: [generationReferenceButtonRef],
|
|
onDismiss: () => setIsGenerationReferenceMenuOpen?.(false),
|
|
});
|
|
|
|
return (
|
|
<>
|
|
<form
|
|
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label="生成图片"
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (dialog.status !== 'generating') {
|
|
onSubmit(dialog);
|
|
}
|
|
}}
|
|
>
|
|
<div className="image-canvas-editor__reference-strip">
|
|
{references.map((reference, index) => (
|
|
<ReferenceChip
|
|
key={reference.id}
|
|
reference={reference}
|
|
index={index}
|
|
onRemove={() =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog
|
|
? {
|
|
...resetFailedDialogStatus(currentDialog),
|
|
generationReferences: (
|
|
currentDialog.generationReferences ?? []
|
|
).filter((item) => item.id !== reference.id),
|
|
}
|
|
: currentDialog,
|
|
)
|
|
}
|
|
/>
|
|
))}
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={generationReferenceButtonRef}
|
|
tone="default"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
label="参考图"
|
|
ariaLabel="添加参考图"
|
|
disabled={dialog.status === 'generating'}
|
|
isAdd
|
|
onClick={() => onToggleReferenceMenu?.()}
|
|
/>
|
|
</div>
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
aria-label="生成提示词"
|
|
value={dialog.prompt}
|
|
disabled={dialog.status === 'generating'}
|
|
placeholder="今天想生成什么画面?"
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__generation-prompt"
|
|
onChange={(event) =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog
|
|
? {
|
|
...resetFailedDialogStatus(currentDialog),
|
|
prompt: event.target.value,
|
|
}
|
|
: currentDialog,
|
|
)
|
|
}
|
|
/>
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
<ImageCanvasGenerationImageOptionsView
|
|
dialog={dialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
includeDimensions
|
|
onRememberImageModel={onRememberImageModel}
|
|
optionLabelPrefix="生成图片"
|
|
cost={EDITOR_GENERATION_MUD_POINT_CONFIG.image}
|
|
submitLabel="生成"
|
|
submitAriaLabel="生成"
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
/>
|
|
</div>
|
|
{dialog.status === 'generating' ? (
|
|
<PlatformStatusMessage
|
|
tone="info"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="status"
|
|
>
|
|
生成中
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
{dialog.status === 'failed' ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="alert"
|
|
>
|
|
{dialog.errorMessage}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
<EditorIconButton
|
|
className="image-canvas-editor__generation-close"
|
|
label="关闭生成图片"
|
|
icon={X}
|
|
variant="surfaceFloating"
|
|
disabled={dialog.status === 'generating'}
|
|
onClick={onClose}
|
|
/>
|
|
</form>
|
|
{isGenerationReferenceMenuOpen && generationReferenceButtonRef
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
|
label="参考图来源"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
generationReferenceButtonRef.current,
|
|
'above',
|
|
)}
|
|
>
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen?.(false);
|
|
setIsPickingGenerationReferenceFromCanvas?.(true);
|
|
}}
|
|
>
|
|
从画布中选择
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen?.(false);
|
|
setIsPickingGenerationReferenceFromCanvas?.(false);
|
|
onRequestUpload('generation-reference');
|
|
}}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</>
|
|
);
|
|
}
|