Update index.html
Browse files- index.html +128 -2
index.html
CHANGED
|
@@ -83,6 +83,10 @@ body.light #chat{
|
|
| 83 |
.msg.user .who{display:none}
|
| 84 |
.msg.err{align-self:center;max-width:92%;background:var(--sys);color:var(--sys-text);font-size:12.5px;
|
| 85 |
border-radius:8px;padding:6px 12px;text-align:center;box-shadow:0 1px 1px rgba(0,0,0,.3)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
@keyframes fadein{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
| 87 |
@keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
|
| 88 |
.msg.streaming .body::after{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
|
|
@@ -224,6 +228,9 @@ footer a{color:var(--accent);text-decoration:none}
|
|
| 224 |
<div><label>Rondas</label><input id="set-rounds" type="number" step="1" min="1" max="20"></div>
|
| 225 |
<div><label>Orden de turnos</label><select id="set-order"><option value="seq">Secuencial</option><option value="rand">Aleatorio por ronda</option></select></div>
|
| 226 |
</div>
|
|
|
|
|
|
|
|
|
|
| 227 |
<label>Agentes (JSON: name, emoji, color, system)</label>
|
| 228 |
<textarea id="set-agents" spellcheck="false"></textarea>
|
| 229 |
<button id="apply-settings">Aplicar</button>
|
|
@@ -241,7 +248,7 @@ footer a{color:var(--accent);text-decoration:none}
|
|
| 241 |
<div id="status">Iniciando…</div>
|
| 242 |
<div id="progress"><div></div></div>
|
| 243 |
<footer>
|
| 244 |
-
<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.</span>
|
| 245 |
<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>
|
| 246 |
</footer>
|
| 247 |
</div>
|
|
@@ -292,6 +299,13 @@ const CONFIG = {
|
|
| 292 |
rounds: parseInt(params.get('rounds')) || 2,
|
| 293 |
autoload: params.get('autoload') !== 'false',
|
| 294 |
order: params.get('order') === 'rand' ? 'rand' : 'seq',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
};
|
| 296 |
if (params.get('theme') === 'light') document.body.classList.add('light');
|
| 297 |
const AGENTS_KEY = 'mad_agents';
|
|
@@ -521,6 +535,105 @@ function addErr(text) {
|
|
| 521 |
chatEl.appendChild(d);
|
| 522 |
}
|
| 523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
async function loadModel(repo, file) {
|
| 525 |
const url = CONFIG.modelUrl || `https://huggingface.co/${repo}/resolve/main/${file}`;
|
| 526 |
startBtn.disabled = true;
|
|
@@ -595,6 +708,7 @@ async function agentTurn(agent) {
|
|
| 595 |
{ role: 'system', content: agent.system + ' ' + RULES },
|
| 596 |
{ role: 'user', content:
|
| 597 |
`Tema del debate: ${topic}\n\n` +
|
|
|
|
| 598 |
(view ? `Conversaci\u00f3n hasta ahora:\n${view}\n\n` : 'A\u00fan no hay intervenciones.\n\n') +
|
| 599 |
`Ahora hablas t\u00fa, ${agent.name}. Escribe tu intervenci\u00f3n.` },
|
| 600 |
];
|
|
@@ -696,21 +810,30 @@ async function runDebate() {
|
|
| 696 |
}
|
| 697 |
}
|
| 698 |
|
| 699 |
-
startBtn.addEventListener('click', () => {
|
| 700 |
if (!wllama || running) return;
|
| 701 |
const newTopic = topicEl.value.trim();
|
|
|
|
| 702 |
if (newTopic && newTopic !== topic) {
|
| 703 |
topic = newTopic;
|
| 704 |
transcript = [];
|
| 705 |
chatEl.innerHTML = '';
|
| 706 |
resume = { round: 1, order: null, pos: 0 };
|
|
|
|
|
|
|
| 707 |
const m = addMsg(null, true);
|
| 708 |
m.body.textContent = newTopic;
|
| 709 |
transcript.push({ name: null, text: 'Tema propuesto: ' + newTopic });
|
| 710 |
recordTopic(newTopic);
|
|
|
|
| 711 |
} else if (!topic) {
|
| 712 |
topic = 'Tema libre';
|
| 713 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
runDebate();
|
| 715 |
});
|
| 716 |
stopBtn.addEventListener('click', () => { stopRequested = true; setStatus('Deteniendo\u2026'); });
|
|
@@ -768,6 +891,7 @@ document.getElementById('settings-btn').addEventListener('click', () => {
|
|
| 768 |
document.getElementById('set-temp').value = CONFIG.temperature;
|
| 769 |
document.getElementById('set-rounds').value = CONFIG.rounds;
|
| 770 |
document.getElementById('set-order').value = CONFIG.order;
|
|
|
|
| 771 |
document.getElementById('set-agents').value = JSON.stringify(agents, null, 2);
|
| 772 |
}
|
| 773 |
settingsPanel.classList.toggle('open');
|
|
@@ -782,6 +906,8 @@ document.getElementById('apply-settings').addEventListener('click', async () =>
|
|
| 782 |
CONFIG.temperature = parseFloat(document.getElementById('set-temp').value) || CONFIG.temperature;
|
| 783 |
CONFIG.rounds = parseInt(document.getElementById('set-rounds').value) || CONFIG.rounds;
|
| 784 |
CONFIG.order = document.getElementById('set-order').value === 'rand' ? 'rand' : 'seq';
|
|
|
|
|
|
|
| 785 |
const newCtx = parseInt(document.getElementById('set-ctx').value) || CONFIG.nCtx;
|
| 786 |
const newRepo = document.getElementById('set-repo').value.trim();
|
| 787 |
const newFile = document.getElementById('set-file').value.trim();
|
|
|
|
| 83 |
.msg.user .who{display:none}
|
| 84 |
.msg.err{align-self:center;max-width:92%;background:var(--sys);color:var(--sys-text);font-size:12.5px;
|
| 85 |
border-radius:8px;padding:6px 12px;text-align:center;box-shadow:0 1px 1px rgba(0,0,0,.3)}
|
| 86 |
+
.msg.sys{align-self:center;max-width:92%;background:var(--sys);color:var(--sys-text);font-size:12.5px;
|
| 87 |
+
border-radius:8px;padding:8px 14px;text-align:left;box-shadow:0 1px 1px rgba(0,0,0,.3)}
|
| 88 |
+
.msg.sys a{color:var(--accent);word-break:break-word}
|
| 89 |
+
#settings input[type=checkbox]{width:auto}
|
| 90 |
@keyframes fadein{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
| 91 |
@keyframes pulse{0%,100%{opacity:.3}50%{opacity:1}}
|
| 92 |
.msg.streaming .body::after{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;
|
|
|
|
| 228 |
<div><label>Rondas</label><input id="set-rounds" type="number" step="1" min="1" max="20"></div>
|
| 229 |
<div><label>Orden de turnos</label><select id="set-order"><option value="seq">Secuencial</option><option value="rand">Aleatorio por ronda</option></select></div>
|
| 230 |
</div>
|
| 231 |
+
<div class="row">
|
| 232 |
+
<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>
|
| 233 |
+
</div>
|
| 234 |
<label>Agentes (JSON: name, emoji, color, system)</label>
|
| 235 |
<textarea id="set-agents" spellcheck="false"></textarea>
|
| 236 |
<button id="apply-settings">Aplicar</button>
|
|
|
|
| 248 |
<div id="status">Iniciando…</div>
|
| 249 |
<div id="progress"><div></div></div>
|
| 250 |
<footer>
|
| 251 |
+
<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>
|
| 252 |
<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>
|
| 253 |
</footer>
|
| 254 |
</div>
|
|
|
|
| 299 |
rounds: parseInt(params.get('rounds')) || 2,
|
| 300 |
autoload: params.get('autoload') !== 'false',
|
| 301 |
order: params.get('order') === 'rand' ? 'rand' : 'seq',
|
| 302 |
+
search: (() => {
|
| 303 |
+
const p = params.get('search');
|
| 304 |
+
if (p === '1') return true;
|
| 305 |
+
if (p === '0') return false;
|
| 306 |
+
try { const v = localStorage.getItem('mad_search'); if (v !== null) return v === '1'; } catch (e) {}
|
| 307 |
+
return true;
|
| 308 |
+
})(),
|
| 309 |
};
|
| 310 |
if (params.get('theme') === 'light') document.body.classList.add('light');
|
| 311 |
const AGENTS_KEY = 'mad_agents';
|
|
|
|
| 535 |
chatEl.appendChild(d);
|
| 536 |
}
|
| 537 |
|
| 538 |
+
let webContext = ''; // bloque de contexto web inyectado en los prompts del debate actual
|
| 539 |
+
let webSources = []; // fuentes numeradas del debate actual
|
| 540 |
+
|
| 541 |
+
function addSys(build) {
|
| 542 |
+
const d = document.createElement('div');
|
| 543 |
+
d.className = 'msg sys';
|
| 544 |
+
const body = document.createElement('div');
|
| 545 |
+
body.className = 'body';
|
| 546 |
+
d.appendChild(body);
|
| 547 |
+
chatEl.appendChild(d);
|
| 548 |
+
build(body);
|
| 549 |
+
chatEl.scrollTop = chatEl.scrollHeight;
|
| 550 |
+
return d;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
const SEARCH_TIMEOUT_MS = 12000;
|
| 554 |
+
async function fetchJson(url, opts) {
|
| 555 |
+
const ctrl = new AbortController();
|
| 556 |
+
const t = setTimeout(() => ctrl.abort(), SEARCH_TIMEOUT_MS);
|
| 557 |
+
try {
|
| 558 |
+
const r = await fetch(url, Object.assign({}, opts, { signal: ctrl.signal }));
|
| 559 |
+
return { ok: r.ok, status: r.status, json: r.ok ? await r.json().catch(() => null) : null };
|
| 560 |
+
} finally { clearTimeout(t); }
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Solo se envia el tema (query); nunca la conversacion. Sin claves: Tavily keyless
|
| 564 |
+
// (doc oficial: docs.tavily.com/documentation/keyless) y Wikipedia action API (origin=*).
|
| 565 |
+
async function fetchWebContext(query) {
|
| 566 |
+
const sources = [];
|
| 567 |
+
const failed = [];
|
| 568 |
+
try {
|
| 569 |
+
const r = await fetchJson('https://api.tavily.com/search', {
|
| 570 |
+
method: 'POST',
|
| 571 |
+
headers: { 'Content-Type': 'application/json', 'X-Tavily-Access-Mode': 'keyless' },
|
| 572 |
+
body: JSON.stringify({ query: query, max_results: 4 }),
|
| 573 |
+
});
|
| 574 |
+
if (r.ok && r.json && Array.isArray(r.json.results)) {
|
| 575 |
+
for (const x of r.json.results) {
|
| 576 |
+
if (x && x.url && x.title) sources.push({ title: String(x.title), url: String(x.url), snippet: String(x.content || '').slice(0, 280) });
|
| 577 |
+
}
|
| 578 |
+
if (!r.json.results.length) failed.push('Tavily no devolvi\u00f3 resultados');
|
| 579 |
+
} else {
|
| 580 |
+
failed.push('Tavily respondi\u00f3 ' + r.status + (r.status === 429 ? ' (l\u00edmite gratuito alcanzado; prueba m\u00e1s tarde)' : ''));
|
| 581 |
+
}
|
| 582 |
+
} catch (e) { failed.push('Tavily no accesible (' + (e && e.name === 'AbortError' ? 'tiempo de espera' : 'red') + ')'); }
|
| 583 |
+
try {
|
| 584 |
+
const u = 'https://es.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srlimit=3&srsearch=' + encodeURIComponent(query);
|
| 585 |
+
const r = await fetchJson(u);
|
| 586 |
+
if (r.ok && r.json && r.json.query && Array.isArray(r.json.query.search)) {
|
| 587 |
+
for (const x of r.json.query.search) {
|
| 588 |
+
const t = String(x && x.title || '');
|
| 589 |
+
if (!t) continue;
|
| 590 |
+
sources.push({ title: 'Wikipedia: ' + t, url: 'https://es.wikipedia.org/wiki/' + encodeURIComponent(t.replace(/ /g, '_')), snippet: String(x.snippet || '').replace(/<[^>]+>/g, '').slice(0, 220) });
|
| 591 |
+
}
|
| 592 |
+
if (!r.json.query.search.length) failed.push('Wikipedia sin resultados');
|
| 593 |
+
} else {
|
| 594 |
+
failed.push('Wikipedia respondi\u00f3 ' + r.status);
|
| 595 |
+
}
|
| 596 |
+
} catch (e) { failed.push('Wikipedia no accesible (' + (e && e.name === 'AbortError' ? 'tiempo de espera' : 'red') + ')'); }
|
| 597 |
+
return { sources: sources.slice(0, 6), failed: failed };
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
async function prepareWebContext(q) {
|
| 601 |
+
addSys(b => { b.textContent = '\u{1F50E} Buscando contexto web sobre \u00ab' + q + '\u00bb (solo se env\u00eda el tema a Tavily y Wikipedia)\u2026'; });
|
| 602 |
+
let res;
|
| 603 |
+
try { res = await fetchWebContext(q); }
|
| 604 |
+
catch (e) { res = { sources: [], failed: ['error inesperado'] }; }
|
| 605 |
+
webSources = res.sources;
|
| 606 |
+
if (webSources.length) {
|
| 607 |
+
webContext = webSources.map((s, i) => '[' + (i + 1) + '] ' + s.title + ' \u2014 ' + s.url + '\n' + s.snippet).join('\n');
|
| 608 |
+
if (webContext.length > 1600) webContext = webContext.slice(0, 1600);
|
| 609 |
+
addSys(b => {
|
| 610 |
+
const t = document.createElement('div');
|
| 611 |
+
t.textContent = '\u{1F4DA} Fuentes para este debate (los agentes pueden citarlas como [1], [2]\u2026):';
|
| 612 |
+
b.appendChild(t);
|
| 613 |
+
const ol = document.createElement('ol');
|
| 614 |
+
ol.style.margin = '6px 0 0 18px';
|
| 615 |
+
webSources.forEach(s => {
|
| 616 |
+
const li = document.createElement('li');
|
| 617 |
+
const a = document.createElement('a');
|
| 618 |
+
if (/^https?:\/\//i.test(s.url)) { a.href = s.url; a.target = '_blank'; a.rel = 'noopener'; }
|
| 619 |
+
a.textContent = s.title;
|
| 620 |
+
li.appendChild(a);
|
| 621 |
+
ol.appendChild(li);
|
| 622 |
+
});
|
| 623 |
+
b.appendChild(ol);
|
| 624 |
+
if (res.failed.length) {
|
| 625 |
+
const f = document.createElement('div');
|
| 626 |
+
f.style.marginTop = '4px';
|
| 627 |
+
f.textContent = '\u26a0\uFE0F ' + res.failed.join(' \u00b7 ');
|
| 628 |
+
b.appendChild(f);
|
| 629 |
+
}
|
| 630 |
+
});
|
| 631 |
+
} else {
|
| 632 |
+
webContext = '';
|
| 633 |
+
addSys(b => { b.textContent = '\u26a0\uFE0F B\u00fasqueda web sin resultados ahora (' + (res.failed.join(' \u00b7 ') || 'error') + '). El debate contin\u00faa sin contexto web.'; });
|
| 634 |
+
}
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
async function loadModel(repo, file) {
|
| 638 |
const url = CONFIG.modelUrl || `https://huggingface.co/${repo}/resolve/main/${file}`;
|
| 639 |
startBtn.disabled = true;
|
|
|
|
| 708 |
{ role: 'system', content: agent.system + ' ' + RULES },
|
| 709 |
{ role: 'user', content:
|
| 710 |
`Tema del debate: ${topic}\n\n` +
|
| 711 |
+
(webContext ? `Contexto web (fuentes numeradas; si las usas, c\u00edtalas como [1], [2]\u2026):\n${webContext}\n\n` : '') +
|
| 712 |
(view ? `Conversaci\u00f3n hasta ahora:\n${view}\n\n` : 'A\u00fan no hay intervenciones.\n\n') +
|
| 713 |
`Ahora hablas t\u00fa, ${agent.name}. Escribe tu intervenci\u00f3n.` },
|
| 714 |
];
|
|
|
|
| 810 |
}
|
| 811 |
}
|
| 812 |
|
| 813 |
+
startBtn.addEventListener('click', async () => {
|
| 814 |
if (!wllama || running) return;
|
| 815 |
const newTopic = topicEl.value.trim();
|
| 816 |
+
let freshTopic = false;
|
| 817 |
if (newTopic && newTopic !== topic) {
|
| 818 |
topic = newTopic;
|
| 819 |
transcript = [];
|
| 820 |
chatEl.innerHTML = '';
|
| 821 |
resume = { round: 1, order: null, pos: 0 };
|
| 822 |
+
webContext = '';
|
| 823 |
+
webSources = [];
|
| 824 |
const m = addMsg(null, true);
|
| 825 |
m.body.textContent = newTopic;
|
| 826 |
transcript.push({ name: null, text: 'Tema propuesto: ' + newTopic });
|
| 827 |
recordTopic(newTopic);
|
| 828 |
+
freshTopic = true;
|
| 829 |
} else if (!topic) {
|
| 830 |
topic = 'Tema libre';
|
| 831 |
}
|
| 832 |
+
if (CONFIG.search && freshTopic) {
|
| 833 |
+
startBtn.disabled = true;
|
| 834 |
+
try { await prepareWebContext(topic); } catch (e) { console.warn('busqueda web', e); }
|
| 835 |
+
startBtn.disabled = false;
|
| 836 |
+
}
|
| 837 |
runDebate();
|
| 838 |
});
|
| 839 |
stopBtn.addEventListener('click', () => { stopRequested = true; setStatus('Deteniendo\u2026'); });
|
|
|
|
| 891 |
document.getElementById('set-temp').value = CONFIG.temperature;
|
| 892 |
document.getElementById('set-rounds').value = CONFIG.rounds;
|
| 893 |
document.getElementById('set-order').value = CONFIG.order;
|
| 894 |
+
document.getElementById('set-search').checked = CONFIG.search;
|
| 895 |
document.getElementById('set-agents').value = JSON.stringify(agents, null, 2);
|
| 896 |
}
|
| 897 |
settingsPanel.classList.toggle('open');
|
|
|
|
| 906 |
CONFIG.temperature = parseFloat(document.getElementById('set-temp').value) || CONFIG.temperature;
|
| 907 |
CONFIG.rounds = parseInt(document.getElementById('set-rounds').value) || CONFIG.rounds;
|
| 908 |
CONFIG.order = document.getElementById('set-order').value === 'rand' ? 'rand' : 'seq';
|
| 909 |
+
CONFIG.search = document.getElementById('set-search').checked;
|
| 910 |
+
try { localStorage.setItem('mad_search', CONFIG.search ? '1' : '0'); } catch (e) {}
|
| 911 |
const newCtx = parseInt(document.getElementById('set-ctx').value) || CONFIG.nCtx;
|
| 912 |
const newRepo = document.getElementById('set-repo').value.trim();
|
| 913 |
const newFile = document.getElementById('set-file').value.trim();
|