5976c2b337
图标素材生成支持上传普通参考图并提交到后端参考图数组。 UI设计图生成和UI素材提取支持上传参考图并写入生成输入记录。 同步前后端契约、OpenAPI、文档和相关单测。
543 lines
21 KiB
TypeScript
543 lines
21 KiB
TypeScript
import { ClipboardList, Cpu, ImagePlus } from 'lucide-react';
|
|
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
|
|
import { PlatformActionButton } from '../common/PlatformActionButton';
|
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import {
|
|
PlatformSelectField,
|
|
PlatformTextField,
|
|
} from '../common/PlatformTextField';
|
|
import type {
|
|
GenerateDialogState,
|
|
SpecFormValues,
|
|
SpecGenerationType,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
|
import {
|
|
calculateEditorSpecGenerationPrice,
|
|
calculateEditorUiDesignPrice,
|
|
CHARACTER_SPEC_VIEW_OPTIONS,
|
|
getEditorImageModelDisplayName,
|
|
IMAGE_MODEL_GPT_IMAGE_2,
|
|
resolveEditorImageSizeLabel,
|
|
SPEC_GENERATION_ASPECT_RATIO,
|
|
SPEC_GENERATION_IMAGE_SIZE,
|
|
SPEC_GENERATION_MODEL,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
|
|
|
type ImageCanvasSpecGenerationPanelViewProps = {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
isGenerationReferenceMenuOpen?: boolean;
|
|
generationReferenceButtonRef?: RefObject<HTMLButtonElement | null>;
|
|
setIsGenerationReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingGenerationReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
|
|
setGenerateDialog?: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
renderEditorPortal?: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle?: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onOpenSpecDialog?: (specType: SpecGenerationType) => void;
|
|
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onRememberImageModel?: (model: string) => void;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
};
|
|
|
|
function shouldShowSpecInputPlaceholders(dialog: GenerateDialogState) {
|
|
return dialog.specType === 'character' || dialog.specType === 'ui';
|
|
}
|
|
|
|
export function ImageCanvasSpecGenerationPanelView({
|
|
dialog,
|
|
style,
|
|
isGenerationReferenceMenuOpen = false,
|
|
generationReferenceButtonRef,
|
|
setIsGenerationReferenceMenuOpen,
|
|
setIsPickingGenerationReferenceFromCanvas,
|
|
setGenerateDialog,
|
|
renderEditorPortal = (node) => node,
|
|
buildPortalMenuStyle = () => ({}),
|
|
onOpenSpecDialog,
|
|
onUpdateSpecFormValue,
|
|
onRequestUpload,
|
|
onRememberImageModel = () => {},
|
|
onSubmit,
|
|
}: ImageCanvasSpecGenerationPanelViewProps) {
|
|
const isUiDesignDialog = dialog.mode === 'ui-design';
|
|
const showSpecInputPlaceholders = shouldShowSpecInputPlaceholders(dialog);
|
|
const specSizeLabel = resolveEditorImageSizeLabel({
|
|
aspectRatio: SPEC_GENERATION_ASPECT_RATIO,
|
|
imageSize: SPEC_GENERATION_IMAGE_SIZE,
|
|
});
|
|
const specModelLabel = getEditorImageModelDisplayName(SPEC_GENERATION_MODEL);
|
|
const referenceLabel = isUiDesignDialog ? 'UI设计图标规范' : '参考图';
|
|
const referenceSlotLabel = isUiDesignDialog
|
|
? '图标规范'
|
|
: dialog.specType === 'character'
|
|
? '角色规范'
|
|
: '参考图';
|
|
const uploadTarget: UploadTarget = isUiDesignDialog
|
|
? 'ui-design-icon-spec'
|
|
: 'spec-reference';
|
|
const reference = isUiDesignDialog
|
|
? dialog.uiDesignSpecReference
|
|
: dialog.specReference;
|
|
const isGenerating = dialog.status === 'generating';
|
|
const specGenerationCost = calculateEditorSpecGenerationPrice(
|
|
SPEC_GENERATION_MODEL,
|
|
);
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: isGenerationReferenceMenuOpen,
|
|
boundaryRefs: [generationReferenceButtonRef],
|
|
onDismiss: () => setIsGenerationReferenceMenuOpen?.(false),
|
|
});
|
|
|
|
const openReferenceMenu = () => {
|
|
if (setIsGenerationReferenceMenuOpen) {
|
|
setIsGenerationReferenceMenuOpen((open) => !open);
|
|
return;
|
|
}
|
|
onRequestUpload(uploadTarget);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<form
|
|
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__spec-composer"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label={isUiDesignDialog ? '生成UI设计图' : '生成规范'}
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (dialog.status !== 'generating') {
|
|
onSubmit(dialog);
|
|
}
|
|
}}
|
|
>
|
|
<div className="image-canvas-editor__reference-strip">
|
|
{isUiDesignDialog || dialog.specType ? (
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={generationReferenceButtonRef}
|
|
tone={isUiDesignDialog ? 'ui' : 'spec'}
|
|
icon={
|
|
isUiDesignDialog ? (
|
|
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
|
) : (
|
|
<ClipboardList className="h-4 w-4" aria-hidden="true" />
|
|
)
|
|
}
|
|
imageSrc={reference?.src}
|
|
objectKey={reference?.objectKey}
|
|
label={referenceSlotLabel}
|
|
ariaLabel={referenceLabel}
|
|
title={reference?.label ?? referenceLabel}
|
|
disabled={isGenerating}
|
|
isAdd={!reference}
|
|
onClick={openReferenceMenu}
|
|
onRemove={
|
|
reference && setGenerateDialog
|
|
? () =>
|
|
setGenerateDialog((currentDialog) => {
|
|
if (isUiDesignDialog) {
|
|
return currentDialog?.mode === 'ui-design'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
uiDesignSpecReference: null,
|
|
}
|
|
: currentDialog;
|
|
}
|
|
return currentDialog?.mode === 'spec'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
specReference: null,
|
|
}
|
|
: currentDialog;
|
|
})
|
|
: undefined
|
|
}
|
|
removeLabel={`删除${referenceLabel}`}
|
|
/>
|
|
) : null}
|
|
{isUiDesignDialog
|
|
? (dialog.generationReferences ?? []).map((item, index) => (
|
|
<ImageCanvasReferenceSlot
|
|
key={item.id}
|
|
tone="default"
|
|
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
|
|
imageSrc={item.src}
|
|
objectKey={item.objectKey}
|
|
label={`参考图${index + 1}`}
|
|
ariaLabel={item.label || `参考图${index + 1}`}
|
|
title={item.label}
|
|
disabled={isGenerating}
|
|
onRemove={
|
|
setGenerateDialog
|
|
? () =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'ui-design'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
generationReferences: (
|
|
currentDialog.generationReferences ?? []
|
|
).filter(
|
|
(referenceItem) =>
|
|
referenceItem.id !== item.id,
|
|
),
|
|
}
|
|
: currentDialog,
|
|
)
|
|
: undefined
|
|
}
|
|
removeLabel={`删除${item.label || `参考图${index + 1}`}`}
|
|
/>
|
|
))
|
|
: null}
|
|
{isUiDesignDialog ? (
|
|
<ImageCanvasReferenceSlot
|
|
tone="default"
|
|
icon={<ImagePlus className="h-4 w-4" aria-hidden="true" />}
|
|
label="参考图"
|
|
ariaLabel="上传UI设计参考图"
|
|
disabled={isGenerating}
|
|
isAdd
|
|
onClick={() => onRequestUpload('generation-reference')}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
<div className="image-canvas-editor__spec-fields">
|
|
{isUiDesignDialog ? (
|
|
<label className="image-canvas-editor__field-block image-canvas-editor__field-block--single">
|
|
<PlatformTextField
|
|
variant="textarea"
|
|
aria-label="UI设计要求"
|
|
value={dialog.prompt}
|
|
disabled={isGenerating}
|
|
placeholder="你希望这个 UI 长什么样?"
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__generation-prompt image-canvas-editor__generation-prompt--borderless"
|
|
onChange={(event) => {
|
|
const nextPrompt = event.target.value;
|
|
if (setGenerateDialog) {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'ui-design'
|
|
? {
|
|
...currentDialog,
|
|
prompt: nextPrompt,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
return;
|
|
}
|
|
onUpdateSpecFormValue('customPrompt', nextPrompt);
|
|
}}
|
|
/>
|
|
</label>
|
|
) : dialog.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={dialog.specValues?.customPrompt ?? ''}
|
|
placeholder="写下你想要的任何内容,陶泥儿帮你形成新的约束"
|
|
disabled={isGenerating}
|
|
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={dialog.specValues?.playSetting ?? ''}
|
|
placeholder={
|
|
showSpecInputPlaceholders
|
|
? '这是什么类型的游戏?'
|
|
: undefined
|
|
}
|
|
disabled={isGenerating}
|
|
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={dialog.specValues?.artStyle ?? ''}
|
|
placeholder={
|
|
showSpecInputPlaceholders
|
|
? '游戏的画风是怎样的?'
|
|
: undefined
|
|
}
|
|
disabled={isGenerating}
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__spec-input"
|
|
onChange={(event) =>
|
|
onUpdateSpecFormValue('artStyle', event.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
{dialog.specType === 'character' ? (
|
|
<>
|
|
<label className="image-canvas-editor__field-block">
|
|
<PlatformFieldLabel
|
|
variant="form"
|
|
className="image-canvas-editor__field-title"
|
|
>
|
|
头身比
|
|
</PlatformFieldLabel>
|
|
<PlatformSelectField
|
|
aria-label="头身比"
|
|
value={dialog.specValues?.bodyRatio ?? '3'}
|
|
disabled={isGenerating}
|
|
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={dialog.specValues?.characterView ?? ''}
|
|
disabled={isGenerating}
|
|
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}
|
|
</>
|
|
)}
|
|
</div>
|
|
{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 image-canvas-editor__spec-footer">
|
|
{isUiDesignDialog ? (
|
|
<ImageCanvasGenerationImageOptionsView
|
|
dialog={dialog}
|
|
setGenerateDialog={setGenerateDialog ?? (() => undefined)}
|
|
includeDimensions
|
|
onRememberImageModel={onRememberImageModel}
|
|
lockedModel={IMAGE_MODEL_GPT_IMAGE_2}
|
|
cost={calculateEditorUiDesignPrice(
|
|
IMAGE_MODEL_GPT_IMAGE_2,
|
|
dialog.imageSize,
|
|
)}
|
|
submitLabel="生成"
|
|
submitAriaLabel="生成UI设计图"
|
|
optionLabelPrefix="生成图片"
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
/>
|
|
) : (
|
|
<>
|
|
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--dimensions image-canvas-editor__readonly-generation-option">
|
|
<PlatformInlineOptionButton
|
|
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--dimensions"
|
|
aria-label={`生成图片尺寸 ${specSizeLabel}`}
|
|
disabled
|
|
>
|
|
{specSizeLabel}
|
|
</PlatformInlineOptionButton>
|
|
</div>
|
|
<div className="image-canvas-editor__option-popover-anchor image-canvas-editor__option-popover-anchor--model image-canvas-editor__readonly-generation-option">
|
|
<PlatformInlineOptionButton
|
|
className="image-canvas-editor__option-cluster image-canvas-editor__option-cluster--model"
|
|
aria-label={`生成图片模型 ${specModelLabel}`}
|
|
disabled
|
|
>
|
|
<span className="image-canvas-editor__model-trigger-label">
|
|
<span
|
|
className="image-canvas-editor__model-icon"
|
|
aria-hidden="true"
|
|
>
|
|
<Cpu />
|
|
</span>
|
|
<span>{specModelLabel}</span>
|
|
</span>
|
|
</PlatformInlineOptionButton>
|
|
</div>
|
|
<PlatformActionButton
|
|
type="submit"
|
|
tone="secondary"
|
|
size="xs"
|
|
shape="pill"
|
|
className="image-canvas-editor__generation-submit image-canvas-editor__spec-submit"
|
|
disabled={isGenerating}
|
|
aria-label="提交生成规范"
|
|
>
|
|
{isGenerating ? (
|
|
'生成中'
|
|
) : (
|
|
<>
|
|
<span>生成</span>
|
|
<span className="image-canvas-editor__mud-point-inline">
|
|
{specGenerationCost}泥点
|
|
</span>
|
|
</>
|
|
)}
|
|
</PlatformActionButton>
|
|
</>
|
|
)}
|
|
</div>
|
|
</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>
|
|
{isUiDesignDialog ? (
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen?.(false);
|
|
onOpenSpecDialog?.('icon');
|
|
}}
|
|
>
|
|
新建图标规范
|
|
</PlatformFloatingMenuItem>
|
|
) : null}
|
|
<PlatformFloatingMenuItem
|
|
onClick={() => {
|
|
setIsGenerationReferenceMenuOpen?.(false);
|
|
setIsPickingGenerationReferenceFromCanvas?.(false);
|
|
onRequestUpload(uploadTarget);
|
|
}}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</>
|
|
);
|
|
}
|