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; | |