From b60bc9eb6f55bff540dd0237ea820ad7280b9fc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 22:43:36 +0000 Subject: [PATCH 01/12] Add conversation export feature for HTML and Markdown formats - Added export button with dropdown menu in content header - Implemented exportAsMarkdown() function to export conversations as .md files - Implemented exportAsHTML() function to export conversations as standalone .html files - Added helper functions for file sanitization and download - Export button appears when a conversation is selected - Supports both ChatGPT and Claude conversation formats - Maintains conversation metadata (title, date, source) in exports - HTML export respects current dark/light mode theme --- llm-chat-explorer.html | 346 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 342 insertions(+), 4 deletions(-) diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index a172910..a706d07 100644 --- a/llm-chat-explorer.html +++ b/llm-chat-explorer.html @@ -568,6 +568,76 @@ background-color: #444; color: #f0f0f0; } + + /* Export button styles */ + .export-container { + position: relative; + display: inline-block; + } + + .export-button { + background: none; + border: none; + cursor: pointer; + padding: 8px 12px; + border-radius: var(--radius); + background-color: var(--primary-light); + color: var(--primary-color); + font-size: 14px; + transition: opacity 0.2s; + display: inline-flex; + align-items: center; + gap: 5px; + } + + body.dark-mode .export-button { + color: #ffffff; + } + + .export-button:hover { + opacity: 0.8; + } + + .export-dropdown { + display: none; + position: absolute; + top: 100%; + right: 0; + margin-top: 5px; + background-color: var(--content-bg); + border: 1px solid var(--border-color); + border-radius: var(--radius); + box-shadow: var(--shadow); + min-width: 180px; + z-index: 1000; + } + + .export-dropdown.show { + display: block; + } + + .export-option { + padding: 10px 15px; + cursor: pointer; + transition: background-color 0.2s; + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + color: var(--text-color); + } + + .export-option:first-child { + border-radius: var(--radius) var(--radius) 0 0; + } + + .export-option:last-child { + border-radius: 0 0 var(--radius) var(--radius); + } + + .export-option:hover { + background-color: var(--hover-color); + } @@ -628,9 +698,24 @@

Select a chat

- +
+
+ +
+
+ Export as Markdown +
+
+ Export as HTML +
+
+
+ +
@@ -1244,6 +1329,18 @@

Settings

const chat = chatData[chatId]; document.getElementById("chat-title").textContent = chat.title; const chatLink = document.getElementById("chat-link"); + + // Set current chat data for export functionality + currentChatData = chat; + + // Show export button if there are messages + const exportButton = document.getElementById("export-button"); + if (chat.messages && chat.messages.length > 0) { + exportButton.style.display = "inline-flex"; + } else { + exportButton.style.display = "none"; + } + if (chat.url) { chatLink.href = chat.url; chatLink.style.display = "inline-flex"; @@ -1340,6 +1437,17 @@

Settings

const lang = localStorage.getItem('language') || 'en'; const chatLink = document.getElementById("chat-link"); + // Set current chat data for export functionality + currentChatData = chat; + + // Show export button if there are messages + const exportButton = document.getElementById("export-button"); + if (chat.messages && chat.messages.length > 0) { + exportButton.style.display = "inline-flex"; + } else { + exportButton.style.display = "none"; + } + // Only show the link if there are messages if (chat.messages && chat.messages.length > 0) { if (chat.service === "Claude" && chat.uuid) { @@ -1526,7 +1634,237 @@

Settings

console.error(translations[lang].copyError + ": " + err); }); }); - + + // Export functionality + let currentChatData = null; + + // Function to export as Markdown + function exportAsMarkdown(chatData) { + const lang = localStorage.getItem('language') || 'en'; + let markdown = `# ${chatData.title}\n\n`; + + if (chatData.created_time) { + markdown += `**Date:** ${chatData.created_time}\n`; + } + if (chatData.service) { + markdown += `**Source:** ${chatData.service}\n`; + } + markdown += `\n---\n\n`; + + chatData.messages.forEach((msg, index) => { + const sender = msg.role === 'user' ? translations[lang].userSender : chatData.service; + markdown += `## ${sender}\n\n${msg.text}\n\n`; + }); + + const filename = sanitizeFilename(chatData.title) + '.md'; + downloadFile(markdown, filename, 'text/markdown'); + } + + // Function to export as HTML + function exportAsHTML(chatData) { + const lang = localStorage.getItem('language') || 'en'; + const isDarkMode = document.body.classList.contains('dark-mode'); + + let html = ` + + + + + ${escapeHtml(chatData.title)} + + + +

