Exportar una conversación de Gemini a PDF, Word o Markdown

Guarda una conversación entera de Gemini como documento, tus preguntas y todas las respuestas, con fórmulas, código y tablas. Gratis y sin extensiones.

Abre la conversación que quieras en gemini.google.com y ejecuta allí el script.

export-gemini.js · 201 líneaspégalo en la consola (F12) de gemini.google.com
// Responsive Muse · Gemini Conversation Exporter · v1.0 · 2026-10-22
// Saves the Gemini conversation you have open as Markdown, Word (.doc) or PDF, with code
// blocks, lists and tables kept. Runs only on gemini.google.com. Gemini loads older messages
// as you scroll up, so the script scrolls from the top and collects every message on the way.
// It makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Exportar chat de Gemini';
  const NAMES = { user: 'Tú', ai: 'Gemini' };     // headings used in the file
  const DELAY = 1000;                              // pause between scrolls, in milliseconds

  if (location.hostname !== 'gemini.google.com' || !/^\/(app|gem)\//.test(location.pathname)) {
    alert('Abre primero la conversación de Gemini que quieres exportar (gemini.google.com/app/…).');
    return;
  }
  // A small box on the page shows progress and asks before changing anything,
  // so you never need to read or edit the console.
  const many = (n, word) => `${n} ${n === 1 ? word : ({ canal: 'canales', 'canción': 'canciones', 'publicación': 'publicaciones', 'elemento guardado': 'elementos guardados' })[word] || word + 's'}`;   // 1 vídeo, 2 vídeos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
      'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
      'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
    const head = document.createElement('strong');
    head.textContent = TOOL;
    const msg = document.createElement('p');
    msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
    box.append(head, msg, row);
    document.body.append(box);
    const button = (label, primary, onClick) => {
      const b = document.createElement('button');
      b.textContent = label;
      b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
        (primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
      b.onclick = onClick;
      row.append(b);
    };
    return {
      text(t) { msg.textContent = t; },
      ask(t, yes) {
        msg.textContent = t;
        row.replaceChildren();
        return new Promise(r => {
          button('Cancelar', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Parar', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Cerrar', true, () => box.remove()]]); },
      close() { box.remove(); },
    };
  })();
  // Turn a rendered message (HTML) back into Markdown: headings, lists, code, tables, links
  const toMarkdown = root => {
    const skip = el => el.matches('button, svg, script, style, [aria-hidden="true"], .sr-only, [data-markdown-copy="exclude"], ' +
      '.code-block-decoration, [class*="code-block-header"]');
    const walk = (node, depth) => {
      if (node.nodeType === 3) return node.textContent;
      if (node.nodeType !== 1 || skip(node)) return '';
      const kids = () => [...node.childNodes].map(n => walk(n, depth)).join('');
      const tag = node.tagName.toLowerCase();
      // Math (KaTeX) is on the page three times; keep only its TeX source
      if (node.classList.contains('katex')) {
        const tex = node.querySelector('annotation[encoding="application/x-tex"]')?.textContent.trim();
        if (tex) return node.closest('.katex-display') ? `\n\n$$${tex}$$\n\n` : `$${tex}$`;
      }
      if (/^h[1-6]$/.test(tag)) return `\n\n${'#'.repeat(+tag[1])} ${kids().trim()}\n\n`;
      if (tag === 'p') return `\n\n${kids().trim()}\n\n`;
      if (tag === 'br') return '\n';
      if (tag === 'hr') return '\n\n---\n\n';
      if (tag === 'strong' || tag === 'b') return `**${kids()}**`;
      if (tag === 'em' || tag === 'i') return `*${kids()}*`;
      if (tag === 'a') return `[${kids()}](${node.href})`;
      if (tag === 'img') return node.alt ? `![${node.alt}](${node.src})` : '';
      if (tag === 'pre') {
        const code = node.querySelector('code') || node;
        const lang = ((code.className || '').match(/language-([\w+-]+)/) || [])[1] || '';
        return `\n\n\`\`\`${lang}\n${code.textContent.replace(/\n$/, '')}\n\`\`\`\n\n`;
      }
      if (tag === 'code') return '`' + node.textContent + '`';
      if (tag === 'blockquote') return '\n\n' + kids().trim().split('\n').map(l => '> ' + l).join('\n') + '\n\n';
      if (tag === 'ul' || tag === 'ol') {
        const items = [...node.children].filter(li => li.tagName === 'LI').map((li, i) => {
          const body = [...li.childNodes].map(n => walk(n, depth + 1)).join('').trim().replace(/\n{2,}/g, '\n');
          return '  '.repeat(depth) + (tag === 'ol' ? `${i + 1}. ` : '- ') + body;
        });
        return '\n' + items.join('\n') + '\n';
      }
      if (tag === 'table') {
        const rows = [...node.querySelectorAll('tr')].map(tr =>
          '| ' + [...tr.children].map(c => walk(c, depth).trim().replace(/\|/g, '\\|').replace(/\n/g, ' ')).join(' | ') + ' |');
        if (rows.length) rows.splice(1, 0, '|' + ' --- |'.repeat(rows[0].split(' | ').length));
        return '\n\n' + rows.join('\n') + '\n\n';
      }
      return kids();
    };
    return walk(root, 0).replace(/\n{3,}/g, '\n\n').trim();
  };
  // Offer the chat as Markdown, Word or PDF (all built here, in your browser)
  const deliverChat = (title, messages, names, note = '') => {
    const date = new Date().toISOString().slice(0, 10);
    const md = `# ${title}\n\n_Exportado el ${date} desde ${location.href}_\n\n` + messages.map(m =>
      `## ${m.role === 'user' ? names.user : names.ai}\n\n${m.markdown}`).join('\n\n---\n\n') + '\n';
    const esc = s => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    const html = '<!doctype html><html><head><meta charset="utf-8"><title>' + esc(title) + '</title><style>' +
      'body{font:15px/1.6 Georgia,serif;max-width:760px;margin:40px auto;padding:0 20px;color:#111}' +
      'h1{font:700 26px system-ui,sans-serif}h2{font:600 15px system-ui,sans-serif;text-transform:uppercase;letter-spacing:.05em;color:#555;margin-top:32px}' +
      '.msg{white-space:pre-wrap}.user{background:#f3f4f6;padding:12px 16px;border-radius:8px}' +
      'pre,code{font:13px Consolas,monospace}hr{border:0;border-top:1px solid #ddd}</style></head><body>' +
      `<h1>${esc(title)}</h1><p><em>Exportado el ${date}</em></p>` + messages.map(m =>
        `<h2>${esc(m.role === 'user' ? names.user : names.ai)}</h2><div class="msg ${m.role}">${esc(m.markdown)}</div>`).join('<hr>') +
      '</body></html>';
    const file = (title || 'chat').replace(/[\\/:*?"<>|]+/g, '_').slice(0, 80);
    const save = (body, ext, type) => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([body], { type }));
      a.download = `${file}.${ext}`;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`Guardado como ${file}.${ext} (mira tu carpeta de Descargas).`);
    };
    const pdf = () => {
      const w = window.open('', '_blank');
      if (!w) { panel.text('El navegador ha bloqueado la ventana nueva. Permite las ventanas emergentes en esta web y vuelve a probar.'); return; }
      w.document.write(html);
      w.document.close();
      w.focus();
      w.print();
      panel.text('Elige «Guardar como PDF» como impresora en la ventana que se ha abierto.');
    };
    const copy = () => navigator.clipboard.writeText(md)
      .then(() => panel.text('Copiado en Markdown. Pégalo en Notion, Obsidian o cualquier editor.'))
      .catch(() => panel.text('El navegador ha bloqueado el portapapeles: usa Markdown.'));
    panel.actions(`Listo: ${many(messages.length, 'mensaje')}.${note ? '\n' + note : ''}`, [
      ['Copiar', false, copy],
      ['Markdown', false, () => save(md, 'md', 'text/markdown;charset=utf-8')],
      ['Word', false, () => save('\ufeff' + html, 'doc', 'application/msword')],
      ['PDF', true, pdf],
    ]);
  };
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const turns = () => [...document.querySelectorAll('.conversation-container')];
  const first = turns()[0];
  let box = first;
  while (box && !(box.scrollHeight > box.clientHeight + 50 && /(auto|scroll)/.test(getComputedStyle(box).overflowY))) box = box.parentElement;
  if (!first) {
    panel.finish('No se han encontrado mensajes. Puede que Gemini haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }
  box = box || document.scrollingElement;

  // 1. Scroll to the very top until no older messages load
  let last = -1, idle = 0;
  while (idle < 3) {
    box.scrollTop = 0;
    await sleep(DELAY);
    const n = turns().length;
    idle = n === last ? idle + 1 : 0;
    last = n;
    panel.text('Cargando el principio de la conversación…');
  }

  // 2. Scroll down, collecting each turn (your prompt, then Gemini's answer) once
  const seen = new Map();
  const collect = () => {
    turns().forEach((t, i) => {
      const user = t.querySelector('user-query .query-text, user-query');
      const ai = t.querySelector('model-response message-content .markdown, model-response .markdown');
      const key = t.id || (user?.textContent || '').slice(0, 200) + '|' + i;
      if (seen.has(key)) return;
      const parts = [];
      if (user) parts.push({ role: 'user', markdown: user.textContent.replace(/^(You said|Has dicho)\s*/i, '').trim() });
      if (ai) parts.push({ role: 'ai', markdown: toMarkdown(ai) });
      if (parts.length) seen.set(key, parts);
    });
  };
  idle = 0;
  while (idle < 3) {
    const before = seen.size;
    collect();
    const atEnd = box.scrollTop + box.clientHeight >= box.scrollHeight - 5;
    box.scrollTop += box.clientHeight * 0.8;
    await sleep(DELAY);
    idle = seen.size === before && atEnd ? idle + 1 : 0;
    panel.text(`Leyendo mensajes… ${many([...seen.values()].flat().length, 'mensaje')}`);
  }
  const messages = [...seen.values()].flat();
  if (!messages.length) {
    panel.finish('No se han encontrado mensajes. Puede que Gemini haya cambiado su diseño: avísanos en responsive-muse.com.');
    return 0;
  }
  deliverChat(document.title.replace(/\s*-\s*Google Gemini$/, '') || 'Conversación de Gemini', messages, NAMES);
  return messages.length;
})();

