File size: 15,018 Bytes
c3cc66d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86f849d
 
c3cc66d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86f849d
c3cc66d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86f849d
 
c3cc66d
86f849d
 
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
import React, { useState, useEffect } from 'react';
import { 
  Image as ImageIcon, Loader2, Sparkles, Terminal, AlertCircle, 
  Settings, Layers, Cpu, Maximize, Zap, Wifi, WifiOff, ChevronDown, ChevronUp, Play, RefreshCw
} from 'lucide-react';

// ⚠️ CẤU HÌNH: DÁN LINK NGROK CỦA BẠN VÀO ĐÂY
const API_URL = "https://thay-link-ngrok-cua-ban-vao-day.ngrok-free.app";

// Danh sách Sampler (Giữ nguyên vì đây là config cứng của Diffusers)
const SAMPLERS = [
  "Euler a", "Euler", "LMS", "Heun", "DPM2", "DPM2 a",
  "DPM++ 2S a", "DPM++ 2M", "DPM++ SDE", "DPM++ 2M SDE",
  "DPM++ 2M Karras", "DPM++ SDE Karras", "DDIM", "UniPC"
];

const App = () => {
  // --- STATE ---
  // Core Inputs
  const [prompt, setPrompt] = useState('A futuristic city with neon lights, cyberpunk style');
  const [negPrompt, setNegPrompt] = useState('blurry, bad quality, watermark, text, ugly, distorted, nsfw');
  
  // Dynamic Lists (Dữ liệu từ Server)
  const [checkpoints, setCheckpoints] = useState(["Loading..."]);
  const [loras, setLoras] = useState(["None"]);
  const [vaes, setVaes] = useState(["Default"]);
  const [upscalers, setUpscalers] = useState(["None"]);

  // Selections
  const [selectedCheckpoint, setSelectedCheckpoint] = useState("");
  const [selectedLora, setSelectedLora] = useState("None");
  const [selectedVae, setSelectedVae] = useState("Default");
  const [selectedUpscaler, setSelectedUpscaler] = useState("None");
  
  // Advanced Settings
  const [steps, setSteps] = useState(30);
  const [cfgScale, setCfgScale] = useState(7.0);
  const [seed, setSeed] = useState(-1);
  const [sampler, setSampler] = useState("DPM++ 2M Karras");
  const [upscaleStrength, setUpscaleStrength] = useState(0.35);
  const [upscaleFactor, setUpscaleFactor] = useState(1.0); // 1.0 = No upscale
  const [showAdvanced, setShowAdvanced] = useState(false);

  // System
  const [generatedImage, setGeneratedImage] = useState(null);
  const [loading, setLoading] = useState(false);
  const [fetchingInfo, setFetchingInfo] = useState(false);
  const [error, setError] = useState('');
  const [logs, setLogs] = useState([]);
  const [serverStatus, setServerStatus] = useState('unknown');

  // --- FUNCTIONS ---
  const addLog = (message) => {
    const timestamp = new Date().toLocaleTimeString();
    setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 50));
  };

  // Hàm lấy danh sách model từ Kaggle
  const fetchServerInfo = async () => {
    if (API_URL.includes("thay-link")) return;
    
    setFetchingInfo(true);
    try {
      addLog("Connecting to Kaggle Server...");
      const res = await fetch(`${API_URL}/info`);
      if (!res.ok) throw new Error("Server not ready");
      
      const data = await res.json();
      
      // Cập nhật danh sách
      if (data.models?.length > 0) {
        setCheckpoints(data.models);
        setSelectedCheckpoint(data.models[0]); // Auto select first
      }
      if (data.loras?.length > 0) setLoras(data.loras);
      if (data.vaes?.length > 0) setVaes(data.vaes);
      if (data.upscalers?.length > 0) setUpscalers(data.upscalers);
      
      setServerStatus('connected');
      addLog(`Connected! Found ${data.models.length} checkpoints.`);
      
    } catch (err) {
      console.error(err);
      setServerStatus('disconnected');
      addLog("Failed to fetch model list. Is server running?");
    } finally {
      setFetchingInfo(false);
    }
  };

  // Auto fetch khi mount
  useEffect(() => {
    fetchServerInfo();
  }, []);

  const handleGenerate = async () => {
    if (serverStatus === 'disconnected') {
      setError('Chưa kết nối được Server!');
      return;
    }

    setLoading(true);
    setError('');
    addLog(`Generating... ${selectedCheckpoint}`);

    try {
      // Logic Upscaler: Ưu tiên Model name, nếu ko chọn model thì dùng factor (High-Res Fix)
      let upscalerValue = "None";
      if (selectedUpscaler !== "None") {
        upscalerValue = selectedUpscaler;
      } else if (upscaleFactor > 1) {
        upscalerValue = upscaleFactor.toString();
      }

      const payload = {
        prompt: prompt,
        negative_prompt: negPrompt,
        steps: steps,
        cfg_scale: cfgScale,
        seed: seed,
        sampler_name: sampler,
        checkpoint: selectedCheckpoint,
        lora: selectedLora,
        vae: selectedVae,
        upscaler: upscalerValue,
        upscale_strength: upscaleStrength
      };

      const response = await fetch(`${API_URL}/generate`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload),
      });

      if (!response.ok) throw new Error(`Server Error: ${response.status}`);
      const data = await response.json();
      
      if (data.image) {
        setGeneratedImage(data.image);
        addLog('Image finished!');
      }

    } catch (err) {
      setError(err.message);
      addLog(`Error: ${err.message}`);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen bg-slate-950 text-slate-200 font-sans">
      
      {/* Header */}
      <header className="border-b border-slate-800 bg-slate-900/80 backdrop-blur-md sticky top-0 z-50">
        <div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
          <div className="flex items-center gap-3">
            <div className="bg-gradient-to-br from-indigo-500 to-purple-600 p-2 rounded-lg">
              <Sparkles className="w-5 h-5 text-white" />
            </div>
            <h1 className="font-bold text-lg text-white">Kaggle Studio <span className="text-purple-400">Pro</span></h1>
          </div>

          <div className="flex items-center gap-3">
             <button onClick={fetchServerInfo} className="p-2 hover:bg-slate-800 rounded-full transition-colors" title="Refresh Models">
                <RefreshCw className={`w-4 h-4 ${fetchingInfo ? 'animate-spin text-purple-400' : 'text-slate-400'}`} />
             </button>
             <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'}`}>
                {serverStatus === 'connected' ? <Wifi className="w-4 h-4" /> : <WifiOff className="w-4 h-4" />}
                <span className="text-xs font-bold">{serverStatus === 'connected' ? 'ONLINE' : 'OFFLINE'}</span>
             </div>
          </div>
        </div>
      </header>

      <main className="max-w-7xl mx-auto p-4 lg:p-6 grid lg:grid-cols-12 gap-6">
        
        {/* --- LEFT: CONTROLS --- */}
        <div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-5">
          
          {/* Models */}
          <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 space-y-4">
            <div>
              <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>
              <div className="relative">
                <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">
                  {checkpoints.map(m => <option key={m} value={m}>{m}</option>)}
                </select>
              </div>
            </div>

            <div className="grid grid-cols-2 gap-2">
                <div>
                  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">LoRA</label>
                  <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">
                    {loras.map(m => <option key={m} value={m}>{m}</option>)}
                  </select>
                </div>
                <div>
                  <label className="text-[10px] font-bold text-slate-400 uppercase mb-1">VAE</label>
                  <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">
                    {vaes.map(m => <option key={m} value={m}>{m}</option>)}
                  </select>
                </div>
            </div>
          </section>

          {/* Prompt */}
          <section className="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3">
             <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..." />
             <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..." />
          </section>

          {/* Advanced Toggle */}
          <section className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
            <button onClick={() => setShowAdvanced(!showAdvanced)} className="w-full flex items-center justify-between p-3 hover:bg-slate-800/50 transition-colors text-slate-300">
              <span className="text-xs font-bold uppercase flex items-center gap-2"><Settings className="w-3 h-3" /> Advanced</span>
              {showAdvanced ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
            </button>

            {showAdvanced && (
              <div className="p-4 border-t border-slate-800 bg-slate-950/30 space-y-4">
                {/* Steps & CFG */}
                <div className="space-y-3">
                    <div className="flex justify-between text-[10px] uppercase text-slate-500 font-bold"><span>Steps: {steps}</span><span>CFG: {cfgScale}</span></div>
                    <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" />
                    <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" />
                </div>

                {/* Sampler & Seed */}
                <div className="grid grid-cols-2 gap-2">
                    <div>
                        <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Sampler</label>
                        <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">
                            {SAMPLERS.map(s => <option key={s} value={s}>{s}</option>)}
                        </select>
                    </div>
                    <div>
                        <label className="text-[10px] font-bold text-slate-500 uppercase block mb-1">Seed (-1 = Random)</label>
                        <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" />
                    </div>
                </div>

                {/* Upscale */}
                <div className="pt-2 border-t border-slate-800/50">
                    <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>
                    
                    {/* Upscale Model Dropdown */}
                    <div className="mb-2">
                      <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">
                          {upscalers.map(u => <option key={u} value={u}>{u}</option>)}
                      </select>
                    </div>

                    <div className="flex gap-2 mb-2">
                        {[1.0, 1.5, 2.0].map(f => (
                            <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'}`}>
                                {f === 1.0 ? 'Off' : `${f}x`}
                            </button>
                        ))}
                    </div>
                    {upscaleFactor > 1 && (
                        <div>
                             <label className="text-[10px] text-slate-500 block mb-1">Denoise Strength: {upscaleStrength}</label>
                             <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" />
                        </div>
                    )}
                </div>
              </div>
            )}
          </section>

          <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]'}`}>
            {loading ? <Loader2 className="animate-spin" /> : <Play className="fill-current" />}
            {loading ? 'GENERATING...' : 'GENERATE'}
          </button>
          
          {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>}

        </div>

        {/* --- RIGHT: PREVIEW --- */}
        <div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-4">
          <div className="bg-slate-900 border border-slate-800 rounded-2xl flex items-center justify-center min-h-[500px] relative overflow-hidden">
             {!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>}
             {generatedImage && <img src={generatedImage} alt="Result" className="max-w-full max-h-full object-contain shadow-2xl" />}
          </div>
          
          <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">
            {logs.map((log, i) => <div key={i}>{log}</div>)}
          </div>
        </div>

      </main>
    </div>
  );
};

export default App;