├── src ├── background.js ├── assets │ └── icon.png ├── manifest.json ├── popup.html ├── popup.css └── popup.js ├── LICENSE.txt ├── .gitignore └── README.md /src/background.js: -------------------------------------------------------------------------------- 1 | // No scripts here -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Luo-Yihang/ChatGPT-History-Downloader/HEAD/src/assets/icon.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ChatGPT History Downloader", 3 | "version": "0.30", 4 | "manifest_version": 3, 5 | "background": { 6 | "service_worker": "background.js" 7 | }, 8 | "permissions": ["scripting", "activeTab", "downloads"], 9 | "action": { 10 | "default_popup": "popup.html", 11 | "default_icon": "./assets/icon.png" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ChatGPT History Downloader 8 | 9 | 10 | 11 | 12 |
13 |
14 |
💬
15 |
ChatGPT History Downloader
16 |
17 | 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luo Yihang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /tmp 4 | /out-tsc 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # Bower dependency directory (https://bower.io/) 25 | bower_components 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # IDEs and editors 31 | .idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | 46 | # misc 47 | .sass-cache 48 | connect.lock 49 | typings 50 | 51 | # Logs 52 | logs 53 | *.log 54 | npm-debug.log* 55 | yarn-debug.log* 56 | yarn-error.log* 57 | 58 | 59 | # Dependency directories 60 | node_modules/ 61 | jspm_packages/ 62 | 63 | # Optional npm cache directory 64 | .npm 65 | 66 | # Optional eslint cache 67 | .eslintcache 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variables file 79 | .env 80 | 81 | # next.js build output 82 | .next 83 | 84 | # Lerna 85 | lerna-debug.log 86 | 87 | # System Files 88 | .DS_Store 89 | Thumbs.db -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | 5 | Logo 6 | 7 | 8 |

ChatGPT-History-Downloader

9 | 10 |

11 | Never lose a conversation with OpenAI ChatGPT again! With the ChatGPT-History-Downloader, you can easily save your chat history as Markdown files locally. The extension automatically detects and references images and files in your conversations. ChatGPT-History-Downloader is a browser extension that supports Google Chrome / Microsoft Edge. 12 |
13 |

