0e47a54a91
统一画布生成占位图标,按图片、规范、角色、图标、UI、视频、音频、宣发和快速编辑区分。 让图片、规范、宣发和视频新建占位按面板尺寸或固定输出尺寸创建,并在参数变化时同步占位大小。 完善快速编辑、音频生成、宣发素材和生成类面板相关契约与测试。 更新生成类面板 Lovart 改造文档和项目决策记录。
213 lines
7.4 KiB
TypeScript
213 lines
7.4 KiB
TypeScript
import { ImageIcon } 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 type {
|
|
GenerateDialogState,
|
|
SpecGenerationType,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
|
import {
|
|
calculateEditorIconSpritesheetPrice,
|
|
DEFAULT_ICON_DESCRIPTIONS,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
|
|
|
type ImageCanvasIconSpritesheetComposerViewProps = {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isIconSpecMenuOpen: boolean;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
renderEditorPortal: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onUpdateIconDescriptionText: (value: string) => void;
|
|
onRememberImageModel: (model: string) => void;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
};
|
|
|
|
export function ImageCanvasIconSpritesheetComposerView({
|
|
dialog,
|
|
style,
|
|
iconSpecButtonRef,
|
|
isIconSpecMenuOpen,
|
|
setGenerateDialog,
|
|
setIsIconSpecMenuOpen,
|
|
setIsPickingIconSpecFromCanvas,
|
|
renderEditorPortal,
|
|
buildPortalMenuStyle,
|
|
onOpenSpecDialog,
|
|
onRequestUpload,
|
|
onUpdateIconDescriptionText,
|
|
onRememberImageModel,
|
|
onSubmit,
|
|
}: ImageCanvasIconSpritesheetComposerViewProps) {
|
|
const descriptionText =
|
|
dialog.prompt ||
|
|
(dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS).join('\n');
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: isIconSpecMenuOpen,
|
|
boundaryRefs: [iconSpecButtonRef],
|
|
onDismiss: () => setIsIconSpecMenuOpen(false),
|
|
});
|
|
|
|
const pickIconSpecFromCanvas = () => {
|
|
setIsPickingIconSpecFromCanvas(true);
|
|
setIsIconSpecMenuOpen(false);
|
|
};
|
|
|
|
const openIconSpecDialog = () => {
|
|
setIsIconSpecMenuOpen(false);
|
|
onOpenSpecDialog('icon');
|
|
};
|
|
|
|
const requestIconSpecUpload = () => {
|
|
setIsIconSpecMenuOpen(false);
|
|
onRequestUpload('icon-spec');
|
|
};
|
|
|
|
return (
|
|
<form
|
|
className="image-canvas-editor__icon-composer"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label="生成图标素材"
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (dialog.status !== 'generating') {
|
|
onSubmit({ ...dialog, prompt: descriptionText });
|
|
}
|
|
}}
|
|
>
|
|
<div className="image-canvas-editor__reference-strip">
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={iconSpecButtonRef}
|
|
tone="icon"
|
|
className="image-canvas-editor__icon-spec-card"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
imageSrc={dialog.iconSpecReference?.src}
|
|
label="图标规范"
|
|
ariaLabel={dialog.iconSpecReference?.label ?? '图标规范'}
|
|
title={dialog.iconSpecReference?.label ?? '图标规范'}
|
|
disabled={dialog.status === 'generating'}
|
|
isAdd={!dialog.iconSpecReference}
|
|
onClick={() => setIsIconSpecMenuOpen((open) => !open)}
|
|
onRemove={
|
|
dialog.iconSpecReference
|
|
? () =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
iconSpecReference: null,
|
|
}
|
|
: currentDialog,
|
|
)
|
|
: undefined
|
|
}
|
|
removeLabel="删除图标规范"
|
|
/>
|
|
{isIconSpecMenuOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
|
label="图标规范来源"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(
|
|
iconSpecButtonRef.current,
|
|
'above',
|
|
)}
|
|
>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={pickIconSpecFromCanvas}
|
|
>
|
|
从画布中选择
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={openIconSpecDialog}
|
|
>
|
|
新建图标规范
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={requestIconSpecUpload}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</div>
|
|
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
aria-label="素材描述"
|
|
value={descriptionText}
|
|
disabled={dialog.status === 'generating'}
|
|
placeholder="描述要生成的多个素材,每行一个。"
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__generation-prompt image-canvas-editor__generation-prompt--borderless"
|
|
onChange={(event) => onUpdateIconDescriptionText(event.target.value)}
|
|
/>
|
|
</label>
|
|
{dialog.status === 'failed' ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="alert"
|
|
>
|
|
{dialog.errorMessage}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
<ImageCanvasGenerationImageOptionsView
|
|
dialog={dialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
includeDimensions
|
|
onRememberImageModel={onRememberImageModel}
|
|
optionLabelPrefix="生成图片"
|
|
cost={calculateEditorIconSpritesheetPrice(dialog.imageModel)}
|
|
submitLabel="生成"
|
|
submitAriaLabel="生成"
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
/>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|