¿Cómo imprimir una conversación completa de ChatGPT?

Al imprimir una conversación larga de ChatGPT pueden faltar mensajes. Usa este script de la consola para imprimir el chat completo o guardarlo como PDF.

Si Ctrl + P solo imprime una parte de tu conversación de ChatGPT, el problema no suele ser la impresora. Es posible que ChatGPT no tenga todos los mensajes cargados en la página al mismo tiempo.

Tienes dos opciones prácticas:

  • Sin instalar nada: ejecuta el script de abajo y, cuando cargue toda la conversación, imprímela o guárdala como PDF.
  • Para exportar con frecuencia: usa una extensión del navegador para guardar el chat completo como PDF en un clic.

¿Por qué ChatGPT solo imprime parte de la conversación?

Al revisar el DOM de ChatGPT se observa que las conversaciones largas usan renderizado virtual. Mientras te desplazas, ChatGPT muestra los mensajes necesarios y puede retirar de la página el contenido que queda fuera de la pantalla. Esto evita que un chat largo ralentice el navegador, pero también significa que este solo puede imprimir lo que está presente en el DOM en ese momento.

El siguiente script recorre automáticamente la conversación, reúne todos los mensajes y abre la ventana de impresión al terminar.

Método 1: imprimir el chat completo con un script

Es la opción adecuada si solo necesitas imprimir una conversación y no quieres instalar una extensión.

Paso 1: abre la consola de Chrome

Abre la conversación de ChatGPT que quieres imprimir y después las herramientas para desarrolladores de Chrome:

  • Mac: Command + Option + J
  • Windows o Linux: Ctrl + Mayús + J

Selecciona la pestaña Console. Pegarás el script en el área vacía marcada en la imagen.

Herramientas para desarrolladores de Chrome con la pestaña Console y el área para pegar el script resaltadas

Abre la pestaña Console y pega el script en el área indicada.

Paso 2: copia y ejecuta el script

Pulsa Copiar en la esquina superior derecha del bloque de código. Vuelve a la consola de Chrome, pega el script y pulsa Enter.

