-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathApp.tsx
More file actions
205 lines (178 loc) · 8.28 KB
/
Copy pathApp.tsx
File metadata and controls
205 lines (178 loc) · 8.28 KB
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
import React, { useState } from 'react';
import { ImageUploader } from './components/ImageUploader';
import { PromptControls } from './components/PromptControls';
import { ImageGallery } from './components/ImageGallery';
import { GeneratedImage, GenerationSettings, AspectRatio } from './types';
import { generateProductImage } from './services/geminiService';
import { resizeImage } from './utils/fileUtils';
import { Sparkles, Camera, AlertCircle } from 'lucide-react';
const App = () => {
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [generatedImages, setGeneratedImages] = useState<GeneratedImage[]>([]);
const [isGenerating, setIsGenerating] = useState(false);
const [shouldStop, setShouldStop] = useState(false);
const [error, setError] = useState<string | null>(null);
const [settings, setSettings] = useState<GenerationSettings>({
customPrompt: '',
preset: null,
aspectRatio: AspectRatio.SQUARE,
customWidth: '1080',
customHeight: '1080'
});
const handleStop = () => {
setShouldStop(true);
};
const handleGenerate = async () => {
if (!selectedImage) return;
setIsGenerating(true);
setShouldStop(false);
setError(null);
setGeneratedImages([]); // Clear previous results
const promptToUse = settings.customPrompt.trim() || "Professional product photography, studio lighting, high resolution, 4k, clean background";
let apiAspectRatio = settings.aspectRatio;
let customW = 1080;
let customH = 1080;
if (apiAspectRatio === AspectRatio.CUSTOM) {
customW = parseInt(settings.customWidth || '1080', 10) || 1080;
customH = parseInt(settings.customHeight || '1080', 10) || 1080;
const targetRatioNum = customW / customH;
let closestRatioStr = AspectRatio.SQUARE;
let minDiff = Infinity;
const standardRatios = {
[AspectRatio.SQUARE]: 1,
[AspectRatio.PORTRAIT]: 3/4,
[AspectRatio.LANDSCAPE]: 4/3,
[AspectRatio.TALL]: 9/16
};
for (const [ratioStr, ratioVal] of Object.entries(standardRatios)) {
const diff = Math.abs(ratioVal - targetRatioNum);
if (diff < minDiff) {
minDiff = diff;
closestRatioStr = ratioStr as AspectRatio;
}
}
apiAspectRatio = closestRatioStr;
}
// Execute requests sequentially to avoid hitting Gemini API rate limits (429 Resource Exhausted)
const TARGET_COUNT = 4;
for (let i = 0; i < TARGET_COUNT; i++) {
// Check if we should stop before each request
if (shouldStop) break;
try {
let url = await generateProductImage({
imageBase64: selectedImage,
prompt: promptToUse,
aspectRatio: apiAspectRatio
});
// Check again after the request in case it was stopped during the wait
if (shouldStop) break;
if (url) {
// Apply custom resizing if user asked for a custom size
if (settings.aspectRatio === AspectRatio.CUSTOM) {
url = await resizeImage(url, customW, customH);
}
const newImage: GeneratedImage = {
id: Math.random().toString(36).substr(2, 9),
url,
promptUsed: promptToUse,
createdAt: Date.now()
};
// Add image to state immediately as it becomes available
setGeneratedImages(prev => [...prev, newImage]);
}
} catch (e: any) {
console.error(`Generation ${i + 1} failed`, e);
const errorMessage = e?.message || '';
if (errorMessage.includes('429') || errorMessage.includes('quota') || errorMessage.includes('RESOURCE_EXHAUSTED')) {
setError("API quota exceeded or rate limit reached. Please check your Gemini API plan and billing details.");
break; // Stop trying to generate more if we hit a rate limit/quota
} else if (errorMessage.includes('500') || errorMessage.includes('UNKNOWN') || errorMessage.includes('xhr error')) {
setError(`API Service Error: ${errorMessage || 'Unknown error'}`);
break; // Stop on backend/proxy failures
} else {
setError(`Generation failed: ${errorMessage || 'Unknown error'}`);
// Continue to next attempt for other potential transient errors
}
}
// Add a small delay between requests to be respectful of rate limits,
// unless it's the last request.
if (i < TARGET_COUNT - 1) {
// Use a promise that can be "interrupted" or just check shouldStop after the wait
await new Promise(resolve => setTimeout(resolve, 500)); // 500ms delay
}
}
setIsGenerating(false);
setShouldStop(false);
};
return (
<div className="min-h-screen bg-slate-50 text-slate-900 pb-20">
{/* Header */}
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-blue-600 to-indigo-600 rounded-lg flex items-center justify-center text-white">
<Camera className="w-5 h-5" />
</div>
<h1 className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-700 to-indigo-700">
EcomLens AI
</h1>
</div>
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-gray-500 hidden sm:inline-block">Powered by Gemini Flash</span>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Left Column: Controls */}
<div className="lg:col-span-4 space-y-6">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-100">
<h2 className="text-lg font-bold text-gray-900 mb-4 flex items-center gap-2">
<Sparkles className="w-5 h-5 text-indigo-500" />
Input Source
</h2>
<ImageUploader
selectedImage={selectedImage}
onImageSelect={setSelectedImage}
onClear={() => {
setSelectedImage(null);
setGeneratedImages([]);
setError(null);
}}
/>
</div>
<PromptControls
settings={settings}
onChange={setSettings}
isGenerating={isGenerating}
onGenerate={handleGenerate}
onStop={handleStop}
hasImage={!!selectedImage}
/>
</div>
{/* Right Column: Results */}
<div className="lg:col-span-8">
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl text-red-700 flex items-start gap-3">
<AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
<p className="text-sm">{error}</p>
</div>
)}
{generatedImages.length > 0 || isGenerating ? (
<ImageGallery images={generatedImages} isGenerating={isGenerating} />
) : (
<div className="h-full min-h-[400px] flex flex-col items-center justify-center bg-white rounded-2xl border border-dashed border-gray-300 p-8 text-center text-gray-400">
<div className="w-16 h-16 bg-gray-50 rounded-full flex items-center justify-center mb-4">
<Camera className="w-8 h-8 text-gray-300" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">Ready to Create?</h3>
<p className="max-w-md mx-auto">Upload a product photo and select a style to generate professional e-commerce variations instantly.</p>
</div>
)}
</div>
</div>
</main>
</div>
);
};
export default App;