import { Check, ChevronDown, Clock, Cpu, Film, ImageIcon, Music, Repeat, Sparkles, Undo2, WandSparkles, } from 'lucide-react'; import { type CSSProperties, type Dispatch, type ReactNode, type RefObject, type SetStateAction, useCallback, useEffect, useId, useRef, useState, } from 'react'; import { SOUND_EFFECT_DURATION_MAX_SECONDS, SOUND_EFFECT_DURATION_MIN_SECONDS, } from '../../../packages/shared/src/contracts/editorAudio'; import { AutoGrowTextArea } from '../common/AutoGrowTextArea'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformFloatingMenu, PlatformFloatingMenuItem, } from '../common/PlatformFloatingMenu'; import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { ImageCanvasBackgroundMusicPresetMarquee } from './ImageCanvasBackgroundMusicPresetMarquee'; import { appendBackgroundMusicPromptPreset, BACKGROUND_MUSIC_PROMPT_PRESETS, type BackgroundMusicPromptPreset, } from './ImageCanvasBackgroundMusicPresetModel'; import { BACKGROUND_MUSIC_PROMPT_MAX_CODE_POINTS, canCompleteBackgroundMusicPrompt, canGenerateBackgroundMusicFromPrompt, canSimplifyBackgroundMusicPrompt, countPromptCodePoints, toBackgroundMusicGenerationDialog, } from './ImageCanvasBackgroundMusicPromptModel'; import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView'; import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView'; import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView'; import { ImageCanvasCropExpandPanelView } from './ImageCanvasCropExpandPanelView'; import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView'; import { ImageCanvasEditorPortal } from './ImageCanvasEditorPortal'; import type { CanvasGenerationDialogState, CanvasLayer, CharacterAnimationPanelState, CropExpandPanelState, GenerateDialogState, QuickEditPanelState, SpecFormValues, SpecGenerationType, UploadTarget, } from './ImageCanvasEditorTypes'; import { calculateEditorBackgroundMusicPrice, calculateEditorSoundEffectPrice, calculateEditorVideoPrice, DEFAULT_BACKGROUND_MUSIC_MODEL, DEFAULT_SOUND_EFFECT_DURATION_SECONDS, DEFAULT_SOUND_EFFECT_MODEL, DEFAULT_VIDEO_ASPECT_RATIO, DEFAULT_VIDEO_DURATION_SECONDS, DEFAULT_VIDEO_SOUND, EDITOR_SOUND_EFFECT_MODEL_OPTIONS, EDITOR_VIDEO_ASPECT_RATIO_OPTIONS, EDITOR_VIDEO_MODEL_OPTIONS, EDITOR_VIDEO_RESOLUTION_OPTIONS, resizeGenerationPlaceholderToVideoSelection, SPEC_TYPE_LABEL, VIDEO_MODEL_SEEDANCE_2_FAST, } from './ImageCanvasGenerationModel'; import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView'; import { ImageCanvasPublicationMaterialsDemoPanelView } from './ImageCanvasPublicationMaterialsDemoPanelView'; import { getPublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel'; import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView'; import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot'; import { ImageCanvasSceneStyleControl } from './ImageCanvasSceneStyleControl'; import { ImageCanvasSoundEffectPresetMarquee } from './ImageCanvasSoundEffectPresetMarquee'; import { appendSoundEffectPromptPreset, SOUND_EFFECT_PROMPT_PRESETS, type SoundEffectPromptPreset, } from './ImageCanvasSoundEffectPresetModel'; import { canGenerateSoundEffectFromPrompt, canOptimizeSoundEffectPrompt, countSoundEffectPromptCodePoints, SOUND_EFFECT_PROMPT_MAX_CODE_POINTS, toSoundEffectGenerationDialog, } from './ImageCanvasSoundEffectPromptModel'; import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView'; import type { BackgroundMusicPromptAssistComposerController } from './useImageCanvasBackgroundMusicPromptAssist'; import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss'; import type { SoundEffectPromptAssistComposerController } from './useImageCanvasSoundEffectPromptAssist'; type ImageCanvasGenerationComposerViewProps = { specToolWrapRef: RefObject; publicationReferenceButtonRef: RefObject; characterSpecButtonRef: RefObject; characterReferenceButtonRef: RefObject; generationReferenceButtonRef: RefObject; iconSpecButtonRef: RefObject; isSpecMenuOpen: boolean; isGenerationReferenceMenuOpen: boolean; isPublicationReferenceMenuOpen: boolean; isCharacterSpecMenuOpen: boolean; isCharacterReferenceMenuOpen: boolean; isIconSpecMenuOpen: boolean; isUiDesignSpecMenuOpen: boolean; isPickingGenerationReferenceFromCanvas: boolean; isPickingQuickEditReferenceFromCanvas: boolean; isPickingPublicationReferenceFromCanvas: boolean; isPickingCharacterSpecFromCanvas: boolean; isPickingCharacterReferenceFromCanvas: boolean; isPickingIconSpecFromCanvas: boolean; isPickingUiDesignSpecFromCanvas: boolean; hasPendingImageReferenceUploads?: boolean; generateDialog: GenerateDialogState | null; backgroundMusicPromptAssist?: BackgroundMusicPromptAssistComposerController; soundEffectPromptAssist?: SoundEffectPromptAssistComposerController; updateCanvasGenerationDialogById: ( dialogId: string, updater: ( dialog: CanvasGenerationDialogState, ) => CanvasGenerationDialogState | null, ) => void; generationComposerStyle: CSSProperties | null; iconComposerStyle: CSSProperties | null; quickEditPanel: QuickEditPanelState | null; quickEditSourceLayer: CanvasLayer | null; quickEditPanelStyle: CSSProperties | null; cropExpandPanel: CropExpandPanelState | null; cropExpandSourceLayer: CanvasLayer | null; cropExpandPanelStyle: CSSProperties | null; characterAnimationPanel: CharacterAnimationPanelState | null; characterAnimationSourceLayer: CanvasLayer | null; characterAnimationPanelStyle: CSSProperties | null; characterAnimationPrice: number; setGenerateDialog: Dispatch>; setQuickEditPanel: Dispatch>; setCropExpandPanel: Dispatch>; setCharacterAnimationPanel: Dispatch< SetStateAction >; setIsGenerationReferenceMenuOpen: Dispatch>; setIsPublicationReferenceMenuOpen: Dispatch>; setIsCharacterSpecMenuOpen: Dispatch>; setIsCharacterReferenceMenuOpen: Dispatch>; setIsIconSpecMenuOpen: Dispatch>; setIsUiDesignSpecMenuOpen: Dispatch>; setIsPickingGenerationReferenceFromCanvas: Dispatch>; setIsPickingQuickEditReferenceFromCanvas: Dispatch>; setIsPickingPublicationReferenceFromCanvas: Dispatch>; setIsPickingCharacterSpecFromCanvas: Dispatch>; setIsPickingCharacterReferenceFromCanvas: Dispatch>; setIsPickingIconSpecFromCanvas: Dispatch>; setIsPickingUiDesignSpecFromCanvas: Dispatch>; onOpenSpecDialog: (specType: SpecGenerationType) => void; onRequestUpload: (target: UploadTarget) => void; /** 参考图来源菜单里的「从项目素材中选择」;不传则不显示该项。 */ onRequestProjectAssetPicker?: () => void; onSubmitImageGeneration: (dialog: GenerateDialogState) => void; onSubmitIconSpritesheetGeneration: (dialog: GenerateDialogState) => void; onSubmitQuickEdit: () => void; onSubmitCropExpand: () => void; onSubmitCharacterAnimation: () => void; onRetryPerfectPixelOperation?: (dialogId: string) => void; onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void; onUpdateIconDescriptionText: (value: string) => void; onUpdateCharacterAnimationDuration: (frameCountValue: string) => void; onRememberImageModel: (model: string) => void; onSpecMenuPointerEnter?: () => void; onSpecMenuPointerLeave?: () => void; }; function PerfectPixelOperationPanel({ dialog, style, onRetry, }: { dialog: GenerateDialogState; style: CSSProperties; onRetry?: (dialogId: string) => void; }) { const isPending = dialog.status === 'pending-confirmation'; const isPreparedFailure = dialog.status === 'failed' && Boolean(dialog.perfectPixelOperation); const isGenerating = dialog.status === 'generating'; const isInvalid = dialog.perfectPixelOperationInvalid === true; const message = isInvalid ? (dialog.errorMessage ?? '完美像素操作快照无效,禁止自动重试。') : isPending ? (dialog.errorMessage ?? '结果尚未确认,系统不会自动重复提交。') : isGenerating ? '完美像素处理中' : dialog.status === 'failed' ? (dialog.errorMessage ?? '完美像素处理失败') : '完美像素处理已完成'; return (
event.stopPropagation()} > 完美像素 {message} {!isInvalid && (isPending || isPreparedFailure) && dialog.id && dialog.perfectPixelOperation ? ( onRetry?.(dialog.id!)} > 重试同一完美像素操作 ) : null}
); } 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) { return {node}; } function resetFailedVideoDialogStatus(dialog: GenerateDialogState) { return { ...dialog, status: dialog.status === 'failed' ? 'idle' : dialog.status, errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage, }; } function isSeedanceVideoModel(model: string | undefined) { return model === 'seedance2.0' || model === 'seedance2.0-fast'; } function VideoReferenceChip({ reference, index, onRemove, }: { reference: { id: string; label: string; src: string; mediaType?: string; objectKey?: string | null; }; index: number; onRemove: () => void; }) { const mediaType = reference.mediaType ?? 'image'; const labelPrefix = mediaType === 'video' ? '参考视频' : mediaType === 'audio' ? '参考音频' : '参考图'; const label = reference.label || `${labelPrefix}${index + 1}`; const icon = mediaType === 'video' ? (