// conversations-ui.js - UI Management for Conversations // UI Manager for Conversations class ConversationsUI { constructor() { this.container = document.getElementById("conversationsList"); this.currentConversationId = null; this.renameConversationId = null; this.allConversationsCache = []; // Cache for search } // Initialize (call after DOM ready) init() { this.initRenameModal(); this.initAllConversationsModal(); } // Render the list of conversations async renderConversationsList() { if (!this.container) return; const conversations = await conversationManager.getAllConversations(); if (conversations.length === 0) { this.showEmptyState(); return; } // Clear container this.container.innerHTML = ""; // Render each conversation (limit to 10 most recent) const recentConversations = conversations.slice(0, 10); for (const conv of recentConversations) { const item = await this.createConversationItem(conv); this.container.appendChild(item); } // Add "Show All" link if there are more conversations if (conversations.length > 10) { const showAllLink = document.createElement("button"); showAllLink.className = "btn-link w-full"; showAllLink.textContent = `📚 Show All (${conversations.length - 10} more)...`; showAllLink.style.marginTop = "8px"; showAllLink.addEventListener("click", () => { this.openAllConversationsModal(); }); this.container.appendChild(showAllLink); } } // Create a conversation item element async createConversationItem(conversation) { const item = document.createElement("div"); item.className = "conversation-item"; item.dataset.conversationId = conversation.id; // Check if this is the active conversation if (conversation.id === conversationManager.currentConversationId) { item.classList.add("active"); } // Get message count const messages = await conversationManager.getMessages(conversation.id); const messageCount = messages.length; // Format time const timeAgo = this.getTimeAgo(conversation.updatedAt); // Extract model short name const modelShort = this.getModelShortName(conversation.model); // Create HTML structure item.innerHTML = `
${escapeHtml(conversation.title)}
🕐 ${timeAgo} 💬 ${messageCount} msgs ${modelShort}
`; // Add click handler to load conversation item.addEventListener("click", async e => { // Don't trigger if clicking on action buttons if (e.target.closest(".conversation-actions")) { return; } await this.handleConversationClick(conversation.id); }); // Add action button handlers const deleteBtn = item.querySelector('[data-action="delete"]'); const renameBtn = item.querySelector('[data-action="rename"]'); const exportBtn = item.querySelector('[data-action="export"]'); deleteBtn.addEventListener("click", async e => { e.stopPropagation(); await this.handleDeleteConversation(conversation.id, item); }); renameBtn.addEventListener("click", async e => { e.stopPropagation(); await this.handleRenameConversation(conversation.id); }); exportBtn.addEventListener("click", async e => { e.stopPropagation(); await this.handleExportConversation(conversation.id); }); return item; } // Handle conversation click (switch to conversation) async handleConversationClick(conversationId) { try { await conversationManager.loadConversation(conversationId); // No need to refresh here - loadConversationIntoUI already does it } catch (error) { console.error("Failed to load conversation:", error); showNotification("Failed to load conversation", "error"); } } // Handle delete conversation async handleDeleteConversation(conversationId, itemElement) { if (!confirm("Are you sure you want to delete this conversation? This action cannot be undone.")) return; try { itemElement.classList.add("deleting"); await new Promise(resolve => setTimeout(resolve, 300)); await conversationManager.deleteConversation(conversationId); // If deleted current conversation, startNewConversation handles everything (including refresh) if (conversationId === conversationManager.currentConversationId) { await startNewConversation(); } else { // Just refresh list if deleted another conversation await this.refresh(); } showNotification("Conversation deleted", "success"); } catch (error) { console.error("Failed to delete conversation:", error); showNotification("Failed to delete conversation", "error"); itemElement.classList.remove("deleting"); } } // Handle rename conversation async handleRenameConversation(conversationId) { const conversation = await db.conversations.get(conversationId); if (!conversation) return; // Show rename modal this.showRenameModal(conversationId, conversation.title); } // Show rename modal showRenameModal(conversationId, currentTitle) { this.renameConversationId = conversationId; const modal = document.getElementById("renameModal"); const input = document.getElementById("renameInput"); input.value = currentTitle; modal.classList.remove("hidden"); setTimeout(() => { input.focus(); input.select(); }, 100); } // Close rename modal closeRenameModal() { const modal = document.getElementById("renameModal"); modal.classList.add("hidden"); this.renameConversationId = null; } // Confirm rename async confirmRename() { const input = document.getElementById("renameInput"); const newTitle = input.value.trim(); if (!newTitle || !this.renameConversationId) { this.closeRenameModal(); return; } try { await conversationManager.updateConversationTitle(this.renameConversationId, newTitle); await this.refresh(); showNotification("Conversation renamed", "success"); } catch (error) { console.error("Failed to rename conversation:", error); showNotification("Failed to rename conversation", "error"); } this.closeRenameModal(); } // Initialize rename modal events initRenameModal() { document.getElementById("closeRenameModal").addEventListener("click", () => this.closeRenameModal()); document.getElementById("cancelRename").addEventListener("click", () => this.closeRenameModal()); document.getElementById("confirmRename").addEventListener("click", () => this.confirmRename()); document.getElementById("renameModal").addEventListener("click", e => { if (e.target.id === "renameModal") this.closeRenameModal(); }); document.getElementById("renameInput").addEventListener("keydown", e => { if (e.key === "Enter") { e.preventDefault(); this.confirmRename(); } if (e.key === "Escape") { this.closeRenameModal(); } }); } // ======================================== // All Conversations Modal // ======================================== initAllConversationsModal() { const modal = document.getElementById("allConversationsModal"); const closeBtn = document.getElementById("closeAllConversationsModal"); const searchInput = document.getElementById("conversationSearchInput"); closeBtn.addEventListener("click", () => this.closeAllConversationsModal()); modal.addEventListener("click", e => { if (e.target === modal) this.closeAllConversationsModal(); }); // Search with debounce let searchTimeout; searchInput.addEventListener("input", e => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.filterAllConversations(e.target.value); }, 200); }); // ESC to close document.addEventListener("keydown", e => { if (e.key === "Escape" && !modal.classList.contains("hidden")) { this.closeAllConversationsModal(); } }); } async openAllConversationsModal() { const modal = document.getElementById("allConversationsModal"); const listContainer = document.getElementById("allConversationsList"); const searchInput = document.getElementById("conversationSearchInput"); // Clear search searchInput.value = ""; // Load all conversations this.allConversationsCache = await conversationManager.getAllConversations(); // Render list await this.renderAllConversationsList(this.allConversationsCache); // Show modal modal.classList.remove("hidden"); searchInput.focus(); } closeAllConversationsModal() { const modal = document.getElementById("allConversationsModal"); modal.classList.add("hidden"); } async renderAllConversationsList(conversations) { const listContainer = document.getElementById("allConversationsList"); if (conversations.length === 0) { listContainer.innerHTML = `

