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

Update src/App.jsx

Browse files
Files changed (1) hide show
  1. src/App.jsx +145 -66
src/App.jsx CHANGED
@@ -1,12 +1,11 @@
1
  import React, { useState, useEffect } from 'react';
2
  import {
3
  Image as ImageIcon, Loader2, Sparkles, Terminal, AlertCircle,
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",
@@ -15,6 +14,10 @@ const SAMPLERS = [
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
 
@@ -41,28 +44,42 @@ const App = () => {
41
  const [fetchingInfo, setFetchingInfo] = useState(false);
42
  const [error, setError] = useState('');
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
 
@@ -75,56 +92,46 @@ const App = () => {
75
  if (data.upscalers?.length > 0) setUpscalers(data.upscalers);
76
 
77
  setServerStatus('connected');
78
- addLog(`Connected! Found ${data.models.length} checkpoints.`);
79
 
80
  } catch (err) {
81
  console.error(err);
82
  setServerStatus('disconnected');
83
- addLog("Failed to fetch model list. Is server running?");
 
84
  } finally {
85
  setFetchingInfo(false);
86
  }
87
  };
88
 
 
89
  useEffect(() => {
90
- fetchServerInfo();
91
  }, []);
92
 
93
  const handleGenerate = async () => {
94
- if (serverStatus === 'disconnected') {
95
- setError('Chưa kết nối được Server!');
96
  return;
97
  }
98
 
99
  setLoading(true);
100
  setError('');
101
- addLog(`Generating... ${selectedCheckpoint}`);
102
 
103
  try {
104
  let upscalerValue = "None";
105
- if (selectedUpscaler !== "None") {
106
- upscalerValue = selectedUpscaler;
107
- } else if (upscaleFactor > 1) {
108
- upscalerValue = upscaleFactor.toString();
109
- }
110
 
111
  const payload = {
112
- prompt: prompt,
113
- negative_prompt: negPrompt,
114
- steps: steps,
115
- cfg_scale: cfgScale,
116
- seed: seed,
117
- sampler_name: sampler,
118
- checkpoint: selectedCheckpoint,
119
- lora: selectedLora,
120
- vae: selectedVae,
121
- upscaler: upscalerValue,
122
- upscale_strength: upscaleStrength
123
  };
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",
@@ -137,7 +144,7 @@ const App = () => {
137
 
138
  if (data.image) {
139
  setGeneratedImage(data.image);
140
- addLog('Image finished!');
141
  }
142
 
143
  } catch (err) {
@@ -149,21 +156,20 @@ const App = () => {
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">
158
  <Sparkles className="w-5 h-5 text-white" />
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'}`} />
165
- </button>
166
- <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full border ${serverStatus === 'connected' ? 'bg-green-500/10 border-green-500/20 text-green-400' : 'bg-red-500/10 border-red-500/20 text-red-400'}`}>
167
  {serverStatus === 'connected' ? <Wifi className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
168
  <span className="text-xs font-bold">{serverStatus === 'connected' ? 'ONLINE' : 'OFFLINE'}</span>
169
  </div>
@@ -172,16 +178,47 @@ const App = () => {
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>
@@ -199,24 +236,28 @@ const App = () => {
199
  </section>
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>
@@ -229,42 +270,80 @@ const App = () => {
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'}`}>
242
  {f === 1.0 ? 'Off' : `${f}x`}
243
  </button>
244
  ))}
245
  </div>
 
 
 
 
 
 
246
  </div>
247
  </div>
248
  )}
249
  </section>
250
 
251
- <button onClick={handleGenerate} disabled={loading} className={`w-full py-3 rounded-xl font-bold text-white shadow-lg flex items-center justify-center gap-2 ${loading ? 'bg-slate-800 opacity-50' : 'bg-gradient-to-r from-purple-600 to-pink-600 hover:scale-[0.98]'}`}>
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 --- */}
259
  <div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-4">
260
- <div className="bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-center min-h-[500px] relative overflow-hidden">
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
  );
 
1
  import React, { useState, useEffect } from 'react';
2
  import {
3
  Image as ImageIcon, Loader2, Sparkles, Terminal, AlertCircle,
4
+ Settings, Layers, Cpu, Maximize, Zap, Wifi, WifiOff, ChevronDown, ChevronUp, Play, RefreshCw, Link as LinkIcon
5
  } from 'lucide-react';
6
 
7
+ // Link mặc định (Nếu sai, bạn thể nhập đè trên giao diện web)
8
+ const DEFAULT_API_URL = "https://thay-link-ngrok-cua-ban-vao-day.ngrok-free.app";
 
9
 
