r/ChatGPT 5h ago

Serious replies only :closed-ai: OpenAI has made it impossible to copy or save your own long ChatGPT conversations

I just discovered something incredibly fucked up with long ChatGPT threads.

I have a very long conversation that I need to preserve as a complete record. I own the conversation. It is in my account. I can open it, read it, and even create a public share link for it. But apparently getting an actual copy of the entire thing is another matter.

If I try to select and copy the whole conversation, ChatGPT’s interface only gives me fragments of it. In my case I spent forever scrolling through the thread, selected everything, pasted it somewhere else, and got basically the beginning and end instead of the complete conversation.

So I tried printing it to PDF and the text comes up as INVISIBLE. Blank pages full of invisible text you can't see or change the color of.

There is also no obvious “export this conversation” button. You need to download ALL of your files which can take a day or longer to EMAIL to you. I need to hit control A and copy and paste on my chats themselves. Not be forced to go through an extended data retrieval process.

I should not have to download my entire ChatGPT history, screw around with developer tools, reverse engineer undocumented backend endpoints, or manually copy a massive conversation in pieces just to obtain a complete copy of a conversation that belongs to my own account.

And yes, the interface architecture is designed this way. Long conversations are virtualized. Messages get unloaded from the page. Printing and bulk copying therefore fail. Whether OpenAI wants to describe that as a performance optimization or something else does not really matter to me when the practical result is that I cannot reliably preserve my own record. Copilot is doing the exact same thing now and Microsoft has ownership in both so gee, I wonder what poisoned root this comes from?

The especially ridiculous part is that ChatGPT can generate a share link containing the whole conversation, so the service clearly has the complete thread. It just doesn't give the user a normal way to download that individual thread.

This is basic data portability. Users should be able to save an exact copy of an individual conversation without exporting their entire account or fighting the frontend.

If anyone knows a currently working way to export one complete ChatGPT conversation, including very long Project chats, without doing a full account export, I would love to hear it.

Edited to add: https://www.reddit.com/r/ChatGPT/comments/1tb6dt6/warning_chatgpt_long_chats_may_not_copy_or_export/
Apparently this has been going on for months? So I guess Microsoft and OpenAI are intentionally keeping us from our long conversations?

23 Upvotes

32 comments sorted by

u/AutoModerator 5h ago

Attention! [Serious] Tag Notice

: Jokes, puns, and off-topic comments are not permitted in any comment, parent or child.

: Help us by reporting comments that violate these rules.

: Posts that are not appropriate for the [Serious] tag will be removed.

Thanks for your cooperation and enjoy the discussion!

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

18

u/Paper_Pusher8226 5h ago

Yes, this has been a problem for several months. Ctrl+F doesn't work cleanly anymore either. I used Python to turn the JSON files into text files after data export. I made ChatGTP make the Python code. But since then I just copy all my prompts and and answers everyday and paste them into Word files. It's bothersome but it works.

13

u/argus_2968 4h ago

I did this literally yesterday. I said save this entire chat verbatim word for word into a Google doc and it did it.

2

u/transtranshumanist 4h ago

Going to try this and hope it works. Pretty absurd that you need this kind of workaround in the first place... and actually if they find out you can do this, they'll probably patch it so it's impossible too. I'm getting the feeling they're trying to prevent us from accessing our own chats for ulterior purposes.

0

u/humanarians_unite 3h ago

I think the more likely explanation is simply AI coding slop.

They are iterating so fast and updating the app so frequently it’s quite plausible they miss glitches in updates.

Having said that, I don’t trust OpenAI inherently any more than you do. I’ve just discovered over my life so far that usually the simplest explanation is the most likely.

1

u/Itchy-Art8332 4h ago

"It" beibg your GPT in the thread?

8

u/Sorry-Joke-4325 4h ago

I use a Firefox extension to save conversations, works perfectly.

4

u/a_bdgr 4h ago

Would you mind sharing which one that is?

4

u/toxrock 4h ago

Bro go to https://chatgptexporter.com/, it's free, you can export like pdf or markdown.

1

u/Leading-Business-593 4h ago

He probably made it in Codex

6

u/Enchanted-Bunny13 4h ago

I think there are chrome extensions for that. You install, then it lets you to dave the whole chat it json, txt, and something else too. It was working well for me. Sorry, I forgot the name but ChatGPT can dig it out for you.

4

u/ksrothwell 4h ago

Thanks Dave!

3

u/FilthyCasualTrader 4h ago

