// ===== KIMI MEMORY UI MANAGER ===== class KimiMemoryUI { constructor() { this.memorySystem = null; this.isInitialized = false; } async init() { if (!window.kimiMemorySystem) { console.warn("Memory system not available"); return; } this.memorySystem = window.kimiMemorySystem; this.setupEventListeners(); await this.updateMemoryStats(); this.isInitialized = true; } setupEventListeners() { // Memory toggle const memoryToggle = document.getElementById("memory-toggle"); if (memoryToggle) { memoryToggle.addEventListener("click", () => this.toggleMemorySystem()); } // View memories button const viewMemoriesBtn = document.getElementById("view-memories"); if (viewMemoriesBtn) { viewMemoriesBtn.addEventListener("click", () => this.openMemoryModal()); } // Add memory button const addMemoryBtn = document.getElementById("add-memory"); if (addMemoryBtn) { addMemoryBtn.addEventListener("click", () => { this.addManualMemory(); ensureVideoNeutralOnUIChange(); }); } // Memory modal close const memoryClose = document.getElementById("memory-close"); if (memoryClose) { memoryClose.addEventListener("click", () => { this.closeMemoryModal(); ensureVideoNeutralOnUIChange(); }); } // Memory export const memoryExport = document.getElementById("memory-export"); if (memoryExport) { memoryExport.addEventListener("click", () => this.exportMemories()); } // Memory filter const memoryFilter = document.getElementById("memory-filter-category"); if (memoryFilter) { memoryFilter.addEventListener("change", () => { this.filterMemories(); ensureVideoNeutralOnUIChange(); }); } // Memory search const memorySearch = document.getElementById("memory-search"); if (memorySearch) { memorySearch.addEventListener("input", () => this.filterMemories()); } // Close modal on overlay click const memoryOverlay = document.getElementById("memory-overlay"); if (memoryOverlay) { memoryOverlay.addEventListener("click", e => { if (e.target === memoryOverlay) { this.closeMemoryModal(); } }); } // Delegated handler for memory-source clicks / touch / keyboard const memoryList = document.getElementById("memory-list"); if (memoryList) { // Click and touch memoryList.addEventListener("click", e => this.handleMemorySourceToggle(e)); memoryList.addEventListener("touchstart", e => this.handleMemorySourceToggle(e)); // Keyboard accessibility: Enter / Space when focused on .memory-source memoryList.addEventListener("keydown", e => { const target = e.target; if (target && target.classList && target.classList.contains("memory-source")) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); this.toggleSourceContentForElement(target); } } }); } } async toggleMemorySystem() { if (!this.memorySystem) return; const toggle = document.getElementById("memory-toggle"); const enabled = !this.memorySystem.memoryEnabled; await this.memorySystem.toggleMemorySystem(enabled); if (toggle) { toggle.setAttribute("aria-checked", enabled.toString()); toggle.classList.toggle("active", enabled); } // Show feedback this.showFeedback(enabled ? "Memory system enabled" : "Memory system disabled"); } async addManualMemory() { const categorySelect = document.getElementById("memory-category"); const contentInput = document.getElementById("memory-content"); if (!categorySelect || !contentInput) return; const category = categorySelect.value; const content = contentInput.value.trim(); if (!content) { this.showFeedback("Please enter memory content", "error"); return; } try { await this.memorySystem.addMemory({ category: category, content: content, type: "manual", confidence: 1.0 }); contentInput.value = ""; await this.updateMemoryStats(); this.showFeedback("Memory added successfully"); } catch (error) { console.error("Error adding memory:", error); this.showFeedback("Error adding memory", "error"); } } async openMemoryModal() { const overlay = document.getElementById("memory-overlay"); if (!overlay) return; overlay.style.display = "flex"; await this.loadMemories(); } closeMemoryModal() { const overlay = document.getElementById("memory-overlay"); if (overlay) { overlay.style.display = "none"; // Ensure background video resumes after closing memory modal const kv = window.kimiVideo; if (kv && kv.activeVideo) { try { const v = kv.activeVideo; if (v.ended) { if (typeof kv.returnToNeutral === "function") kv.returnToNeutral(); } else if (v.paused) { // Use centralized video utility for play window.KimiVideoManager.getVideoElement(v) .play() .catch(() => { if (typeof kv.returnToNeutral === "function") kv.returnToNeutral(); }); } } catch {} } } } async loadMemories() { if (!this.memorySystem) return; try { const memories = await this.memorySystem.getAllMemories(); console.log("Loading memories into UI:", memories.length); this.renderMemories(memories); } catch (error) { console.error("Error loading memories:", error); } } async filterMemories() { const filterSelect = document.getElementById("memory-filter-category"); const searchInput = document.getElementById("memory-search"); if (!this.memorySystem) return; try { const category = filterSelect?.value; const searchTerm = searchInput?.value.toLowerCase().trim(); let memories; if (category) { memories = await this.memorySystem.getMemoriesByCategory(category); } else { memories = await this.memorySystem.getAllMemories(); } // Apply search filter if search term exists if (searchTerm) { memories = memories.filter( memory => memory.content.toLowerCase().includes(searchTerm) || memory.category.toLowerCase().includes(searchTerm) || (memory.sourceText && memory.sourceText.toLowerCase().includes(searchTerm)) ); } this.renderMemories(memories); } catch (error) { console.error("Error filtering memories:", error); } } renderMemories(memories) { const memoryList = document.getElementById("memory-list"); if (!memoryList) return; console.log("Rendering memories:", memories); // Debug logging if (memories.length === 0) { memoryList.innerHTML = `

