// ==UserScript== // @name DeepSeek Chat Exporter (Markdown & PDF & PNG - English improved version) // @namespace http://tampermonkey.net/ // @version 1.8.2 // @description Export DeepSeek chat history to Markdown, PDF and PNG formats // @author HSyuf/Blueberrycongee/endolith // @match https://chat.deepseek.com/* // @grant GM_addStyle // @grant GM_download // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @license MIT // @require https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js // @downloadURL https://update.greasyfork.icu/scripts/529946/DeepSeek%20Chat%20Exporter%20%28Markdown%20%20PDF%20%20PNG%20-%20English%20improved%20version%29.user.js // @updateURL https://update.greasyfork.icu/scripts/529946/DeepSeek%20Chat%20Exporter%20%28Markdown%20%20PDF%20%20PNG%20-%20English%20improved%20version%29.meta.js // ==/UserScript== (function () { 'use strict'; // ===================== // Configuration // ===================== const config = { chatContainerSelector: '.dad65929', // Chat container userMessageSelector: '._9663006 > .fbb737a4', // Direct selector for user message content aiClassPrefix: '_4f9bf79', // AI message related class prefix aiReplyContainer: '_43c05b5', // Main container for AI replies searchHintSelector: '._58a6d71._19db599', // Search/thinking time thinkingChainSelector: '.e1675d8b', // Thinking chain finalAnswerSelector: 'div.ds-markdown.ds-markdown--block', // Final answer titleSelector: '.d8ed659a', // Chat title selector exportFileName: 'DeepSeek', // Changed from DeepSeek_Chat_Export // Header strings used in exports userHeader: 'User', assistantHeader: 'Assistant', thoughtsHeader: 'Thought Process', }; // User preferences with defaults const preferences = { convertLatexDelimiters: GM_getValue('convertLatexDelimiters', true), }; // Register menu command for toggling LaTeX delimiter conversion GM_registerMenuCommand('Toggle LaTeX Delimiter Conversion', () => { preferences.convertLatexDelimiters = !preferences.convertLatexDelimiters; GM_setValue('convertLatexDelimiters', preferences.convertLatexDelimiters); alert(`LaTeX delimiter conversion is now ${preferences.convertLatexDelimiters ? 'enabled' : 'disabled'}`); }); let __exportPNGLock = false; // Global lock to prevent duplicate clicks // ===================== // Tool functions // ===================== /** * Gets the message content if the node contains a user message, null otherwise * @param {HTMLElement} node - The DOM node to check * @returns {string|null} The user message content if found, null otherwise */ function getUserMessage(node) { const messageDiv = node.querySelector(config.userMessageSelector); return messageDiv ? messageDiv.firstChild.textContent.trim() : null; } /** * Checks if a DOM node represents an AI message * @param {HTMLElement} node - The DOM node to check * @returns {boolean} True if the node is an AI message */ function isAIMessage(node) { return node.classList.contains(config.aiClassPrefix); } /** * Extracts search or thinking time information from a node * @param {HTMLElement} node - The DOM node to extract from * @returns {string|null} Markdown formatted search/thinking info or null if not found */ function extractSearchOrThinking(node) { const hintNode = node.querySelector(config.searchHintSelector); return hintNode ? `**${hintNode.textContent.trim()}**` : null; } /** * Extracts and formats the AI's thinking chain as blockquotes * @param {HTMLElement} node - The DOM node containing the thinking chain * @returns {string|null} Markdown formatted thinking chain with header or null if not found */ function extractThinkingChain(node) { // Get the parent container first - this is the main AI reply container const containerNode = node; // Node is already the thinking chain container if (!containerNode) { console.debug('Could not find aiReplyContainer parent container'); return null; } // Get its React fiber - this connects the DOM to React's internal tree const fiberKey = Object.keys(containerNode).find(key => key.startsWith('__reactFiber$')); if (!fiberKey) return null; // Navigate the React fiber tree to find the content: let current = containerNode[fiberKey]; // Start at container div current = current.child; // First child: Empty div._9ecc93a current = current.sibling; // Sibling: Anonymous component current = current.child; // Child: Component with content prop // Check if we found the content if (!current?.memoizedProps?.content) { console.debug('Could not find markdown content in Memo'); return null; } return `### ${config.thoughtsHeader}\n\n> ${current.memoizedProps.content.split('\n').join('\n> ')}`; } /** * Extracts the final answer content from React fiber's memoizedProps * @param {HTMLElement} node - The DOM node containing the answer * @returns {string|null} Raw markdown content or null if not found */ function extractFinalAnswer(node) { const answerNode = node.querySelector(config.finalAnswerSelector); if (!answerNode) { console.debug('No answer node found'); return null; } // Get React fiber const fiberKey = Object.keys(answerNode).find(key => key.startsWith('__reactFiber$')); if (!fiberKey) { console.error('React fiber not found'); return null; } // Navigate directly to the markdown component (2 levels up) const fiber = answerNode[fiberKey]; // Start at div const level1 = fiber.return; // First parent const markdownComponent = level1?.return; // Second parent (has markdown) // If any navigation step failed or the component doesn't have markdown, return null if (!markdownComponent?.memoizedProps?.markdown) { console.error('Could not find markdown at expected location in React tree'); return null; } return markdownComponent.memoizedProps.markdown; } /** * Collects and formats all messages in the chat in chronological order * @returns {string[]} Array of markdown formatted messages */ function getOrderedMessages() { const messages = []; const chatContainer = document.querySelector(config.chatContainerSelector); if (!chatContainer) { console.error('Chat container not found'); return messages; } for (const node of chatContainer.children) { const userMessage = getUserMessage(node); if (userMessage) { messages.push(`## ${config.userHeader}\n\n${userMessage}`); } else if (isAIMessage(node)) { let output = ''; const searchHint = extractSearchOrThinking(node); if (searchHint) output += `${searchHint}\n\n`; const thinkingChainNode = node.querySelector(config.thinkingChainSelector); if (thinkingChainNode) { const thinkingChain = extractThinkingChain(thinkingChainNode); if (thinkingChain) output += `${thinkingChain}\n\n`; } const finalAnswer = extractFinalAnswer(node); if (finalAnswer) output += `${finalAnswer}\n\n`; if (output.trim()) { messages.push(`## ${config.assistantHeader}\n\n${output.trim()}`); } } } return messages; } /** * Extracts the chat title from the page * @returns {string|null} The chat title if found, null otherwise */ function getChatTitle() { const titleElement = document.querySelector(config.titleSelector); return titleElement ? titleElement.textContent.trim() : null; } /** * Generates the complete markdown content from all messages * @returns {string} Complete markdown formatted chat history */ function generateMdContent() { const messages = getOrderedMessages(); const title = getChatTitle(); let content = title ? `# ${title}\n\n` : ''; content += messages.length ? messages.join('\n\n---\n\n') : ''; // Convert LaTeX formats only if enabled if (preferences.convertLatexDelimiters) { // Use replacement functions to properly handle newlines and whitespace content = content // Inline math: \( ... \) → $ ... $ .replace(/\\\(\s*(.*?)\s*\\\)/g, (match, group) => `$${group}$`) // Display math: \[ ... \] → $$ ... $$ .replace(/\\\[([\s\S]*?)\\\]/g, (match, group) => `$$${group}$$`); } return content; } /** * Creates a filename-safe version of a string * @param {string} str - The string to make filename-safe * @param {number} maxLength - Maximum length of the resulting string * @returns {string} A filename-safe version of the input string */ function makeFilenameSafe(str, maxLength = 50) { if (!str) return ''; return str .replace(/[^a-zA-Z0-9-_\s]/g, '') // Remove special characters .replace(/\s+/g, '_') // Replace spaces with underscores .slice(0, maxLength) // Truncate to maxLength .replace(/_+$/, '') // Remove trailing underscores .trim(); } /** * Generates a filename-safe ISO 8601 timestamp * @returns {string} Formatted timestamp YYYY-MM-DD_HH_MM_SS */ function getFormattedTimestamp() { const now = new Date(); return now.toISOString() .replace(/[T:]/g, '_') // Replace T and : with _ .replace(/\..+/, ''); // Remove milliseconds and timezone } // ===================== // Export functions // ===================== /** * Exports the chat history as a markdown file * Handles math expressions and creates a downloadable .md file */ function exportMarkdown() { const mdContent = generateMdContent(); if (!mdContent) { alert("No chat history found!"); return; } const title = getChatTitle(); const safeTitle = makeFilenameSafe(title, 30); const titlePart = safeTitle ? `_${safeTitle}` : ''; const blob = new Blob([mdContent], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${config.exportFileName}${titlePart}_${getFormattedTimestamp()}.md`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 5000); } /** * Exports the chat history as a PDF * Creates a styled HTML version and opens the browser's print dialog */ function exportPDF() { const mdContent = generateMdContent(); if (!mdContent) return; const printContent = ` DeepSeek Chat Export ${mdContent.replace(new RegExp(`## ${config.userHeader}\\n\\n`, 'g'), `

