diff --git a/llm-chat-explorer.html b/llm-chat-explorer.html index a172910..92f2f93 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 +
+
+
+ +
@@ -1056,6 +1141,37 @@

Settings

return `${day}/${month}/${year}`; } + // Function to format message content + function formatMessage(text) { + if (!text) return ''; + // Convert links and code blocks + text = text.replace(/(https?:\/\/[^\s]+)/g, '$1'); + text = text.replace(/```(\w*)\n?([\s\S]+?)```/g, (match, lang, code) => { + const trimmedCode = code.trim(); + const codeElement = `
${escapeHtml(trimmedCode)}
`; + + let copyButton = ''; + if (!trimmedCode.startsWith("Viewing artifacts")) { + copyButton = ``; + } + + return `
${copyButton}${codeElement}
`; + }); + text = text.replace(/(?)\n/g, '
'); + + // Highlight searched words in the content if the checkbox is active + const searchTerm = document.getElementById("search-bar").value.toLowerCase().trim(); + const searchInContent = document.getElementById("search-content-toggle") && document.getElementById("search-content-toggle").checked; + if (searchTerm && searchInContent) { + const words = searchTerm.split(/\s+/).filter(word => word.length > 0); + words.forEach(word => { + const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')})`, 'gi'); + text = text.replace(regex, `$1`); + }); + } + return text; + } + function processData(data) { const lang = localStorage.getItem('language') || 'en'; let conversations = []; @@ -1075,7 +1191,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 @@ -1244,6 +1360,16 @@

Settings

const chat = chatData[chatId]; document.getElementById("chat-title").textContent = chat.title; const chatLink = document.getElementById("chat-link"); + + // 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"; + } + if (chat.url) { chatLink.href = chat.url; chatLink.style.display = "inline-flex"; @@ -1290,36 +1416,7 @@

Settings

const chatContent = document.getElementById("chat-content"); chatContent.scrollTop = scrollPreference === 'end' ? chatContent.scrollHeight : 0; } - function formatMessage(text) { - if (!text) return ''; - // Convert links and code blocks - text = text.replace(/(https?:\/\/[^\s]+)/g, '$1'); - text = text.replace(/```(\w*)\n?([\s\S]+?)```/g, (match, lang, code) => { - const trimmedCode = code.trim(); - const codeElement = `
${escapeHtml(trimmedCode)}
`; - - let copyButton = ''; - if (!trimmedCode.startsWith("Viewing artifacts")) { - copyButton = ``; - } - - return `
${copyButton}${codeElement}
`; - }); - text = text.replace(/(?)\n/g, '
'); - - // Highlight searched words in the content if the checkbox is active - const searchTerm = document.getElementById("search-bar").value.toLowerCase().trim(); - const searchInContent = document.getElementById("search-content-toggle") && document.getElementById("search-content-toggle").checked; - if (searchTerm && searchInContent) { - const words = searchTerm.split(/\s+/).filter(word => word.length > 0); - words.forEach(word => { - const regex = new RegExp(`(${word.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')})`, 'gi'); - text = text.replace(regex, `$1`); - }); - } - return text; - } - + // Update the chat counter if defined document.getElementById("chat-counter-text").textContent = conversations.length + " " + translations[lang].chatCountSuffix; @@ -1340,6 +1437,15 @@

Settings

const lang = localStorage.getItem('language') || 'en'; const chatLink = document.getElementById("chat-link"); + // 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"; + } + // Only show the link if there are messages if (chat.messages && chat.messages.length > 0) { if (chat.service === "Claude" && chat.uuid) { @@ -1526,7 +1632,152 @@

Settings

console.error(translations[lang].copyError + ": " + err); }); }); - + + // Export functionality + // 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`; + } + 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) { + // Get the current chat content HTML + const chatContent = document.getElementById('chat-content'); + const chatTitle = document.getElementById('chat-title'); + const contentHeader = document.querySelector('.content-header'); + + if (!chatContent || !chatTitle) return; + + // Clone the content header and chat content + const headerClone = contentHeader.cloneNode(true); + const contentClone = chatContent.cloneNode(true); + + // Remove share button only (not its parent which contains the title) + const shareButton = headerClone.querySelector('#share-button'); + if (shareButton) shareButton.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(); + + // 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 = ` + + + + + ${escapeHtml(chatData.title)} + + + +
+ ${headerClone.outerHTML} + ${contentClone.outerHTML} +
+ +`; + + const filename = sanitizeFilename(chatData.title) + '.html'; + downloadFile(html, filename, 'text/html'); + } + + // Helper function to sanitize filename + function sanitizeFilename(filename) { + // Only remove characters that are actually problematic for filenames + // Keep spaces, hyphens, parentheses, etc. + return filename + .replace(/[\/\\:*?"<>|]/g, '-') + .substring(0, 200); + } + + // 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'); + + const chatId = exportButton.getAttribute('data-chat-id'); + if (chatId && window.chatData && window.chatData[chatId]) { + const chatData = window.chatData[chatId]; + if (format === 'markdown') { + exportAsMarkdown(chatData); + } else if (format === 'html') { + exportAsHTML(chatData); + } + } + }); + }); +