14 |
15 | 16 | 17 | ## Getting Started 18 | **Google Chrome / Microsoft Edge** 19 | 1. Clone the repo 20 | ```sh 21 | git clone https://github.com/Luo-Yihang/ChatGPT-History-Downloader 22 | ``` 23 | 2. In Chrome/Edge go to the extensions page (`chrome://extensions` / `edge://extensions`). 24 | 3. Enable Developer Mode. 25 | 4. Drag this `src` folder in the repo anywhere on the page to import it (do not delete the folder afterwards). 26 | 27 | ## Usage 28 | 29 | 1. Switch to the ChatGPT tab in the browser 30 | 2. Click the `Extensions` button (it is usually on the top right corner of the browser), and select the `ChatGPT-History-Downloader` extension 31 | 3. Click the download button to save your chat history 32 | 33 | 34 | ## A sample downloaded Markdown output 35 | ``` 36 | **User**: Hi! How are you? 37 | 38 | -------- 39 | **ChatGPT**: Hello! As an AI language model, I don't have feelings like humans do, but I'm here to help you with any questions or tasks you have. How can I assist you today? 40 | 41 | -------- 42 | **User**: Can you analyze this image? ![Image](image_2_0.png) 43 | 44 | -------- 45 | **ChatGPT**: I can see the image you've shared. It appears to be... 46 | ``` 47 | 48 | 49 | ## License 50 | 51 | Distributed under the MIT License. See `LICENSE.txt` for more information. 52 | -------------------------------------------------------------------------------- /src/popup.css: -------------------------------------------------------------------------------- 1 | /* Reset and base styles */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; 10 | background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 50%, #2d2d2d 100%); 11 | min-height: 100vh; 12 | display: flex; 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | /* Main container */ 18 | main { 19 | background: linear-gradient(145deg, #1a1a1a 0%, #2d2d2d 100%); 20 | border: 1px solid rgba(255, 255, 255, 0.1); 21 | border-radius: 20px; 22 | box-shadow: 23 | 0 20px 40px rgba(0, 0, 0, 0.6), 24 | inset 0 1px 0 rgba(255, 255, 255, 0.1), 25 | inset 0 -1px 0 rgba(0, 0, 0, 0.3); 26 | padding: 24px; 27 | text-align: center; 28 | max-width: 280px; 29 | width: 100%; 30 | position: relative; 31 | } 32 | 33 | /* Download area */ 34 | .download-area { 35 | display: flex; 36 | flex-direction: column; 37 | align-items: center; 38 | gap: 16px; 39 | } 40 | 41 | /* Icon */ 42 | .icon { 43 | font-size: 32px; 44 | margin-bottom: 8px; 45 | filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3)); 46 | } 47 | 48 | /* Title */ 49 | .title { 50 | font-size: 15px; 51 | font-weight: 500; 52 | color: #e0e0e0; 53 | margin-bottom: 16px; 54 | letter-spacing: -0.2px; 55 | text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5); 56 | line-height: 1.4; 57 | text-align: center; 58 | } 59 | 60 | /* Status message */ 61 | .status-message { 62 | min-height: 16px; 63 | font-size: 13px; 64 | color: #a0a0a0; 65 | margin-bottom: 8px; 66 | transition: all 0.3s ease; 67 | opacity: 0; 68 | text-align: center; 69 | } 70 | 71 | .status-message.show { 72 | opacity: 1; 73 | } 74 | 75 | .status-message.success { 76 | color: #4ade80; 77 | } 78 | 79 | .status-message.error { 80 | color: #f87171; 81 | } 82 | 83 | .status-message.info { 84 | color: #60a5fa; 85 | } 86 | 87 | /* Download button */ 88 | .download-button { 89 | background: linear-gradient(145deg, #3b3b3b 0%, #2d2d2d 100%); 90 | color: #ffffff; 91 | border: 1px solid rgba(255, 255, 255, 0.1); 92 | padding: 14px 28px; 93 | border-radius: 14px; 94 | font-size: 14px; 95 | font-weight: 600; 96 | cursor: pointer; 97 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 98 | box-shadow: 99 | 0 8px 32px rgba(0, 0, 0, 0.4), 100 | inset 0 1px 0 rgba(255, 255, 255, 0.1), 101 | inset 0 -1px 0 rgba(0, 0, 0, 0.2); 102 | letter-spacing: 0.3px; 103 | text-transform: none; 104 | position: relative; 105 | overflow: hidden; 106 | margin: 0 auto; 107 | display: block; 108 | width: fit-content; 109 | min-width: 160px; 110 | text-align: center; 111 | } 112 | 113 | .download-button::before { 114 | content: ''; 115 | position: absolute; 116 | top: 0; 117 | left: -100%; 118 | width: 100%; 119 | height: 100%; 120 | background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent); 121 | transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1); 122 | } 123 | 124 | .download-button:hover { 125 | background: linear-gradient(145deg, #4a4a4a 0%, #3b3b3b 100%); 126 | transform: translateY(-2px); 127 | box-shadow: 128 | 0 12px 40px rgba(0, 0, 0, 0.5), 129 | inset 0 1px 0 rgba(255, 255, 255, 0.15), 130 | inset 0 -1px 0 rgba(0, 0, 0, 0.3); 131 | border-color: rgba(255, 255, 255, 0.2); 132 | } 133 | 134 | .download-button:hover::before { 135 | left: 100%; 136 | } 137 | 138 | .download-button:active { 139 | transform: translateY(0); 140 | background: linear-gradient(145deg, #2d2d2d 0%, #3b3b3b 100%); 141 | box-shadow: 142 | 0 4px 20px rgba(0, 0, 0, 0.6), 143 | inset 0 1px 0 rgba(255, 255, 255, 0.05), 144 | inset 0 -1px 0 rgba(0, 0, 0, 0.4); 145 | } 146 | 147 | .download-button:focus { 148 | outline: none; 149 | box-shadow: 150 | 0 0 0 3px rgba(255, 255, 255, 0.1), 151 | 0 8px 32px rgba(0, 0, 0, 0.4); 152 | } 153 | 154 | /* Loading state */ 155 | .download-button.loading { 156 | background: linear-gradient(145deg, #404040 0%, #2d2d2d 100%); 157 | cursor: not-allowed; 158 | transform: none; 159 | color: #808080; 160 | } 161 | 162 | .download-button.loading::after { 163 | content: "⏳"; 164 | margin-left: 6px; 165 | animation: spin 1s linear infinite; 166 | } 167 | 168 | @keyframes spin { 169 | from { transform: rotate(0deg); } 170 | to { transform: rotate(360deg); } 171 | } 172 | 173 | /* Success state */ 174 | .download-button.success { 175 | background: linear-gradient(145deg, #065f46 0%, #047857 100%); 176 | border-color: rgba(74, 222, 128, 0.3); 177 | box-shadow: 178 | 0 8px 32px rgba(0, 0, 0, 0.4), 179 | inset 0 1px 0 rgba(74, 222, 128, 0.2), 180 | inset 0 -1px 0 rgba(0, 0, 0, 0.3); 181 | } 182 | 183 | .download-button.success::after { 184 | content: "✓"; 185 | margin-left: 6px; 186 | color: #4ade80; 187 | } 188 | 189 | /* Responsive design */ 190 | @media (max-width: 400px) { 191 | main { 192 | margin: 16px; 193 | padding: 20px 16px; 194 | } 195 | 196 | .download-button { 197 | padding: 12px 24px; 198 | font-size: 13px; 199 | min-width: 140px; 200 | } 201 | } -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | const downloadButton = document.getElementById("download-markdown"); 3 | const statusMessage = document.getElementById("status-message"); 4 | 5 | function showStatus(message, type = 'info') { 6 | statusMessage.textContent = message; 7 | statusMessage.className = `status-message show ${type}`; 8 | } 9 | 10 | function hideStatus() { 11 | statusMessage.className = 'status-message'; 12 | } 13 | 14 | function setButtonState(state) { 15 | downloadButton.className = `download-button ${state}`; 16 | downloadButton.disabled = state === 'loading'; 17 | } 18 | 19 | downloadButton.addEventListener("click", async function () { 20 | setButtonState('loading'); 21 | showStatus('Processing conversation content...', 'info'); 22 | 23 | try { 24 | const tab = await getCurrentTab(); 25 | 26 | chrome.scripting.executeScript({ 27 | target: { tabId: tab.id }, 28 | function: downloadMarkdown, 29 | }); 30 | 31 | showStatus('Download successful!', 'success'); 32 | setButtonState('success'); 33 | 34 | // Reset button state after 2 seconds 35 | setTimeout(() => { 36 | setButtonState('ready'); 37 | hideStatus(); 38 | }, 2000); 39 | 40 | } catch (error) { 41 | console.error('Error during download:', error); 42 | showStatus('Download failed, please check console.', 'error'); 43 | setButtonState('ready'); 44 | 45 | // Reset error message after 3 seconds 46 | setTimeout(() => { 47 | hideStatus(); 48 | }, 3000); 49 | } 50 | }); 51 | 52 | async function getCurrentTab() { 53 | const queryOptions = { active: true, currentWindow: true }; 54 | const [tab] = await chrome.tabs.query(queryOptions); 55 | return tab; 56 | } 57 | 58 | function downloadMarkdown() { 59 | function h(html) { 60 | // Don't process if it's already markdown-like content 61 | if (html.includes('```') && !html.includes('<')) { 62 | return html.trim(); 63 | } 64 | 65 | console.log('Processing HTML:', html); 66 | 67 | let result = html; 68 | 69 | // Process formatting tags FIRST (before code blocks) 70 | result = result 71 | .replace(/]*>/g, "**") // replace strong tags with markdown bold (including attributes) 72 | .replace(/<\/strong>/g, "**") 73 | .replace(/]*>/g, "**") // replace b tags with markdown bold (including attributes) 74 | .replace(/<\/b>/g, "**") 75 | .replace(/]*>/g, "*") // replace em tags with markdown italic (including attributes) 76 | .replace(/<\/em>/g, "*") 77 | .replace(/]*>/g, "*") // replace i tags with markdown italic (including attributes) 78 | .replace(/<\/i>/g, "*"); 79 | 80 | console.log('After formatting tags:', result); 81 | 82 | // Process other structural elements 83 | result = result 84 | .replace(/

