import { ClipboardList, Cpu, ImagePlus, Loader2, Sparkles, Undo2, } 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 { AutoGrowTextArea } from '../common/AutoGrowTextArea'; 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; setIsGenerationReferenceMenuOpen?: Dispatch>; setIsPickingGenerationReferenceFromCanvas?: Dispatch>; setGenerateDialog?: Dispatch>; 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; hasPendingImageReferenceUploads?: boolean; 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 = () => {}, hasPendingImageReferenceUploads = false, 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(INITIAL_ICON_SPEC_OPTIMIZATION_STATE); const [artStyleOptimization, setArtStyleOptimization] = useState(INITIAL_ICON_SPEC_OPTIMIZATION_STATE); const [optimizationError, setOptimizationError] = useState( null, ); const optimizationDialogKey = `${dialog.id ?? 'active'}:${dialog.mode}:${dialog.specType ?? ''}`; const activeOptimizationDialogKeyRef = useRef( optimizationDialogKey, ); const optimizationRequestVersionsRef = useRef({ playSetting: 0, artStyle: 0, }); useEffect(() => { activeOptimizationDialogKeyRef.current = optimizationDialogKey; optimizationRequestVersionsRef.current.playSetting += 1; optimizationRequestVersionsRef.current.artStyle += 1; setPlaySettingOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE); setArtStyleOptimization(INITIAL_ICON_SPEC_OPTIMIZATION_STATE); setOptimizationError(null); return () => { if (activeOptimizationDialogKeyRef.current === optimizationDialogKey) { activeOptimizationDialogKeyRef.current = null; } }; }, [optimizationDialogKey]); const isOptimizingIconSpec = playSettingOptimization.optimizing || artStyleOptimization.optimizing; const hasRequiredIconSpecValues = isValidIconSpecPrompt(dialog.specValues?.playSetting) && isValidIconSpecPrompt(dialog.specValues?.artStyle); const canSubmit = !isGenerating && !hasPendingImageReferenceUploads && (!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 undoValue = dialog.specValues?.[key] ?? ''; const value = undoValue.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 }); } 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 ( <>
event.stopPropagation()} >
{isUiDesignDialog || dialog.specType ? (
{isUiDesignDialog ? (
{ 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); }} />
) : null} {!isUiDesignDialog && dialog.specType === 'custom' ? (
自定义规范提示词 onUpdateSpecFormValue('customPrompt', value) } />
) : 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 (
{item.title} { onUpdateSpecFormValue( item.key, limitIconSpecPrompt(nextValue), ); (item.field === 'playSetting' ? setPlaySettingOptimization : setArtStyleOptimization)( INITIAL_ICON_SPEC_OPTIMIZATION_STATE, ); setOptimizationError(null); }} />
optimizeIconSpecField(item.field)} > {item.optimization.optimizing ? ( <> {item.optimization.undoValue !== null && !item.optimization.optimizing ? ( undoIconSpecOptimization(item.field)} > ) : null} {getIconSpecPromptLength(value)}/ {EDITOR_ICON_SPEC_PROMPT_MAX_LENGTH}
); })} ) : null} {!isUiDesignDialog && dialog.specType !== 'custom' && !isIconSpec ? ( <> {dialog.specType === 'character' ? ( <> ) : null} ) : null}
{dialog.status === 'failed' ? ( {dialog.errorMessage} ) : null} {optimizationError ? ( {optimizationError} ) : null}
{isUiDesignDialog ? ( undefined)} includeDimensions onRememberImageModel={onRememberImageModel} hasPendingImageReferenceUploads={hasPendingImageReferenceUploads} lockedModel={IMAGE_MODEL_GPT_IMAGE_2} cost={calculateEditorUiDesignPrice( IMAGE_MODEL_GPT_IMAGE_2, dialog.imageSize, )} submitLabel="生成" submitAriaLabel="生成UI设计图" onSubmitRequest={() => { if (dialog.status !== 'generating') { onSubmit(dialog); } }} optionLabelPrefix="生成图片" renderEditorPortal={renderEditorPortal} buildPortalMenuStyle={buildPortalMenuStyle} /> ) : ( <>
{specSizeLabel}
{specModelLabel}
{ if (dialog.status !== 'generating') { onSubmit(dialog); } }} > {isGenerating ? ( '生成中' ) : ( <> 生成 {specGenerationCost}泥点 )} )}
{isGenerationReferenceMenuOpen && generationReferenceButtonRef ? renderEditorPortal( { setIsGenerationReferenceMenuOpen?.(false); setIsPickingGenerationReferenceFromCanvas?.(true); }} > 从画布中选择 {isUiDesignDialog ? ( { setIsGenerationReferenceMenuOpen?.(false); onOpenSpecDialog?.('icon'); }} > 新建图标规范 ) : null} { setIsGenerationReferenceMenuOpen?.(false); setIsPickingGenerationReferenceFromCanvas?.(false); onRequestUpload(uploadTarget); }} > 上传图片 , ) : null} ); }