import { Check, ChevronDown, Cpu } from 'lucide-react'; import { type CSSProperties, type Dispatch, type ReactNode, type SetStateAction, useCallback, useRef, useState, } from 'react'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformFloatingMenu } from '../common/PlatformFloatingMenu'; import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import type { GenerateDialogState } from './ImageCanvasEditorTypes'; import { EDITOR_IMAGE_DIMENSION_OPTIONS, EDITOR_IMAGE_MODEL_OPTIONS, IMAGE_MODEL_NANOBANANA2, normalizeEditorImageModel, resizeGenerationPlaceholderToImageSelection, } from './ImageCanvasGenerationModel'; import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss'; type ImageCanvasGenerationImageOptionsViewProps = { dialog: GenerateDialogState; setGenerateDialog: Dispatch>; includeDimensions: boolean; onRememberImageModel: (model: string) => void; optionLabelPrefix?: string; cost?: number; submitLabel?: string; submitAriaLabel?: string; renderEditorPortal?: (node: ReactNode) => ReactNode; buildPortalMenuStyle?: ( anchor: HTMLElement | null, placement: 'above' | 'below', ) => CSSProperties; }; type OpenPanel = 'dimensions' | 'model' | null; function resetFailedDialogStatus(dialog: GenerateDialogState) { return { ...dialog, status: dialog.status === 'failed' ? 'idle' : dialog.status, errorMessage: dialog.status === 'failed' ? undefined : dialog.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 = normalizeEditorImageModel(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 getImageModelLabel(value: string) { return ( EDITOR_IMAGE_MODEL_OPTIONS.find((option) => option.value === value) ?.label ?? value ); } function OptionChoice({ children, selected, className, onClick, }: { children: ReactNode; selected: boolean; className?: string; onClick: () => void; }) { return ( ); } export function ImageCanvasGenerationImageOptionsView({ dialog, setGenerateDialog, includeDimensions, onRememberImageModel, optionLabelPrefix, cost, submitLabel = '生成', submitAriaLabel = '生成', renderEditorPortal = (node) => node, buildPortalMenuStyle = () => ({}), }: ImageCanvasGenerationImageOptionsViewProps) { const [openPanel, setOpenPanel] = useState(null); const dimensionsButtonRef = useRef(null); const modelButtonRef = useRef(null); const selection = normalizeImageDialogSelection(dialog); const selectedModelLabel = getImageModelLabel(selection.model); const isGenerating = dialog.status === 'generating'; const labelPrefix = optionLabelPrefix ?? '生成图片'; const dismissOpenPanel = useCallback(() => setOpenPanel(null), []); useImageCanvasFloatingOptionDismiss({ isOpen: openPanel !== null, boundaryRefs: [dimensionsButtonRef, modelButtonRef], onDismiss: dismissOpenPanel, }); const updateDialog = (patch: Partial) => { setGenerateDialog((currentDialog) => currentDialog && currentDialog.mode === dialog.mode ? resizeGenerationPlaceholderToImageSelection({ ...resetFailedDialogStatus(currentDialog), ...patch, }) : currentDialog, ); }; const updateImageModel = (model: string) => { onRememberImageModel(model); setGenerateDialog((currentDialog) => { if (!currentDialog || currentDialog.mode !== dialog.mode) { return currentDialog; } const nextOptions = getImageDimensionOptions(model); const nextAspectRatios = nextOptions.aspectRatios as readonly string[]; const nextImageSizes = nextOptions.imageSizes as readonly string[]; return resizeGenerationPlaceholderToImageSelection({ ...resetFailedDialogStatus(currentDialog), imageModel: model, aspectRatio: currentDialog.aspectRatio && nextAspectRatios.includes(currentDialog.aspectRatio) ? currentDialog.aspectRatio : nextOptions.aspectRatios[0], imageSize: currentDialog.imageSize && nextImageSizes.includes(currentDialog.imageSize) ? currentDialog.imageSize : (nextOptions.imageSizes.find((size) => size === '1K') ?? nextOptions.imageSizes[0]), }); }); }; const togglePanel = (panel: Exclude) => { setOpenPanel((currentPanel) => (currentPanel === panel ? null : panel)); }; return ( <> {includeDimensions ? (
} onClick={() => togglePanel('dimensions')} > {selection.aspectRatio} · {selection.imageSize} {openPanel === 'dimensions' ? renderEditorPortal( event.stopPropagation()} >
比例
{selection.options.aspectRatios.map((aspectRatio) => ( updateDialog({ aspectRatio })} > ))}
尺寸
{selection.options.imageSizes.map((imageSize) => ( updateDialog({ imageSize })} > {imageSize} ))}
, ) : null}
) : null}
} onClick={() => togglePanel('model')} > {selectedModelLabel} {openPanel === 'model' ? renderEditorPortal( event.stopPropagation()} >
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => { const selected = selection.model === option.value; return ( updateImageModel(option.value)} > {option.label} ); })}
, ) : null}
{typeof cost === 'number' ? ( {isGenerating ? ( '生成中' ) : ( <> {submitLabel} {cost}泥点 )} ) : null} ); }