Spaces:
Running
Running
// ===== 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()); | |
} | |
// Memory modal close | |
const memoryClose = document.getElementById("memory-close"); | |
if (memoryClose) { | |
memoryClose.addEventListener("click", () => this.closeMemoryModal()); | |
} | |
// 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()); | |
} | |
// 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(); | |
} | |
}); | |
} | |
} | |
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 = ` | |
<div class="memory-empty"> | |
<i class="fas fa-brain"></i> | |
<p>No memories found. Start chatting to build memories automatically, or add them manually.</p> | |
</div> | |
`; | |
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 += ` | |
<div class="memory-category-group"> | |
<h4 class="memory-category-header"> | |
${this.getCategoryIcon(category)} ${this.formatCategoryName(category)} | |
<span class="memory-category-count">(${categoryMemories.length})</span> | |
</h4> | |
<div class="memory-category-items"> | |
`; | |
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 += ` | |
<div class="memory-item ${isAutomatic ? "memory-auto" : "memory-manual"}" data-memory-id="${memory.id}"> | |
<div class="memory-header"> | |
<div class="memory-badges"> | |
<span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "🤖 Auto" : "✋ Manual"}</span> | |
<span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span> | |
${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""} | |
<span class="memory-importance importance-${importanceLevel}" title="Importance: ${importancePct}% (${importanceLevel})">${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)}</span> | |
</div> | |
</div> | |
<div class="memory-preview"> | |
<div class="memory-preview-text ${isLongContent ? "memory-preview-short" : ""}" id="preview-${memory.id}"> | |
${this.highlightMemoryContent(previewText)} | |
</div> | |
${ | |
isLongContent | |
? ` | |
<div class="memory-preview-full" id="full-${memory.id}" style="display: none;"> | |
${this.highlightMemoryContent(memory.content)} | |
</div> | |
<button class="memory-expand-btn" onclick="kimiMemoryUI.toggleMemoryContent('${memory.id}')"> | |
<i class="fas fa-chevron-down" id="icon-${memory.id}"></i> Voir plus | |
</button> | |
` | |
: "" | |
} | |
</div> | |
${tagsHtml} | |
<div class="memory-meta"> | |
<span class="memory-date">${this.formatDate(memory.timestamp)}</span> | |
${ | |
memory.sourceText | |
? `<span class="memory-source" title="${ | |
window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml | |
? window.KimiValidationUtils.escapeHtml(memory.sourceText) | |
: memory.sourceText | |
}">📝 Extrait de conversation</span>` | |
: `<span>📝 Ajouté manuellement</span>` | |
} | |
</div> | |
<div class="memory-actions"> | |
<button class="memory-edit-btn" onclick="kimiMemoryUI.editMemory('${memory.id}')" title="Modifier cette mémoire"> | |
<i class="fas fa-edit"></i> | |
</button> | |
<button class="memory-delete-btn" onclick="kimiMemoryUI.deleteMemory('${memory.id}')" title="Supprimer cette mémoire"> | |
<i class="fas fa-trash"></i> | |
</button> | |
</div> | |
</div> | |
`; | |
}); | |
html += ` | |
</div> | |
</div> | |
`; | |
}); | |
memoryList.innerHTML = html; | |
} | |
// 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 => `<span class="memory-tag ${classify(tag)}" title="${escape(tag)}">${escape(tag)}</span>`) | |
.join(""); | |
const moreChip = moreCount > 0 ? `<span class="memory-tag tag-more" title="${moreCount} more">+${moreCount}</span>` : ""; | |
return `<div class="memory-tags">${chips}${moreChip}</div>`; | |
} | |
formatCategoryName(category) { | |
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, | |
'<mark style="background: var(--primary-color); color: white; padding: 1px 3px; border-radius: 2px;">$1</mark>' | |
); | |
} | |
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 = '<i class="fas fa-chevron-down"></i> Voir plus'; | |
} else { | |
previewShort.style.display = "none"; | |
previewFull.style.display = "block"; | |
icon.className = "fas fa-chevron-up"; | |
expandBtn.innerHTML = '<i class="fas fa-chevron-up"></i> Voir moins'; | |
} | |
} | |
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 = ` | |
<h3 style="margin: 0 0 20px 0; color: var(--text-primary);"> | |
<i class="fas fa-edit"></i> Edit Memory | |
</h3> | |
<div style="margin-bottom: 16px;"> | |
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Category:</label> | |
<select id="edit-memory-category" class="kimi-select" style="width: 100%;"> | |
<option value="personal" ${memory.category === "personal" ? "selected" : ""}>Personal Info</option> | |
<option value="preferences" ${memory.category === "preferences" ? "selected" : ""}>Likes & Dislikes</option> | |
<option value="relationships" ${memory.category === "relationships" ? "selected" : ""}>Relationships</option> | |
<option value="activities" ${memory.category === "activities" ? "selected" : ""}>Activities & Hobbies</option> | |
<option value="goals" ${memory.category === "goals" ? "selected" : ""}>Goals & Plans</option> | |
<option value="experiences" ${memory.category === "experiences" ? "selected" : ""}>Experiences</option> | |
<option value="important" ${memory.category === "important" ? "selected" : ""}>Important Events</option> | |
</select> | |
</div> | |
<div style="margin-bottom: 20px;"> | |
<label style="display: block; margin-bottom: 8px; font-weight: 500;">Content:</label> | |
<textarea id="edit-memory-content" class="kimi-input" style="width: 100%; height: 100px; resize: vertical;" placeholder="Memory content...">${memory.content}</textarea> | |
</div> | |
<div style="display: flex; gap: 12px; justify-content: flex-end;"> | |
<button id="cancel-edit" class="kimi-button" style="background: #6c757d;"> | |
<i class="fas fa-times"></i> Cancel | |
</button> | |
<button id="save-edit" class="kimi-button"> | |
<i class="fas fa-save"></i> Save | |
</button> | |
</div> | |
`; | |
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 newCategory = dialog.querySelector("#edit-memory-category").value; | |
const newContent = dialog.querySelector("#edit-memory-content").value.trim(); | |
if (!newContent) { | |
this.showFeedback("Le contenu ne peut pas être vide", "error"); | |
return; | |
} | |
console.log(`🔄 Tentative de mise à jour de la mémoire ID: ${memoryId}`); | |
console.log("Nouvelles données:", { category: newCategory, content: newContent }); | |
try { | |
const result = await this.memorySystem.updateMemory(memoryId, { | |
category: newCategory, | |
content: newContent | |
}); | |
console.log("Résultat de l'update:", result); | |
if (result === true) { | |
// Fermer le modal | |
document.body.removeChild(overlay); | |
// Forcer le rechargement complet | |
await this.loadMemories(); | |
await this.updateMemoryStats(); | |
this.showFeedback("Mémoire mise à jour avec succès"); | |
console.log("✅ Interface mise à jour"); | |
} else { | |
this.showFeedback("Erreur: Impossible de mettre à jour la mémoire", "error"); | |
console.error("❌ Update échoué, résultat:", result); | |
} | |
} catch (error) { | |
console.error("Error updating memory:", error); | |
this.showFeedback("Erreur lors de la mise à jour de la mémoire", "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 de l'interface (utile pour debug) | |
async forceRefresh() { | |
console.log("🔄 Force refresh de l'interface mémoire..."); | |
try { | |
if (this.memorySystem) { | |
// Migrer les IDs si nécessaire | |
await this.memorySystem.migrateIncompatibleIDs(); | |
// Recharger les mémoires | |
await this.loadMemories(); | |
await this.updateMemoryStats(); | |
console.log("✅ Refresh forcé terminé"); | |
} | |
} catch (error) { | |
console.error("❌ Erreur lors du refresh forcé:", 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; | |