Spaces:
Running
Running
| import React, { useState } from "react"; | |
| import { ApiService } from "../utils/apiService"; | |
| import { debugLog } from "../utils/config"; | |
| import DataViewer from "./DataViewer"; | |
| const Step4 = ({ | |
| apiKey, | |
| s3Link, | |
| config: generationConfig, | |
| fileMetadata, | |
| generatedDataLink, | |
| setGeneratedDataLink, | |
| stepNumber, | |
| stepTitle, | |
| stepIcon, | |
| enabled = true, | |
| }) => { | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const [generationStatus, setGenerationStatus] = useState(""); | |
| const [generationProgress, setGenerationProgress] = useState(0); | |
| const [hasError, setHasError] = useState(false); | |
| const [errorMessage, setErrorMessage] = useState(""); | |
| const generateSyntheticData = async () => { | |
| // Prevent action if step is disabled | |
| if (!enabled) { | |
| debugLog("Generation attempted but step is disabled"); | |
| return; | |
| } | |
| setIsGenerating(true); | |
| setGenerationStatus("Initializing generation..."); | |
| setGenerationProgress(0); | |
| setHasError(false); | |
| setErrorMessage(""); | |
| try { | |
| debugLog("Starting synthetic data generation", { | |
| s3Link, | |
| config: generationConfig, | |
| fileMetadata, | |
| apiKeyPrefix: apiKey.substring(0, 8) + "...", | |
| }); | |
| // Use the ApiService with retry logic | |
| const result = await ApiService.retryRequest(async () => { | |
| // Update progress during API call | |
| setGenerationStatus("Sending request to AI model..."); | |
| setGenerationProgress(10); | |
| return await ApiService.generateSyntheticData(apiKey, s3Link, { | |
| ...generationConfig, | |
| fileSizeBytes: fileMetadata?.fileSizeBytes || 0, | |
| sourceFileRows: fileMetadata?.sourceFileRows || 0, | |
| }); | |
| }); | |
| // If the API returns progress updates, handle them | |
| if (result.jobId) { | |
| // For now, simulate progress since polling is not implemented | |
| await simulateProgress(); | |
| // In a real implementation, you would poll for job status here | |
| setGeneratedDataLink( | |
| result.data?.fileUrl || | |
| result.fileUrl || | |
| result.download_link || | |
| result.link || | |
| result.s3_url | |
| ); | |
| } else { | |
| // Simulate progress for immediate results | |
| await simulateProgress(); | |
| // Handle the specific response format: { "status": "success", "data": { "fileUrl": "..." } } | |
| const dataLink = | |
| result.data?.fileUrl || | |
| result.fileUrl || | |
| result.s3_url || | |
| result.download_link || | |
| result.link; | |
| if (!dataLink) { | |
| throw new Error( | |
| "No download link received from the API. The generation may have failed on the server side." | |
| ); | |
| } | |
| setGeneratedDataLink(dataLink); | |
| } | |
| setGenerationStatus("Generation completed successfully!"); | |
| setGenerationProgress(100); | |
| debugLog("Synthetic data generation completed", { | |
| downloadLink: | |
| result.data?.fileUrl || | |
| result.fileUrl || | |
| result.s3_url || | |
| result.download_link || | |
| result.link, | |
| status: result.status, | |
| message: result.message, | |
| fullResult: result, | |
| }); | |
| } catch (error) { | |
| debugLog("Synthetic data generation failed", error); | |
| setHasError(true); | |
| setGenerationProgress(0); | |
| // Create a user-friendly error message | |
| let friendlyErrorMessage = "An error occurred during generation."; | |
| if (error.message.includes("403") || error.message.includes("401")) { | |
| friendlyErrorMessage = | |
| "Authentication failed. Please check your API key and try again."; | |
| } else if (error.message.includes("404")) { | |
| friendlyErrorMessage = | |
| "Generation service not found. Please try again later."; | |
| } else if (error.message.includes("500")) { | |
| friendlyErrorMessage = | |
| "Server error occurred. The service may be temporarily unavailable."; | |
| } else if ( | |
| error.message.includes("timeout") || | |
| error.message.includes("TimeoutError") | |
| ) { | |
| friendlyErrorMessage = | |
| "Request timed out. The generation process may take longer than expected. Please try again."; | |
| } else if ( | |
| error.message.includes("Network") || | |
| error.message.includes("fetch") | |
| ) { | |
| friendlyErrorMessage = | |
| "Network error. Please check your internet connection and try again."; | |
| } else if (error.message.includes("No download link")) { | |
| friendlyErrorMessage = | |
| "Generation completed but no download link was provided. Please try generating again."; | |
| } else if (error.message) { | |
| friendlyErrorMessage = error.message; | |
| } | |
| setErrorMessage(friendlyErrorMessage); | |
| setGenerationStatus(`β Generation failed: ${friendlyErrorMessage}`); | |
| } finally { | |
| setIsGenerating(false); | |
| } | |
| }; | |
| const simulateProgress = async () => { | |
| const steps = [ | |
| { progress: 25, message: "Analyzing data structure..." }, | |
| { progress: 50, message: "Training AI model..." }, | |
| { progress: 75, message: "Generating synthetic data..." }, | |
| { progress: 90, message: "Finalizing output..." }, | |
| ]; | |
| for (const step of steps) { | |
| setGenerationProgress(step.progress); | |
| setGenerationStatus(step.message); | |
| await new Promise((resolve) => setTimeout(resolve, 1500)); | |
| } | |
| }; | |
| const handleDownload = () => { | |
| if (generatedDataLink) { | |
| debugLog("Downloading generated data", { link: generatedDataLink }); | |
| // Open in new tab so user can see if download works | |
| const newWindow = window.open(generatedDataLink, "_blank"); | |
| // If popup blocked, provide fallback | |
| if (!newWindow) { | |
| // Fallback: create a temporary download link | |
| const link = document.createElement("a"); | |
| link.href = generatedDataLink; | |
| link.download = `synthetic_data_${Date.now()}.csv`; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| } | |
| } | |
| }; | |
| const handleCopyLink = async () => { | |
| if (generatedDataLink) { | |
| try { | |
| await navigator.clipboard.writeText(generatedDataLink); | |
| // You could add a toast notification here | |
| debugLog("Link copied to clipboard"); | |
| } catch (error) { | |
| debugLog("Failed to copy link:", error); | |
| // Fallback for older browsers | |
| const textArea = document.createElement("textarea"); | |
| textArea.value = generatedDataLink; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(textArea); | |
| } | |
| } | |
| }; | |
| const isReadyForGeneration = | |
| apiKey && | |
| s3Link && | |
| generationConfig.targetColumn && | |
| generationConfig.numRows; | |
| return ( | |
| <div className="step-container fade-in"> | |
| <div className="step-header"> | |
| <h2> | |
| <span className="step-number">{stepNumber}</span> | |
| {stepIcon} {stepTitle} | |
| </h2> | |
| <p>Generate high-quality synthetic data based on your configuration</p> | |
| </div> | |
| <div className="step-body"> | |
| {!generatedDataLink && !isGenerating && !hasError && ( | |
| <div className="generate-section"> | |
| <div | |
| className={`status-message ${ | |
| isReadyForGeneration ? "info" : "warning" | |
| }`} | |
| > | |
| <div className="status-message-icon"> | |
| {isReadyForGeneration ? "β " : "β οΈ"} | |
| </div> | |
| <div className="status-message-content"> | |
| <h4> | |
| {isReadyForGeneration | |
| ? "Ready for Generation" | |
| : "Configuration Required"} | |
| </h4> | |
| {!isReadyForGeneration && ( | |
| <p | |
| style={{ | |
| marginTop: "0.5rem", | |
| fontSize: "0.875rem", | |
| opacity: 0.9, | |
| }} | |
| > | |
| Please complete all previous steps before generating data. | |
| </p> | |
| )} | |
| <div className="status-summary"> | |
| <div className="status-row"> | |
| <span className="status-label">π API Key:</span> | |
| <span | |
| className={`status-badge ${ | |
| apiKey ? "success" : "warning" | |
| }`} | |
| > | |
| {apiKey ? "β Valid" : "β Required"} | |
| </span> | |
| </div> | |
| <div className="status-row"> | |
| <span className="status-label">π Data:</span> | |
| <span | |
| className={`status-badge ${ | |
| s3Link ? "success" : "warning" | |
| }`} | |
| > | |
| {s3Link ? "β Uploaded" : "β Required"} | |
| </span> | |
| </div> | |
| <div className="status-row"> | |
| <span className="status-label">π― Target:</span> | |
| <span | |
| className={`status-badge ${ | |
| generationConfig.targetColumn ? "success" : "warning" | |
| }`} | |
| > | |
| {generationConfig.targetColumn | |
| ? `β ${generationConfig.targetColumn}` | |
| : "β Not set"} | |
| </span> | |
| </div> | |
| <div className="status-row"> | |
| <span className="status-label">π₯ Source:</span> | |
| <span className="status-badge info"> | |
| {fileMetadata?.sourceFileRows || 0} rows | |
| </span> | |
| </div> | |
| <div className="status-row"> | |
| <span className="status-label">π€ Generate:</span> | |
| <span className="status-badge info"> | |
| {generationConfig.numRows} rows | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <button | |
| className="btn btn-primary generate-btn" | |
| onClick={generateSyntheticData} | |
| disabled={!isReadyForGeneration || !enabled} | |
| style={{ marginTop: "1.5rem" }} | |
| > | |
| π― Generate Synthetic Data | |
| </button> | |
| </div> | |
| )} | |
| {isGenerating && ( | |
| <div className="generation-progress"> | |
| <div className="spinner"></div> | |
| <div className="file-upload-text" style={{ marginTop: "1rem" }}> | |
| {generationStatus} | |
| </div> | |
| <div className="progress-bar"> | |
| <div | |
| className="progress-fill" | |
| style={{ width: `${generationProgress}%` }} | |
| ></div> | |
| </div> | |
| <div | |
| className="file-upload-subtext" | |
| style={{ marginTop: "0.75rem" }} | |
| > | |
| {generationProgress}% Complete β’ This may take a few minutes | |
| </div> | |
| </div> | |
| )} | |
| {hasError && !isGenerating && ( | |
| <div className="error-section"> | |
| <div className="status-message error"> | |
| <div className="status-message-icon">β</div> | |
| <div className="status-message-content"> | |
| <h4>Generation Failed</h4> | |
| <p>There was an error generating your synthetic data.</p> | |
| <div className="error-details"> | |
| <strong>Error:</strong> {errorMessage} | |
| </div> | |
| <div | |
| className="error-help" | |
| style={{ marginTop: "0.75rem", fontSize: "0.875rem" }} | |
| > | |
| <strong>What you can try:</strong> | |
| <ul | |
| style={{ | |
| marginTop: "0.5rem", | |
| paddingLeft: "1.5rem", | |
| textAlign: "left", | |
| }} | |
| > | |
| <li>Check your internet connection and try again</li> | |
| <li> | |
| Verify your API key is valid and has sufficient credits | |
| </li> | |
| <li>Try reducing the number of rows to generate</li> | |
| <li>Contact support if the problem persists</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <div style={{ marginTop: "1.5rem", textAlign: "center" }}> | |
| <button | |
| className="btn btn-primary" | |
| onClick={() => { | |
| setHasError(false); | |
| setErrorMessage(""); | |
| setGenerationStatus(""); | |
| setGenerationProgress(0); | |
| }} | |
| style={{ marginRight: "1rem" }} | |
| > | |
| π Try Again | |
| </button> | |
| <button | |
| className="btn btn-secondary" | |
| onClick={() => { | |
| // Reset to initial state | |
| setHasError(false); | |
| setErrorMessage(""); | |
| setGenerationStatus(""); | |
| setGenerationProgress(0); | |
| setGeneratedDataLink(""); | |
| }} | |
| > | |
| π Start Over | |
| </button> | |
| </div> | |
| </div> | |
| )} | |
| {generatedDataLink && !isGenerating && ( | |
| <div className="results-section"> | |
| <div | |
| className="status-message success" | |
| style={{ marginBottom: "1.5rem" }} | |
| > | |
| <div className="status-message-icon">π</div> | |
| <div className="status-message-content"> | |
| <h4>Generation Complete!</h4> | |
| <p style={{ margin: "0.5rem 0" }}> | |
| Successfully generated {generationConfig.numRows} rows of | |
| synthetic data targeting the{" "} | |
| <strong>{generationConfig.targetColumn}</strong> column. | |
| </p> | |
| </div> | |
| </div> | |
| <div | |
| className="data-preview-section" | |
| style={{ marginTop: "1.5rem" }} | |
| > | |
| <div | |
| className="preview-header" | |
| style={{ | |
| display: "flex", | |
| justifyContent: "space-between", | |
| alignItems: "center", | |
| marginBottom: "1rem", | |
| flexWrap: "wrap", | |
| gap: "1rem", | |
| }} | |
| > | |
| <h4 style={{ margin: 0, color: "var(--text-primary)" }}> | |
| π Data Preview | |
| </h4> | |
| <div | |
| className="action-buttons" | |
| style={{ | |
| display: "flex", | |
| gap: "0.75rem", | |
| flexWrap: "wrap", | |
| }} | |
| > | |
| <button | |
| className="btn btn-success" | |
| onClick={handleDownload} | |
| title="Download the generated synthetic data file" | |
| > | |
| π₯ Download | |
| </button> | |
| <button | |
| className="btn btn-outline" | |
| onClick={handleCopyLink} | |
| title="Copy download link to clipboard" | |
| > | |
| π Copy Link | |
| </button> | |
| <button | |
| className="btn btn-secondary" | |
| onClick={() => { | |
| setGeneratedDataLink(""); | |
| setGenerationProgress(0); | |
| setGenerationStatus(""); | |
| setHasError(false); | |
| setErrorMessage(""); | |
| }} | |
| title="Generate new synthetic data with different parameters" | |
| > | |
| π New Generation | |
| </button> | |
| </div> | |
| </div> | |
| <DataViewer s3Url={generatedDataLink} showPreviewOnly={true} /> | |
| <div | |
| className="preview-info" | |
| style={{ | |
| marginTop: "1rem", | |
| padding: "0.75rem", | |
| background: "var(--bg-tertiary)", | |
| borderRadius: "8px", | |
| fontSize: "0.875rem", | |
| color: "var(--text-secondary)", | |
| textAlign: "center", | |
| }} | |
| > | |
| π‘ Showing preview of first few rows. Download the complete file | |
| ({generationConfig.numRows} rows) using the button above. | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Step4; | |