No conversations found

`; return; } // Count display let html = `
${conversations.length} conversation${conversations.length > 1 ? "s" : ""}
`; for (const conv of conversations) { const messages = await conversationManager.getMessages(conv.id); const msgCount = messages.length; const timeAgo = this.getTimeAgo(conv.updatedAt); const modelShort = this.getModelShortName(conv.model); const isActive = conv.id === conversationManager.currentConversationId; html += `
${escapeHtml(conv.title)}
🕐 ${timeAgo} 💬 ${msgCount} msgs ${modelShort}
`; } listContainer.innerHTML = html; // Add click handlers listContainer.querySelectorAll(".all-conversations-item").forEach(item => { item.addEventListener("click", async e => { // Check if clicked on action button const actionBtn = e.target.closest("[data-action]"); if (actionBtn) { e.stopPropagation(); const action = actionBtn.dataset.action; const id = parseInt(actionBtn.dataset.id); if (action === "export") { await this.handleExportConversation(id); } else if (action === "delete") { await this.handleDeleteFromModal(id); } return; } // Load conversation const id = parseInt(item.dataset.id); await conversationManager.loadConversation(id); this.closeAllConversationsModal(); await this.refresh(); }); }); } async handleDeleteFromModal(conversationId) { if (!confirm("Delete this conversation?")) return; try { await conversationManager.deleteConversation(conversationId); // Remove from cache this.allConversationsCache = this.allConversationsCache.filter(c => c.id !== conversationId); // Re-render await this.renderAllConversationsList(this.allConversationsCache); await this.refresh(); showNotification("Conversation deleted", "success"); } catch (error) { showNotification("Failed to delete conversation", "error"); } } filterAllConversations(query) { const q = query.toLowerCase().trim(); if (!q) { this.renderAllConversationsList(this.allConversationsCache); return; } const filtered = this.allConversationsCache.filter(conv => conv.title.toLowerCase().includes(q)); this.renderAllConversationsList(filtered); } // Handle export conversation with format choice async handleExportConversation(conversationId) { try { const data = await conversationManager.exportConversation(conversationId); const format = await showExportFormatModal(data.conversation.title); if (!format) return; // User cancelled const filename = sanitizeFilename(data.conversation.title); if (format === "json") { // Export as JSON (full data, re-importable) const jsonContent = JSON.stringify(data, null, 2); downloadFile(jsonContent, `${filename}.json`, "application/json"); showNotification("Exported as JSON", "success"); } else { // Export as Markdown (human-readable) let markdown = `# ${data.conversation.title}\n\n`; markdown += `*Exported: ${new Date().toLocaleString()}*\n`; markdown += `*Model: ${data.conversation.model}*\n\n---\n\n`; for (const msg of data.messages) { const role = msg.role === "user" ? "**You**" : "**Assistant**"; const time = new Date(msg.timestamp).toLocaleString(); markdown += `### ${role} *(${time})*\n\n${msg.content}\n\n`; if (msg.reasoning) { markdown += `
\nReasoning\n\n${msg.reasoning}\n\n
\n\n`; } markdown += "---\n\n"; } downloadFile(markdown, `${filename}.md`, "text/markdown"); showNotification("Exported as Markdown", "success"); } } catch (error) { console.error("Export failed:", error); showNotification("Failed to export conversation", "error"); } } // Show empty state showEmptyState() { this.container.innerHTML = `

No conversations yet

Start a new chat to begin

`; } // Get time ago string getTimeAgo(dateString) { const date = new Date(dateString); const now = new Date(); const seconds = Math.floor((now - date) / 1000); if (seconds < 60) return "Just now"; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`; return date.toLocaleDateString(); } // Get model short name getModelShortName(modelFullName) { if (modelFullName.includes("120b")) return "GPT-120b"; if (modelFullName.includes("20b")) return "GPT-20b"; return "GPT"; } // Refresh the list async refresh() { await this.renderConversationsList(); } } // Create global instance const conversationsUI = new ConversationsUI(); // Export for use in other scripts if (typeof window !== "undefined") { window.conversationsUI = conversationsUI; }