10
  const SAMPLERS = [
11
  "Euler a", "Euler", "LMS", "Heun", "DPM2", "DPM2 a",
 
14
  ];
15
 
16
  const App = () => {
17
+ // --- STATE ---
18
+ const [apiUrl, setApiUrl] = useState(DEFAULT_API_URL);
19
+ const [isCustomUrl, setIsCustomUrl] = useState(false); // Check nếu user đang nhập tay
20
+
21
  const [prompt, setPrompt] = useState('A futuristic city with neon lights, cyberpunk style');
22
  const [negPrompt, setNegPrompt] = useState('blurry, bad quality, watermark, text, ugly, distorted, nsfw');
23
 
 
44
  const [fetchingInfo, setFetchingInfo] = useState(false);
45
  const [error, setError] = useState('');
46
  const [logs, setLogs] = useState([]);
47
+ const [serverStatus, setServerStatus] = useState('unknown'); // unknown, connected, disconnected
48
 
49
+ // --- FUNCTIONS ---
50
  const addLog = (message) => {
51
  const timestamp = new Date().toLocaleTimeString();
52
  setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 50));
53
  };
54
 
55
+ // Hàm kết nối server (dùng cho cả lúc mới vào và lúc bấm nút Connect)
56
+ const connectToServer = async (urlToConnect) => {
57
+ const targetUrl = urlToConnect || apiUrl;
58
 
59
+ // Nếu vẫn là link mặc định chưa sửa thì bỏ qua
60
+ if (targetUrl.includes("thay-link")) {
61
+ setServerStatus('disconnected');
62
+ return;
63
+ }
64
+
65
+ // Clean URL (bỏ dấu / ở cuối nếu có)
66
+ const cleanUrl = targetUrl.replace(/\/$/, "");
67
+ if (cleanUrl !== apiUrl) setApiUrl(cleanUrl);
68
+
69
  setFetchingInfo(true);
70
+ setError('');
71
+
72
  try {
73
+ addLog(`Connecting to: ${cleanUrl}...`);
74
 
75
+ const res = await fetch(`${cleanUrl}/info`, {
76
+ headers: { "ngrok-skip-browser-warning": "69420" }
 
 
 
77
  });
78
 
79
+ if (!res.ok) {
80
+ const text = await res.text();
81
+ throw new Error(`Server Error (${res.status}): ${text.slice(0, 50)}...`);
82
+ }
83
 
84
  const data = await res.json();
85
 
 
92
  if (data.upscalers?.length > 0) setUpscalers(data.upscalers);
93
 
94
  setServerStatus('connected');
95
+ addLog(`✅ Connected! Found ${data.models.length} checkpoints.`);
96
 
97
  } catch (err) {
98
  console.error(err);
99
  setServerStatus('disconnected');
100
+ setError(err.message);
101
+ addLog(`❌ Connection Failed: ${err.message}`);
102
  } finally {
103
  setFetchingInfo(false);
104
  }
105
  };
106
 
107
+ // Auto connect khi mới vào
108
  useEffect(() => {
109
+ connectToServer();
110
  }, []);
111
 
