Virtual-Kimi / kimi-js /kimi-module.js
VirtualKimi's picture
Upload kimi-module.js
c7438a1 verified
// 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);
}
}