| | <!DOCTYPE html>
|
| | <html lang="es">
|
| | <head>
|
| | <meta charset="UTF-8">
|
| | <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| | <title>AI Studio PRO - Multi-Model Editor</title>
|
| | <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
| | <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
| | <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
| | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
| | <style>
|
| | :root {
|
| | --bg: #0a0a0a;
|
| | --sidebar: #121212;
|
| | --card: #1e1e1e;
|
| | --primary: #ff4d4d;
|
| | --accent: #ff8080;
|
| | --text: #e0e0e0;
|
| | --muted: #888888;
|
| | --border: #333333;
|
| | }
|
| | body {
|
| | font-family: 'Inter', system-ui, sans-serif;
|
| | background: var(--bg);
|
| | color: var(--text);
|
| | margin: 0;
|
| | overflow: hidden;
|
| | height: 100vh;
|
| | }
|
| | .app-layout { display: flex; height: 100vh; }
|
| |
|
| | .sidebar {
|
| | width: 380px;
|
| | background: var(--sidebar);
|
| | border-right: 1px solid var(--border);
|
| | padding: 25px;
|
| | overflow-y: auto;
|
| | display: flex;
|
| | flex-direction: column;
|
| | gap: 20px;
|
| | }
|
| | .logo { font-size: 1.5rem; font-weight: 900; color: var(--primary); display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
|
| |
|
| | .main-content { flex: 1; padding: 40px; overflow-y: auto; position: relative; }
|
| |
|
| | .form-group { display: flex; flex-direction: column; gap: 8px; }
|
| | label { font-size: 0.75rem; font-weight: 700; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
| |
|
| | textarea, select, input {
|
| | background: var(--card); border: 1px solid var(--border); border-radius: 10px;
|
| | padding: 12px; color: white; font-family: inherit; outline: none; transition: border 0.2s;
|
| | font-size: 0.9rem;
|
| | }
|
| | textarea:focus, select:focus, input:focus { border-color: var(--primary); }
|
| |
|
| | .slider-group { display: flex; align-items: center; gap: 15px; }
|
| | input[type="range"] { flex: 1; accent-color: var(--primary); }
|
| |
|
| | .btn-generate {
|
| | background: var(--primary); color: white; border: none; padding: 15px;
|
| | border-radius: 12px; font-weight: 800; cursor: pointer; font-size: 1rem;
|
| | display: flex; align-items: center; justify-content: center; gap: 10px;
|
| | transition: all 0.2s;
|
| | margin-top: 10px;
|
| | }
|
| | .btn-generate:hover { transform: translateY(-2px); filter: brightness(1.1); box-shadow: 0 10px 20px rgba(255, 77, 77, 0.2); }
|
| | .btn-generate:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
| |
|
| | .dropzone {
|
| | border: 2px dashed var(--border); border-radius: 20px; padding: 40px;
|
| | text-align: center; cursor: pointer; transition: all 0.2s;
|
| | background: rgba(255,255,255,0.02);
|
| | display: block;
|
| | }
|
| | .dropzone:hover, .dropzone.drag-over { border-color: var(--primary); background: rgba(255,77,77,0.05); }
|
| | .dropzone i { font-size: 3rem; color: var(--muted); margin-bottom: 15px; }
|
| | .dropzone.drag-over i { color: var(--primary); transform: scale(1.1); transition: transform 0.2s; }
|
| |
|
| | .gallery-grid {
|
| | display: grid;
|
| | grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
| | gap: 10px;
|
| | margin-top: 10px;
|
| | }
|
| | .gallery-item {
|
| | aspect-ratio: 1;
|
| | border-radius: 8px;
|
| | overflow: hidden;
|
| | border: 1px solid var(--border);
|
| | position: relative;
|
| | }
|
| | .gallery-item img {
|
| | width: 100%;
|
| | height: 100%;
|
| | object-fit: cover;
|
| | }
|
| |
|
| | .result-container { margin-top: 20px; display: flex; flex-direction: column; align-items: center; gap: 15px; }
|
| | .result-img { max-width: 100%; border-radius: 20px; box-shadow: 0 20px 40px rgba(0,0,0,0.5); border: 1px solid var(--border); }
|
| |
|
| | .loader {
|
| | width: 30px; height: 30px; border: 3px solid rgba(255,255,255,0.1); border-top-color: var(--primary);
|
| | border-radius: 50%; animation: spin 1s linear infinite;
|
| | }
|
| | @keyframes spin { to { transform: rotate(360deg); } }
|
| |
|
| | .footer-info { font-size: 0.75rem; color: var(--muted); margin-top: auto; padding-top: 20px; border-top: 1px solid var(--border); }
|
| |
|
| | .model-card {
|
| | padding: 10px; border: 1px solid var(--border); border-radius: 10px; cursor: pointer;
|
| | transition: all 0.2s; display: flex; align-items: center; gap: 12px; background: var(--card);
|
| | }
|
| | .model-card.active { border-color: var(--primary); background: rgba(255, 77, 77, 0.1); }
|
| | .model-card i { font-size: 1.2rem; color: var(--muted); }
|
| | .model-card.active i { color: var(--primary); }
|
| |
|
| | .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 5px; }
|
| | .status-online { background: #22c55e; box-shadow: 0 0 8px #22c55e; }
|
| | .status-offline { background: #ef4444; }
|
| | .status-text { font-size: 0.6rem; text-transform: uppercase; font-weight: bold; }
|
| | </style>
|
| | </head>
|
| | <body>
|
| | <div id="root"></div>
|
| |
|
| | <script type="text/babel">
|
| | const { useState } = React;
|
| |
|
| | const QWEN_LORA_OPTIONS = [
|
| | 'Multiple-Angles', 'Photo-to-Anime', 'Anime-V2', 'Light-Migration', 'Upscaler',
|
| | 'Style-Transfer', 'Manga-Tone', 'Anything2Real', 'Fal-Multiple-Angles', 'Polaroid-Photo',
|
| | 'Unblur-Anything', 'Midnight-Noir-Eyes-Spotlight', 'Hyper-Realistic-Portrait',
|
| | 'Ultra-Realistic-Portrait', 'Pixar-Inspired-3D', 'Noir-Comic-Book', 'Any-light',
|
| | 'Studio-DeLight', 'Cinematic-FlatLog'
|
| | ];
|
| |
|
| | const FLUX_STYLE_OPTIONS = [
|
| | 'None', 'Cinematic', 'Anime', 'Cyberpunk', 'Fantasy', '3D Render', 'Oil Painting', 'Sketch', 'Comic Book', 'Photography'
|
| | ];
|
| |
|
| | function App() {
|
| | const [images, setImages] = useState([]);
|
| | const [previews, setPreviews] = useState([]);
|
| | const [prompt, setPrompt] = useState('');
|
| | const [model, setModel] = useState('firered');
|
| | const [lora, setLora] = useState('Photo-to-Anime');
|
| | const [fluxStyle, setFluxStyle] = useState('None');
|
| | const [steps, setSteps] = useState(4);
|
| | const [guidance, setGuidance] = useState(1.0);
|
| | const [seed, setSeed] = useState(0);
|
| | const [width, setWidth] = useState(1024);
|
| | const [height, setHeight] = useState(1024);
|
| | const [azimuth, setAzimuth] = useState(0);
|
| | const [elevation, setElevation] = useState(0);
|
| | const [distance, setDistance] = useState(1.0);
|
| | const [rewritePrompt, setRewritePrompt] = useState(false);
|
| | const [modelStatus, setModelStatus] = useState({});
|
| | const [isGenerating, setIsGenerating] = useState(false);
|
| | const [isDragging, setIsDragging] = useState(false);
|
| | const [result, setResult] = useState(null);
|
| |
|
| | const API_URL = window.location.origin;
|
| |
|
| | React.useEffect(() => {
|
| | const checkStatus = async () => {
|
| | try {
|
| | const res = await fetch(`${API_URL}/status`);
|
| | const data = await res.json();
|
| | setModelStatus(data);
|
| | } catch (e) {
|
| | console.error("Error checking status:", e);
|
| | }
|
| | };
|
| | checkStatus();
|
| | const interval = setInterval(checkStatus, 30000); // Check every 30s
|
| | return () => clearInterval(interval);
|
| | }, []);
|
| |
|
| | const processFiles = (files) => {
|
| | if (files.length > 0) {
|
| | setImages(files);
|
| | const newPreviews = files.map(file => URL.createObjectURL(file));
|
| | setPreviews(newPreviews);
|
| | }
|
| | };
|
| |
|
| | const handleImageChange = (e) => {
|
| | processFiles(Array.from(e.target.files));
|
| | };
|
| |
|
| | const handleDragOver = (e) => {
|
| | e.preventDefault();
|
| | setIsDragging(true);
|
| | };
|
| |
|
| | const handleDragLeave = () => {
|
| | setIsDragging(false);
|
| | };
|
| |
|
| | const handleDrop = (e) => {
|
| | e.preventDefault();
|
| | setIsDragging(false);
|
| | const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
|
| | processFiles(files);
|
| | };
|
| |
|
| | const useOutputAsInput = async () => {
|
| | if (!result) return;
|
| | try {
|
| | const response = await fetch(`${API_URL}${result}`);
|
| | const blob = await response.blob();
|
| | const file = new File([blob], `edit_iteration_${Date.now()}.webp`, { type: 'image/webp' });
|
| | setImages([file]);
|
| | setPreviews([URL.createObjectURL(file)]);
|
| | setResult(null);
|
| | } catch (e) {
|
| | alert("Error al transferir la imagen");
|
| | }
|
| | };
|
| |
|
| | const generateImage = async () => {
|
| | if (model !== 'turbo' && model !== 'banana' && images.length === 0) return alert("Sube al menos una imagen para editar");
|
| | if (!prompt) return alert("Escribe un prompt descriptivo");
|
| |
|
| | setIsGenerating(true);
|
| | setResult(null);
|
| |
|
| | const formData = new FormData();
|
| | if (images.length > 0) {
|
| | images.forEach(img => formData.append('images', img));
|
| | }
|
| | formData.append('prompt', prompt);
|
| | formData.append('model', model);
|
| | formData.append('lora_adapter', lora);
|
| | formData.append('style_name', fluxStyle);
|
| | formData.append('steps', steps);
|
| | formData.append('guidance_scale', guidance);
|
| | formData.append('seed', seed);
|
| | formData.append('width', width);
|
| | formData.append('height', height);
|
| | formData.append('azimuth', azimuth);
|
| | formData.append('elevation', elevation);
|
| | formData.append('distance', distance);
|
| | formData.append('rewrite_prompt', rewritePrompt);
|
| | formData.append('randomize_seed', true);
|
| |
|
| | try {
|
| | const res = await fetch(`${API_URL}/edit-image`, { method: 'POST', body: formData });
|
| | const data = await res.json();
|
| | if (data.success) {
|
| | setResult(data.images[0]);
|
| | } else {
|
| | alert("Error: " + (data.detail || "Error desconocido"));
|
| | }
|
| | } catch (e) {
|
| | alert("Error de conexión");
|
| | } finally {
|
| | setIsGenerating(false);
|
| | }
|
| | };
|
| |
|
| | return (
|
| | <div className="app-layout">
|
| | <div className="sidebar">
|
| | <div className="logo">
|
| | <i className="fa-solid fa-wand-sparkles"></i>
|
| | <span>AI Studio <span>PRO</span></span>
|
| | </div>
|
| |
|
| | <div className="form-group">
|
| | <label>Seleccionar Modelo</label>
|
| | <div style={{display:'flex', flexDirection:'column', gap:'8px'}}>
|
| | <div className={`model-card ${model === 'firered' ? 'active' : ''}`} onClick={() => setModel('firered')}>
|
| | <i className="fa-solid fa-fire"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>FireRed Fast</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Velocidad extrema (Edición)</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.firered ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.firered ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'qwen' ? 'active' : ''}`} onClick={() => setModel('qwen')}>
|
| | <i className="fa-solid fa-robot"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen Lora</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Precisión con estilos LoRA</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.qwen ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.qwen ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'flux' ? 'active' : ''}`} onClick={() => setModel('flux')}>
|
| | <i className="fa-solid fa-bolt"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Flux Klein</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Calidad FLUX.2 LoRA Studio</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.flux ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.flux ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'turbo' ? 'active' : ''}`} onClick={() => {setModel('turbo'); if(steps < 9) setSteps(9);}}>
|
| | <i className="fa-solid fa-gauge-high"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Turbo (Generation)</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Texto a Imagen Ultrarrápido</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.turbo ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.turbo ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'banana' ? 'active' : ''}`} onClick={() => {setModel('banana'); if(steps < 20) setSteps(20);}}>
|
| | <i className="fa-solid fa-lemon"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Nano Banana</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Alta Calidad Flux (Generation)</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.banana ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.banana ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'qwen_rapid' ? 'active' : ''}`} onClick={() => setModel('qwen_rapid')}>
|
| | <i className="fa-solid fa-bolt-lightning"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen Rapid AIO</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Edición SFW V2.3 (Fast)</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.qwen_rapid ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.qwen_rapid ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'anypose' ? 'active' : ''}`} onClick={() => setModel('anypose')}>
|
| | <i className="fa-solid fa-person-walking"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen AnyPose</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Transferencia de Pose 2511</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.anypose ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.anypose ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === 'qwen3_vl' ? 'active' : ''}`} onClick={() => setModel('qwen3_vl')}>
|
| | <i className="fa-solid fa-brain"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen3 VL MAX</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Vision-Language (Unredacted)</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus.qwen3_vl ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus.qwen3_vl ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | <div className={`model-card ${model === '3d_camera' ? 'active' : ''}`} onClick={() => setModel('3d_camera')}>
|
| | <i className="fa-solid fa-camera-rotate"></i>
|
| | <div style={{flex:1}}>
|
| | <div style={{fontWeight:'bold', fontSize:'0.85rem'}}>Qwen 3D Camera</div>
|
| | <div style={{fontSize:'0.65rem', color:'var(--muted)'}}>Control de Ángulo 3D</div>
|
| | </div>
|
| | <div className="status-text">
|
| | <span className={`status-dot ${modelStatus["3d_camera"] ? 'status-online' : 'status-offline'}`}></span>
|
| | {modelStatus["3d_camera"] ? 'Online' : 'Offline'}
|
| | </div>
|
| | </div>
|
| | </div>
|
| | </div>
|
| |
|
| | {model === 'qwen' && (
|
| | <div className="form-group">
|
| | <label>Adaptador LoRA</label>
|
| | <select value={lora} onChange={(e) => setLora(e.target.value)}>
|
| | {QWEN_LORA_OPTIONS.map(opt => <option key={opt} value={opt}>{opt.replace(/-/g, ' ')}</option>)}
|
| | </select>
|
| | </div>
|
| | )}
|
| |
|
| | {model === 'flux' && (
|
| | <div className="form-group">
|
| | <label>Estilo de Arte</label>
|
| | <select value={fluxStyle} onChange={(e) => setFluxStyle(e.target.value)}>
|
| | {FLUX_STYLE_OPTIONS.map(opt => <option key={opt} value={opt}>{opt}</option>)}
|
| | </select>
|
| | </div>
|
| | )}
|
| |
|
| | {(model === 'turbo' || model === 'banana') && (
|
| | <div className="form-group">
|
| | <label>Dimensiones (Ancho x Alto)</label>
|
| | <div style={{display:'flex', gap:'10px'}}>
|
| | <select value={width} onChange={(e) => setWidth(parseInt(e.target.value))} style={{flex:1}}>
|
| | <option value="512">512</option>
|
| | <option value="768">768</option>
|
| | <option value="1024">1024</option>
|
| | </select>
|
| | <select value={height} onChange={(e) => setHeight(parseInt(e.target.value))} style={{flex:1}}>
|
| | <option value="512">512</option>
|
| | <option value="768">768</option>
|
| | <option value="1024">1024</option>
|
| | </select>
|
| | </div>
|
| | </div>
|
| | )}
|
| |
|
| | {model === '3d_camera' && (
|
| | <div style={{display:'flex', flexDirection:'column', gap:'15px', background:'rgba(255,255,255,0.03)', padding:'15px', borderRadius:'12px', border:'1px solid var(--border)'}}>
|
| | <div className="form-group">
|
| | <label>Azimuth (Horizontal: {azimuth}°)</label>
|
| | <input type="range" min="-180" max="180" value={azimuth} onChange={(e) => setAzimuth(parseFloat(e.target.value))} />
|
| | </div>
|
| | <div className="form-group">
|
| | <label>Elevation (Vertical: {elevation}°)</label>
|
| | <input type="range" min="-90" max="90" value={elevation} onChange={(e) => setElevation(parseFloat(e.target.value))} />
|
| | </div>
|
| | <div className="form-group">
|
| | <label>Distance (Zoom: {distance})</label>
|
| | <input type="range" min="0.1" max="2.0" step="0.1" value={distance} onChange={(e) => setDistance(parseFloat(e.target.value))} />
|
| | </div>
|
| | </div>
|
| | )}
|
| |
|
| | {(model === 'qwen_rapid' || model === 'anypose') && (
|
| | <div className="form-group" style={{flexDirection:'row', alignItems:'center', gap:'10px', background:'rgba(255,255,255,0.03)', padding:'12px', borderRadius:'10px', border:'1px solid var(--border)', cursor:'pointer'}} onClick={() => setRewritePrompt(!rewritePrompt)}>
|
| | <input type="checkbox" checked={rewritePrompt} onChange={() => {}} style={{width:'18px', height:'18px', cursor:'pointer'}} />
|
| | <label style={{cursor:'pointer', margin:0, textTransform:'none'}}>Optimizar con Qwen-2.5-VL</label>
|
| | </div>
|
| | )}
|
| |
|
| | {model === 'anypose' && (
|
| | <div style={{fontSize:'0.7rem', color:'var(--primary)', padding:'10px', background:'rgba(255,77,77,0.05)', borderRadius:'8px', border:'1px solid rgba(255,77,77,0.2)'}}>
|
| | <i className="fa-solid fa-circle-info"></i> AnyPose requiere 2 imágenes: la 1ª es la Referencia y la 2ª es la Pose.
|
| | </div>
|
| | )}
|
| |
|
| | <div className="form-group">
|
| | <label>{(model === 'turbo' || model === 'banana') ? 'Prompt de Creación' : 'Prompt de Edición'}</label>
|
| | <textarea
|
| | placeholder={(model === 'turbo' || model === 'banana') ? "Describe la imagen que quieres crear..." : "Escribe los cambios deseados..."}
|
| | rows="3"
|
| | value={prompt}
|
| | onChange={(e) => setPrompt(e.target.value)}
|
| | />
|
| | </div>
|
| |
|
| | <div className="form-group">
|
| | <label>Pasos ({steps}) {(model !== 'turbo' && model !== 'banana') && `/ Guía (${guidance})`}</label>
|
| | <div style={{display:'flex', gap:'10px'}}>
|
| | <input type="range" min="1" max="50" value={steps} onChange={(e) => setSteps(parseInt(e.target.value))} style={{flex:1}} />
|
| | {(model !== 'turbo' && model !== 'banana') && (
|
| | <input type="range" min="0.1" max="10" step="0.1" value={guidance} onChange={(e) => setGuidance(parseFloat(e.target.value))} style={{flex:1}} />
|
| | )}
|
| | </div>
|
| | </div>
|
| |
|
| | <button className="btn-generate" onClick={generateImage} disabled={isGenerating}>
|
| | {isGenerating ? <div className="loader"></div> : <><i className="fa-solid fa-magic-wand-sparkles"></i> {(model === 'turbo' || model === 'banana') ? 'Generar Imagen' : 'Ejecutar IA'}</>}
|
| | </button>
|
| |
|
| | <div className="footer-info">
|
| | {model === 'firered' && 'Model: FireRed-Fast'}
|
| | {model === 'qwen' && 'Model: Qwen-2511-LoRA'}
|
| | {model === 'flux' && 'Model: FLUX.2-Klein'}
|
| | {model === 'turbo' && 'Model: Z-Image-Turbo'}
|
| | {model === 'banana' && 'Model: Nano-Banana'}
|
| | {model === 'qwen_rapid' && 'Model: Qwen-Rapid-AIO'}
|
| | {model === 'anypose' && 'Model: Qwen-AnyPose'}
|
| | {model === 'qwen3_vl' && 'Model: Qwen3-VL-MAX'}
|
| | {model === '3d_camera' && 'Model: Qwen-3D-Camera'}
|
| | </div>
|
| | </div>
|
| |
|
| | <div className="main-content">
|
| | {previews.length === 0 && model !== 'turbo' ? (
|
| | <label
|
| | className={`dropzone ${isDragging ? 'drag-over' : ''}`}
|
| | onDragOver={handleDragOver}
|
| | onDragLeave={handleDragLeave}
|
| | onDrop={handleDrop}
|
| | >
|
| | <input type="file" multiple hidden onChange={handleImageChange} accept="image/*" />
|
| | <i className="fa-solid fa-images"></i>
|
| | <h2>Galería de Entrada</h2>
|
| | <p>Arrastra y suelta imágenes aquí o haz clic para subir</p>
|
| | </label>
|
| | ) : (
|
| | <div style={{display: 'grid', gridTemplateColumns: (model === 'turbo' && previews.length === 0) ? '1fr' : '1fr 1fr', gap: '30px', height: '100%'}}>
|
| | {(previews.length > 0 || model !== 'turbo') && (
|
| | <div>
|
| | <label>Input Gallery ({previews.length})</label>
|
| | <div className="gallery-grid">
|
| | {previews.map((src, i) => (
|
| | <div key={i} className="gallery-item">
|
| | <img src={src} />
|
| | </div>
|
| | ))}
|
| | </div>
|
| | {previews.length > 0 && (
|
| | <button onClick={() => {setImages([]); setPreviews([]); setResult(null);}} style={{marginTop:'15px', background:'transparent', color:'var(--muted)', border:'none', cursor:'pointer', fontSize:'0.8rem'}}>
|
| | <i className="fa-solid fa-trash"></i> Limpiar todo
|
| | </button>
|
| | )}
|
| | {model === 'turbo' && previews.length === 0 && (
|
| | <div style={{padding:'40px', color:'var(--muted)', background:'var(--sidebar)', borderRadius:'15px', marginTop:'20px'}}>
|
| | <p><i className="fa-solid fa-info-circle"></i> En modo Turbo, no necesitas subir imágenes. Describe tu idea a la izquierda.</p>
|
| | </div>
|
| | )}
|
| | </div>
|
| | )}
|
| | <div style={{maxWidth: (model === 'turbo' && previews.length === 0) ? '800px' : 'none', margin: '0 auto', width: '100%'}}>
|
| | <label>AI Output</label>
|
| | <div className="result-container">
|
| | {isGenerating && (
|
| | <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:'15px', padding:'40px'}}>
|
| | <div className="loader" style={{width:'50px', height:'50px'}}></div>
|
| | <div style={{color:'var(--muted)', fontSize:'0.9rem'}}>Invocando a la IA...</div>
|
| | </div>
|
| | )}
|
| | {result && (
|
| | <>
|
| | <img src={`${API_URL}${result}`} className="result-img" />
|
| | <div style={{display:'flex', gap:'10px', width:'100%'}}>
|
| | <a href={`${API_URL}${result}`} download className="btn-generate" style={{background:'#22c55e', textDecoration:'none', flex:1}}>
|
| | <i className="fa-solid fa-download"></i> Descargar
|
| | </a>
|
| | <button onClick={useOutputAsInput} className="btn-generate" style={{background:'#3b82f6', flex:1}}>
|
| | <i className="fa-solid fa-arrow-left"></i> Usar como Entrada
|
| | </button>
|
| | </div>
|
| | </>
|
| | )}
|
| | {!result && !isGenerating && (
|
| | <div style={{padding:'60px 20px', color:'var(--muted)', border:'1px dashed var(--border)', borderRadius:'15px', textAlign:'center', width:'100%'}}>
|
| | <i className="fa-solid fa-wand-magic-sparkles" style={{fontSize:'2rem', marginBottom:'15px', opacity:0.3}}></i>
|
| | <br/>El resultado aparecerá aquí
|
| | </div>
|
| | )}
|
| | </div>
|
| | </div>
|
| | </div>
|
| | )}
|
| | </div>
|
| | </div>
|
| | );
|
| | }
|
| |
|
| | const root = ReactDOM.createRoot(document.getElementById('root'));
|
| | root.render(<App />);
|
| | </script>
|
| | </body>
|
| | </html>
|
| |
|