Spaces:
Running
Running
import React, { useState, useRef } from "react"; | |
import Papa from "papaparse"; | |
import { ApiService } from "../utils/apiService"; | |
import { config, debugLog } from "../utils/config"; | |
const Step2 = ({ | |
uploadedFile, | |
setUploadedFile, | |
s3Link, | |
setS3Link, | |
fileMetadata, | |
setFileMetadata, | |
stepNumber, | |
stepTitle, | |
stepIcon, | |
enabled = true, | |
// Add API key props for validation | |
apiKey, | |
isApiKeyValid, | |
}) => { | |
const [outputFormat, setOutputFormat] = useState("tabular"); | |
const [isUploading, setIsUploading] = useState(false); | |
const [csvData, setCsvData] = useState(null); | |
const [dragOver, setDragOver] = useState(false); | |
const fileInputRef = useRef(null); | |
const handleFileUpload = async (file) => { | |
// Prevent action if step is disabled | |
if (!enabled) { | |
debugLog("File upload attempted but step is disabled"); | |
return; | |
} | |
if (!file) { | |
alert("No file selected"); | |
return; | |
} | |
if (!file.name.endsWith(".csv")) { | |
alert("Please upload a CSV file. Only CSV files are supported."); | |
return; | |
} | |
// Check file size against configured limit | |
const maxSizeBytes = config.maxFileSizeMB * 1024 * 1024; | |
if (file.size > maxSizeBytes) { | |
alert( | |
`File size (${(file.size / 1024 / 1024).toFixed( | |
2 | |
)}MB) exceeds maximum allowed size of ${config.maxFileSizeMB}MB` | |
); | |
return; | |
} | |
// Check if file is empty | |
if (file.size === 0) { | |
alert( | |
"The selected file is empty. Please choose a valid CSV file with data." | |
); | |
return; | |
} | |
setUploadedFile(file); | |
debugLog("File selected for upload", { | |
name: file.name, | |
size: file.size, | |
type: file.type, | |
}); | |
// Initialize file metadata with file size | |
setFileMetadata({ | |
fileSizeBytes: file.size, | |
sourceFileRows: 0, // Will be updated after CSV parsing | |
}); | |
// Parse CSV for preview | |
Papa.parse(file, { | |
complete: (results) => { | |
if (results.errors && results.errors.length > 0) { | |
debugLog("CSV parsing warnings", results.errors); | |
} | |
if (!results.data || results.data.length === 0) { | |
alert( | |
"The CSV file appears to be empty or invalid. Please check your file and try again." | |
); | |
setUploadedFile(null); | |
setFileMetadata({ fileSizeBytes: 0, sourceFileRows: 0 }); | |
return; | |
} | |
setCsvData(results.data); | |
// Update file metadata with row count | |
setFileMetadata({ | |
fileSizeBytes: file.size, | |
sourceFileRows: results.data.length, | |
}); | |
debugLog("CSV parsed for preview", { | |
rows: results.data.length, | |
columns: results.data[0] ? Object.keys(results.data[0]).length : 0, | |
}); | |
}, | |
header: true, | |
skipEmptyLines: true, | |
error: (error) => { | |
debugLog("CSV parsing error", error); | |
alert("Error parsing CSV file. Please ensure it's a valid CSV format."); | |
setUploadedFile(null); | |
}, | |
}); | |
// Upload to S3 using ApiService | |
setIsUploading(true); | |
try { | |
debugLog("Starting file upload to S3"); | |
// Use the ApiService with retry logic | |
const result = await ApiService.retryRequest(async () => { | |
return await ApiService.uploadFileToS3(file); | |
}); | |
// Handle different response structures | |
const s3Url = | |
result.s3_link || result.link || result.publicUrl || result.url; | |
setS3Link(s3Url); | |
debugLog("File uploaded successfully", { | |
s3Link: s3Url, | |
result: result, | |
}); | |
} catch (error) { | |
debugLog("File upload failed", error); | |
// More specific error messages | |
let errorMessage = "Failed to upload file. Please try again."; | |
if (error.message.includes("credentials")) { | |
errorMessage = | |
"AWS credentials are not configured properly. Please check your .env.local file and restart the application."; | |
} else if (error.message.includes("bucket")) { | |
errorMessage = | |
"Storage bucket configuration issue. Please check your S3 bucket name in .env.local file."; | |
} else if (error.message.includes("size")) { | |
errorMessage = `File size exceeds the maximum limit of ${config.maxFileSizeMB}MB.`; | |
} else if ( | |
error.message.includes("network") || | |
error.message.includes("fetch") || | |
error.message.includes("CORS") | |
) { | |
errorMessage = | |
"Network/CORS error. This might be due to S3 bucket CORS configuration or network connectivity issues."; | |
} else if (error.message.includes("AccessDenied")) { | |
errorMessage = | |
"Access denied to S3 bucket. Please check your AWS permissions."; | |
} | |
alert(errorMessage); | |
setUploadedFile(null); | |
setCsvData(null); | |
} finally { | |
setIsUploading(false); | |
} | |
}; | |
const handleDrop = (e) => { | |
// Prevent action if step is disabled | |
if (!enabled) return; | |
e.preventDefault(); | |
setDragOver(false); | |
const files = e.dataTransfer.files; | |
if (files.length > 0) { | |
handleFileUpload(files[0]); | |
} | |
}; | |
const handleDragOver = (e) => { | |
e.preventDefault(); | |
setDragOver(true); | |
}; | |
const handleDragLeave = (e) => { | |
e.preventDefault(); | |
setDragOver(false); | |
}; | |
const renderDataPreview = () => { | |
if (!csvData || csvData.length === 0) return null; | |
// Show more rows for better preview (up to 20 rows) | |
const previewData = csvData.slice(0, 20); | |
const columns = Object.keys(previewData[0]); | |
return ( | |
<div className="data-preview"> | |
<h4> | |
π Data Preview ({csvData.length} rows total, showing first{" "} | |
{previewData.length}) | |
</h4> | |
<div className="step2-data-table-container"> | |
<table className="data-table-scrollable"> | |
<thead> | |
<tr> | |
{columns.map((col, index) => ( | |
<th key={index}>{col}</th> | |
))} | |
</tr> | |
</thead> | |
<tbody> | |
{previewData.map((row, index) => ( | |
<tr key={index}> | |
{columns.map((col, colIndex) => ( | |
<td key={colIndex} title={row[col]}> | |
{row[col]} | |
</td> | |
))} | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
{csvData.length > 20 && ( | |
<p className="preview-note"> | |
Showing first 20 rows of {csvData.length} total rows | |
</p> | |
)} | |
</div> | |
); | |
}; | |
return ( | |
<div className="step-container fade-in"> | |
<div className="step-header"> | |
<h2> | |
<span className="step-number">{stepNumber}</span> | |
{stepIcon} {stepTitle} | |
</h2> | |
<p> | |
Choose your output format and upload a CSV file for analysis and | |
synthetic data generation. | |
</p> | |
</div> | |
<div className="step-body"> | |
{!enabled && ( | |
<div | |
className="status-message warning" | |
style={{ marginBottom: "1.5rem" }} | |
> | |
<div className="status-message-icon">β οΈ</div> | |
<div className="status-message-content"> | |
<h4>API Key Required</h4> | |
<p> | |
Please complete Step 1 (API Key Validation) before uploading | |
files. | |
</p> | |
</div> | |
</div> | |
)} | |
<div className="form-group output-format-section"> | |
<label>Output Format</label> | |
<div className="output-options"> | |
<div | |
className={`output-option ${ | |
outputFormat === "tabular" ? "selected" : "" | |
}`} | |
onClick={() => setOutputFormat("tabular")} | |
> | |
<h4>π Tabular</h4> | |
<p>CSV format with structured rows and columns</p> | |
</div> | |
<div className="output-option disabled" title="Coming Soon"> | |
<h4>π JSONL (Coming Soon)</h4> | |
<p>JSON Lines format for advanced use cases</p> | |
</div> | |
</div> | |
</div> | |
<div className="form-group file-upload-section"> | |
<label>π€ Upload Source File</label> | |
{uploadedFile && s3Link ? ( | |
<div className="file-uploaded"> | |
<div className="file-info"> | |
<div className="file-icon">π</div> | |
<div className="file-details"> | |
<h4>{uploadedFile.name}</h4> | |
<p>Successfully uploaded and ready for processing</p> | |
<p | |
style={{ | |
fontSize: "0.75rem", | |
opacity: 0.8, | |
marginTop: "0.25rem", | |
}} | |
> | |
Size: {(uploadedFile.size / 1024 / 1024).toFixed(2)} MB | |
</p> | |
</div> | |
<button | |
className="btn btn-secondary" | |
onClick={() => { | |
setUploadedFile(null); | |
setS3Link(""); | |
setCsvData(null); | |
setFileMetadata({ fileSizeBytes: 0, sourceFileRows: 0 }); | |
}} | |
style={{ marginLeft: "auto" }} | |
> | |
π Change File | |
</button> | |
</div> | |
</div> | |
) : ( | |
<div | |
className={`file-upload-area ${dragOver ? "drag-over" : ""}`} | |
onDrop={handleDrop} | |
onDragOver={handleDragOver} | |
onDragLeave={handleDragLeave} | |
onClick={() => fileInputRef.current?.click()} | |
> | |
<input | |
ref={fileInputRef} | |
type="file" | |
accept=".csv" | |
onChange={(e) => handleFileUpload(e.target.files[0])} | |
/> | |
{isUploading ? ( | |
<div> | |
<div className="spinner"></div> | |
<div className="file-upload-text">Uploading your file...</div> | |
<div className="file-upload-subtext"> | |
Please wait while we process your data | |
</div> | |
</div> | |
) : ( | |
<div> | |
<div className="file-upload-icon">π</div> | |
<div className="file-upload-text"> | |
Drop your CSV file here | |
</div> | |
<div className="file-upload-subtext"> | |
or click to browse β’ Max {config.maxFileSizeMB}MB β’ CSV | |
files only | |
</div> | |
</div> | |
)} | |
</div> | |
)} | |
</div> | |
{renderDataPreview()} | |
</div> | |
</div> | |
); | |
}; | |
export default Step2; | |