${escapeHtml(chatData.title)}

+ \n`; + + chatData.messages.forEach((msg, index) => { + const sender = msg.role === 'user' ? translations[lang].userSender : chatData.service; + const messageClass = msg.role === 'user' ? 'message-user' : 'message-assistant'; + const avatarLetter = msg.role === 'user' ? 'U' : 'A'; + + html += `
+
+
${avatarLetter}
+ ${escapeHtml(sender)} +
+
${formatMessageForExport(msg.text)}
+
\n`; + }); + + html += ` +`; + + const filename = sanitizeFilename(chatData.title) + '.html'; + downloadFile(html, filename, 'text/html'); + } + + // Helper function to format messages for export (similar to formatMessage but for export) + function formatMessageForExport(text) { + if (!text) return ''; + text = escapeHtml(text); + // Convert links + text = text.replace(/(https?:\/\/[^\s]+)/g, '$1'); + // Convert code blocks + text = text.replace(/```(\w*)\n?([\s\S]+?)```/g, (match, lang, code) => { + return `
${code.trim()}
`; + }); + // Convert line breaks + text = text.replace(/\n/g, '
'); + return text; + } + + // Helper function to sanitize filename + function sanitizeFilename(filename) { + return filename + .replace(/[^a-z0-9]/gi, '_') + .replace(/_+/g, '_') + .substring(0, 100); + } + + // Helper function to download file + function downloadFile(content, filename, mimeType) { + const blob = new Blob([content], { type: mimeType }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } + + // Export button event listeners + const exportButton = document.getElementById('export-button'); + const exportDropdown = document.getElementById('export-dropdown'); + + exportButton.addEventListener('click', function(e) { + e.stopPropagation(); + exportDropdown.classList.toggle('show'); + }); + + // Close dropdown when clicking outside + document.addEventListener('click', function(e) { + if (!e.target.closest('.export-container')) { + exportDropdown.classList.remove('show'); + } + }); + + // Handle export option clicks + document.querySelectorAll('.export-option').forEach(option => { + option.addEventListener('click', function() { + const format = this.getAttribute('data-format'); + exportDropdown.classList.remove('show'); + + if (currentChatData) { + if (format === 'markdown') { + exportAsMarkdown(currentChatData); + } else if (format === 'html') { + exportAsHTML(currentChatData); + } + } + }); + }); + From 206283e1b296a74f5c3961024d8ed0efd0249ea2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 22:52:04 +0000 Subject: [PATCH 02/12] Remove source service from Markdown export metadata --- llm-chat-explorer.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index a706d07..e6e3ef6 100644 --- a/llm-chat-explorer.html +++ b/llm-chat-explorer.html @@ -1646,9 +1646,6 @@

Settings

if (chatData.created_time) { markdown += `**Date:** ${chatData.created_time}\n`; } - if (chatData.service) { - markdown += `**Source:** ${chatData.service}\n`; - } markdown += `\n---\n\n`; chatData.messages.forEach((msg, index) => { From ff5a23175ceb5392a935c6afcf1ed07d777f992a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 22:53:29 +0000 Subject: [PATCH 03/12] Refactor HTML export to DRY by reusing app's CSS styles - Added getExportStyles() function to dynamically extract CSS from main stylesheet - Reuses existing CSS classes and variables instead of duplicating styles - Automatically inherits dark mode styles when applicable - Reduces code duplication and improves maintainability --- llm-chat-explorer.html | 135 ++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 77 deletions(-) diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index e6e3ef6..1e2efec 100644 --- a/llm-chat-explorer.html +++ b/llm-chat-explorer.html @@ -1657,107 +1657,88 @@

Settings

downloadFile(markdown, filename, 'text/markdown'); } - // Function to export as HTML - function exportAsHTML(chatData) { - const lang = localStorage.getItem('language') || 'en'; + // Function to get CSS styles for export + function getExportStyles() { const isDarkMode = document.body.classList.contains('dark-mode'); + const styleElement = document.querySelector('style'); - let html = ` - - - - - ${escapeHtml(chatData.title)} - From c07c3120add74034c0a6dea8fb6747ead3d8940e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 22:55:50 +0000 Subject: [PATCH 04/12] Simplify export implementation - Remove currentChatData global variable - Store chatData globally as window.chatData - Use data-chat-id attribute on export button to track selected chat - Simplify CSS export by copying styles directly instead of regex parsing - More maintainable and easier to understand --- llm-chat-explorer.html | 128 +++++++++++++++++++++++++++++------------ 1 file changed, 90 insertions(+), 38 deletions(-) diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index 1e2efec..77efd50 100644 --- a/llm-chat-explorer.html +++ b/llm-chat-explorer.html @@ -1160,7 +1160,7 @@

