'use client'; import { ModeToggle } from '@/components/mode-toggle'; import type { WorkbenchMode } from '@/components/mode-toggle'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Slider } from '@/components/ui/slider'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import type { GptImageModel } from '@/lib/cost-utils'; import { useI18n } from '@/lib/i18n'; import { getImageOutputFormatLabel, getImageQualityLabel } from '@/lib/image-display-labels'; import type { ImageUpstreamFormBackend, ImageUpstreamFormPromptOptimization, ImageUpstreamFormStreamingStrategy, ImageUpstreamFormThinking } from '@/lib/image-upstream-form'; import { getImageUpstreamRouteImpactKeys, isImageUpstreamStreamingStrategySelectable, resolveImageUpstreamEffectiveStreamingStrategy } from '@/lib/image-upstream-form'; import { buildIntegerRangeOptions, clampIntegerToRange, getPartialImagesRangeForBackend, type ImageUpstreamProfile, type PartialImagesCount } from '@/lib/image-upstream-profile'; import type { ImageStreamMode, ImageStreamingStrategy } from '@/lib/image-upstream-strategy'; import { getPresetTooltip, getSizePresetOptions, validateGptImage2Size, validatePositiveIntegerImageSize } from '@/lib/size-utils'; import type { SizePreset } from '@/lib/size-utils'; import { resolveStreamingBatchToggleState } from '@/lib/streaming-batch'; import { getStreamingStatusLabel } from '@/lib/streaming-status-label'; import { Bookmark, Eraser, Save, Square, RectangleHorizontal, RectangleVertical, Sparkles, SlidersHorizontal, ChevronDown, CircleOff, Tally1, Tally2, Tally3, Tally4, Loader2, X, ScanEye, UploadCloud, Lock, LockOpen, HelpCircle, SquareDashed, FileImage, WandSparkles, Globe2, ShieldCheck, ShieldAlert } from 'lucide-react'; import Image from 'next/image'; import * as React from 'react'; type DrawnPoint = { x: number; y: number; size: number; }; export type EditingFormData = { prompt: string; n: number; size: SizePreset; customWidth: number; customHeight: number; quality: 'low' | 'medium' | 'high' | 'auto'; output_format: 'png' | 'jpeg' | 'webp'; output_compression?: number; moderation: 'low' | 'auto'; imageFiles: File[]; maskFile: File | null; model: GptImageModel; image_backend: ImageUpstreamFormBackend; streaming_strategy: ImageUpstreamFormStreamingStrategy; responsesModel: string; thinking: ImageUpstreamFormThinking; promptOptimization: ImageUpstreamFormPromptOptimization; forceWeb: boolean; enableParallelBatch: boolean; }; type EditingFormProps = { onSubmit: (data: EditingFormData) => void; onSaveInspiration: (prompt: string) => void; canApplyRandomInspiration: boolean; onPickRandomInspiration: () => string; isLoading: boolean; showLoadingState?: boolean; isActive: boolean; currentMode: WorkbenchMode; onModeChange: (mode: WorkbenchMode) => void; reuseContext: EditingReuseContext | null; onClearReuseContext: () => void; isPasswordRequiredByBackend: boolean | null; clientPasswordHash: string | null; onOpenPasswordDialog: () => void; editModel: EditingFormData['model']; setEditModel: React.Dispatch>; imageFiles: File[]; sourceImagePreviewUrls: string[]; setImageFiles: React.Dispatch>; setSourceImagePreviewUrls: React.Dispatch>; maxImages: number; editPrompt: string; setEditPrompt: React.Dispatch>; editN: number[]; setEditN: React.Dispatch>; editSize: EditingFormData['size']; setEditSize: React.Dispatch>; editCustomWidth: number; setEditCustomWidth: React.Dispatch>; editCustomHeight: number; setEditCustomHeight: React.Dispatch>; editQuality: EditingFormData['quality']; setEditQuality: React.Dispatch>; editOutputFormat: EditingFormData['output_format']; setEditOutputFormat: React.Dispatch>; editCompression: number[]; setEditCompression: React.Dispatch>; upstreamProfile: ImageUpstreamProfile; upstreamProfileMixed?: boolean; editModeration: EditingFormData['moderation']; setEditModeration: React.Dispatch>; editBrushSize: number[]; setEditBrushSize: React.Dispatch>; editShowMaskEditor: boolean; setEditShowMaskEditor: React.Dispatch>; editGeneratedMaskFile: File | null; setEditGeneratedMaskFile: React.Dispatch>; editIsMaskSaved: boolean; setEditIsMaskSaved: React.Dispatch>; editOriginalImageSize: { width: number; height: number } | null; setEditOriginalImageSize: React.Dispatch>; editDrawnPoints: DrawnPoint[]; setEditDrawnPoints: React.Dispatch>; editMaskPreviewUrl: string | null; setEditMaskPreviewUrl: React.Dispatch>; streamMode: ImageStreamMode; setStreamMode: React.Dispatch>; allowStreamingBatch: boolean; enableParallelBatch: boolean; setEnableParallelBatch: React.Dispatch>; partialImages: PartialImagesCount; setPartialImages: React.Dispatch>; allowResponsesImageBackend: boolean; hasDefaultResponsesModel: boolean; editImageBackend: EditingFormData['image_backend']; setEditImageBackend: React.Dispatch>; editStreamingStrategy: EditingFormData['streaming_strategy']; defaultStreamingStrategy: ImageStreamingStrategy; setEditStreamingStrategy: React.Dispatch>; editResponsesModel: string; setEditResponsesModel: React.Dispatch>; editThinking: EditingFormData['thinking']; setEditThinking: React.Dispatch>; editPromptOptimization: EditingFormData['promptOptimization']; setEditPromptOptimization: React.Dispatch>; editForceWeb: boolean; setEditForceWeb: React.Dispatch>; requestSummaryLabel: string; initialAdvancedOpen?: boolean; initialAdvancedTab?: AdvancedTab; }; export type EditingReuseContext = { sourceLabel: string; restoredFields: string[]; promptPreview: string; }; type AdvancedTab = 'output' | 'model' | 'stream' | 'route'; const RadioItemWithIcon = ({ value, id, label, Icon, disabled = false, tooltip }: { value: string; id: string; label: string; Icon: React.ElementType; disabled?: boolean; tooltip?: React.ReactNode; }) => { const item = ( {label} ); if (!tooltip) return item; return ( {item} {tooltip} ); }; function getSizePresetLabel( preset: SizePreset, t: (key: string, values?: Record) => string ): string { switch (preset) { case 'auto': return t('common.auto'); case 'custom': return t('common.custom'); case 'square': return t('common.square'); case 'landscape': return t('common.landscape'); case 'portrait': return t('common.portrait'); case 'square-1k': return '1K'; case 'square-2k': return '2K'; case 'square-4k': return '4K'; case 'wide-4k': return '16:9'; case 'tall-4k': return '9:16'; } } function getSizePresetIcon(preset: SizePreset): React.ElementType { switch (preset) { case 'auto': return Sparkles; case 'custom': return SquareDashed; case 'landscape': case 'wide-4k': return RectangleHorizontal; case 'portrait': case 'tall-4k': return RectangleVertical; default: return Square; } } function getBackendLabel(backend: EditingFormData['image_backend'], t: (key: string) => string): string { if (backend === 'images-api') return t('upstream.backendImages'); if (backend === 'responses-image-generation') return t('upstream.backendResponses'); return t('upstream.serverDefault'); } function getWorkbenchBackendLabel(backend: EditingFormData['image_backend'], t: (key: string) => string): string { if (backend === 'images-api') return t('upstream.backendImages'); if (backend === 'responses-image-generation') return t('upstream.backendResponses'); return t('upstream.workbenchDefaultRoute'); } function getStreamModeLabel(streamMode: ImageStreamMode, t: (key: string) => string): string { if (streamMode === 'stream') return t('streaming.modeStream'); if (streamMode === 'non_stream') return t('streaming.modeNonStream'); return t('streaming.modeAuto'); } export function EditingForm({ onSubmit, onSaveInspiration, canApplyRandomInspiration, onPickRandomInspiration, isLoading, showLoadingState = isLoading, isActive, currentMode, onModeChange, reuseContext, onClearReuseContext, isPasswordRequiredByBackend, clientPasswordHash, onOpenPasswordDialog, editModel, setEditModel, imageFiles, sourceImagePreviewUrls, setImageFiles, setSourceImagePreviewUrls, maxImages, editPrompt, setEditPrompt, editN, setEditN, editSize, setEditSize, editCustomWidth, setEditCustomWidth, editCustomHeight, setEditCustomHeight, editQuality, setEditQuality, editOutputFormat, setEditOutputFormat, editCompression, setEditCompression, upstreamProfile, upstreamProfileMixed = false, editModeration, setEditModeration, editBrushSize, setEditBrushSize, editShowMaskEditor, setEditShowMaskEditor, editGeneratedMaskFile, setEditGeneratedMaskFile, editIsMaskSaved, setEditIsMaskSaved, editOriginalImageSize, setEditOriginalImageSize, editDrawnPoints, setEditDrawnPoints, editMaskPreviewUrl, setEditMaskPreviewUrl, streamMode, setStreamMode, allowStreamingBatch, enableParallelBatch, setEnableParallelBatch, partialImages, setPartialImages, allowResponsesImageBackend, hasDefaultResponsesModel, editImageBackend, setEditImageBackend, editStreamingStrategy, defaultStreamingStrategy, setEditStreamingStrategy, editResponsesModel, setEditResponsesModel, editThinking, setEditThinking, editPromptOptimization, setEditPromptOptimization, editForceWeb, setEditForceWeb, requestSummaryLabel, initialAdvancedOpen = false, initialAdvancedTab = 'output' }: EditingFormProps) { const { locale, t } = useI18n(); const [firstImagePreviewUrl, setFirstImagePreviewUrl] = React.useState(null); const isGptImage2 = editModel === 'gpt-image-2'; const usesPositiveIntegerCustomSize = upstreamProfile.gptImage2.sizePolicy === 'positive-integer'; const customSizeValidation = editSize === 'custom' ? usesPositiveIntegerCustomSize ? validatePositiveIntegerImageSize(editCustomWidth, editCustomHeight) : validateGptImage2Size(editCustomWidth, editCustomHeight) : { valid: true as const }; const customSizeInvalid = editSize === 'custom' && !customSizeValidation.valid; const editCustomPixels = editCustomWidth * editCustomHeight; const editCustomRatio = editCustomWidth > 0 && editCustomHeight > 0 ? t('form.ratio', { ratio: ( Math.max(editCustomWidth, editCustomHeight) / Math.min(editCustomWidth, editCustomHeight) ).toFixed(2) }) : t('form.noRatio'); const editCustomSizeError = customSizeValidation.valid ? null : t(customSizeValidation.reasonKey, customSizeValidation.values); const showCompression = editOutputFormat === 'jpeg' || editOutputFormat === 'webp'; const effectiveStreamingStrategy = resolveImageUpstreamEffectiveStreamingStrategy({ streamingStrategy: editStreamingStrategy, defaultStreamingStrategy }); const streamingDisabledByStrategy = effectiveStreamingStrategy === 'off'; const parallelBatchToggle = resolveStreamingBatchToggleState({ allowStreamingBatch, userEnabled: enableParallelBatch, targetCount: editN[0], streamMode, streamingStrategy: effectiveStreamingStrategy }); const canEnableParallelBatch = parallelBatchToggle.canEnable; const parallelBatchChecked = parallelBatchToggle.checked; const parallelBatchUnavailableKey = parallelBatchToggle.unavailableReasonKey; const [isAdvancedOpen, setIsAdvancedOpen] = React.useState(initialAdvancedOpen); const [advancedTab, setAdvancedTab] = React.useState(initialAdvancedTab); const requiresResponsesModel = editImageBackend === 'responses-image-generation' && !hasDefaultResponsesModel && !editResponsesModel.trim(); const submitDisabledReason = React.useMemo(() => { if (isLoading) return ''; if (imageFiles.length === 0) return t('ux.disabledSourceImage'); if (imageFiles.length > maxImages) return t('alert.maxImages', { count: maxImages }); if (!editPrompt.trim()) return t('ux.disabledPrompt'); if (editDrawnPoints.length > 0 && !editGeneratedMaskFile && !editIsMaskSaved) { return t('ux.disabledUnsavedMask'); } if (requiresResponsesModel) return t('upstream.responsesModelRequired'); if (customSizeInvalid) return editCustomSizeError || t('ux.disabledCustomSize'); return ''; }, [ customSizeInvalid, editCustomSizeError, editDrawnPoints.length, editGeneratedMaskFile, editIsMaskSaved, editPrompt, imageFiles.length, isLoading, maxImages, requiresResponsesModel, t ]); const advancedSummary = [ `${t('form.quality')}: ${getImageQualityLabel(editQuality, t)}`, `${t('form.outputFormat')}: ${getImageOutputFormatLabel(editOutputFormat, t)}`, getBackendLabel(editImageBackend, t) ].join(', '); const streamModeLabel = getStreamModeLabel(streamMode, t); const streamStatusLabel = getStreamingStatusLabel(streamMode, t); const workbenchBackendLabel = getWorkbenchBackendLabel(editImageBackend, t); const editSizePresetOptions = getSizePresetOptions({ model: editModel, upstreamProfile }); const partialImagesRange = React.useMemo( () => getPartialImagesRangeForBackend(upstreamProfile, editImageBackend), [editImageBackend, upstreamProfile] ); const partialImageOptions = buildIntegerRangeOptions(partialImagesRange) as PartialImagesCount[]; // custom 仅对 gpt-image-2 有效,切换到旧模型时重置。 React.useEffect(() => { if (isActive && !isGptImage2 && editSize === 'custom') { setEditSize('auto'); } }, [editSize, isActive, isGptImage2, setEditSize]); React.useEffect(() => { if (isActive && streamingDisabledByStrategy && streamMode !== 'non_stream') { setStreamMode('non_stream'); } }, [isActive, streamingDisabledByStrategy, streamMode, setStreamMode]); React.useEffect(() => { if (isActive && (partialImages < partialImagesRange.min || partialImages > partialImagesRange.max)) { setPartialImages(clampIntegerToRange(partialImages, partialImagesRange) as PartialImagesCount); } }, [isActive, partialImages, partialImagesRange, setPartialImages]); React.useEffect(() => { if (isActive && (editN[0] < upstreamProfile.editCount.min || editN[0] > upstreamProfile.editCount.max)) { setEditN([Math.min(upstreamProfile.editCount.max, Math.max(upstreamProfile.editCount.min, editN[0]))]); } }, [editN, isActive, setEditN, upstreamProfile.editCount.max, upstreamProfile.editCount.min]); const canvasRef = React.useRef(null); const visualFeedbackCanvasRef = React.useRef(null); const isDrawing = React.useRef(false); const lastPos = React.useRef<{ x: number; y: number } | null>(null); const imageInputRef = React.useRef(null); const maskInputRef = React.useRef(null); const primaryImagePreviewUrl = sourceImagePreviewUrls[0] ?? null; const hasSourceImages = imageFiles.length > 0; const hasSourcePreviews = sourceImagePreviewUrls.length > 0; const canAddSourceImage = !isLoading && imageFiles.length < maxImages; React.useEffect(() => { if (editOriginalImageSize) { if (!visualFeedbackCanvasRef.current) { visualFeedbackCanvasRef.current = document.createElement('canvas'); } visualFeedbackCanvasRef.current.width = editOriginalImageSize.width; visualFeedbackCanvasRef.current.height = editOriginalImageSize.height; } }, [editOriginalImageSize]); React.useEffect(() => { let cancelled = false; queueMicrotask(() => { if (cancelled) return; setEditGeneratedMaskFile(null); setEditIsMaskSaved(false); setEditOriginalImageSize(null); setFirstImagePreviewUrl(null); setEditDrawnPoints([]); setEditMaskPreviewUrl(null); }); if (primaryImagePreviewUrl) { const img = new window.Image(); img.onload = () => { if (cancelled) return; setEditOriginalImageSize({ width: img.width, height: img.height }); }; img.src = primaryImagePreviewUrl; queueMicrotask(() => { if (!cancelled) { setFirstImagePreviewUrl(primaryImagePreviewUrl); } }); } else { queueMicrotask(() => { if (!cancelled) { setEditShowMaskEditor(false); } }); } return () => { cancelled = true; }; }, [ primaryImagePreviewUrl, setEditGeneratedMaskFile, setEditIsMaskSaved, setEditOriginalImageSize, setFirstImagePreviewUrl, setEditDrawnPoints, setEditMaskPreviewUrl, setEditShowMaskEditor ]); React.useEffect(() => { const displayCtx = canvasRef.current?.getContext('2d'); const displayCanvas = canvasRef.current; const feedbackCanvas = visualFeedbackCanvasRef.current; if (!displayCtx || !displayCanvas || !feedbackCanvas || !editOriginalImageSize) return; const feedbackCtx = feedbackCanvas.getContext('2d'); if (!feedbackCtx) return; feedbackCtx.clearRect(0, 0, feedbackCanvas.width, feedbackCanvas.height); feedbackCtx.fillStyle = 'red'; editDrawnPoints.forEach((point) => { feedbackCtx.beginPath(); feedbackCtx.arc(point.x, point.y, point.size, 0, Math.PI * 2); feedbackCtx.fill(); }); displayCtx.clearRect(0, 0, displayCanvas.width, displayCanvas.height); displayCtx.save(); displayCtx.globalAlpha = 0.5; displayCtx.drawImage(feedbackCanvas, 0, 0, displayCanvas.width, displayCanvas.height); displayCtx.restore(); }, [editDrawnPoints, editOriginalImageSize]); const getMousePos = (e: React.MouseEvent | React.TouchEvent) => { const canvas = canvasRef.current; if (!canvas) return null; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX; const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY; return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY }; }; const addPoint = (x: number, y: number) => { setEditDrawnPoints((prevPoints) => [...prevPoints, { x, y, size: editBrushSize[0] }]); setEditIsMaskSaved(false); setEditMaskPreviewUrl(null); }; const startDrawing = (e: React.MouseEvent | React.TouchEvent) => { e.preventDefault(); isDrawing.current = true; const currentPos = getMousePos(e); if (!currentPos) return; lastPos.current = currentPos; addPoint(currentPos.x, currentPos.y); }; const drawLine = (e: React.MouseEvent | React.TouchEvent) => { if (!isDrawing.current) return; e.preventDefault(); const currentPos = getMousePos(e); if (!currentPos || !lastPos.current) return; const dist = Math.hypot(currentPos.x - lastPos.current.x, currentPos.y - lastPos.current.y); const angle = Math.atan2(currentPos.y - lastPos.current.y, currentPos.x - lastPos.current.x); const step = Math.max(1, editBrushSize[0] / 4); for (let i = step; i < dist; i += step) { const x = lastPos.current.x + Math.cos(angle) * i; const y = lastPos.current.y + Math.sin(angle) * i; addPoint(x, y); } addPoint(currentPos.x, currentPos.y); lastPos.current = currentPos; }; const drawMaskStroke = (ctx: CanvasRenderingContext2D, x: number, y: number, size: number) => { ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(x, y, size, 0, Math.PI * 2); ctx.fill(); }; const stopDrawing = () => { isDrawing.current = false; lastPos.current = null; }; const handleClearMask = () => { setEditDrawnPoints([]); setEditGeneratedMaskFile(null); setEditIsMaskSaved(false); setEditMaskPreviewUrl(null); }; const generateAndSaveMask = () => { if (!editOriginalImageSize || editDrawnPoints.length === 0) { setEditGeneratedMaskFile(null); setEditIsMaskSaved(false); setEditMaskPreviewUrl(null); return; } const offscreenCanvas = document.createElement('canvas'); offscreenCanvas.width = editOriginalImageSize.width; offscreenCanvas.height = editOriginalImageSize.height; const offscreenCtx = offscreenCanvas.getContext('2d'); if (!offscreenCtx) return; offscreenCtx.fillStyle = '#000000'; offscreenCtx.fillRect(0, 0, offscreenCanvas.width, offscreenCanvas.height); offscreenCtx.globalCompositeOperation = 'destination-out'; editDrawnPoints.forEach((point) => { drawMaskStroke(offscreenCtx, point.x, point.y, point.size); }); try { const dataUrl = offscreenCanvas.toDataURL('image/png'); setEditMaskPreviewUrl(dataUrl); } catch (e) { console.error('生成遮罩预览 data URL 失败:', e); setEditMaskPreviewUrl(null); } offscreenCanvas.toBlob((blob) => { if (blob) { const maskFile = new File([blob], 'generated-mask.png', { type: 'image/png' }); setEditGeneratedMaskFile(maskFile); setEditIsMaskSaved(true); } else { console.error('生成遮罩 blob 失败。'); setEditIsMaskSaved(false); setEditMaskPreviewUrl(null); } }, 'image/png'); }; const handleImageFileChange = (event: React.ChangeEvent) => { if (event.target.files) { const newFiles = Array.from(event.target.files); const totalFiles = imageFiles.length + newFiles.length; if (totalFiles > maxImages) { alert(t('alert.maxImages', { count: maxImages })); const allowedNewFiles = newFiles.slice(0, maxImages - imageFiles.length); if (allowedNewFiles.length === 0) { event.target.value = ''; return; } newFiles.splice(allowedNewFiles.length); } setImageFiles((prevFiles) => [...prevFiles, ...newFiles]); const newFilePromises = newFiles.map((file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); }); }); Promise.all(newFilePromises) .then((newUrls) => { setSourceImagePreviewUrls((prevUrls) => [...prevUrls, ...newUrls]); }) .catch((error) => { console.error('读取新图片文件失败:', error); }); event.target.value = ''; } }; const handleRemoveImage = (indexToRemove: number) => { setImageFiles((prevFiles) => prevFiles.filter((_, index) => index !== indexToRemove)); setSourceImagePreviewUrls((prevUrls) => prevUrls.filter((_, index) => index !== indexToRemove)); }; const handleMaskFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file || !editOriginalImageSize) { event.target.value = ''; return; } if (file.type !== 'image/png') { alert(t('alert.maskInvalidType')); event.target.value = ''; return; } const reader = new FileReader(); const img = new window.Image(); const objectUrl = URL.createObjectURL(file); img.onload = () => { if (img.width !== editOriginalImageSize.width || img.height !== editOriginalImageSize.height) { alert( t('alert.maskDimensionMismatch', { actual: `${img.width}x${img.height}`, expected: `${editOriginalImageSize.width}x${editOriginalImageSize.height}` }) ); URL.revokeObjectURL(objectUrl); event.target.value = ''; return; } setEditGeneratedMaskFile(file); setEditIsMaskSaved(true); setEditDrawnPoints([]); reader.onloadend = () => { setEditMaskPreviewUrl(reader.result as string); URL.revokeObjectURL(objectUrl); }; reader.onerror = () => { console.error('读取遮罩预览文件失败。'); setEditMaskPreviewUrl(null); URL.revokeObjectURL(objectUrl); }; reader.readAsDataURL(file); event.target.value = ''; }; img.onerror = () => { alert(t('alert.maskLoadFailed')); URL.revokeObjectURL(objectUrl); event.target.value = ''; }; img.src = objectUrl; }; const handleSubmit = () => { if (imageFiles.length === 0) { alert(t('alert.editNoImage')); return; } if (imageFiles.length > maxImages) { alert(t('alert.maxImages', { count: maxImages })); return; } if (editDrawnPoints.length > 0 && !editGeneratedMaskFile && !editIsMaskSaved) { alert(t('alert.saveMaskBeforeSubmit')); return; } if (customSizeInvalid) { return; } if (partialImages < partialImagesRange.min || partialImages > partialImagesRange.max) { setPartialImages(clampIntegerToRange(partialImages, partialImagesRange) as PartialImagesCount); return; } if (editN[0] < upstreamProfile.editCount.min || editN[0] > upstreamProfile.editCount.max) { setEditN([clampIntegerToRange(editN[0], upstreamProfile.editCount)]); return; } const formData: EditingFormData = { prompt: editPrompt, n: editN[0], size: editSize, customWidth: editCustomWidth, customHeight: editCustomHeight, quality: editQuality, output_format: editOutputFormat, ...(showCompression ? { output_compression: editCompression[0] } : {}), moderation: editModeration, imageFiles: imageFiles, maskFile: editGeneratedMaskFile, model: editModel, image_backend: editImageBackend, streaming_strategy: editStreamingStrategy, responsesModel: editResponsesModel, thinking: editThinking, promptOptimization: editPromptOptimization, forceWeb: editForceWeb, enableParallelBatch: parallelBatchChecked }; onSubmit(formData); }; return (
{t('workbench.creationSheet')} {isPasswordRequiredByBackend && ( )}
{reuseContext && (

{t('reuse.editAppliedTitle')}

{reuseContext.sourceLabel}

{reuseContext.restoredFields.map((field) => ( {field} ))}

{reuseContext.promptPreview}

{t('reuse.editMutableHint')}

)}
{t('edit.referenceTitle')}

{t('edit.referenceHint', { count: maxImages })}

{t('edit.referenceCount', { count: imageFiles.length, max: maxImages })}
= maxImages} className='hidden' /> {!hasSourceImages && (
{t('edit.referenceRequiredNote')}
)} {hasSourceImages && !hasSourcePreviews && (
{t('edit.referencePreparing')}
)} {hasSourcePreviews && (
{sourceImagePreviewUrls.map((url, index) => (
{t('edit.sourcePreview',
))}
)}