GAN_Project / frontend /src /App.jsx
Utkarsh64's picture
Update frontend/src/App.jsx
cfa3145 verified
import { useEffect, useMemo, useState } from "react";
import "./App.css";
const API_URL = "/enhance";
const ACCEPTED_TYPES = ["image/png", "image/jpeg", "image/jpg", "image/webp"];
function App() {
const [selectedFile, setSelectedFile] = useState(null);
const [inputPreviewUrl, setInputPreviewUrl] = useState("");
const [outputPreviewUrl, setOutputPreviewUrl] = useState("");
const [isEnhancing, setIsEnhancing] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [isDragging, setIsDragging] = useState(false);
const [successMessage, setSuccessMessage] = useState("");
const fileSizeInMb = useMemo(() => {
if (!selectedFile) return "";
return `${(selectedFile.size / (1024 * 1024)).toFixed(2)} MB`;
}, [selectedFile]);
useEffect(() => {
return () => {
if (inputPreviewUrl) URL.revokeObjectURL(inputPreviewUrl);
if (outputPreviewUrl) URL.revokeObjectURL(outputPreviewUrl);
};
}, [inputPreviewUrl, outputPreviewUrl]);
const clearResult = () => {
if (outputPreviewUrl) {
URL.revokeObjectURL(outputPreviewUrl);
}
setOutputPreviewUrl("");
};
const resetAll = () => {
if (inputPreviewUrl) URL.revokeObjectURL(inputPreviewUrl);
if (outputPreviewUrl) URL.revokeObjectURL(outputPreviewUrl);
setSelectedFile(null);
setInputPreviewUrl("");
setOutputPreviewUrl("");
setErrorMessage("");
setSuccessMessage("");
setIsDragging(false);
};
const validateAndSetFile = (file) => {
if (!file) return;
if (!ACCEPTED_TYPES.includes(file.type)) {
setErrorMessage("Please upload PNG, JPG, JPEG, or WEBP formats only.");
setSuccessMessage("");
return;
}
if (file.size > 50 * 1024 * 1024) {
setErrorMessage("Image must be smaller than 50 MB.");
setSuccessMessage("");
return;
}
if (inputPreviewUrl) URL.revokeObjectURL(inputPreviewUrl);
const newInputUrl = URL.createObjectURL(file);
setSelectedFile(file);
setInputPreviewUrl(newInputUrl);
setErrorMessage("");
setSuccessMessage("");
clearResult();
};
const handleFileChange = (event) => {
const file = event.target.files?.[0];
validateAndSetFile(file);
};
const handleDrop = (event) => {
event.preventDefault();
setIsDragging(false);
const file = event.dataTransfer.files?.[0];
validateAndSetFile(file);
};
const downloadEnhanced = () => {
const a = document.createElement("a");
a.href = outputPreviewUrl;
a.download = "enhanced-image.png";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
};
const enhanceImage = async () => {
if (!selectedFile) {
setErrorMessage("Please select an image first.");
return;
}
setIsEnhancing(true);
setErrorMessage("");
setSuccessMessage("");
const formData = new FormData();
formData.append("image", selectedFile);
try {
const response = await fetch(API_URL, {
method: "POST",
body: formData,
});
if (!response.ok) {
let serverMessage = "Enhancement failed. Please try again.";
try {
const errorBody = await response.json();
serverMessage = errorBody.error || serverMessage;
} catch {
serverMessage = `Enhancement failed with status ${response.status}.`;
}
throw new Error(serverMessage);
}
const blob = await response.blob();
const outputUrl = URL.createObjectURL(blob);
if (outputPreviewUrl) URL.revokeObjectURL(outputPreviewUrl);
setOutputPreviewUrl(outputUrl);
setSuccessMessage("✓ Enhancement complete! Compare the images below.");
} catch (error) {
setErrorMessage(error.message || "An unexpected error occurred.");
setSuccessMessage("");
} finally {
setIsEnhancing(false);
}
};
return (
<main className="app-container">
{/* Header */}
<header className="app-header">
<div className="header-content">
<div className="logo-section">
<div className="logo-icon"></div>
<h1 className="app-title">LumiGAN</h1>
<div className="brand-image" aria-hidden="true"></div>
</div>
<p className="app-tagline">Professional Low-Light Image Enhancement</p>
</div>
</header>
{/* Main Content */}
<section className="app-shell">
{/* Hero Section */}
<div className="hero-section">
<div className="hero-content">
<p className="eyebrow">POWERED BY GENERATIVE AI</p>
<h2 className="hero-title">Brighten Your Darkness</h2>
<p className="hero-description">
Transform underexposed photos into stunning, well-lit images using advanced
neural networks. Perfect for night photography, dimly lit interiors, and more.
</p>
<div className="feature-pills">
<span className="pill">🚀 Real-Time Processing</span>
<span className="pill">🎨 Neural Enhancement</span>
<span className="pill">📊 Before/After Comparison</span>
</div>
</div>
</div>
{/* Main Layout: Upload + Results */}
<div className="main-layout">
{/* Upload Bar */}
<div className={`upload-bar ${outputPreviewUrl ? "has-results" : ""}`}>
<div className="sidebar-header">
<h3>Upload Image</h3>
</div>
<div
className={`dropzone compact-dropzone ${isDragging ? "dropzone-active" : ""} ${inputPreviewUrl ? "has-file" : ""}`}
onDragEnter={(event) => {
event.preventDefault();
setIsDragging(true);
}}
onDragOver={(event) => event.preventDefault()}
onDragLeave={() => setIsDragging(false)}
onDrop={handleDrop}
>
{!inputPreviewUrl ? (
<div className="compact-upload-content">
<div className="compact-upload-icon">📸</div>
<p className="compact-upload-text">Choose image</p>
<label className="upload-button compact-button" htmlFor="imagePicker">
<span>Select</span>
</label>
<input
id="imagePicker"
type="file"
accept="image/png,image/jpeg,image/jpg,image/webp"
onChange={handleFileChange}
/>
</div>
) : (
<div className="file-preview compact-preview">
<img src={inputPreviewUrl} alt="Preview" className="preview-image compact-preview-image" />
<div className="file-meta">
<p className="file-name-compact">{selectedFile?.name}</p>
<p className="file-size-compact">{fileSizeInMb}</p>
</div>
<label className="change-button compact-button" htmlFor="imagePicker">
</label>
<input
id="imagePicker"
type="file"
accept="image/png,image/jpeg,image/jpg,image/webp"
onChange={handleFileChange}
/>
</div>
)}
</div>
{/* Alerts */}
{errorMessage && <div className="alert alert-error compact-alert">{errorMessage}</div>}
{successMessage && <div className="alert alert-success compact-alert">{successMessage}</div>}
{/* Action Button */}
{inputPreviewUrl && !outputPreviewUrl && (
<button
className={`enhance-button compact-enhance-button ${isEnhancing ? "processing" : ""}`}
onClick={enhanceImage}
disabled={isEnhancing}
>
{isEnhancing ? (
<>
<span className="spinner"></span>
Processing...
</>
) : (
"✨ Enhance"
)}
</button>
)}
{/* Reset Button */}
{outputPreviewUrl && (
<button className="reset-button compact-reset-button" onClick={resetAll}>
↻ New Image
</button>
)}
</div>
{/* Results Section */}
{outputPreviewUrl && (
<div className="results-main">
<h3 className="section-title">Results</h3>
<div className="comparison-images">
<div className="comparison-item">
<div className="comparison-label">Original</div>
<img src={inputPreviewUrl} alt="Original" className="comparison-image" />
</div>
<div className="comparison-item">
<div className="comparison-label">Enhanced</div>
<img src={outputPreviewUrl} alt="Enhanced" className="comparison-image" />
</div>
</div>
<div className="result-actions">
<button className="download-button" onClick={downloadEnhanced}>
⬇️ Download Enhanced
</button>
</div>
</div>
)}
</div>
<section className="losses-section">
<div className="losses-header">
<div>
<h3>Training Snapshot</h3>
<p className="losses-subtitle">Last epoch metrics (Source to Target samples)</p>
</div>
</div>
<div className="loss-grid">
<div className="loss-card">
<div className="loss-name">source_discr</div>
<div className="loss-value">0.31682008504867554</div>
<p className="loss-desc">
Discriminator loss for the source domain. Lower means better real/fake separation.
</p>
</div>
<div className="loss-card">
<div className="loss-name">target_discr</div>
<div className="loss-value">0.21157756447792053</div>
<p className="loss-desc">
Discriminator loss for the target domain. Lower means better real/fake separation.
</p>
</div>
<div className="loss-card">
<div className="loss-name">source_gener</div>
<div className="loss-value">0.7708375255266825</div>
<p className="loss-desc">
Generator adversarial loss for source to target mapping. Lower is better.
</p>
</div>
<div className="loss-card">
<div className="loss-name">target_gener</div>
<div className="loss-value">0.6412161688009897</div>
<p className="loss-desc">
Generator adversarial loss for target to source mapping. Lower is better.
</p>
</div>
<div className="loss-card">
<div className="loss-name">recon_target_loss</div>
<div className="loss-value">0.05315116047859192</div>
<p className="loss-desc">
Cycle reconstruction loss for target domain consistency. Lower means better cycle match.
</p>
</div>
<div className="loss-card">
<div className="loss-name">recon_source_loss</div>
<div className="loss-value">0.03360244259238243</div>
<p className="loss-desc">
Cycle reconstruction loss for source domain consistency. Lower means better cycle match.
</p>
</div>
<div className="loss-card">
<div className="loss-name">ident_target</div>
<div className="loss-value">0.09518671035766602</div>
<p className="loss-desc">
Identity loss on target images. Encourages keeping colors and structure when already in target domain.
</p>
</div>
<div className="loss-card">
<div className="loss-name">ident_source</div>
<div className="loss-value">0.049587182700634</div>
<p className="loss-desc">
Identity loss on source images. Encourages minimal change when already in source domain.
</p>
</div>
</div>
</section>
{/* Empty State */}
{!inputPreviewUrl && !outputPreviewUrl && !errorMessage && (
<div className="empty-state">
<div className="empty-icon">🌙</div>
<p>Ready to enhance your low-light photos?</p>
</div>
)}
</section>
{/* Footer */}
<footer className="app-footer">
<p>LumiGAN © 2026 • Advanced Image Enhancement</p>
</footer>
</main>
);
}
export default App;