import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { CloudUpload, Eye, Pencil, Plus, RotateCcw, Share2, Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { ResizableHandle, ResizablePanel, ResizablePanelGroup, type ResizablePanelGroupHandle, } from '@/components/ui/resizable' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { MediaSelector } from '@/components/widgets/mediaSelector/MediaSelector' import type { MediaTab } from '@/components/widgets/mediaSelector/MediaSelector' import { useT } from '@/lib/i18n' import { mediaContentToViewUrl } from '@/lib/media-url' import { getSegmentTrackPresentation } from '@/lib/multitrack-segment-style' import { computeSlotItems } from '@/lib/timeline-utils' import { applyCombinedTaskTexts, formatMultiTrackDurationTimecode, frameToSeconds, getSelectedTaskUserPrompt, getSelectedTaskUserPromptPatch, getMultiTrackTaskType, MULTITRACK_CONTINUITY_MODES, MULTITRACK_DEFAULT_CONTINUITY_MODE, MULTITRACK_DEFAULT_FRAME_RATE, MULTITRACK_DEFAULT_REF_IMAGE_SIZE, MULTITRACK_REF_IMAGE_SIZES, MULTITRACK_TASK_MODES, parseMultiTrackDurationTimecode, segmentDuration, } from '@/lib/multitrack-utils' import { cn } from '@/lib/utils' import { createTaskImage, activeTaskImages, canEnableSharedTaskImage, MAX_TASK_IMAGES, sharedTaskImageUpdates, splitSelectedTaskMedia, taskImageIdentity, taskImageReferenceIndex, taskImageSlotNumber, taskImagesFromContent, uploadTaskImageFile, } from '@/lib/task-image-utils' import { invalidateMediaListCache } from '@/stores/media-list-store' import type { MultiTrack, MultiTrackContinuityMode, MultiTrackRefImageSize, MultiTrackSegment, MultiTrackSegmentContent, MultiTrackSourceType, MultiTrackTaskImage, MultiTrackTaskMode, MultiTrackUserPromptVariant, } from '@/types/multitrack' import { PanoramaImagePreview } from '@/components/widgets/panorama/PanoramaImagePreview' import { PromptContentEditor, type PromptReferenceResource } from './PromptContentEditor' type PromptTab = 'user' | 'system' type EditMode = 'individual' | 'combined' const TASK_IMAGE_PANEL_MIN_SIZE = 180 const TASK_IMAGE_PANEL_MAX_SIZE = '55%' const TASK_PROMPT_PANEL_MIN_SIZE = 280 const TASK_EDITOR_PANEL_GAP = 16 const TASK_IMAGES_PANEL_ID = 'task-images-resizable-panel' const TASK_PROMPT_PANEL_ID = 'task-prompt-resizable-panel' interface TaskEditorPanelDrag { pointerId: number startClientX: number startImageSize: number groupWidth: number } interface TrackSegmentContentUpdate { segmentId: string patch: Partial } interface TaskSegmentEditorProps { segment: MultiTrackSegment node?: unknown app?: unknown trackSegments?: MultiTrackSegment[] selectedSegments?: MultiTrackSegment[] videoSegments?: MultiTrackSegment[] mediaTracks?: MultiTrack[] frameRate?: number totalFrames?: number imageIndexOffset?: number format?: string onContentChange: (patch: Partial) => void onTrackSegmentsContentChange?: (updates: TrackSegmentContentUpdate[]) => void onTrackSegmentsChange?: (segments: MultiTrackSegment[]) => void onDurationChange?: (duration: number) => void onOpenImagePreview?: (imageId: string) => void } interface SystemPromptResponse { items?: SystemPromptOption[] } interface SystemPromptOption { task_type?: string system_prompt?: string format?: string modes?: MultiTrackTaskMode[] } let cachedSystemPromptOptions: SystemPromptOption[] | undefined let systemPromptOptionsRequest: Promise | null = null function taskImages(segment: MultiTrackSegment): MultiTrackTaskImage[] { return taskImagesFromContent(segment.content.images) } function moveImage(images: MultiTrackTaskImage[], sourceId: string, targetId: string): MultiTrackTaskImage[] { const sourceIndex = images.findIndex((item) => item.id === sourceId) const targetIndex = images.findIndex((item) => item.id === targetId) if (sourceIndex < 0 || targetIndex < 0 || sourceIndex === targetIndex) return images const nextImages = [...images] const [moved] = nextImages.splice(sourceIndex, 1) nextImages.splice(targetIndex, 0, moved) return nextImages } function imageDisplayName(image: MultiTrackTaskImage): string { return image.file_name ?? image.file_path ?? image.local_path ?? image.url ?? image.slot_name ?? image.id } function imageSelectorValue(image: MultiTrackTaskImage | undefined): string { if (!image) return '' if (image.slot_name) return `__slot__:${image.slot_name}` return image.file_path ?? image.local_path ?? image.url ?? '' } function imageSelectorTab(image: MultiTrackTaskImage | undefined): MediaTab { if (image?.source_type === 'output') return 'outputs' if (image?.source_type === 'local') return 'local' if (image?.source_type === 'url') return 'url' if (image?.source_type === 'slot') return 'slot' return 'inputs' } function selectedImageSource( filePath: string, source?: 'input' | 'output' | 'local', ): MultiTrackSourceType { if (source) return source if (filePath.startsWith('__slot__:')) return 'slot' if (filePath.startsWith('http://') || filePath.startsWith('https://')) return 'url' return 'input' } function hasVideoInRange(segment: MultiTrackSegment, videoSegments: MultiTrackSegment[]): boolean { return videoSegments.some((videoSegment) => ( videoSegment.content.media_type === 'video' && videoSegment.start_frame < segment.end_frame && videoSegment.end_frame > segment.start_frame )) } function getDefaultSystemPromptForSegment( segment: MultiTrackSegment, options: SystemPromptOption[], videoSegments: MultiTrackSegment[], format?: string, ): string { const images = activeTaskImages(segment.content.images) const taskType = getMultiTrackTaskType( segment.content.task_mode ?? 'default', images.length, hasVideoInRange(segment, videoSegments), ) const mode = segment.content.task_mode ?? 'default' const promptTaskType = taskType === 'fl2v' || taskType === 'fmlf2v' ? 'i2v' : taskType const formatMatch = options.find((option) => ( option.format === format && (option.modes?.includes(mode) || option.task_type === taskType) )) return formatMatch?.system_prompt ?? options.find((option) => option.task_type === promptTaskType && option.format === undefined)?.system_prompt ?? '' } async function loadSystemPromptOptions(): Promise { if (cachedSystemPromptOptions) return cachedSystemPromptOptions if (!systemPromptOptionsRequest) { systemPromptOptionsRequest = (async () => { const response = await fetch('/easy-media/prompt/system-prompts') if (!response.ok) { throw new Error(`Failed to load system prompt options: ${response.status}`) } const result = await response.json() as SystemPromptResponse const options = Array.isArray(result.items) ? result.items : [] cachedSystemPromptOptions = options return options })().catch((error: unknown) => { systemPromptOptionsRequest = null throw error }) } return systemPromptOptionsRequest } export function TaskSegmentEditor({ segment, node, app, trackSegments, selectedSegments, videoSegments = [], mediaTracks = [], frameRate = MULTITRACK_DEFAULT_FRAME_RATE, totalFrames, imageIndexOffset = 0, format, onContentChange, onTrackSegmentsContentChange, onTrackSegmentsChange, onDurationChange, onOpenImagePreview, }: Readonly) { const t = useT() const draggedImageIdRef = useRef(null) const [promptTab, setPromptTab] = useState('user') const [editMode, setEditMode] = useState('individual') const [mediaSelectorOpen, setMediaSelectorOpen] = useState(false) const [reselectImageId, setReselectImageId] = useState(null) const [isImageDragOver, setIsImageDragOver] = useState(false) const [applyPromptToAllOpen, setApplyPromptToAllOpen] = useState(false) const [systemPromptOptions, setSystemPromptOptions] = useState(cachedSystemPromptOptions ?? null) const [systemPromptLoading, setSystemPromptLoading] = useState(false) const [isDurationEditing, setIsDurationEditing] = useState(false) const duration = frameToSeconds(segmentDuration(segment), frameRate) const formattedDuration = formatMultiTrackDurationTimecode(duration, frameRate) const [durationInput, setDurationInput] = useState(formattedDuration) const images = taskImages(segment) const slotItems = useMemo( () => computeSlotItems(node, app, 'image'), [node, app, mediaSelectorOpen], ) const reselectImage = images.find((image) => image.id === reselectImageId) const mode = segment.content.task_mode ?? 'default' const continuityMode = segment.content.continuity_mode ?? MULTITRACK_DEFAULT_CONTINUITY_MODE const refImageSize = segment.content.ref_image_size ?? MULTITRACK_DEFAULT_REF_IMAGE_SIZE const editableSegments = useMemo(() => ( trackSegments && trackSegments.length > 0 ? trackSegments : [segment] ), [segment, trackSegments]) const combinedPromptValue = editableSegments.map((item) => ( getSelectedTaskUserPrompt(item.content) )).join('|') const [combinedPromptInput, setCombinedPromptInput] = useState(combinedPromptValue) const taskIndex = Math.max(0, editableSegments.findIndex((item) => item.id === segment.id)) const firstTaskSegmentId = editableSegments.reduce((first, item) => ( item.start_frame < first.start_frame ? item : first )).id const hasSelectedContinuityTargets = selectedSegments?.some((item) => item.id !== firstTaskSegmentId) === true const promptVariant: MultiTrackUserPromptVariant = segment.content.user_prompt_variant === 'b' ? 'b' : 'a' const promptValue = editMode === 'combined' ? combinedPromptInput : getSelectedTaskUserPrompt(segment.content) const usesMiniMaxPromptPlaceholder = format === 'MiniMax' const promptPlaceholder = usesMiniMaxPromptPlaceholder ? t('multitrack.minimaxPromptPlaceholder') : t('multitrack.promptPlaceholder') const segmentHasVideoInRange = hasVideoInRange(segment, videoSegments) const systemPromptDefault = getDefaultSystemPromptForSegment(segment, systemPromptOptions ?? [], videoSegments, format) const systemPromptValue = editMode === 'combined' ? editableSegments.map((item) => ( item.content.system_prompt ?? getDefaultSystemPromptForSegment(item, systemPromptOptions ?? [], videoSegments, format) )).join('|') : segment.content.system_prompt || systemPromptDefault const promptResources = useMemo(() => { const imageResources = activeTaskImages(images).map((image, index) => ({ id: `image:${image.id}`, type: 'image' as const, index: index + 1, label: t('multitrack.referencePicture', { n: index + 1 }), detail: imageDisplayName(image), token: `@${t('multitrack.referencePicture', { n: index + 1 })}`, color: getSegmentTrackPresentation('task').backgroundColor, thumbnailUrl: mediaContentToViewUrl({ source_type: image.source_type ?? 'input', file_path: image.file_path, local_path: image.local_path, url: image.url, slot_name: image.slot_name, }) ?? undefined, })) const videoTracks = mediaTracks.filter((track) => ( track.type === 'video' && track.segments.some((item) => item.content.media_type === 'video') )) const audioTracks = mediaTracks.filter((track) => ( track.type === 'audio' && track.segments.some((item) => item.content.media_type === 'audio') )) const videoAudioResources = videoTracks.map((track, index) => { const resourceIndex = index + 1 const label = t('multitrack.referenceAudioItem', { n: resourceIndex }) return { id: `video-audio:${track.id}`, type: 'audio' as const, index: resourceIndex, label, detail: track.name, token: `@${label}`, color: getSegmentTrackPresentation('video').waveformColor ?? getSegmentTrackPresentation('video').backgroundColor, } }) const audioResources = audioTracks.map((track, index) => { const resourceIndex = videoTracks.length + index + 1 const label = t('multitrack.referenceAudioItem', { n: resourceIndex }) return { id: `audio:${track.id}`, type: 'audio' as const, index: resourceIndex, label, detail: track.name, token: `@${label}`, color: getSegmentTrackPresentation('audio').waveformColor ?? getSegmentTrackPresentation('audio').backgroundColor, } }) const videoResources = videoTracks.map((track, index) => { const resourceIndex = index + 1 const label = t('multitrack.referenceVideoItem', { n: resourceIndex }) return { id: `video:${track.id}`, type: 'video' as const, index: resourceIndex, label, detail: track.name, token: `@${label}`, color: getSegmentTrackPresentation('video').waveformColor ?? getSegmentTrackPresentation('video').backgroundColor, } }) return [...imageResources, ...videoAudioResources, ...audioResources, ...videoResources] }, [images, mediaTracks, t]) useEffect(() => { setDurationInput(formattedDuration) }, [formattedDuration]) useEffect(() => { setIsDurationEditing(false) }, [segment.id]) useEffect(() => { setCombinedPromptInput((current) => current === combinedPromptValue ? current : combinedPromptValue) }, [combinedPromptValue]) function commitDuration() { const nextDuration = parseMultiTrackDurationTimecode(durationInput, frameRate) if (nextDuration === null) { setDurationInput(formattedDuration) setIsDurationEditing(false) return } setDurationInput(formatMultiTrackDurationTimecode(nextDuration, frameRate)) setIsDurationEditing(false) if (nextDuration !== duration) onDurationChange?.(nextDuration) } useEffect(() => { if (promptTab !== 'system') return if (systemPromptOptions !== null) return let cancelled = false setSystemPromptLoading(true) loadSystemPromptOptions() .then((options) => { if (!cancelled) setSystemPromptOptions(options) }) .catch((error: unknown) => { console.error('[TaskSegmentEditor] failed to load system prompt options:', error) if (!cancelled) setSystemPromptOptions([]) }) .finally(() => { if (!cancelled) setSystemPromptLoading(false) }) return () => { cancelled = true } }, [promptTab, systemPromptOptions]) function hasDraggedImageFile(dataTransfer: DataTransfer): boolean { const files = Array.from(dataTransfer.files) if (files.some((file) => file.type.startsWith('image/'))) return true return Array.from(dataTransfer.items ?? []).some((item) => item.kind === 'file' && item.type.startsWith('image/')) } function handleImageDragEnter(event: React.DragEvent) { if (!event.dataTransfer || !hasDraggedImageFile(event.dataTransfer)) return event.preventDefault() setIsImageDragOver(true) } function handleImageDragOver(event: React.DragEvent) { if (!event.dataTransfer || !hasDraggedImageFile(event.dataTransfer)) return event.preventDefault() setIsImageDragOver(true) } function handleImageDragLeave(event: React.DragEvent) { const relatedTarget = event.relatedTarget if (relatedTarget instanceof Node && event.currentTarget.contains(relatedTarget)) return setIsImageDragOver(false) } async function handleDrop(event: React.DragEvent) { event.preventDefault() setIsImageDragOver(false) if (!event.dataTransfer) return const remainingSlots = MAX_TASK_IMAGES - images.length if (remainingSlots <= 0) return const files = Array.from(event.dataTransfer.files) .filter((file) => file.type.startsWith('image/')) .slice(0, remainingSlots) if (files.length === 0) return const results = await Promise.allSettled(files.map((file) => uploadTaskImageFile(file))) const uploaded = results.flatMap((result) => { if (result.status === 'fulfilled') return [result.value] console.error('[TaskSegmentEditor] failed to upload task image:', result.reason) return [] }) if (uploaded.length > 0) { invalidateMediaListCache('inputs') onContentChange({ images: [...images, ...uploaded] }) } } function handleSelectedMedia(filePath: string, source?: 'input' | 'output' | 'local') { const remainingSlots = reselectImageId ? 1 : MAX_TASK_IMAGES - images.length if (remainingSlots <= 0) return const selectedPaths = splitSelectedTaskMedia(filePath).slice(0, remainingSlots) const sourceType = selectedImageSource(filePath, source) const nextImages = selectedPaths.map((path) => createTaskImage(path, sourceType)) if (nextImages.length === 0) return if (reselectImageId) { const replacement = { ...nextImages[0], id: reselectImageId } if (reselectImage?.shared_reference === true && onTrackSegmentsContentChange) { const oldIdentity = taskImageIdentity(reselectImage) const unsharedUpdates = new Map( sharedTaskImageUpdates(editableSegments, reselectImage, false) .map((update) => [update.segmentId, update.images]), ) const replacementSourceSegments = editableSegments.map((item) => ({ ...item, content: { ...item.content, images: item.id === segment.id ? [replacement, ...(unsharedUpdates.get(item.id) ?? []).filter((image) => ( taskImageIdentity(image) !== oldIdentity ))] : unsharedUpdates.get(item.id) ?? taskImages(item), }, })) onTrackSegmentsContentChange( sharedTaskImageUpdates(replacementSourceSegments, replacement, true).map((update) => ({ segmentId: update.segmentId, patch: { images: update.images }, })), ) setReselectImageId(null) setMediaSelectorOpen(false) return } onContentChange({ images: images.map((image) => image.id === reselectImageId ? replacement : image), }) } else { onContentChange({ images: [...images, ...nextImages] }) } setReselectImageId(null) setMediaSelectorOpen(false) } function commitCombinedPrompt(value: string) { const parts = value === '' ? [''] : value.split(/[||]/) if (onTrackSegmentsChange) { const trackEndFrame = editableSegments.reduce((max, item) => Math.max(max, item.end_frame), 0) onTrackSegmentsChange(applyCombinedTaskTexts( parts, editableSegments, totalFrames ?? trackEndFrame, segment.color, )) return } onTrackSegmentsContentChange?.(editableSegments.map((item, index) => ({ segmentId: item.id, patch: getSelectedTaskUserPromptPatch(item.content, parts[index] ?? ''), }))) } function handlePromptChange(value: string) { if (editMode === 'combined') { setCombinedPromptInput(value) commitCombinedPrompt(value) return } onContentChange(getSelectedTaskUserPromptPatch(segment.content, value)) } function handleSystemPromptChange(value: string) { if (editMode === 'combined') { const parts = value.split('|') onTrackSegmentsContentChange?.(editableSegments.map((item, index) => ({ segmentId: item.id, patch: { system_prompt: parts[index] ?? '' }, }))) return } onContentChange({ system_prompt: value }) } function handleApplyPromptToAll() { const patch = { user_prompt: promptValue, user_prompt_b: promptValue, } if (onTrackSegmentsContentChange) { onTrackSegmentsContentChange(editableSegments.map((item) => ({ segmentId: item.id, patch, }))) } else { onContentChange(patch) } setApplyPromptToAllOpen(false) } function handleDropdownContentChange(patch: Partial) { if (!selectedSegments || selectedSegments.length <= 1 || !onTrackSegmentsContentChange) { onContentChange(patch) return } onTrackSegmentsContentChange(selectedSegments.map((item) => ({ segmentId: item.id, patch: patch.continuity_mode !== undefined && item.id === firstTaskSegmentId ? { ...patch, continuity_mode: MULTITRACK_DEFAULT_CONTINUITY_MODE } : patch, }))) } function handleDeleteImage(imageId: string) { const image = images.find((item) => item.id === imageId) if (image?.shared_reference === true && onTrackSegmentsContentChange) { const identity = taskImageIdentity(image) const updates = sharedTaskImageUpdates(editableSegments, image, false).map((update) => ({ segmentId: update.segmentId, patch: { images: update.segmentId === segment.id ? update.images.filter((item) => taskImageIdentity(item) !== identity) : update.images, }, })) onTrackSegmentsContentChange(updates) return } onContentChange({ images: images.filter((image) => image.id !== imageId) }) } function handleSharedImageChange(image: MultiTrackTaskImage, enabled: boolean) { if (enabled && !canEnableSharedTaskImage(editableSegments, image)) return const updates = sharedTaskImageUpdates(editableSegments, image, enabled) if (onTrackSegmentsContentChange) { onTrackSegmentsContentChange(updates.map((update) => ({ segmentId: update.segmentId, patch: { images: update.images }, }))) return } const current = updates.find((update) => update.segmentId === segment.id) if (current) onContentChange({ images: current.images }) } function handleMutedImageChange(imageId: string, muted: boolean) { onContentChange({ images: images.map((image) => image.id === imageId ? { ...image, muted } : image), }) } const imageGridColumns = images.length > 0 && images.length < 4 ? 'grid-cols-2' : 'grid-cols-3' const imagePickerSurfaceClass = isImageDragOver ? 'border-primary bg-accent/20' : 'border-border bg-muted/20' const containerRef = useRef(null) const editorContentRef = useRef(null) const imageGridRef = useRef(null) const imageDropZoneRef = useRef(null) const panelGroupRef = useRef(null) const panelDragRef = useRef(null) const [showEditModeToggle, setShowEditModeToggle] = useState(true) const [imagePanelDefaultSize, setImagePanelDefaultSize] = useState('40%') const [imageCellWidth, setImageCellWidth] = useState(120) useLayoutEffect(() => { const content = editorContentRef.current if (!content) return const styles = globalThis.getComputedStyle(content) const innerWidth = content.clientWidth - Number.parseFloat(styles.paddingLeft) - Number.parseFloat(styles.paddingRight) const innerHeight = content.clientHeight - Number.parseFloat(styles.paddingTop) - Number.parseFloat(styles.paddingBottom) if (innerWidth <= 0 || innerHeight <= 0) return const maximumImageWidth = Math.min( innerWidth * 0.55, innerWidth - TASK_PROMPT_PANEL_MIN_SIZE - TASK_EDITOR_PANEL_GAP, ) setImagePanelDefaultSize(Math.max( TASK_IMAGE_PANEL_MIN_SIZE, Math.min(innerHeight, maximumImageWidth), )) }, []) useLayoutEffect(() => { const target = imageDropZoneRef.current ?? imageGridRef.current if (!target) return const update = () => { const width = target.clientWidth if (width <= 0) return setImageCellWidth((prev) => (Math.abs(prev - width) > 1 ? width : prev)) } update() const observer = new ResizeObserver(update) observer.observe(target) return () => observer.disconnect() }, [images.length]) const actionSharedSize = Math.round(Math.max(14, Math.min(imageCellWidth * 0.07, 28))) const actionNormalSize = Math.round(Math.max(14, Math.min(imageCellWidth * 0.08, 32))) const svgIconSize = Math.round(Math.max(10, Math.min(imageCellWidth * 0.05, 20))) const textIconSize = Math.round(Math.max(10, Math.min(imageCellWidth * 0.06, 18))) useEffect(() => { const container = containerRef.current if (!container) return const observer = new ResizeObserver((entries) => { const width = entries[0]?.contentRect.width ?? container.clientWidth setShowEditModeToggle(width >= 600) }) observer.observe(container) // Initial check setShowEditModeToggle(container.clientWidth >= 600) return () => observer.disconnect() }, []) function handlePanelResizePointerDown(event: React.PointerEvent) { if (event.button !== 0) return event.preventDefault() event.stopPropagation() const layout = panelGroupRef.current?.getLayout() const groupWidth = event.currentTarget.parentElement?.getBoundingClientRect().width ?? 0 const startImageSize = layout?.[TASK_IMAGES_PANEL_ID] if (groupWidth <= 0 || startImageSize === undefined) return panelDragRef.current = { pointerId: event.pointerId, startClientX: event.clientX, startImageSize, groupWidth, } event.currentTarget.setPointerCapture?.(event.pointerId) } function handlePanelResizePointerMove(event: React.PointerEvent) { const drag = panelDragRef.current if (!drag || drag.pointerId !== event.pointerId) return event.preventDefault() event.stopPropagation() const imageSize = drag.startImageSize + ((event.clientX - drag.startClientX) / drag.groupWidth) * 100 panelGroupRef.current?.setLayout({ [TASK_IMAGES_PANEL_ID]: imageSize, [TASK_PROMPT_PANEL_ID]: 100 - imageSize, }) } function finishPanelResize(event: React.PointerEvent) { if (panelDragRef.current?.pointerId !== event.pointerId) return event.preventDefault() event.stopPropagation() panelDragRef.current = null if (event.currentTarget.hasPointerCapture?.(event.pointerId)) { event.currentTarget.releasePointerCapture?.(event.pointerId) } } return (
{editMode === 'individual' && (
{ setMediaSelectorOpen(open) if (!open) setReselectImageId(null) }} > {images.length === 0 ? (
setReselectImageId(null)} onKeyDown={(event) => { if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() setMediaSelectorOpen(true) }} > {t('multitrack.selectImage')} {t('multitrack.imageDropHint')}
) : (
{images.map((image, index) => { const imageUrl = mediaContentToViewUrl({ source_type: image.source_type ?? 'input', file_path: image.file_path, local_path: image.local_path, url: image.url, slot_name: image.slot_name, }) const canShareImage = image.shared_reference === true || canEnableSharedTaskImage(editableSegments, image) const actionSize = image.shared_reference ? actionSharedSize : actionNormalSize const iconSvgStyle = { width: `${svgIconSize}px`, height: `${svgIconSize}px` } const imageReferenceIndex = taskImageReferenceIndex(images, image.id, imageIndexOffset) return (
{ setReselectImageId(image.id) setMediaSelectorOpen(true) }} onKeyDown={(event) => { if (event.target !== event.currentTarget) return if (event.key !== 'Enter' && event.key !== ' ') return event.preventDefault() setReselectImageId(image.id) setMediaSelectorOpen(true) }} onDragStart={() => { draggedImageIdRef.current = image.id }} onDragOver={(event) => event.preventDefault()} onDrop={() => { const sourceId = draggedImageIdRef.current draggedImageIdRef.current = null if (!sourceId) return onContentChange({ images: moveImage(images, sourceId, image.id) }) }} > {imageUrl && image.panorama_view ? ( ) : imageUrl ? ( {imageDisplayName(image)} ) : (
{image.source_type === 'slot' ? t('mediaSelector.slotImage', { n: taskImageSlotNumber(image, index) }) : imageDisplayName(image)}
)} ) })} {images.length < MAX_TASK_IMAGES && ( )}
)}
)} {editMode === 'individual' ? ( { panelDragRef.current = null }} /> ) : null}
{editMode === 'individual' && (
{promptTab === 'user' ? (
onContentChange({ user_prompt_variant: value as MultiTrackUserPromptVariant, })} > A B
{t('multitrack.userPromptVariantTooltip')}
) : Boolean(segment.content.system_prompt) && ( {t('multitrack.resetSystemPrompt')} )}
)} {editMode === 'combined' ? (

{t('maintainTrack.combinedHint')}

) : promptTab === 'user' ? (
) : (
)}
setEditMode(value as EditMode)} className={showEditModeToggle ? undefined : 'hidden'}> {t('multitrack.individualEdit')} {t('multitrack.combinedEdit')}
{isDurationEditing ? ( setDurationInput(event.currentTarget.value)} onBlur={commitDuration} onKeyDown={(event) => { if (event.key !== 'Enter') return event.preventDefault() commitDuration() }} /> ) : (
{t('multitrack.taskNumber', { n: taskIndex + 1 })} {formattedDuration}
{onDurationChange && ( )}
)}
{format === 'MiniMax' && (taskIndex > 0 || hasSelectedContinuityTargets) && ( )} {format === 'MiniMax' && ['ref','edit'].includes(mode) && ( )}
{t('multitrack.applyPromptToAllTitle')} {t('multitrack.applyPromptToAllDescription')}
) }