Download index.html from fivetech/Multi-Agent-Debate-MiMo-WebGPU: direct link, hf CLI and curl.
- Browser
- Download file 49.9 kB
-
https://huggingface.co/spaces/fivetech/Multi-Agent-Debate-MiMo-WebGPU/resolve/main/index.html
- Command line
-
hf download hf://spaces/fivetech/Multi-Agent-Debate-MiMo-WebGPU/index.html
-
curl -L -o index.html https://huggingface.co/spaces/fivetech/Multi-Agent-Debate-MiMo-WebGPU/resolve/main/index.html
49.9 kB
| <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 ;border:2px dashed var(--border);color:var(--muted); | |
| font-size:22px;font-weight:400;box-shadow:none } | |
| .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">🤖</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">🗑</button></div> | |
| <div id="hist-list"></div> | |
| </div> | |
| </aside> | |
| <div id="main"> | |
| <header> | |
| <span class="logo">🤖</span> | |
| <span class="title" id="title">Multi-Agent Debate <span class="subtitle" id="subtitle">Ada, Bruno, Carla, Tú</span></span> | |
| <button id="settings-btn" title="Ajustes">⚙</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á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úsqueda web al empezar un tema (Tavily sin clave + Wikipedia; solo se envía el tema, nunca la conversació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…"> | |
| <button id="start-btn" disabled>Cargando modelo…</button> | |
| <button id="stop-btn">Detener</button> | |
| </div> | |
| <div id="status">Iniciando…</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) · inspirado en el widget de <a href="https://localellm.com" target="_blank" rel="noopener">localellm.com</a> · 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> | |