Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
321 changes: 286 additions & 35 deletions llm-chat-explorer.html
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
</style>
</head>
<body>
Expand Down Expand Up @@ -628,9 +698,24 @@ <h2 id="chat-title" style="margin:0;">Select a chat</h2>
<i class="fas fa-share"></i>
</button>
</div>
<a id="chat-link" href="#" target="_blank" class="external-link" style="display: none;">
<i class="fas fa-external-link-alt"></i> View on
</a>
<div style="display:inline-flex; align-items:center; gap:10px;">
<div class="export-container">
<button id="export-button" class="export-button" title="Export conversation" style="display: none;">
<i class="fas fa-download"></i> Export
</button>
<div id="export-dropdown" class="export-dropdown">
<div class="export-option" data-format="markdown">
<i class="fab fa-markdown"></i> Export as Markdown
</div>
<div class="export-option" data-format="html">
<i class="fab fa-html5"></i> Export as HTML
</div>
</div>
</div>
<a id="chat-link" href="#" target="_blank" class="external-link" style="display: none;">
<i class="fas fa-external-link-alt"></i> View on
</a>
</div>
</div>
<div id="chat-content" class="chat-messages">
<div class="empty-state">
Expand Down Expand Up @@ -1056,6 +1141,37 @@ <h2>Settings</h2>
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, '<a href="$1" target="_blank" style="color: var(--primary-color);">$1</a>');
text = text.replace(/```(\w*)\n?([\s\S]+?)```/g, (match, lang, code) => {
const trimmedCode = code.trim();
const codeElement = `<pre class="code-block">${escapeHtml(trimmedCode)}</pre>`;

let copyButton = '';
if (!trimmedCode.startsWith("Viewing artifacts")) {
copyButton = `<button class="copy-code-btn" onclick="copyCode(this)">Copy</button>`;
}

return `<div class="code-block-container">${copyButton}${codeElement}</div>`;
});
text = text.replace(/(?<!<\/pre>)\n/g, '<br>');

// 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, `<span class="highlight">$1</span>`);
});
}
return text;
}

function processData(data) {
const lang = localStorage.getItem('language') || 'en';
let conversations = [];
Expand All @@ -1075,7 +1191,7 @@ <h2>Settings</h2>
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
Expand Down Expand Up @@ -1244,6 +1360,16 @@ <h2>Settings</h2>
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";
Expand Down Expand Up @@ -1290,36 +1416,7 @@ <h2>Settings</h2>
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, '<a href="$1" target="_blank" style="color: var(--primary-color);">$1</a>');
text = text.replace(/```(\w*)\n?([\s\S]+?)```/g, (match, lang, code) => {
const trimmedCode = code.trim();
const codeElement = `<pre class="code-block">${escapeHtml(trimmedCode)}</pre>`;

let copyButton = '';
if (!trimmedCode.startsWith("Viewing artifacts")) {
copyButton = `<button class="copy-code-btn" onclick="copyCode(this)">Copy</button>`;
}

return `<div class="code-block-container">${copyButton}${codeElement}</div>`;
});
text = text.replace(/(?<!<\/pre>)\n/g, '<br>');

// 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, `<span class="highlight">$1</span>`);
});
}
return text;
}


// Update the chat counter if defined
document.getElementById("chat-counter-text").textContent = conversations.length + " " + translations[lang].chatCountSuffix;

Expand All @@ -1340,6 +1437,15 @@ <h2>Settings</h2>
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) {
Expand Down Expand Up @@ -1526,7 +1632,152 @@ <h2>Settings</h2>
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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${escapeHtml(chatData.title)}</title>
<style>
${document.querySelector('style')?.textContent || ''}

body {
margin: 0;
padding: 0;
}

.content {
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<div class="content">
${headerClone.outerHTML}
${contentClone.outerHTML}
</div>
</body>
</html>`;

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);
}
}
});
});

</script>
</body>
</html>