import React, { useState, useEffect, useCallback } from "react";
import { useKindeAuth } from "@kinde-oss/kinde-auth-react";
import Button from "./ui/button";
interface SelectionImportDialogProps {
isOpen: boolean;
onClose: () => void;
clusterId: string;
datasetId: string;
onImportComplete: () => void;
}
type ImportState = 'folder_selection' | 'scanning' | 'validation' | 'ready' | 'importing' | 'completed' | 'error';
interface FileValidation {
fileName: string;
hasWav: boolean;
hasPng: boolean;
isValid: boolean;
error?: string;
}
interface ParsedFile {
baseName: string;
startTime: number;
endTime: number;
wavFile: FileSystemFileHandle;
pngFile: FileSystemFileHandle;
species: string;
callType?: string | undefined;
folderPath: string;
}
interface ValidationResult {
validFiles: ParsedFile[];
invalidFiles: FileValidation[];
species: string[];
callTypes: string[];
totalFiles: number;
}
interface ImportProgress {
totalSelections: number;
processedSelections: number;
errors: Array<{ fileName: string; error: string }>;
}
interface DatabaseMapping {
species: Map<string, string>; // folder name -> species ID
callTypes: Map<string, string>; // folder name -> call type ID
files: Map<string, string>; // base filename -> file ID
}
interface MappingValidation {
missingSpecies: string[];
missingCallTypes: string[];
missingFiles: string[];
}
const SelectionImportDialog: React.FC<SelectionImportDialogProps> = ({
isOpen,
onClose,
clusterId,
datasetId,
onImportComplete
}) => {
const { isAuthenticated, getAccessToken } = useKindeAuth();
const [importState, setImportState] = useState<ImportState>('folder_selection');
const [selectedFolder, setSelectedFolder] = useState<FileSystemDirectoryHandle | null>(null);
const [folderPath, setFolderPath] = useState<string>('');
const [isSupported, setIsSupported] = useState<boolean>(false);
// Validation state
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null);
const [scanProgress, setScanProgress] = useState<{ current: number; total: number }>({ current: 0, total: 0 });
// Database mapping state
const [databaseMapping, setDatabaseMapping] = useState<DatabaseMapping | null>(null);
const [mappingValidation, setMappingValidation] = useState<MappingValidation | null>(null);
// Import state
const [importProgress, setImportProgress] = useState<ImportProgress>({
totalSelections: 0,
processedSelections: 0,
errors: []
});
const [lastError, setLastError] = useState<string | null>(null);
// Check browser support for File System Access API
useEffect(() => {
setIsSupported('showDirectoryPicker' in window);
}, []);
const handleSelectFolder = useCallback(async () => {
if (!isSupported) {
console.error('File System Access API not supported');
return;
}
try {
// Show directory picker
const directoryHandle = await window.showDirectoryPicker({
mode: 'read',
startIn: 'documents'
});
setSelectedFolder(directoryHandle);
setFolderPath(directoryHandle.name);
console.log('Selected folder:', directoryHandle.name);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
// User cancelled the picker - close the dialog
console.log('Folder selection cancelled');
onClose();
} else {
console.error('Error selecting folder:', error);
}
}
}, [isSupported, onClose]);
// Reset state when dialog opens and trigger folder picker
useEffect(() => {
if (isOpen) {
setImportState('folder_selection');
setSelectedFolder(null);
setFolderPath('');
setValidationResult(null);
setScanProgress({ current: 0, total: 0 });
setImportProgress({ totalSelections: 0, processedSelections: 0, errors: [] });
setLastError(null);
// If browser supports it, immediately show folder picker
if (isSupported) {
// Use setTimeout to avoid calling handleSelectFolder directly in useEffect
setTimeout(() => {
handleSelectFolder();
}, 100);
}
}
}, [isOpen, isSupported, handleSelectFolder]);
const handleClose = useCallback(() => {
setImportState('folder_selection');
setSelectedFolder(null);
setFolderPath('');
setValidationResult(null);
setScanProgress({ current: 0, total: 0 });
setImportProgress({ totalSelections: 0, processedSelections: 0, errors: [] });
setLastError(null);
onClose();
}, [onClose]);
const fetchDatabaseMappings = useCallback(async (species: string[], callTypes: string[], fileBaseNames: string[]) => {
if (!isAuthenticated) return null;
try {
const accessToken = await getAccessToken();
// Fetch species for this dataset
const speciesResponse = await fetch(`/api/species?datasetId=${encodeURIComponent(datasetId)}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!speciesResponse.ok) {
throw new Error('Failed to fetch species');
}
const speciesData = await speciesResponse.json();
const speciesMap = new Map<string, string>();
const availableSpecies = speciesData.data || [];
// Map species folder names to species IDs
for (const speciesName of species) {
const found = availableSpecies.find((s: { id: string; label: string }) => s.label === speciesName);
if (found) {
speciesMap.set(speciesName, found.id);
}
}
// Fetch call types for the found species
const callTypeMap = new Map<string, string>();
const foundSpeciesIds = Array.from(speciesMap.values());
if (foundSpeciesIds.length > 0 && callTypes.length > 0) {
const callTypePromises = foundSpeciesIds.map(async (speciesId) => {
const callTypeResponse = await fetch(`/api/call-types?speciesId=${encodeURIComponent(speciesId)}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (callTypeResponse.ok) {
const callTypeData = await callTypeResponse.json();
return callTypeData.data || [];
}
return [];
});
const allCallTypes = (await Promise.all(callTypePromises)).flat();
// Map call type folder names to call type IDs
for (const callTypeName of callTypes) {
const found = allCallTypes.find((ct: { id: string; label: string }) => ct.label === callTypeName);
if (found) {
callTypeMap.set(callTypeName, found.id);
}
}
}
// Fetch files for this cluster to map base names to file IDs
const filesResponse = await fetch(`/api/files?clusterId=${encodeURIComponent(clusterId)}&pageSize=1000`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!filesResponse.ok) {
throw new Error('Failed to fetch cluster files');
}
const filesData = await filesResponse.json();
const fileMap = new Map<string, string>();
const availableFiles = filesData.data || [];
// Map base file names to file IDs
for (const baseName of fileBaseNames) {
const found = availableFiles.find((f: { id: string; fileName: string }) => {
// Extract base name from file name (remove extension and path)
const fileName = f.fileName.replace(/\.(wav|WAV)$/, '');
return fileName === baseName;
});
if (found) {
fileMap.set(baseName, found.id);
}
}
return {
species: speciesMap,
callTypes: callTypeMap,
files: fileMap
};
} catch (error) {
console.error('Error fetching database mappings:', error);
throw error;
}
}, [isAuthenticated, getAccessToken, datasetId, clusterId]);
const validateDatabaseMappings = (result: ValidationResult, mapping: DatabaseMapping): MappingValidation => {
const missingSpecies = result.species.filter(name => !mapping.species.has(name));
const missingCallTypes = result.callTypes.filter(name => !mapping.callTypes.has(name));
const missingFiles = result.validFiles
.map(f => f.baseName)
.filter(name => !mapping.files.has(name));
return {
missingSpecies,
missingCallTypes,
missingFiles
};
};
const parseFilename = (filename: string): { baseName: string; startTime: number; endTime: number } | null => {
// Remove extension
const nameWithoutExt = filename.replace(/\.(png|wav)$/i, '');
// Split on last dash to get timing info
const parts = nameWithoutExt.split('-');
if (parts.length < 3) {
return null;
}
// Get start and end times (last two parts)
const endTime = parseFloat(parts[parts.length - 1]);
const startTime = parseFloat(parts[parts.length - 2]);
// Get base name (everything except last two parts)
const baseName = parts.slice(0, -2).join('-');
if (isNaN(startTime) || isNaN(endTime) || endTime <= startTime) {
return null;
}
return { baseName, startTime, endTime };
};
const scanFolder = useCallback(async (directoryHandle: FileSystemDirectoryHandle) => {
const allFiles = new Map<string, { wav?: FileSystemFileHandle; png?: FileSystemFileHandle; species: string; callType?: string | undefined; folderPath: string }>();
let totalFiles = 0;
// First, count total files for progress
const countFiles = async (dirHandle: FileSystemDirectoryHandle, path = '') => {
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
totalFiles++;
} else if (handle.kind === 'directory') {
await countFiles(handle as FileSystemDirectoryHandle, `${path}/${name}`);
}
}
};
await countFiles(directoryHandle);
setScanProgress({ current: 0, total: totalFiles });
// Recursive function to scan directories
const scanDirectory = async (dirHandle: FileSystemDirectoryHandle, currentPath = '', species = '', callType?: string) => {
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
const fileHandle = handle as FileSystemFileHandle;
const isWav = name.toLowerCase().endsWith('.wav');
const isPng = name.toLowerCase().endsWith('.png');
if (isWav || isPng) {
const parsed = parseFilename(name);
if (parsed) {
const key = `${currentPath}/${parsed.baseName}`;
if (!allFiles.has(key)) {
allFiles.set(key, { species, callType: callType || undefined, folderPath: currentPath });
}
const fileEntry = allFiles.get(key)!;
if (isWav) {
fileEntry.wav = fileHandle;
} else if (isPng) {
fileEntry.png = fileHandle;
}
}
}
setScanProgress(prev => ({ ...prev, current: prev.current + 1 }));
} else if (handle.kind === 'directory') {
const dirName = name;
const subDirHandle = handle as FileSystemDirectoryHandle;
const newPath = currentPath ? `${currentPath}/${dirName}` : dirName;
// Determine if this is a species folder or call type folder
if (!species) {
// This is a species folder
await scanDirectory(subDirHandle, newPath, dirName);
} else {
// This is a call type folder
await scanDirectory(subDirHandle, newPath, species, dirName);
}
}
}
};
await scanDirectory(directoryHandle);
// Validate files and create results
const validFiles: ParsedFile[] = [];
const invalidFiles: FileValidation[] = [];
const species = new Set<string>();
const callTypes = new Set<string>();
for (const [baseName, fileEntry] of allFiles.entries()) {
const hasWav = !!fileEntry.wav;
const hasPng = !!fileEntry.png;
const isValid = hasWav && hasPng;
if (isValid && fileEntry.wav && fileEntry.png) {
// Parse timing from PNG filename
const pngName = fileEntry.png.name;
const parsed = parseFilename(pngName);
if (parsed) {
validFiles.push({
baseName: parsed.baseName,
startTime: parsed.startTime,
endTime: parsed.endTime,
wavFile: fileEntry.wav,
pngFile: fileEntry.png,
species: fileEntry.species,
callType: fileEntry.callType || undefined,
folderPath: fileEntry.folderPath
});
species.add(fileEntry.species);
if (fileEntry.callType) {
callTypes.add(fileEntry.callType);
}
} else {
invalidFiles.push({
fileName: baseName,
hasWav,
hasPng,
isValid: false,
error: 'Could not parse timing from filename'
});
}
} else {
invalidFiles.push({
fileName: baseName,
hasWav,
hasPng,
isValid: false,
error: `Missing ${!hasWav ? 'WAV' : ''} ${!hasWav && !hasPng ? 'and ' : ''}${!hasPng ? 'PNG' : ''} file`
});
}
}
return {
validFiles,
invalidFiles,
species: Array.from(species),
callTypes: Array.from(callTypes),
totalFiles: validFiles.length + invalidFiles.length
};
}, []);
const handleStartScan = useCallback(async () => {
if (!selectedFolder) return;
try {
setImportState('scanning');
console.log('Starting folder scan for:', folderPath);
const result = await scanFolder(selectedFolder);
setValidationResult(result);
// Fetch database mappings to validate entities exist
setImportState('validation');
console.log('Fetching database mappings...');
const fileBaseNames = result.validFiles.map(f => f.baseName);
const mapping = await fetchDatabaseMappings(result.species, result.callTypes, fileBaseNames);
if (mapping) {
setDatabaseMapping(mapping);
const mappingValidation = validateDatabaseMappings(result, mapping);
setMappingValidation(mappingValidation);
const hasValidationIssues = result.invalidFiles.length > 0 ||
mappingValidation.missingSpecies.length > 0 ||
mappingValidation.missingCallTypes.length > 0 ||
mappingValidation.missingFiles.length > 0;
if (hasValidationIssues) {
setImportState('validation');
} else {
setImportState('ready');
}
} else {
throw new Error('Failed to fetch database mappings');
}
} catch (error) {
console.error('Error during folder scan:', error);
setImportState('error');
setLastError(error instanceof Error ? error.message : 'Unknown error occurred');
}
}, [selectedFolder, folderPath, scanFolder, fetchDatabaseMappings]);
const handleStartImport = useCallback(async () => {
if (!validationResult || !databaseMapping || !isAuthenticated) return;
try {
setImportState('importing');
setImportProgress({
totalSelections: validationResult.validFiles.length,
processedSelections: 0,
errors: []
});
// Get first available filter for this dataset - we'll use a simple approach
const accessToken = await getAccessToken();
// Fetch filters to get a valid filter ID
const filtersResponse = await fetch('/api/filters', {
headers: { Authorization: `Bearer ${accessToken}` },
});
let filterId = 'default-filter-id'; // fallback
if (filtersResponse.ok) {
const filtersData = await filtersResponse.json();
const filters = filtersData.data || [];
if (filters.length > 0) {
filterId = filters[0].id; // Use first available filter
}
}
// Map files to database entities and create selections
const selectionsToImport = validationResult.validFiles
.filter(file => databaseMapping.files.has(file.baseName)) // Only include files that exist in DB
.map(file => {
const fileId = databaseMapping.files.get(file.baseName)!;
const speciesId = databaseMapping.species.get(file.species);
if (!speciesId) {
console.warn(`Species not found: ${file.species}`);
return null;
}
const callTypeIds = file.callType ?
databaseMapping.callTypes.has(file.callType) ? [databaseMapping.callTypes.get(file.callType)!] : []
: [];
return {
fileId,
startTime: file.startTime,
endTime: file.endTime,
species: [{
speciesId,
callTypes: callTypeIds
}]
};
})
.filter(Boolean); // Remove null entries
if (selectionsToImport.length === 0) {
throw new Error('No valid selections to import after mapping to database entities');
}
const response = await fetch('/api/selections', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
datasetId,
filterId,
selections: selectionsToImport
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`HTTP ${response.status}: ${errorData.error || errorData.details || 'Unknown error'}`);
}
const result = await response.json();
console.log('Import completed:', result);
setImportState('completed');
setTimeout(() => {
onImportComplete();
handleClose();
}, 2000);
} catch (error) {
console.error('Error during import:', error);
setImportState('error');
setLastError(error instanceof Error ? error.message : 'Unknown error occurred');
}
}, [validationResult, databaseMapping, isAuthenticated, datasetId, getAccessToken, onImportComplete, handleClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b">
<h2 className="text-lg font-semibold text-gray-900">Import Selections</h2>
<button
onClick={handleClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Content */}
<div className="p-6">
{!isSupported ? (
// Browser not supported
<div className="text-center">
<div className="mb-4">
<svg className="w-12 h-12 text-yellow-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Browser Not Supported</h3>
<p className="text-sm text-gray-600 mb-4">
Selection import requires a modern browser with File System Access API support.
Please use Chrome 86+, Edge 86+, or another compatible browser.
</p>
</div>
) : importState === 'folder_selection' ? (
// Folder selection state
<div className="text-center">
{selectedFolder ? (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-md">
<div className="flex items-center">
<svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-green-800">Selected: {folderPath}</span>
</div>
</div>
) : (
<div className="mb-6">
<Button
onClick={handleSelectFolder}
variant="default"
size="lg"
className="w-full"
>
Select Clips Folder
</Button>
</div>
)}
<div className="text-xs text-gray-500 bg-gray-50 p-3 rounded-md">
<p className="mb-2">📁 Expected folder structure:</p>
<p className="font-mono text-left">
Clips_ModelName_Date/<br />
├── Species Name/<br />
│ ├── Call Type/ (optional)<br />
│ │ ├── file-start-end.wav<br />
│ │ └── file-start-end.png<br />
│ └── ...
</p>
</div>
</div>
) : importState === 'scanning' ? (
// Scanning state
<div className="text-center">
<div className="mb-4">
<svg className="w-12 h-12 text-blue-500 mx-auto animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Scanning Folder</h3>
<p className="text-sm text-gray-600 mb-4">Validating WAV/PNG pairs...</p>
{scanProgress.total > 0 && (
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(scanProgress.current / scanProgress.total) * 100}%` }}
></div>
</div>
)}
<p className="text-xs text-gray-500 mt-2">
{scanProgress.current} / {scanProgress.total} files scanned
</p>
</div>
) : importState === 'validation' && validationResult ? (
// Validation state - show validation results
<div>
<div className="mb-4 text-center">
<svg className="w-12 h-12 text-yellow-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2 text-center">Validation Results</h3>
<div className="bg-gray-50 p-3 rounded-md text-sm space-y-2">
<div className="flex justify-between">
<span>Valid files:</span>
<span className="text-green-600 font-medium">{validationResult.validFiles.length}</span>
</div>
<div className="flex justify-between">
<span>Invalid files:</span>
<span className="text-red-600 font-medium">{validationResult.invalidFiles.length}</span>
</div>
{mappingValidation && (
<>
<div className="flex justify-between">
<span>Missing species:</span>
<span className="text-red-600 font-medium">{mappingValidation.missingSpecies.length}</span>
</div>
<div className="flex justify-between">
<span>Missing call types:</span>
<span className="text-red-600 font-medium">{mappingValidation.missingCallTypes.length}</span>
</div>
<div className="flex justify-between">
<span>Missing files:</span>
<span className="text-red-600 font-medium">{mappingValidation.missingFiles.length}</span>
</div>
</>
)}
</div>
{/* File validation issues */}
{validationResult.invalidFiles.length > 0 && (
<div className="mt-4">
<p className="text-sm font-medium text-gray-900 mb-2">File Issues:</p>
<div className="max-h-32 overflow-y-auto space-y-1 bg-red-50 p-2 rounded">
{validationResult.invalidFiles.map((file, index) => (
<div key={index} className="text-xs text-red-600">
<span className="font-medium">{file.fileName}:</span> {file.error}
</div>
))}
</div>
</div>
)}
{/* Database mapping issues */}
{mappingValidation && (
<>
{mappingValidation.missingSpecies.length > 0 && (
<div className="mt-4">
<p className="text-sm font-medium text-gray-900 mb-2">Missing Species in Database:</p>
<div className="max-h-24 overflow-y-auto space-y-1 bg-red-50 p-2 rounded">
{mappingValidation.missingSpecies.map((species, index) => (
<div key={index} className="text-xs text-red-600">
{species}
</div>
))}
</div>
</div>
)}
{mappingValidation.missingCallTypes.length > 0 && (
<div className="mt-4">
<p className="text-sm font-medium text-gray-900 mb-2">Missing Call Types in Database:</p>
<div className="max-h-24 overflow-y-auto space-y-1 bg-red-50 p-2 rounded">
{mappingValidation.missingCallTypes.map((callType, index) => (
<div key={index} className="text-xs text-red-600">
{callType}
</div>
))}
</div>
</div>
)}
{mappingValidation.missingFiles.length > 0 && (
<div className="mt-4">
<p className="text-sm font-medium text-gray-900 mb-2">Missing Files in Cluster:</p>
<div className="max-h-24 overflow-y-auto space-y-1 bg-red-50 p-2 rounded">
{mappingValidation.missingFiles.map((fileName, index) => (
<div key={index} className="text-xs text-red-600">
{fileName}
</div>
))}
</div>
</div>
)}
</>
)}
</div>
) : importState === 'ready' && validationResult ? (
// Ready state - show summary
<div className="text-center">
<div className="mb-4">
<svg className="w-12 h-12 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Ready to Import</h3>
<div className="bg-gray-50 p-3 rounded-md text-sm space-y-1 text-left">
<div className="flex justify-between">
<span>Selections to import:</span>
<span className="font-medium">{validationResult.validFiles.length}</span>
</div>
<div className="flex justify-between">
<span>Species found:</span>
<span className="font-medium">{validationResult.species.length}</span>
</div>
<div className="flex justify-between">
<span>Call types found:</span>
<span className="font-medium">{validationResult.callTypes.length}</span>
</div>
</div>
</div>
) : importState === 'importing' ? (
// Importing state
<div className="text-center">
<div className="mb-4">
<svg className="w-12 h-12 text-blue-500 mx-auto animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Importing Selections</h3>
<p className="text-sm text-gray-600 mb-4">
Creating selections, labels, and call type associations...
</p>
{importProgress.totalSelections > 0 && (
<div className="space-y-2">
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${(importProgress.processedSelections / importProgress.totalSelections) * 100}%` }}
></div>
</div>
<p className="text-xs text-gray-500">
{importProgress.processedSelections} / {importProgress.totalSelections} selections processed
</p>
</div>
)}
</div>
) : importState === 'completed' ? (
// Completed state
<div className="text-center">
<div className="mb-4">
<svg className="w-12 h-12 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Import Complete</h3>
<p className="text-sm text-gray-600 mb-4">
Successfully imported {validationResult?.validFiles.length || 0} selections
</p>
</div>
) : importState === 'error' ? (
// Error state
<div className="text-center">
<div className="mb-4">
<svg className="w-12 h-12 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Import Error</h3>
<p className="text-sm text-gray-600 mb-4">
{lastError || 'An error occurred during import'}
</p>
</div>
) : (
// Fallback
<div className="text-center">
<p className="text-gray-600">Processing...</p>
</div>
)}
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t bg-gray-50">
{importState === 'error' || importState === 'completed' ? (
<Button
onClick={handleClose}
variant="default"
>
{importState === 'completed' ? 'Done' : 'Close'}
</Button>
) : (
<>
<Button
onClick={handleClose}
variant="secondary"
disabled={importState === 'importing'}
>
{importState === 'importing' ? 'Importing...' : 'Cancel'}
</Button>
{selectedFolder && importState === 'folder_selection' && (
<Button
onClick={handleStartScan}
variant="default"
>
Validate
</Button>
)}
{(importState === 'ready' || (importState === 'validation' && validationResult?.validFiles && validationResult.validFiles.length > 0)) && (
<Button
onClick={handleStartImport}
variant="default"
disabled={!validationResult?.validFiles?.length || !databaseMapping ||
(mappingValidation !== null && (mappingValidation.missingFiles.length > 0 || mappingValidation.missingSpecies.length > 0))}
>
Import {validationResult?.validFiles.length || 0} Selections
</Button>
)}
</>
)}
</div>
</div>
</div>
);
};
export default SelectionImportDialog;