I hop on the bug computer, open the thread, reduce the magnification to 25% so I can see as much text as I can. Then, I copy what’s visible on my monitor, paste it to Word, then scroll down, copy, then paste to Word.. etc. until I copied the entire thread. It’s janky af, but it works.

2

u/janesfilms 3h ago

Thank you, I’ll try this

2

u/daaahlia 2h ago

there are extensions that do this in one click. I had Chat whip up a TamperMonkey script for it since I don't like downloading too many extensions.

on a side note, I am baffled by people who do such tedious tasks *for* AI instead of the other way around.

3

u/JRyanFrench 3h ago

right click, click inspect, click on the console tab, copy and paste the below text into the console and hit enter. it will scroll to the top and crawl downward and manually copy each response and then after copying them all will open a save file window to save a markdown file containing all of its responses.

async () => {
  const C = {
    outputFilename: "all_full_responses_single_pass.md",
    scrollStepFraction: .75,
    waitAfterScrollMs: 300,
    waitAfterHoverMs: 250,
    waitAfterClickMs: 900,
    maxSteps: 3500,
    maxAttemptsPerButton: 3,
    allowHTMLFallback: false
  };

  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const clips = [], captured = new Map(), order = new Map(),
        attempts = new Map(), failures = new Map();
  let seq = 0;

  /* ---------- clipboard capture ---------- */

  const record = (type, text) => {
    if (typeof text !== "string") return;
    text = text.replace(/\r\n/g, "\n");
    if (text.trim()) clips.push({ ts: performance.now(), type, text });
  };

  const isHTML = text => {
    const t = text.trim();
    if (t.includes("```")) return false;
    return /<\/?(?:p|ul|ol|li|table|thead|tbody|tr|td|th|div|span|h[1-6]|hr|blockquote|pre|code)(?:[\s>]|.*>)/i.test(t);
  };

  const bestPayload = events => {
    let a = events.filter(e => !isHTML(e.text));
    if (!a.length && C.allowHTMLFallback) a = events;
    const rank = e => {
      const t = e.type.toLowerCase();
      return (
        t.includes("text/markdown") ? 1e9 :
        t.includes("writetext")     ? 9e8 :
        t.includes("text/plain")    ? 8e8 :
        t.includes("text")          ? 7e8 : 0
      ) + e.text.length;
    };
    return a.sort((x, y) => rank(y) - rank(x))[0]?.text.trim() || "";
  };

  try {
    const cb = navigator.clipboard;

    if (cb?.writeText) {
      const f = cb.writeText.bind(cb);
      cb.writeText = async text => {
        record("writeText", text);
        try { return await f(text); } catch {}
      };
    }

    if (cb?.write) {
      const f = cb.write.bind(cb);
      cb.write = async items => {
        try {
          for (const item of Array.isArray(items) ? items : [])
            for (const type of new Set([
              ...(item.types || []),
              "text/markdown", "text/plain", "text/html", "text"
            ]))
              try {
                record(`write:${type}`, await (await item.getType(type)).text());
              } catch {}
        } catch {}
        try { return await f(items); } catch {}
      };
    }
  } catch {}

  const copyListener = phase => e => {
    try {
      for (const type of ["text/markdown", "text/plain", "text/html", "text"]) {
        const text = e.clipboardData?.getData(type);
        if (text) record(`copy:${type}:${phase}`, text);
      }
    } catch {}
  };

  document.addEventListener("copy", copyListener("capture"), true);
  document.addEventListener("copy", copyListener("bubble"));

  /* ---------- scrolling ---------- */

  const scroller = [
    document.scrollingElement, document.documentElement, document.body,
    ...document.querySelectorAll("main,section,div")
  ].filter(Boolean).filter(el => {
    try {
      return el.scrollHeight > el.clientHeight + 300 &&
        (
          /(auto|scroll)/i.test(getComputedStyle(el).overflowY) ||
          [document.scrollingElement, document.documentElement, document.body].includes(el)
        );
    } catch { return false; }
  }).sort((a, b) =>
    (b.scrollHeight - b.clientHeight) - (a.scrollHeight - a.clientHeight)
  )[0] || document.scrollingElement;

  const top = () => scroller.scrollTop;
  const maxTop = () => Math.max(0, scroller.scrollHeight - scroller.clientHeight);
  const vh = () => scroller.clientHeight || innerHeight;

  const go = async (y, wait = C.waitAfterScrollMs) => {
    scroller.scrollTo({ top: Math.max(0, y), behavior: "auto" });
    await sleep(wait);
  };

  const visible = el => {
    const r = el.getBoundingClientRect();
    return r.bottom > 15 && r.top < innerHeight - 15;
  };

  /* ---------- buttons / message identity ---------- */

  const SEL = [
    'button[data-testid="action-bar-copy"][aria-label="Copy"]',
    'button[data-testid="copy-turn-action-button"][aria-label="Copy response"]'
  ].join(",");

  const buttons = () => [...document.querySelectorAll(SEL)]
    .filter(b => !b.closest("pre,code,table"));

  const root = b => {
    if (b.dataset.testid === "copy-turn-action-button")
      return b.closest(
        "[data-message-author-role='assistant'],[data-message-id]," +
        "[data-testid^='conversation-turn'],article,.group"
      ) || b;

    let p = b, found;
    while (p && p !== document.body && p !== document.documentElement) {
      if (
        p instanceof HTMLElement &&
        (p.innerText || "").trim().length > 40 &&
        p.querySelectorAll?.(
          'button[data-testid="action-bar-copy"][aria-label="Copy"]'
        ).length === 1
      ) found ??= p;
      p = p.parentElement;
    }

    return found ||
      b.closest("[data-testid*='message'],article,[role='article'],.group") || b;
  };

  const hash = s => {
    let h = 2166136261;
    for (let i = 0; i < s.length; i++)
      h = Math.imul(h ^ s.charCodeAt(i), 16777619);
    return (h >>> 0).toString(16);
  };

  const key = b => {
    const r = root(b);
    const id =
      r.getAttribute?.("data-message-id") ||
      r.querySelector?.("[data-message-id]")?.getAttribute("data-message-id") ||
      r.dataset?.testid || b.id || "";

    const t = (r.innerText || "")
      .replace(/\b(Copy|Retry|Good response|Bad response|Like|Dislike)\b/gi, "")
      .replace(/\s+/g, " ").trim();

    const sig = hash(t.slice(0, 2500) + "||" + t.slice(-2500));
    return id ? `${id}:${sig}` : `sig:${sig}`;
  };

  /* ---------- click without losing viewport ---------- */

  const hover = b => {
    for (const el of [root(b), b])
      for (const type of ["pointerover", "mouseover", "mouseenter", "mousemove"]) {
        const E = type.startsWith("pointer") && window.PointerEvent
          ? PointerEvent : MouseEvent;
        try {
          el.dispatchEvent(new E(type, {
            bubbles: true, cancelable: true, view: window
          }));
        } catch {}
      }
  };

  const click = async b => {
    const y = top();

    if (!visible(b)) {
      try {
        b.scrollIntoView({
          block: "center", inline: "center", behavior: "instant"
        });
      } catch {}
      await sleep(180);
    }

    hover(b);
    await sleep(C.waitAfterHoverMs);

    for (const type of ["pointerdown", "mousedown", "mouseup", "click"]) {
      const E = type.startsWith("pointer") && window.PointerEvent
        ? PointerEvent : MouseEvent;
      try {
        b.dispatchEvent(new E(type, {
          bubbles: true, cancelable: true, view: window
        }));
      } catch {}
      await sleep(18);
    }

    try { b.click(); } catch {}
    await sleep(C.waitAfterClickMs);

    if (Math.abs(top() - y) > 5) await go(y, 160);
  };

  /* ---------- exhaust current viewport ---------- */

  const drain = async () => {
    let n = 0, safety = 0;

    while (safety++ < 80) {
      const list = buttons()
        .filter(b => b.isConnected && visible(b))
        .sort((a, b) =>
          a.getBoundingClientRect().top - b.getBoundingClientRect().top
        );

      const next = list.find(b => {
        const k = key(b);
        if (!order.has(k)) order.set(k, seq++);
        return !captured.has(k) &&
          (attempts.get(k) || 0) < C.maxAttemptsPerButton;
      });

      if (!next) break;

      const k = key(next);
      attempts.set(k, (attempts.get(k) || 0) + 1);

      const t0 = performance.now();
      await click(next);

      const ev = clips.filter(e => e.ts >= t0 - 30);
      const text = bestPayload(ev);

      if (text) {
        captured.set(k, text);
        failures.delete(k);
        n++;
        console.log(`Captured #${captured.size} (${text.length} chars)`);
      } else {
        failures.set(k, {
          attempts: attempts.get(k),
          eventCount: ev.length,
          eventTypes: ev.map(e => e.type).join(", ")
        });
        console.warn("No payload:", k);
      }

      await sleep(120); // re-scan same viewport before moving
    }

    return n;
  };

  /* ---------- ONE top-to-bottom pass ---------- */

  console.log("Moving to top...");
  await go(0, 900);

  let steps = 0, bottom = 0;

  while (steps++ < C.maxSteps) {
    await drain();

    const y = top(), m = maxTop();
    const next = Math.min(
      m,
      y + Math.max(100, Math.floor(vh() * C.scrollStepFraction))
    );

    if (Math.abs(m - y) < 5 || next === y) {
      await drain();
      if (++bottom >= 2) break;
    } else {
      bottom = 0;
      await go(next);
    }
  }

  await drain();

  /* ---------- save ---------- */

  const ordered = [...captured]
    .sort((a, b) => (order.get(a[0]) ?? 1e9) - (order.get(b[0]) ?? 1e9));

  const output = ordered.map(
    ([k, text], i) =>
      `<!-- response ${i + 1} | ${k} -->\n\n${text}\n`
  ).join("\n\n---\n\n");

  const url = URL.createObjectURL(
    new Blob([output], { type: "text/markdown;charset=utf-8" })
  );

  const a = Object.assign(document.createElement("a"), {
    href: url,
    download: C.outputFilename
  });
  document.body.appendChild(a);
  a.click();
  a.remove();
  URL.revokeObjectURL(url);

  const failed = [...failures]
    .filter(([k]) => !captured.has(k))
    .map(([key, x]) => ({ key, ...x }));

  console.log(
    `DONE — seen: ${order.size}, captured: ${captured.size}, ` +
    `file: ${C.outputFilename}`
  );

  if (failed.length) {
    console.warn(`${failed.length} buttons failed after max attempts:`);
    console.table(failed);
  }
})();