/g, "\n\n") // replace p tags with double newlines 85 | .replace(/<\/p>/g, "") 86 | .replace(//g, "\n") // replace br tags with newlines 87 | .replace(/

    /g, "\n") // remove ul tags 88 | .replace(/<\/ul>/g, "\n") 89 | .replace(/
      /g, "\n") // remove ol tags 90 | .replace(/<\/ol>/g, "\n") 91 | .replace(/
    1. /g, "- ") // replace li tags with markdown list 92 | .replace(/<\/li>/g, "\n") 93 | .replace(/]*>/g, "\n# ") // replace h1 tags with markdown h1 94 | .replace(/<\/h1>/g, "\n") 95 | .replace(/]*>/g, "\n## ") // replace h2 tags with markdown h2 96 | .replace(/<\/h2>/g, "\n") 97 | .replace(/]*>/g, "\n### ") // replace h3 tags with markdown h3 98 | .replace(/<\/h3>/g, "\n") 99 | .replace(/]*>/g, "\n#### ") // replace h4 tags with markdown h4 100 | .replace(/<\/h4>/g, "\n"); 101 | 102 | console.log('After structural elements:', result); 103 | 104 | // Handle code blocks - process pre/code combinations FIRST 105 | result = result.replace(/]*>]*>([\s\S]*?)<\/code><\/pre>/g, (match, code) => { 106 | const cleanCode = code 107 | .replace(/</g, '<') 108 | .replace(/>/g, '>') 109 | .replace(/&/g, '&') 110 | .replace(/"/g, '"') 111 | .replace(/ /g, ' ') 112 | .trim(); 113 | console.log('Processing code block:', cleanCode); 114 | 115 | // Check for language class to determine markdown syntax 116 | const languageMatch = match.match(/class="[^"]*language-([^"\s]+)/); 117 | const language = languageMatch ? languageMatch[1] : ''; 118 | 119 | if (language) { 120 | return "\n```" + language + "\n" + cleanCode + "\n```\n"; 121 | } else { 122 | return "\n```\n" + cleanCode + "\n```\n"; 123 | } 124 | }); 125 | 126 | result = result.replace(/]*>([\s\S]*?)<\/code>/g, (match, code) => { 127 | const cleanCode = code 128 | .replace(/</g, '<') 129 | .replace(/>/g, '>') 130 | .replace(/&/g, '&') 131 | .replace(/"/g, '"') 132 | .replace(/ /g, ' ') 133 | .trim(); 134 | console.log('Processing inline code:', cleanCode); 135 | 136 | const languageMatch = match.match(/class="[^"]*language-([^"\s]+)/); 137 | const language = languageMatch ? languageMatch[1] : ''; 138 | 139 | if (!cleanCode.includes('\n')) { 140 | return "`" + cleanCode + "`"; 141 | } 142 | 143 | if (language) { 144 | return "\n```" + language + "\n" + cleanCode + "\n```\n"; 145 | } else { 146 | return "\n```\n" + cleanCode + "\n```\n"; 147 | } 148 | }); 149 | 150 | console.log('After code blocks:', result); 151 | 152 | // Remove UI elements and clean HTML 153 | result = result 154 | .replace(/]*>.*?<\/button>/g, "") 155 | .replace(/]*class="[^"]*copy[^"]*"[^>]*>.*?<\/div>/g, "") 156 | .replace(/]*class="[^"]*edit[^"]*"[^>]*>.*?<\/div>/g, "") 157 | .replace(/Copy code/g, "") 158 | .replace(/Edit/g, "") 159 | .replace(/Copy/g, "") 160 | .replace(/]*class="[^"]*copy[^"]*"[^>]*>.*?<\/span>/g, "") 161 | .replace(/]*class="[^"]*edit[^"]*"[^>]*>.*?<\/span>/g, "") 162 | .replace(/]*class="[^"]*language[^"]*"[^>]*>.*?<\/div>/g, "") 163 | .replace(/]*class="[^"]*language[^"]*"[^>]*>.*?<\/span>/g, "") 164 | .replace(/]*>(.*?)<\/span>/g, "$1") 165 | .replace(/<[a-zA-Z][^>]*>/g, "") 166 | .replace(/<\/[a-zA-Z][^>]*>/g, "") 167 | .replace( 168 | /This content may violate our content policy\. If you believe this to be in error, please submit your feedback — your input will aid our research in this area\./g, 169 | "" 170 | ) 171 | .replace(/ /g, " ") 172 | .replace(/&/g, "&") 173 | .replace(/</g, "<") 174 | .replace(/>/g, ">") 175 | .replace(/"/g, '"') 176 | .replace(/\n\s*\n\s*\n/g, "\n\n") 177 | .replace(/^\s+|\s+$/g, "") 178 | .replace(/[a-zA-Z]+\*{4,}/g, "") 179 | .replace(/\n{3,}/g, "\n\n") 180 | .trim(); 181 | 182 | console.log('Final result:', result); 183 | return result; 184 | } 185 | 186 | async function processMessageContent(contentElement, messageIndex) { 187 | let content = contentElement.innerHTML; 188 | 189 | console.log('Processing message content:', contentElement); 190 | console.log('Content HTML:', content); 191 | 192 | // Check for images and replace with markdown references 193 | const images = contentElement.querySelectorAll('img'); 194 | console.log(`Found ${images.length} images`); 195 | 196 | for (let i = 0; i < images.length; i++) { 197 | const img = images[i]; 198 | const src = img.src || img.getAttribute('src') || img.getAttribute('data-src'); 199 | 200 | if (src) { 201 | console.log(`Image ${i}: ${src}`); 202 | const imgMarkdown = `![Image ${i + 1}](${src})`; 203 | content = content.replace(img.outerHTML, imgMarkdown); 204 | console.log(`Replaced image with: ${imgMarkdown}`); 205 | } 206 | } 207 | 208 | // Check for files and replace with markdown references 209 | const fileSelectors = [ 210 | 'a[download]', 211 | 'a[href*="blob:"]', 212 | 'div[class*="text-token-text-primary"] a', 213 | 'div[class*="border-token-border"] a' 214 | ]; 215 | 216 | let allFiles = []; 217 | fileSelectors.forEach(selector => { 218 | const files = contentElement.querySelectorAll(selector); 219 | allFiles = allFiles.concat(Array.from(files)); 220 | }); 221 | 222 | const uniqueFiles = [...new Set(allFiles)]; 223 | console.log(`Found ${uniqueFiles.length} files`); 224 | 225 | for (let i = 0; i < uniqueFiles.length; i++) { 226 | const fileElement = uniqueFiles[i]; 227 | const href = fileElement.href || fileElement.getAttribute('href'); 228 | 229 | if (href) { 230 | let fileName = fileElement.download || 231 | fileElement.getAttribute('download'); 232 | 233 | if (!fileName) { 234 | const filenameElement = fileElement.querySelector('div[class*="font-semibold"], .font-semibold, [class*="truncate"]'); 235 | if (filenameElement) { 236 | fileName = filenameElement.textContent.trim(); 237 | } else { 238 | fileName = fileElement.textContent.trim() || `file_${i}`; 239 | } 240 | } 241 | 242 | console.log(`File ${i}: ${fileName} - ${href}`); 243 | const fileMarkdown = `[📎 ${fileName}](${href})`; 244 | content = content.replace(fileElement.outerHTML, fileMarkdown); 245 | console.log(`Replaced file with: ${fileMarkdown}`); 246 | } 247 | } 248 | 249 | return { 250 | content: h(content), 251 | hasMedia: false, 252 | mediaFiles: [] 253 | }; 254 | } 255 | 256 | (async () => { 257 | console.log('Starting ChatGPT message extraction...'); 258 | 259 | // Try multiple selectors for different ChatGPT versions 260 | const selectorSets = [ 261 | '[data-message-author-role]', 262 | 'article[data-testid*="conversation-turn"]', 263 | '.group.text-token-text-primary', 264 | '[class*="group"][class*="text-token"]', 265 | 'article', 266 | '[class*="group"]' 267 | ]; 268 | 269 | let messageElements = []; 270 | for (const selector of selectorSets) { 271 | messageElements = document.querySelectorAll(selector); 272 | console.log(`Selector "${selector}" found ${messageElements.length} elements`); 273 | if (messageElements.length > 0) break; 274 | } 275 | 276 | if (messageElements.length === 0) { 277 | console.error('No message elements found!'); 278 | alert('Unable to find conversation content. Please ensure you are on a ChatGPT conversation page.'); 279 | return; 280 | } 281 | 282 | // Setup markdown file with title 283 | let t = `# ${ 284 | document.querySelector("title")?.innerText || "Conversation with ChatGPT" 285 | }\n\n`; 286 | 287 | let messageIndex = 0; 288 | 289 | console.log(`Processing ${messageElements.length} message elements...`); 290 | 291 | for (const s of messageElements) { 292 | const contentSelectors = [ 293 | '[class*="whitespace-pre-wrap"]', 294 | '[class*="prose"]', 295 | '[class*="markdown"]', 296 | '.markdown', 297 | '[class*="text-base"]', 298 | '[data-testid*="content"]', 299 | 'p', 300 | 'div' 301 | ]; 302 | 303 | let contentElement = null; 304 | for (const selector of contentSelectors) { 305 | contentElement = s.querySelector(selector); 306 | if (contentElement && contentElement.textContent.trim()) { 307 | console.log(`Found content with selector: ${selector}`); 308 | break; 309 | } 310 | } 311 | 312 | if (contentElement && contentElement.textContent.trim()) { 313 | let isUser = false; 314 | 315 | const userIndicators = [ 316 | s.querySelector('img[alt*="user"], img[alt*="User"]'), 317 | s.querySelector('[data-message-author-role="user"]'), 318 | s.querySelector('[data-testid*="user"]'), 319 | s.getAttribute('data-message-author-role') === 'user', 320 | s.getAttribute('data-turn') === 'user' 321 | ]; 322 | 323 | isUser = userIndicators.some(indicator => indicator); 324 | 325 | const username = isUser ? "User" : "ChatGPT"; 326 | 327 | console.log(`Processing ${username} message ${messageIndex}`); 328 | 329 | const processedContent = await processMessageContent(contentElement, messageIndex); 330 | 331 | t += messageIndex > 0 ? "\n---\n\n" : ""; 332 | t += `**${username}**:\n\n${processedContent.content}\n\n`; 333 | 334 | messageIndex++; 335 | } 336 | } 337 | 338 | console.log('Downloading markdown file...'); 339 | 340 | // Download markdown file 341 | const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); 342 | const o = document.createElement("a"); 343 | o.download = `ChatGPT_Conversation_${timestamp}.md`; 344 | o.href = URL.createObjectURL(new Blob([t], {type: 'text/markdown'})); 345 | o.style.display = "none"; 346 | document.body.appendChild(o); 347 | o.click(); 348 | document.body.removeChild(o); 349 | console.log('Markdown file downloaded successfully'); 350 | })(); 351 | } 352 | }); --------------------------------------------------------------------------------