fivetech's picture
Update index.html
def8156 verified
Raw History Blame Contribute Delete
49.9 kB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Agent Debate - MiMo-V2.6-Distill-Qwen-9B</title>
<style>
:root{
--bg:#0b141a; --surface:#111b21; --surface2:#202c33; --border:#2a3942;
--text:#e9edef; --muted:#8696a0; --accent:#00a884;
--header:#202c33; --header-text:#e9edef; --header-sub:#8696a0;
--bubble-in:#202c33; --bubble-out:#005c4b; --sys:#2a3942; --sys-text:#ffd279;
--input:#2a3942; --track:rgba(255,255,255,.12); --disabled:#374248;
}
body.light{
--bg:#efeae2; --surface:#ffffff; --surface2:#f0f2f5; --border:#d1d7db;
--text:#111b21; --muted:#667781;
--header:#008069; --header-text:#ffffff; --header-sub:rgba(255,255,255,.75);
--bubble-in:#ffffff; --bubble-out:#d9fdd3; --sys:#fdf3c6; --sys-text:#54656f;
--input:#ffffff; --track:rgba(11,20,26,.08); --disabled:#aebac1;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;font-size:15px}
#app{max-width:1200px;margin:0 auto;display:flex;height:100vh;height:100dvh;
box-shadow:0 0 14px rgba(0,0,0,.4);background:var(--bg)}
/* --- Lista de contactos (agentes) --- */
#sidebar{width:300px;flex:none;display:flex;flex-direction:column;min-height:0;
background:var(--surface);border-right:1px solid var(--border)}
.side-header{display:flex;align-items:center;gap:10px;padding:10px 16px;
background:var(--header);color:var(--header-text)}
.side-logo{width:38px;height:38px;border-radius:50%;flex:none;font-size:19px;
background:linear-gradient(135deg,#00c49a,#025c4b);display:flex;align-items:center;justify-content:center;
box-shadow:0 0 10px rgba(0,168,132,.45)}
.side-title{font-weight:600;font-size:16px;letter-spacing:.1px}
.side-count{font-weight:400;color:var(--muted);font-size:12.5px;margin-left:4px}
#contacts{flex:1;overflow-y:auto;padding:6px 0}
.contact{display:flex;align-items:center;gap:12px;padding:9px 14px;transition:background .15s}
.contact:hover{background:var(--surface2)}
.contact.active{background:var(--surface2)}
.c-avatar{width:46px;height:46px;border-radius:50%;display:flex;align-items:center;justify-content:center;
font-size:22px;color:#fff;flex:none}
.c-info{flex:1;min-width:0;display:flex;flex-direction:column}
.c-name{font-size:15px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.c-role{font-size:12.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.contact.active .c-role{color:var(--accent);font-style:italic}
.c-dot{width:9px;height:9px;border-radius:50%;background:var(--accent);flex:none;box-shadow:0 0 6px var(--accent)}
/* --- Columna principal --- */
#main{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}
header{display:flex;align-items:center;gap:10px;padding:8px 16px;background:var(--header);color:var(--header-text);
border-bottom:none;box-shadow:0 1px 3px rgba(0,0,0,.3);z-index:2}
header .logo{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#00c49a,#025c4b);
display:flex;align-items:center;justify-content:center;font-size:19px;color:#fff;flex:none;
box-shadow:0 0 10px rgba(0,168,132,.45)}
header .title{font-weight:600;font-size:16px;letter-spacing:.1px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
header .subtitle{font-size:12px;color:var(--header-sub);font-weight:400}
#settings-btn{background:none;border:none;color:var(--header-text);border-radius:50%;
width:38px;height:38px;cursor:pointer;font-size:18px;display:flex;align-items:center;justify-content:center;transition:.15s}
#settings-btn:hover{background:rgba(255,255,255,.12)}
#chat{flex:1;overflow-y:auto;padding:20px 8%;display:flex;flex-direction:column;gap:8px;
background-color:var(--bg);
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Cg fill='none' stroke='%232a3942' stroke-width='1.4' opacity='0.55'%3E%3Ccircle cx='20' cy='20' r='6'/%3E%3Cpath d='M50 15h16a6 6 0 0 1 6 6v8a6 6 0 0 1-6 6h-9l-5 5v-5h-2a6 6 0 0 1-6-6v-8a6 6 0 0 1 6-6z'/%3E%3Cpath d='M15 60l4 4 8-8'/%3E%3Ccircle cx='95' cy='70' r='8'/%3E%3Cpath d='M40 95c4-6 12-6 16 0'/%3E%3Cpath d='M85 25l2.5 5 5 2.5-5 2.5-2.5 5-2.5-5-5-2.5 5-2.5z'/%3E%3Crect x='60' y='55' width='12' height='12' rx='3'/%3E%3Cpath d='M20 100q6-8 12 0'/%3E%3C/g%3E%3C/svg%3E")}
body.light #chat{
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Cg fill='none' stroke='%23d8d1c6' stroke-width='1.4' opacity='0.5'%3E%3Ccircle cx='20' cy='20' r='6'/%3E%3Cpath d='M50 15h16a6 6 0 0 1 6 6v8a6 6 0 0 1-6 6h-9l-5 5v-5h-2a6 6 0 0 1-6-6v-8a6 6 0 0 1 6-6z'/%3E%3Cpath d='M15 60l4 4 8-8'/%3E%3Ccircle cx='95' cy='70' r='8'/%3E%3Cpath d='M40 95c4-6 12-6 16 0'/%3E%3Cpath d='M85 25l2.5 5 5 2.5-5 2.5-2.5 5-2.5-5-5-2.5 5-2.5z'/%3E%3Crect x='60' y='55' width='12' height='12' rx='3'/%3E%3Cpath d='M20 100q6-8 12 0'/%3E%3C/g%3E%3C/svg%3E")}
#chat::-webkit-scrollbar,#contacts::-webkit-scrollbar{width:6px;height:6px}
#chat::-webkit-scrollbar-thumb,#contacts::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
.msg{display:flex;gap:8px;animation:fadein .2s ease;max-width:100%}
.msg .avatar{width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;
font-size:17px;color:#fff;flex:none;margin-top:0}
.msg .content{flex:1;min-width:0;max-width:75%;background:var(--bubble-in);border-radius:8px;padding:6px 9px 8px;
box-shadow:0 1px 1px rgba(0,0,0,.3)}
.msg .who{font-size:13px;font-weight:600;margin-bottom:2px}
.msg .who .round{font-weight:400;color:var(--muted);margin-left:6px;font-size:11px}
.msg .body{line-height:1.42;word-wrap:break-word;color:var(--text);font-size:14.5px}
.msg .body p{margin:.35em 0}
.msg.user{justify-content:flex-end}
.msg.user .content{flex:none;background:var(--bubble-out);border:none;
border-radius:8px;padding:7px 10px}
.msg.user .who{display:none}
.msg.err{align-self:center;max-width:92%;background:var(--sys);color:var(--sys-text);font-size:12.5px;
border-radius:8px;padding:6px 12px;text-align:center;box-shadow:0 1px 1px rgba(0,0,0,.3)}
.msg.sys{align-self:center;max-width:92%;background:var(--sys);color:var(--sys-text);font-size:12.5px;
border-radius:8px;padding:8px 14px;text-align:left;box-shadow:0 1px 1px rgba(0,0,0,.3)}
.msg.sys a{color:var(--accent);word-break:break-word}
#settings input[type=checkbox]{width:auto}
@keyframes fadein{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
.msg.streaming .body::after{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
background:var(--accent);margin-left:5px;animation:pulse 1s infinite}
details.think{margin:2px 0 6px;font-size:12.5px;color:var(--muted)}
details.think summary{cursor:pointer;font-size:12px;font-style:italic}
details.think .t{white-space:pre-wrap;border-left:3px solid var(--border);padding-left:10px;margin-top:5px;font-size:12.5px}
#controls{padding:8px 12px;background:var(--surface);display:flex;gap:8px;align-items:center;border-top:none}
#topic-sugg{flex:none;max-width:170px;background:var(--input);color:var(--muted);border:none;
border-radius:8px;padding:11px 8px;font-size:13px;cursor:pointer;outline:none}
#topic{flex:1;min-width:0;background:var(--input);border:none;color:var(--text);
border-radius:8px;padding:11px 14px;font-size:15px;outline:none}
#topic::placeholder{color:var(--muted)}
#start-btn{flex:none;background:var(--accent);color:#fff;border:none;border-radius:999px;padding:11px 18px;
cursor:pointer;font-weight:600;font-size:14px;transition:background .15s;white-space:nowrap;box-shadow:0 1px 2px rgba(0,0,0,.3)}
#start-btn:hover{background:#02b08a}
#start-btn:disabled{background:var(--disabled);color:var(--muted);cursor:default;box-shadow:none}
#stop-btn{flex:none;background:transparent;border:1px solid #ea0038;color:#ea0038;border-radius:999px;
padding:10px 16px;cursor:pointer;font-weight:600;font-size:14px;display:none}
#settings{display:none;margin:8px 12px 10px;padding:16px;border:1px solid var(--border);
border-radius:12px;background:var(--surface);box-shadow:0 2px 6px rgba(0,0,0,.3)}
#settings.open{display:block}
#settings label{display:block;font-size:12px;color:var(--muted);margin:10px 0 4px}
#settings input,#settings textarea,#settings select{width:100%;background:var(--input);color:var(--text);
border:1px solid var(--border);border-radius:8px;padding:9px 12px;font-size:14px;font-family:inherit;outline:none}
#settings input:focus,#settings textarea:focus,#settings select:focus{border-color:var(--accent)}
#settings textarea{min-height:150px;font-family:ui-monospace,"SF Mono",Menlo,monospace;font-size:12px}
#settings .row{display:flex;gap:10px;flex-wrap:wrap}
#settings .row>div{flex:1;min-width:120px}
#apply-settings{margin-top:12px;background:var(--accent);border:none;color:#fff;border-radius:999px;
padding:10px 18px;cursor:pointer;font-weight:600}
#status{padding:6px 16px;font-size:12px;color:var(--muted);background:var(--bg)}
#progress{height:3px;background:var(--track)}
#progress>div{height:100%;width:0;background:var(--accent);transition:width .25s}
footer{padding:10px 16px;font-size:11px;color:var(--muted);border-top:1px solid var(--border);background:var(--surface);
display:flex;gap:8px 16px;flex-wrap:wrap;justify-content:center;text-align:center}
footer a{color:var(--accent);text-decoration:none}
/* --- Editor interactivo de agente --- */
.contact{cursor:pointer}
#agent-modal{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:10;
align-items:center;justify-content:center;padding:16px}
#agent-modal.open{display:flex}
#agent-modal .card{width:100%;max-width:420px;max-height:90vh;overflow-y:auto;background:var(--surface);
border:1px solid var(--border);border-radius:12px;padding:16px;box-shadow:0 8px 24px rgba(0,0,0,.4)}
#agent-modal h3{margin:0 0 4px;font-size:16px;color:var(--text)}
#agent-modal .hint{font-size:12px;color:var(--muted);margin-bottom:4px}
#agent-modal label{display:block;font-size:12px;color:var(--muted);margin:10px 0 4px}
#agent-modal input,#agent-modal textarea{width:100%;background:var(--input);color:var(--text);
border:1px solid var(--border);border-radius:8px;padding:9px 12px;font-size:14px;font-family:inherit;outline:none}
#agent-modal input:focus,#agent-modal textarea:focus{border-color:var(--accent)}
#agent-modal textarea{min-height:110px;resize:vertical}
#agent-modal .row{display:flex;gap:10px}
#agent-modal .row>div{flex:1;min-width:0}
#agent-emoji{text-align:center}
#agent-color{padding:4px;height:41px;cursor:pointer}
#agent-modal .btns{display:flex;gap:10px;margin-top:14px;justify-content:flex-end}
#agent-save{background:var(--accent);border:none;color:#fff;border-radius:999px;padding:10px 18px;cursor:pointer;font-weight:600}
#agent-cancel{background:transparent;border:1px solid var(--border);color:var(--muted);border-radius:999px;
padding:10px 18px;cursor:pointer;font-weight:600}
.add-av{background:transparent !important;border:2px dashed var(--border);color:var(--muted);
font-size:22px;font-weight:400;box-shadow:none !important}
.add-contact:hover .add-av{border-color:var(--accent);color:var(--accent)}
.add-contact:hover .c-name{color:var(--accent)}
#agent-delete{background:transparent;border:1px solid #ea0038;color:#ea0038;border-radius:999px;
padding:10px 18px;cursor:pointer;font-weight:600;margin-right:auto}
/* --- Historial de temas --- */
#history{flex:none;max-height:34%;display:flex;flex-direction:column;border-top:1px solid var(--border)}
.hist-header{display:flex;align-items:center;justify-content:space-between;padding:8px 16px 4px;
font-size:12.5px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.4px}
#hist-clear{background:none;border:none;color:var(--muted);cursor:pointer;font-size:14px;padding:2px 4px;border-radius:6px}
#hist-clear:hover{color:var(--text);background:var(--surface2)}
#hist-list{overflow-y:auto;padding:0 8px 8px}
.hist-item{padding:7px 10px;border-radius:8px;cursor:pointer}
.hist-item:hover{background:var(--surface2)}
.hist-topic{font-size:13.5px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hist-meta{font-size:11.5px;color:var(--muted);margin-top:1px}
.hist-empty{font-size:12px;color:var(--muted);font-style:italic;padding:6px 10px}
@media (max-width:760px){
#app{flex-direction:column;max-width:none}
#sidebar{width:100%;flex:none;border-right:none;border-bottom:1px solid var(--border)}
.side-header{display:none}
#contacts{display:flex;overflow-x:auto;padding:8px 10px;gap:6px}
.contact{flex:none;flex-direction:column;gap:4px;padding:6px 10px;min-width:64px;justify-content:center}
.contact .c-info{align-items:center;text-align:center}
.c-role{display:none}
.c-dot{display:none}
.c-avatar{width:44px;height:44px;font-size:21px}
#chat{padding:12px 4%;gap:7px}
#controls{padding:8px 8px}
header{padding:8px 10px}
header .subtitle{display:none}
.msg .avatar{width:32px;height:32px;font-size:16px}
.msg .content{max-width:85%}
#settings{margin:0 8px 8px}
#history{display:none}
#topic-sugg{max-width:110px}
}
</style>
</head>
<body>
<div id="app">
<aside id="sidebar">
<div class="side-header">
<span class="side-logo">&#129302;</span>
<span class="side-title">Agentes<span class="side-count" id="side-count">(3)</span></span>
</div>
<div id="contacts"></div>
<div id="history">
<div class="hist-header"><span>Temas debatidos</span><button id="hist-clear" title="Borrar historial">&#128465;</button></div>
<div id="hist-list"></div>
</div>
</aside>
<div id="main">
<header>
<span class="logo">&#129302;</span>
<span class="title" id="title">Multi-Agent Debate <span class="subtitle" id="subtitle">Ada, Bruno, Carla, T&uacute;</span></span>
<button id="settings-btn" title="Ajustes">&#9881;</button>
</header>
<div id="settings">
<div class="row">
<div><label>Repositorio Hugging Face</label><input id="set-repo"></div>
<div><label>Archivo GGUF</label><input id="set-file"></div>
</div>
<div class="row">
<div><label>Contexto (tokens)</label><input id="set-ctx" type="number" step="512" min="2048"></div>
<div><label>M&aacute;x. tokens por turno</label><input id="set-maxtokens" type="number" step="32" min="64"></div>
<div><label>Temperatura</label><input id="set-temp" type="number" step="0.05" min="0" max="2"></div>
<div><label>Rondas</label><input id="set-rounds" type="number" step="1" min="1" max="20"></div>
<div><label>Orden de turnos</label><select id="set-order"><option value="seq">Secuencial</option><option value="rand">Aleatorio por ronda</option></select></div>
</div>
<div class="row">
<div><label style="display:flex;align-items:center;gap:8px;cursor:pointer;margin-top:10px"><input id="set-search" type="checkbox"> 🌐 B&uacute;squeda web al empezar un tema (Tavily sin clave + Wikipedia; solo se env&iacute;a el tema, nunca la conversaci&oacute;n)</label></div>
</div>
<label>Agentes (JSON: name, emoji, color, system)</label>
<textarea id="set-agents" spellcheck="false"></textarea>
<button id="apply-settings">Aplicar</button>
</div>
<div id="chat"></div>
<div id="controls">
<select id="topic-sugg" title="Temas sugeridos"></select>
<input id="topic" placeholder="Propón un tema y los agentes debaten&hellip;">
<button id="start-btn" disabled>Cargando modelo&hellip;</button>
<button id="stop-btn">Detener</button>
</div>
<div id="status">Iniciando&hellip;</div>
<div id="progress"><div></div></div>
<footer>
<span>La conversación se procesa en tu navegador (WebGPU si está disponible; si no, CPU/WASM). Solo se conecta a Hugging Face para descargar el modelo (5,8 GB la primera vez) y a jsDelivr para las librerías. Si la búsqueda web está activada, solo el tema se envía a Tavily (modo sin clave) y a Wikipedia para traer contexto y fuentes.</span>
<span>Motor: <a href="https://github.com/ngxson/wllama" target="_blank" rel="noopener">wllama</a> (MIT) &middot; inspirado en el widget de <a href="https://localellm.com" target="_blank" rel="noopener">localellm.com</a> &middot; modelo <a href="https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Distill-Qwen-9B" target="_blank" rel="noopener">MiMo-V2.6-Distill-Qwen-9B</a> (MIT, Xiaomi)</span>
</footer>
</div>
</div>
<div id="agent-modal">
<div class="card">
<h3 id="agent-modal-title">Editar agente</h3>
<div class="hint">Cambia su nombre y sus cualidades; se aplica al instante en esta sesión, igual que los Ajustes.</div>
<div class="row">
<div style="flex:2"><label>Nombre</label><input id="agent-name"></div>
<div><label>Avatar (emoji)</label><input id="agent-emoji" maxlength="4"></div>
<div><label>Color</label><input id="agent-color" type="color"></div>
</div>
<label>Cualidades (personalidad e instrucciones)</label>
<textarea id="agent-system" spellcheck="false" placeholder="Ej.: Eres Dora, la optimista del grupo. Ves oportunidades en cada idea y animas al grupo a probar."></textarea>
<div class="btns">
<button id="agent-delete">Eliminar</button>
<button id="agent-cancel">Cancelar</button>
<button id="agent-save">Guardar</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script type="module">
import { Wllama } from 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/index.js';
const params = new URLSearchParams(location.search);
const DEFAULT_AGENTS = [
{ name: 'Ada', emoji: '\u{1F4A1}', color: '#58a6ff',
system: 'Eres Ada, la analista entusiasta del grupo. Defiendes las ideas con argumentos concretos, propones caminos a seguir y reconoces los puntos fuertes de los dem\u00e1s.' },
{ name: 'Bruno', emoji: '\u{1F9D0}', color: '#f0883e',
system: 'Eres Bruno, el esc\u00e9ptico del grupo. Cuestionas supuestos, se\u00f1alas riesgos y fallos l\u00f3gicos, y pides evidencia. Eres duro con las ideas y respetuoso con las personas.' },
{ name: 'Carla', emoji: '\u{1F3AF}', color: '#3fb950',
system: 'Eres Carla, la moderadora. Resume lo dicho, integra los desacuerdos y propone s\u00edntesis o el siguiente paso. Cierra cada ronda con una conclusi\u00f3n breve.' },
];
const CONFIG = {
modelUrl: params.get('modelUrl') || '',
repo: params.get('repo') || 'bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF',
file: params.get('file') || 'MiMo-V2.6-Distill-Qwen-9B-Q4_K_M.gguf',
nCtx: parseInt(params.get('ctx')) || 8192,
maxTokens: parseInt(params.get('maxTokens')) || 384,
temperature: parseFloat(params.get('temp')) || 0.7,
rounds: parseInt(params.get('rounds')) || 2,
autoload: params.get('autoload') !== 'false',
order: params.get('order') === 'rand' ? 'rand' : 'seq',
search: (() => {
const p = params.get('search');
if (p === '1') return true;
if (p === '0') return false;
try { const v = localStorage.getItem('mad_search'); if (v !== null) return v === '1'; } catch (e) {}
return true;
})(),
};
if (params.get('theme') === 'light') document.body.classList.add('light');
const AGENTS_KEY = 'mad_agents';
function loadStoredAgents() {
try {
const a = JSON.parse(localStorage.getItem(AGENTS_KEY));
return (Array.isArray(a) && a.length >= 2 && a.every(x => x && x.name && x.system)) ? a : null;
} catch (e) { return null; }
}
function persistAgents() {
try { localStorage.setItem(AGENTS_KEY, JSON.stringify(agents)); } catch (e) {}
}
let agents = loadStoredAgents() || DEFAULT_AGENTS;
if (params.get('agents')) {
try { agents = JSON.parse(params.get('agents')); } catch (e) { console.warn('agents JSON inv\u00e1lido', e); }
}
const chatEl = document.getElementById('chat');
const statusEl = document.getElementById('status');
const progressEl = document.getElementById('progress').firstElementChild;
const topicEl = document.getElementById('topic');
const startBtn = document.getElementById('start-btn');
const stopBtn = document.getElementById('stop-btn');
const contactsEl = document.getElementById('contacts');
const subtitleEl = document.getElementById('subtitle');
let wllama = null;
let running = false;
let stopRequested = false;
let transcript = []; // {name|null(user), text}
let topic = '';
function setStatus(t) { statusEl.textContent = t; }
function setProgress(p) { progressEl.style.width = Math.max(0, Math.min(100, p)) + '%'; }
function esc(s){ const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
// --- Avatares y lista de contactos ---
function shade(hex, amt) {
const n = parseInt(String(hex).replace('#','').slice(0,6), 16);
if (isNaN(n)) return hex;
const f = v => Math.max(0, Math.min(255, v + amt));
const r = f(n >> 16), g = f((n >> 8) & 255), b = f(n & 255);
return '#' + ((r << 16) | (g << 8) | b).toString(16).padStart(6, '0');
}
function avatarBg(color) {
return `linear-gradient(135deg, ${shade(color, 34)} 0%, ${color} 55%, ${shade(color, -38)} 100%)`;
}
function roleOf(agent) {
const m = /Eres\s+[^,]+,\s*([^\.]+)\./i.exec(agent.system || '');
let r = m ? m[1].trim() : 'Agente del debate';
r = r.replace(/\s+del grupo\s*$/i, '').replace(/^(el|la|los|las)\s+/i, '');
return r ? r.charAt(0).toUpperCase() + r.slice(1) : 'Agente del debate';
}
function renderContacts() {
contactsEl.innerHTML = '';
agents.forEach((a, i) => {
const c = document.createElement('div');
c.className = 'contact';
c.dataset.name = a.name;
c.title = 'Editar agente';
c.addEventListener('click', () => openAgentEditor(i));
const av = document.createElement('div');
av.className = 'c-avatar';
av.style.background = avatarBg(a.color);
av.style.boxShadow = `0 0 12px ${a.color}66`;
av.textContent = a.emoji;
const info = document.createElement('div');
info.className = 'c-info';
const nm = document.createElement('div');
nm.className = 'c-name'; nm.textContent = a.name;
const rl = document.createElement('div');
rl.className = 'c-role'; rl.textContent = roleOf(a);
info.appendChild(nm); info.appendChild(rl);
const dot = document.createElement('div');
dot.className = 'c-dot';
c.appendChild(av); c.appendChild(info); c.appendChild(dot);
contactsEl.appendChild(c);
});
const add = document.createElement('div');
add.className = 'contact add-contact';
add.title = 'A\u00f1adir agente';
add.innerHTML = '<div class="c-avatar add-av">+</div><div class="c-info">' +
'<div class="c-name">A\u00f1adir agente</div><div class="c-role">Nuevo participante</div></div>';
add.addEventListener('click', () => openAgentEditor(-1));
contactsEl.appendChild(add);
document.getElementById('side-count').textContent = `(${agents.length})`;
subtitleEl.textContent = agents.map(a => a.name).join(', ') + ', T\u00fa';
}
function markActive(name) {
contactsEl.querySelectorAll('.contact').forEach(c => {
const on = c.dataset.name === name;
c.classList.toggle('active', on);
const rl = c.querySelector('.c-role');
if (on) { if (!rl.dataset.orig) rl.dataset.orig = rl.textContent; rl.textContent = 'escribiendo\u2026'; }
else if (rl.dataset.orig) { rl.textContent = rl.dataset.orig; delete rl.dataset.orig; }
});
}
// --- Editor interactivo de agentes (editar, a\u00f1adir, quitar) ---
const agentModal = document.getElementById('agent-modal');
const deleteBtn = document.getElementById('agent-delete');
let editingAgent = -1;
function openAgentEditor(i) {
editingAgent = i;
const a = agents[i] || {};
document.getElementById('agent-modal-title').textContent = i >= 0 ? 'Editar agente' : 'A\u00f1adir agente';
deleteBtn.style.display = i >= 0 ? 'inline-block' : 'none';
document.getElementById('agent-name').value = a.name || '';
document.getElementById('agent-emoji').value = a.emoji || '\u{1F916}';
document.getElementById('agent-color').value = /^#[0-9a-fA-F]{6}$/.test(a.color || '') ? a.color : '#bf5af2';
document.getElementById('agent-system').value = a.system || '';
agentModal.classList.add('open');
document.getElementById('agent-name').focus();
}
document.getElementById('agent-cancel').addEventListener('click', () => agentModal.classList.remove('open'));
agentModal.addEventListener('click', e => { if (e.target === agentModal) agentModal.classList.remove('open'); });
deleteBtn.addEventListener('click', () => {
const a = agents[editingAgent];
if (!a) return;
if (agents.length <= 2) { alert('Se necesitan al menos 2 agentes.'); return; }
if (!confirm(`\u00bfEliminar a ${a.name} del debate?`)) return;
agents.splice(editingAgent, 1);
persistAgents();
renderContacts();
agentModal.classList.remove('open');
setStatus(`Agente eliminado: ${a.name}. Quedan ${agents.length}.`);
});
document.getElementById('agent-save').addEventListener('click', () => {
const name = document.getElementById('agent-name').value.trim();
if (!name) { alert('El nombre no puede estar vac\u00edo.'); return; }
const emoji = document.getElementById('agent-emoji').value.trim() || '\u{1F916}';
const color = document.getElementById('agent-color').value;
const system = document.getElementById('agent-system').value.trim();
if (!system) { alert('Describe las cualidades del agente.'); return; }
if (editingAgent >= 0) agents[editingAgent] = { name, emoji, color, system };
else agents.push({ name, emoji, color, system });
persistAgents();
renderContacts();
agentModal.classList.remove('open');
setStatus(editingAgent >= 0 ? `Agente actualizado: ${name}.` : `Agente a\u00f1adido: ${name}. Ya son ${agents.length}.`);
});
// --- Historial de temas (localStorage) ---
const HIST_KEY = 'mad_history';
const histListEl = document.getElementById('hist-list');
function loadHistory() {
try { return JSON.parse(localStorage.getItem(HIST_KEY)) || []; } catch (e) { return []; }
}
function saveHistory(h) {
h = h.slice(0, 30);
for (let attempt = 0; attempt < 3; attempt++) {
try { localStorage.setItem(HIST_KEY, JSON.stringify(h)); return; } catch (e) {}
let trimmed = false;
h.forEach(it => (it.msgs || []).forEach(m => {
if (m.text.length > 800) { m.text = m.text.slice(0, 800); trimmed = true; }
}));
if (!trimmed) h.forEach(it => { delete it.msgs; delete it.ctx; delete it.sources; });
}
}
function renderHistory() {
const h = loadHistory();
histListEl.innerHTML = '';
if (!h.length) {
const e = document.createElement('div');
e.className = 'hist-empty';
e.textContent = 'Los temas que debatas se guardar\u00e1n aqu\u00ed, en este navegador.';
histListEl.appendChild(e);
return;
}
h.forEach(item => {
const d = document.createElement('div');
d.className = 'hist-item';
d.title = (Array.isArray(item.msgs) && item.msgs.length) ? 'Recuperar esta conversaci\u00f3n' : 'Reutilizar este tema';
const t = document.createElement('div');
t.className = 'hist-topic'; t.textContent = item.topic;
const m = document.createElement('div');
m.className = 'hist-meta';
m.textContent = new Date(item.ts).toLocaleString('es-ES', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' }) +
' \u00b7 ' + item.agents + ' agentes' +
((Array.isArray(item.msgs) && item.msgs.length) ? ' \u00b7 ' + item.msgs.filter(x => x.name).length + ' interv.' : '');
d.appendChild(t); d.appendChild(m);
d.addEventListener('click', () => {
if (running) return;
const fresh = loadHistory().find(x => x.ts === item.ts) || item;
if (Array.isArray(fresh.msgs) && fresh.msgs.length) restoreConversation(fresh);
else { topicEl.value = fresh.topic; topicEl.focus(); }
});
histListEl.appendChild(d);
});
}
function recordTopic(topicText) {
const h = loadHistory();
if (h[0] && h[0].topic === topicText) {
h[0].ts = Date.now();
h[0].agents = agents.length;
saveHistory(h);
renderHistory();
return h[0].ts;
}
const ts = Date.now();
h.unshift({ topic: topicText, ts: ts, agents: agents.length });
saveHistory(h);
renderHistory();
return ts;
}
// --- Guardado y restauracion de la conversacion completa ---
let currentHistId = null;
function saveTranscriptToHistory() {
if (!currentHistId || !transcript.length) return;
const h = loadHistory();
const it = h.find(x => x.ts === currentHistId);
if (!it) return;
it.msgs = transcript.slice(-80).map(m => ({ name: m.name, text: String(m.text).slice(0, 4000) }));
it.agents = agents.length;
if (webContext) it.ctx = webContext; else delete it.ctx;
if (webSources.length) it.sources = webSources; else delete it.sources;
saveHistory(h);
renderHistory();
}
window.addEventListener('beforeunload', saveTranscriptToHistory);
function restoreConversation(item) {
saveTranscriptToHistory();
topic = item.topic;
topicEl.value = item.topic;
transcript = item.msgs.map(m => ({ name: m.name, text: m.text }));
chatEl.innerHTML = '';
resume = { round: 1, order: null, pos: 0 };
currentHistId = item.ts;
webContext = item.ctx || '';
webSources = Array.isArray(item.sources) ? item.sources : [];
const um = addMsg(null, true);
um.body.textContent = item.topic;
let n = 0;
transcript.forEach(m => {
if (!m.name) return;
n++;
const a = agents.find(x => x.name === m.name) || { name: m.name, emoji: '\u{1F916}', color: '#8b949e' };
const r = addMsg(a);
r.body.innerHTML = (typeof marked !== 'undefined') ? marked.parse(m.text) : esc(m.text).replace(/\n/g, '<br>');
});
if (webSources.length) renderSourcesMsg(webSources, []);
setStatus('Conversaci\u00f3n recuperada (' + n + ' intervenciones). Puedes continuar el debate.');
}
document.getElementById('hist-clear').addEventListener('click', () => {
if (!loadHistory().length) return;
if (!confirm('\u00bfBorrar el historial de temas?')) return;
saveHistory([]);
renderHistory();
});
renderContacts();
renderHistory();
function addMsg(agent, isUser = false) {
const d = document.createElement('div');
d.className = 'msg' + (isUser ? ' user' : '');
const content = document.createElement('div');
content.className = 'content';
const who = document.createElement('div');
who.className = 'who';
const body = document.createElement('div');
body.className = 'body';
if (isUser) { who.textContent = 'T\u00fa'; }
else {
const av = document.createElement('div');
av.className = 'avatar';
av.style.background = avatarBg(agent.color);
av.style.boxShadow = '0 0 8px ' + agent.color + '55';
av.textContent = agent.emoji;
d.appendChild(av);
who.textContent = agent.name;
who.style.color = agent.color;
}
content.appendChild(who); content.appendChild(body);
d.appendChild(content);
chatEl.appendChild(d);
chatEl.scrollTop = chatEl.scrollHeight;
return { el: d, body, content };
}
function addErr(text) {
const d = document.createElement('div');
d.className = 'msg err';
d.textContent = text;
chatEl.appendChild(d);
}
let webContext = ''; // bloque de contexto web inyectado en los prompts del debate actual
let webSources = []; // fuentes numeradas del debate actual
function addSys(build) {
const d = document.createElement('div');
d.className = 'msg sys';
const body = document.createElement('div');
body.className = 'body';
d.appendChild(body);
chatEl.appendChild(d);
build(body);
chatEl.scrollTop = chatEl.scrollHeight;
return d;
}
const SEARCH_TIMEOUT_MS = 12000;
async function fetchJson(url, opts) {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), SEARCH_TIMEOUT_MS);
try {
const r = await fetch(url, Object.assign({}, opts, { signal: ctrl.signal }));
return { ok: r.ok, status: r.status, json: r.ok ? await r.json().catch(() => null) : null };
} finally { clearTimeout(t); }
}
// Solo se envia el tema (query); nunca la conversacion. Sin claves: Tavily keyless
// (doc oficial: docs.tavily.com/documentation/keyless) y Wikipedia action API (origin=*).
const SEARCH_STOPWORDS = new Set(('sobre entre contra desde donde cuando cuanto mejor peor debe deberia ' +
'puede podria conviene hacer tener porque para como cuales cual estos estas esto esta aquellos aquellas ' +
'merece pena aprender espanol idioma cosas algo cada muy mas').split(' '));
function normTxt(t) { return String(t).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''); }
function topicTerms(q) {
return normTxt(q).replace(/[^a-z0-9\u00f1\s]/g, ' ').split(/\s+/)
.filter(w => w.length >= 4 && !SEARCH_STOPWORDS.has(w))
.sort((a, b) => b.length - a.length);
}
async function fetchWebContext(query) {
const sources = [];
const failed = [];
try {
const r = await fetchJson('https://api.tavily.com/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Tavily-Access-Mode': 'keyless' },
body: JSON.stringify({ query: query, max_results: 4 }),
});
if (r.ok && r.json && Array.isArray(r.json.results)) {
for (const x of r.json.results) {
if (x && x.url && x.title) sources.push({ title: String(x.title), url: String(x.url), snippet: String(x.content || '').slice(0, 280) });
}
if (!r.json.results.length) failed.push('Tavily no devolvi\u00f3 resultados');
} else {
failed.push('Tavily respondi\u00f3 ' + r.status + (r.status === 429 ? ' (l\u00edmite gratuito alcanzado; prueba m\u00e1s tarde)' : ''));
}
} catch (e) { failed.push('Tavily no accesible (' + (e && e.name === 'AbortError' ? 'tiempo de espera' : 'red') + ')'); }
try {
const u = 'https://es.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srlimit=3&srsearch=' + encodeURIComponent(query);
const r = await fetchJson(u);
if (r.ok && r.json && r.json.query && Array.isArray(r.json.query.search)) {
for (const x of r.json.query.search) {
const t = String(x && x.title || '');
if (!t) continue;
sources.push({ title: 'Wikipedia: ' + t, url: 'https://es.wikipedia.org/wiki/' + encodeURIComponent(t.replace(/ /g, '_')), snippet: String(x.snippet || '').replace(/<[^>]+>/g, '').slice(0, 220) });
}
if (!r.json.query.search.length) failed.push('Wikipedia sin resultados');
} else {
failed.push('Wikipedia respondi\u00f3 ' + r.status);
}
} catch (e) { failed.push('Wikipedia no accesible (' + (e && e.name === 'AbortError' ? 'tiempo de espera' : 'red') + ')'); }
const top = topicTerms(query).slice(0, 2);
const missing = top.filter(t => !sources.some(x => normTxt(x.title + ' ' + x.snippet).includes(t)));
return { sources: sources.slice(0, 6), failed: failed, missing: missing };
}
function renderSourcesMsg(sources, failed) {
addSys(b => {
const t = document.createElement('div');
t.textContent = '\u{1F4DA} Fuentes de este debate (los agentes pueden citarlas como [1], [2]\u2026):';
b.appendChild(t);
const ol = document.createElement('ol');
ol.style.margin = '6px 0 0 18px';
sources.forEach(s => {
const li = document.createElement('li');
const a = document.createElement('a');
if (/^https?:\/\//i.test(s.url)) { a.href = s.url; a.target = '_blank'; a.rel = 'noopener'; }
a.textContent = s.title;
li.appendChild(a);
ol.appendChild(li);
});
b.appendChild(ol);
if (failed && failed.length) {
const f = document.createElement('div');
f.style.marginTop = '4px';
f.textContent = '\u26a0\uFE0F ' + failed.join(' \u00b7 ');
b.appendChild(f);
}
});
}
async function prepareWebContext(q) {
addSys(b => { b.textContent = '\u{1F50E} Buscando contexto web sobre \u00ab' + q + '\u00bb (solo se env\u00eda el tema a Tavily y Wikipedia)\u2026'; });
let res;
try { res = await fetchWebContext(q); }
catch (e) { res = { sources: [], failed: ['error inesperado'] }; }
webSources = res.sources;
if (webSources.length) {
webContext = webSources.map((s, i) => '[' + (i + 1) + '] ' + s.title + ' \u2014 ' + s.url + '\n' + s.snippet).join('\n');
if (webContext.length > 1600) webContext = webContext.slice(0, 1600);
const warns = res.failed.slice();
if (res.missing && res.missing.length) {
warns.push('Ninguna fuente encontrada menciona \u00ab' + res.missing.join('\u00bb ni \u00ab') +
'\u00bb: es contexto general, no evidencia espec\u00edfica de ese t\u00e9rmino.');
}
renderSourcesMsg(webSources, warns);
} else {
webContext = '';
addSys(b => { b.textContent = '\u26a0\uFE0F B\u00fasqueda web sin resultados ahora (' + (res.failed.join(' \u00b7 ') || 'error') + '). El debate contin\u00faa sin contexto web.'; });
}
}
async function loadModel(repo, file) {
const url = CONFIG.modelUrl || `https://huggingface.co/${repo}/resolve/main/${file}`;
startBtn.disabled = true;
setProgress(0);
setStatus(`Descargando ${file} (ctx ${CONFIG.nCtx})\u2026`);
try {
if (wllama) await wllama.exit();
wllama = new Wllama({
'default': 'https://cdn.jsdelivr.net/npm/@wllama/wllama@3.5.1/esm/wasm/wllama.wasm',
}, { parallelDownloads: 5 });
await wllama.loadModelFromUrl(url, {
n_gpu_layers: 999,
n_ctx: CONFIG.nCtx,
n_batch: 512,
n_threads: Math.max(1, Math.min(8, navigator.hardwareConcurrency || 4)),
progressCallback: ({ loaded, total }) => {
if (total > 0) {
const pct = Math.round((loaded / total) * 100);
setProgress(pct);
setStatus(`Descargando\u2026 ${pct}% (${(loaded / 1048576).toFixed(0)} MB)`);
}
},
});
setProgress(100);
setStatus(`Modelo listo. ${agents.length} agentes: ${agents.map(a => a.name).join(', ')}.`);
startBtn.disabled = false;
startBtn.textContent = 'Iniciar debate';
topicEl.focus();
} catch (e) {
console.error(e);
setStatus('Error al cargar el modelo');
addErr('No se pudo cargar el modelo: ' + (e?.message || e));
const retry = document.createElement('button');
retry.textContent = 'Limpiar cach\u00e9 y reintentar';
retry.style.cssText = 'margin-top:6px;background:var(--accent);border:none;color:#fff;border-radius:999px;padding:8px 14px;cursor:pointer;font-weight:700';
retry.addEventListener('click', async () => {
retry.disabled = true; retry.textContent = 'Limpiando\u2026';
try {
if ('caches' in window) for (const k of await caches.keys()) await caches.delete(k);
if (indexedDB.databases) for (const db of await indexedDB.databases())
await new Promise(r => { const q = indexedDB.deleteDatabase(db.name); q.onsuccess = q.onerror = q.onblocked = r; });
if (navigator.storage?.estimate) {
const dir = await navigator.storage.getDirectory().catch(() => null);
if (dir) for await (const k of dir.keys()) await dir.removeEntry(k, { recursive: true }).catch(() => {});
}
} catch (_) {}
loadModel(repo, file);
});
const errBox = chatEl.lastElementChild;
if (errBox) { errBox.appendChild(document.createElement('br')); errBox.appendChild(retry); }
}
}
const RULES = 'Est\u00e1s en una conversaci\u00f3n entre varios agentes. ' +
'Responde SOLO con tu intervenci\u00f3n: 2-5 frases, directas, en el idioma de la conversaci\u00f3n. ' +
'No repitas lo ya dicho; reacciona a la \u00faltima intervenci\u00f3n. ' +
'No escribas tu nombre ni ning\u00fan prefijo como "Nombre:". No uses listas largas.';
function transcriptText(maxChars = 9000) {
const lines = transcript.map(m => `${m.name || 'Usuario'}: ${m.text}`);
let out = lines.join('\n\n');
while (out.length > maxChars && lines.length > 2) {
lines.splice(1, 1); // conserva el tema (primera l\u00ednea) y lo m\u00e1s reciente
out = lines.join('\n\n');
}
return out;
}
async function agentTurn(agent) {
const view = transcriptText();
const messages = [
{ role: 'system', content: agent.system + ' ' + RULES },
{ role: 'user', content:
`Tema del debate: ${topic}\n\n` +
(webContext ? `Contexto web (fuentes numeradas; si las usas, c\u00edtalas como [1], [2]\u2026):\n${webContext}\n\n` : '') +
(view ? `Conversaci\u00f3n hasta ahora:\n${view}\n\n` : 'A\u00fan no hay intervenciones.\n\n') +
`Ahora hablas t\u00fa, ${agent.name}. Escribe tu intervenci\u00f3n.` },
];
const { el, body } = addMsg(agent);
el.classList.add('streaming');
markActive(agent.name);
let think = null;
let answer = '', reasoning = '', count = 0;
setStatus(`${agent.name} est\u00e1 escribiendo\u2026`);
try {
const stream = await wllama.createChatCompletion({
messages,
max_tokens: CONFIG.maxTokens,
temperature: CONFIG.temperature,
stream: true,
});
for await (const chunk of stream) {
if (stopRequested) break;
const delta = chunk.choices?.[0]?.delta || {};
const tk = delta.content || '';
const rs = delta.reasoning_content || '';
if (rs) {
reasoning += rs;
if (!think) {
think = document.createElement('details');
think.className = 'think';
think.innerHTML = '<summary>Pensamiento</summary><div class="t"></div>';
body.parentNode.insertBefore(think, body);
}
think.querySelector('.t').textContent = reasoning;
}
if (tk) { answer += tk; body.textContent = answer; }
if (tk || rs) { count++; if (count % 10 === 0) setStatus(`${agent.name}\u2026 ${count} tokens`); }
chatEl.scrollTop = chatEl.scrollHeight;
}
} catch (e) {
console.error(e);
el.classList.add('err');
body.textContent = 'Error: ' + (e?.message || e);
el.classList.remove('streaming');
markActive(null);
return;
}
el.classList.remove('streaming');
if (stopRequested) { el.remove(); markActive(null); setStatus('Detenido'); return; }
answer = answer.replace(/^\s*(?:\*\*)?[A-Za-z\u00c0-\u024f]+:\s*/, '').trim();
if (!answer) { el.remove(); markActive(null); setStatus('Listo'); return; }
body.innerHTML = (typeof marked !== 'undefined') ? marked.parse(answer) : esc(answer).replace(/\n/g, '<br>');
transcript.push({ name: agent.name, text: answer });
markActive(null);
setStatus('Listo');
}
let resume = { round: 1, order: null, pos: 0 };
function shuffled(n) {
const a = [...Array(n).keys()];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
function validOrder(o, n) {
return Array.isArray(o) && o.length === n && new Set(o).size === n &&
o.every(v => Number.isInteger(v) && v >= 0 && v < n);
}
async function runDebate() {
running = true;
stopRequested = false;
startBtn.style.display = 'none';
stopBtn.style.display = 'inline-block';
topicEl.disabled = true;
try {
for (let r = resume.round; r <= CONFIG.rounds && !stopRequested; r++) {
setStatus(`Ronda ${r}/${CONFIG.rounds}`);
let order;
if (r === resume.round && validOrder(resume.order, agents.length)) order = resume.order;
else order = CONFIG.order === 'rand' ? shuffled(agents.length) : [...Array(agents.length).keys()];
let startPos = (r === resume.round) ? resume.pos : 0;
if (startPos >= order.length) startPos = 0;
for (let p = startPos; p < order.length; p++) {
if (stopRequested) { resume = { round: r, order, pos: p }; break; }
await agentTurn(agents[order[p]]);
if (stopRequested) { resume = { round: r, order, pos: p }; break; }
}
if (!stopRequested) resume = { round: r + 1, order: null, pos: 0 };
}
if (!stopRequested) resume = { round: 1, order: null, pos: 0 };
} finally {
running = false;
saveTranscriptToHistory();
stopBtn.style.display = 'none';
startBtn.style.display = 'inline-block';
startBtn.textContent = 'Continuar debate';
startBtn.disabled = false;
topicEl.disabled = false;
setStatus(stopRequested ? 'Detenido. Puedes continuar o cambiar de tema.' : 'Debate terminado. Puedes continuar.');
}
}
startBtn.addEventListener('click', async () => {
if (!wllama || running) return;
const newTopic = topicEl.value.trim();
let freshTopic = false;
if (newTopic && newTopic !== topic) {
saveTranscriptToHistory();
topic = newTopic;
transcript = [];
chatEl.innerHTML = '';
resume = { round: 1, order: null, pos: 0 };
webContext = '';
webSources = [];
const m = addMsg(null, true);
m.body.textContent = newTopic;
transcript.push({ name: null, text: 'Tema propuesto: ' + newTopic });
currentHistId = recordTopic(newTopic);
freshTopic = true;
} else if (!topic) {
topic = 'Tema libre';
}
if (CONFIG.search && freshTopic) {
startBtn.disabled = true;
try { await prepareWebContext(topic); } catch (e) { console.warn('busqueda web', e); }
startBtn.disabled = false;
}
runDebate();
});
stopBtn.addEventListener('click', () => { stopRequested = true; setStatus('Deteniendo\u2026'); });
topicEl.addEventListener('keydown', e => {
if (e.key === 'Enter') { e.preventDefault(); startBtn.click(); }
});
// --- Temas sugeridos (10 al azar en cada carga) ---
const TOPIC_POOL = [
'\u00bfConviene ejecutar LLMs en el navegador con WebGPU?',
'\u00bfEs mejor el caf\u00e9 solo o con leche?',
'\u00bfDeber\u00edan los coches aut\u00f3nomos decidir en un accidente?',
'\u00bfEs el trabajo remoto m\u00e1s productivo que la oficina?',
'\u00bfHa mejorado la IA la programaci\u00f3n o solo la ha acelerado?',
'\u00bfMerece la pena aprender Harbour en 2026?',
'\u00bfDeber\u00eda durar un smartphone 7 a\u00f1os?',
'Pizza con pi\u00f1a: \u00bfgenialidad o sacrilegio?',
'\u00bfSuscripci\u00f3n mensual o licencia perpetua para el software?',
'\u00bfEs la privacidad m\u00e1s importante que la comodidad?',
'\u00bfDeben los ni\u00f1os aprender a programar antes de los 10 a\u00f1os?',
'\u00bfSobrevivir\u00e1 el open source a la IA?',
'\u00bfSQL o NoSQL para la tienda de un barrio?',
'\u00bfTienen sentido los coches el\u00e9ctricos sin red de carga?',
'\u00bfEs la bater\u00eda el cuello de botella de la tecnolog\u00eda?',
'\u00bfEl 5G fue revoluci\u00f3n o marketing?',
'\u00bfPC de sobremesa o port\u00e1til hoy?',
'\u00bfSon los videojuegos cultura o solo ocio?',
'\u00bfEs mejor vivir en la ciudad o en un pueblo?',
'\u00bfDeber\u00eda existir un derecho al olvido digital?',
];
const suggEl = document.getElementById('topic-sugg');
(function fillSuggestions() {
const ph = document.createElement('option');
ph.value = ''; ph.textContent = '\u{1F3B2} 10 temas al azar\u2026';
suggEl.appendChild(ph);
[...TOPIC_POOL].sort(() => Math.random() - .5).slice(0, 10).forEach(t => {
const o = document.createElement('option');
o.value = t; o.textContent = t;
suggEl.appendChild(o);
});
})();
suggEl.addEventListener('change', () => {
if (suggEl.value) { topicEl.value = suggEl.value; topicEl.focus(); }
suggEl.selectedIndex = 0;
});
// --- Settings ---
const settingsPanel = document.getElementById('settings');
document.getElementById('settings-btn').addEventListener('click', () => {
if (!settingsPanel.classList.contains('open')) {
document.getElementById('set-repo').value = CONFIG.repo;
document.getElementById('set-file').value = CONFIG.file;
document.getElementById('set-ctx').value = CONFIG.nCtx;
document.getElementById('set-maxtokens').value = CONFIG.maxTokens;
document.getElementById('set-temp').value = CONFIG.temperature;
document.getElementById('set-rounds').value = CONFIG.rounds;
document.getElementById('set-order').value = CONFIG.order;
document.getElementById('set-search').checked = CONFIG.search;
document.getElementById('set-agents').value = JSON.stringify(agents, null, 2);
}
settingsPanel.classList.toggle('open');
});
document.getElementById('apply-settings').addEventListener('click', async () => {
try { agents = JSON.parse(document.getElementById('set-agents').value); }
catch (e) { alert('JSON de agentes inv\u00e1lido: ' + e.message); return; }
if (!Array.isArray(agents) || agents.length < 2) { alert('Se necesitan al menos 2 agentes.'); return; }
persistAgents();
renderContacts();
CONFIG.maxTokens = parseInt(document.getElementById('set-maxtokens').value) || CONFIG.maxTokens;
CONFIG.temperature = parseFloat(document.getElementById('set-temp').value) || CONFIG.temperature;
CONFIG.rounds = parseInt(document.getElementById('set-rounds').value) || CONFIG.rounds;
CONFIG.order = document.getElementById('set-order').value === 'rand' ? 'rand' : 'seq';
CONFIG.search = document.getElementById('set-search').checked;
try { localStorage.setItem('mad_search', CONFIG.search ? '1' : '0'); } catch (e) {}
const newCtx = parseInt(document.getElementById('set-ctx').value) || CONFIG.nCtx;
const newRepo = document.getElementById('set-repo').value.trim();
const newFile = document.getElementById('set-file').value.trim();
const reload = newCtx !== CONFIG.nCtx || newRepo !== CONFIG.repo || newFile !== CONFIG.file;
CONFIG.nCtx = newCtx; CONFIG.repo = newRepo; CONFIG.file = newFile;
settingsPanel.classList.remove('open');
if (reload || !wllama) { transcript = []; chatEl.innerHTML = ''; await loadModel(CONFIG.repo, CONFIG.file); }
else setStatus(`Ajustes aplicados. Agentes: ${agents.map(a => a.name).join(', ')}.`);
});
if (CONFIG.autoload) loadModel(CONFIG.repo, CONFIG.file);
else setStatus('Modo manual: carga el modelo desde Ajustes.');
</script>
</body>
</html>