Spaces:
Sleeping
Sleeping
| 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; | |