<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI PROMPT IDE v7.0 // DUAL CORE</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
/* --- ESTILOS DARK MODE IDE --- */
:root {
--bg: #050505; --panel: #111; --border: #252525;
--text: #eee; --dim: #666;
--nano-color: #00f2ff; /* Cian para Nanobana */
--veo-color: #ff9f1c; /* Naranja para Veo Video */
--active-color: var(--nano-color); /* Cambia dinámicamente */
--font: 'JetBrains Mono', monospace;
}
* { box-sizing: border-box; outline: none; }
body { background: var(--bg); color: var(--text); font-family: var(--font); margin: 0; display: flex; flex-direction: column; height: 100vh; font-size: 12px; }
/* --- NAVEGACIÓN SUPERIOR --- */
.top-nav { display: flex; background: #080808; border-bottom: 1px solid var(--border); height: 50px; }
.nav-item {
flex: 1; text-align: center; padding: 15px; cursor: pointer; font-weight: bold; letter-spacing: 2px;
text-transform: uppercase; color: var(--dim); transition: 0.3s; border-bottom: 2px solid transparent;
}
.nav-item:hover { color: var(--text); background: #111; }
.nav-item.active { color: var(--active-color); border-bottom: 2px solid var(--active-color); background: rgba(255,255,255,0.02); }
/* --- LAYOUT PRINCIPAL --- */
.main-layout { display: grid; grid-template-columns: 400px 1fr; gap: 0; flex-grow: 1; overflow: hidden; }
.input-panel { background: var(--panel); border-right: 1px solid var(--border); padding: 20px; overflow-y: auto; display: flex; flex-direction: column; }
.output-panel { background: #080808; padding: 20px; overflow-y: auto; }
/* --- COMPONENTES DE INPUT --- */
h2 { font-size: 11px; letter-spacing: 1px; color: var(--dim); border-bottom: 1px solid var(--border); padding-bottom: 5px; margin: 20px 0 15px 0; font-weight: 700; text-transform: uppercase;}
label { display: block; color: var(--dim); margin-bottom: 5px; font-size: 10px; font-weight: bold; text-transform: uppercase; }
input, select, textarea { width: 100%; background: #000; border: 1px solid var(--border); color: var(--text); padding: 10px; font-family: var(--font); font-size: 12px; margin-bottom: 15px; resize: vertical; }
input:focus, select:focus, textarea:focus { border-color: var(--active-color); }
/* ZONAS DE IMAGEN DINÁMICAS */
.mode-section { display: none; }
.mode-section.active { display: block; }
/* GESTOR DE ESCENAS (VEO) & PREVIEW (NANO) */
.preview-box { height: 120px; background: #000; border: 1px dashed var(--border); display: flex; align-items: center; justify-content: center; cursor: pointer; margin-bottom: 15px; position: relative; }
.preview-box:hover { border-color: var(--active-color); }
.preview-box img { max-width: 100%; max-height: 100%; display: none; }
.scene-slot { border: 1px solid var(--border); padding: 10px; margin-bottom: 10px; background: #0a0a0a; }
.btn-add { background: transparent; border: 1px dashed var(--border); color: var(--dim); width: 100%; padding: 8px; cursor: pointer; }
.btn-add:hover { border-color: var(--active-color); color: var(--active-color); }
.btn-del-scene { float: right; cursor: pointer; color: #666; } .btn-del-scene:hover { color: red; }
/* BOTON RUN */
.btn-run { background: var(--active-color); color: #000; font-weight: 800; padding: 15px; border: none; width: 100%; margin-top: auto; cursor: pointer; text-transform: uppercase; letter-spacing: 1px; transition: 0.3s; }
.btn-run:hover { filter: brightness(1.1); box-shadow: 0 0 15px var(--active-color); }
.btn-run:disabled { background: #333; color: #555; cursor: not-allowed; box-shadow: none; }
/* --- RESULTADOS (Consola) --- */
.result-block { border-left: 3px solid var(--active-color); padding-left: 15px; margin-bottom: 25px; background: rgba(255,255,255,0.02); padding: 15px; }
.result-header { color: var(--active-color); font-weight: bold; margin-bottom: 10px; font-size: 11px; display: flex; justify-content: space-between; }
.copy-btn { background: #222; border: 1px solid #333; color: var(--dim); padding: 3px 8px; font-size: 10px; cursor: pointer; }
.copy-btn:hover { color: var(--active-color); border-color: var(--active-color); }
/* Estilo para JSON */
pre.json-output { white-space: pre-wrap; color: #a5d6ff; font-size: 11px; }
.string { color: #ce9178; } .number { color: #b5cea8; } .boolean { color: #569cd6; } .null { color: #569cd6; } .key { color: #9cdcfe; }
</style>
</head>
<body>
<nav class="top-nav">
<div class="nav-item active" id="navNano" onclick="switchMode('nano')">NANOBANA (REALISTIC PHOTO)</div>
<div class="nav-item" id="navVeo" onclick="switchMode('veo')">VEO 3.1 (VIDEO SCENES)</div>
</nav>
<div class="main-layout">
<div class="input-panel">
<div>
<label>API KEY (GEMINI)</label>
<input type="password" id="apiKey" placeholder="Pegar AIza...">
</div>
<div>
<label>INSTRUCCIONES ADICIONALES (TEXTO)</label>
<textarea id="userInstructions" rows="4" placeholder="Ej: Usa la persona de la imagen, pero ponla en una cafetería de noche lloviendo. Que la iluminación sea dramática."></textarea>
</div>
<div>
<label>FORMATO DE SALIDA</label>
<select id="outputFormat">
<option value="text">PROMPT NORMAL (Raw Text)</option>
<option value="json">ESTRUCTURA JSON (Programático)</option>
</select>
</div>
<div id="sectionNano" class="mode-section active">
<h2>// REFERENCIA FOTOGRÁFICA</h2>
<div class="preview-box" onclick="document.getElementById('nanoFile').click()">
<input type="file" id="nanoFile" accept="image/*" style="display:none" onchange="handleNanoFile(this)">
<img id="nanoImg">
<span id="nanoPlaceholder" style="color:var(--dim)">+ SUBIR SUJETO</span>
</div>
</div>
<div id="sectionVeo" class="mode-section">
<h2>// REFERENCIAS POR ESCENA</h2>
<div id="scenesContainer"></div>
<button class="btn-add" onclick="addSceneSlot()">+ AÑADIR ESCENA DE VIDEO</button>
</div>
<button id="btnRun" class="btn-run" onclick="runAI()">GENERAR PROMPT</button>
</div>
<div class="output-panel" id="consoleOutput">
<div style="color:var(--dim)">> IDE listo. Selecciona modo y sube referencias...</div>
</div>
</div>
<script type="module">
import { GoogleGenerativeAI } from "https://esm.run/@google/generative-ai";
// --- ESTADO GLOBAL ---
let currentMode = 'nano';
let veoScenes = {};
let sceneCounter = 0;
let nanoFileRef = null;
const root = document.documentElement;
// --- INICIALIZACIÓN ---
window.onload = () => {
const k = localStorage.getItem('ide_key');
if(k) document.getElementById('apiKey').value = k;
switchMode('nano'); // Iniciar en modo foto
};
// --- LÓGICA DE CAMBIO DE MODO ---
window.switchMode = (mode) => {
currentMode = mode;
// Actualizar UI Nav
document.querySelectorAll('.nav-item').forEach(el => el.classList.remove('active'));
document.getElementById(mode === 'nano' ? 'navNano' : 'navVeo').classList.add('active');
// Actualizar Secciones Visibles
document.querySelectorAll('.mode-section').forEach(el => el.classList.remove('active'));
document.getElementById(mode === 'nano' ? 'sectionNano' : 'sectionVeo').classList.add('active');
// Actualizar Color Variables CSS
const newColor = mode === 'nano' ? 'var(--nano-color)' : 'var(--veo-color)';
root.style.setProperty('--active-color', newColor);
// Resetear estado del botón
checkRunButtonState();
};
// --- GESTIÓN DE ARCHIVOS (NANO) ---
window.handleNanoFile = (input) => {
if(input.files[0]) {
nanoFileRef = input.files[0];
const r = new FileReader();
r.onload = e => {
document.getElementById('nanoImg').src = e.target.result;
document.getElementById('nanoImg').style.display = 'block';
document.getElementById('nanoPlaceholder').style.display = 'none';
};
r.readAsDataURL(input.files[0]);
checkRunButtonState();
}
};
// --- GESTIÓN DE ESCENAS (VEO) ---
window.addSceneSlot = () => {
sceneCounter++;
const id = `s${sceneCounter}`;
const html = `
<div class="scene-slot" id="slot-${id}">
<div style="font-weight:bold; margin-bottom:5px; color:var(--veo-color)">ESCENA ${sceneCounter} <span class="btn-del-scene" onclick="delScene('${id}')">×</span></div>
<div class="preview-box" style="height:80px" onclick="document.getElementById('f-${id}').click()">
<input type="file" id="f-${id}" style="display:none" onchange="handleVeoFile(this, '${id}')">
<img id="img-${id}">
<span id="txt-${id}" style="color:var(--dim); font-size:10px;">+ REF VISUAL</span>
</div>
</div>`;
document.getElementById('scenesContainer').insertAdjacentHTML('beforeend', html);
checkRunButtonState();
};
window.handleVeoFile = (input, id) => {
if(input.files[0]) {
veoScenes[id] = input.files[0];
const r = new FileReader();
r.onload = e => {
document.getElementById(`img-${id}`).src = e.target.result;
document.getElementById(`img-${id}`).style.display = 'block';
document.getElementById(`txt-${id}`).style.display = 'none';
};
r.readAsDataURL(input.files[0]);
checkRunButtonState();
}
};
window.delScene = (id) => { document.getElementById(`slot-${id}`).remove(); delete veoScenes[id]; checkRunButtonState(); };
function checkRunButtonState() {
let ready = false;
if(currentMode === 'nano') ready = !!nanoFileRef;
if(currentMode === 'veo') ready = Object.keys(veoScenes).length > 0;
document.getElementById('btnRun').disabled = !ready;
document.getElementById('btnRun').innerText = ready ? (currentMode === 'nano' ? 'GENERAR FOTO PROMPT' : 'GENERAR VIDEO PROMPTS') : 'ESPERANDO REFERENCIAS...';
}
// --- NÚCLEO DE IA (EL CEREBRO) ---
window.runAI = async () => {
const key = document.getElementById('apiKey').value;
if(!key) return alert("Falta API Key");
localStorage.setItem('ide_key', key);
const userInstructions = document.getElementById('userInstructions').value;
const outputFormat = document.getElementById('outputFormat').value;
const consoleOut = document.getElementById('consoleOutput');
const btn = document.getElementById('btnRun');
btn.disabled = true;
btn.innerText = "PROCESANDO...";
consoleOut.innerHTML = `<div style="color:var(--dim)">> Iniciando análisis en modo ${currentMode.toUpperCase()}...</div>`;
const genAI = new GoogleGenerativeAI(key);
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
try {
let prompt = "";
let imageParts = [];
// 1. PREPARAR DATOS SEGÚN MODO
if(currentMode === 'nano') {
const base64 = await toBase64(nanoFileRef);
imageParts.push({ inlineData: { data: base64.split(',')[1], mimeType: nanoFileRef.type } });
prompt = `
ACT AS: Senior Prompt Engineer for Realistic Photography (Nanobana/SDXL).
TASK: Create a detailed prompt based on the image AND user instructions.
USER INSTRUCTIONS: "${userInstructions || 'Keep original appearance'}"
FOCUS: Forensic facial/body details intact, but apply user environment/action changes.
Style: Photorealistic, raw photo, 8k.
`;
} else {
// Modo Veo - Procesamos múltiples imágenes
prompt = `
ACT AS: AI Video Director for Veo 3.1 / Runway Gen-3.
TASK: Create sequential video prompts for ${Object.keys(veoScenes).length} scenes.
USER GLOBAL INSTRUCTIONS: "${userInstructions || 'Cinematic sequence'}"
INSTRUCTION: Analyze each image as a distinct scene reference. Maintain character consistency if the same person appears. apply user instructions to the overall mood/action.
`;
// Nota: Gemini Flash actualmente maneja mejor 1 imagen + prompt complejo.
// Para multiescena real, iteramos. Para este ejemplo, enviaremos la primera escena como referencia principal de estilo.
const firstSceneKey = Object.keys(veoScenes)[0];
if(firstSceneKey) {
const file = veoScenes[firstSceneKey];
const base64 = await toBase64(file);
imageParts.push({ inlineData: { data: base64.split(',')[1], mimeType: file.type } });
prompt += "\nNOTE: Use the provided image as the primary visual reference for style and subjects for SCENE 1.";
}
}
// 2. AÑADIR RESTRICCIÓN DE FORMATO
if(outputFormat === 'json') {
prompt += `
\nIMPORTANT: OUTPUT MUST BE PURE VALID JSON ONLY. NO MARKDOWN.
Structure needed:
{
"mode": "${currentMode}",
"prompts": [
{ "scene_id": 1, "prompt_text": "..." },
{ "scene_id": 2, "prompt_text": "..." } // if applicable
],
"technical_notes": "..."
}
`;
} else {
prompt += `\nOUTPUT FORMAT: Raw text prompts, separated by double newlines if multiple scenes. Start with tag list: (masterpiece, best quality)...`;
}
// 3. EJECUTAR
consoleOut.innerHTML += `<div style="color:var(--dim)">> Enviando a Gemini...</div>`;
const result = await model.generateContent([prompt, ...imageParts]);
const text = result.response.text();
// 4. RENDERIZAR RESULTADO
renderResult(text, outputFormat);
} catch (e) {
consoleOut.innerHTML += `<div style="color:red">> ERROR: ${e.message}</div>`;
} finally {
checkRunButtonState();
}
};
function renderResult(text, format) {
const consoleOut = document.getElementById('consoleOutput');
let contentHtml = '';
if(format === 'json') {
// Intentar formatear bonito el JSON
try {
const jsonObj = JSON.parse(text);
// Función simple para colorear sintaxis JSON
const jsonHtml = JSON.stringify(jsonObj, null, 2)
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
let cls = 'number';
if (/^"/.test(match)) {
if (/:$/.test(match)) { cls = 'key'; } else { cls = 'string'; }
} else if (/true|false/.test(match)) { cls = 'boolean'; } else if (/null/.test(match)) { cls = 'null'; }
return '<span class="' + cls + '">' + match + '</span>';
});
contentHtml = `<pre class="json-output">${jsonHtml}</pre>`;
} catch (e) {
contentHtml = `<div style="color:red">Error parseando JSON de la IA. Mostrando texto crudo:</div><pre>${text}</pre>`;
}
} else {
contentHtml = `<div class="prompt-content">${text}</div>`;
}
const html = `
<div class="result-block">
<div class="result-header">
SALIDA GENERADA [MODO: ${currentMode.toUpperCase()} | FMT: ${format.toUpperCase()}]
<button class="copy-btn" onclick="copiarTexto(this)">COPIAR</button>
</div>
${contentHtml}
</div>`;
consoleOut.innerHTML = html;
}
window.copiarTexto = (btn) => {
const content = btn.parentElement.nextElementSibling.innerText;
navigator.clipboard.writeText(content);
btn.innerText = "OK!"; setTimeout(()=>btn.innerText="COPIAR", 1000);
}
function toBase64(file) { return new Promise((r) => { const reader = new FileReader(); reader.onloadend = () => r(reader.result); reader.readAsDataURL(file); }); }
</script>
</body>
</html>