No memories found. Start chatting to build memories automatically, or add them manually.

`; return; } // Group memories by category for better organization const groupedMemories = memories.reduce((groups, memory) => { const category = memory.category || "other"; if (!groups[category]) groups[category] = []; groups[category].push(memory); return groups; }, {}); let html = ""; Object.entries(groupedMemories).forEach(([category, categoryMemories]) => { html += `

${this.getCategoryIcon(category)} ${this.formatCategoryName(category)} (${categoryMemories.length})

`; categoryMemories.forEach(memory => { const confidence = Math.round(memory.confidence * 100); const isAutomatic = memory.type === "auto_extracted"; const previewLength = 120; const isLongContent = memory.content.length > previewLength; const previewText = isLongContent ? memory.content.substring(0, previewLength) + "..." : memory.content; const wordCount = memory.content.split(/\s+/).length; const importance = typeof memory.importance === "number" ? memory.importance : 0.5; const importanceLevel = this.getImportanceLevelFromValue(importance); const importancePct = Math.round(importance * 100); const tagsHtml = this.renderTags(memory.tags || []); html += `
${window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(memory.title || "") : memory.title || ""}${!memory.title ? "" : ""}
${memory.type === "auto_extracted" ? "๐Ÿค– Auto" : "โœ‹ Manual"} ${confidence}% ${isLongContent ? `${wordCount} mots` : ""} ${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)}
${ !memory.title ? `
${this.highlightMemoryContent(previewText)}
${ isLongContent ? ` ` : "" }
` : "" } ${tagsHtml}
${this.formatDate(memory.timestamp)} ${ memory.sourceText ? `` : `` }
${ memory.sourceText ? `` : "" }
`; }); html += `
`; }); memoryList.innerHTML = html; // Apply translations to dynamic content if (window.applyTranslations && typeof window.applyTranslations === "function") { window.applyTranslations(); } } // Map importance value [0..1] to level string getImportanceLevelFromValue(value) { if (value >= 0.8) return "high"; if (value >= 0.6) return "medium"; return "low"; } // Render tags as compact chips; show up to 4 then "+N" renderTags(tags) { if (!Array.isArray(tags) || tags.length === 0) return ""; const maxVisible = 4; const visible = tags.slice(0, maxVisible); const moreCount = tags.length - visible.length; const escape = txt => window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(String(txt)) : String(txt); const classify = tag => { if (tag.startsWith("relationship:")) return "tag-relationship"; if (tag.startsWith("boundary:")) return "tag-boundary"; if (tag.startsWith("time:")) return "tag-time"; if (tag.startsWith("type:")) return "tag-type"; return "tag-generic"; }; const chips = visible .map(tag => `${escape(tag)}`) .join(""); const moreChip = moreCount > 0 ? `+${moreCount}` : ""; return `
${chips}${moreChip}
`; } formatCategoryName(category) { // Try to resolve via i18n keys first, fallback to hardcoded English const i18nKey = `memory_category_${category}`; if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") { const val = window.kimiI18nManager.t(i18nKey); if (val && val !== i18nKey) return val; } const names = { personal: "Personal Information", preferences: "Likes & Dislikes", relationships: "Relationships & People", activities: "Activities & Hobbies", goals: "Goals & Aspirations", experiences: "Shared Experiences", important: "Important Events" }; return names[category] || category.charAt(0).toUpperCase() + category.slice(1); } getConfidenceLevel(confidence) { if (confidence >= 80) return "high"; if (confidence >= 60) return "medium"; return "low"; } formatDate(timestamp) { const date = new Date(timestamp); const now = new Date(); const diffTime = now - date; const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); if (diffDays === 0) return "Today"; if (diffDays === 1) return "Yesterday"; if (diffDays < 7) return `${diffDays} days ago`; return date.toLocaleDateString(); } highlightMemoryContent(content) { // Escape HTML first using centralized util const escapedContent = window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(content) : content; // Simple highlighting for search terms if there's a search active const searchInput = document.getElementById("memory-search"); if (searchInput && searchInput.value.trim()) { const searchTerm = searchInput.value.trim(); const regex = new RegExp(`(${searchTerm})`, "gi"); return escapedContent.replace( regex, '$1' ); } return escapedContent; } // Removed duplicate escapeHtml; use window.KimiValidationUtils.escapeHtml instead getCategoryIcon(category) { const icons = { personal: "๐Ÿ‘ค", preferences: "โค๏ธ", relationships: "๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ", activities: "๐ŸŽฏ", goals: "๐ŸŽฏ", experiences: "โญ", important: "๐Ÿ“Œ" }; return icons[category] || "๐Ÿ“"; } toggleMemoryContent(memoryId) { const previewShort = document.getElementById(`preview-${memoryId}`); const previewFull = document.getElementById(`full-${memoryId}`); const icon = document.getElementById(`icon-${memoryId}`); const expandBtn = icon?.closest(".memory-expand-btn"); if (!previewShort || !previewFull || !icon || !expandBtn) return; const isExpanded = previewFull.style.display !== "none"; if (isExpanded) { previewShort.style.display = "block"; previewFull.style.display = "none"; icon.className = "fas fa-chevron-down"; expandBtn.innerHTML = ' '; } else { previewShort.style.display = "none"; previewFull.style.display = "block"; icon.className = "fas fa-chevron-up"; expandBtn.innerHTML = ' '; } // Re-apply translations for the new button label if (window.applyTranslations && typeof window.applyTranslations === "function") { window.applyTranslations(); } } // Handle delegated click/touch events for .memory-source handleMemorySourceToggle(e) { try { const el = e.target.closest && e.target.closest(".memory-source"); if (!el) return; // Prevent triggering parent handlers e.stopPropagation(); e.preventDefault(); this.toggleSourceContentForElement(el); } catch (err) { console.error("Error handling memory-source toggle", err); } } // Toggle the adjacent .memory-source-content for a given .memory-source element toggleSourceContentForElement(sourceEl) { if (!sourceEl) return; // The memory id is present on the nearest .memory-item const item = sourceEl.closest(".memory-item"); if (!item) return; const id = item.getAttribute("data-memory-id"); if (!id) return; const contentEl = document.getElementById(`source-content-${id}`); if (!contentEl) return; const isVisible = contentEl.style.display !== "none" && contentEl.style.display !== ""; // Close any other open source contents document.querySelectorAll(".memory-source-content").forEach(el => { if (el !== contentEl) el.style.display = "none"; }); if (isVisible) { contentEl.style.display = "none"; sourceEl.setAttribute("aria-expanded", "false"); } else { // simple sliding animation via max-height for smoother appearance contentEl.style.display = "block"; sourceEl.setAttribute("aria-expanded", "true"); } } async editMemory(memoryId) { if (!this.memorySystem) return; try { // Get the memory to edit const memories = await this.memorySystem.getAllMemories(); const memory = memories.find(m => m.id == memoryId); if (!memory) { this.showFeedback("Memory not found", "error"); return; } // Create edit dialog const overlay = document.createElement("div"); overlay.className = "memory-edit-overlay"; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.8); display: flex; justify-content: center; align-items: center; z-index: 10001; `; const dialog = document.createElement("div"); dialog.className = "memory-edit-dialog"; dialog.style.cssText = ` background: var(--background-secondary); border-radius: 12px; padding: 24px; width: 90%; max-width: 500px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); `; dialog.innerHTML = `

`; overlay.appendChild(dialog); document.body.appendChild(overlay); // Handle buttons dialog.querySelector("#cancel-edit").addEventListener("click", () => { document.body.removeChild(overlay); }); dialog.querySelector("#save-edit").addEventListener("click", async () => { const newTitle = dialog.querySelector("#edit-memory-title").value.trim(); const newCategory = dialog.querySelector("#edit-memory-category").value; const newContent = dialog.querySelector("#edit-memory-content").value.trim(); if (!newContent) { this.showFeedback( window.kimiI18nManager ? window.kimiI18nManager.t("validation_empty_message") : "Content cannot be empty", "error" ); return; } console.log(`๐Ÿ”„ Attempting to update memory ID: ${memoryId}`); console.log("New data:", { category: newCategory, content: newContent }); try { // Also update sourceText so the "๐Ÿ“ Memory of the conversation" shows edited content const result = await this.memorySystem.updateMemory(memoryId, { title: newTitle, category: newCategory, content: newContent, sourceText: newContent }); console.log("Update result:", result); if (result === true) { // Close the modal document.body.removeChild(overlay); // Force full reload await this.loadMemories(); await this.updateMemoryStats(); this.showFeedback(window.kimiI18nManager ? window.kimiI18nManager.t("saved") : "Saved"); console.log("โœ… UI updated"); } else { this.showFeedback( window.kimiI18nManager ? window.kimiI18nManager.t("fallback_general_error") : "Error: unable to update memory", "error" ); console.error("โŒ Update failed, result:", result); } } catch (error) { console.error("Error updating memory:", error); this.showFeedback( window.kimiI18nManager ? window.kimiI18nManager.t("fallback_general_error") : "Error updating memory", "error" ); } }); // Close on overlay click overlay.addEventListener("click", e => { if (e.target === overlay) { document.body.removeChild(overlay); } }); } catch (error) { console.error("Error editing memory:", error); this.showFeedback("Error loading memory for editing", "error"); } } async deleteMemory(memoryId) { if (!confirm("Are you sure you want to delete this memory?")) return; try { await this.memorySystem.deleteMemory(memoryId); await this.loadMemories(); await this.updateMemoryStats(); this.showFeedback("Memory deleted"); } catch (error) { console.error("Error deleting memory:", error); this.showFeedback("Error deleting memory", "error"); } } async exportMemories() { if (!this.memorySystem) return; try { const exportData = await this.memorySystem.exportMemories(); if (exportData) { const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `kimi-memories-${new Date().toISOString().split("T")[0]}.json`; a.click(); URL.revokeObjectURL(url); this.showFeedback("Memories exported successfully"); } } catch (error) { console.error("Error exporting memories:", error); this.showFeedback("Error exporting memories", "error"); } } async updateMemoryStats() { if (!this.memorySystem) return; try { const stats = await this.memorySystem.getMemoryStats(); const memoryCount = document.getElementById("memory-count"); const memoryToggle = document.getElementById("memory-toggle"); if (memoryCount) { memoryCount.textContent = `${stats.total} memories`; } // Update toggle state if (memoryToggle) { const enabled = this.memorySystem.memoryEnabled; memoryToggle.setAttribute("aria-checked", enabled.toString()); memoryToggle.classList.toggle("active", enabled); // Add visual indicator for memory status const indicator = memoryToggle.querySelector(".memory-indicator") || document.createElement("div"); if (!memoryToggle.querySelector(".memory-indicator")) { indicator.className = "memory-indicator"; memoryToggle.appendChild(indicator); } indicator.style.cssText = ` position: absolute; top: -2px; right: -2px; width: 8px; height: 8px; border-radius: 50%; background: ${enabled ? "#27ae60" : "#e74c3c"}; border: 2px solid white; `; } } catch (error) { console.error("Error updating memory stats:", error); } } // Force refresh of the memory UI (useful for debugging) async forceRefresh() { console.log("๐Ÿ”„ Force refresh of memory UI..."); try { if (this.memorySystem) { // Migrate IDs if necessary await this.memorySystem.migrateIncompatibleIDs(); // Reload memories await this.loadMemories(); await this.updateMemoryStats(); console.log("โœ… Forced refresh completed"); } } catch (error) { console.error("โŒ Error during forced refresh:", error); } } showFeedback(message, type = "success") { // Create feedback element const feedback = document.createElement("div"); feedback.className = `memory-feedback memory-feedback-${type}`; feedback.textContent = message; // Style the feedback based on type let backgroundColor; switch (type) { case "error": backgroundColor = "#e74c3c"; break; case "info": backgroundColor = "#3498db"; break; default: backgroundColor = "#27ae60"; } // Style the feedback Object.assign(feedback.style, { position: "fixed", top: "20px", right: "20px", padding: "12px 20px", borderRadius: "6px", color: "white", backgroundColor: backgroundColor, boxShadow: "0 4px 12px rgba(0,0,0,0.2)", zIndex: "10000", fontSize: "14px", fontWeight: "500", opacity: "0", transform: "translateX(100%)", transition: "all 0.3s ease" }); document.body.appendChild(feedback); // Animate in setTimeout(() => { feedback.style.opacity = "1"; feedback.style.transform = "translateX(0)"; }, 10); // Remove after delay (longer for info messages, shorter for others) const delay = type === "info" ? 2000 : 3000; setTimeout(() => { feedback.style.opacity = "0"; feedback.style.transform = "translateX(100%)"; setTimeout(() => { if (feedback.parentNode) { feedback.parentNode.removeChild(feedback); } }, 300); }, delay); } } // Initialize memory UI when DOM is ready document.addEventListener("DOMContentLoaded", async () => { window.kimiMemoryUI = new KimiMemoryUI(); // Wait for memory system to be ready const waitForMemorySystem = () => { if (window.kimiMemorySystem) { window.kimiMemoryUI.init(); } else { setTimeout(waitForMemorySystem, 100); } }; setTimeout(waitForMemorySystem, 1000); // Give time for initialization }); window.KimiMemoryUI = KimiMemoryUI; function ensureVideoNeutralOnUIChange() { if (window.kimiVideo && window.kimiVideo.getCurrentVideoInfo) { const info = window.kimiVideo.getCurrentVideoInfo(); if (info && info.ended) window.kimiVideo.returnToNeutral(); } }