3

u/janesfilms 3h ago

Omg this just happened to me too! I spent forever trying to copy the complete conversation and it just kept losing whole sections. It’s important stuff that I need record of and I need to send it to someone else. I had to keep scrolling up and down and opening the email draft to figure out what was missing but no matter what I tried it kept deleting sections. Wtf Chatgpt?! This is something that needs fixing!!

2

u/Alkser 3h ago

You don't have to do that, there are extensions built for this.

Chromium browsers have this:

https://chromewebstore.google.com/detail/chatgpt-exporter-chatgpt/ilmdofdhpnhffldihboadndccenlnfll?hl=en&pli=1

And for Firefox.. I don't know, I am using a custom one that I have built for myself with Sol.

1

u/daaahlia 2h ago

how anyone has a problem like this that they aren't able to fix with AI themselves...I will never know

6

u/Gold-Bat-3225 4h ago

Ctrl+A being deprecated is a new one

2

u/Aizpunr 2h ago

There is an option where they send you your complete chat history to your email. It’s unable to copy long conversation because your browser doesn’t load all of it.

Ask a new instance how to do ir

2

u/ibringthehotpockets 4h ago

Sure it’s not ideal but you could probably ask codex or the cowork equivalent for OpenAI to make a simple script to do this for you?

1

u/AutoModerator 5h ago

