Created
August 29, 2025 19:00
-
-
Save e-orlov/580f2214e32155f383b9cbe342e3225a to your computer and use it in GitHub Desktop.
Tampermonkey userscript to download chatGPT and customGPT responses as clean HTML
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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