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...
{projects.length === 0 ? 'No projects yet' : 'No matching projects'}
{projects.length === 0 ? 'Create one to start organizing' : 'Try a different search'}
Created {formatDate(selectedProject.createdAt)} · {selectedProjectItems.length} items
{selectedImageIds.length} images selected
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 */}{img.filename}
{img.size_kb} KB • {expiresIn}h left
{isSelected && ({vid.filename}
{vid.size_mb} MB • {expiresIn}h left
{selectedFiles.length} file(s) selected
This project is empty
Generate images or videos and save them here
{item.prompt}
{storyboards.length} storyboard{storyboards.length !== 1 ? 's' : ''}
No storyboards yet
Select images from your library to create a storyboard
Updated {formatDate(board.updatedAt)}
Select a project
Choose a project from the sidebar or create a new one
{previewItem.prompt}
{previewItem.prompt && ( )}