Akimitsujiro commited on
Commit
e194163
·
verified ·
1 Parent(s): e3cbcbe

Update src/App.jsx

Browse files
Files changed (1) hide show
  1. src/App.jsx +37 -65
src/App.jsx CHANGED
@@ -4,10 +4,10 @@ import {
4
  Settings, Layers, Cpu, Maximize, Zap, Wifi, WifiOff, ChevronDown, ChevronUp, Play, RefreshCw
5
  } from 'lucide-react';
6
 
7
- // ⚠️ CẤU HÌNH: DÁN LINK NGROK CỦA BẠN VÀO ĐÂY
8
- const API_URL = "pretty-dory-noble.ngrok-free.app";
 
9
 
10
- // Danh sách Sampler (Giữ nguyên vì đây là config cứng của Diffusers)
11
  const SAMPLERS = [
12
  "Euler a", "Euler", "LMS", "Heun", "DPM2", "DPM2 a",
13
  "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE",
@@ -15,33 +15,27 @@ const SAMPLERS = [
15
  ];
16
 
17
  const App = () => {
18
- // --- STATE ---
19
- // Core Inputs
20
  const [prompt, setPrompt] = useState('A futuristic city with neon lights, cyberpunk style');
21
  const [negPrompt, setNegPrompt] = useState('blurry, bad quality, watermark, text, ugly, distorted, nsfw');
22
 
23
- // Dynamic Lists (Dữ liệu từ Server)
24
  const [checkpoints, setCheckpoints] = useState(["Loading..."]);
25
  const [loras, setLoras] = useState(["None"]);
26
  const [vaes, setVaes] = useState(["Default"]);
27
  const [upscalers, setUpscalers] = useState(["None"]);
28
 
29
- // Selections
30
  const [selectedCheckpoint, setSelectedCheckpoint] = useState("");
31
  const [selectedLora, setSelectedLora] = useState("None");
32
  const [selectedVae, setSelectedVae] = useState("Default");
33
  const [selectedUpscaler, setSelectedUpscaler] = useState("None");
34
 
35
- // Advanced Settings
36
  const [steps, setSteps] = useState(30);
37
  const [cfgScale, setCfgScale] = useState(7.0);
38
  const [seed, setSeed] = useState(-1);
39
  const [sampler, setSampler] = useState("DPM++ 2M Karras");
40
  const [upscaleStrength, setUpscaleStrength] = useState(0.35);
41
- const [upscaleFactor, setUpscaleFactor] = useState(1.0); // 1.0 = No upscale
42
  const [showAdvanced, setShowAdvanced] = useState(false);
43
 
44
- // System
45
  const [generatedImage, setGeneratedImage] = useState(null);
46
  const [loading, setLoading] = useState(false);
47
  const [fetchingInfo, setFetchingInfo] = useState(false);
@@ -49,28 +43,32 @@ const App = () => {
49
  const [logs, setLogs] = useState([]);
50
  const [serverStatus, setServerStatus] = useState('unknown');
51
 
52
- // --- FUNCTIONS ---
53
  const addLog = (message) => {
54
  const timestamp = new Date().toLocaleTimeString();
55
  setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 50));
56
  };
57
 
58
- // Hàm lấy danh sách model từ Kaggle
59
  const fetchServerInfo = async () => {
60
  if (API_URL.includes("thay-link")) return;
61
 
62
  setFetchingInfo(true);
63
  try {
64
  addLog("Connecting to Kaggle Server...");
65
- const res = await fetch(`${API_URL}/info`);
 
 
 
 
 
 
 
66
  if (!res.ok) throw new Error("Server not ready");
67
 
68
  const data = await res.json();
69
 
70
- // Cập nhật danh sách
71
  if (data.models?.length > 0) {
72
  setCheckpoints(data.models);
73
- setSelectedCheckpoint(data.models[0]); // Auto select first
74
  }
75
  if (data.loras?.length > 0) setLoras(data.loras);
76
  if (data.vaes?.length > 0) setVaes(data.vaes);
@@ -88,7 +86,6 @@ const App = () => {
88
  }
89
  };
90
 
91
- // Auto fetch khi mount
92
  useEffect(() => {
93
  fetchServerInfo();
94
  }, []);
@@ -104,7 +101,6 @@ const App = () => {
104
  addLog(`Generating... ${selectedCheckpoint}`);
105
 
106
  try {
107
- // Logic Upscaler: Ưu tiên Model name, nếu ko chọn model thì dùng factor (High-Res Fix)
108
  let upscalerValue = "None";
109
  if (selectedUpscaler !== "None") {
110
  upscalerValue = selectedUpscaler;
@@ -128,7 +124,11 @@ const App = () => {
128
 
129
  const response = await fetch(`${API_URL}/generate`, {
130
  method: 'POST',
131
- headers: { 'Content-Type': 'application/json' },
 
 
 
 
132
  body: JSON.stringify(payload),
133
  });
134
 
@@ -149,10 +149,9 @@ const App = () => {
149
  };
150
 
151
  return (
152
- <div className="min-h-screen bg-slate-950 text-slate-200 font-sans">
153
-
154
  {/* Header */}
155
- <header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-md sticky top-0 z-50">
156
  <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
157
  <div className="flex items-center gap-3">
158
  <div className="bg-gradient-to-br from-indigo-500 to-purple-600 p-2 rounded-lg">
@@ -160,7 +159,6 @@ const App = () => {
160
  </div>
161
  <h1 className="font-bold text-lg text-white">Kaggle Studio <span className="text-purple-400">Pro</span></h1>
162
  </div>
163
-
164
  <div className="flex items-center gap-3">
165
  <button onClick={fetchServerInfo} className="p-2 hover:bg-slate-800 rounded-full transition-colors" title="Refresh Models">
166
  <RefreshCw className={`w-4 h-4 ${fetchingInfo ? 'animate-spin text-purple-400' : 'text-slate-400'}`} />
@@ -173,34 +171,27 @@ const App = () => {
173
  </div>
174
  </header>
175
 
176
- <main className="max-w-7xl mx-auto p-4 lg:p-6 grid lg:grid-cols-12 gap-6">
177
-
178
- {/* --- LEFT: CONTROLS --- */}
179
  <div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-5">
180
-
181
  {/* Models */}
182
  <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 space-y-4">
183
  <div>
184
- <label className="text-xs font-bold text-slate-400 uppercase mb-1 flex items-center gap-1">
185
- <Layers className="w-3 h-3" /> Checkpoint
186
- </label>
187
- <div className="relative">
188
- <select value={selectedCheckpoint} onChange={e => setSelectedCheckpoint(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-xs rounded-lg p-2.5 outline-none focus:border-purple-500">
189
- {checkpoints.map(m => <option key={m} value={m}>{m}</option>)}
190
- </select>
191
- </div>
192
  </div>
193
-
194
  <div className="grid grid-cols-2 gap-2">
195
  <div>
196
  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">LoRA</label>
197
- <select value={selectedLora} onChange={e => setSelectedLora(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-xs rounded-lg p-2 outline-none">
198
  {loras.map(m => <option key={m} value={m}>{m}</option>)}
199
  </select>
200
  </div>
201
  <div>
202
  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">VAE</label>
203
- <select value={selectedVae} onChange={e => setSelectedVae(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-xs rounded-lg p-2 outline-none">
204
  {vaes.map(m => <option key={m} value={m}>{m}</option>)}
205
  </select>
206
  </div>
@@ -209,51 +200,42 @@ const App = () => {
209
 
210
  {/* Prompt */}
211
  <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3">
212
- <textarea value={prompt} onChange={e => setPrompt(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-sm focus:border-purple-500 outline-none h-28 placeholder:text-slate-600" placeholder="Positive Prompt..." />
213
- <textarea value={negPrompt} onChange={e => setNegPrompt(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-xs focus:border-red-500 outline-none h-16 placeholder:text-slate-600" placeholder="Negative Prompt..." />
214
  </section>
215
 
216
- {/* Advanced Toggle */}
217
  <section className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
218
  <button onClick={() => setShowAdvanced(!showAdvanced)} className="w-full flex items-center justify-between p-3 hover:bg-slate-800/50 transition-colors text-slate-300">
219
  <span className="text-xs font-bold uppercase flex items-center gap-2"><Settings className="w-3 h-3" /> Advanced</span>
220
  {showAdvanced ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
221
  </button>
222
-
223
  {showAdvanced && (
224
  <div className="p-4 border-t border-slate-800 bg-slate-950/30 space-y-4">
225
- {/* Steps & CFG */}
226
  <div className="space-y-3">
227
  <div className="flex justify-between text-[10px] uppercase text-slate-500 font-bold"><span>Steps: {steps}</span><span>CFG: {cfgScale}</span></div>
228
  <input type="range" min="10" max="60" value={steps} onChange={e => setSteps(Number(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500" />
229
  <input type="range" min="1" max="20" step="0.5" value={cfgScale} onChange={e => setCfgScale(Number(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500" />
230
  </div>
231
-
232
- {/* Sampler & Seed */}
233
  <div className="grid grid-cols-2 gap-2">
234
  <div>
235
  <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Sampler</label>
236
- <select value={sampler} onChange={e => setSampler(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-[10px] rounded p-1.5 outline-none">
237
  {SAMPLERS.map(s => <option key={s} value={s}>{s}</option>)}
238
  </select>
239
  </div>
240
  <div>
241
- <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Seed (-1 = Random)</label>
242
- <input type="number" value={seed} onChange={e => setSeed(Number(e.target.value))} className="w-full bg-slate-950 border border-slate-700 text-[10px] rounded p-1.5 outline-none" />
243
  </div>
244
  </div>
245
-
246
- {/* Upscale */}
247
  <div className="pt-2 border-t border-slate-800/50">
248
- <label className="text-[10px] font-bold text-slate-400 uppercase mb-2 flex items-center gap-1"><Maximize className="w-3 h-3" /> Upscale (High-Res)</label>
249
-
250
- {/* Upscale Model Dropdown */}
251
  <div className="mb-2">
252
- <select value={selectedUpscaler} onChange={e => setSelectedUpscaler(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-[10px] rounded p-1.5 outline-none mb-1">
253
  {upscalers.map(u => <option key={u} value={u}>{u}</option>)}
254
  </select>
255
  </div>
256
-
257
  <div className="flex gap-2 mb-2">
258
  {[1.0, 1.5, 2.0].map(f => (
259
  <button key={f} onClick={() => setUpscaleFactor(f)} className={`flex-1 py-1 text-[10px] rounded border ${upscaleFactor === f ? 'bg-purple-600 border-purple-500 text-white' : 'bg-slate-950 border-slate-700 text-slate-400'}`}>
@@ -261,12 +243,6 @@ const App = () => {
261
  </button>
262
  ))}
263
  </div>
264
- {upscaleFactor > 1 && (
265
- <div>
266
- <label className="text-[10px] text-slate-500 block mb-1">Denoise Strength: {upscaleStrength}</label>
267
- <input type="range" min="0.1" max="0.5" step="0.05" value={upscaleStrength} onChange={e => setUpscaleStrength(Number(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500" />
268
- </div>
269
- )}
270
  </div>
271
  </div>
272
  )}
@@ -276,9 +252,7 @@ const App = () => {
276
  {loading ? <Loader2 className="animate-spin" /> : <Play className="fill-current" />}
277
  {loading ? 'GENERATING...' : 'GENERATE'}
278
  </button>
279
-
280
  {error && <div className="p-3 bg-red-900/20 border border-red-500/20 rounded-lg text-red-400 text-xs flex items-center gap-2"><AlertCircle className="w-4 h-4" />{error}</div>}
281
-
282
  </div>
283
 
284
  {/* --- RIGHT: PREVIEW --- */}
@@ -287,15 +261,13 @@ const App = () => {
287
  {!generatedImage && <div className="text-slate-600 flex flex-col items-center"><ImageIcon className="w-12 h-12 mb-2 opacity-20" /><span className="text-sm font-mono opacity-50">Ready to Imagine</span></div>}
288
  {generatedImage && <img src={generatedImage} alt="Result" className="max-w-full max-h-full object-contain shadow-2xl" />}
289
  </div>
290
-
291
  <div className="bg-black/40 border border-slate-800 rounded-xl p-3 h-32 overflow-y-auto font-mono text-[10px] text-slate-400">
292
  {logs.map((log, i) => <div key={i}>{log}</div>)}
293
  </div>
294
  </div>
295
-
296
  </main>
297
  </div>
298
  );
299
  };
300
 
301
- export default App;
 
4
  Settings, Layers, Cpu, Maximize, Zap, Wifi, WifiOff, ChevronDown, ChevronUp, Play, RefreshCw
5
  } from 'lucide-react';
6
 
7
+ // ⚠️ QUAN TRỌNG: Thay link Ngrok cố định của bạn vào đây
8
+ // dụ: https://pretty-dory-noble.ngrok-free.app
9
+ const API_URL = "https://thay-link-ngrok-cua-ban-vao-day.ngrok-free.app";
10
 
 
11
  const SAMPLERS = [
12
  "Euler a", "Euler", "LMS", "Heun", "DPM2", "DPM2 a",
13
  "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE",
 
15
  ];
16
 
17
  const App = () => {
 
 
18
  const [prompt, setPrompt] = useState('A futuristic city with neon lights, cyberpunk style');
19
  const [negPrompt, setNegPrompt] = useState('blurry, bad quality, watermark, text, ugly, distorted, nsfw');
20
 
 
21
  const [checkpoints, setCheckpoints] = useState(["Loading..."]);
22
  const [loras, setLoras] = useState(["None"]);
23
  const [vaes, setVaes] = useState(["Default"]);
24
  const [upscalers, setUpscalers] = useState(["None"]);
25
 
 
26
  const [selectedCheckpoint, setSelectedCheckpoint] = useState("");
27
  const [selectedLora, setSelectedLora] = useState("None");
28
  const [selectedVae, setSelectedVae] = useState("Default");
29
  const [selectedUpscaler, setSelectedUpscaler] = useState("None");
30
 
 
31
  const [steps, setSteps] = useState(30);
32
  const [cfgScale, setCfgScale] = useState(7.0);
33
  const [seed, setSeed] = useState(-1);
34
  const [sampler, setSampler] = useState("DPM++ 2M Karras");
35
  const [upscaleStrength, setUpscaleStrength] = useState(0.35);
36
+ const [upscaleFactor, setUpscaleFactor] = useState(1.0);
37
  const [showAdvanced, setShowAdvanced] = useState(false);
38
 
 
39
  const [generatedImage, setGeneratedImage] = useState(null);
40
  const [loading, setLoading] = useState(false);
41
  const [fetchingInfo, setFetchingInfo] = useState(false);
 
43
  const [logs, setLogs] = useState([]);
44
  const [serverStatus, setServerStatus] = useState('unknown');
45
 
 
46
  const addLog = (message) => {
47
  const timestamp = new Date().toLocaleTimeString();
48
  setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 50));
49
  };
50
 
 
51
  const fetchServerInfo = async () => {
52
  if (API_URL.includes("thay-link")) return;
53
 
54
  setFetchingInfo(true);
55
  try {
56
  addLog("Connecting to Kaggle Server...");
57
+
58
+ // 👇 THÊM HEADER NÀY ĐỂ BỎ QUA MÀN HÌNH CẢNH BÁO CỦA NGROK
59
+ const res = await fetch(`${API_URL}/info`, {
60
+ headers: {
61
+ "ngrok-skip-browser-warning": "69420",
62
+ }
63
+ });
64
+
65
  if (!res.ok) throw new Error("Server not ready");
66
 
67
  const data = await res.json();
68
 
 
69
  if (data.models?.length > 0) {
70
  setCheckpoints(data.models);
71
+ setSelectedCheckpoint(data.models[0]);
72
  }
73
  if (data.loras?.length > 0) setLoras(data.loras);
74
  if (data.vaes?.length > 0) setVaes(data.vaes);
 
86
  }
87
  };
88
 
 
89
  useEffect(() => {
90
  fetchServerInfo();
91
  }, []);
 
101
  addLog(`Generating... ${selectedCheckpoint}`);
102
 
103
  try {
 
104
  let upscalerValue = "None";
105
  if (selectedUpscaler !== "None") {
106
  upscalerValue = selectedUpscaler;
 
124
 
125
  const response = await fetch(`${API_URL}/generate`, {
126
  method: 'POST',
127
+ // 👇 THÊM HEADER VÀO CẢ CHỖ NÀY NỮA
128
+ headers: {
129
+ 'Content-Type': 'application/json',
130
+ "ngrok-skip-browser-warning": "69420",
131
+ },
132
  body: JSON.stringify(payload),
133
  });
134
 
 
149
  };
150
 
151
  return (
152
+ <div className="min-h-screen bg-slate-950 text-slate-200 font-sans p-4">
 
153
  {/* Header */}
154
+ <header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-md sticky top-0 z-50 mb-6 rounded-xl">
155
  <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
156
  <div className="flex items-center gap-3">
157
  <div className="bg-gradient-to-br from-indigo-500 to-purple-600 p-2 rounded-lg">
 
159
  </div>
160
  <h1 className="font-bold text-lg text-white">Kaggle Studio <span className="text-purple-400">Pro</span></h1>
161
  </div>
 
162
  <div className="flex items-center gap-3">
163
  <button onClick={fetchServerInfo} className="p-2 hover:bg-slate-800 rounded-full transition-colors" title="Refresh Models">
164
  <RefreshCw className={`w-4 h-4 ${fetchingInfo ? 'animate-spin text-purple-400' : 'text-slate-400'}`} />
 
171
  </div>
172
  </header>
173
 
174
+ <main className="max-w-7xl mx-auto grid lg:grid-cols-12 gap-6">
175
+ {/* LEFT: CONTROLS */}
 
176
  <div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-5">
 
177
  {/* Models */}
178
  <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 space-y-4">
179
  <div>
180
+ <label className="text-xs font-bold text-slate-400 uppercase mb-1 flex items-center gap-1"><Layers className="w-3 h-3" /> Checkpoint</label>
181
+ <select value={selectedCheckpoint} onChange={e => setSelectedCheckpoint(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-xs rounded-lg p-2.5 outline-none focus:border-purple-500 text-slate-200">
182
+ {checkpoints.map(m => <option key={m} value={m}>{m}</option>)}
183
+ </select>
 
 
 
 
184
  </div>
 
185
  <div className="grid grid-cols-2 gap-2">
186
  <div>
187
  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">LoRA</label>
188
+ <select value={selectedLora} onChange={e => setSelectedLora(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-xs rounded-lg p-2 outline-none text-slate-200">
189
  {loras.map(m => <option key={m} value={m}>{m}</option>)}
190
  </select>
191
  </div>
192
  <div>
193
  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">VAE</label>
194
+ <select value={selectedVae} onChange={e => setSelectedVae(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-xs rounded-lg p-2 outline-none text-slate-200">
195
  {vaes.map(m => <option key={m} value={m}>{m}</option>)}
196
  </select>
197
  </div>
 
200
 
201
  {/* Prompt */}
202
  <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3">
203
+ <textarea value={prompt} onChange={e => setPrompt(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-sm focus:border-purple-500 outline-none h-28 placeholder:text-slate-600 text-slate-200" placeholder="Positive Prompt..." />
204
+ <textarea value={negPrompt} onChange={e => setNegPrompt(e.target.value)} className="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-xs focus:border-red-500 outline-none h-16 placeholder:text-slate-600 text-slate-200" placeholder="Negative Prompt..." />
205
  </section>
206
 
207
+ {/* Advanced */}
208
  <section className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
209
  <button onClick={() => setShowAdvanced(!showAdvanced)} className="w-full flex items-center justify-between p-3 hover:bg-slate-800/50 transition-colors text-slate-300">
210
  <span className="text-xs font-bold uppercase flex items-center gap-2"><Settings className="w-3 h-3" /> Advanced</span>
211
  {showAdvanced ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
212
  </button>
 
213
  {showAdvanced && (
214
  <div className="p-4 border-t border-slate-800 bg-slate-950/30 space-y-4">
 
215
  <div className="space-y-3">
216
  <div className="flex justify-between text-[10px] uppercase text-slate-500 font-bold"><span>Steps: {steps}</span><span>CFG: {cfgScale}</span></div>
217
  <input type="range" min="10" max="60" value={steps} onChange={e => setSteps(Number(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500" />
218
  <input type="range" min="1" max="20" step="0.5" value={cfgScale} onChange={e => setCfgScale(Number(e.target.value))} className="w-full h-1 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500" />
219
  </div>
 
 
220
  <div className="grid grid-cols-2 gap-2">
221
  <div>
222
  <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Sampler</label>
223
+ <select value={sampler} onChange={e => setSampler(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-[10px] rounded p-1.5 outline-none text-slate-200">
224
  {SAMPLERS.map(s => <option key={s} value={s}>{s}</option>)}
225
  </select>
226
  </div>
227
  <div>
228
+ <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Seed</label>
229
+ <input type="number" value={seed} onChange={e => setSeed(Number(e.target.value))} className="w-full bg-slate-950 border border-slate-700 text-[10px] rounded p-1.5 outline-none text-slate-200" />
230
  </div>
231
  </div>
 
 
232
  <div className="pt-2 border-t border-slate-800/50">
233
+ <label className="text-[10px] font-bold text-slate-400 uppercase mb-2 flex items-center gap-1"><Maximize className="w-3 h-3" /> Upscale</label>
 
 
234
  <div className="mb-2">
235
+ <select value={selectedUpscaler} onChange={e => setSelectedUpscaler(e.target.value)} className="w-full bg-slate-950 border border-slate-700 text-[10px] rounded p-1.5 outline-none mb-1 text-slate-200">
236
  {upscalers.map(u => <option key={u} value={u}>{u}</option>)}
237
  </select>
238
  </div>
 
239
  <div className="flex gap-2 mb-2">
240
  {[1.0, 1.5, 2.0].map(f => (
241
  <button key={f} onClick={() => setUpscaleFactor(f)} className={`flex-1 py-1 text-[10px] rounded border ${upscaleFactor === f ? 'bg-purple-600 border-purple-500 text-white' : 'bg-slate-950 border-slate-700 text-slate-400'}`}>
 
243
  </button>
244
  ))}
245
  </div>
 
 
 
 
 
 
246
  </div>
247
  </div>
248
  )}
 
252
  {loading ? <Loader2 className="animate-spin" /> : <Play className="fill-current" />}
253
  {loading ? 'GENERATING...' : 'GENERATE'}
254
  </button>
 
255
  {error && <div className="p-3 bg-red-900/20 border border-red-500/20 rounded-lg text-red-400 text-xs flex items-center gap-2"><AlertCircle className="w-4 h-4" />{error}</div>}
 
256
  </div>
257
 
258
  {/* --- RIGHT: PREVIEW --- */}
 
261
  {!generatedImage && <div className="text-slate-600 flex flex-col items-center"><ImageIcon className="w-12 h-12 mb-2 opacity-20" /><span className="text-sm font-mono opacity-50">Ready to Imagine</span></div>}
262
  {generatedImage && <img src={generatedImage} alt="Result" className="max-w-full max-h-full object-contain shadow-2xl" />}
263
  </div>
 
264
  <div className="bg-black/40 border border-slate-800 rounded-xl p-3 h-32 overflow-y-auto font-mono text-[10px] text-slate-400">
265
  {logs.map((log, i) => <div key={i}>{log}</div>)}
266
  </div>
267
  </div>
 
268
  </main>
269
  </div>
270
  );
271
  };
272
 
273
+ export default App;