Marcador: arrastra el botón del medio a tu barra de marcadores, abre gemini.google.com y pulsa el marcador. Ejecuta el mismo script con las opciones de arriba.

Por qué es seguro ejecutarloComprobación automática superada 23 oct 2026 · probado en una cuenta real el 22 oct 2026
  • No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de gemini.google.com cargando tu lista.
  • No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
  • Solo funciona en gemini.google.com. En cualquier otra web se detiene antes de hacer nada.
  • Corto y legible. 201 líneas comentadas, sin minificar ni ofuscar.
  • Muestra su progreso en la página. Un pequeño recuadro te dice qué está haciendo y al final te ofrece Descargar o Copiar.
  • El mismo código para todos. SHA-256 de la versión por defecto: 9e80c98db1cd1218…
Compruébalo tú mismo en 30 segundos
  1. Léelo: todas las líneas están arriba, con comentarios.
  2. Busca en el código fetch, XMLHttpRequest, cookie, localStorage o eval. No aparece ninguna.
  3. Abre la pestaña Red (Network) antes de ejecutarlo: solo verás peticiones a gemini.google.com.
¿Te ha funcionado?

Cómo usarlo

  1. Abre la conversación en gemini.google.com.
  2. Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe permitir pegar (o allow pasting si lo tienes en inglés) y pulsa Enter.
  3. Copia el script, pégalo en la consola y pulsa Enter. Aparece un recuadro: pulsa Markdown, Word o PDF (o Copiar para pegarlo en Notion u Obsidian).

Toda la conversación, no una respuesta

Gemini solo deja enviar a Google Docs las respuestas de una en una. Este script guarda la conversación completa en un único archivo.

Preguntas frecuentes

¿No puedo exportar a Google Docs desde Gemini?

Gemini puede enviar una sola respuesta a Docs, de una en una. Este script guarda la conversación entera, tus preguntas y todas las respuestas, en un solo archivo.

¿Se conservan el código, las tablas y las fórmulas?

Sí. El código mantiene su lenguaje, las tablas pasan a tablas Markdown y las fórmulas se exportan en TeX ($…$), que Notion, Obsidian y la mayoría de editores Markdown muestran como fórmulas.

¿Cómo saco un PDF?

Pulsa PDF en el recuadro: se abre una vista de impresión limpia. Elige «Guardar como PDF» como impresora. No se sube nada a ningún sitio.

¿Funciona con chats muy largos?

Sí. La web solo mantiene en pantalla parte de un chat largo, así que el script se desplaza desde el principio y recoge cada mensaje por el camino. Deja la pestaña abierta hasta que el recuadro muestre los botones.

Más herramientas gratis

Exportar y hacer copia