import { History, ImagePlus, Loader2, Sparkles, Trash2 } from 'lucide-react'; import { type ReactNode, useEffect, useRef, useState } from 'react'; import { ResolvedAssetImage } from '../ResolvedAssetImage'; import { PlatformActionButton } from './PlatformActionButton'; import { PlatformFieldLabel } from './PlatformFieldLabel'; import { PlatformIconBadge } from './PlatformIconBadge'; import { PlatformIconButton } from './PlatformIconButton'; import { PlatformModalCloseButton } from './PlatformModalCloseButton'; import { PlatformPillBadge } from './PlatformPillBadge'; import { PlatformPillSwitch } from './PlatformPillSwitch'; import { PlatformStatusMessage } from './PlatformStatusMessage'; import { PlatformTextField } from './PlatformTextField'; import { PlatformUploadPreviewCard } from './PlatformUploadPreviewCard'; export type CreativeImageInputReferenceImage = { id: string; label: string; imageSrc: string; assetObjectId?: string | null; }; export type CreativeImageInputPanelLabels = { imageField: string; uploadImage: string; replaceImage: string; emptyImageHint: string; removeImage: string; removeImageConfirmTitle: string; removeImageConfirmBody: string; promptReferenceUpload: string; promptReferencePreviewAlt: string; closePromptReferencePreview: string; previewMainImage?: string; closeMainImagePreview?: string; history?: string; }; export type CreativeImageInputPanelProps = { className?: string; fillHeight?: boolean; disabled?: boolean; isSubmitting?: boolean; mainImageMode?: 'edit' | 'preview'; mainImageClickMode?: 'upload' | 'preview'; canUploadMainImage?: boolean; canUseImageHistory?: boolean; canRemoveMainImage?: boolean; canToggleAiRedraw?: boolean; canUploadPromptReferences?: boolean; uploadedImageSrc: string; uploadedImageAlt: string; uploadedImageRefreshKey?: string | number | null; mainImageMeta?: ReactNode; mainImageInputId: string; mainImageAccept?: string; promptTextareaId: string; prompt: string; promptLabel: string; promptAriaLabel?: string; promptRows?: number; aiRedraw: boolean; promptReferenceImages: CreativeImageInputReferenceImage[]; promptReferenceLimit?: number; imageLimitHint?: string | null; imageModelPicker?: ReactNode; error?: string | null; inputError?: string | null; showSubmitButton?: boolean; submitLabel: string; submitCostLabel?: string | null; submitDisabled: boolean; labels: CreativeImageInputPanelLabels; onMainImageFileSelect: (file: File) => void; onMainImageRemove: () => void; onAiRedrawChange: (enabled: boolean) => void; onPromptChange: (value: string) => void; onPromptReferenceFilesSelect?: (files: File[]) => void; onPromptReferenceRemove?: (referenceId: string) => void; onHistoryClick?: () => void; onSubmit: () => void; }; const DEFAULT_IMAGE_ACCEPT = 'image/png,image/jpeg,image/webp'; const DEFAULT_PROMPT_REFERENCE_LIMIT = 5; export function CreativeImageInputPanel({ className = '', fillHeight = true, disabled = false, isSubmitting = false, mainImageMode = 'edit', mainImageClickMode = 'preview', canUploadMainImage = true, canUseImageHistory = true, canRemoveMainImage = true, canToggleAiRedraw = true, canUploadPromptReferences, uploadedImageSrc, uploadedImageAlt, uploadedImageRefreshKey = null, mainImageMeta = null, mainImageInputId, mainImageAccept = DEFAULT_IMAGE_ACCEPT, promptTextareaId, prompt, promptLabel, promptAriaLabel, promptRows = 2, aiRedraw, promptReferenceImages, promptReferenceLimit = DEFAULT_PROMPT_REFERENCE_LIMIT, imageLimitHint = null, imageModelPicker = null, error = null, inputError = null, showSubmitButton = true, submitLabel, submitCostLabel = null, submitDisabled, labels, onMainImageFileSelect, onMainImageRemove, onAiRedrawChange, onPromptChange, onPromptReferenceFilesSelect, onPromptReferenceRemove, onHistoryClick, onSubmit, }: CreativeImageInputPanelProps) { const mainImageInputRef = useRef(null); const [previewReferenceImage, setPreviewReferenceImage] = useState(null); const [isMainImagePreviewOpen, setIsMainImagePreviewOpen] = useState(false); const [isRemoveImageConfirmOpen, setIsRemoveImageConfirmOpen] = useState(false); const showPrompt = mainImageMode === 'preview' || !uploadedImageSrc || aiRedraw; const shouldShowPromptReferences = canUploadPromptReferences ?? !uploadedImageSrc; const promptReferenceUploadDisabled = disabled || promptReferenceImages.length >= promptReferenceLimit; const canEditMainImage = mainImageMode === 'edit'; const isMainImageUploadEnabled = canEditMainImage && canUploadMainImage; const shouldShowHistoryButton = canEditMainImage && canUseImageHistory && Boolean(onHistoryClick); const shouldPreviewMainImage = mainImageClickMode === 'preview' && Boolean(uploadedImageSrc); const shouldShowMainImageUploadButton = isMainImageUploadEnabled && shouldPreviewMainImage; useEffect(() => { if (uploadedImageSrc) { setPreviewReferenceImage(null); } else { setIsMainImagePreviewOpen(false); } }, [uploadedImageSrc]); useEffect(() => { if ( previewReferenceImage && !promptReferenceImages.some( (reference) => reference.id === previewReferenceImage.id, ) ) { setPreviewReferenceImage(null); } }, [previewReferenceImage, promptReferenceImages]); const bodyClassName = fillHeight ? 'creative-image-input-panel__body puzzle-creation-form-body flex min-h-0 flex-1 flex-col overflow-hidden pr-0 lg:overflow-y-auto lg:pr-1' : 'creative-image-input-panel__body puzzle-creation-form-body flex flex-none flex-col overflow-visible pr-0 lg:pr-1'; const sectionClassName = fillHeight ? 'creative-image-input-panel__section puzzle-creation-form-section flex min-h-0 flex-1 flex-col overflow-hidden lg:overflow-visible' : 'creative-image-input-panel__section puzzle-creation-form-section flex flex-none flex-col overflow-visible'; const gridSizeClassName = fillHeight ? 'min-h-0 flex-1' : 'flex-none'; const imageFieldClassName = fillHeight ? 'creative-image-input-panel__image-field puzzle-image-field flex min-h-0 min-w-0 flex-1 flex-col' : 'creative-image-input-panel__image-field puzzle-image-field flex min-w-0 flex-none flex-col'; const imageFrameClassName = fillHeight ? 'creative-image-input-panel__image-frame puzzle-image-card-frame flex min-h-0 flex-1 items-center justify-center' : 'creative-image-input-panel__image-frame puzzle-image-card-frame flex flex-none items-center justify-center'; const imageCardClassName = fillHeight ? 'creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square h-full min-h-[14rem] max-h-full max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem] lg:h-auto lg:w-full' : 'creative-image-input-panel__image-card puzzle-image-upload-card relative aspect-square w-full min-h-[14rem] max-w-full overflow-hidden rounded-[1.25rem] border border-[var(--platform-subpanel-border)] bg-white/90 shadow-[0_12px_28px_rgba(15,23,42,0.08)] transition sm:min-h-[18rem]'; return (
{labels.imageField}
{isMainImageUploadEnabled ? ( { const file = event.currentTarget.files?.[0] ?? null; event.currentTarget.value = ''; if (file) { onMainImageFileSelect(file); } }} className="sr-only" /> ) : null} {shouldPreviewMainImage ? (
{mainImageMeta ? (
{mainImageMeta}
) : null} {imageLimitHint ? (
{imageLimitHint}
) : null}
{showPrompt ? (
onPromptChange(event.target.value)} size="lg" density="roomy" className="h-[6rem] min-h-[6rem] rounded-[1.15rem] pb-14 font-normal placeholder:text-zinc-400 sm:h-[7.5rem] sm:min-h-[7.5rem] lg:h-[9.25rem] lg:min-h-[9.25rem]" aria-label={promptAriaLabel ?? promptLabel} /> {imageModelPicker} {shouldShowPromptReferences && onPromptReferenceFilesSelect ? ( { const files = Array.from( event.currentTarget.files ?? [], ); event.currentTarget.value = ''; if (files.length > 0) { onPromptReferenceFilesSelect(files); } }} className="sr-only" /> } className={`absolute bottom-3 right-3 z-10 h-8 w-8 border-[var(--platform-subpanel-border)] bg-white/96 hover:bg-[var(--platform-subpanel-fill)] ${ promptReferenceUploadDisabled ? 'cursor-not-allowed opacity-55' : 'cursor-pointer' }`} /> ) : null}
{shouldShowPromptReferences && promptReferenceImages.length > 0 ? (
{promptReferenceImages.map((reference) => ( setPreviewReferenceImage(reference)} onRemove={ onPromptReferenceRemove ? () => onPromptReferenceRemove(reference.id) : undefined } disabled={disabled} resolveAsset className="h-12 w-12 rounded-[0.75rem] bg-white/90 shadow-sm" previewButtonProps={{ title: reference.label }} removeButtonProps={{ title: '移除参考图', className: 'right-0.5 top-0.5 bg-white/94 text-[var(--platform-text-strong)] shadow-sm hover:bg-white hover:text-[var(--platform-accent)]', }} /> ))}
) : null}
) : null}
{inputError ? ( {inputError} ) : null} {error ? ( {error} ) : null}
{showSubmitButton ? (
{isSubmitting ? ( ) : null} {submitLabel} {submitCostLabel ? ( {submitCostLabel} ) : null}
) : null} {previewReferenceImage ? (
setPreviewReferenceImage(null)} >
event.stopPropagation()} >
{previewReferenceImage.label}
setPreviewReferenceImage(null)} className="shrink-0" />
) : null} {isMainImagePreviewOpen && uploadedImageSrc ? (
setIsMainImagePreviewOpen(false)} >
event.stopPropagation()} >
{labels.previewMainImage ?? uploadedImageAlt}
setIsMainImagePreviewOpen(false)} className="shrink-0" />
) : null} {isRemoveImageConfirmOpen ? (
{labels.removeImageConfirmTitle}
{labels.removeImageConfirmBody}
setIsRemoveImageConfirmOpen(false)} > 取消 { onMainImageRemove(); setIsRemoveImageConfirmOpen(false); }} > 移除
) : null}
); } export default CreativeImageInputPanel;