Spaces:
Running
Running
| // KIMI MODULE SYSTEM | |
| // KimiDataManager has been extracted to kimi-data-manager.js | |
| // (kept global via window.KimiDataManager) | |
| // Fonctions utilitaires et logique (référencent window.*) | |
| function updateFavorabilityLabel(characterKey) { | |
| const favorabilityLabel = document.getElementById("favorability-label"); | |
| if (favorabilityLabel && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[characterKey]) { | |
| // New semantics: show overall personality average (independent display) | |
| favorabilityLabel.removeAttribute("for"); // decouple from any specific slider | |
| favorabilityLabel.setAttribute("data-i18n", "personality_average_of"); | |
| favorabilityLabel.setAttribute("data-i18n-params", JSON.stringify({ name: window.KIMI_CHARACTERS[characterKey].name })); | |
| favorabilityLabel.textContent = `💖 Personality average of ${window.KIMI_CHARACTERS[characterKey].name}`; | |
| if (!favorabilityLabel.getAttribute("title")) { | |
| favorabilityLabel.setAttribute( | |
| "title", | |
| "Average of (Affection + Playfulness + Intelligence + Empathy + Humor + Romance) / 6" | |
| ); | |
| } | |
| applyTranslations(); | |
| } | |
| } | |
| // Simplified personality average computation using centralized system | |
| function computePersonalityAverage(traits) { | |
| return window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50; | |
| } | |
| // Update UI elements (bar + percentage text + label) based on overall personality average | |
| async function updateGlobalPersonalityUI(characterKey = null) { | |
| try { | |
| const db = window.kimiDB; | |
| if (!db) return; | |
| const character = characterKey || (await db.getSelectedCharacter()); | |
| const traits = await db.getAllPersonalityTraits(character); | |
| const avg = computePersonalityAverage(traits); | |
| // Reuse existing favorability bar elements for global average | |
| const bar = document.getElementById("favorability-bar"); | |
| const text = document.getElementById("favorability-text"); | |
| if (bar) bar.style.width = `${avg}%`; | |
| if (text) text.textContent = `${avg.toFixed(2)}%`; | |
| // Update label content if character provided | |
| updateFavorabilityLabel(character); | |
| } catch (e) { | |
| console.warn("Failed to update global personality UI", e); | |
| } | |
| } | |
| window.updateGlobalPersonalityUI = updateGlobalPersonalityUI; | |
| async function loadCharacterSection() { | |
| const kimiDB = window.kimiDB; | |
| if (!kimiDB) return; | |
| const characterGrid = document.getElementById("character-grid"); | |
| if (!characterGrid) return; | |
| while (characterGrid.firstChild) { | |
| characterGrid.removeChild(characterGrid.firstChild); | |
| } | |
| const selectedCharacter = await kimiDB.getSelectedCharacter(); | |
| for (const [key, info] of Object.entries(window.KIMI_CHARACTERS)) { | |
| const card = document.createElement("div"); | |
| card.className = `character-card${key === selectedCharacter ? " selected" : ""}`; | |
| card.dataset.character = key; | |
| // Create character card elements safely | |
| const img = document.createElement("img"); | |
| img.src = info.image; | |
| img.alt = info.name; | |
| const infoDiv = document.createElement("div"); | |
| infoDiv.className = "character-info"; | |
| const nameDiv = document.createElement("div"); | |
| nameDiv.className = "character-name"; | |
| nameDiv.textContent = info.name; | |
| const detailsDiv = document.createElement("div"); | |
| detailsDiv.className = "character-details"; | |
| const ageDiv = document.createElement("div"); | |
| ageDiv.className = "character-age"; | |
| ageDiv.setAttribute("data-i18n", "character_age"); | |
| ageDiv.setAttribute("data-i18n-params", JSON.stringify({ age: info.age })); | |
| const birthplaceDiv = document.createElement("div"); | |
| birthplaceDiv.className = "character-birthplace"; | |
| birthplaceDiv.setAttribute("data-i18n", "character_birthplace"); | |
| birthplaceDiv.setAttribute("data-i18n-params", JSON.stringify({ birthplace: info.birthplace })); | |
| const summaryDiv = document.createElement("div"); | |
| summaryDiv.className = "character-summary"; | |
| summaryDiv.setAttribute("data-i18n", `character_summary_${key}`); | |
| detailsDiv.appendChild(ageDiv); | |
| detailsDiv.appendChild(birthplaceDiv); | |
| detailsDiv.appendChild(summaryDiv); | |
| infoDiv.appendChild(nameDiv); | |
| infoDiv.appendChild(detailsDiv); | |
| const promptLabel = document.createElement("div"); | |
| promptLabel.className = "character-prompt-label"; | |
| promptLabel.setAttribute("data-i18n", "system_prompt"); | |
| promptLabel.textContent = "System Prompt"; | |
| const promptInput = document.createElement("textarea"); | |
| promptInput.className = "character-prompt-input"; | |
| promptInput.id = `prompt-${key}`; | |
| promptInput.rows = 6; | |
| // Create buttons container | |
| const buttonsContainer = document.createElement("div"); | |
| buttonsContainer.className = "character-prompt-buttons"; | |
| // Save button | |
| const saveButton = document.createElement("button"); | |
| saveButton.className = "kimi-button character-save-btn"; | |
| saveButton.id = `save-${key}`; | |
| saveButton.setAttribute("data-i18n", "save"); | |
| saveButton.textContent = "Save"; | |
| // Reset button | |
| const resetButton = document.createElement("button"); | |
| resetButton.className = "kimi-button character-reset-btn"; | |
| resetButton.id = `reset-${key}`; | |
| resetButton.setAttribute("data-i18n", "reset_to_default"); | |
| resetButton.textContent = "Reset to Default"; | |
| buttonsContainer.appendChild(saveButton); | |
| buttonsContainer.appendChild(resetButton); | |
| card.appendChild(img); | |
| card.appendChild(infoDiv); | |
| card.appendChild(promptLabel); | |
| card.appendChild(promptInput); | |
| card.appendChild(buttonsContainer); | |
| characterGrid.appendChild(card); | |
| } | |
| applyTranslations(); | |
| // Initialize prompt values and button event listeners | |
| for (const key of Object.keys(window.KIMI_CHARACTERS)) { | |
| const promptInput = document.getElementById(`prompt-${key}`); | |
| const saveButton = document.getElementById(`save-${key}`); | |
| const resetButton = document.getElementById(`reset-${key}`); | |
| if (promptInput) { | |
| const prompt = await kimiDB.getSystemPromptForCharacter(key); | |
| promptInput.value = prompt; | |
| promptInput.disabled = key !== selectedCharacter; | |
| } | |
| // Save button event listener | |
| if (saveButton) { | |
| saveButton.addEventListener("click", async () => { | |
| if (promptInput) { | |
| await kimiDB.setSystemPromptForCharacter(key, promptInput.value); | |
| // Visual feedback | |
| const originalText = saveButton.textContent; | |
| saveButton.textContent = "Saved!"; | |
| saveButton.classList.add("success"); | |
| saveButton.disabled = true; | |
| setTimeout(() => { | |
| saveButton.setAttribute("data-i18n", "save"); | |
| applyTranslations(); | |
| saveButton.classList.remove("success"); | |
| saveButton.disabled = false; | |
| }, 1500); | |
| // Refresh personality if this is the selected character | |
| if (key === selectedCharacter && window.kimiLLM && window.kimiLLM.refreshMemoryContext) { | |
| await window.kimiLLM.refreshMemoryContext(); | |
| } | |
| } | |
| }); | |
| } | |
| // Reset button event listener | |
| if (resetButton) { | |
| resetButton.addEventListener("click", async () => { | |
| const defaultPrompt = window.KIMI_CHARACTERS[key]?.defaultPrompt || ""; | |
| if (promptInput) { | |
| promptInput.value = defaultPrompt; | |
| await kimiDB.setSystemPromptForCharacter(key, defaultPrompt); | |
| // Visual feedback | |
| const originalText = resetButton.textContent; | |
| resetButton.textContent = "Reset!"; | |
| resetButton.classList.add("animated"); | |
| resetButton.setAttribute("data-i18n", "reset_done"); | |
| applyTranslations(); | |
| setTimeout(() => { | |
| resetButton.setAttribute("data-i18n", "reset_to_default"); | |
| applyTranslations(); | |
| resetButton.classList.remove("animated"); | |
| }, 1500); | |
| // Refresh personality if this is the selected character | |
| if (key === selectedCharacter && window.kimiLLM && window.kimiLLM.refreshMemoryContext) { | |
| await window.kimiLLM.refreshMemoryContext(); | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| characterGrid.querySelectorAll(".character-card").forEach(card => { | |
| card.addEventListener("click", async () => { | |
| characterGrid.querySelectorAll(".character-card").forEach(c => c.classList.remove("selected")); | |
| card.classList.add("selected"); | |
| const charKey = card.dataset.character; | |
| for (const key of Object.keys(window.KIMI_CHARACTERS)) { | |
| const promptInput = document.getElementById(`prompt-${key}`); | |
| const saveButton = document.getElementById(`save-${key}`); | |
| const resetButton = document.getElementById(`reset-${key}`); | |
| if (promptInput) promptInput.disabled = key !== charKey; | |
| if (saveButton) saveButton.disabled = key !== charKey; | |
| if (resetButton) resetButton.disabled = key !== charKey; | |
| } | |
| updateFavorabilityLabel(charKey); | |
| const chatHeaderName = document.querySelector(".chat-header span[data-i18n]"); | |
| if (chatHeaderName) { | |
| const info = window.KIMI_CHARACTERS[charKey] || window.KIMI_CHARACTERS.kimi; | |
| chatHeaderName.setAttribute("data-i18n", `chat_with_${charKey}`); | |
| applyTranslations(); | |
| } | |
| // Update personality trait sliders with selected character's traits | |
| await updatePersonalitySliders(charKey); | |
| }); | |
| }); | |
| // Initialize personality sliders with current selected character's traits | |
| await updatePersonalitySliders(selectedCharacter); | |
| } | |
| async function getBasicResponse(reaction) { | |
| // Use centralized fallback manager instead of duplicated logic | |
| if (window.KimiFallbackManager) { | |
| return await window.KimiFallbackManager.getEmotionalResponse(reaction); | |
| } | |
| // Fallback to legacy system if KimiFallbackManager not available | |
| const i18n = window.kimiI18nManager; | |
| return i18n ? i18n.t("fallback_technical_error") : "Sorry, I'm having technical difficulties! 💕"; | |
| } | |
| // Déporté vers KimiEmotionSystem: utiliser window.updatePersonalityTraitsFromEmotion | |
| async function analyzeAndReact(text, useAdvancedLLM = true, onStreamToken = null) { | |
| const kimiDB = window.kimiDB; | |
| const kimiLLM = window.kimiLLM; | |
| const kimiVideo = window.kimiVideo; | |
| const kimiMemory = window.kimiMemory; | |
| const isSystemReady = window.isSystemReady; | |
| try { | |
| // Validate and sanitize input | |
| if (!text || typeof text !== "string") { | |
| throw new Error("Invalid input text"); | |
| } | |
| const sanitizedText = window.KimiSecurityUtils?.sanitizeInput(text) || text.trim(); | |
| if (!sanitizedText) { | |
| throw new Error("Empty input after sanitization"); | |
| } | |
| const lowerText = sanitizedText.toLowerCase(); | |
| let reaction = window.kimiAnalyzeEmotion(sanitizedText, "auto"); | |
| let emotionIntensity = 0; | |
| let response; | |
| const selectedCharacter = await kimiDB.getSelectedCharacter(); | |
| const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| const avg = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50; | |
| const affection = typeof traits.affection === "number" ? traits.affection : 55; | |
| const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || ""; | |
| // Only trigger listening videos for voice input, NOT for text chat | |
| // Text chat should keep neutral videos until LLM response processing begins | |
| if (typeof window.updatePersonalityTraitsFromEmotion === "function") { | |
| await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText); | |
| } | |
| if (useAdvancedLLM && isSystemReady && kimiLLM) { | |
| try { | |
| const providerPref = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter"; | |
| const apiKey = | |
| kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref) : null; | |
| if (apiKey && apiKey.trim() !== "") { | |
| try { | |
| if (window.dispatchEvent) { | |
| window.dispatchEvent(new CustomEvent("chat:typing:start")); | |
| } | |
| } catch (e) {} | |
| // Use streaming if onStreamToken callback is provided | |
| if (onStreamToken && typeof kimiLLM.chatStreaming === "function") { | |
| response = await kimiLLM.chatStreaming(sanitizedText, onStreamToken); | |
| } else { | |
| response = await kimiLLM.chat(sanitizedText); | |
| } | |
| try { | |
| if (window.dispatchEvent) { | |
| window.dispatchEvent(new CustomEvent("chat:typing:stop")); | |
| } | |
| } catch (e) {} | |
| const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| // If user explicitly requested dancing, show dancing during Kimi's response | |
| const lang = await kimiDB.getPreference("selectedLanguage", "en"); | |
| const keywords = | |
| (window.KIMI_CONTEXT_KEYWORDS && | |
| (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || | |
| {}; | |
| const dancingWords = keywords.dancing || ["dance", "dancing"]; | |
| const userAskedDance = dancingWords.some(w => sanitizedText.toLowerCase().includes(w.toLowerCase())); | |
| if (userAskedDance) { | |
| kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection); | |
| } else { | |
| // Use emotion analysis from the LLM RESPONSE, not user input | |
| const responseEmotion = window.kimiEmotionSystem?.analyzeEmotionValidated(response) || "positive"; | |
| kimiVideo.respondWithEmotion(responseEmotion, updatedTraits, updatedTraits.affection); | |
| } | |
| if (kimiLLM.updatePersonalityFromResponse) { | |
| await kimiLLM.updatePersonalityFromResponse(sanitizedText, response); | |
| const selectedCharacter2 = await kimiDB.getSelectedCharacter(); | |
| const traits2 = await kimiDB.getAllPersonalityTraits(selectedCharacter2); | |
| if (kimiVideo && kimiVideo.setMoodByPersonality) { | |
| kimiVideo.setMoodByPersonality(traits2); | |
| } | |
| } | |
| } else { | |
| // No API key configured - use centralized fallback | |
| response = window.KimiFallbackManager | |
| ? window.KimiFallbackManager.getFallbackMessage("api_missing") | |
| : "To chat with me, add your API key in settings! 💕"; | |
| const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection); | |
| } | |
| } catch (error) { | |
| console.warn("LLM not available:", error.message); | |
| try { | |
| if (window.dispatchEvent) { | |
| window.dispatchEvent(new CustomEvent("chat:typing:stop")); | |
| } | |
| } catch (e) {} | |
| // Still show API key message if no key is configured | |
| const providerPref2 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter"; | |
| const apiKey = | |
| kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref2) : null; | |
| if (!apiKey || apiKey.trim() === "") { | |
| response = window.KimiFallbackManager | |
| ? window.KimiFallbackManager.getFallbackMessage("api_missing") | |
| : "To chat with me, add your API key in settings! 💕"; | |
| } else { | |
| response = await getBasicResponse(reaction); | |
| } | |
| const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| const lang = await kimiDB.getPreference("selectedLanguage", "en"); | |
| const keywords = | |
| (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || | |
| {}; | |
| const dancingWords = keywords.dancing || ["dance", "dancing"]; | |
| const userAskedDance = dancingWords.some(w => sanitizedText.toLowerCase().includes(w.toLowerCase())); | |
| if (userAskedDance) { | |
| kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection); | |
| } else { | |
| kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection); | |
| } | |
| } | |
| } else { | |
| // System not ready - check if it's because of missing API key | |
| const providerPref3 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter"; | |
| const apiKey = | |
| kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref3) : null; | |
| if (!apiKey || apiKey.trim() === "") { | |
| response = window.KimiFallbackManager | |
| ? window.KimiFallbackManager.getFallbackMessage("api_missing") | |
| : "To chat with me, add your API key in settings! 💕"; | |
| } else { | |
| response = await getBasicResponse(reaction); | |
| } | |
| const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| const lang = await kimiDB.getPreference("selectedLanguage", "en"); | |
| const keywords = | |
| (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {}; | |
| const dancingWords = keywords.dancing || ["dance", "dancing"]; | |
| const userAskedDance = dancingWords.some(w => sanitizedText.toLowerCase().includes(w.toLowerCase())); | |
| if (userAskedDance) { | |
| kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection); | |
| } else { | |
| kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection); | |
| } | |
| } | |
| // Use token usage collected by LLM manager if available | |
| let tokenInfo = null; | |
| if (window._lastKimiTokenUsage) { | |
| tokenInfo = window._lastKimiTokenUsage; | |
| window._lastKimiTokenUsage = null; // consume once | |
| } else if (window.KimiTokenUtils) { | |
| // Fallback approximate (no system prompt included) | |
| try { | |
| const est = window.KimiTokenUtils.estimate; | |
| tokenInfo = { tokensIn: est(sanitizedText), tokensOut: est(response) }; | |
| } catch {} | |
| } | |
| await kimiMemory.saveConversation(sanitizedText, response, tokenInfo); | |
| if (typeof updateStats === "function") { | |
| updateStats(); | |
| } | |
| // Extract memories automatically from conversation if system is enabled | |
| if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) { | |
| try { | |
| const extractedMemories = await window.kimiMemorySystem.extractMemoryFromText(sanitizedText, response); | |
| if (extractedMemories && extractedMemories.length > 0) { | |
| // Update memory stats in UI | |
| if (window.kimiMemoryUI && window.kimiMemoryUI.isInitialized) { | |
| await window.kimiMemoryUI.updateMemoryStats(); | |
| // Show subtle notification for extracted memories | |
| window.kimiMemoryUI.showFeedback( | |
| `💭 ${extractedMemories.length} new ${extractedMemories.length === 1 ? "memory" : "memories"} learned`, | |
| "info" | |
| ); | |
| } | |
| } | |
| } catch (error) { | |
| console.warn("Memory extraction error:", error); | |
| } | |
| } | |
| return response; | |
| } catch (error) { | |
| console.error("Error in analyzeAndReact:", error); | |
| // Use centralized fallback response | |
| const fallbackResponse = window.KimiFallbackManager | |
| ? window.KimiFallbackManager.getFallbackMessage("technical_error") | |
| : "I'm sorry, I encountered an issue processing your message. Please try again."; | |
| try { | |
| // Attempt to save the error for debugging while still providing user feedback | |
| if (kimiMemory && kimiMemory.saveConversation) { | |
| await kimiMemory.saveConversation(text || "Error", fallbackResponse); | |
| } | |
| } catch (saveError) { | |
| console.error("Failed to save error conversation:", saveError); | |
| } | |
| return fallbackResponse; | |
| } | |
| } | |
| function addMessageToChat(sender, text, conversationId = null) { | |
| const chatMessages = document.getElementById("chat-messages"); | |
| // Allow empty text for streaming (we'll update it progressively) | |
| if (text === undefined || text === null) return; | |
| const messageDiv = document.createElement("div"); | |
| messageDiv.className = `message ${sender}`; | |
| const time = new Date().toLocaleTimeString("en-US", { | |
| hour: "2-digit", | |
| minute: "2-digit" | |
| }); | |
| const messageTimeDiv = document.createElement("div"); | |
| messageTimeDiv.className = "message-time"; | |
| messageTimeDiv.style.display = "flex"; | |
| messageTimeDiv.style.justifyContent = "space-between"; | |
| messageTimeDiv.style.alignItems = "center"; | |
| const timeSpan = document.createElement("span"); | |
| timeSpan.textContent = time; | |
| timeSpan.style.flex = "1"; | |
| const deleteBtn = document.createElement("button"); | |
| deleteBtn.className = "delete-message-btn"; | |
| const icon = document.createElement("i"); | |
| icon.className = "fas fa-trash"; | |
| deleteBtn.appendChild(icon); | |
| deleteBtn.style.background = "none"; | |
| deleteBtn.style.border = "none"; | |
| deleteBtn.style.cursor = "pointer"; | |
| deleteBtn.style.color = "#aaa"; | |
| deleteBtn.style.fontSize = "1em"; | |
| deleteBtn.style.marginLeft = "8px"; | |
| deleteBtn.setAttribute("aria-label", "Delete message"); | |
| deleteBtn.addEventListener("click", async function (e) { | |
| e.stopPropagation(); | |
| messageDiv.remove(); | |
| if (conversationId && window.kimiDB && window.kimiDB.deleteSingleMessage) { | |
| await window.kimiDB.deleteSingleMessage(conversationId, sender); | |
| } | |
| }); | |
| messageTimeDiv.appendChild(timeSpan); | |
| messageTimeDiv.appendChild(deleteBtn); | |
| const textDiv = document.createElement("div"); | |
| // Use formatted text with HTML support (secure formatting) | |
| if (text && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) { | |
| textDiv.innerHTML = window.KimiValidationUtils.formatChatText(text); | |
| } else { | |
| textDiv.textContent = text || ""; // Fallback to plain text | |
| } | |
| messageDiv.appendChild(textDiv); | |
| messageDiv.appendChild(messageTimeDiv); | |
| chatMessages.appendChild(messageDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| // Return an object that allows updating the message content for streaming | |
| return { | |
| updateText: newText => { | |
| // Use formatted text for streaming updates too | |
| if (newText && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) { | |
| textDiv.innerHTML = window.KimiValidationUtils.formatChatText(newText); | |
| } else { | |
| textDiv.textContent = newText; | |
| } | |
| // Throttle scrolling to prevent visual stuttering during streaming | |
| if (!textDiv._scrollTimeout) { | |
| textDiv._scrollTimeout = setTimeout(() => { | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| textDiv._scrollTimeout = null; | |
| }, 50); // Throttle to 20 FPS max | |
| } | |
| }, | |
| element: messageDiv, | |
| textElement: textDiv | |
| }; | |
| } | |
| async function loadChatHistory() { | |
| const kimiDB = window.kimiDB; | |
| const kimiMemory = window.kimiMemory; | |
| const chatMessages = document.getElementById("chat-messages"); | |
| while (chatMessages.firstChild) { | |
| chatMessages.removeChild(chatMessages.firstChild); | |
| } | |
| if (kimiDB) { | |
| try { | |
| const recent = await kimiDB.getRecentConversations(10); | |
| if (recent.length === 0) { | |
| const greeting = await kimiMemory.getGreeting(); | |
| addMessageToChat("kimi", greeting); | |
| } else { | |
| recent.forEach(conv => { | |
| addMessageToChat("user", conv.user, conv.id); | |
| addMessageToChat("kimi", conv.kimi, conv.id); | |
| }); | |
| } | |
| } catch (error) { | |
| console.error("Error while loading history:", error); | |
| const greeting = await kimiMemory.getGreeting(); | |
| addMessageToChat("kimi", greeting); | |
| } | |
| } else { | |
| const greeting = await kimiMemory.getGreeting(); | |
| addMessageToChat("kimi", greeting); | |
| } | |
| } | |
| async function loadSettingsData() { | |
| const kimiDB = window.kimiDB; | |
| const kimiLLM = window.kimiLLM; | |
| if (!kimiDB) return; | |
| try { | |
| // Batch load preferences for better performance | |
| const preferenceKeys = [ | |
| "voiceRate", | |
| "voicePitch", | |
| "voiceVolume", | |
| "selectedLanguage", | |
| "providerApiKey", | |
| "llmProvider", | |
| "llmModelId", | |
| "selectedCharacter", | |
| "llmTemperature", | |
| "llmMaxTokens", | |
| "llmTopP", | |
| "llmFrequencyPenalty", | |
| "llmPresencePenalty", | |
| "enableStreaming" | |
| ]; | |
| const preferences = await kimiDB.getPreferencesBatch(preferenceKeys); | |
| // Set default values for missing preferences | |
| const voiceRate = preferences.voiceRate !== undefined ? preferences.voiceRate : 1.1; | |
| const voicePitch = preferences.voicePitch !== undefined ? preferences.voicePitch : 1.1; | |
| const voiceVolume = preferences.voiceVolume !== undefined ? preferences.voiceVolume : 0.8; | |
| const selectedLanguage = preferences.selectedLanguage || "en"; | |
| // Normalize legacy formats to primary subtag (e.g., 'en-US' -> 'en') | |
| const normSelectedLanguage = (function (raw) { | |
| if (!raw) return "en"; | |
| let r = String(raw).toLowerCase(); | |
| if (r.includes(":")) r = r.split(":").pop(); | |
| r = r.replace("_", "-"); | |
| return r.includes("-") ? r.split("-")[0] : r; | |
| })(selectedLanguage); | |
| const apiKey = preferences.providerApiKey || ""; | |
| const provider = preferences.llmProvider || "openrouter"; | |
| // Resolve baseUrl based on provider-specific stored preferences to avoid cross-provider leaks | |
| const placeholders = window.KimiProviderPlaceholders || {}; | |
| let baseUrl; | |
| if (provider === "openai-compatible" || provider === "ollama") { | |
| const key = `llmBaseUrl_${provider}`; | |
| try { | |
| baseUrl = await kimiDB.getPreference(key, provider === "openai-compatible" ? "" : placeholders[provider]); | |
| } catch (e) { | |
| baseUrl = provider === "openai-compatible" ? "" : placeholders[provider]; | |
| } | |
| } else { | |
| baseUrl = placeholders[provider] || placeholders.openai; | |
| } | |
| const modelId = preferences.llmModelId || (window.kimiLLM ? window.kimiLLM.currentModel : ""); | |
| const selectedCharacter = preferences.selectedCharacter || "kimi"; | |
| const llmTemperature = preferences.llmTemperature !== undefined ? preferences.llmTemperature : 0.9; | |
| const llmMaxTokens = preferences.llmMaxTokens !== undefined ? preferences.llmMaxTokens : 400; | |
| const llmTopP = preferences.llmTopP !== undefined ? preferences.llmTopP : 0.9; | |
| const llmFrequencyPenalty = preferences.llmFrequencyPenalty !== undefined ? preferences.llmFrequencyPenalty : 0.9; | |
| const llmPresencePenalty = preferences.llmPresencePenalty !== undefined ? preferences.llmPresencePenalty : 0.8; | |
| const enableStreaming = preferences.enableStreaming !== undefined ? preferences.enableStreaming : true; | |
| // Update UI with voice settings | |
| const languageSelect = document.getElementById("language-selection"); | |
| if (languageSelect) languageSelect.value = normSelectedLanguage; | |
| updateSlider("voice-rate", voiceRate); | |
| updateSlider("voice-pitch", voicePitch); | |
| updateSlider("voice-volume", voiceVolume); | |
| // Update LLM settings | |
| updateSlider("llm-temperature", llmTemperature); | |
| updateSlider("llm-max-tokens", llmMaxTokens); | |
| updateSlider("llm-top-p", llmTopP); | |
| updateSlider("llm-frequency-penalty", llmFrequencyPenalty); | |
| updateSlider("llm-presence-penalty", llmPresencePenalty); | |
| // Update streaming toggle | |
| const streamingToggle = document.getElementById("enable-streaming"); | |
| if (streamingToggle) { | |
| if (enableStreaming) { | |
| streamingToggle.classList.add("active"); | |
| } else { | |
| streamingToggle.classList.remove("active"); | |
| } | |
| streamingToggle.setAttribute("aria-checked", String(enableStreaming)); | |
| } | |
| // Batch load personality traits | |
| const traitNames = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"]; | |
| const personality = await kimiDB.getPersonalityTraitsBatch(traitNames, selectedCharacter); | |
| const defaults = [65, 55, 70, 75, 60, 50]; | |
| traitNames.forEach((trait, index) => { | |
| const value = typeof personality[trait] === "number" ? personality[trait] : defaults[index]; | |
| updateSlider(`trait-${trait}`, value); | |
| // Update memory cache for affection | |
| if (trait === "affection" && window.kimiMemory) { | |
| window.kimiMemory.affectionTrait = value; | |
| } | |
| }); | |
| // Sync personality traits to ensure consistency | |
| await syncPersonalityTraits(selectedCharacter); | |
| await updateStats(); | |
| // Update API key input | |
| const apiKeyInput = document.getElementById("provider-api-key"); | |
| if (apiKeyInput) { | |
| // Get the API key for the current provider | |
| let providerKey = ""; | |
| if (window.KimiProviderUtils) { | |
| providerKey = await window.KimiProviderUtils.getApiKey(kimiDB, provider); | |
| } else { | |
| providerKey = apiKey; // fallback to old method | |
| } | |
| apiKeyInput.value = providerKey || ""; | |
| } | |
| const providerSelect = document.getElementById("llm-provider"); | |
| if (providerSelect) providerSelect.value = provider; | |
| const baseUrlInput = document.getElementById("llm-base-url"); | |
| if (baseUrlInput) { | |
| // Determine whether base URL should be editable for this provider | |
| const isModifiable = provider === "openai-compatible" || provider === "ollama"; | |
| // Provider-specific defaults/placeholders | |
| const placeholders = { | |
| openrouter: "https://openrouter.ai/api/v1/chat/completions", | |
| openai: "https://api.openai.com/v1/chat/completions", | |
| groq: "https://api.groq.com/openai/v1/chat/completions", | |
| together: "https://api.together.xyz/v1/chat/completions", | |
| deepseek: "https://api.deepseek.com/chat/completions", | |
| "openai-compatible": "", | |
| ollama: "http://localhost:11434/api/chat" | |
| }; | |
| const placeholder = placeholders[provider] || placeholders.openai; | |
| baseUrlInput.placeholder = provider === "openai-compatible" ? "" : placeholder; | |
| if (isModifiable) { | |
| // Show stored baseUrl for modifiable providers (could be empty) | |
| baseUrlInput.value = baseUrl || ""; | |
| baseUrlInput.disabled = false; | |
| baseUrlInput.style.opacity = "1"; | |
| } else { | |
| // For fixed providers show the provider URL as value and make input readonly | |
| baseUrlInput.value = placeholder; | |
| baseUrlInput.disabled = true; | |
| baseUrlInput.style.opacity = "0.6"; | |
| } | |
| } | |
| const modelIdInput = document.getElementById("llm-model-id"); | |
| if (modelIdInput) { | |
| if (provider === "openrouter") { | |
| modelIdInput.value = modelId; | |
| } else { | |
| modelIdInput.value = ""; // only placeholder for non-OpenRouter providers | |
| } | |
| } | |
| // For non-OpenRouter providers we keep placeholder per provider; the value is already set above. | |
| const apiKeyLabel = document.getElementById("api-key-label"); | |
| if (apiKeyLabel) { | |
| apiKeyLabel.textContent = window.KimiProviderUtils | |
| ? window.KimiProviderUtils.getLabelForProvider(provider) | |
| : "API Key"; | |
| } | |
| loadAvailableModels(); | |
| } catch (error) { | |
| console.error("Error while loading settings:", error); | |
| } | |
| } | |
| function updateSlider(id, value) { | |
| const slider = document.getElementById(id); | |
| const valueSpan = document.getElementById(`${id}-value`); | |
| if (slider && valueSpan) { | |
| slider.value = value; | |
| valueSpan.textContent = value; | |
| } | |
| } | |
| async function updatePersonalitySliders(characterKey) { | |
| const kimiDB = window.kimiDB; | |
| if (!kimiDB) return; | |
| try { | |
| // Get current traits from database for this character | |
| const savedTraits = await kimiDB.getAllPersonalityTraits(characterKey); | |
| // Get default traits from KIMI_CHARACTERS constants | |
| const characterDefaults = window.KIMI_CHARACTERS[characterKey]?.traits || {}; | |
| // Get unified defaults | |
| const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { | |
| affection: 55, | |
| playfulness: 55, | |
| intelligence: 70, | |
| empathy: 75, | |
| humor: 60, | |
| romance: 50 | |
| }; | |
| // Use saved traits if they exist, otherwise fall back to character defaults, then unified defaults | |
| const traits = { | |
| affection: savedTraits.affection ?? characterDefaults.affection ?? unifiedDefaults.affection, | |
| playfulness: savedTraits.playfulness ?? characterDefaults.playfulness ?? unifiedDefaults.playfulness, | |
| intelligence: savedTraits.intelligence ?? characterDefaults.intelligence ?? unifiedDefaults.intelligence, | |
| empathy: savedTraits.empathy ?? characterDefaults.empathy ?? unifiedDefaults.empathy, | |
| humor: savedTraits.humor ?? characterDefaults.humor ?? unifiedDefaults.humor, | |
| romance: savedTraits.romance ?? characterDefaults.romance ?? unifiedDefaults.romance | |
| }; | |
| // Check if sliders exist before updating them | |
| const sliderUpdates = [ | |
| { id: "trait-affection", value: traits.affection }, | |
| { id: "trait-playfulness", value: traits.playfulness }, | |
| { id: "trait-intelligence", value: traits.intelligence }, | |
| { id: "trait-empathy", value: traits.empathy }, | |
| { id: "trait-humor", value: traits.humor }, | |
| { id: "trait-romance", value: traits.romance } | |
| ]; | |
| for (const update of sliderUpdates) { | |
| const slider = document.getElementById(update.id); | |
| if (slider) { | |
| updateSlider(update.id, update.value); | |
| } | |
| } | |
| } catch (error) { | |
| console.error("Error updating personality sliders:", error); | |
| } | |
| } | |
| async function updateStats() { | |
| const kimiDB = window.kimiDB; | |
| if (!kimiDB) return; | |
| const character = await kimiDB.getSelectedCharacter(); | |
| // Retrieve token usage (fallback to 0) | |
| const tokensIn = await kimiDB.getPreference(`totalTokensIn_${character}`, 0); | |
| const tokensOut = await kimiDB.getPreference(`totalTokensOut_${character}`, 0); | |
| const charDefAff = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]?.traits?.affection) || null; | |
| const genericAff = (window.getTraitDefaults && window.getTraitDefaults().affection) || 55; | |
| const defaultAff = typeof charDefAff === "number" ? charDefAff : genericAff; | |
| const affectionTrait = await kimiDB.getPersonalityTrait("affection", defaultAff, character); | |
| const conversations = await kimiDB.getAllConversations(character); | |
| let firstInteraction = await kimiDB.getPreference(`firstInteraction_${character}`); | |
| if (!firstInteraction && conversations.length > 0) { | |
| firstInteraction = conversations[0].timestamp; | |
| await kimiDB.setPreference(`firstInteraction_${character}`, firstInteraction); | |
| } | |
| const tokensEl = document.getElementById("tokens-usage"); | |
| const favorabilityEl = document.getElementById("current-favorability"); | |
| const conversationsEl = document.getElementById("conversations-count"); | |
| const daysEl = document.getElementById("days-together"); | |
| if (tokensEl) tokensEl.textContent = `${tokensIn} / ${tokensOut}`; | |
| if (favorabilityEl) { | |
| const v = Number(affectionTrait) || 0; | |
| favorabilityEl.textContent = `${Math.max(0, Math.min(100, v)).toFixed(2)}%`; | |
| } | |
| if (conversationsEl) conversationsEl.textContent = conversations.length; | |
| if (firstInteraction && daysEl) { | |
| const days = Math.floor((new Date() - new Date(firstInteraction)) / (1000 * 60 * 60 * 24)); | |
| daysEl.textContent = days; | |
| } | |
| } | |
| function initializeAllSliders() { | |
| const sliders = [ | |
| "voice-rate", | |
| "voice-pitch", | |
| "voice-volume", | |
| "trait-affection", | |
| "trait-playfulness", | |
| "trait-intelligence", | |
| "trait-empathy", | |
| "trait-humor", | |
| "trait-romance", | |
| "llm-temperature", | |
| "llm-max-tokens", | |
| "llm-top-p", | |
| "llm-frequency-penalty", | |
| "llm-presence-penalty", | |
| "interface-opacity" | |
| ]; | |
| sliders.forEach(sliderId => { | |
| const slider = document.getElementById(sliderId); | |
| const valueSpan = document.getElementById(`${sliderId}-value`); | |
| if (slider && valueSpan) { | |
| valueSpan.textContent = slider.value; | |
| } | |
| }); | |
| } | |
| async function syncLLMMaxTokensSlider() { | |
| const kimiDB = window.kimiDB; | |
| const llmMaxTokensSlider = document.getElementById("llm-max-tokens"); | |
| const llmMaxTokensValue = document.getElementById("llm-max-tokens-value"); | |
| if (llmMaxTokensSlider && llmMaxTokensValue && kimiDB) { | |
| const saved = await kimiDB.getPreference("llmMaxTokens", 400); | |
| llmMaxTokensSlider.value = saved; | |
| llmMaxTokensValue.textContent = saved; | |
| } | |
| } | |
| async function syncLLMTemperatureSlider() { | |
| const kimiDB = window.kimiDB; | |
| const llmTemperatureSlider = document.getElementById("llm-temperature"); | |
| const llmTemperatureValue = document.getElementById("llm-temperature-value"); | |
| if (llmTemperatureSlider && llmTemperatureValue && kimiDB) { | |
| const saved = await kimiDB.getPreference("llmTemperature", 0.8); | |
| llmTemperatureSlider.value = saved; | |
| llmTemperatureValue.textContent = saved; | |
| } | |
| } | |
| function updateTabsScrollIndicator() { | |
| const tabsContainer = document.querySelector(".settings-tabs"); | |
| if (!tabsContainer) return; | |
| const isOverflowing = tabsContainer.scrollWidth > tabsContainer.clientWidth; | |
| if (isOverflowing) { | |
| tabsContainer.classList.remove("no-overflow"); | |
| } else { | |
| tabsContainer.classList.add("no-overflow"); | |
| } | |
| } | |
| async function loadAvailableModels() { | |
| // Prevent multiple simultaneous calls | |
| if (loadAvailableModels._loading) { | |
| return; | |
| } | |
| loadAvailableModels._loading = true; | |
| const kimiLLM = window.kimiLLM; | |
| if (!kimiLLM) { | |
| console.warn("❌ KimiLLM not yet initialized for loadAvailableModels"); | |
| loadAvailableModels._loading = false; | |
| return; | |
| } | |
| const modelsContainer = document.getElementById("models-container"); | |
| if (!modelsContainer) { | |
| console.warn("❌ Models container not found"); | |
| loadAvailableModels._loading = false; | |
| return; | |
| } | |
| try { | |
| const stats = await kimiLLM.getModelStats(); | |
| const signature = JSON.stringify(Object.keys(stats.available || {}).sort()); | |
| if (loadAvailableModels._rendered && loadAvailableModels._signature === signature) { | |
| const currentId = stats.current && stats.current.id; | |
| const cards = modelsContainer.querySelectorAll(".model-card"); | |
| cards.forEach(card => { | |
| if (card.dataset.modelId === currentId) { | |
| card.classList.add("selected"); | |
| } else { | |
| card.classList.remove("selected"); | |
| } | |
| }); | |
| loadAvailableModels._loading = false; | |
| return; | |
| } | |
| while (modelsContainer.firstChild) { | |
| modelsContainer.removeChild(modelsContainer.firstChild); | |
| } | |
| // Check if we have available models | |
| if (!stats.available || Object.keys(stats.available).length === 0) { | |
| console.warn("⚠️ No models available in stats"); | |
| const noModelsDiv = document.createElement("div"); | |
| noModelsDiv.className = "no-models-message"; | |
| noModelsDiv.innerHTML = ` | |
| <p>⚠️ No models available. Please check your API key.</p> | |
| `; | |
| modelsContainer.appendChild(noModelsDiv); | |
| loadAvailableModels._loading = false; | |
| return; | |
| } | |
| // Only log once when models are loaded, not repeated calls | |
| if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) { | |
| if (window.KIMI_CONFIG?.DEBUG?.ENABLED) { | |
| console.log(`✅ Loaded ${Object.keys(stats.available).length} LLM models`); | |
| } | |
| loadAvailableModels._lastLoadTime = Date.now(); | |
| } | |
| const createCard = (id, model) => { | |
| const modelDiv = document.createElement("div"); | |
| modelDiv.className = `model-card ${id === stats.current.id ? "selected" : ""}`; | |
| modelDiv.dataset.modelId = id; | |
| const searchable = [model.name || "", model.provider || "", id, (model.strengths || []).join(" ")] | |
| .join(" ") | |
| .toLowerCase(); | |
| modelDiv.dataset.search = searchable; | |
| // Create model card elements safely | |
| const modelHeader = document.createElement("div"); | |
| modelHeader.className = "model-header"; | |
| const modelName = document.createElement("div"); | |
| modelName.className = "model-name"; | |
| modelName.textContent = model.name; | |
| const modelProvider = document.createElement("div"); | |
| modelProvider.className = "model-provider"; | |
| modelProvider.textContent = model.provider; | |
| modelHeader.appendChild(modelName); | |
| modelHeader.appendChild(modelProvider); | |
| const modelDescription = document.createElement("div"); | |
| modelDescription.className = "model-description"; | |
| const rawIn = model.pricing && typeof model.pricing.input !== "undefined" ? model.pricing.input : "N/A"; | |
| const rawOut = model.pricing && typeof model.pricing.output !== "undefined" ? model.pricing.output : "N/A"; | |
| const inNum = typeof rawIn === "number" ? rawIn : typeof rawIn === "string" ? Number(rawIn) : NaN; | |
| const outNum = typeof rawOut === "number" ? rawOut : typeof rawOut === "string" ? Number(rawOut) : NaN; | |
| const inIsNum = Number.isFinite(inNum); | |
| const outIsNum = Number.isFinite(outNum); | |
| const bothNA = !inIsNum && !outIsNum; | |
| const bothZero = inIsNum && outIsNum && inNum === 0 && outNum === 0; | |
| const isFreeName = | |
| /free/i.test(model.name || "") || | |
| /free/i.test(id || "") || | |
| (Array.isArray(model.strengths) && model.strengths.some(s => /free/i.test(s))); | |
| const fmt = n => { | |
| if (!Number.isFinite(n)) return "N/A"; | |
| const roundedInt = Math.round(n); | |
| if (Math.abs(n - roundedInt) < 1e-6) return `${roundedInt}$`; | |
| return `${n.toFixed(2)}$`; | |
| }; | |
| let inStr = inIsNum ? (inNum === 0 ? "Free" : fmt(inNum)) : "N/A"; | |
| let outStr = outIsNum ? (outNum === 0 ? "Free" : fmt(outNum)) : "N/A"; | |
| let priceText; | |
| if (bothZero || isFreeName) { | |
| priceText = "Price: Free"; | |
| } else if (bothNA) { | |
| priceText = "Price: N/A"; | |
| } else { | |
| priceText = `Price: ${inStr} per 1M input tokens, ${outStr} per 1M output tokens`; | |
| } | |
| modelDescription.textContent = `Context: ${model.contextWindow.toLocaleString()} tokens | ${priceText}`; | |
| const modelStrengths = document.createElement("div"); | |
| modelStrengths.className = "model-strengths"; | |
| if (priceText === "Price: Free") { | |
| const badge = document.createElement("span"); | |
| badge.className = "strength-tag"; | |
| badge.textContent = "Free"; | |
| modelStrengths.appendChild(badge); | |
| } | |
| model.strengths.forEach(strength => { | |
| const strengthTag = document.createElement("span"); | |
| strengthTag.className = "strength-tag"; | |
| strengthTag.textContent = strength; | |
| modelStrengths.appendChild(strengthTag); | |
| }); | |
| modelDiv.appendChild(modelHeader); | |
| modelDiv.appendChild(modelDescription); | |
| modelDiv.appendChild(modelStrengths); | |
| modelDiv.addEventListener("click", async () => { | |
| try { | |
| await kimiLLM.setCurrentModel(id); | |
| document.querySelectorAll(".model-card").forEach(card => card.classList.remove("selected")); | |
| modelDiv.classList.add("selected"); | |
| console.log(`🤖 Model switched to: ${model.name}`); | |
| // Show brief feedback to user | |
| const feedback = document.createElement("div"); | |
| feedback.textContent = `Model changed to ${model.name}`; | |
| feedback.style.cssText = ` | |
| position: fixed; top: 20px; right: 20px; z-index: 10000; | |
| background: #27ae60; color: white; padding: 12px 20px; | |
| border-radius: 6px; font-size: 14px; font-weight: 500; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| `; | |
| document.body.appendChild(feedback); | |
| setTimeout(() => feedback.remove(), 3000); | |
| } catch (error) { | |
| console.error("Error while changing model:", error); | |
| // Show error feedback | |
| const errorFeedback = document.createElement("div"); | |
| errorFeedback.textContent = `Error changing model: ${error.message}`; | |
| errorFeedback.style.cssText = ` | |
| position: fixed; top: 20px; right: 20px; z-index: 10000; | |
| background: #e74c3c; color: white; padding: 12px 20px; | |
| border-radius: 6px; font-size: 14px; font-weight: 500; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.2); | |
| `; | |
| document.body.appendChild(errorFeedback); | |
| setTimeout(() => errorFeedback.remove(), 5000); | |
| } | |
| }); | |
| return modelDiv; | |
| }; | |
| const recommendedIds = | |
| window.kimiLLM && Array.isArray(window.kimiLLM.recommendedModelIds) ? window.kimiLLM.recommendedModelIds : []; | |
| const recommendedEntries = recommendedIds.map(id => [id, stats.available[id]]).filter(([, model]) => !!model); | |
| const otherEntries = Object.entries(stats.available) | |
| .filter(([id]) => !recommendedIds.includes(id)) | |
| .sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0])); | |
| const searchWrap = document.createElement("div"); | |
| searchWrap.className = "models-search-container"; | |
| const searchInput = document.createElement("input"); | |
| searchInput.type = "text"; | |
| searchInput.className = "kimi-input"; | |
| searchInput.id = "models-search"; | |
| // use i18n placeholder key | |
| if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") { | |
| searchInput.setAttribute("data-i18n-placeholder", "models_search_placeholder"); | |
| } else { | |
| searchInput.placeholder = "Filter models..."; | |
| } | |
| searchWrap.appendChild(searchInput); | |
| modelsContainer.appendChild(searchWrap); | |
| if (typeof loadAvailableModels._searchValue === "string") { | |
| searchInput.value = loadAvailableModels._searchValue; | |
| } | |
| if (recommendedEntries.length > 0) { | |
| const recSection = document.createElement("div"); | |
| recSection.className = "models-section recommended-models"; | |
| const title = document.createElement("div"); | |
| title.className = "models-section-title"; | |
| // i18n aware title | |
| title.setAttribute("data-i18n", "models_recommended_title"); | |
| recSection.appendChild(title); | |
| const list = document.createElement("div"); | |
| list.className = "models-list"; | |
| recommendedEntries.forEach(([id, model]) => { | |
| list.appendChild(createCard(id, model)); | |
| }); | |
| recSection.appendChild(list); | |
| modelsContainer.appendChild(recSection); | |
| } | |
| if (otherEntries.length > 0) { | |
| const allSection = document.createElement("div"); | |
| allSection.className = "models-section all-models"; | |
| const header = document.createElement("div"); | |
| header.className = "models-section-title"; | |
| const toggleBtn = document.createElement("button"); | |
| toggleBtn.type = "button"; | |
| toggleBtn.className = "kimi-button"; | |
| toggleBtn.style.marginLeft = "8px"; | |
| // toggle show/hide label via i18n when available | |
| if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") { | |
| const currentKey = loadAvailableModels._allCollapsed === false ? "button_hide" : "button_show"; | |
| toggleBtn.setAttribute("data-i18n", currentKey); | |
| toggleBtn.textContent = window.kimiI18nManager.t(currentKey); | |
| } else { | |
| toggleBtn.textContent = loadAvailableModels._allCollapsed === false ? "Hide" : "Show"; | |
| } | |
| const label = document.createElement("span"); | |
| label.setAttribute("data-i18n", "models_all_title"); | |
| header.appendChild(label); | |
| header.appendChild(toggleBtn); | |
| const refreshBtn = document.createElement("button"); | |
| refreshBtn.type = "button"; | |
| refreshBtn.className = "kimi-button"; | |
| refreshBtn.style.marginLeft = "8px"; | |
| if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") { | |
| refreshBtn.setAttribute("data-i18n", "button_refresh"); | |
| } else { | |
| refreshBtn.textContent = "Refresh"; | |
| } | |
| refreshBtn.addEventListener("click", async () => { | |
| try { | |
| refreshBtn.disabled = true; | |
| const oldText = refreshBtn.textContent; | |
| refreshBtn.textContent = "Refreshing..."; | |
| if (window.kimiLLM && window.kimiLLM.refreshRemoteModels) { | |
| await window.kimiLLM.refreshRemoteModels(); | |
| } | |
| loadAvailableModels._signature = null; | |
| loadAvailableModels._rendered = false; | |
| const savedSearch = searchInput.value; | |
| loadAvailableModels._searchValue = savedSearch; | |
| await loadAvailableModels(); | |
| } catch (e) { | |
| console.error("Error refreshing models:", e); | |
| } finally { | |
| refreshBtn.disabled = false; | |
| refreshBtn.textContent = "Refresh"; | |
| } | |
| }); | |
| header.appendChild(refreshBtn); | |
| const list = document.createElement("div"); | |
| list.className = "models-list"; | |
| otherEntries.forEach(([id, model]) => { | |
| list.appendChild(createCard(id, model)); | |
| }); | |
| const collapsed = loadAvailableModels._allCollapsed !== false; | |
| list.style.display = collapsed ? "none" : "block"; | |
| toggleBtn.addEventListener("click", () => { | |
| const nowCollapsed = list.style.display !== "none"; | |
| list.style.display = nowCollapsed ? "none" : "block"; | |
| loadAvailableModels._allCollapsed = nowCollapsed; | |
| if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") { | |
| const key = nowCollapsed ? "button_show" : "button_hide"; | |
| toggleBtn.setAttribute("data-i18n", key); | |
| toggleBtn.textContent = window.kimiI18nManager.t(key); | |
| } else { | |
| toggleBtn.textContent = nowCollapsed ? "Show" : "Hide"; | |
| } | |
| }); | |
| allSection.appendChild(header); | |
| allSection.appendChild(list); | |
| modelsContainer.appendChild(allSection); | |
| } | |
| const applyFilter = term => { | |
| const q = (term || "").toLowerCase().trim(); | |
| const cards = modelsContainer.querySelectorAll(".model-card"); | |
| cards.forEach(card => { | |
| const hay = card.dataset.search || ""; | |
| card.style.display = q && !hay.includes(q) ? "none" : ""; | |
| }); | |
| }; | |
| searchInput.addEventListener("input", e => { | |
| loadAvailableModels._searchValue = e.target.value; | |
| applyFilter(e.target.value); | |
| }); | |
| if (searchInput.value) { | |
| applyFilter(searchInput.value); | |
| } | |
| loadAvailableModels._rendered = true; | |
| loadAvailableModels._signature = signature; | |
| } catch (error) { | |
| console.error("Error loading available models:", error); | |
| const errorDiv = document.createElement("div"); | |
| errorDiv.className = "models-error-message"; | |
| // Escape any content from error.message to prevent XSS when inserted into innerHTML | |
| const safeMsg = | |
| window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml | |
| ? window.KimiValidationUtils.escapeHtml(error.message || String(error)) | |
| : String(error.message || error); | |
| errorDiv.innerHTML = ` | |
| <p>❌ Error loading models: ${safeMsg}</p> | |
| `; | |
| modelsContainer.appendChild(errorDiv); | |
| } finally { | |
| loadAvailableModels._loading = false; | |
| } | |
| } | |
| // Debug utilities removed for production optimization | |
| async function sendMessage() { | |
| const chatInput = document.getElementById("chat-input"); | |
| const waitingIndicator = document.getElementById("waiting-indicator"); | |
| let message = chatInput.value; | |
| // Enhanced input validation using our new validation utils | |
| const validation = window.KimiValidationUtils?.validateMessage(message); | |
| if (!validation || !validation.valid) { | |
| // Show error to user | |
| if (validation?.error) { | |
| addMessageToChat("system", `❌ ${validation.error}`); | |
| } | |
| // Use sanitized version if available | |
| if (validation?.sanitized) { | |
| chatInput.value = validation.sanitized; | |
| } | |
| return; | |
| } | |
| message = validation.sanitized || message.trim(); | |
| if (!message) return; | |
| addMessageToChat("user", message); | |
| chatInput.value = ""; | |
| if (waitingIndicator) waitingIndicator.style.display = "inline-block"; | |
| try { | |
| // Check if streaming is enabled (you can add a preference for this) | |
| const streamingEnabled = await window.kimiDB?.getPreference( | |
| "enableStreaming", | |
| window.KIMI_CONFIG?.DEFAULTS?.ENABLE_STREAMING ?? true | |
| ); | |
| if (streamingEnabled && window.kimiLLM && typeof window.kimiLLM.chatStreaming === "function") { | |
| // Use streaming through analyzeAndReact | |
| let streamingResponse = ""; | |
| const messageObj = addMessageToChat("kimi", ""); // Start with empty message | |
| // Safety check: ensure messageObj is valid | |
| if (!messageObj || typeof messageObj.updateText !== "function") { | |
| console.error("Failed to create streaming message object, falling back to non-streaming"); | |
| const response = await analyzeAndReact(message); | |
| let finalResponse = response; | |
| if (!finalResponse || typeof finalResponse !== "string" || finalResponse.trim().length < 2) { | |
| finalResponse = window.getLocalizedEmotionalResponse | |
| ? window.getLocalizedEmotionalResponse("neutral") | |
| : "I'm here for you!"; | |
| } | |
| addMessageToChat("kimi", finalResponse); | |
| if (window.voiceManager && !message.startsWith("Vous:")) { | |
| window.voiceManager.speak(finalResponse); | |
| } | |
| if (waitingIndicator) waitingIndicator.style.display = "none"; | |
| return; | |
| } | |
| try { | |
| // Start streaming response processing | |
| if (window.KIMI_CONFIG?.DEBUG?.ENABLED) { | |
| console.log("🔄 Starting streaming response..."); | |
| } | |
| let emotionDetected = false; | |
| const response = await analyzeAndReact(message, true, token => { | |
| streamingResponse += token; | |
| if (messageObj && messageObj.updateText) { | |
| messageObj.updateText(streamingResponse); | |
| } | |
| // Progressive analysis disabled to prevent UI flickering during streaming | |
| // All analysis will be done after streaming completes | |
| }); | |
| // Streaming completed | |
| if (window.KIMI_CONFIG?.DEBUG?.ENABLED) { | |
| console.log("✅ Streaming completed, final response length:", streamingResponse.length); | |
| } | |
| // Final processing after streaming completes | |
| let finalResponse = streamingResponse || response; | |
| if (!finalResponse || finalResponse.trim().length < 2) { | |
| finalResponse = window.getLocalizedEmotionalResponse | |
| ? window.getLocalizedEmotionalResponse("neutral") | |
| : "I'm here for you!"; | |
| if (messageObj && messageObj.updateText) { | |
| messageObj.updateText(finalResponse); | |
| } | |
| } else { | |
| if (messageObj && messageObj.updateText) { | |
| messageObj.updateText(finalResponse); | |
| } | |
| } | |
| // Voice synthesis after streaming completes (if not started during streaming) | |
| if (window.voiceManager && !message.startsWith("Vous:") && finalResponse.length > 20) { | |
| // Check if voice synthesis should happen | |
| const shouldSpeak = await window.kimiDB?.getPreference( | |
| "voiceEnabled", | |
| window.KIMI_CONFIG?.DEFAULTS?.VOICE_ENABLED ?? true | |
| ); | |
| if (shouldSpeak) { | |
| window.voiceManager.speak(finalResponse); | |
| } | |
| } | |
| // Final comprehensive system updates | |
| try { | |
| // Final emotion analysis if not done during streaming | |
| if (!emotionDetected && window.kimiAnalyzeEmotion) { | |
| const finalEmotion = window.kimiAnalyzeEmotion(finalResponse); | |
| if (finalEmotion && finalEmotion !== "neutral") { | |
| emotionDetected = true; | |
| } | |
| } | |
| // Final personality update | |
| if (window.updatePersonalityTraitsFromEmotion && finalResponse.length > 50) { | |
| const finalEmotion = window.kimiAnalyzeEmotion ? window.kimiAnalyzeEmotion(finalResponse) : "neutral"; | |
| await window.updatePersonalityTraitsFromEmotion(finalEmotion, finalResponse); | |
| } | |
| // Final memory extraction | |
| if (window.kimiMemory && typeof window.kimiMemory.extractMemoriesFromConversation === "function") { | |
| await window.kimiMemory.extractMemoriesFromConversation(message, finalResponse); | |
| } | |
| // Final video state adjustment | |
| if (window.kimiVideo && window.kimiDB) { | |
| const selectedCharacter = await window.kimiDB.getSelectedCharacter(); | |
| const traits = await window.kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| if (traits && emotionDetected) { | |
| window.kimiVideo.setMoodByPersonality(traits); | |
| } | |
| } | |
| } catch (finalError) { | |
| console.warn("Final system updates failed:", finalError); | |
| } | |
| if (waitingIndicator) waitingIndicator.style.display = "none"; | |
| } catch (streamingError) { | |
| console.warn("Streaming failed, falling back to non-streaming:", streamingError); | |
| // Fallback to non-streaming | |
| const response = await analyzeAndReact(message); | |
| let finalResponse = response; | |
| if (!finalResponse || typeof finalResponse !== "string" || finalResponse.trim().length < 2) { | |
| finalResponse = window.getLocalizedEmotionalResponse | |
| ? window.getLocalizedEmotionalResponse("neutral") | |
| : "I'm here for you!"; | |
| } | |
| if (messageObj && messageObj.updateText) { | |
| messageObj.updateText(finalResponse); | |
| } | |
| if (window.voiceManager && !message.startsWith("Vous:")) { | |
| window.voiceManager.speak(finalResponse); | |
| } | |
| if (waitingIndicator) waitingIndicator.style.display = "none"; | |
| } | |
| } else { | |
| // Use non-streaming (original behavior) | |
| const response = await analyzeAndReact(message); | |
| let finalResponse = response; | |
| // If the LLM's response is empty, null, or too short, use the emotional fallback. | |
| if (!finalResponse || typeof finalResponse !== "string" || finalResponse.trim().length < 2) { | |
| finalResponse = window.getLocalizedEmotionalResponse | |
| ? window.getLocalizedEmotionalResponse("neutral") | |
| : "I'm here for you!"; | |
| } | |
| setTimeout(() => { | |
| addMessageToChat("kimi", finalResponse); | |
| if (window.voiceManager && !message.startsWith("Vous:")) { | |
| window.voiceManager.speak(finalResponse); | |
| } | |
| if (waitingIndicator) waitingIndicator.style.display = "none"; | |
| }, 1000); | |
| } | |
| } catch (error) { | |
| console.error("Error while generating response:", error); | |
| const i18n = window.kimiI18nManager; | |
| const fallbackResponse = i18n | |
| ? i18n.t("fallback_general_error") | |
| : "Sorry my love, I am having a little technical issue! 💕"; | |
| addMessageToChat("kimi", fallbackResponse); | |
| if (window.voiceManager) { | |
| window.voiceManager.speak(fallbackResponse); | |
| } | |
| if (waitingIndicator) waitingIndicator.style.display = "none"; | |
| } | |
| } | |
| function setupSettingsListeners(kimiDB, kimiMemory) { | |
| const voiceRateSlider = document.getElementById("voice-rate"); | |
| const voicePitchSlider = document.getElementById("voice-pitch"); | |
| const voiceVolumeSlider = document.getElementById("voice-volume"); | |
| const languageSelect = document.getElementById("language-selection"); | |
| const voiceSelect = document.getElementById("voice-selection"); | |
| // Affection restored as editable trait. | |
| const traitSliders = [ | |
| "trait-affection", | |
| "trait-playfulness", | |
| "trait-intelligence", | |
| "trait-empathy", | |
| "trait-humor", | |
| "trait-romance" | |
| ]; | |
| const llmTemperatureSlider = document.getElementById("llm-temperature"); | |
| const llmMaxTokensSlider = document.getElementById("llm-max-tokens"); | |
| const llmTopPSlider = document.getElementById("llm-top-p"); | |
| const llmFrequencyPenaltySlider = document.getElementById("llm-frequency-penalty"); | |
| const llmPresencePenaltySlider = document.getElementById("llm-presence-penalty"); | |
| const enableStreamingToggle = document.getElementById("enable-streaming"); | |
| const colorThemeSelect = document.getElementById("color-theme"); | |
| const interfaceOpacitySlider = document.getElementById("interface-opacity"); | |
| // SIMPLE FIX: Initialize _kimiListenerCleanup to prevent undefined error | |
| if (!window._kimiListenerCleanup) { | |
| window._kimiListenerCleanup = []; | |
| } | |
| // Create debounced functions for better performance | |
| const debouncedVoiceRateUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("voiceRate", parseFloat(value)); | |
| if (kimiMemory && kimiMemory.preferences) { | |
| kimiMemory.preferences.voiceRate = parseFloat(value); | |
| } | |
| }, 300); | |
| const debouncedVoicePitchUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("voicePitch", parseFloat(value)); | |
| if (kimiMemory && kimiMemory.preferences) { | |
| kimiMemory.preferences.voicePitch = parseFloat(value); | |
| } | |
| }, 300); | |
| const debouncedVoiceVolumeUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("voiceVolume", parseFloat(value)); | |
| if (kimiMemory && kimiMemory.preferences) { | |
| kimiMemory.preferences.voiceVolume = parseFloat(value); | |
| } | |
| }, 300); | |
| const debouncedLLMTempUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("llmTemperature", parseFloat(value)); | |
| if (window.kimiLLMManager) window.kimiLLMManager.temperature = parseFloat(value); | |
| }, 300); | |
| const debouncedLLMTokensUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("llmMaxTokens", parseInt(value)); | |
| if (window.kimiLLMManager) window.kimiLLMManager.maxTokens = parseInt(value); | |
| }, 300); | |
| const debouncedLLMTopPUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("llmTopP", parseFloat(value)); | |
| if (window.kimiLLMManager) window.kimiLLMManager.topP = parseFloat(value); | |
| }, 300); | |
| const debouncedLLMFrequencyPenaltyUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("llmFrequencyPenalty", parseFloat(value)); | |
| if (window.kimiLLMManager) window.kimiLLMManager.frequencyPenalty = parseFloat(value); | |
| }, 300); | |
| const debouncedLLMPresencePenaltyUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("llmPresencePenalty", parseFloat(value)); | |
| if (window.kimiLLMManager) window.kimiLLMManager.presencePenalty = parseFloat(value); | |
| }, 300); | |
| const debouncedOpacityUpdate = window.KimiPerformanceUtils?.debounce(async value => { | |
| if (kimiDB) await kimiDB.setPreference("interfaceOpacity", parseFloat(value)); | |
| if (window.kimiAppearanceManager && window.kimiAppearanceManager.changeInterfaceOpacity) | |
| await window.kimiAppearanceManager.changeInterfaceOpacity(parseFloat(value)); | |
| }, 300); | |
| if (voiceRateSlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate"); | |
| // Preserve legitimate zero values (avoid using || which treats 0 as falsy) | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 1.1; | |
| document.getElementById("voice-rate-value").textContent = value; | |
| e.target.value = value; // Ensure slider shows validated value | |
| debouncedVoiceRateUpdate(value); | |
| }; | |
| voiceRateSlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => voiceRateSlider.removeEventListener("input", listener)); | |
| } | |
| if (voicePitchSlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 1.1; | |
| document.getElementById("voice-pitch-value").textContent = value; | |
| e.target.value = value; | |
| debouncedVoicePitchUpdate(value); | |
| }; | |
| voicePitchSlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => voicePitchSlider.removeEventListener("input", listener)); | |
| } | |
| if (voiceVolumeSlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 0.8; | |
| document.getElementById("voice-volume-value").textContent = value; | |
| e.target.value = value; | |
| debouncedVoiceVolumeUpdate(value); | |
| }; | |
| voiceVolumeSlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => voiceVolumeSlider.removeEventListener("input", listener)); | |
| } | |
| // Note: Language selector event listener is now handled by VoiceManager.setupLanguageSelector() | |
| // This prevents duplicate event listeners and ensures proper voice/language coordination | |
| // Note: Voice selector event listener is now handled by VoiceManager.updateVoiceSelector() | |
| // This prevents duplicate event listeners and ensures proper voice preference coordination | |
| // Batch personality traits optimization | |
| let personalityBatchTimeout = null; | |
| const pendingTraitChanges = {}; | |
| traitSliders.forEach(traitId => { | |
| const traitSlider = document.getElementById(traitId); | |
| if (traitSlider) { | |
| traitSlider.removeEventListener("input", window["_kimiTraitListener_" + traitId]); | |
| window["_kimiTraitListener_" + traitId] = async e => { | |
| const trait = traitId.replace("trait-", ""); | |
| const value = parseInt(e.target.value, 10); | |
| // Update UI immediately for responsive feel | |
| const valueSpan = document.getElementById(traitId + "-value"); | |
| if (valueSpan) { | |
| valueSpan.textContent = value; | |
| } | |
| // Store pending change for batch processing | |
| pendingTraitChanges[trait] = value; | |
| // Clear existing timeout and set new one for batch save | |
| if (personalityBatchTimeout) { | |
| clearTimeout(personalityBatchTimeout); | |
| } | |
| personalityBatchTimeout = setTimeout(async () => { | |
| if (kimiDB && Object.keys(pendingTraitChanges).length > 0) { | |
| try { | |
| // Use batch operation for all pending changes (affection included) | |
| await kimiDB.setPersonalityBatch(pendingTraitChanges); | |
| // Side-effects handled by central 'personality:updated' listener. | |
| } catch (error) { | |
| console.error("Error batch saving personality traits:", error); | |
| } | |
| // Clear pending changes | |
| Object.keys(pendingTraitChanges).forEach(key => delete pendingTraitChanges[key]); | |
| } | |
| }, 500); // Debounce for 500ms to batch multiple rapid changes | |
| }; | |
| traitSlider.addEventListener("input", window["_kimiTraitListener_" + traitId]); | |
| } | |
| }); | |
| if (llmTemperatureSlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 0.9; | |
| document.getElementById("llm-temperature-value").textContent = value; | |
| e.target.value = value; | |
| debouncedLLMTempUpdate(value); | |
| }; | |
| llmTemperatureSlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => llmTemperatureSlider.removeEventListener("input", listener)); | |
| } | |
| if (llmMaxTokensSlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens"); | |
| const value = validation?.value || parseInt(e.target.value) || 400; | |
| document.getElementById("llm-max-tokens-value").textContent = value; | |
| e.target.value = value; | |
| debouncedLLMTokensUpdate(value); | |
| }; | |
| llmMaxTokensSlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => llmMaxTokensSlider.removeEventListener("input", listener)); | |
| } | |
| if (llmTopPSlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 0.9; | |
| document.getElementById("llm-top-p-value").textContent = value; | |
| e.target.value = value; | |
| debouncedLLMTopPUpdate(value); | |
| }; | |
| llmTopPSlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => llmTopPSlider.removeEventListener("input", listener)); | |
| } | |
| if (llmFrequencyPenaltySlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 0.9; | |
| document.getElementById("llm-frequency-penalty-value").textContent = value; | |
| e.target.value = value; | |
| debouncedLLMFrequencyPenaltyUpdate(value); | |
| }; | |
| llmFrequencyPenaltySlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => llmFrequencyPenaltySlider.removeEventListener("input", listener)); | |
| } | |
| if (llmPresencePenaltySlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 0.8; | |
| document.getElementById("llm-presence-penalty-value").textContent = value; | |
| e.target.value = value; | |
| debouncedLLMPresencePenaltyUpdate(value); | |
| }; | |
| llmPresencePenaltySlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => llmPresencePenaltySlider.removeEventListener("input", listener)); | |
| } | |
| if (enableStreamingToggle) { | |
| const listener = async () => { | |
| try { | |
| const isEnabled = enableStreamingToggle.classList.contains("active"); | |
| const newState = !isEnabled; | |
| enableStreamingToggle.classList.toggle("active", newState); | |
| enableStreamingToggle.setAttribute("aria-checked", newState ? "true" : "false"); | |
| if (kimiDB) await kimiDB.setPreference("enableStreaming", newState); | |
| } catch (error) { | |
| console.error("Error toggling streaming:", error); | |
| } | |
| }; | |
| enableStreamingToggle.addEventListener("click", listener); | |
| window._kimiListenerCleanup.push(() => enableStreamingToggle.removeEventListener("click", listener)); | |
| } | |
| if (colorThemeSelect) { | |
| colorThemeSelect.removeEventListener("change", window._kimiColorThemeListener); | |
| window._kimiColorThemeListener = async e => { | |
| if (kimiDB) await kimiDB.setPreference("colorTheme", e.target.value); | |
| if (window.kimiAppearanceManager && window.kimiAppearanceManager.changeTheme) | |
| await window.kimiAppearanceManager.changeTheme(e.target.value); | |
| }; | |
| colorThemeSelect.addEventListener("change", window._kimiColorThemeListener); | |
| } | |
| if (interfaceOpacitySlider) { | |
| const listener = e => { | |
| const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity"); | |
| let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value); | |
| if (isNaN(value)) value = 0.8; | |
| document.getElementById("interface-opacity-value").textContent = value; | |
| e.target.value = value; | |
| debouncedOpacityUpdate(value); | |
| }; | |
| interfaceOpacitySlider.addEventListener("input", listener); | |
| window._kimiListenerCleanup.push(() => interfaceOpacitySlider.removeEventListener("input", listener)); | |
| } | |
| // Animation toggle is handled by KimiAppearanceManager | |
| // Remove the duplicate handler to prevent conflicts | |
| // Real-time transcript toggle (shows live speech transcription and AI responses) | |
| const transcriptToggle = document.getElementById("transcript-toggle"); | |
| if (transcriptToggle) { | |
| if (kimiDB && kimiDB.getPreference) { | |
| kimiDB.getPreference("showTranscript", window.KIMI_CONFIG?.DEFAULTS?.SHOW_TRANSCRIPT ?? true).then(showTranscript => { | |
| transcriptToggle.classList.toggle("active", showTranscript); | |
| transcriptToggle.setAttribute("aria-checked", showTranscript ? "true" : "false"); | |
| }); | |
| } | |
| const onToggle = async () => { | |
| const enabled = !transcriptToggle.classList.contains("active"); | |
| transcriptToggle.classList.toggle("active", enabled); | |
| transcriptToggle.setAttribute("aria-checked", enabled ? "true" : "false"); | |
| // Save transcript display preference | |
| if (kimiDB && kimiDB.setPreference) { | |
| await kimiDB.setPreference("showTranscript", enabled); | |
| } | |
| // Apply change immediately if transcript is currently visible | |
| if (window.kimiVoiceManager && window.kimiVoiceManager.updateTranscriptVisibility) { | |
| if (!enabled) { | |
| // Hide transcript immediately if disabled (uses centralized logic) | |
| await window.kimiVoiceManager.updateTranscriptVisibility(false); | |
| } | |
| // If enabled, transcript will show naturally during next voice interaction | |
| } | |
| }; | |
| transcriptToggle.onclick = onToggle; | |
| transcriptToggle.onkeydown = async e => { | |
| if (e.key === " " || e.key === "Enter") { | |
| e.preventDefault(); | |
| await onToggle(); | |
| } | |
| }; | |
| } | |
| } | |
| // Exposer globalement (KimiDataManager already exposed in kimi-data-manager.js) | |
| window.updateFavorabilityLabel = updateFavorabilityLabel; | |
| window.loadCharacterSection = loadCharacterSection; | |
| window.getBasicResponse = getBasicResponse; | |
| window.analyzeAndReact = analyzeAndReact; | |
| window.addMessageToChat = addMessageToChat; | |
| window.loadChatHistory = loadChatHistory; | |
| window.loadSettingsData = loadSettingsData; | |
| window.updateSlider = updateSlider; | |
| // DYNAMIC SLIDER SYNC | |
| async function refreshAllSliders() { | |
| if (!window.kimiDB) return; | |
| const prefMap = [ | |
| ["voice-rate", "voiceRate", "VOICE_RATE"], | |
| ["voice-pitch", "voicePitch", "VOICE_PITCH"], | |
| ["voice-volume", "voiceVolume", "VOICE_VOLUME"], | |
| ["llm-temperature", "llmTemperature", "LLM_TEMPERATURE"], | |
| ["llm-max-tokens", "llmMaxTokens", "LLM_MAX_TOKENS"], | |
| ["llm-top-p", "llmTopP", "LLM_TOP_P"], | |
| ["llm-frequency-penalty", "llmFrequencyPenalty", "LLM_FREQUENCY_PENALTY"], | |
| ["llm-presence-penalty", "llmPresencePenalty", "LLM_PRESENCE_PENALTY"], | |
| ["interface-opacity", "interfaceOpacity", "INTERFACE_OPACITY"] | |
| ]; | |
| for (const [sliderId, prefKey, defaultKey] of prefMap) { | |
| try { | |
| const el = document.getElementById(sliderId); | |
| if (!el) continue; | |
| const stored = await window.kimiDB.getPreference(prefKey, window.KIMI_CONFIG?.DEFAULTS?.[defaultKey]); | |
| if (typeof stored === "number" || (typeof stored === "string" && stored !== null)) { | |
| updateSlider(sliderId, stored); | |
| } | |
| } catch {} | |
| } | |
| // Load streaming preference | |
| try { | |
| const enableStreamingToggle = document.getElementById("enable-streaming"); | |
| if (enableStreamingToggle) { | |
| const streamingEnabled = await window.kimiDB.getPreference( | |
| "enableStreaming", | |
| window.KIMI_CONFIG?.DEFAULTS?.ENABLE_STREAMING ?? true | |
| ); | |
| enableStreamingToggle.classList.toggle("active", streamingEnabled); | |
| enableStreamingToggle.setAttribute("aria-checked", streamingEnabled ? "true" : "false"); | |
| } | |
| } catch {} | |
| } | |
| window.refreshAllSliders = refreshAllSliders; | |
| const _debouncedPrefUpdate = window.KimiPerformanceUtils | |
| ? window.KimiPerformanceUtils.debounce(evt => { | |
| const key = evt.detail?.key; | |
| if (!key) return; | |
| const keyToSlider = { | |
| voiceRate: "voice-rate", | |
| voicePitch: "voice-pitch", | |
| voiceVolume: "voice-volume", | |
| llmTemperature: "llm-temperature", | |
| llmMaxTokens: "llm-max-tokens", | |
| llmTopP: "llm-top-p", | |
| llmFrequencyPenalty: "llm-frequency-penalty", | |
| llmPresencePenalty: "llm-presence-penalty", | |
| interfaceOpacity: "interface-opacity" | |
| }; | |
| const sliderId = keyToSlider[key]; | |
| if (sliderId && typeof evt.detail.value !== "undefined") { | |
| updateSlider(sliderId, evt.detail.value); | |
| } | |
| }, 120) | |
| : null; | |
| window.addEventListener("preferenceUpdated", evt => { | |
| if (_debouncedPrefUpdate) _debouncedPrefUpdate(evt); | |
| }); | |
| window.updatePersonalitySliders = updatePersonalitySliders; | |
| window.updateStats = updateStats; | |
| window.initializeAllSliders = initializeAllSliders; | |
| window.syncLLMMaxTokensSlider = syncLLMMaxTokensSlider; | |
| window.syncLLMTemperatureSlider = syncLLMTemperatureSlider; | |
| window.updateTabsScrollIndicator = updateTabsScrollIndicator; | |
| window.loadAvailableModels = loadAvailableModels; | |
| window.sendMessage = sendMessage; | |
| window.setupSettingsListeners = setupSettingsListeners; | |
| window.syncPersonalityTraits = syncPersonalityTraits; | |
| window.validateEmotionContext = validateEmotionContext; | |
| window.ensureVideoContextConsistency = ensureVideoContextConsistency; | |
| document.addEventListener("DOMContentLoaded", function () { | |
| const toggleBtn = document.getElementById("toggle-personality-traits"); | |
| const cheatPanel = document.getElementById("personality-traits-panel"); | |
| if (toggleBtn && cheatPanel) { | |
| toggleBtn.addEventListener("click", function () { | |
| const expanded = toggleBtn.getAttribute("aria-expanded") === "true"; | |
| toggleBtn.setAttribute("aria-expanded", !expanded); | |
| cheatPanel.classList.toggle("open", !expanded); | |
| }); | |
| } | |
| // Refresh UI models list when the LLM model changes programmatically | |
| try { | |
| window.addEventListener("llmModelChanged", () => { | |
| if (typeof window.loadAvailableModels === "function") { | |
| window.loadAvailableModels(); | |
| } | |
| }); | |
| } catch (e) {} | |
| // Typing indicator wiring | |
| try { | |
| // Soft tweak of API key input attributes shortly after load to reduce password manager prompts | |
| setTimeout(() => { | |
| const apiInput = document.getElementById("openrouter-api-key"); | |
| if (apiInput) { | |
| apiInput.setAttribute("autocomplete", "new-password"); | |
| apiInput.setAttribute("name", "openrouter_api_key"); | |
| apiInput.setAttribute("data-lpignore", "true"); | |
| apiInput.setAttribute("data-1p-ignore", "true"); | |
| apiInput.setAttribute("data-bwignore", "true"); | |
| apiInput.setAttribute("data-form-type", "other"); | |
| apiInput.setAttribute("autocapitalize", "none"); | |
| apiInput.setAttribute("autocorrect", "off"); | |
| apiInput.setAttribute("spellcheck", "false"); | |
| } | |
| }, 300); | |
| window.addEventListener("chat:typing:start", () => { | |
| const waitingIndicator = document.getElementById("waiting-indicator"); | |
| const globalTyping = document.getElementById("global-typing-indicator"); | |
| clearTimeout(window._kimiTypingDelayTimer); | |
| window._kimiTypingDelayTimer = setTimeout(() => { | |
| if (waitingIndicator) waitingIndicator.classList.add("visible"); | |
| if (globalTyping) globalTyping.classList.add("visible"); | |
| }, 150); | |
| // Safety auto-hide after 10s in case stop event is blocked | |
| clearTimeout(window._kimiTypingSafetyTimer); | |
| window._kimiTypingSafetyTimer = setTimeout(() => { | |
| if (waitingIndicator) waitingIndicator.classList.remove("visible"); | |
| if (globalTyping) globalTyping.classList.remove("visible"); | |
| }, 10000); | |
| }); | |
| window.addEventListener("chat:typing:stop", () => { | |
| const waitingIndicator = document.getElementById("waiting-indicator"); | |
| const globalTyping = document.getElementById("global-typing-indicator"); | |
| if (waitingIndicator) waitingIndicator.classList.remove("visible"); | |
| if (globalTyping) globalTyping.classList.remove("visible"); | |
| clearTimeout(window._kimiTypingSafetyTimer); | |
| clearTimeout(window._kimiTypingDelayTimer); | |
| }); | |
| } catch (e) {} | |
| }); | |
| // Function to sync all personality traits with database and UI | |
| async function syncPersonalityTraits(characterName = null) { | |
| const kimiDB = window.kimiDB; | |
| if (!kimiDB) return; | |
| const selectedCharacter = characterName || (await kimiDB.getSelectedCharacter()); | |
| const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| // Build required traits prioritizing character-specific defaults (fallback to generic) | |
| const getRequiredTraits = () => { | |
| const charDefaults = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]?.traits) || {}; | |
| let generic = {}; | |
| if (window.KimiEmotionSystem) { | |
| const emotionSystem = new window.KimiEmotionSystem(kimiDB); | |
| generic = emotionSystem.TRAIT_DEFAULTS; | |
| } else if (window.getTraitDefaults) { | |
| generic = window.getTraitDefaults(); | |
| } else { | |
| generic = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 }; | |
| } | |
| // Character defaults take precedence over generic defaults | |
| return { ...generic, ...charDefaults }; | |
| }; | |
| const requiredTraits = getRequiredTraits(); | |
| let needsUpdate = false; | |
| const updatedTraits = {}; | |
| for (const [trait, defaultValue] of Object.entries(requiredTraits)) { | |
| const currentValue = traits[trait]; | |
| if (typeof currentValue !== "number" || currentValue < 0 || currentValue > 100) { | |
| updatedTraits[trait] = defaultValue; | |
| needsUpdate = true; | |
| } else { | |
| updatedTraits[trait] = currentValue; | |
| } | |
| } | |
| // Update database if needed | |
| if (needsUpdate) { | |
| await kimiDB.setPersonalityBatch(updatedTraits, selectedCharacter); | |
| } | |
| // Update UI sliders | |
| for (const [trait, value] of Object.entries(updatedTraits)) { | |
| updateSlider(`trait-${trait}`, value); | |
| } | |
| // Update memory cache | |
| if (window.kimiMemory && updatedTraits.affection) { | |
| window.kimiMemory.affectionTrait = updatedTraits.affection; | |
| if (window.updateGlobalPersonalityUI) { | |
| window.updateGlobalPersonalityUI(); | |
| } else if (window.kimiMemory.updateFavorabilityBar) { | |
| // Fallback (will internally compute average now) | |
| window.kimiMemory.updateFavorabilityBar(); | |
| } | |
| } | |
| // Video/voice updates are centralized in the 'personality:updated' listener. | |
| return updatedTraits; | |
| } | |
| // Simplified validation using centralized emotion system | |
| function validateEmotionContext(emotion) { | |
| return window.kimiEmotionSystem?.validateEmotion(emotion) || "neutral"; | |
| } | |
| // Simplified video context consistency check using centralized system | |
| async function ensureVideoContextConsistency() { | |
| if (!window.kimiVideo || !window.kimiDB) return; | |
| const selectedCharacter = await window.kimiDB.getSelectedCharacter(); | |
| const traits = await window.kimiDB.getAllPersonalityTraits(selectedCharacter); | |
| // Validate current video context using centralized validation | |
| const currentInfo = window.kimiVideo.getCurrentVideoInfo(); | |
| const validatedEmotion = validateEmotionContext(currentInfo.emotion); | |
| if (validatedEmotion !== currentInfo.emotion) { | |
| window.kimiVideo.switchToContext("neutral", "neutral", null, traits, traits.affection); | |
| } | |
| } | |