import React, { useState, useEffect, useRef } from 'react'; import { FolderOpen, Plus, Image, Video, Trash2, Edit2, Check, X, Download, Clock, Search, Grid, List, Loader2, AlertCircle, Play, RefreshCw, Layers, CheckSquare, Square, Upload, Wand2, Database, ArrowRightLeft, Maximize2, Minimize2, Copy } from 'lucide-react'; import useProjects from '../hooks/useProjects'; import useCustomPresets from '../hooks/useCustomPresets'; import VideoPlayer from './VideoPlayer'; import StoryboardEditor from './StoryboardEditor'; const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInImageGen }) => { // Helper function to download items (handles both base64 and URL data) const downloadItem = async (item) => { try { const isUrl = item.data && (item.data.startsWith('http') || item.data.startsWith('/')); let blob; let filename = `lux-studio-${item.id}.${(item.mimeType || 'image/png').split('/')[1]}`; if (isUrl) { // For URLs, fetch the file and download as blob const response = await fetch(item.data); blob = await response.blob(); } else { // For base64 data, convert to Blob const base64Data = item.data.startsWith('data:') ? item.data.split(',')[1] : item.data; const byteCharacters = atob(base64Data); const byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) { byteNumbers[i] = byteCharacters.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); blob = new Blob([byteArray], { type: item.mimeType || 'image/png' }); } // Create download link const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } catch (err) { console.error('Download failed:', err); // Fallback to data URL method const link = document.createElement('a'); link.href = item.data.startsWith('data:') ? item.data : `data:${item.mimeType};base64,${item.data}`; link.download = `lux-studio-${item.id}.${(item.mimeType || 'image/png').split('/')[1]}`; link.click(); } }; const { projects, isLoading, isReady, error: dbError, createProject, deleteProject, renameProject, getProjectItems, removeItemFromProject, moveItemToProject, addItemToProject, exportProject, createStoryboard, getStoryboards, getStoryboard, updateStoryboard, deleteStoryboard } = useProjects(); const { presets: customPresets } = useCustomPresets(); // Resolve preset display name (handles both built-in and custom:uuid values) const getPresetDisplayName = (applicationValue) => { if (!applicationValue) return null; if (applicationValue.startsWith('custom:')) { const preset = customPresets.find(p => p.value === applicationValue); return preset ? preset.name : 'Custom Preset'; } return applicationValue; }; const [selectedProject, setSelectedProject] = useState(null); // Sync selectedProject with activeProjectId when projects load useEffect(() => { if (activeProjectId && projects.length > 0 && !selectedProject) { const project = projects.find(p => p.id === activeProjectId); if (project) { setSelectedProject(project); } } }, [activeProjectId, projects]); // Handle project selection - notify parent const handleSelectProject = (project) => { setSelectedProject(project); if (onProjectSelect) { onProjectSelect(project.id, project.name); } }; const [selectedProjectItems, setSelectedProjectItems] = useState([]); const [isCreating, setIsCreating] = useState(false); const [newProjectName, setNewProjectName] = useState(''); const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(''); const [viewMode, setViewMode] = useState('grid'); const [searchQuery, setSearchQuery] = useState(''); const [error, setError] = useState(''); const [previewItem, setPreviewItem] = useState(null); const [movingItemId, setMovingItemId] = useState(null); const [previewExpanded, setPreviewExpanded] = useState(false); // Sub-tab state: 'library' or 'storyboards' const [activeSubTab, setActiveSubTab] = useState('library'); // Selection mode for creating storyboards const [isSelecting, setIsSelecting] = useState(false); const [selectedImageIds, setSelectedImageIds] = useState([]); // Storyboards state const [storyboards, setStoryboards] = useState([]); const [isCreatingStoryboard, setIsCreatingStoryboard] = useState(false); const [newStoryboardName, setNewStoryboardName] = useState(''); const [activeStoryboard, setActiveStoryboard] = useState(null); const [editingStoryboardId, setEditingStoryboardId] = useState(null); // For editing existing storyboard frames // File upload ref const fileInputRef = useRef(null); const [isUploading, setIsUploading] = useState(false); // Import from backend state const [copiedPrompt, setCopiedPrompt] = useState(false); const [showImportModal, setShowImportModal] = useState(false); const [availableSessions, setAvailableSessions] = useState([]); const [selectedFiles, setSelectedFiles] = useState([]); const [importing, setImporting] = useState(false); const [importProgress, setImportProgress] = useState({ current: 0, total: 0 }); // Load items and storyboards when project is selected useEffect(() => { if (selectedProject) { loadProjectItems(selectedProject.id); loadStoryboards(selectedProject.id); // Clear selection and active storyboard when switching projects setSelectedImageIds([]); setIsSelecting(false); setActiveStoryboard(null); } }, [selectedProject]); const loadProjectItems = async (projectId) => { try { const items = await getProjectItems(projectId); setSelectedProjectItems(items); } catch (err) { console.error('Failed to load project items:', err); setSelectedProjectItems([]); } }; const loadStoryboards = async (projectId) => { try { const boards = await getStoryboards(projectId); setStoryboards(boards); } catch (err) { console.error('Failed to load storyboards:', err); setStoryboards([]); } }; // Handle image upload const handleImageUpload = async (event) => { const files = event.target.files; if (!files || files.length === 0 || !selectedProject) return; setIsUploading(true); setError(''); try { for (const file of files) { // Only accept images if (!file.type.startsWith('image/')) { continue; } // Read file as base64 const base64 = await new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { const result = reader.result; // Remove data URL prefix to get pure base64 const base64Data = result.split(',')[1]; resolve(base64Data); }; reader.onerror = reject; reader.readAsDataURL(file); }); // Add to project await addItemToProject(selectedProject.id, { type: 'image', prompt: `Uploaded: ${file.name}`, data: base64, mimeType: file.type, thumbnail: base64 }); } // Reload items await loadProjectItems(selectedProject.id); } catch (err) { setError('Failed to upload image: ' + err.message); console.error('Upload error:', err); } finally { setIsUploading(false); // Reset file input if (fileInputRef.current) { fileInputRef.current.value = ''; } } }; // API URL helper for import endpoints const getApiUrl = (endpoint) => { if (import.meta.env.DEV) { return `/api/${endpoint}`; } const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015'; return `${apiUrl}/${endpoint}`; }; // Fetch available backend session files const fetchAvailableSessions = async () => { try { const response = await fetch(getApiUrl('list_session_files.php')); const data = await response.json(); if (data.success) { setAvailableSessions(data.sessions); return data.sessions; } else { throw new Error(data.error || 'Failed to fetch sessions'); } } catch (err) { console.error('Error fetching sessions:', err); setError('Failed to load backend files: ' + err.message); return []; } }; // Handle opening import modal const handleOpenImportModal = async () => { if (!selectedProject) { setError('Please select a project first'); return; } setShowImportModal(true); setSelectedFiles([]); await fetchAvailableSessions(); }; // Toggle file selection const toggleFileSelection = (sessionId, fileType, filename) => { const fileKey = `${sessionId}:${fileType}:${filename}`; setSelectedFiles(prev => { if (prev.includes(fileKey)) { return prev.filter(f => f !== fileKey); } else { return [...prev, fileKey]; } }); }; // Import selected files const handleImportFiles = async () => { if (selectedFiles.length === 0 || !selectedProject) return; setImporting(true); setError(''); setImportProgress({ current: 0, total: selectedFiles.length }); try { for (let i = 0; i < selectedFiles.length; i++) { const fileKey = selectedFiles[i]; const [sessionId, fileType, filename] = fileKey.split(':'); setImportProgress({ current: i + 1, total: selectedFiles.length }); // Fetch file from backend const response = await fetch( getApiUrl(`get_session_file.php?session_id=${encodeURIComponent(sessionId)}&file_type=${fileType}&filename=${encodeURIComponent(filename)}`) ); const data = await response.json(); if (!data.success) { console.error(`Failed to fetch ${filename}:`, data.error); continue; } // Add to project if (fileType === 'image') { await addItemToProject(selectedProject.id, { type: 'image', prompt: `Imported: ${filename}`, settings: {}, data: data.data, // Base64 mimeType: data.mime_type, thumbnail: data.data }); } else if (fileType === 'video') { // Skip videos for now - they need to be in generated_videos folder // TODO: Implement proper video import (copy to generated_videos or create session streaming endpoint) console.warn(`Skipping video import for ${filename} - not yet supported`); continue; } } // Reload items await loadProjectItems(selectedProject.id); // Close modal setShowImportModal(false); setSelectedFiles([]); setAvailableSessions([]); alert(`Successfully imported ${selectedFiles.length} file(s)`); } catch (err) { setError('Import failed: ' + err.message); console.error('Import error:', err); } finally { setImporting(false); setImportProgress({ current: 0, total: 0 }); } }; // Open a storyboard for editing const handleOpenStoryboard = async (storyboardId) => { try { const board = await getStoryboard(storyboardId); setActiveStoryboard(board); } catch (err) { console.error('Failed to load storyboard:', err); setError(err.message); } }; // Handle storyboard update from editor const handleUpdateStoryboard = async (storyboardId, updates) => { try { const updated = await updateStoryboard(storyboardId, updates); setActiveStoryboard(updated); await loadStoryboards(selectedProject.id); return updated; } catch (err) { setError(err.message); throw err; } }; // Handle storyboard deletion from editor const handleDeleteStoryboard = async (storyboardId) => { try { await deleteStoryboard(storyboardId); setActiveStoryboard(null); await loadStoryboards(selectedProject.id); } catch (err) { setError(err.message); throw err; } }; // Handle generate video from storyboard frame const handleGenerateVideoFromFrame = (videoData) => { if (onRerunVideo) { onRerunVideo(videoData); } }; // Toggle image selection const toggleImageSelection = (imageId) => { setSelectedImageIds(prev => prev.includes(imageId) ? prev.filter(id => id !== imageId) : [...prev, imageId] ); }; // Create storyboard from selected images const handleCreateStoryboard = async () => { if (!newStoryboardName.trim() || selectedImageIds.length === 0) return; try { setError(''); await createStoryboard(selectedProject.id, newStoryboardName.trim(), selectedImageIds); setNewStoryboardName(''); setIsCreatingStoryboard(false); setSelectedImageIds([]); setIsSelecting(false); await loadStoryboards(selectedProject.id); setActiveSubTab('storyboards'); } catch (err) { setError(err.message); } }; // Edit storyboard frames - go to library with current frames pre-selected const handleEditStoryboardFrames = (storyboardId, currentFrameIds) => { setEditingStoryboardId(storyboardId); setSelectedImageIds(currentFrameIds); setIsSelecting(true); setActiveSubTab('library'); setActiveStoryboard(null); }; // Update storyboard with new frame selection const handleUpdateStoryboardFrames = async () => { if (!editingStoryboardId || selectedImageIds.length === 0) return; try { setError(''); // Get current storyboard to preserve annotations where possible const currentStoryboard = await getStoryboard(editingStoryboardId); const existingFrames = currentStoryboard?.frames || []; // Build new frames array, preserving annotations for existing frames const newFrames = selectedImageIds.map((imageId, index) => { const existingFrame = existingFrames.find(f => f.imageId === imageId); return { imageId, order: index, annotation: existingFrame?.annotation || '' }; }); await updateStoryboard(editingStoryboardId, { frames: newFrames }); // Reload and go back to the storyboard await loadStoryboards(selectedProject.id); const updatedBoard = await getStoryboard(editingStoryboardId); setActiveStoryboard(updatedBoard); setEditingStoryboardId(null); setSelectedImageIds([]); setIsSelecting(false); setActiveSubTab('storyboards'); } catch (err) { setError(err.message); } }; // Handle create project const handleCreateProject = async () => { if (!newProjectName.trim()) return; try { setError(''); const newProject = await createProject(newProjectName.trim()); setNewProjectName(''); setIsCreating(false); setSelectedProject(newProject); // Notify parent of new project selection if (onProjectSelect) { onProjectSelect(newProject.id, newProject.name); } } catch (err) { setError(err.message); } }; // Handle delete project const handleDeleteProject = async (projectId) => { if (!confirm('Are you sure you want to delete this project and all its items?')) return; try { setError(''); await deleteProject(projectId); if (selectedProject?.id === projectId) { setSelectedProject(null); setSelectedProjectItems([]); // Clear parent state if deleted project was active if (onProjectSelect) { onProjectSelect(null, null); } } } catch (err) { setError(err.message); } }; // Handle rename project const handleRenameProject = async (projectId) => { if (!editName.trim()) return; try { setError(''); const updated = await renameProject(projectId, editName.trim()); setEditingId(null); setEditName(''); if (selectedProject?.id === projectId) { setSelectedProject(updated); // Notify parent of name change if (onProjectSelect) { onProjectSelect(updated.id, updated.name); } } } catch (err) { setError(err.message); } }; // Handle delete item const handleDeleteItem = async (itemId) => { if (!confirm('Delete this item?')) return; try { setError(''); await removeItemFromProject(selectedProject.id, itemId); setSelectedProjectItems(prev => prev.filter(i => i.id !== itemId)); } catch (err) { setError(err.message); } }; // Handle export project const handleExportProject = async (projectId) => { try { setError(''); const data = await exportProject(projectId); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${data.project.name.replace(/\s+/g, '_')}_export.json`; link.click(); URL.revokeObjectURL(url); } catch (err) { setError(err.message); } }; // Filter projects by search const filteredProjects = projects.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) ); // Format date const formatDate = (timestamp) => { const date = new Date(timestamp); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined }); }; // Count items by type const countItemsByType = (items, type) => items.filter(i => i.type === type).length; // Show loading state if (!isReady || isLoading) { return (

Loading projects...

); } return (
{/* Left Sidebar: Project List */}

Projects

{/* Error Display */} {(error || dbError) && (
{error || dbError}
)} {/* Search */}
setSearchQuery(e.target.value)} placeholder="Search projects..." className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded text-slate-200 placeholder-slate-500 focus:border-cinema-gold focus:outline-none text-sm" />
{/* Create New Project */} {isCreating && (
setNewProjectName(e.target.value)} placeholder="Project name..." className="flex-1 px-3 py-2 bg-slate-800 border border-cinema-gold rounded text-slate-200 placeholder-slate-500 focus:outline-none text-sm" autoFocus onKeyDown={(e) => e.key === 'Enter' && handleCreateProject()} />
)} {/* Project List */}
{filteredProjects.length === 0 ? (

{projects.length === 0 ? 'No projects yet' : 'No matching projects'}

{projects.length === 0 ? 'Create one to start organizing' : 'Try a different search'}

) : ( filteredProjects.map((project) => (
handleSelectProject(project)} className={`group p-4 rounded cursor-pointer transition-all ${ selectedProject?.id === project.id ? 'bg-cinema-gold/20 border border-cinema-gold' : 'bg-slate-800 hover:bg-slate-700' }`} > {editingId === project.id ? (
e.stopPropagation()}> setEditName(e.target.value)} className="flex-1 px-2 py-1 bg-slate-800 border border-cinema-gold rounded text-slate-200 text-sm" autoFocus onKeyDown={(e) => e.key === 'Enter' && handleRenameProject(project.id)} />
) : ( <>

{project.name}

{formatDate(project.updatedAt)}
)}
)) )}
{/* Right Panel: Project Contents */}
{selectedProject ? (
{/* Project Header */}

{selectedProject.name}

Created {formatDate(selectedProject.createdAt)} · {selectedProjectItems.length} items

{/* Sub-tabs: Library | Storyboards */}
{/* Library Controls (only show when Library tab is active) */} {activeSubTab === 'library' && (
{/* Selection mode toggle */} {/* Create/Update Storyboard button (when images are selected) */} {isSelecting && selectedImageIds.length > 0 && ( editingStoryboardId ? (
) : ( ) )}
{/* Upload and View mode toggles */}
{/* Upload button */} {/* Import from Backend button */} {/* Separator */}
)} {/* Create Storyboard Modal */} {isCreatingStoryboard && (

Name your storyboard

setNewStoryboardName(e.target.value)} placeholder="Storyboard name..." className="flex-1 px-3 py-2 bg-slate-800 border border-slate-600 rounded text-slate-200 placeholder-slate-500 focus:border-cinema-gold focus:outline-none text-sm" autoFocus onKeyDown={(e) => e.key === 'Enter' && handleCreateStoryboard()} />

{selectedImageIds.length} images selected

)} {/* Import from Backend Modal */} {showImportModal && (

Import from Backend

{availableSessions.length === 0 ? (

No backend files found

Generate some images or videos to see them here

) : ( <>

Found {availableSessions.reduce((acc, s) => acc + s.images.length, 0)} image(s) in {availableSessions.length} session(s). Files auto-delete after 24 hours. {availableSessions.reduce((acc, s) => acc + s.videos.length, 0) > 0 && ( Note: Video import not yet supported (videos must be in generated_videos folder) )}

{/* Session list */}
{availableSessions.map((session) => (

Session: {session.session_id.substring(0, 8)}...

{session.images.length} image(s)
{/* Images */} {session.images.length > 0 && (

Images

{session.images.map((img) => { const fileKey = `${session.session_id}:image:${img.filename}`; const isSelected = selectedFiles.includes(fileKey); const expiresIn = Math.floor(img.time_remaining / 3600); return (
toggleFileSelection(session.session_id, 'image', img.filename)} className={`relative p-2 rounded border-2 cursor-pointer transition-all ${ isSelected ? 'border-cinema-gold bg-cinema-gold/10' : 'border-slate-700 hover:border-slate-600' }`} >

{img.filename}

{img.size_kb} KB • {expiresIn}h left

{isSelected && (
)}
); })}
)} {/* Videos - Hidden for now since import not supported */} {false && session.videos.length > 0 && (

Videos (Import Not Supported)

{session.videos.map((vid) => { const expiresIn = Math.floor(vid.time_remaining / 3600); return (

{vid.filename}

{vid.size_mb} MB • {expiresIn}h left

); })}
)}
))}
{/* Action buttons */}

{selectedFiles.length} file(s) selected

)}
)} {/* LIBRARY TAB CONTENT */} {activeSubTab === 'library' && ( <> {/* Project Items */} {selectedProjectItems.length === 0 ? (

This project is empty

Generate images or videos and save them here

) : (
{selectedProjectItems.map((item) => ( viewMode === 'grid' ? (
{ if (isSelecting && item.type === 'image') { toggleImageSelection(item.id); } else { setPreviewItem(item); } }} className={`group relative aspect-square bg-slate-800 rounded overflow-hidden border transition-all cursor-pointer ${ isSelecting && selectedImageIds.includes(item.id) ? 'border-cinema-gold ring-2 ring-cinema-gold' : 'border-slate-700 hover:border-cinema-gold' }`} > {/* Selection Checkbox (only for images in selection mode) */} {isSelecting && item.type === 'image' && (
)} {/* Thumbnail or placeholder — NEVER use full item.data here (causes OOM with many large images) */} {item.thumbnail ? ( {item.prompt} ) : (
{item.type === 'image' ? ( ) : (
)} {/* Type Badge */}
{item.type === 'image' ? 'IMG' : item.settings?.engine === 'kling' ? 'KLING' : 'VEO'}
{/* Hover Overlay (hide when selecting) */} {!isSelecting && (
)}
) : (
{ if (isSelecting && item.type === 'image') { toggleImageSelection(item.id); } else { setPreviewItem(item); } }} className={`flex items-center gap-4 p-3 bg-slate-800 rounded border transition-all cursor-pointer ${ isSelecting && selectedImageIds.includes(item.id) ? 'border-cinema-gold bg-cinema-gold/10' : 'border-slate-700 hover:border-slate-600' }`} > {/* Selection Checkbox for list view */} {isSelecting && item.type === 'image' && (
)}
{item.type === 'image' ? ( ) : (

{item.prompt}

{formatDate(item.createdAt)} {item.settings?.application && ( {getPresetDisplayName(item.settings.application)} )}
{!isSelecting && (
{movingItemId === item.id && (
e.stopPropagation()} >
Move to...
{projects .filter(p => p.id !== selectedProject?.id) .map(p => ( )) } {projects.filter(p => p.id !== selectedProject?.id).length === 0 && (
No other projects
)}
)}
)}
) ))}
)} )} {/* STORYBOARDS TAB CONTENT */} {activeSubTab === 'storyboards' && (
{/* Show StoryboardEditor if a storyboard is active */} {activeStoryboard ? ( setActiveStoryboard(null)} onUpdate={handleUpdateStoryboard} onDelete={handleDeleteStoryboard} onGenerateVideo={handleGenerateVideoFromFrame} onEditFrames={handleEditStoryboardFrames} /> ) : ( <> {/* Storyboards Header */}

{storyboards.length} storyboard{storyboards.length !== 1 ? 's' : ''}

{/* Storyboards List */} {storyboards.length === 0 ? (

No storyboards yet

Select images from your library to create a storyboard

) : (
{storyboards.map((board) => (
handleOpenStoryboard(board.id)} className="group p-4 bg-slate-800 rounded hover:bg-slate-700 transition-all cursor-pointer" >

{board.name}

{board.frames?.length || 0} frames
{/* Frame thumbnails preview */}
{board.frames?.slice(0, 4).map((frame, idx) => { const frameItem = selectedProjectItems.find(item => item.id === frame.imageId); return (
{frameItem && ( )}
); })} {(board.frames?.length || 0) > 4 && (
+{board.frames.length - 4}
)}

Updated {formatDate(board.updatedAt)}

))}
)} )}
)}
) : (

Select a project

Choose a project from the sidebar or create a new one

)}
{/* Preview Modal */} {previewItem && (
{ setPreviewItem(null); setPreviewExpanded(false); }} >
e.stopPropagation()} > {/* Close + Expand buttons */}
{/* Content */}
{previewItem.type === 'image' ? ( {previewItem.prompt} ) : (
{ await addItemToProject(selectedProject.id, { type: 'image', prompt: frameData.prompt, data: frameData.data, mimeType: frameData.mimeType, thumbnail: frameData.data }); // Reload items to show the new frame loadProjectItems(selectedProject.id); } : null} />
)} {/* Info bar */}

{previewItem.prompt}

{previewItem.prompt && ( )}
{previewItem.settings?.application && (
{getPresetDisplayName(previewItem.settings.application)}
)} {previewItem.type === 'video' && (
{previewItem.settings?.engine === 'kling' ? `Kling ${previewItem.settings?.klingModel?.replace('kling-', '').toUpperCase() || 'AI'}` : `Veo ${previewItem.settings?.modelType === 'fast' ? 'Fast' : 'Std'}`} {previewItem.settings?.engine === 'kling' && previewItem.settings?.klingTaskId && ( ID: {previewItem.settings.klingTaskId.substring(0, 8)}... )}
)}
{formatDate(previewItem.createdAt)}
{/* Edit in Image Gen button for images */} {previewItem.type === 'image' && onEditInImageGen && ( )} {/* Generate Video from image (load as first frame) */} {previewItem.type === 'image' && onRerunVideo && ( )} {/* Re-run button for videos */} {previewItem.type === 'video' && onRerunVideo && ( )} {/* Move to another project */} {projects.filter(p => p.id !== selectedProject?.id).length > 0 && (
{movingItemId === previewItem.id && (
{projects .filter(p => p.id !== selectedProject?.id) .map(p => ( )) }
)}
)}
)}
); }; export default ProjectsTab;