javascript
// Load and print a complete ChatGPT conversation.
(async () => {

  /************************************************************
   * CONFIG
   ************************************************************/

  const MESSAGE_SELECTOR = 'section .text-base';

  // 每次向上滚动多少个 viewport
  const STEP_RATIO = 1.2;

  // 每次滚动后的等待时间
  const SCROLL_WAIT = 180;

  // 到顶部以后等待历史消息加载
  const TOP_WAIT = 500;

  // 连续几次顶部无变化后结束
  const TOP_STABLE_LIMIT = 3;

  const sleep = ms =>
    new Promise(resolve => setTimeout(resolve, ms));


  console.log('🚀 AI Chat Print started');


  /************************************************************
   * 1. FIND SCROLL CONTAINER
   ************************************************************/

  function findScrollContainer() {

    const firstMessage =
      document.querySelector(MESSAGE_SELECTOR);

    if (firstMessage) {

      let el =
        firstMessage.parentElement;

      while (
        el &&
        el !== document.body
      ) {

        const style =
          getComputedStyle(el);

        const scrollable =
          el.scrollHeight >
            el.clientHeight + 10 &&
          (
            style.overflowY === 'auto' ||
            style.overflowY === 'scroll'
          );

        if (scrollable) {
          return el;
        }

        el =
          el.parentElement;
      }
    }


    const candidates =
      [...document.querySelectorAll('*')]
        .filter(el => {

          const style =
            getComputedStyle(el);

          return (
            el.scrollHeight >
              el.clientHeight + 10 &&
            (
              style.overflowY === 'auto' ||
              style.overflowY === 'scroll'
            )
          );

        })
        .sort(
          (a, b) =>
            b.scrollHeight -
            a.scrollHeight
        );


    return (
      candidates[0] ||
      document.scrollingElement ||
      document.documentElement
    );
  }


  const scrollContainer =
    findScrollContainer();


  console.log(
    '📜 Scroll container:',
    scrollContainer
  );


  /************************************************************
   * 2. DATA
   ************************************************************/

  const messageMap =
    new Map();

  const edges =
    new Map();

  const firstSeenStep =
    new Map();

  let scanStep = 0;


  /************************************************************
   * 3. HASH
   ************************************************************/

  function hashString(str) {

    let hash =
      2166136261;

    for (
      let i = 0;
      i < str.length;
      i++
    ) {

      hash ^=
        str.charCodeAt(i);

      hash =
        Math.imul(
          hash,
          16777619
        );
    }

    return (
      hash >>> 0
    ).toString(36);
  }


  /************************************************************
   * 4. MESSAGE KEY
   ************************************************************/

  function getMessageKey(node) {

    const section =
      node.closest('section');


    const candidates =
      [
        node,
        section
      ].filter(Boolean);


    const attrs = [
      'data-message-id',
      'data-turn-id',
      'data-id',
      'id'
    ];


    for (
      const el of candidates
    ) {

      for (
        const attr of attrs
      ) {

        const value =
          el.getAttribute?.(attr);

        if (value) {

          return (
            `${attr}:${value}`
          );
        }
      }
    }


    const text =
      node.innerText || '';

    const html =
      node.innerHTML || '';


    return (
      'hash:' +
      hashString(
        text +
        '\n---HTML---\n' +
        html
      )
    );
  }


  /************************************************************
   * 5. COLLECT CURRENT DOM
   ************************************************************/

  function collect() {

    const nodes =
      [
        ...document.querySelectorAll(
          MESSAGE_SELECTOR
        )
      ];


    const keys = [];


    for (
      const node of nodes
    ) {

      const key =
        getMessageKey(node);


      keys.push(key);


      if (
        !messageMap.has(key)
      ) {

        messageMap.set(
          key,
          node.cloneNode(true)
        );


        firstSeenStep.set(
          key,
          scanStep
        );
      }
    }


    /**
     * 保存当前 DOM 中观察到的顺序:
     *
     * A -> B
     * B -> C
     */
    for (
      let i = 0;
      i < keys.length - 1;
      i++
    ) {

      const a =
        keys[i];

      const b =
        keys[i + 1];


      if (
        a === b
      ) {
        continue;
      }


      if (
        !edges.has(a)
      ) {

        edges.set(
          a,
          new Set()
        );
      }


      edges
        .get(a)
        .add(b);
    }


    console.log(
      `📦 step=${scanStep}`,
      `DOM=${nodes.length}`,
      `total=${messageMap.size}`
    );


    return {
      nodes,
      keys
    };
  }


  /************************************************************
   * 6. SCROLL TO BOTTOM FIRST
   ************************************************************/

  async function scrollToBottomFully() {

    console.log(
      '⬇️ Scrolling to bottom...'
    );


    let stable = 0;
    let lastTop = -1;
    let rounds = 0;


    while (
      stable < 3 &&
      rounds < 30
    ) {

      rounds++;


      scrollContainer.scrollTop =
        scrollContainer.scrollHeight;


      await sleep(250);


      const now =
        scrollContainer.scrollTop;


      const max =
        Math.max(
          0,
          scrollContainer.scrollHeight -
          scrollContainer.clientHeight
        );


      if (
        Math.abs(
          now - lastTop
        ) < 2 &&
        Math.abs(
          max - now
        ) < 3
      ) {

        stable++;

      } else {

        stable = 0;
      }


      lastTop =
        now;
    }


    console.log(
      '✅ Bottom reached'
    );
  }


  /************************************************************
   * 7. SCAN FROM BOTTOM TO TOP
   ************************************************************/

  async function scanFromBottomToTop() {

    console.log(
      '⬆️ Scanning upward...'
    );


    let topStable = 0;
    let safety = 0;


    while (
      topStable <
        TOP_STABLE_LIMIT &&
      safety < 10000
    ) {

      safety++;
      scanStep++;


      collect();


      const before =
        scrollContainer.scrollTop;


      /**
       * 还没到顶部
       */
      if (
        before > 1
      ) {

        const distance =
          Math.max(
            300,
            scrollContainer.clientHeight *
              STEP_RATIO
          );


        scrollContainer.scrollTop =
          Math.max(
            0,
            before - distance
          );


        await sleep(
          SCROLL_WAIT
        );


        collect();


        topStable = 0;

        continue;
      }


      /**
       * 已经到顶部
       *
       * 等待网站继续加载更早消息
       */
      const oldHeight =
        scrollContainer.scrollHeight;


      const oldTotal =
        messageMap.size;


      await sleep(
        TOP_WAIT
      );


      scanStep++;

      collect();


      const newHeight =
        scrollContainer.scrollHeight;


      const newTotal =
        messageMap.size;


      if (
        newTotal > oldTotal ||
        newHeight >
          oldHeight + 5 ||
        scrollContainer.scrollTop > 1
      ) {

        topStable = 0;


        console.log(
          '➕ Older messages loaded:',
          newTotal
        );

      } else {

        topStable++;


        console.log(
          `⏳ Top stable ${topStable}/${TOP_STABLE_LIMIT}`
        );
      }
    }


    collect();


    console.log(
      '✅ Scan complete:',
      messageMap.size
    );
  }


  /************************************************************
   * 8. FINAL ORDER
   ************************************************************/

  function getFinalOrder() {

    /**
     * 最优情况:
     *
     * 扫描结束后页面已经把全部消息留在 DOM 中。
     *
     * 这种情况下直接相信当前 DOM 顺序,
     * 不再自己排序。
     */

    const finalNodes =
      [
        ...document.querySelectorAll(
          MESSAGE_SELECTOR
        )
      ];


    const finalKeys =
      finalNodes.map(
        getMessageKey
      );


    const finalKeySet =
      new Set(finalKeys);


    const allCollectedPresent =
      [...messageMap.keys()]
        .every(
          key =>
            finalKeySet.has(key)
        );


    if (
      allCollectedPresent &&
      finalKeys.length
    ) {

      console.log(
        '🎯 Using final DOM order'
      );


      const seen =
        new Set();


      return finalKeys.filter(
        key => {

          if (
            seen.has(key)
          ) {
            return false;
          }

          seen.add(key);

          return true;
        }
      );
    }


    /**
     * fallback:
     *
     * 如果还是严格虚拟滚动,
     * 根据之前记录的局部 DOM 顺序重建。
     */

    console.log(
      '🧩 Rebuilding virtual list order'
    );


    const allKeys =
      [...messageMap.keys()];


    const indegree =
      new Map();


    allKeys.forEach(
      key =>
        indegree.set(
          key,
          0
        )
    );


    edges.forEach(
      (targets, from) => {

        targets.forEach(
          to => {

            if (
              indegree.has(to) &&
              from !== to
            ) {

              indegree.set(
                to,
                indegree.get(to) + 1
              );
            }
          }
        );
      }
    );


    let queue =
      allKeys.filter(
        key =>
          indegree.get(key) === 0
      );


    /**
     * 因为是从底部往顶部扫描,
     * 越晚第一次看到的消息通常越靠前。
     */
    queue.sort(
      (a, b) =>
        (
          firstSeenStep.get(b) || 0
        ) -
        (
          firstSeenStep.get(a) || 0
        )
    );


    const result = [];


    while (
      queue.length
    ) {

      const key =
        queue.shift();


      result.push(key);


      const targets =
        edges.get(key);


      if (
        !targets
      ) {
        continue;
      }


      for (
        const to of targets
      ) {

        if (
          !indegree.has(to)
        ) {
          continue;
        }


        indegree.set(
          to,
          indegree.get(to) - 1
        );


        if (
          indegree.get(to) === 0
        ) {

          queue.push(to);


          queue.sort(
            (a, b) =>
              (
                firstSeenStep.get(b) || 0
              ) -
              (
                firstSeenStep.get(a) || 0
              )
          );
        }
      }
    }


    /**
     * 防止异常关系导致遗漏
     */
    const used =
      new Set(result);


    for (
      const key of allKeys
    ) {

      if (
        !used.has(key)
      ) {

        result.push(key);
      }
    }


    return result;
  }


  /************************************************************
   * 9. START
   ************************************************************/

  await scrollToBottomFully();


  scanStep++;

  collect();


  await scanFromBottomToTop();


  const orderedKeys =
    getFinalOrder();


  console.log(
    '📚 Final messages:',
    orderedKeys.length
  );


  /************************************************************
   * 10. CLEAN OLD PREVIEW
   ************************************************************/

  document
    .querySelector(
      '#ai-export-preview'
    )
    ?.remove();


  document
    .querySelector(
      '#ai-export-print-style'
    )
    ?.remove();


  /************************************************************
   * 11. CREATE PREVIEW
   ************************************************************/

  const preview =
    document.createElement(
      'div'
    );


  preview.id =
    'ai-export-preview';


  preview.innerHTML = `

    <div id="ai-export-toolbar">

      <button id="ai-export-close">
        Close
      </button>

      <button id="ai-export-print">
        Print PDF
      </button>

      <span id="ai-export-count">
      </span>

    </div>

    <div id="ai-export-content">
    </div>

  `;


  preview.style.cssText = `

    position: fixed;

    inset: 0;

    z-index: 2147483647;

    overflow-y: auto;

    overflow-x: hidden;

    background: white;

    padding: 0;

  `;


  /************************************************************
   * 12. TOOLBAR
   ************************************************************/

  const toolbar =
    preview.querySelector(
      '#ai-export-toolbar'
    );


  toolbar.style.cssText = `

    position: sticky;

    top: 0;

    z-index: 9999;

    display: flex;

    align-items: center;

    gap: 10px;

    padding: 12px 20px;

    background: white;

    border-bottom:
      1px solid rgba(0,0,0,.15);

  `;


  toolbar
    .querySelectorAll(
      'button'
    )
    .forEach(
      button => {

        button.style.cssText = `

          padding: 7px 14px;

          cursor: pointer;

          border:
            1px solid #aaa;

          border-radius: 6px;

          background: white;

          color: black;

        `;
      }
    );


  /************************************************************
   * 13. CONTENT
   ************************************************************/

  const content =
    preview.querySelector(
      '#ai-export-content'
    );


  content.style.cssText = `

    width: 100%;

    max-width: 1000px;

    margin: 0 auto;

    padding:
      30px 40px 80px;

    box-sizing:
      border-box;

  `;


  /************************************************************
   * 14. AI EXPORTER BANNER
   *
   * 只在当前页面显示。
   * 打印 PDF 时隐藏。
   ************************************************************/

  const lang =
    (
      navigator.language ||
      'en'
    )
      .replace(
        '_',
        '-'
      );


  const aiExporterUrl =
    'https://chromewebstore.google.com/detail/' +
    'ai-exporter-save-chatgpt/' +
    'kagjkiiecagemklhmhkabbalfpbianbe' +
    `?hl=${encodeURIComponent(lang)}` +
    '&utm_source=chatgptprint';


  const promotion =
    document.createElement(
      'div'
    );


  promotion.id =
    'ai-export-promotion';


  promotion.innerHTML = `

    <div class="ai-export-promo-main">

      <div class="ai-export-promo-badge">
        AI Exporter
      </div>

      <div class="ai-export-promo-text">

        <div class="ai-export-promo-title">
          Export ChatGPT to PDF with AI Exporter
        </div>

        <div class="ai-export-promo-description">
          PDF, Word, Markdown and more.
        </div>

      </div>

      <a
        class="ai-export-promo-button"
        href="${aiExporterUrl}"
        target="_blank"
        rel="noopener noreferrer"
      >
        Get AI Exporter →
      </a>

    </div>

  `;


  promotion.style.cssText = `

    margin:
      0 auto 34px;

    padding:
      18px 20px;

    max-width:
      920px;

    box-sizing:
      border-box;

    border:
      1px solid rgba(59,130,246,.25);

    border-radius:
      14px;

    background:
      linear-gradient(
        135deg,
        #eff6ff 0%,
        #eef2ff 52%,
        #f5f3ff 100%
      );

    color:
      #172554;

    box-shadow:
      0 6px 22px
      rgba(59,130,246,.10);

    font-family:
      -apple-system,
      BlinkMacSystemFont,
      "Segoe UI",
      sans-serif;

  `;


  const promoMain =
    promotion.querySelector(
      '.ai-export-promo-main'
    );


  promoMain.style.cssText = `

    display: flex;

    align-items: center;

    gap: 16px;

  `;


  const badge =
    promotion.querySelector(
      '.ai-export-promo-badge'
    );


  badge.style.cssText = `

    flex:
      0 0 auto;

    padding:
      7px 10px;

    border-radius:
      8px;

    background:
      #2563eb;

    color:
      white;

    font-size:
      13px;

    line-height:
      1;

    font-weight:
      700;

    letter-spacing:
      .2px;

  `;


  const promoText =
    promotion.querySelector(
      '.ai-export-promo-text'
    );


  promoText.style.cssText = `

    flex:
      1 1 auto;

    min-width:
      0;

  `;


  const promoTitle =
    promotion.querySelector(
      '.ai-export-promo-title'
    );


  promoTitle.style.cssText = `

    margin-bottom:
      4px;

    font-size:
      16px;

    line-height:
      1.45;

    font-weight:
      700;

    color:
      #172554;

  `;


  const promoDescription =
    promotion.querySelector(
      '.ai-export-promo-description'
    );


  promoDescription.style.cssText = `

    font-size:
      13px;

    line-height:
      1.5;

    color:
      #475569;

  `;


  const promoButton =
    promotion.querySelector(
      '.ai-export-promo-button'
    );


  promoButton.style.cssText = `

    flex:
      0 0 auto;

    padding:
      9px 13px;

    border-radius:
      8px;

    background:
      #2563eb;

    color:
      white;

    font-size:
      13px;

    line-height:
      1.2;

    font-weight:
      600;

    text-decoration:
      none;

    white-space:
      nowrap;

  `;


  /**
   * 广告放在所有对话最上方
   */
  content.appendChild(
    promotion
  );


  /************************************************************
   * 15. APPEND MESSAGES
   ************************************************************/

  for (
    const key of orderedKeys
  ) {

    const node =
      messageMap.get(key);


    if (
      !node
    ) {
      continue;
    }


    content.appendChild(
      node.cloneNode(true)
    );
  }


  /************************************************************
   * 16. MESSAGE COUNT
   ************************************************************/

  preview.querySelector(
    '#ai-export-count'
  ).textContent =
    `${orderedKeys.length} messages`;


  document.body.appendChild(
    preview
  );


  /************************************************************
   * 17. PRINT CSS
   ************************************************************/

  const printStyle =
    document.createElement(
      'style'
    );


  printStyle.id =
    'ai-export-print-style';


  printStyle.textContent = `

    @media print {

      html,
      body {

        width:
          auto !important;

        height:
          auto !important;

        min-height:
          0 !important;

        max-height:
          none !important;

        overflow:
          visible !important;

      }


      body > *:not(#ai-export-preview) {

        display:
          none !important;

      }


      #ai-export-preview {

        display:
          block !important;

        position:
          static !important;

        inset:
          auto !important;

        width:
          100% !important;

        height:
          auto !important;

        min-height:
          0 !important;

        max-height:
          none !important;

        overflow:
          visible !important;

        padding:
          0 !important;

        margin:
          0 !important;

        background:
          white !important;

      }


      #ai-export-toolbar {

        display:
          none !important;

      }


      /******************************************************
       * PDF 中隐藏 AI Exporter 推荐
       ******************************************************/

      #ai-export-promotion {

        display:
          none !important;

      }


      #ai-export-content {

        display:
          block !important;

        position:
          static !important;

        width:
          100% !important;

        max-width:
          none !important;

        height:
          auto !important;

        min-height:
          0 !important;

        max-height:
          none !important;

        overflow:
          visible !important;

        margin:
          0 !important;

        padding:
          0 !important;

      }


      #ai-export-content > * {

        overflow:
          visible !important;

        max-height:
          none !important;

      }

    }

  `;


  document.head.appendChild(
    printStyle
  );


  /************************************************************
   * 18. CLOSE
   ************************************************************/

  preview.querySelector(
    '#ai-export-close'
  ).onclick = () => {

    preview.remove();

    printStyle.remove();

    console.log(
      '✅ Preview closed'
    );
  };


  /************************************************************
   * 19. PRINT
   ************************************************************/

  function printPDF() {

    console.log(
      '🖨️ Opening print dialog...'
    );


    /**
     * 强制完成 layout
     */
    preview.offsetHeight;


    requestAnimationFrame(
      () => {

        requestAnimationFrame(
          () => {

            window.print();

          }
        );

      }
    );
  }


  preview.querySelector(
    '#ai-export-print'
  ).onclick =
    printPDF;


  console.log(
    '✅ Export preview ready'
  );


  console.log(
    `📄 ${orderedKeys.length} messages`
  );


  /************************************************************
   * 20. AUTO PRINT
   ************************************************************/

  await sleep(500);


  printPDF();

})();