112
  const handleGenerate = async () => {
113
+ if (serverStatus !== 'connected') {
114
+ setError('Please connect to server first!');
115
  return;
116
  }
117
 
118
  setLoading(true);
119
  setError('');
120
+ addLog(`🎨 Generating...`);
121
 
122
  try {
123
  let upscalerValue = "None";
124
+ if (selectedUpscaler !== "None") upscalerValue = selectedUpscaler;
125
+ else if (upscaleFactor > 1) upscalerValue = upscaleFactor.toString();
 
 
 
126
 
127
  const payload = {
128
+ prompt, negative_prompt: negPrompt, steps, cfg_scale: cfgScale, seed,
129
+ sampler_name: sampler, checkpoint: selectedCheckpoint, lora: selectedLora,
130
+ vae: selectedVae, upscaler: upscalerValue, upscale_strength: upscaleStrength
 
 
 
 
 
 
 
 
131
  };
132
 
133
+ const response = await fetch(`${apiUrl}/generate`, {
134
  method: 'POST',
 
135
  headers: {
136
  'Content-Type': 'application/json',
137
  "ngrok-skip-browser-warning": "69420",
 
144
 
145
  if (data.image) {
146
  setGeneratedImage(data.image);
147
+ addLog('Image finished!');
148
  }
149
 
150
  } catch (err) {
 
156
  };
157
 
158
  return (
159
+ <div className="min-h-screen bg-slate-950 text-slate-200 font-sans p-4 selection:bg-purple-500/30">
160
+
161
  {/* Header */}
162
+ <header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-md sticky top-0 z-50 mb-6 rounded-xl shadow-lg">
163
  <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
164
  <div className="flex items-center gap-3">
165
+ <div className="bg-gradient-to-br from-indigo-500 to-purple-600 p-2 rounded-lg shadow-inner">
166
  <Sparkles className="w-5 h-5 text-white" />
167
  </div>
168
+ <h1 className="font-bold text-lg text-white tracking-tight">Kaggle Studio <span className="text-purple-400">Pro</span></h1>
169
  </div>
170
+
171
  <div className="flex items-center gap-3">
172
+ <div className={`flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all ${serverStatus === 'connected' ? 'bg-green-500/10 border-green-500/20 text-green-400' : 'bg-red-500/10 border-red-500/20 text-red-400'}`}>
 
 
 
173
  {serverStatus === 'connected' ? <Wifi className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
174
  <span className="text-xs font-bold">{serverStatus === 'connected' ? 'ONLINE' : 'OFFLINE'}</span>
175
  </div>
 
178
  </header>
179
 
180
  <main className="max-w-7xl mx-auto grid lg:grid-cols-12 gap-6">
181
+
182
+ {/* --- LEFT: CONTROLS --- */}
183
  <div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-5">
184
+
185
+ {/* 🔴 CONNECTION BOX (Hiện khi mất kết nối) */}
186
+ {serverStatus !== 'connected' && (
187
+ <section className="bg-red-900/10 border border-red-500/30 rounded-xl p-4 animate-in fade-in slide-in-from-top-2">
188
+ <label className="text-xs font-bold text-red-400 uppercase mb-2 flex items-center gap-1">
189
+ <LinkIcon className="w-3 h-3" /> Server Connection
190
+ </label>
191
+ <div className="flex gap-2">
192
+ <input
193
+ type="text"
194
+ value={apiUrl}
195
+ onChange={(e) => setApiUrl(e.target.value)}
196
+ placeholder="https://xxx.ngrok-free.app"
197
+ className="flex-1 bg-slate-950 border border-slate-700 text-[10px] rounded-lg px-2 py-2 outline-none focus:border-red-500 font-mono"
198
+ />
199
+ <button
200
+ onClick={() => connectToServer()}
201
+ disabled={fetchingInfo}
202
+ className="bg-red-500/20 hover:bg-red-500/30 text-red-400 border border-red-500/50 rounded-lg px-3 py-2 transition-colors"
203
+ >
204
+ {fetchingInfo ? <Loader2 className="w-4 h-4 animate-spin" /> : <RefreshCw className="w-4 h-4" />}
205
+ </button>
206
+ </div>
207
+ <p className="text-[10px] text-slate-500 mt-2">Paste link Ngrok từ Kaggle vào đây và bấm Connect.</p>
208
+ </section>
209
+ )}
210
+
211
  {/* Models */}
212
+ <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 space-y-4 shadow-sm">
213
  <div>
214
+ <label className="text-xs font-bold text-slate-400 uppercase mb-1 flex items-center gap-1">
215
+ <Layers className="w-3 h-3" /> Checkpoint
216
+ </label>
217
+ <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 transition-all">
218
  {checkpoints.map(m => <option key={m} value={m}>{m}</option>)}
219
  </select>
220
  </div>
221
+
222
  <div className="grid grid-cols-2 gap-2">
223
  <div>
224
  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">LoRA</label>
 
236
  </section>
237
 
238
  {/* Prompt */}
239
+ <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3 shadow-sm">
240
+ <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 resize-none transition-all" placeholder="Positive Prompt..." />
241
+ <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 resize-none transition-all" placeholder="Negative Prompt..." />
242
  </section>
243
 
244
+ {/* Advanced Toggle */}
245
+ <section className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-sm">
246
  <button onClick={() => setShowAdvanced(!showAdvanced)} className="w-full flex items-center justify-between p-3 hover:bg-slate-800/50 transition-colors text-slate-300">
247
  <span className="text-xs font-bold uppercase flex items-center gap-2"><Settings className="w-3 h-3" /> Advanced</span>
248
  {showAdvanced ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
249
  </button>
250
+
251
  {showAdvanced && (
252
+ <div className="p-4 border-t border-slate-800 bg-slate-950/30 space-y-4 animate-in slide-in-from-top-2">
253
+ {/* Steps & CFG */}
254
  <div className="space-y-3">
255
  <div className="flex justify-between text-[10px] uppercase text-slate-500 font-bold"><span>Steps: {steps}</span><span>CFG: {cfgScale}</span></div>
256
  <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" />
257
  <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" />
258
  </div>
259
+
260
+ {/* Sampler & Seed */}
261
  <div className="grid grid-cols-2 gap-2">
262
  <div>
263
  <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Sampler</label>
 
270
  <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" />
271
  </div>
272
  </div>
273
+
274
+ {/* Upscale */}
275
  <div className="pt-2 border-t border-slate-800/50">
276
+ <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>
277
+
278
  <div className="mb-2">
279
  <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">
280
  {upscalers.map(u => <option key={u} value={u}>{u}</option>)}
281
  </select>
282
  </div>
283
+
284
  <div className="flex gap-2 mb-2">
285
  {[1.0, 1.5, 2.0].map(f => (
286
+ <button key={f} onClick={() => setUpscaleFactor(f)} className={`flex-1 py-1 text-[10px] rounded border transition-colors ${upscaleFactor === f ? 'bg-purple-600 border-purple-500 text-white' : 'bg-slate-950 border-slate-700 text-slate-400 hover:bg-slate-800'}`}>
287
  {f === 1.0 ? 'Off' : `${f}x`}
288
  </button>
289
  ))}
290
  </div>
291
+ {upscaleFactor > 1 && (
292
+ <div>
293
+ <label className="text-[10px] text-slate-500 block mb-1">Denoise Strength: {upscaleStrength}</label>
294
+ <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" />
295
+ </div>
296
+ )}
297
  </div>
298
  </div>
299
  )}
300
  </section>
301
 
302
+ <button onClick={handleGenerate} disabled={loading} className={`w-full py-3 rounded-xl font-bold text-white shadow-lg flex items-center justify-center gap-2 transition-all ${loading ? 'bg-slate-800 opacity-50 cursor-not-allowed' : 'bg-gradient-to-r from-purple-600 to-pink-600 hover:scale-[0.98] hover:shadow-purple-500/25'}`}>
303
  {loading ? <Loader2 className="animate-spin" /> : <Play className="fill-current" />}
304
  {loading ? 'GENERATING...' : 'GENERATE'}
305
  </button>
306
+
307
+ {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 animate-in fade-in slide-in-from-top-1"><AlertCircle className="w-4 h-4 shrink-0" />{error}</div>}
308
+
309
  </div>
310
 
311
  {/* --- RIGHT: PREVIEW --- */}
312
  <div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-4">
313
+ <div className="bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-center min-h-[500px] relative overflow-hidden shadow-2xl">
314
+ {/* Background Grid */}
315
+ <div className="absolute inset-0 opacity-20 pointer-events-none"
316
+ style={{backgroundImage: 'radial-gradient(#4f46e5 1px, transparent 1px)', backgroundSize: '24px 24px'}}>
317
+ </div>
318
+
319
+ {!generatedImage && <div className="text-slate-600 flex flex-col items-center z-10"><ImageIcon className="w-16 h-16 mb-4 opacity-20" /><span className="text-sm font-mono opacity-50">Ready to Imagine</span></div>}
320
+
321
+ {generatedImage && (
322
+ <div className="relative w-full h-full p-2 z-10">
323
+ <img src={generatedImage} alt="Result" className="w-full h-full object-contain rounded-lg shadow-lg animate-in zoom-in duration-300" />
324
+ <a href={generatedImage} download="image.png" className="absolute top-4 right-4 bg-slate-900/80 text-white p-2 rounded-lg hover:bg-purple-600 transition-colors border border-slate-700"><Maximize className="w-5 h-5"/></a>
325
+ </div>
326
+ )}
327
+
328
+ {/* Loading Overlay */}
329
+ {loading && (
330
+ <div className="absolute inset-0 bg-slate-950/80 backdrop-blur-sm z-20 flex flex-col items-center justify-center">
331
+ <div className="w-16 h-16 border-4 border-slate-700 border-t-purple-500 rounded-full animate-spin"></div>
332
+ <p className="mt-4 text-purple-400 text-sm font-mono animate-pulse">Diffusion Process Running...</p>
333
+ </div>
334
+ )}
335
  </div>
336
+
337
+ <div className="bg-black rounded-xl border border-slate-800 p-3 h-36 overflow-y-auto font-mono text-[10px] text-slate-400 scrollbar-thin scrollbar-thumb-slate-800">
338
+ <div className="flex items-center gap-2 mb-2 pb-2 border-b border-slate-900 text-slate-500 font-bold uppercase"><Terminal className="w-3 h-3" /> System Logs</div>
339
+ {logs.map((log, i) => (
340
+ <div key={i} className="hover:text-slate-200 transition-colors py-0.5 border-l-2 border-transparent hover:border-purple-500 pl-2">
341
+ <span className="opacity-30 mr-2">{i+1}</span>{log}
342
+ </div>
343
+ ))}
344
  </div>
345
  </div>
346
+
347
  </main>
348
  </div>
349
  );