Skip to content

Instantly share code, notes, and snippets.

@LukasMFR
Created December 12, 2025 12:36
Show Gist options
  • Select an option

  • Save LukasMFR/6865ef67aee37a8c677928234072bfbf to your computer and use it in GitHub Desktop.

Select an option

Save LukasMFR/6865ef67aee37a8c677928234072bfbf to your computer and use it in GitHub Desktop.
JavaScript snippet to export a ChatGPT conversation from the web UI to a clean Markdown file, with correct user/assistant attribution, code block preservation, and basic media placeholders. Designed to be run directly in the browser console (Safari/Chrome/Firefox).
(() => {
function formatDate(date = new Date()) {
return date.toISOString().split("T")[0];
}
function escapeMarkdown(text) {
return text
.replace(/\\/g, "\\\\")
.replace(/\*/g, "\\*")
.replace(/_/g, "\\_")
.replace(/`/g, "\\`")
.replace(/\n{3,}/g, "\n\n");
}
function processMessageContent(element) {
const clone = element.cloneNode(true);
// Replace <pre><code> blocks
clone.querySelectorAll("pre").forEach((pre) => {
const code = pre.innerText.trim();
const langMatch = pre
.querySelector("code")
?.className?.match(/language-([a-zA-Z0-9]+)/);
const lang = langMatch ? langMatch[1] : "";
pre.replaceWith(`\n\n\`\`\`${lang}\n${code}\n\`\`\`\n`);
});
// Replace images and canvas with placeholders
clone.querySelectorAll("img, canvas").forEach((el) => {
el.replaceWith("[Image or Canvas]");
});
return escapeMarkdown(clone.innerText.trim());
}
function getUserDisplayName() {
// Best-effort attempts (depends on UI versions)
const candidates = [
'[data-testid="user-menu-button"]',
'button[aria-label*="Account"]',
'button[aria-label*="account"]',
'button[aria-label*="Profil"]',
'button[aria-label*="Profile"]',
];
for (const sel of candidates) {
const el = document.querySelector(sel);
const txt = el?.innerText?.trim();
if (txt && txt.length >= 2 && txt.length <= 40) return txt;
}
// Fallback: ask once
const asked = (window.__CHAT_EXPORT_USERNAME__ || "").trim();
if (asked) return asked;
const name = prompt(
"What first name / display name do you want to use for YOUR messages?",
"You"
);
window.__CHAT_EXPORT_USERNAME__ = (name || "You").trim();
return window.__CHAT_EXPORT_USERNAME__ || "You";
}
// 1) Robust selection of messages
let messageNodes = Array.from(document.querySelectorAll('[data-message-author-role]'));
// Fallback if the attribute does not exist (different UI)
if (messageNodes.length === 0) {
messageNodes = Array.from(document.querySelectorAll('div[class*="group"]'));
}
const userName = getUserDisplayName();
const lines = [];
const title = document.title?.trim() || "Conversation with ChatGPT";
const date = formatDate();
const url = window.location.href;
lines.push(`# ${title}\n`);
lines.push(`**Date:** ${date}`);
lines.push(`**Source:** [chat.openai.com](${url})\n`);
lines.push(`---\n`);
messageNodes.forEach((node) => {
// 2) Reliable role detection
let role = node.getAttribute?.("data-message-author-role");
// If we're on the "group" fallback, try to find a parent that has the role
if (!role) {
const parentWithRole = node.closest?.('[data-message-author-role]');
role = parentWithRole?.getAttribute?.("data-message-author-role");
}
const sender =
role === "user" ? userName :
role === "assistant" ? "ChatGPT" :
role ? role : "Unknown";
// 3) Content extraction
const block =
node.querySelector?.('[data-message-content]') ||
node.querySelector?.(".markdown, .prose, .whitespace-pre-wrap");
if (!block) return;
const content = processMessageContent(block);
if (!content) return;
lines.push(`### **${sender}**\n`);
lines.push(content);
lines.push("\n---\n");
});
const markdown = lines.join("\n").trim();
const blob = new Blob([markdown], { type: "text/markdown" });
const a = document.createElement("a");
a.download = `ChatGPT_Conversation_${date}.md`;
a.href = URL.createObjectURL(blob);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment