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