Akimitsujiro's picture
Update src/App.jsx
3f6fdde verified
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;