El script es largo; usa el botón en lugar de seleccionarlo manualmente.

Pega en la consola únicamente código de una fuente de confianza. Ejecuta el script solo en conversaciones a las que tengas permiso para acceder.

Paso 3: espera a que se abra la ventana de impresión

No cierres la pestaña de ChatGPT mientras se ejecuta el script. La página se desplazará automáticamente y la consola mostrará cuántos mensajes se han recopilado. Cuanto más larga sea la conversación, más tardará. Al finalizar, Chrome abrirá automáticamente la ventana de impresión.

Selecciona tu impresora para imprimir en papel. Para crear un archivo, elige Guardar como PDF en “Destino” y pulsa Guardar.

Ventana de impresión de Chrome configurada para guardar como PDF tras recopilar los mensajes de ChatGPT

La consola muestra el número de mensajes durante el proceso y abre la ventana de impresión al terminar.

Método 2: exportar el chat completo con una extensión

Si prefieres no usar las herramientas para desarrolladores, AI Exporter  ofrece un proceso más sencillo. Abre la conversación, haz clic en el icono de AI Exporter en la barra de Chrome y selecciona PDF en la ventana emergente. La extensión cargará el chat completo y lo guardará como PDF.

Ventana de AI Exporter sobre una conversación de ChatGPT con la opción PDF resaltada

Abre la extensión y selecciona PDF para guardar toda la conversación.

Este método requiere instalar una extensión, pero resulta más rápido para exportaciones habituales y conserva bloques de código, tablas, imágenes y fórmulas sin configuración adicional. También puedes elegir mensajes concretos si no necesitas el chat completo.

¿Qué método te conviene?

Tu situaciónMejor opción
Solo necesitas imprimir un chat y no quieres instalar nadaScript de la consola
Te manejas bien con las herramientas para desarrolladoresScript de la consola
Guardas conversaciones de ChatGPT con frecuenciaExtensión del navegador
Necesitas conservar código, tablas, fórmulas, imágenes o ajustes de PDFExtensión del navegador

Para una impresión puntual, empieza con el script. Para exportaciones frecuentes o un PDF mejor presentado, la extensión es más cómoda.


Este artículo original fue escrito por Colin. Si lo vuelves a publicar, incluye un enlace a este artículo como fuente original.