diff --git a/src/claude_code_transcripts/__init__.py b/src/claude_code_transcripts/__init__.py index 4c64ac8..b595c10 100644 --- a/src/claude_code_transcripts/__init__.py +++ b/src/claude_code_transcripts/__init__.py @@ -883,6 +883,8 @@ def render_message(log_type, message_json, timestamp): body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-color); color: var(--text-color); margin: 0; padding: 16px; line-height: 1.6; } .container { max-width: 800px; margin: 0 auto; } h1 { font-size: 1.5rem; margin-bottom: 24px; padding-bottom: 8px; border-bottom: 2px solid var(--user-border); } +.header-row { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; border-bottom: 2px solid var(--user-border); padding-bottom: 8px; margin-bottom: 24px; } +.header-row h1 { border-bottom: none; padding-bottom: 0; margin-bottom: 0; flex: 1; min-width: 200px; } .message { margin-bottom: 16px; border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .message.user { background: var(--user-bg); border-left: 4px solid var(--user-border); } .message.assistant { background: var(--card-bg); border-left: 4px solid var(--assistant-border); } @@ -993,7 +995,25 @@ def render_message(log_type, message_json, timestamp): .index-item-long-text { margin-top: 8px; padding: 12px; background: var(--card-bg); border-radius: 8px; border-left: 3px solid var(--assistant-border); } .index-item-long-text .truncatable.truncated::after { background: linear-gradient(to bottom, transparent, var(--card-bg)); } .index-item-long-text-content { color: var(--text-color); } -@media (max-width: 600px) { body { padding: 8px; } .message, .index-item { border-radius: 8px; } .message-content, .index-item-content { padding: 12px; } pre { font-size: 0.8rem; padding: 8px; } } +#search-box { display: none; align-items: center; gap: 8px; } +#search-box input { padding: 6px 12px; border: 1px solid var(--assistant-border); border-radius: 6px; font-size: 16px; width: 180px; } +#search-box button, #modal-search-btn, #modal-close-btn { background: var(--user-border); color: white; border: none; border-radius: 6px; padding: 6px 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; } +#search-box button:hover, #modal-search-btn:hover { background: #1565c0; } +#modal-close-btn { background: var(--text-muted); margin-left: 8px; } +#modal-close-btn:hover { background: #616161; } +#search-modal { border: none; border-radius: 12px; box-shadow: 0 4px 24px rgba(0,0,0,0.2); padding: 0; width: 90vw; max-width: 900px; height: 80vh; max-height: 80vh; display: flex; flex-direction: column; } +#search-modal::backdrop { background: rgba(0,0,0,0.5); } +.search-modal-header { display: flex; align-items: center; gap: 8px; padding: 16px; border-bottom: 1px solid var(--assistant-border); background: var(--bg-color); border-radius: 12px 12px 0 0; } +.search-modal-header input { flex: 1; padding: 8px 12px; border: 1px solid var(--assistant-border); border-radius: 6px; font-size: 16px; } +#search-status { padding: 8px 16px; font-size: 0.85rem; color: var(--text-muted); border-bottom: 1px solid rgba(0,0,0,0.06); } +#search-results { flex: 1; overflow-y: auto; padding: 16px; } +.search-result { margin-bottom: 16px; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } +.search-result a { display: block; text-decoration: none; color: inherit; } +.search-result a:hover { background: rgba(25, 118, 210, 0.05); } +.search-result-page { padding: 6px 12px; background: rgba(0,0,0,0.03); font-size: 0.8rem; color: var(--text-muted); border-bottom: 1px solid rgba(0,0,0,0.06); } +.search-result-content { padding: 12px; } +.search-result mark { background: #fff59d; padding: 1px 2px; border-radius: 2px; } +@media (max-width: 600px) { body { padding: 8px; } .message, .index-item { border-radius: 8px; } .message-content, .index-item-content { padding: 12px; } pre { font-size: 0.8rem; padding: 8px; } #search-box input { width: 120px; } #search-modal { width: 95vw; height: 90vh; } } """ JS = """ diff --git a/src/claude_code_transcripts/templates/index.html b/src/claude_code_transcripts/templates/index.html index e650a7f..30ed6ea 100644 --- a/src/claude_code_transcripts/templates/index.html +++ b/src/claude_code_transcripts/templates/index.html @@ -3,9 +3,34 @@ {% block title %}Claude Code transcript - Index{% endblock %} {% block content %} -
{{ prompt_num }} prompts · {{ total_messages }} messages · {{ total_tool_calls }} tool calls · {{ total_commits }} commits · {{ total_pages }} pages
{{ index_items_html|safe }} {{ pagination_html|safe }} + + + {%- endblock %} \ No newline at end of file diff --git a/src/claude_code_transcripts/templates/search.js b/src/claude_code_transcripts/templates/search.js new file mode 100644 index 0000000..48a6e1d --- /dev/null +++ b/src/claude_code_transcripts/templates/search.js @@ -0,0 +1,277 @@ +(function() { + var totalPages = {{ total_pages }}; + var searchBox = document.getElementById('search-box'); + var searchInput = document.getElementById('search-input'); + var searchBtn = document.getElementById('search-btn'); + var modal = document.getElementById('search-modal'); + var modalInput = document.getElementById('modal-search-input'); + var modalSearchBtn = document.getElementById('modal-search-btn'); + var modalCloseBtn = document.getElementById('modal-close-btn'); + var searchStatus = document.getElementById('search-status'); + var searchResults = document.getElementById('search-results'); + + if (!searchBox || !modal) return; + + // Hide search on file:// protocol (doesn't work due to CORS restrictions) + if (window.location.protocol === 'file:') return; + + // Show search box (progressive enhancement) + searchBox.style.display = 'flex'; + + // Gist preview support - detect if we're on gistpreview.github.io + var isGistPreview = window.location.hostname === 'gistpreview.github.io'; + var gistId = null; + var gistOwner = null; + var gistInfoLoaded = false; + + if (isGistPreview) { + // Extract gist ID from URL query string like ?78a436a8a9e7a2e603738b8193b95410/index.html + var queryMatch = window.location.search.match(/^\?([a-f0-9]+)/i); + if (queryMatch) { + gistId = queryMatch[1]; + } + } + + async function loadGistInfo() { + if (!isGistPreview || !gistId || gistInfoLoaded) return; + try { + var response = await fetch('https://api.github.com/gists/' + gistId); + if (response.ok) { + var info = await response.json(); + gistOwner = info.owner.login; + gistInfoLoaded = true; + } + } catch (e) { + console.error('Failed to load gist info:', e); + } + } + + function getPageFetchUrl(pageFile) { + if (isGistPreview && gistOwner && gistId) { + // Use raw gist URL for fetching content + return 'https://gist.githubusercontent.com/' + gistOwner + '/' + gistId + '/raw/' + pageFile; + } + return pageFile; + } + + function getPageLinkUrl(pageFile) { + if (isGistPreview && gistId) { + // Use gistpreview URL format for navigation links + return '?' + gistId + '/' + pageFile; + } + return pageFile; + } + + function escapeHtml(text) { + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function openModal(query) { + modalInput.value = query || ''; + searchResults.innerHTML = ''; + searchStatus.textContent = ''; + modal.showModal(); + modalInput.focus(); + if (query) { + performSearch(query); + } + } + + function closeModal() { + modal.close(); + // Update URL to remove search fragment, preserving path and query string + if (window.location.hash.startsWith('#search=')) { + history.replaceState(null, '', window.location.pathname + window.location.search); + } + } + + function updateUrlHash(query) { + if (query) { + // Preserve path and query string when adding hash + history.replaceState(null, '', window.location.pathname + window.location.search + '#search=' + encodeURIComponent(query)); + } + } + + function highlightTextNodes(element, searchTerm) { + var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null, false); + var nodesToReplace = []; + + while (walker.nextNode()) { + var node = walker.currentNode; + if (node.nodeValue.toLowerCase().indexOf(searchTerm.toLowerCase()) !== -1) { + nodesToReplace.push(node); + } + } + + nodesToReplace.forEach(function(node) { + var text = node.nodeValue; + var regex = new RegExp('(' + escapeRegex(searchTerm) + ')', 'gi'); + var parts = text.split(regex); + if (parts.length > 1) { + var span = document.createElement('span'); + parts.forEach(function(part) { + if (part.toLowerCase() === searchTerm.toLowerCase()) { + var mark = document.createElement('mark'); + mark.textContent = part; + span.appendChild(mark); + } else { + span.appendChild(document.createTextNode(part)); + } + }); + node.parentNode.replaceChild(span, node); + } + }); + } + + function fixInternalLinks(element, pageFile) { + // Update all internal anchor links to include the page file + var links = element.querySelectorAll('a[href^="#"]'); + links.forEach(function(link) { + var href = link.getAttribute('href'); + link.setAttribute('href', pageFile + href); + }); + } + + function processPage(pageFile, html, query) { + var parser = new DOMParser(); + var doc = parser.parseFromString(html, 'text/html'); + var resultsFromPage = 0; + + // Find all message blocks + var messages = doc.querySelectorAll('.message'); + messages.forEach(function(msg) { + var text = msg.textContent || ''; + if (text.toLowerCase().indexOf(query.toLowerCase()) !== -1) { + resultsFromPage++; + + // Get the message ID for linking + var msgId = msg.id || ''; + var pageLinkUrl = getPageLinkUrl(pageFile); + var link = pageLinkUrl + (msgId ? '#' + msgId : ''); + + // Clone the message HTML and highlight matches + var clone = msg.cloneNode(true); + // Fix internal links to include the page file + fixInternalLinks(clone, pageLinkUrl); + highlightTextNodes(clone, query); + + var resultDiv = document.createElement('div'); + resultDiv.className = 'search-result'; + resultDiv.innerHTML = '' + + '