// 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 = `
`;
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;
}