Akimitsujiro commited on
Commit
c3cc66d
·
verified ·
1 Parent(s): fcabceb

Update src/App.js

Browse files
Files changed (1) hide show
  1. src/App.js +294 -18
src/App.js CHANGED
@@ -1,25 +1,301 @@
1
- import logo from './logo.svg';
2
- import './App.css';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
- function App() {
5
  return (
6
- <div className="App">
7
- <header className="App-header">
8
- <img src={logo} className="App-logo" alt="logo" />
9
- <p>
10
- Edit <code>src/App.js</code> and save to reload.
11
- </p>
12
- <a
13
- className="App-link"
14
- href="https://reactjs.org"
15
- target="_blank"
16
- rel="noopener noreferrer"
17
- >
18
- Learn React
19
- </a>
 
 
 
 
 
 
 
 
20
  </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  </div>
22
  );
23
- }
24
 
25
  export default App;
 
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
+ // ⚠️ CẤU HÌNH: DÁN LINK NGROK CỦA BẠN VÀO ĐÂY
8
+ const API_URL = "https://thay-link-ngrok-cua-ban-vao-day.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",
14
+ "DPM++ 2M Karras", "DPM++ SDE Karras", "DDIM", "UniPC"
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);
48
+ const [error, setError] = useState('');
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);
77
+ if (data.upscalers?.length > 0) setUpscalers(data.upscalers);
78
+
79
+ setServerStatus('connected');
80
+ addLog(`Connected! Found ${data.models.length} checkpoints.`);
81
+
82
+ } catch (err) {
83
+ console.error(err);
84
+ setServerStatus('disconnected');
85
+ addLog("Failed to fetch model list. Is server running?");
86
+ } finally {
87
+ setFetchingInfo(false);
88
+ }
89
+ };
90
+
91
+ // Auto fetch khi mount
92
+ useEffect(() => {
93
+ fetchServerInfo();
94
+ }, []);
95
+
96
+ const handleGenerate = async () => {
97
+ if (serverStatus === 'disconnected') {
98
+ setError('Chưa kết nối được Server!');
99
+ return;
100
+ }
101
+
102
+ setLoading(true);
103
+ setError('');
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;
111
+ } else if (upscaleFactor > 1) {
112
+ upscalerValue = upscaleFactor.toString();
113
+ }
114
+
115
+ const payload = {
116
+ prompt: prompt,
117
+ negative_prompt: negPrompt,
118
+ steps: steps,
119
+ cfg_scale: cfgScale,
120
+ seed: seed,
121
+ sampler_name: sampler,
122
+ checkpoint: selectedCheckpoint,
123
+ lora: selectedLora,
124
+ vae: selectedVae,
125
+ upscaler: upscalerValue,
126
+ upscale_strength: upscaleStrength
127
+ };
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
+
135
+ if (!response.ok) throw new Error(`Server Error: ${response.status}`);
136
+ const data = await response.json();
137
+
138
+ if (data.image) {
139
+ setGeneratedImage(data.image);
140
+ addLog('Image finished!');
141
+ }
142
+
143
+ } catch (err) {
144
+ setError(err.message);
145
+ addLog(`Error: ${err.message}`);
146
+ } finally {
147
+ setLoading(false);
148
+ }
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">
159
+ <Sparkles className="w-5 h-5 text-white" />
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'}`} />
167
+ </button>
168
+ <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'}`}>
169
+ {serverStatus === 'connected' ? <Wifi className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
170
+ <span className="text-xs font-bold">{serverStatus === 'connected' ? 'ONLINE' : 'OFFLINE'}</span>
171
+ </div>
172
+ </div>
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>
207
+ </div>
208
+ </section>
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'}`}>
260
+ {f === 1.0 ? 'Off' : `${f}x`}
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
+ )}
273
+ </section>
274
+
275
+ <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]'}`}>
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 --- */}
285
+ <div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-4">
286
+ <div className="bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-center min-h-[500px] relative overflow-hidden">
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;