Settings

window.totalChats = conversations.length; let chatListHTML = ''; - const chatData = {}; + const chatData = window.chatData = {}; conversations.forEach((chat, index) => { // Use 'title' (ChatGPT) or 'name' (Claude) for the title @@ -1330,13 +1330,11 @@

Settings

document.getElementById("chat-title").textContent = chat.title; const chatLink = document.getElementById("chat-link"); - // Set current chat data for export functionality - currentChatData = chat; - - // Show export button if there are messages + // Show export button if there are messages and set chat ID const exportButton = document.getElementById("export-button"); if (chat.messages && chat.messages.length > 0) { exportButton.style.display = "inline-flex"; + exportButton.setAttribute("data-chat-id", chatId); } else { exportButton.style.display = "none"; } @@ -1437,13 +1435,11 @@

Settings

const lang = localStorage.getItem('language') || 'en'; const chatLink = document.getElementById("chat-link"); - // Set current chat data for export functionality - currentChatData = chat; - - // Show export button if there are messages + // Show export button if there are messages and set chat ID const exportButton = document.getElementById("export-button"); if (chat.messages && chat.messages.length > 0) { exportButton.style.display = "inline-flex"; + exportButton.setAttribute("data-chat-id", chatId); } else { exportButton.style.display = "none"; } @@ -1636,8 +1632,6 @@

Settings

}); // Export functionality - let currentChatData = null; - // Function to export as Markdown function exportAsMarkdown(chatData) { const lang = localStorage.getItem('language') || 'en'; @@ -1660,31 +1654,34 @@

Settings

// Function to get CSS styles for export function getExportStyles() { const isDarkMode = document.body.classList.contains('dark-mode'); - const styleElement = document.querySelector('style'); - - if (!styleElement) return ''; - - // Extract only the necessary CSS rules - const cssText = styleElement.textContent; - // Get CSS variables - const rootVars = cssText.match(/:root\s*{[^}]+}/)?.[0] || ''; - const darkModeVars = isDarkMode ? (cssText.match(/body\.dark-mode\s*{[^}]+}/)?.[0] || '') : ''; - - // Get message-related styles - const messageStyles = [ - /\.message\s*{[^}]+}/g, - /\.message-user\s*{[^}]+}/g, - /\.message-assistant\s*{[^}]+}/g, - /\.message-header\s*{[^}]+}/g, - /\.message-content\s*{[^}]+}/g, - /\.avatar\s*{[^}]+}/g, - /\.code-block\s*{[^}]+}/g, - /body\.dark-mode \.code-block\s*{[^}]+}/g - ].map(regex => cssText.match(regex)?.[0] || '').join('\n '); + return `:root { + --primary-color: #10a37f; + --primary-light: #e6f7f2; + --text-color: #343541; + --text-light: #8e8ea0; + --sidebar-bg: #f7f7f8; + --content-bg: #ffffff; + --border-color: #e5e5e6; + --shadow: 0 2px 10px rgba(0, 0, 0, 0.05); + --radius: 8px; + } + ${isDarkMode ? ` + body { + --primary-color: #10a37f; + --primary-light: #0e8a69; + --text-color: #f0f0f0; + --text-light: #aaa; + --sidebar-bg: #1e1e1e; + --content-bg: #121212; + --border-color: #333; + --shadow: none; + } - return `${rootVars} - ${darkModeVars ? `\n ${darkModeVars.replace('body.dark-mode', 'body')}` : ''} + .code-block { + background-color: #2b2b2b; + color: #f8f8f2; + }` : ''} * { margin: 0; @@ -1715,7 +1712,60 @@

Settings

