Spaces:
Build error
Build error
| import React, { useState } from 'react'; | |
| import { | |
| Sparkles, Copy, Trash2, Wand2, Terminal, | |
| Palette, Zap, Check, FlaskConical, Skull, BrainCircuit, Loader2, AlertCircle, Key | |
| } from 'lucide-react'; | |
| // SỬA LỖI: Dùng CDN trực tiếp để chạy được ngay trên mọi môi trường | |
| import { HfInference } from "https://esm.sh/@huggingface/inference"; | |
| // --- CẤU HÌNH --- | |
| // ⚠️ QUAN TRỌNG: Để trống dòng này khi commit lên Hugging Face để tránh lỗi lộ secret | |
| const DEFAULT_HF_TOKEN = ""; | |
| const AI_MODEL = "HuggingFaceH4/zephyr-7b-beta"; | |
| const STYLES = [ | |
| { id: 'none', label: 'None', prompt: '' }, | |
| { id: 'anime', label: 'Anime/Manga', prompt: 'anime style, studio ghibli style, vibrant colors, makoto shinkai style, detailed background' }, | |
| { id: 'photo', label: 'Photorealistic', prompt: 'photorealistic, 8k, highly detailed, raw photo, dslr, soft lighting, f/1.8, fujifilm' }, | |
| { id: 'cyberpunk', label: 'Cyberpunk', prompt: 'cyberpunk, neon lights, futuristic, synthwave, high tech, night city, rain reflection' }, | |
| { id: 'fantasy', label: 'Dark Fantasy', prompt: 'dark fantasy, oil painting, intricate details, magical, mystical, greg rutkowski, dungeons and dragons' }, | |
| { id: '3d', label: '3D Render', prompt: '3d render, unreal engine 5, octane render, ray tracing, blender, cinema 4d, cgsociety' }, | |
| { id: 'ink', label: 'Ink Illustration', prompt: 'ink illustration, thick lines, hatching, cross-hatching, black and white, comic style' }, | |
| { id: 'nsfw_soft', label: 'Soft Artistic', prompt: 'soft lighting, artistic nude, elegant pose, classical art style, marble texture, dreamy atmosphere' }, | |
| ]; | |
| const QUALITY_TAGS = "masterpiece, best quality, ultra-detailed, 8k, hdr, high resolution, absurdres, sharp focus"; | |
| const NEGATIVE_PRESETS = { | |
| default: "blurry, low quality, text, watermark, bad anatomy, distorted, ugly, pixelated, grain, noise, lowres, error, unfinished, jpeg artifacts", | |
| realistic: "cartoon, drawing, anime, illustration, painting, sketch, fake, render, 3d, doll, plastic", | |
| anime: "photo, realistic, 3d, nose, lips, realism" | |
| }; | |
| const App = () => { | |
| const [basePrompt, setBasePrompt] = useState(""); | |
| const [selectedStyle, setSelectedStyle] = useState(STYLES[1]); | |
| const [addQuality, setAddQuality] = useState(true); | |
| const [customTags, setCustomTags] = useState(""); | |
| const [negativePrompt, setNegativePrompt] = useState(NEGATIVE_PRESETS.default); | |
| const [finalPrompt, setFinalPrompt] = useState(""); | |
| const [copied, setCopied] = useState(false); | |
| // State mới cho chế độ AI và Token | |
| const [useAI, setUseAI] = useState(false); | |
| const [userToken, setUserToken] = useState(DEFAULT_HF_TOKEN); | |
| const [isLoadingAI, setIsLoadingAI] = useState(false); | |
| const [aiError, setAiError] = useState(""); | |
| const generateManual = () => { | |
| let parts = []; | |
| if (basePrompt.trim()) parts.push(basePrompt.trim()); | |
| if (customTags.trim()) parts.push(customTags.trim()); | |
| if (selectedStyle.id !== 'none' && selectedStyle.prompt) { | |
| parts.push(selectedStyle.prompt); | |
| } | |
| if (addQuality) { | |
| parts.push(QUALITY_TAGS); | |
| } | |
| setFinalPrompt(parts.join(", ")); | |
| }; | |
| const generateWithAI = async () => { | |
| if (!basePrompt.trim()) return; | |
| // Kiểm tra Token nếu dùng AI Mode | |
| const tokenToUse = userToken.trim() || DEFAULT_HF_TOKEN; | |
| // Nếu không có token, vẫn thử gọi (có thể bị rate limit ở chế độ public) | |
| setIsLoadingAI(true); | |
| setAiError(""); | |
| setFinalPrompt(""); | |
| try { | |
| const hf = new HfInference(tokenToUse || undefined); | |
| const styleText = selectedStyle.id !== 'none' ? `Style: ${selectedStyle.label} (${selectedStyle.prompt}).` : ""; | |
| const qualityText = addQuality ? "Ensure high quality, detailed, 8k resolution." : ""; | |
| const customText = customTags ? `Include these elements: ${customTags}.` : ""; | |
| const systemPrompt = `You are an expert Stable Diffusion prompt engineer. | |
| Task: Rewrite the user's concept into a highly detailed, comma-separated prompt optimized for SDXL. | |
| Rules: | |
| 1. Use keywords and phrases, separated by commas. | |
| 2. Focus on visual description, lighting, composition, and texture. | |
| 3. ${styleText} | |
| 4. ${qualityText} | |
| 5. ${customText} | |
| 6. Output ONLY the prompt string. No explanations.`; | |
| const userMessage = `Concept: ${basePrompt}`; | |
| const output = await hf.textGeneration({ | |
| model: AI_MODEL, | |
| inputs: `<|system|>\n${systemPrompt}</s>\n<|user|>\n${userMessage}</s>\n<|assistant|>\n`, | |
| parameters: { | |
| max_new_tokens: 150, | |
| temperature: 0.7, | |
| top_p: 0.95, | |
| repetition_penalty: 1.2, | |
| } | |
| }); | |
| let result = output.generated_text; | |
| if (result.includes("<|assistant|>")) { | |
| result = result.split("<|assistant|>")[1]; | |
| } | |
| setFinalPrompt(result.trim()); | |
| } catch (err) { | |
| console.error(err); | |
| setAiError("Lỗi AI: " + (err.message || "Kết nối thất bại.")); | |
| generateManual(); // Fallback | |
| } finally { | |
| setIsLoadingAI(false); | |
| } | |
| }; | |
| const handleGenerate = () => { | |
| if (useAI) { | |
| generateWithAI(); | |
| } else { | |
| generateManual(); | |
| } | |
| }; | |
| const handleCopy = () => { | |
| navigator.clipboard.writeText(finalPrompt); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }; | |
| const handleCopyAll = () => { | |
| const fullText = `Positive: ${finalPrompt}\nNegative: ${negativePrompt}`; | |
| navigator.clipboard.writeText(fullText); | |
| alert("Copied both Positive & Negative!"); | |
| }; | |
| const clearAll = () => { | |
| setBasePrompt(""); | |
| setCustomTags(""); | |
| setFinalPrompt(""); | |
| setAiError(""); | |
| }; | |
| return ( | |
| <div className="min-h-screen bg-slate-950 text-slate-200 font-sans p-4 md:p-8 selection:bg-pink-500/30"> | |
| <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"> | |
| <div className="flex items-center gap-3"> | |
| <div className="bg-gradient-to-tr from-pink-500 to-purple-600 p-2.5 rounded-xl shadow-lg shadow-purple-900/20"> | |
| <FlaskConical className="w-6 h-6 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="font-bold text-2xl text-white tracking-tight">SDXL Prompt <span className="text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-purple-400">Alchemist</span></h1> | |
| <p className="text-xs text-slate-500 font-mono">Uncensored Logic & AI Assistant</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-3"> | |
| {/* Ô nhập Token (Ẩn nếu không dùng AI) */} | |
| {useAI && ( | |
| <div className="relative group"> | |
| <input | |
| type="password" | |
| placeholder="HF Token (Optional)" | |
| value={userToken} | |
| onChange={(e) => setUserToken(e.target.value)} | |
| 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" | |
| /> | |
| <div className="absolute right-3 top-2.5 text-slate-500 pointer-events-none"> | |
| <Key className="w-3 h-3" /> | |
| </div> | |
| </div> | |
| )} | |
| <button | |
| onClick={() => setUseAI(!useAI)} | |
| className={`flex items-center gap-2 px-4 py-2 rounded-full border transition-all text-xs font-bold ${ | |
| useAI | |
| ? 'bg-purple-600/20 border-purple-500 text-purple-400 shadow-[0_0_10px_rgba(168,85,247,0.3)]' | |
| : 'bg-slate-900 border-slate-700 text-slate-400 hover:bg-slate-800' | |
| }`} | |
| > | |
| {useAI ? <BrainCircuit className="w-4 h-4" /> : <Wand2 className="w-4 h-4" />} | |
| {useAI ? 'AI MODE: ON' : 'MANUAL MODE'} | |
| </button> | |
| </div> | |
| </header> | |
| <main className="max-w-5xl mx-auto grid lg:grid-cols-2 gap-8"> | |
| {/* --- LEFT: INPUTS --- */} | |
| <div className="space-y-6"> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-bold text-slate-300 flex items-center gap-2"> | |
| <Sparkles className="w-4 h-4 text-yellow-400" /> Base Concept | |
| </label> | |
| <textarea | |
| value={basePrompt} | |
| onChange={(e) => setBasePrompt(e.target.value)} | |
| placeholder="Nhập nội dung chính (VD: 1girl, sitting on sofa...)" | |
| className="w-full h-32 bg-slate-900 border border-slate-700 rounded-xl p-4 text-sm focus:border-pink-500 focus:ring-1 focus:ring-pink-500 outline-none transition-all placeholder:text-slate-600 resize-none font-mono" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-bold text-slate-300 flex items-center gap-2"> | |
| <Skull className="w-4 h-4 text-red-400" /> Custom Tags / Details | |
| </label> | |
| <input | |
| type="text" | |
| value={customTags} | |
| onChange={(e) => setCustomTags(e.target.value)} | |
| placeholder="Thêm chi tiết bổ sung..." | |
| className="w-full bg-slate-900 border border-slate-700 rounded-xl p-3 text-sm focus:border-red-500 focus:ring-1 focus:ring-red-500 outline-none transition-all placeholder:text-slate-600 font-mono" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <label className="text-sm font-bold text-slate-300 flex items-center gap-2"> | |
| <Palette className="w-4 h-4 text-blue-400" /> Art Style | |
| </label> | |
| <div className="grid grid-cols-2 sm:grid-cols-3 gap-2"> | |
| {STYLES.map((style) => ( | |
| <button | |
| key={style.id} | |
| onClick={() => setSelectedStyle(style)} | |
| className={`text-[10px] sm:text-xs p-2.5 rounded-lg border transition-all text-left truncate ${ | |
| selectedStyle.id === style.id | |
| ? 'bg-purple-600 border-purple-500 text-white shadow-lg shadow-purple-900/50 font-bold' | |
| : 'bg-slate-900 border-slate-800 text-slate-400 hover:bg-slate-800 hover:border-slate-600' | |
| }`} | |
| > | |
| {style.label} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4 p-4 bg-slate-900/50 rounded-xl border border-slate-800"> | |
| <div className="flex items-center gap-2 cursor-pointer" onClick={() => setAddQuality(!addQuality)}> | |
| <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'}`}> | |
| {addQuality && <Check className="w-3 h-3 text-white" />} | |
| </div> | |
| <span className="text-xs font-medium select-none text-slate-300">Auto Quality Boost</span> | |
| </div> | |
| </div> | |
| </div> | |
| {/* --- RIGHT: OUTPUT --- */} | |
| <div className="flex flex-col gap-6"> | |
| <button | |
| onClick={handleGenerate} | |
| disabled={isLoadingAI} | |
| className={`w-full py-4 rounded-xl font-bold text-white shadow-lg active:scale-[0.98] transition-all flex items-center justify-center gap-2 text-lg ${ | |
| isLoadingAI | |
| ? 'bg-slate-800 cursor-not-allowed opacity-70' | |
| : useAI | |
| ? 'bg-gradient-to-r from-indigo-500 to-purple-600 hover:shadow-purple-500/25' | |
| : 'bg-gradient-to-r from-pink-600 to-purple-600 hover:shadow-pink-500/25' | |
| }`} | |
| > | |
| {isLoadingAI ? ( | |
| <><Loader2 className="w-5 h-5 animate-spin" /> THINKING...</> | |
| ) : ( | |
| useAI ? <><BrainCircuit className="w-5 h-5 fill-current" /> GENERATE WITH AI</> : <><Zap className="w-5 h-5 fill-current" /> MIX & OPTIMIZE</> | |
| )} | |
| </button> | |
| {aiError && ( | |
| <div className="p-3 bg-red-900/20 border border-red-500/30 rounded-lg text-red-400 text-xs flex items-center gap-2"> | |
| <AlertCircle className="w-4 h-4" /> {aiError} | |
| </div> | |
| )} | |
| <div className="flex-1 bg-black/40 border border-slate-800 rounded-xl p-4 flex flex-col relative group min-h-[200px]"> | |
| <label className="text-xs font-bold text-green-400 uppercase mb-2 flex justify-between"> | |
| <span>Final Positive Prompt {useAI && "(AI Generated)"}</span> | |
| <span className="text-slate-600 font-normal">{finalPrompt.length} chars</span> | |
| </label> | |
| <textarea | |
| readOnly | |
| value={finalPrompt} | |
| className="w-full flex-1 bg-transparent border-none outline-none text-slate-200 text-sm resize-none font-mono leading-relaxed" | |
| placeholder={useAI ? "AI will generate detailed prompt here..." : "Prompt kết quả sẽ hiện ở đây..."} | |
| /> | |
| <div className="absolute top-3 right-3 flex gap-2"> | |
| <button | |
| onClick={clearAll} | |
| className="p-2 bg-slate-800/80 text-slate-400 hover:text-red-400 rounded-lg backdrop-blur transition-colors" | |
| title="Clear All" | |
| > | |
| <Trash2 className="w-4 h-4" /> | |
| </button> | |
| <button | |
| onClick={handleCopy} | |
| className={`p-2 rounded-lg backdrop-blur transition-all flex items-center gap-1 font-bold text-xs ${ | |
| copied ? 'bg-green-500 text-white' : 'bg-slate-800/80 text-white hover:bg-purple-600' | |
| }`} | |
| > | |
| {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />} | |
| {copied ? 'COPIED' : 'COPY'} | |
| </button> | |
| </div> | |
| </div> | |
| <div className="bg-slate-900 border border-slate-800 rounded-xl p-4"> | |
| <div className="flex items-center justify-between mb-2"> | |
| <label className="text-xs font-bold text-red-400 uppercase">Negative Prompt</label> | |
| <div className="flex gap-2"> | |
| <select | |
| onChange={(e) => setNegativePrompt(NEGATIVE_PRESETS[e.target.value])} | |
| className="bg-slate-950 border border-slate-700 text-[10px] rounded px-2 py-1 outline-none text-slate-400 hover:text-white cursor-pointer" | |
| > | |
| <option value="default">Default</option> | |
| <option value="realistic">Realistic</option> | |
| <option value="anime">Anime</option> | |
| </select> | |
| <button | |
| onClick={handleCopyAll} | |
| className="text-[10px] bg-slate-800 px-2 py-1 rounded hover:bg-slate-700 text-slate-300" | |
| > | |
| Copy All | |
| </button> | |
| </div> | |
| </div> | |
| <textarea | |
| value={negativePrompt} | |
| onChange={(e) => setNegativePrompt(e.target.value)} | |
| 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" | |
| /> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| export default App; |