Spaces:
Running
Running
File size: 5,918 Bytes
611bed4 370c714 67405d9 370c714 2965cdb 67405d9 370c714 67405d9 370c714 2965cdb 370c714 67405d9 2345a19 370c714 2965cdb 67405d9 2965cdb 370c714 67405d9 2965cdb 67405d9 2965cdb 370c714 2965cdb 370c714 67405d9 611bed4 370c714 611bed4 67405d9 c934d3b 67405d9 611bed4 67405d9 611bed4 370c714 611bed4 370c714 80d27de |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
import React, { useState, useRef, useEffect } from 'react';
import { Product, AppView } from './types';
import { PRODUCTS } from './constants';
import { generateTryOn } from './services/geminiService';
import Navigation from './components/Navigation';
import ProductCard from './components/ProductCard';
import CameraView from './components/CameraView';
import ResultView from './components/ResultView';
import { Sparkles, Timer } from 'lucide-react';
const STORAGE_KEY = 'stylegenie_last_req_v2';
const COOLDOWN_SECONDS = 120; // 2 minutes is safe for public shared keys
function App() {
const [currentView, setCurrentView] = useState<AppView>(AppView.CATALOG);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [userImage, setUserImage] = useState<string | null>(null);
const [generatedResult, setGeneratedResult] = useState<string | null>(null);
const [isGenerating, setIsGenerating] = useState(false);
const [generationError, setGenerationError] = useState<string | null>(null);
const [retryAfter, setRetryAfter] = useState<number | null>(null);
const [globalCooldown, setGlobalCooldown] = useState(0);
// Sync cooldown from localStorage to survive refreshes
useEffect(() => {
const updateCooldown = () => {
const last = localStorage.getItem(STORAGE_KEY);
if (last) {
const diff = (Date.now() - parseInt(last)) / 1000;
if (diff < COOLDOWN_SECONDS) {
setGlobalCooldown(Math.ceil(COOLDOWN_SECONDS - diff));
} else {
setGlobalCooldown(0);
}
}
};
updateCooldown();
const interval = setInterval(updateCooldown, 1000);
return () => clearInterval(interval);
}, []);
const handleSelectProduct = (product: Product) => {
if (globalCooldown > 0) return;
setSelectedProduct(product);
setCurrentView(AppView.CAMERA);
};
const handleBackToCatalog = () => {
setCurrentView(AppView.CATALOG);
setSelectedProduct(null);
setUserImage(null);
setGeneratedResult(null);
setGenerationError(null);
setRetryAfter(null);
};
const handleCapture = async (imageSrc: string) => {
setUserImage(imageSrc);
setCurrentView(AppView.RESULT);
if (selectedProduct) {
await processTryOn(imageSrc, selectedProduct);
}
};
const processTryOn = async (imageSrc: string, product: Product) => {
// Final check for cooldown
if (globalCooldown > 0) {
setRetryAfter(globalCooldown);
return;
}
setIsGenerating(true);
setGenerationError(null);
setRetryAfter(null);
// Set lock immediately to prevent race conditions
localStorage.setItem(STORAGE_KEY, Date.now().toString());
try {
const resultImage = await generateTryOn(imageSrc, product);
setGeneratedResult(resultImage);
} catch (error: any) {
const msg = error.message || "";
if (msg.includes("FREE_LIMIT_HIT") || msg.includes("429")) {
setRetryAfter(COOLDOWN_SECONDS);
} else {
setGenerationError(msg);
}
} finally {
setIsGenerating(false);
}
};
const handleRetake = () => {
setCurrentView(AppView.CAMERA);
setGeneratedResult(null);
setGenerationError(null);
setRetryAfter(null);
};
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900">
<Navigation
showBack={currentView !== AppView.CATALOG}
onBack={handleBackToCatalog}
cooldown={globalCooldown}
/>
{currentView === AppView.CATALOG && (
<main className="max-w-7xl mx-auto p-4 md:p-6 lg:p-8">
<div className="mb-8 text-center max-w-2xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 bg-brand-50 rounded-full border border-brand-100 mb-4">
<Sparkles className="w-4 h-4 text-brand-500" />
<span className="text-[10px] font-bold text-brand-600 uppercase tracking-widest">Optimized Free Mode</span>
</div>
<h1 className="text-3xl md:text-4xl font-bold text-gray-900 mb-4">StyleGenie Try-On</h1>
{globalCooldown > 0 ? (
<div className="flex flex-col items-center gap-2 mt-4 p-4 bg-amber-50 rounded-2xl border border-amber-100 animate-in fade-in zoom-in-95">
<Timer className="w-6 h-6 text-amber-500 animate-pulse" />
<p className="text-amber-700 font-bold">AI is Resting: {globalCooldown}s</p>
<p className="text-amber-600 text-xs">Free tier allows 1 generation every 2 minutes.</p>
</div>
) : (
<p className="text-gray-500 text-lg">Select an item below to see the magic happen.</p>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 md:gap-6">
{PRODUCTS.map(product => (
<ProductCard
key={product.id}
product={product}
onTryOn={handleSelectProduct}
disabled={globalCooldown > 0}
/>
))}
</div>
</main>
)}
{currentView === AppView.CAMERA && selectedProduct && (
<CameraView
product={selectedProduct}
onCapture={handleCapture}
onClose={handleBackToCatalog}
/>
)}
{currentView === AppView.RESULT && selectedProduct && userImage && (
<ResultView
product={selectedProduct}
originalImage={userImage}
generatedImage={generatedResult}
loading={isGenerating}
error={generationError}
retryAfter={retryAfter}
onRetake={handleRetake}
onRetry={() => processTryOn(userImage, selectedProduct)}
/>
)}
</div>
);
}
export default App; |