font-size: 0.9rem; } - ${messageStyles} + .message { + margin-bottom: 20px; + padding: 15px; + border-radius: var(--radius); + box-shadow: var(--shadow); + position: relative; + } + + .message-user { + background-color: var(--primary-light); + margin-left: 50px; + } + + .message-assistant { + background-color: var(--sidebar-bg); + margin-right: 50px; + } + + .message-header { + display: flex; + align-items: center; + margin-bottom: 8px; + } + + .avatar { + width: 28px; + height: 28px; + border-radius: 50%; + background-color: var(--primary-color); + display: flex; + align-items: center; + justify-content: center; + color: white; + margin-right: 10px; + font-size: 14px; + } + + .sender { + font-weight: 600; + font-size: 14px; + } + + .message-content { + line-height: 1.6; + } + + .code-block { + background-color: #dcdcdc; + padding: 1em; + border-radius: 5px; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; + } a { color: var(--primary-color); @@ -1833,11 +1883,13 @@

${escapeHtml(chatData.title)}

const format = this.getAttribute('data-format'); exportDropdown.classList.remove('show'); - if (currentChatData) { + const chatId = exportButton.getAttribute('data-chat-id'); + if (chatId && window.chatData && window.chatData[chatId]) { + const chatData = window.chatData[chatId]; if (format === 'markdown') { - exportAsMarkdown(currentChatData); + exportAsMarkdown(chatData); } else if (format === 'html') { - exportAsHTML(currentChatData); + exportAsHTML(chatData); } } }); From 62be4cd3c2d1cb984c41aefc68d489316819bf54 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 22:58:24 +0000 Subject: [PATCH 05/12] Extract CSS from existing style tag for export - Read CSS directly from the document's -

${escapeHtml(chatData.title)}

- \n`; - - chatData.messages.forEach((msg, index) => { - const sender = msg.role === 'user' ? translations[lang].userSender : chatData.service; - const messageClass = msg.role === 'user' ? 'message-user' : 'message-assistant'; - const avatarLetter = msg.role === 'user' ? 'U' : 'A'; - - html += `
-
-
${avatarLetter}
- ${escapeHtml(sender)} -
-
${formatMessage(msg.text)}
-
\n`; - }); - - html += ` +
+ ${headerClone.outerHTML} + ${contentClone.outerHTML} +
+ `; const filename = sanitizeFilename(chatData.title) + '.html'; From d89c501e20577418a2baaa3a13e07163efc3aee3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 23:09:33 +0000 Subject: [PATCH 10/12] Keep title and add date in HTML export header - Remove only share button, keep title in header - Remove entire right-side div with export/link buttons - Add date next to title in header when available - Date displayed in lighter color as metadata --- llm-chat-explorer.html | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index 39a9f63..ba3e7e7 100644 --- a/llm-chat-explorer.html +++ b/llm-chat-explorer.html @@ -1674,15 +1674,26 @@

Settings

const headerClone = contentHeader.cloneNode(true); const contentClone = chatContent.cloneNode(true); - // Remove share and export buttons from header + // Remove share button only (not its parent which contains the title) const shareButton = headerClone.querySelector('#share-button'); - if (shareButton) shareButton.parentElement.remove(); + if (shareButton) shareButton.remove(); - const exportContainer = headerClone.querySelector('.export-container'); - if (exportContainer) exportContainer.remove(); + // Remove the entire right side div (export container and chat link) + const rightSideDiv = headerClone.querySelector('div[style*="display:inline-flex"]:last-child'); + if (rightSideDiv) rightSideDiv.remove(); - const chatLink = headerClone.querySelector('#chat-link'); - if (chatLink) chatLink.remove(); + // Add date metadata if available + if (chatData.created_time) { + const titleContainer = headerClone.querySelector('div[style*="display:inline-flex"]'); + if (titleContainer) { + const dateSpan = document.createElement('span'); + dateSpan.style.marginLeft = '10px'; + dateSpan.style.fontSize = '0.9rem'; + dateSpan.style.color = 'var(--text-light)'; + dateSpan.textContent = `(${chatData.created_time})`; + titleContainer.appendChild(dateSpan); + } + } // Build the HTML document const html = ` From 956f2bda2d3481fd85aa83fbea18f01c0f695aab Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Nov 2025 23:12:54 +0000 Subject: [PATCH 11/12] Inline getExportStyles() function to simplify code --- llm-chat-explorer.html | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index ba3e7e7..a6747fa 100644 --- a/llm-chat-explorer.html +++ b/llm-chat-explorer.html @@ -1653,14 +1653,6 @@

Settings

downloadFile(markdown, filename, 'text/markdown'); } - // Function to get CSS styles for export - function getExportStyles() { - const styleElement = document.querySelector('style'); - if (!styleElement) return ''; - - return styleElement.textContent; - } - // Function to export as HTML function exportAsHTML(chatData) { // Get the current chat content HTML @@ -1703,7 +1695,7 @@

Settings

${escapeHtml(chatData.title)}