为角色形象规范、UI素材规范、自定义规范面板新增参考图上传入口。 生成规范时携带参考图并自动追加参考图生成规范语义。 补充生成流程和上传流程回归测试。 更新画板角色形象生成入口设计文档。
1516 lines
57 KiB
TypeScript
1516 lines
57 KiB
TypeScript
import {
|
||
ChevronDown,
|
||
ClipboardList,
|
||
ImageIcon,
|
||
ImagePlus,
|
||
X,
|
||
} from 'lucide-react';
|
||
import {
|
||
type CSSProperties,
|
||
type Dispatch,
|
||
type ReactNode,
|
||
type RefObject,
|
||
type SetStateAction,
|
||
} from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
|
||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||
import {
|
||
PlatformFloatingMenu,
|
||
PlatformFloatingMenuItem,
|
||
} from '../common/PlatformFloatingMenu';
|
||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
|
||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||
import {
|
||
PlatformSelectField,
|
||
PlatformTextField,
|
||
} from '../common/PlatformTextField';
|
||
import { UnifiedModal } from '../common/UnifiedModal';
|
||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||
import {
|
||
CHARACTER_ANIMATION_ACTION_PROMPTS,
|
||
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
||
CHARACTER_ANIMATION_RATIO_OPTIONS,
|
||
CHARACTER_SPEC_VIEW_OPTIONS,
|
||
DEFAULT_ICON_DESCRIPTIONS,
|
||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||
EDITOR_IMAGE_MODEL_OPTIONS,
|
||
ICON_DESCRIPTION_LIMIT,
|
||
IMAGE_MODEL_NANOBANANA2,
|
||
SPEC_GENERATION_COST,
|
||
SPEC_TYPE_LABEL,
|
||
} from './ImageCanvasGenerationModel';
|
||
import type {
|
||
CharacterAnimationPanelState,
|
||
CanvasLayer,
|
||
GenerateDialogState,
|
||
QuickEditPanelState,
|
||
SpecFormValues,
|
||
SpecGenerationType,
|
||
UploadTarget,
|
||
} from './ImageCanvasEditorTypes';
|
||
|
||
type ImageCanvasGenerationComposerViewProps = {
|
||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||
isSpecMenuOpen: boolean;
|
||
isCharacterSpecMenuOpen: boolean;
|
||
isCharacterReferenceMenuOpen: boolean;
|
||
isIconSpecMenuOpen: boolean;
|
||
isPickingCharacterSpecFromCanvas: boolean;
|
||
isPickingCharacterReferenceFromCanvas: boolean;
|
||
isPickingIconSpecFromCanvas: boolean;
|
||
generateDialog: GenerateDialogState | null;
|
||
generationComposerStyle: CSSProperties | null;
|
||
iconComposerStyle: CSSProperties | null;
|
||
quickEditPanel: QuickEditPanelState | null;
|
||
quickEditSourceLayer: CanvasLayer | null;
|
||
quickEditPanelStyle: CSSProperties | null;
|
||
quickEditSizeOptions: string[];
|
||
quickEditModelOptions: Array<{ label: string; value: string }>;
|
||
characterAnimationPanel: CharacterAnimationPanelState | null;
|
||
characterAnimationSourceLayer: CanvasLayer | null;
|
||
characterAnimationPanelStyle: CSSProperties | null;
|
||
characterAnimationPrice: number;
|
||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
|
||
setCharacterAnimationPanel: Dispatch<
|
||
SetStateAction<CharacterAnimationPanelState | null>
|
||
>;
|
||
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
||
onRequestUpload: (target: UploadTarget) => void;
|
||
onSubmitImageGeneration: (dialog: GenerateDialogState) => void;
|
||
onSubmitIconSpritesheetGeneration: (dialog: GenerateDialogState) => void;
|
||
onSubmitQuickEdit: () => void;
|
||
onSubmitCharacterAnimation: () => void;
|
||
onCloseGenerateComposer: () => void;
|
||
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
|
||
onUpdateIconDescription: (index: number, value: string) => void;
|
||
onAddIconDescription: () => void;
|
||
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
||
onRememberImageModel: (model: string) => void;
|
||
};
|
||
|
||
function triggerPlaceholderAction(label: string) {
|
||
window.alert(`${label}功能建设中`);
|
||
}
|
||
|
||
function buildPortalMenuStyle(
|
||
anchor: HTMLElement | null,
|
||
placement: 'above' | 'below',
|
||
): CSSProperties {
|
||
const rect = anchor?.getBoundingClientRect();
|
||
if (!rect) {
|
||
return {
|
||
position: 'fixed',
|
||
left: 0,
|
||
top: 0,
|
||
right: 'auto',
|
||
bottom: 'auto',
|
||
zIndex: 70,
|
||
};
|
||
}
|
||
|
||
return {
|
||
position: 'fixed',
|
||
left: Math.round(rect.left),
|
||
top:
|
||
placement === 'above'
|
||
? Math.round(rect.top)
|
||
: Math.round(rect.bottom + 8),
|
||
right: 'auto',
|
||
bottom: 'auto',
|
||
zIndex: 70,
|
||
transform:
|
||
placement === 'above' ? 'translateY(calc(-100% - 0.45rem))' : undefined,
|
||
};
|
||
}
|
||
|
||
function renderEditorPortal(node: ReactNode) {
|
||
if (typeof document === 'undefined') {
|
||
return node;
|
||
}
|
||
return createPortal(node, document.body);
|
||
}
|
||
|
||
function resetFailedDialogStatus(dialog: GenerateDialogState) {
|
||
return {
|
||
...dialog,
|
||
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
||
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
||
};
|
||
}
|
||
|
||
function resetFailedPanelStatus<T extends { status: string; errorMessage?: string }>(
|
||
panel: T,
|
||
) {
|
||
return {
|
||
...panel,
|
||
status: panel.status === 'failed' ? 'idle' : panel.status,
|
||
errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage,
|
||
};
|
||
}
|
||
|
||
function getImageDimensionOptions(model: string | null | undefined) {
|
||
return (
|
||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||
(model ?? IMAGE_MODEL_NANOBANANA2) as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[IMAGE_MODEL_NANOBANANA2]
|
||
);
|
||
}
|
||
|
||
function normalizeImageDialogSelection(dialog: GenerateDialogState) {
|
||
const model = dialog.imageModel ?? IMAGE_MODEL_NANOBANANA2;
|
||
const options = getImageDimensionOptions(model);
|
||
const aspectRatios = options.aspectRatios as readonly string[];
|
||
const imageSizes = options.imageSizes as readonly string[];
|
||
return {
|
||
model,
|
||
aspectRatio:
|
||
dialog.aspectRatio && aspectRatios.includes(dialog.aspectRatio)
|
||
? dialog.aspectRatio
|
||
: options.aspectRatios[0],
|
||
imageSize:
|
||
dialog.imageSize && imageSizes.includes(dialog.imageSize)
|
||
? dialog.imageSize
|
||
: (options.imageSizes.find((size) => size === '1K') ?? options.imageSizes[0]),
|
||
options,
|
||
};
|
||
}
|
||
|
||
function renderImageOptionButtons({
|
||
dialog,
|
||
setGenerateDialog,
|
||
includeDimensions,
|
||
onRememberImageModel,
|
||
}: {
|
||
dialog: GenerateDialogState;
|
||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||
includeDimensions: boolean;
|
||
onRememberImageModel: (model: string) => void;
|
||
}) {
|
||
const selection = normalizeImageDialogSelection(dialog);
|
||
const updateDialog = (patch: Partial<GenerateDialogState>) => {
|
||
setGenerateDialog((currentDialog) =>
|
||
currentDialog && currentDialog.mode === dialog.mode
|
||
? {
|
||
...resetFailedDialogStatus(currentDialog),
|
||
...patch,
|
||
}
|
||
: currentDialog,
|
||
);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{includeDimensions ? (
|
||
<>
|
||
<div className="image-canvas-editor__option-field">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
画面比例
|
||
</PlatformFieldLabel>
|
||
<div className="image-canvas-editor__inline-option-group">
|
||
{selection.options.aspectRatios.map((aspectRatio) => (
|
||
<PlatformInlineOptionButton
|
||
key={aspectRatio}
|
||
className="image-canvas-editor__generation-ratio"
|
||
disabled={dialog.status === 'generating'}
|
||
aria-pressed={selection.aspectRatio === aspectRatio}
|
||
onClick={() => updateDialog({ aspectRatio })}
|
||
>
|
||
{aspectRatio}
|
||
</PlatformInlineOptionButton>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="image-canvas-editor__option-field">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
大小尺寸
|
||
</PlatformFieldLabel>
|
||
<div className="image-canvas-editor__inline-option-group">
|
||
{selection.options.imageSizes.map((imageSize) => (
|
||
<PlatformInlineOptionButton
|
||
key={imageSize}
|
||
className="image-canvas-editor__generation-ratio"
|
||
disabled={dialog.status === 'generating'}
|
||
aria-pressed={selection.imageSize === imageSize}
|
||
onClick={() => updateDialog({ imageSize })}
|
||
>
|
||
{imageSize}
|
||
</PlatformInlineOptionButton>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</>
|
||
) : null}
|
||
<div className="image-canvas-editor__option-field">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
模型
|
||
</PlatformFieldLabel>
|
||
<div className="image-canvas-editor__inline-option-group">
|
||
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => {
|
||
const nextOptions = getImageDimensionOptions(option.value);
|
||
const nextAspectRatios = nextOptions.aspectRatios as readonly string[];
|
||
const nextImageSizes = nextOptions.imageSizes as readonly string[];
|
||
return (
|
||
<PlatformInlineOptionButton
|
||
key={option.value}
|
||
className="image-canvas-editor__generation-model"
|
||
disabled={dialog.status === 'generating'}
|
||
aria-pressed={selection.model === option.value}
|
||
onClick={() => {
|
||
onRememberImageModel(option.value);
|
||
updateDialog({
|
||
imageModel: option.value,
|
||
aspectRatio:
|
||
dialog.aspectRatio &&
|
||
nextAspectRatios.includes(dialog.aspectRatio)
|
||
? dialog.aspectRatio
|
||
: nextOptions.aspectRatios[0],
|
||
imageSize:
|
||
dialog.imageSize &&
|
||
nextImageSizes.includes(dialog.imageSize)
|
||
? dialog.imageSize
|
||
: (nextOptions.imageSizes.find((size) => size === '1K') ??
|
||
nextOptions.imageSizes[0]),
|
||
});
|
||
}}
|
||
>
|
||
{option.label}
|
||
</PlatformInlineOptionButton>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
export function ImageCanvasGenerationComposerView({
|
||
specToolWrapRef,
|
||
characterSpecButtonRef,
|
||
characterReferenceButtonRef,
|
||
iconSpecButtonRef,
|
||
isSpecMenuOpen,
|
||
isCharacterSpecMenuOpen,
|
||
isCharacterReferenceMenuOpen,
|
||
isIconSpecMenuOpen,
|
||
isPickingCharacterSpecFromCanvas,
|
||
isPickingCharacterReferenceFromCanvas,
|
||
isPickingIconSpecFromCanvas,
|
||
generateDialog,
|
||
generationComposerStyle,
|
||
iconComposerStyle,
|
||
quickEditPanel,
|
||
quickEditSourceLayer,
|
||
quickEditPanelStyle,
|
||
quickEditSizeOptions,
|
||
quickEditModelOptions,
|
||
characterAnimationPanel,
|
||
characterAnimationSourceLayer,
|
||
characterAnimationPanelStyle,
|
||
characterAnimationPrice,
|
||
setGenerateDialog,
|
||
setQuickEditPanel,
|
||
setCharacterAnimationPanel,
|
||
setIsCharacterSpecMenuOpen,
|
||
setIsCharacterReferenceMenuOpen,
|
||
setIsIconSpecMenuOpen,
|
||
setIsPickingCharacterSpecFromCanvas,
|
||
setIsPickingCharacterReferenceFromCanvas,
|
||
setIsPickingIconSpecFromCanvas,
|
||
onOpenSpecDialog,
|
||
onRequestUpload,
|
||
onSubmitImageGeneration,
|
||
onSubmitIconSpritesheetGeneration,
|
||
onSubmitQuickEdit,
|
||
onSubmitCharacterAnimation,
|
||
onCloseGenerateComposer,
|
||
onUpdateSpecFormValue,
|
||
onUpdateIconDescription,
|
||
onAddIconDescription,
|
||
onUpdateCharacterAnimationDuration,
|
||
onRememberImageModel,
|
||
}: ImageCanvasGenerationComposerViewProps) {
|
||
return (
|
||
<>
|
||
{isSpecMenuOpen
|
||
? renderEditorPortal(
|
||
<PlatformFloatingMenu
|
||
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
||
label="生成规范类型"
|
||
placement="top-start"
|
||
style={buildPortalMenuStyle(specToolWrapRef.current, 'above')}
|
||
>
|
||
{(['character', 'ui', 'custom'] as const).map((specType) => (
|
||
<PlatformFloatingMenuItem
|
||
key={specType}
|
||
className="image-canvas-editor__spec-menu-item"
|
||
onClick={() => onOpenSpecDialog(specType)}
|
||
>
|
||
{SPEC_TYPE_LABEL[specType]}
|
||
</PlatformFloatingMenuItem>
|
||
))}
|
||
</PlatformFloatingMenu>,
|
||
)
|
||
: null}
|
||
|
||
{generateDialog?.mode === 'generate' &&
|
||
generateDialog.composerOpen !== false &&
|
||
generationComposerStyle ? (
|
||
<form
|
||
className="image-canvas-editor__generation-composer"
|
||
style={generationComposerStyle}
|
||
role="dialog"
|
||
aria-label="生成图片"
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
if (generateDialog.status !== 'generating') {
|
||
onSubmitImageGeneration(generateDialog);
|
||
}
|
||
}}
|
||
>
|
||
<PlatformIconButton
|
||
variant="surfaceFloating"
|
||
className="image-canvas-editor__generation-ref"
|
||
label="添加参考图"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => onRequestUpload('asset')}
|
||
icon={<ImageIcon className="h-4 w-4" />}
|
||
>
|
||
<span>参考图</span>
|
||
</PlatformIconButton>
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label="生成提示词"
|
||
value={generateDialog.prompt}
|
||
disabled={generateDialog.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">
|
||
<PlatformInlineOptionButton
|
||
className="image-canvas-editor__generation-ratio"
|
||
aria-label="生成比例 1:1 2k 1张"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => triggerPlaceholderAction('生成参数')}
|
||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||
>
|
||
中 · 1:1(2k) · 1张
|
||
</PlatformInlineOptionButton>
|
||
<PlatformInlineOptionButton
|
||
className="image-canvas-editor__generation-model"
|
||
aria-label="生成模型 GPT Image"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => triggerPlaceholderAction('模型选择')}
|
||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||
>
|
||
GPT Im...
|
||
</PlatformInlineOptionButton>
|
||
<PlatformActionButton
|
||
type="submit"
|
||
tone="secondary"
|
||
size="xs"
|
||
shape="pill"
|
||
className="image-canvas-editor__generation-submit"
|
||
disabled={generateDialog.status === 'generating'}
|
||
aria-label="生成"
|
||
>
|
||
{generateDialog.status === 'generating' ? '生成中' : '12'}
|
||
</PlatformActionButton>
|
||
</div>
|
||
{generateDialog.status === 'generating' ? (
|
||
<PlatformStatusMessage
|
||
tone="info"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="status"
|
||
>
|
||
生成中
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
{generateDialog.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{generateDialog.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<EditorIconButton
|
||
className="image-canvas-editor__generation-close"
|
||
label="关闭生成图片"
|
||
icon={X}
|
||
variant="surfaceFloating"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={onCloseGenerateComposer}
|
||
/>
|
||
</form>
|
||
) : null}
|
||
|
||
{generateDialog?.mode === 'spec' &&
|
||
generateDialog.composerOpen !== false &&
|
||
generationComposerStyle ? (
|
||
<form
|
||
className="image-canvas-editor__generation-composer image-canvas-editor__spec-composer"
|
||
style={generationComposerStyle}
|
||
role="dialog"
|
||
aria-label="生成规范"
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
if (generateDialog.status !== 'generating') {
|
||
onSubmitImageGeneration(generateDialog);
|
||
}
|
||
}}
|
||
>
|
||
<div className="image-canvas-editor__spec-fields">
|
||
{generateDialog.specType === 'custom' ? (
|
||
<label className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
自定义规范提示词
|
||
</PlatformFieldLabel>
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label="自定义规范提示词"
|
||
value={generateDialog.specValues?.customPrompt ?? ''}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__spec-textarea"
|
||
onChange={(event) =>
|
||
onUpdateSpecFormValue('customPrompt', event.target.value)
|
||
}
|
||
/>
|
||
</label>
|
||
) : (
|
||
<>
|
||
<label className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
玩法设定
|
||
</PlatformFieldLabel>
|
||
<PlatformTextField
|
||
aria-label="玩法设定"
|
||
value={generateDialog.specValues?.playSetting ?? ''}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__spec-input"
|
||
onChange={(event) =>
|
||
onUpdateSpecFormValue('playSetting', event.target.value)
|
||
}
|
||
/>
|
||
</label>
|
||
<label className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
美术风格
|
||
</PlatformFieldLabel>
|
||
<PlatformTextField
|
||
aria-label="美术风格"
|
||
value={generateDialog.specValues?.artStyle ?? ''}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__spec-input"
|
||
onChange={(event) =>
|
||
onUpdateSpecFormValue('artStyle', event.target.value)
|
||
}
|
||
/>
|
||
</label>
|
||
{generateDialog.specType === 'character' ? (
|
||
<>
|
||
<label className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
头身比
|
||
</PlatformFieldLabel>
|
||
<PlatformSelectField
|
||
aria-label="头身比"
|
||
value={generateDialog.specValues?.bodyRatio ?? '3'}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__spec-input"
|
||
onChange={(event) =>
|
||
onUpdateSpecFormValue('bodyRatio', event.target.value)
|
||
}
|
||
>
|
||
{['2', '3', '4', '5', '6'].map((value) => (
|
||
<option key={value} value={value}>
|
||
{value}
|
||
</option>
|
||
))}
|
||
</PlatformSelectField>
|
||
</label>
|
||
<label className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
角色视角
|
||
</PlatformFieldLabel>
|
||
<PlatformSelectField
|
||
aria-label="角色视角"
|
||
value={generateDialog.specValues?.characterView ?? ''}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__spec-input"
|
||
onChange={(event) =>
|
||
onUpdateSpecFormValue(
|
||
'characterView',
|
||
event.target.value,
|
||
)
|
||
}
|
||
>
|
||
{CHARACTER_SPEC_VIEW_OPTIONS.map((value) => (
|
||
<option key={value} value={value}>
|
||
{value}
|
||
</option>
|
||
))}
|
||
</PlatformSelectField>
|
||
</label>
|
||
</>
|
||
) : null}
|
||
</>
|
||
)}
|
||
{generateDialog.specType !== 'icon' ? (
|
||
<div className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
参考图
|
||
</PlatformFieldLabel>
|
||
<button
|
||
type="button"
|
||
className="image-canvas-editor__character-spec-ref image-canvas-editor__reference-tile image-canvas-editor__reference-tile--spec"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => onRequestUpload('spec-reference')}
|
||
>
|
||
<span className="image-canvas-editor__reference-tile-visual">
|
||
{generateDialog.specReference ? (
|
||
<img
|
||
src={generateDialog.specReference.src}
|
||
alt=""
|
||
aria-hidden="true"
|
||
/>
|
||
) : (
|
||
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span className="image-canvas-editor__reference-tile-copy">
|
||
{generateDialog.specReference?.label ?? '添加参考图'}
|
||
</span>
|
||
</button>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
{generateDialog.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{generateDialog.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<div className="image-canvas-editor__spec-footer">
|
||
<PlatformActionButton
|
||
type="submit"
|
||
tone="secondary"
|
||
size="sm"
|
||
className="image-canvas-editor__spec-submit"
|
||
disabled={generateDialog.status === 'generating'}
|
||
aria-label="提交生成规范"
|
||
>
|
||
{generateDialog.status === 'generating'
|
||
? '生成中'
|
||
: `消耗${SPEC_GENERATION_COST}泥点 · 生成`}
|
||
</PlatformActionButton>
|
||
</div>
|
||
</form>
|
||
) : null}
|
||
|
||
{generateDialog?.mode === 'character' && generationComposerStyle ? (
|
||
<form
|
||
className="image-canvas-editor__character-composer"
|
||
style={generationComposerStyle}
|
||
role="dialog"
|
||
aria-label="生成角色形象"
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
if (generateDialog.status !== 'generating') {
|
||
onSubmitImageGeneration(generateDialog);
|
||
}
|
||
}}
|
||
>
|
||
<div className="image-canvas-editor__character-reference-row">
|
||
<div className="image-canvas-editor__field-block image-canvas-editor__character-reference-field image-canvas-editor__character-reference-field--spec">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
角色形象规范
|
||
</PlatformFieldLabel>
|
||
<span className="image-canvas-editor__character-spec-wrap">
|
||
<button
|
||
ref={characterSpecButtonRef}
|
||
type="button"
|
||
className="image-canvas-editor__character-spec-ref image-canvas-editor__reference-tile image-canvas-editor__reference-tile--spec"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => setIsCharacterSpecMenuOpen((open) => !open)}
|
||
>
|
||
<span className="image-canvas-editor__reference-tile-visual">
|
||
{generateDialog.characterSpecReference ? (
|
||
<img
|
||
src={generateDialog.characterSpecReference.src}
|
||
alt=""
|
||
aria-hidden="true"
|
||
/>
|
||
) : (
|
||
<ClipboardList className="h-4 w-4" aria-hidden="true" />
|
||
)}
|
||
</span>
|
||
<span className="image-canvas-editor__reference-tile-copy">
|
||
{generateDialog.characterSpecReference?.label ??
|
||
'角色形象规范'}
|
||
</span>
|
||
</button>
|
||
</span>
|
||
</div>
|
||
{isCharacterSpecMenuOpen
|
||
? renderEditorPortal(
|
||
<PlatformFloatingMenu
|
||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||
label="角色形象规范来源"
|
||
placement="top-start"
|
||
style={buildPortalMenuStyle(
|
||
characterSpecButtonRef.current,
|
||
'above',
|
||
)}
|
||
>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsPickingCharacterSpecFromCanvas(true);
|
||
setIsCharacterSpecMenuOpen(false);
|
||
}}
|
||
>
|
||
从画布中选择
|
||
</PlatformFloatingMenuItem>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsCharacterSpecMenuOpen(false);
|
||
onOpenSpecDialog('character');
|
||
}}
|
||
>
|
||
新建角色形象规范
|
||
</PlatformFloatingMenuItem>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsCharacterSpecMenuOpen(false);
|
||
onRequestUpload('character-spec');
|
||
}}
|
||
>
|
||
上传图片
|
||
</PlatformFloatingMenuItem>
|
||
</PlatformFloatingMenu>,
|
||
)
|
||
: null}
|
||
<div className="image-canvas-editor__field-block image-canvas-editor__character-reference-field image-canvas-editor__character-reference-field--regular">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
常规参考图
|
||
</PlatformFieldLabel>
|
||
<div className="image-canvas-editor__character-reference-list">
|
||
{(generateDialog.characterReferences ?? []).map(
|
||
(reference, index) => (
|
||
<span
|
||
key={reference.id}
|
||
className="image-canvas-editor__character-ref-thumb"
|
||
title={reference.label}
|
||
>
|
||
<img src={reference.src} alt={reference.label} />
|
||
<span className="image-canvas-editor__character-ref-index">
|
||
{index + 1}
|
||
</span>
|
||
</span>
|
||
),
|
||
)}
|
||
<button
|
||
ref={characterReferenceButtonRef}
|
||
type="button"
|
||
className="image-canvas-editor__character-reference-add image-canvas-editor__reference-tile image-canvas-editor__reference-tile--upload"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() =>
|
||
setIsCharacterReferenceMenuOpen((open) => !open)
|
||
}
|
||
>
|
||
<span className="image-canvas-editor__reference-tile-visual">
|
||
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
||
</span>
|
||
<span className="image-canvas-editor__reference-tile-copy">
|
||
上传常规参考图
|
||
</span>
|
||
</button>
|
||
{isCharacterReferenceMenuOpen
|
||
? renderEditorPortal(
|
||
<PlatformFloatingMenu
|
||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||
label="常规参考图来源"
|
||
placement="top-start"
|
||
style={buildPortalMenuStyle(
|
||
characterReferenceButtonRef.current,
|
||
'above',
|
||
)}
|
||
>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsPickingCharacterReferenceFromCanvas(true);
|
||
setIsCharacterReferenceMenuOpen(false);
|
||
}}
|
||
>
|
||
从画布中选择
|
||
</PlatformFloatingMenuItem>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsCharacterReferenceMenuOpen(false);
|
||
onRequestUpload('character-reference');
|
||
}}
|
||
>
|
||
上传图片
|
||
</PlatformFloatingMenuItem>
|
||
</PlatformFloatingMenu>,
|
||
)
|
||
: null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<label className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
角色设定
|
||
</PlatformFieldLabel>
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label="角色设定"
|
||
value={generateDialog.prompt}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__generation-prompt"
|
||
onChange={(event) =>
|
||
setGenerateDialog((currentDialog) =>
|
||
currentDialog?.mode === 'character'
|
||
? {
|
||
...resetFailedDialogStatus(currentDialog),
|
||
prompt: event.target.value,
|
||
}
|
||
: currentDialog,
|
||
)
|
||
}
|
||
/>
|
||
</label>
|
||
{generateDialog.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{generateDialog.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<div className="image-canvas-editor__generation-composer-footer">
|
||
{renderImageOptionButtons({
|
||
dialog: generateDialog,
|
||
setGenerateDialog,
|
||
includeDimensions: true,
|
||
onRememberImageModel,
|
||
})}
|
||
<PlatformActionButton
|
||
type="submit"
|
||
tone="secondary"
|
||
size="xs"
|
||
shape="pill"
|
||
className="image-canvas-editor__generation-submit"
|
||
disabled={generateDialog.status === 'generating'}
|
||
>
|
||
{generateDialog.status === 'generating' ? '生成中' : '生成'}
|
||
</PlatformActionButton>
|
||
</div>
|
||
</form>
|
||
) : null}
|
||
|
||
{generateDialog?.mode === 'icon' &&
|
||
generateDialog.composerOpen !== false &&
|
||
iconComposerStyle ? (
|
||
<form
|
||
className="image-canvas-editor__icon-composer"
|
||
style={iconComposerStyle}
|
||
role="dialog"
|
||
aria-label="生成图标素材"
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
if (generateDialog.status !== 'generating') {
|
||
onSubmitIconSpritesheetGeneration(generateDialog);
|
||
}
|
||
}}
|
||
>
|
||
<div className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
图标素材规范
|
||
</PlatformFieldLabel>
|
||
<div className="image-canvas-editor__icon-spec-row">
|
||
<span className="image-canvas-editor__character-spec-wrap">
|
||
<button
|
||
ref={iconSpecButtonRef}
|
||
type="button"
|
||
className="image-canvas-editor__icon-spec-card"
|
||
disabled={generateDialog.status === 'generating'}
|
||
aria-label={
|
||
generateDialog.iconSpecReference?.label ?? '图标素材规范'
|
||
}
|
||
onClick={() => setIsIconSpecMenuOpen((open) => !open)}
|
||
>
|
||
<span
|
||
className="image-canvas-editor__icon-spec-preview"
|
||
aria-hidden="true"
|
||
>
|
||
{generateDialog.iconSpecReference?.src ? (
|
||
<img src={generateDialog.iconSpecReference.src} alt="" />
|
||
) : (
|
||
<ImageIcon className="h-5 w-5" />
|
||
)}
|
||
</span>
|
||
<span className="image-canvas-editor__icon-spec-copy">
|
||
<span className="image-canvas-editor__icon-spec-eyebrow">
|
||
图标素材规范
|
||
</span>
|
||
<span className="image-canvas-editor__icon-spec-title">
|
||
{generateDialog.iconSpecReference?.label ?? '待选择'}
|
||
</span>
|
||
</span>
|
||
<span className="image-canvas-editor__icon-spec-state">
|
||
{generateDialog.iconSpecReference ? '已绑定' : '待绑定'}
|
||
</span>
|
||
</button>
|
||
</span>
|
||
{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={() => {
|
||
setIsPickingIconSpecFromCanvas(true);
|
||
setIsIconSpecMenuOpen(false);
|
||
}}
|
||
>
|
||
从画布中选择
|
||
</PlatformFloatingMenuItem>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsIconSpecMenuOpen(false);
|
||
onOpenSpecDialog('icon');
|
||
}}
|
||
>
|
||
新建图标素材规范
|
||
</PlatformFloatingMenuItem>
|
||
<PlatformFloatingMenuItem
|
||
className="image-canvas-editor__context-menu-item"
|
||
onClick={() => {
|
||
setIsIconSpecMenuOpen(false);
|
||
onRequestUpload('icon-spec');
|
||
}}
|
||
>
|
||
上传图片
|
||
</PlatformFloatingMenuItem>
|
||
</PlatformFloatingMenu>,
|
||
)
|
||
: null}
|
||
<div
|
||
className="image-canvas-editor__icon-spec-actions"
|
||
aria-label="图标素材规范操作"
|
||
>
|
||
<button
|
||
type="button"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => {
|
||
setIsPickingIconSpecFromCanvas(true);
|
||
setIsIconSpecMenuOpen(false);
|
||
}}
|
||
>
|
||
画布
|
||
</button>
|
||
<button
|
||
type="button"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => {
|
||
setIsIconSpecMenuOpen(false);
|
||
onOpenSpecDialog('icon');
|
||
}}
|
||
>
|
||
新建
|
||
</button>
|
||
<button
|
||
type="button"
|
||
disabled={generateDialog.status === 'generating'}
|
||
onClick={() => {
|
||
setIsIconSpecMenuOpen(false);
|
||
onRequestUpload('icon-spec');
|
||
}}
|
||
>
|
||
上传
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="image-canvas-editor__field-block">
|
||
<PlatformFieldLabel
|
||
variant="field"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
素材描述
|
||
</PlatformFieldLabel>
|
||
<div className="image-canvas-editor__icon-description-list">
|
||
{(generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS).map(
|
||
(description, index) => (
|
||
<label
|
||
key={index}
|
||
className="image-canvas-editor__icon-description-card"
|
||
>
|
||
<span className="image-canvas-editor__icon-description-index">
|
||
{String(index + 1).padStart(2, '0')}
|
||
</span>
|
||
<span className="image-canvas-editor__icon-description-title">
|
||
素材描述 {index + 1}
|
||
</span>
|
||
<PlatformTextField
|
||
aria-label={`素材描述${index + 1}`}
|
||
value={description}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__icon-description-input"
|
||
onChange={(event) =>
|
||
onUpdateIconDescription(index, event.target.value)
|
||
}
|
||
/>
|
||
</label>
|
||
),
|
||
)}
|
||
</div>
|
||
</div>
|
||
{generateDialog.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{generateDialog.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<div className="image-canvas-editor__icon-footer">
|
||
<button
|
||
type="button"
|
||
className="image-canvas-editor__character-reference-add"
|
||
disabled={
|
||
generateDialog.status === 'generating' ||
|
||
(generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
|
||
.length >= ICON_DESCRIPTION_LIMIT
|
||
}
|
||
onClick={onAddIconDescription}
|
||
>
|
||
添加素材描述
|
||
</button>
|
||
{renderImageOptionButtons({
|
||
dialog: generateDialog,
|
||
setGenerateDialog,
|
||
includeDimensions: true,
|
||
onRememberImageModel,
|
||
})}
|
||
<PlatformActionButton
|
||
type="submit"
|
||
tone="secondary"
|
||
size="xs"
|
||
shape="pill"
|
||
className="image-canvas-editor__generation-submit"
|
||
disabled={generateDialog.status === 'generating'}
|
||
aria-label="生成"
|
||
>
|
||
{generateDialog.status === 'generating' ? '生成中' : '生成'}
|
||
</PlatformActionButton>
|
||
</div>
|
||
</form>
|
||
) : null}
|
||
|
||
{isPickingCharacterSpecFromCanvas ? (
|
||
<div className="image-canvas-editor__canvas-pick-hint">
|
||
请选择画布中的图片作为角色形象规范,按 Esc 退出
|
||
</div>
|
||
) : null}
|
||
{isPickingCharacterReferenceFromCanvas ? (
|
||
<div className="image-canvas-editor__canvas-pick-hint">
|
||
请选择画布中的图片作为常规参考图,按 Esc 退出
|
||
</div>
|
||
) : null}
|
||
{isPickingIconSpecFromCanvas ? (
|
||
<div className="image-canvas-editor__canvas-pick-hint">
|
||
请选择画布中的图标素材规范,按 Esc 退出
|
||
</div>
|
||
) : null}
|
||
|
||
{quickEditPanel &&
|
||
quickEditPanel.status !== 'generating' &&
|
||
quickEditSourceLayer &&
|
||
quickEditPanelStyle ? (
|
||
<form
|
||
className="image-canvas-editor__quick-edit-panel"
|
||
style={quickEditPanelStyle}
|
||
role="dialog"
|
||
aria-label="快速编辑图片"
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
onSubmitQuickEdit();
|
||
}}
|
||
>
|
||
<div className="image-canvas-editor__quick-edit-head">
|
||
<div className="image-canvas-editor__quick-edit-reference">
|
||
<img
|
||
src={quickEditSourceLayer.src}
|
||
alt={`${quickEditSourceLayer.title}参考图`}
|
||
/>
|
||
<span>{quickEditSourceLayer.title}</span>
|
||
</div>
|
||
<EditorIconButton
|
||
label="关闭快速编辑图片"
|
||
title="关闭"
|
||
icon={X}
|
||
onClick={() => setQuickEditPanel(null)}
|
||
/>
|
||
</div>
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label="快速编辑提示词"
|
||
value={quickEditPanel.prompt}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__quick-edit-prompt"
|
||
onChange={(event) =>
|
||
setQuickEditPanel((currentPanel) =>
|
||
currentPanel
|
||
? {
|
||
...resetFailedPanelStatus(currentPanel),
|
||
prompt: event.target.value,
|
||
}
|
||
: currentPanel,
|
||
)
|
||
}
|
||
/>
|
||
<div className="image-canvas-editor__quick-edit-controls">
|
||
<PlatformSelectField
|
||
aria-label="快速编辑尺寸"
|
||
value={quickEditPanel.size}
|
||
size="xs"
|
||
density="compact"
|
||
onChange={(event) =>
|
||
setQuickEditPanel((currentPanel) =>
|
||
currentPanel
|
||
? { ...currentPanel, size: event.target.value }
|
||
: currentPanel,
|
||
)
|
||
}
|
||
>
|
||
{quickEditSizeOptions.map((size) => (
|
||
<option key={size} value={size}>
|
||
{size}
|
||
</option>
|
||
))}
|
||
</PlatformSelectField>
|
||
<PlatformSelectField
|
||
aria-label="快速编辑模型"
|
||
value={quickEditPanel.model}
|
||
size="xs"
|
||
density="compact"
|
||
onChange={(event) =>
|
||
setQuickEditPanel((currentPanel) =>
|
||
currentPanel
|
||
? { ...currentPanel, model: event.target.value }
|
||
: currentPanel,
|
||
)
|
||
}
|
||
>
|
||
{quickEditModelOptions.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</PlatformSelectField>
|
||
</div>
|
||
{quickEditPanel.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
role="alert"
|
||
>
|
||
{quickEditPanel.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<PlatformActionButton
|
||
type="submit"
|
||
tone="secondary"
|
||
size="sm"
|
||
className="image-canvas-editor__quick-edit-submit"
|
||
>
|
||
生成
|
||
</PlatformActionButton>
|
||
</form>
|
||
) : null}
|
||
|
||
{characterAnimationPanel &&
|
||
characterAnimationSourceLayer &&
|
||
characterAnimationPanelStyle ? (
|
||
<form
|
||
className="image-canvas-editor__character-animation-panel"
|
||
style={characterAnimationPanelStyle}
|
||
role="dialog"
|
||
aria-label="角色动画生成面板"
|
||
onPointerDown={(event) => event.stopPropagation()}
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
if (characterAnimationPanel.status !== 'generating') {
|
||
onSubmitCharacterAnimation();
|
||
}
|
||
}}
|
||
>
|
||
<div className="image-canvas-editor__character-animation-head">
|
||
<strong>角色动画</strong>
|
||
<EditorIconButton
|
||
label="关闭角色动画生成面板"
|
||
title="关闭"
|
||
icon={X}
|
||
onClick={() => setCharacterAnimationPanel(null)}
|
||
/>
|
||
</div>
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label="动画描述"
|
||
value={characterAnimationPanel.promptText}
|
||
maxLength={4000}
|
||
disabled={characterAnimationPanel.status === 'generating'}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__character-animation-textarea"
|
||
onChange={(event) =>
|
||
setCharacterAnimationPanel((currentPanel) =>
|
||
currentPanel
|
||
? {
|
||
...resetFailedPanelStatus(currentPanel),
|
||
promptText: event.target.value.slice(0, 4000),
|
||
}
|
||
: currentPanel,
|
||
)
|
||
}
|
||
/>
|
||
<div className="image-canvas-editor__character-animation-presets">
|
||
{CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => (
|
||
<button
|
||
key={preset.label}
|
||
type="button"
|
||
className="image-canvas-editor__character-animation-preset"
|
||
disabled={characterAnimationPanel.status === 'generating'}
|
||
onClick={() =>
|
||
setCharacterAnimationPanel((currentPanel) =>
|
||
currentPanel
|
||
? {
|
||
...currentPanel,
|
||
promptText: preset.text,
|
||
status: 'idle',
|
||
errorMessage: undefined,
|
||
}
|
||
: currentPanel,
|
||
)
|
||
}
|
||
>
|
||
{preset.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
<div className="image-canvas-editor__character-animation-grid">
|
||
<PlatformSelectField
|
||
aria-label="分辨率"
|
||
value={characterAnimationPanel.resolution}
|
||
disabled={characterAnimationPanel.status === 'generating'}
|
||
size="xs"
|
||
density="compact"
|
||
onChange={(event) =>
|
||
setCharacterAnimationPanel((currentPanel) =>
|
||
currentPanel
|
||
? {
|
||
...resetFailedPanelStatus(currentPanel),
|
||
resolution:
|
||
event.target.value === '720p' ? '720p' : '480p',
|
||
}
|
||
: currentPanel,
|
||
)
|
||
}
|
||
>
|
||
<option value="480p">480p</option>
|
||
<option value="720p">720p</option>
|
||
</PlatformSelectField>
|
||
<PlatformSelectField
|
||
aria-label="画面比例"
|
||
value={characterAnimationPanel.ratio}
|
||
disabled={characterAnimationPanel.status === 'generating'}
|
||
size="xs"
|
||
density="compact"
|
||
onChange={(event) =>
|
||
setCharacterAnimationPanel((currentPanel) =>
|
||
currentPanel
|
||
? {
|
||
...resetFailedPanelStatus(currentPanel),
|
||
ratio:
|
||
CHARACTER_ANIMATION_RATIO_OPTIONS.find(
|
||
(item) => item.value === event.target.value,
|
||
)?.value ?? 'same',
|
||
}
|
||
: currentPanel,
|
||
)
|
||
}
|
||
>
|
||
{CHARACTER_ANIMATION_RATIO_OPTIONS.map((option) => (
|
||
<option key={option.value} value={option.value}>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</PlatformSelectField>
|
||
<PlatformSelectField
|
||
aria-label="时长"
|
||
value={String(characterAnimationPanel.frameCount)}
|
||
disabled={characterAnimationPanel.status === 'generating'}
|
||
size="xs"
|
||
density="compact"
|
||
onChange={(event) =>
|
||
onUpdateCharacterAnimationDuration(event.target.value)
|
||
}
|
||
>
|
||
{CHARACTER_ANIMATION_DURATION_OPTIONS.map((option) => (
|
||
<option
|
||
key={option.frameCount}
|
||
value={String(option.frameCount)}
|
||
>
|
||
{option.label}
|
||
</option>
|
||
))}
|
||
</PlatformSelectField>
|
||
</div>
|
||
<div className="image-canvas-editor__character-animation-summary">
|
||
<span
|
||
className="image-canvas-editor__character-animation-summary-text"
|
||
title={characterAnimationPanel.promptText.trim() || undefined}
|
||
aria-label={`生成文本:${
|
||
characterAnimationPanel.promptText.trim() || '动画描述'
|
||
}`}
|
||
>
|
||
{characterAnimationPanel.promptText.trim()
|
||
? characterAnimationPanel.promptText.trim()
|
||
: '动画描述'}
|
||
</span>
|
||
<strong>{characterAnimationPrice}泥点</strong>
|
||
</div>
|
||
{characterAnimationPanel.status === 'completed' &&
|
||
characterAnimationPanel.result ? (
|
||
<PlatformStatusMessage
|
||
tone="success"
|
||
surface="platform"
|
||
size="xs"
|
||
role="status"
|
||
>
|
||
已生成 {characterAnimationPanel.result.frameCount} 帧
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
{characterAnimationPanel.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
role="alert"
|
||
>
|
||
{characterAnimationPanel.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<PlatformActionButton
|
||
type="submit"
|
||
tone="secondary"
|
||
size="sm"
|
||
className="image-canvas-editor__character-animation-submit"
|
||
disabled={characterAnimationPanel.status === 'generating'}
|
||
>
|
||
{characterAnimationPanel.status === 'generating'
|
||
? '生成中'
|
||
: '生成'}
|
||
</PlatformActionButton>
|
||
</form>
|
||
) : null}
|
||
|
||
<UnifiedModal
|
||
open={
|
||
generateDialog?.mode === 'edit' &&
|
||
generateDialog.status !== 'generating'
|
||
}
|
||
title={generateDialog?.mode === 'edit' ? '修改图片' : '生成图片'}
|
||
size="sm"
|
||
closeLabel={
|
||
generateDialog?.mode === 'edit' ? '关闭修改图片' : '关闭生成图片'
|
||
}
|
||
closeDisabled={generateDialog?.status === 'generating'}
|
||
onClose={() => setGenerateDialog(null)}
|
||
panelClassName="image-canvas-editor__generate-dialog"
|
||
bodyClassName="image-canvas-editor__generate-dialog-body"
|
||
>
|
||
{generateDialog?.mode === 'edit' ? (
|
||
<form
|
||
className="image-canvas-editor__generate-form"
|
||
onSubmit={(event) => {
|
||
event.preventDefault();
|
||
if (generateDialog.status !== 'generating') {
|
||
onSubmitImageGeneration(generateDialog);
|
||
}
|
||
}}
|
||
>
|
||
<div className="image-canvas-editor__generate-body">
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label="生成提示词"
|
||
value={generateDialog.prompt}
|
||
disabled={generateDialog.status === 'generating'}
|
||
size="sm"
|
||
density="roomy"
|
||
className="image-canvas-editor__generate-prompt"
|
||
placeholder="描述你想如何修改这张图片"
|
||
onChange={(event) =>
|
||
setGenerateDialog((currentDialog) =>
|
||
currentDialog
|
||
? {
|
||
...currentDialog,
|
||
prompt: event.target.value,
|
||
}
|
||
: currentDialog,
|
||
)
|
||
}
|
||
/>
|
||
{generateDialog.status === 'generating' ? (
|
||
<PlatformStatusMessage
|
||
tone="info"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="status"
|
||
>
|
||
修改中
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
{generateDialog.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{generateDialog.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
<PlatformActionButton
|
||
type="submit"
|
||
size="sm"
|
||
className="image-canvas-editor__generate-submit"
|
||
disabled={generateDialog.status === 'generating'}
|
||
>
|
||
{generateDialog.status === 'generating' ? '修改中' : '修改'}
|
||
</PlatformActionButton>
|
||
</div>
|
||
</form>
|
||
) : null}
|
||
</UnifiedModal>
|
||
</>
|
||
);
|
||
}
|