Skip to content

Instantly share code, notes, and snippets.

@e-orlov
Created August 29, 2025 19:00
Show Gist options
  • Select an option

  • Save e-orlov/580f2214e32155f383b9cbe342e3225a to your computer and use it in GitHub Desktop.

Select an option

Save e-orlov/580f2214e32155f383b9cbe342e3225a to your computer and use it in GitHub Desktop.
Tampermonkey userscript to download chatGPT and customGPT responses as clean HTML
// ==UserScript==
// @name ChatGPT + CustomGPT: Download response as HTML
// @namespace gpt-download-html
// @version 1.5
// @description Adds a "Download as HTML" button to each assistant response across ChatGPT and Custom GPT chats
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
// -------- Utilities --------
function slugify(text, fallback = "chatgpt-response") {
if (!text) return fallback + ".html";
const s = String(text)
.trim()
.toLowerCase()
.replace(/<[^>]+>/g, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || fallback;
return s + ".html";
}
function wrapAsHTML(inner, titleText) {
const title = (titleText || "ChatGPT Response").replace(/\s+/g, " ").trim();
const description = title;
const now = new Date().toISOString();
const style = `
:root { color-scheme: light dark; }
html, body { margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
line-height: 1.6; padding: 24px; display: flex; justify-content: center; }
main { width: 100%; max-width: 860px; }
img, video { max-width: 100%; height: auto; }
pre { overflow: auto; padding: 12px; border-radius: 8px; background: rgba(127,127,127,.08); }
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
table { border-collapse: collapse; width: 100%; margin: 16px 0; }
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; }
h1 { line-height: 1.2; margin-top: 0; }
a { text-decoration: underline; }
figure { margin: 0; }
figcaption { font-size: .9em; opacity: .8; }
.meta { font-size: .9em; opacity: .75; margin-bottom: 12px; }
`;
return `<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title>
<meta name="description" content="${description}">
<meta name="generator" content="ChatGPT export userscript">
<style>${style}</style>
</head>
<body>
<main>
<div class="meta">Exported: ${now}</div>
${inner}
</main>
</body>
</html>`;
}
// -------- Core logic --------
// Robust assistant-turn selector set covering classic + Custom GPT UIs
function findAssistantTurns(root = document) {
const sel = [
// Classic role attribute
'div[data-message-author-role="assistant"]',
// Newer conversation turn container with role on ancestor
'div[data-testid="conversation-turn"][data-role="assistant"]',
// Fallback: any conversation turn that visually matches assistant messages
'div.group\\/conversation-turn:has([data-message-author-role="assistant"])',
// Custom GPT sometimes uses this testid
'div[data-testid="assistant-turn"]',
].join(",");
// Unique-ify results
const seen = new Set();
const nodes = [];
root.querySelectorAll(sel).forEach(n => {
if (!seen.has(n)) { seen.add(n); nodes.push(n); }
});
return nodes;
}
// Find the rendered content n
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment