Spaces:
Build error
Build error
Update src/App.jsx
Browse files- src/App.jsx +47 -41
src/App.jsx
CHANGED
|
@@ -1,20 +1,17 @@
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import {
|
| 3 |
Sparkles, Copy, Trash2, Wand2, Terminal,
|
| 4 |
-
Palette, Zap, Check, FlaskConical, Skull,
|
| 5 |
} from 'lucide-react';
|
| 6 |
-
|
|
|
|
| 7 |
|
| 8 |
-
// --- CẤU HÌNH
|
| 9 |
-
//
|
| 10 |
-
|
| 11 |
-
const HF_TOKEN = "";
|
| 12 |
-
const hf = new HfInference(HF_TOKEN);
|
| 13 |
|
| 14 |
-
// Model chuyên chat/instruct tốt
|
| 15 |
const AI_MODEL = "HuggingFaceH4/zephyr-7b-beta";
|
| 16 |
|
| 17 |
-
// --- DATABASE TỪ KHÓA (Cho chế độ Manual) ---
|
| 18 |
const STYLES = [
|
| 19 |
{ id: 'none', label: 'None', prompt: '' },
|
| 20 |
{ id: 'anime', label: 'Anime/Manga', prompt: 'anime style, studio ghibli style, vibrant colors, makoto shinkai style, detailed background' },
|
|
@@ -43,8 +40,9 @@ const App = () => {
|
|
| 43 |
const [finalPrompt, setFinalPrompt] = useState("");
|
| 44 |
const [copied, setCopied] = useState(false);
|
| 45 |
|
| 46 |
-
// State mới cho chế độ AI
|
| 47 |
const [useAI, setUseAI] = useState(false);
|
|
|
|
| 48 |
const [isLoadingAI, setIsLoadingAI] = useState(false);
|
| 49 |
const [aiError, setAiError] = useState("");
|
| 50 |
|
|
@@ -64,16 +62,21 @@ const App = () => {
|
|
| 64 |
const generateWithAI = async () => {
|
| 65 |
if (!basePrompt.trim()) return;
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
setIsLoadingAI(true);
|
| 68 |
setAiError("");
|
| 69 |
-
setFinalPrompt("");
|
| 70 |
|
| 71 |
try {
|
|
|
|
|
|
|
| 72 |
const styleText = selectedStyle.id !== 'none' ? `Style: ${selectedStyle.label} (${selectedStyle.prompt}).` : "";
|
| 73 |
const qualityText = addQuality ? "Ensure high quality, detailed, 8k resolution." : "";
|
| 74 |
const customText = customTags ? `Include these elements: ${customTags}.` : "";
|
| 75 |
|
| 76 |
-
// Prompt kỹ thuật (System Prompt) để ép AI trả về format chuẩn SD
|
| 77 |
const systemPrompt = `You are an expert Stable Diffusion prompt engineer.
|
| 78 |
Task: Rewrite the user's concept into a highly detailed, comma-separated prompt optimized for SDXL.
|
| 79 |
Rules:
|
|
@@ -97,9 +100,7 @@ const App = () => {
|
|
| 97 |
}
|
| 98 |
});
|
| 99 |
|
| 100 |
-
// Làm sạch kết quả trả về
|
| 101 |
let result = output.generated_text;
|
| 102 |
-
// Cắt bỏ phần prompt đầu vào (nếu model trả về cả input)
|
| 103 |
if (result.includes("<|assistant|>")) {
|
| 104 |
result = result.split("<|assistant|>")[1];
|
| 105 |
}
|
|
@@ -108,9 +109,8 @@ const App = () => {
|
|
| 108 |
|
| 109 |
} catch (err) {
|
| 110 |
console.error(err);
|
| 111 |
-
setAiError("Lỗi
|
| 112 |
-
// Fallback
|
| 113 |
-
generateManual();
|
| 114 |
} finally {
|
| 115 |
setIsLoadingAI(false);
|
| 116 |
}
|
|
@@ -146,7 +146,7 @@ const App = () => {
|
|
| 146 |
return (
|
| 147 |
<div className="min-h-screen bg-slate-950 text-slate-200 font-sans p-4 md:p-8 selection:bg-pink-500/30">
|
| 148 |
|
| 149 |
-
<header className="max-w-5xl mx-auto mb-8 flex items-center justify-between border-b border-slate-800 pb-4">
|
| 150 |
<div className="flex items-center gap-3">
|
| 151 |
<div className="bg-gradient-to-tr from-pink-500 to-purple-600 p-2.5 rounded-xl shadow-lg shadow-purple-900/20">
|
| 152 |
<FlaskConical className="w-6 h-6 text-white" />
|
|
@@ -157,26 +157,41 @@ const App = () => {
|
|
| 157 |
</div>
|
| 158 |
</div>
|
| 159 |
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
</header>
|
| 173 |
|
| 174 |
<main className="max-w-5xl mx-auto grid lg:grid-cols-2 gap-8">
|
| 175 |
|
| 176 |
{/* --- LEFT: INPUTS --- */}
|
| 177 |
<div className="space-y-6">
|
| 178 |
-
|
| 179 |
-
{/* 1. Main Idea */}
|
| 180 |
<div className="space-y-2">
|
| 181 |
<label className="text-sm font-bold text-slate-300 flex items-center gap-2">
|
| 182 |
<Sparkles className="w-4 h-4 text-yellow-400" /> Base Concept
|
|
@@ -189,7 +204,6 @@ const App = () => {
|
|
| 189 |
/>
|
| 190 |
</div>
|
| 191 |
|
| 192 |
-
{/* 2. Custom Tags */}
|
| 193 |
<div className="space-y-2">
|
| 194 |
<label className="text-sm font-bold text-slate-300 flex items-center gap-2">
|
| 195 |
<Skull className="w-4 h-4 text-red-400" /> Custom Tags / Details
|
|
@@ -203,7 +217,6 @@ const App = () => {
|
|
| 203 |
/>
|
| 204 |
</div>
|
| 205 |
|
| 206 |
-
{/* 3. Style */}
|
| 207 |
<div className="space-y-2">
|
| 208 |
<label className="text-sm font-bold text-slate-300 flex items-center gap-2">
|
| 209 |
<Palette className="w-4 h-4 text-blue-400" /> Art Style
|
|
@@ -225,7 +238,6 @@ const App = () => {
|
|
| 225 |
</div>
|
| 226 |
</div>
|
| 227 |
|
| 228 |
-
{/* 4. Options */}
|
| 229 |
<div className="flex items-center gap-4 p-4 bg-slate-900/50 rounded-xl border border-slate-800">
|
| 230 |
<div className="flex items-center gap-2 cursor-pointer" onClick={() => setAddQuality(!addQuality)}>
|
| 231 |
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${addQuality ? 'bg-pink-600 border-pink-600' : 'border-slate-600 bg-slate-800'}`}>
|
|
@@ -234,13 +246,10 @@ const App = () => {
|
|
| 234 |
<span className="text-xs font-medium select-none text-slate-300">Auto Quality Boost</span>
|
| 235 |
</div>
|
| 236 |
</div>
|
| 237 |
-
|
| 238 |
</div>
|
| 239 |
|
| 240 |
{/* --- RIGHT: OUTPUT --- */}
|
| 241 |
<div className="flex flex-col gap-6">
|
| 242 |
-
|
| 243 |
-
{/* Action Button */}
|
| 244 |
<button
|
| 245 |
onClick={handleGenerate}
|
| 246 |
disabled={isLoadingAI}
|
|
@@ -265,7 +274,6 @@ const App = () => {
|
|
| 265 |
</div>
|
| 266 |
)}
|
| 267 |
|
| 268 |
-
{/* Result Box */}
|
| 269 |
<div className="flex-1 bg-black/40 border border-slate-800 rounded-xl p-4 flex flex-col relative group min-h-[200px]">
|
| 270 |
<label className="text-xs font-bold text-green-400 uppercase mb-2 flex justify-between">
|
| 271 |
<span>Final Positive Prompt {useAI && "(AI Generated)"}</span>
|
|
@@ -298,7 +306,6 @@ const App = () => {
|
|
| 298 |
</div>
|
| 299 |
</div>
|
| 300 |
|
| 301 |
-
{/* Negative Prompt */}
|
| 302 |
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
|
| 303 |
<div className="flex items-center justify-between mb-2">
|
| 304 |
<label className="text-xs font-bold text-red-400 uppercase">Negative Prompt</label>
|
|
@@ -325,7 +332,6 @@ const App = () => {
|
|
| 325 |
className="w-full h-24 bg-slate-950 border border-slate-700 rounded-lg p-3 text-xs text-slate-400 focus:border-red-500 outline-none resize-none font-mono"
|
| 326 |
/>
|
| 327 |
</div>
|
| 328 |
-
|
| 329 |
</div>
|
| 330 |
</main>
|
| 331 |
</div>
|
|
|
|
| 1 |
import React, { useState } from 'react';
|
| 2 |
import {
|
| 3 |
Sparkles, Copy, Trash2, Wand2, Terminal,
|
| 4 |
+
Palette, Zap, Check, FlaskConical, Skull, BrainCircuit, Loader2, AlertCircle, Key
|
| 5 |
} from 'lucide-react';
|
| 6 |
+
// SỬA LỖI: Dùng CDN trực tiếp để chạy được ngay trên mọi môi trường
|
| 7 |
+
import { HfInference } from "https://esm.sh/@huggingface/inference";
|
| 8 |
|
| 9 |
+
// --- CẤU HÌNH ---
|
| 10 |
+
// ⚠️ QUAN TRỌNG: Để trống dòng này khi commit lên Hugging Face để tránh lỗi lộ secret
|
| 11 |
+
const DEFAULT_HF_TOKEN = "";
|
|
|
|
|
|
|
| 12 |
|
|
|
|
| 13 |
const AI_MODEL = "HuggingFaceH4/zephyr-7b-beta";
|
| 14 |
|
|
|
|
| 15 |
const STYLES = [
|
| 16 |
{ id: 'none', label: 'None', prompt: '' },
|
| 17 |
{ id: 'anime', label: 'Anime/Manga', prompt: 'anime style, studio ghibli style, vibrant colors, makoto shinkai style, detailed background' },
|
|
|
|
| 40 |
const [finalPrompt, setFinalPrompt] = useState("");
|
| 41 |
const [copied, setCopied] = useState(false);
|
| 42 |
|
| 43 |
+
// State mới cho chế độ AI và Token
|
| 44 |
const [useAI, setUseAI] = useState(false);
|
| 45 |
+
const [userToken, setUserToken] = useState(DEFAULT_HF_TOKEN);
|
| 46 |
const [isLoadingAI, setIsLoadingAI] = useState(false);
|
| 47 |
const [aiError, setAiError] = useState("");
|
| 48 |
|
|
|
|
| 62 |
const generateWithAI = async () => {
|
| 63 |
if (!basePrompt.trim()) return;
|
| 64 |
|
| 65 |
+
// Kiểm tra Token nếu dùng AI Mode
|
| 66 |
+
const tokenToUse = userToken.trim() || DEFAULT_HF_TOKEN;
|
| 67 |
+
// Nếu không có token, vẫn thử gọi (có thể bị rate limit ở chế độ public)
|
| 68 |
+
|
| 69 |
setIsLoadingAI(true);
|
| 70 |
setAiError("");
|
| 71 |
+
setFinalPrompt("");
|
| 72 |
|
| 73 |
try {
|
| 74 |
+
const hf = new HfInference(tokenToUse || undefined);
|
| 75 |
+
|
| 76 |
const styleText = selectedStyle.id !== 'none' ? `Style: ${selectedStyle.label} (${selectedStyle.prompt}).` : "";
|
| 77 |
const qualityText = addQuality ? "Ensure high quality, detailed, 8k resolution." : "";
|
| 78 |
const customText = customTags ? `Include these elements: ${customTags}.` : "";
|
| 79 |
|
|
|
|
| 80 |
const systemPrompt = `You are an expert Stable Diffusion prompt engineer.
|
| 81 |
Task: Rewrite the user's concept into a highly detailed, comma-separated prompt optimized for SDXL.
|
| 82 |
Rules:
|
|
|
|
| 100 |
}
|
| 101 |
});
|
| 102 |
|
|
|
|
| 103 |
let result = output.generated_text;
|
|
|
|
| 104 |
if (result.includes("<|assistant|>")) {
|
| 105 |
result = result.split("<|assistant|>")[1];
|
| 106 |
}
|
|
|
|
| 109 |
|
| 110 |
} catch (err) {
|
| 111 |
console.error(err);
|
| 112 |
+
setAiError("Lỗi AI: " + (err.message || "Kết nối thất bại."));
|
| 113 |
+
generateManual(); // Fallback
|
|
|
|
| 114 |
} finally {
|
| 115 |
setIsLoadingAI(false);
|
| 116 |
}
|
|
|
|
| 146 |
return (
|
| 147 |
<div className="min-h-screen bg-slate-950 text-slate-200 font-sans p-4 md:p-8 selection:bg-pink-500/30">
|
| 148 |
|
| 149 |
+
<header className="max-w-5xl mx-auto mb-8 flex flex-col md:flex-row items-center justify-between border-b border-slate-800 pb-4 gap-4">
|
| 150 |
<div className="flex items-center gap-3">
|
| 151 |
<div className="bg-gradient-to-tr from-pink-500 to-purple-600 p-2.5 rounded-xl shadow-lg shadow-purple-900/20">
|
| 152 |
<FlaskConical className="w-6 h-6 text-white" />
|
|
|
|
| 157 |
</div>
|
| 158 |
</div>
|
| 159 |
|
| 160 |
+
<div className="flex items-center gap-3">
|
| 161 |
+
{/* Ô nhập Token (Ẩn nếu không dùng AI) */}
|
| 162 |
+
{useAI && (
|
| 163 |
+
<div className="relative group">
|
| 164 |
+
<input
|
| 165 |
+
type="password"
|
| 166 |
+
placeholder="HF Token (Optional)"
|
| 167 |
+
value={userToken}
|
| 168 |
+
onChange={(e) => setUserToken(e.target.value)}
|
| 169 |
+
className="bg-slate-900 border border-slate-700 rounded-full px-3 py-2 text-xs w-32 focus:w-48 transition-all outline-none focus:border-purple-500"
|
| 170 |
+
/>
|
| 171 |
+
<div className="absolute right-3 top-2.5 text-slate-500 pointer-events-none">
|
| 172 |
+
<Key className="w-3 h-3" />
|
| 173 |
+
</div>
|
| 174 |
+
</div>
|
| 175 |
+
)}
|
| 176 |
+
|
| 177 |
+
<button
|
| 178 |
+
onClick={() => setUseAI(!useAI)}
|
| 179 |
+
className={`flex items-center gap-2 px-4 py-2 rounded-full border transition-all text-xs font-bold ${
|
| 180 |
+
useAI
|
| 181 |
+
? 'bg-purple-600/20 border-purple-500 text-purple-400 shadow-[0_0_10px_rgba(168,85,247,0.3)]'
|
| 182 |
+
: 'bg-slate-900 border-slate-700 text-slate-400 hover:bg-slate-800'
|
| 183 |
+
}`}
|
| 184 |
+
>
|
| 185 |
+
{useAI ? <BrainCircuit className="w-4 h-4" /> : <Wand2 className="w-4 h-4" />}
|
| 186 |
+
{useAI ? 'AI MODE: ON' : 'MANUAL MODE'}
|
| 187 |
+
</button>
|
| 188 |
+
</div>
|
| 189 |
</header>
|
| 190 |
|
| 191 |
<main className="max-w-5xl mx-auto grid lg:grid-cols-2 gap-8">
|
| 192 |
|
| 193 |
{/* --- LEFT: INPUTS --- */}
|
| 194 |
<div className="space-y-6">
|
|
|
|
|
|
|
| 195 |
<div className="space-y-2">
|
| 196 |
<label className="text-sm font-bold text-slate-300 flex items-center gap-2">
|
| 197 |
<Sparkles className="w-4 h-4 text-yellow-400" /> Base Concept
|
|
|
|
| 204 |
/>
|
| 205 |
</div>
|
| 206 |
|
|
|
|
| 207 |
<div className="space-y-2">
|
| 208 |
<label className="text-sm font-bold text-slate-300 flex items-center gap-2">
|
| 209 |
<Skull className="w-4 h-4 text-red-400" /> Custom Tags / Details
|
|
|
|
| 217 |
/>
|
| 218 |
</div>
|
| 219 |
|
|
|
|
| 220 |
<div className="space-y-2">
|
| 221 |
<label className="text-sm font-bold text-slate-300 flex items-center gap-2">
|
| 222 |
<Palette className="w-4 h-4 text-blue-400" /> Art Style
|
|
|
|
| 238 |
</div>
|
| 239 |
</div>
|
| 240 |
|
|
|
|
| 241 |
<div className="flex items-center gap-4 p-4 bg-slate-900/50 rounded-xl border border-slate-800">
|
| 242 |
<div className="flex items-center gap-2 cursor-pointer" onClick={() => setAddQuality(!addQuality)}>
|
| 243 |
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${addQuality ? 'bg-pink-600 border-pink-600' : 'border-slate-600 bg-slate-800'}`}>
|
|
|
|
| 246 |
<span className="text-xs font-medium select-none text-slate-300">Auto Quality Boost</span>
|
| 247 |
</div>
|
| 248 |
</div>
|
|
|
|
| 249 |
</div>
|
| 250 |
|
| 251 |
{/* --- RIGHT: OUTPUT --- */}
|
| 252 |
<div className="flex flex-col gap-6">
|
|
|
|
|
|
|
| 253 |
<button
|
| 254 |
onClick={handleGenerate}
|
| 255 |
disabled={isLoadingAI}
|
|
|
|
| 274 |
</div>
|
| 275 |
)}
|
| 276 |
|
|
|
|
| 277 |
<div className="flex-1 bg-black/40 border border-slate-800 rounded-xl p-4 flex flex-col relative group min-h-[200px]">
|
| 278 |
<label className="text-xs font-bold text-green-400 uppercase mb-2 flex justify-between">
|
| 279 |
<span>Final Positive Prompt {useAI && "(AI Generated)"}</span>
|
|
|
|
| 306 |
</div>
|
| 307 |
</div>
|
| 308 |
|
|
|
|
| 309 |
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
|
| 310 |
<div className="flex items-center justify-between mb-2">
|
| 311 |
<label className="text-xs font-bold text-red-400 uppercase">Negative Prompt</label>
|
|
|
|
| 332 |
className="w-full h-24 bg-slate-950 border border-slate-700 rounded-lg p-3 text-xs text-slate-400 focus:border-red-500 outline-none resize-none font-mono"
|
| 333 |
/>
|
| 334 |
</div>
|
|
|
|
| 335 |
</div>
|
| 336 |
</main>
|
| 337 |
</div>
|