ac12e929f2
迁移旧版 ui 规范快照并隔离跨对话框优化结果。 关闭非幂等图标规范生成重试并统一参考图解析逻辑。 限制 LLM 重试范围并严格校验优化文本与补全参数。 修正图集键色、数量与素材间距提示约束并补充行为测试。 同步图片画布与生成面板技术文档。
781 lines
30 KiB
TypeScript
781 lines
30 KiB
TypeScript
import { ClipboardList, Cpu, ImagePlus } from 'lucide-react';
|
||
import {
|
||
type CSSProperties,
|
||
type Dispatch,
|
||
type ReactNode,
|
||
type RefObject,
|
||
type SetStateAction,
|
||
useEffect,
|
||
useRef,
|
||
useState,
|
||
} from 'react';
|
||
|
||
import {
|
||
EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH,
|
||
refineEditorIconSpecArtStyle,
|
||
refineEditorIconSpecPlaySetting,
|
||
} from '../../services/image-editor/editorProjectClient';
|
||
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 === 'icon';
|
||
}
|
||
|
||
type IconSpecOptimizationState = {
|
||
optimizing: boolean;
|
||
undoValue: string | null;
|
||
};
|
||
|
||
const INITIAL_ICON_SPEC_OPTIMIZATION_STATE: IconSpecOptimizationState = {
|
||
optimizing: false,
|
||
undoValue: null,
|
||
};
|
||
|
||
function getIconSpecPromptLength(value: string) {
|
||
return Array.from(value).length;
|
||
}
|
||
|
||
function limitIconSpecPrompt(value: string) {
|
||
return Array.from(value)
|
||
.slice(0, EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH)
|
||
.join('');
|
||
}
|
||
|
||
function isValidIconSpecPrompt(value: string | undefined) {
|
||
const normalized = value?.trim() ?? '';
|
||
return (
|
||
normalized.length > 0 &&
|
||
getIconSpecPromptLength(normalized) <= EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH
|
||
);
|
||
}
|
||
|
||
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 isIconSpec = dialog.mode === 'spec' && dialog.specType === 'icon';
|
||
const [playSettingOptimization, setPlaySettingOptimization] =
|
||
useState<IconSpecOptimizationState>(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
|
||
const [artStyleOptimization, setArtStyleOptimization] =
|
||
useState<IconSpecOptimizationState>(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
|
||
const [optimizationError, setOptimizationError] = useState<string | null>(
|
||
null,
|
||
);
|
||
const optimizationDialogKey = `${dialog.id ?? 'active'}:${dialog.mode}:${dialog.specType ?? ''}`;
|
||
const activeOptimizationDialogKeyRef = useRef(optimizationDialogKey);
|
||
const optimizationRequestVersionsRef = useRef({
|
||
playSetting: 0,
|
||
artStyle: 0,
|
||
});
|
||
activeOptimizationDialogKeyRef.current = optimizationDialogKey;
|
||
useEffect(() => {
|
||
optimizationRequestVersionsRef.current.playSetting += 1;
|
||
optimizationRequestVersionsRef.current.artStyle += 1;
|
||
setPlaySettingOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
|
||
setArtStyleOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE);
|
||
setOptimizationError(null);
|
||
}, [dialog.id, dialog.mode, dialog.specType]);
|
||
const isOptimizingIconSpec =
|
||
playSettingOptimization.optimizing || artStyleOptimization.optimizing;
|
||
const hasRequiredIconSpecValues =
|
||
isValidIconSpecPrompt(dialog.specValues?.playSetting) &&
|
||
isValidIconSpecPrompt(dialog.specValues?.artStyle);
|
||
const canSubmit =
|
||
!isGenerating &&
|
||
(!isIconSpec || (hasRequiredIconSpecValues && !isOptimizingIconSpec));
|
||
const specGenerationCost = calculateEditorSpecGenerationPrice(
|
||
SPEC_GENERATION_MODEL,
|
||
);
|
||
useImageCanvasFloatingOptionDismiss({
|
||
isOpen: isGenerationReferenceMenuOpen,
|
||
boundaryRefs: [generationReferenceButtonRef],
|
||
onDismiss: () => setIsGenerationReferenceMenuOpen?.(false),
|
||
});
|
||
|
||
const openReferenceMenu = () => {
|
||
if (setIsGenerationReferenceMenuOpen) {
|
||
setIsGenerationReferenceMenuOpen((open) => !open);
|
||
return;
|
||
}
|
||
onRequestUpload(uploadTarget);
|
||
};
|
||
|
||
const optimizeIconSpecField = async (field: 'playSetting' | 'artStyle') => {
|
||
const isPlaySetting = field === 'playSetting';
|
||
const key: keyof SpecFormValues = isPlaySetting
|
||
? 'playSetting'
|
||
: 'artStyle';
|
||
const value = dialog.specValues?.[key]?.trim() ?? '';
|
||
if (!value) {
|
||
return;
|
||
}
|
||
const setOptimization = isPlaySetting
|
||
? setPlaySettingOptimization
|
||
: setArtStyleOptimization;
|
||
const requestVersion = optimizationRequestVersionsRef.current[field] + 1;
|
||
optimizationRequestVersionsRef.current[field] = requestVersion;
|
||
const requestDialogKey = optimizationDialogKey;
|
||
setOptimization((current) => ({ ...current, optimizing: true }));
|
||
setOptimizationError(null);
|
||
try {
|
||
const refined = isPlaySetting
|
||
? await refineEditorIconSpecPlaySetting(value)
|
||
: await refineEditorIconSpecArtStyle(value);
|
||
if (
|
||
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
|
||
optimizationRequestVersionsRef.current[field] !== requestVersion
|
||
) {
|
||
return;
|
||
}
|
||
onUpdateSpecFormValue(key, refined);
|
||
setOptimization({ optimizing: false, undoValue: value });
|
||
} catch (error) {
|
||
if (
|
||
activeOptimizationDialogKeyRef.current !== requestDialogKey ||
|
||
optimizationRequestVersionsRef.current[field] !== requestVersion
|
||
) {
|
||
return;
|
||
}
|
||
setOptimization((current) => ({ ...current, optimizing: false }));
|
||
const fallbackMessage = isPlaySetting
|
||
? '优化玩法设定失败'
|
||
: '优化美术风格失败';
|
||
const errorMessage =
|
||
error instanceof Error && error.message.trim()
|
||
? error.message
|
||
: fallbackMessage;
|
||
setOptimizationError(errorMessage);
|
||
}
|
||
};
|
||
|
||
const undoIconSpecOptimization = (field: 'playSetting' | 'artStyle') => {
|
||
const isPlaySetting = field === 'playSetting';
|
||
const key: keyof SpecFormValues = isPlaySetting
|
||
? 'playSetting'
|
||
: 'artStyle';
|
||
const state = isPlaySetting
|
||
? playSettingOptimization
|
||
: artStyleOptimization;
|
||
if (state.undoValue === null) {
|
||
return;
|
||
}
|
||
onUpdateSpecFormValue(key, state.undoValue);
|
||
(isPlaySetting ? setPlaySettingOptimization : setArtStyleOptimization)(
|
||
INITIAL_ICON_SPEC_OPTIMIZATION_STATE,
|
||
);
|
||
setOptimizationError(null);
|
||
};
|
||
|
||
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 (canSubmit) {
|
||
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"
|
||
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>
|
||
) : null}
|
||
{!isUiDesignDialog && 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>
|
||
) : null}
|
||
{!isUiDesignDialog && isIconSpec ? (
|
||
<>
|
||
{(
|
||
[
|
||
{
|
||
field: 'playSetting' as const,
|
||
key: 'playSetting' as const,
|
||
title: '玩法设定',
|
||
placeholder: '游戏的核心玩法是什么?',
|
||
optimization: playSettingOptimization,
|
||
},
|
||
{
|
||
field: 'artStyle' as const,
|
||
key: 'artStyle' as const,
|
||
title: '美术风格',
|
||
placeholder: '游戏的画风是怎样的?',
|
||
optimization: artStyleOptimization,
|
||
},
|
||
] as const
|
||
).map((item) => {
|
||
const value = dialog.specValues?.[item.key] ?? '';
|
||
return (
|
||
<label
|
||
key={item.field}
|
||
className="image-canvas-editor__field-block"
|
||
>
|
||
<PlatformFieldLabel
|
||
variant="form"
|
||
className="image-canvas-editor__field-title"
|
||
>
|
||
{item.title}
|
||
</PlatformFieldLabel>
|
||
<div>
|
||
<PlatformTextField
|
||
variant="textarea"
|
||
aria-label={item.title}
|
||
value={value}
|
||
placeholder={item.placeholder}
|
||
disabled={isGenerating || item.optimization.optimizing}
|
||
size="sm"
|
||
density="compact"
|
||
className="image-canvas-editor__spec-textarea"
|
||
onChange={(event) =>
|
||
onUpdateSpecFormValue(
|
||
item.key,
|
||
limitIconSpecPrompt(event.target.value),
|
||
)
|
||
}
|
||
/>
|
||
<div>
|
||
<PlatformActionButton
|
||
type="button"
|
||
tone="ghost"
|
||
size="xxs"
|
||
shape="pill"
|
||
aria-label={`一键优化${item.title}`}
|
||
disabled={
|
||
isGenerating ||
|
||
item.optimization.optimizing ||
|
||
!isValidIconSpecPrompt(value)
|
||
}
|
||
onClick={() => optimizeIconSpecField(item.field)}
|
||
>
|
||
{item.optimization.optimizing
|
||
? '正在优化'
|
||
: '✦ 一键优化'}
|
||
</PlatformActionButton>
|
||
{item.optimization.undoValue !== null &&
|
||
!item.optimization.optimizing ? (
|
||
<>
|
||
<span aria-hidden="true">|</span>
|
||
<PlatformActionButton
|
||
type="button"
|
||
tone="ghost"
|
||
size="xxs"
|
||
shape="pill"
|
||
aria-label={`撤销${item.title}优化`}
|
||
disabled={isGenerating}
|
||
onClick={() =>
|
||
undoIconSpecOptimization(item.field)
|
||
}
|
||
>
|
||
↺ 撤销
|
||
</PlatformActionButton>
|
||
</>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
</label>
|
||
);
|
||
})}
|
||
</>
|
||
) : null}
|
||
{!isUiDesignDialog && dialog.specType !== 'custom' && !isIconSpec ? (
|
||
<>
|
||
<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}
|
||
</>
|
||
) : null}
|
||
</div>
|
||
{dialog.status === 'failed' ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{dialog.errorMessage}
|
||
</PlatformStatusMessage>
|
||
) : null}
|
||
{optimizationError ? (
|
||
<PlatformStatusMessage
|
||
tone="error"
|
||
surface="platform"
|
||
size="xs"
|
||
className="image-canvas-editor__generate-status"
|
||
role="alert"
|
||
>
|
||
{optimizationError}
|
||
</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={!canSubmit}
|
||
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}
|
||
</>
|
||
);
|
||
}
|