${config.userHeader}

`) .replace(new RegExp(`## ${config.assistantHeader}\\n\\n`, 'g'), `

${config.assistantHeader}

`) .replace(new RegExp(`### ${config.thoughtsHeader}\\n`, 'g'), `

${config.thoughtsHeader}

`) .replace(/>\s/g, '') // Remove the blockquote markers for HTML .replace(/\n/g, '
') .replace(/---/g, '

')} `; const printWindow = window.open("", "_blank"); printWindow.document.write(printContent); printWindow.document.close(); setTimeout(() => { printWindow.print(); printWindow.close(); }, 500); } /** * Exports the chat history as a PNG image * Creates a high-resolution screenshot of the chat content */ function exportPNG() { if (__exportPNGLock) return; // Skip if currently exporting __exportPNGLock = true; const chatContainer = document.querySelector(config.chatContainerSelector); if (!chatContainer) { alert("Chat container not found!"); __exportPNGLock = false; return; } // Create sandbox container const sandbox = document.createElement('iframe'); sandbox.style.cssText = ` position: fixed; left: -9999px; top: 0; width: 800px; height: ${window.innerHeight}px; border: 0; visibility: hidden; `; document.body.appendChild(sandbox); // Deep clone and style processing const cloneNode = chatContainer.cloneNode(true); cloneNode.style.cssText = ` width: 800px !important; transform: none !important; overflow: visible !important; position: static !important; background: white !important; max-height: none !important; padding: 20px !important; margin: 0 !important; box-sizing: border-box !important; `; // Clean up interfering elements, exclude icons ['button', 'input', '.ds-message-feedback-container', '.eb23581b.dfa60d66'].forEach(selector => { cloneNode.querySelectorAll(selector).forEach(el => el.remove()); }); // Math formula fix cloneNode.querySelectorAll('.katex-display').forEach(mathEl => { mathEl.style.transform = 'none !important'; mathEl.style.position = 'relative !important'; }); // Inject sandbox sandbox.contentDocument.body.appendChild(cloneNode); sandbox.contentDocument.body.style.background = 'white'; // Wait for resources to load const waitReady = () => Promise.all([document.fonts.ready, new Promise(resolve => setTimeout(resolve, 300))]); waitReady().then(() => { return html2canvas(cloneNode, { scale: 2, useCORS: true, logging: true, backgroundColor: "#FFFFFF" }); }).then(canvas => { canvas.toBlob(blob => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${config.exportFileName}_${getFormattedTimestamp()}.png`; a.click(); setTimeout(() => { URL.revokeObjectURL(url); sandbox.remove(); }, 1000); }, 'image/png'); }).catch(err => { console.error('Screenshot failed:', err); alert(`Export failed: ${err.message}`); }).finally(() => { __exportPNGLock = false; }); } // ===================== // Create Export Menu // ===================== /** * Creates and attaches the export menu buttons to the page */ function createExportMenu() { // Create main menu const menu = document.createElement("div"); menu.className = "ds-exporter-menu"; menu.innerHTML = ` `; // Create settings panel const settingsPanel = document.createElement("div"); settingsPanel.className = "ds-settings-panel"; settingsPanel.innerHTML = `
Convert to $ LaTeX Delimiters
`; // Add event listeners menu.querySelector("#md-btn").addEventListener("click", exportMarkdown); menu.querySelector("#pdf-btn").addEventListener("click", exportPDF); menu.querySelector("#png-btn").addEventListener("click", exportPNG); // Settings button toggle menu.querySelector("#settings-btn").addEventListener("click", () => { settingsPanel.classList.toggle("visible"); }); // LaTeX toggle switch settingsPanel.querySelector("#latex-toggle").addEventListener("change", (e) => { preferences.convertLatexDelimiters = e.target.checked; GM_setValue('convertLatexDelimiters', e.target.checked); }); // Close settings when clicking outside document.addEventListener("click", (e) => { if (!settingsPanel.contains(e.target) && !menu.querySelector("#settings-btn").contains(e.target)) { settingsPanel.classList.remove("visible"); } }); document.body.appendChild(menu); document.body.appendChild(settingsPanel); } // ===================== // Styles // ===================== GM_addStyle(` .ds-exporter-menu { position: fixed; top: 10px; right: 25px; z-index: 999999; background: #ffffff; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); padding: 4px; display: flex; flex-direction: column; gap: 2px; } .export-btn { background: #f8f9fa; color: #333; border: 1px solid #dee2e6; border-radius: 4px; padding: 4px 8px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; min-width: 45px; } .export-btn:hover { background: #e9ecef; } .export-btn:active { background: #dee2e6; } /* Settings panel styles */ .ds-settings-panel { position: fixed; top: 10px; right: 95px; z-index: 999998; background: #ffffff; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); padding: 12px; display: none; color: #333; min-width: 200px; } .ds-settings-panel.visible { display: block; } .ds-settings-row { display: flex; align-items: center; gap: 12px; margin: 4px 0; color: #333; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; white-space: nowrap; } /* Toggle switch styles */ .switch { position: relative; display: inline-block; width: 40px; height: 20px; } .switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; } .slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .slider { background-color: #2196F3; } input:checked + .slider:before { transform: translateX(20px); } .settings-btn { background: none; border: none; cursor: pointer; padding: 4px; font-size: 16px; color: #666; } .settings-btn:hover { color: #333; } `); // ===================== // Initialize // ===================== /** * Initializes the exporter by waiting for the chat container to be ready * and then creating the export menu */ function init() { const checkInterval = setInterval(() => { if (document.querySelector(config.chatContainerSelector)) { clearInterval(checkInterval); createExportMenu(); } }, 500); } init(); })();