fivetech commited on
Commit
85163db
·
verified ·
1 Parent(s): 9f0e407

Update index.html

Browse files
Files changed (1) hide show
  1. 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&hellip;</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) &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>
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&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>
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&hellip;</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) &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>
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();