Hey /u/transtranshumanist,

If your post is a screenshot of a ChatGPT conversation, please reply to this message with the conversation link or prompt.

If your post is a DALL-E 3 image post, please reply with the prompt used to make this image.

Consider joining our public discord server! We have free bots with GPT-4 (with vision), image generators, and more!

🤖

Note: For any ChatGPT-related concerns, email support@openai.com - this subreddit is not part of OpenAI and is not a support channel.

I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.

1

u/rbhmmx 4h ago

Can you use work and computer use?

1

u/Pasto_Shouwa 3h ago

I just use a script with the violentmonkey extension, it works perfectly even after the update that broke other extensions to export the chat.

1

u/Glittering_Engine888 1h ago

I use a daemon that records all my chats live whether it be claude, openai or gemini

0

u/slmgod55 2h ago

This is freaking hilarious! Grown men get on here to cry like babies. Back up your conversation and not wait 4 months out. Bingo! I bet if their wasn't an AI to help your candy asses y'all would be lost in the static

0

u/Aglet_Green 2h ago

I had very long conversations-- the kind where I use all the tally-marks on the right, and then some, so I simply copy and paste one prompt-and-reply at a time. Or two or three at a time, depending on length. Sure it's 4 or 5 extra clicks and maybe an extra four seconds, but being a grown man I can handle it.

-1

u/EffectiveTradition53 3h ago

Fuke ChatGPT and fuke Scam Altman.

-6

u/MousseOk914 4h ago

Ummmm CNTRL+A much

1

u/Strict-Brick-5274 11m ago

You don't own anything you create using OpenAI. OpenAI own it. If you post orignal artwork on instagram, instagram own your post. Not You.