document.addEventListener("DOMContentLoaded", async function () {
const DEFAULT_SYSTEM_PROMPT = window.DEFAULT_SYSTEM_PROMPT;
let kimiDB = null;
let kimiLLM = null;
let isSystemReady = false;
const kimiInit = new KimiInitManager();
let kimiVideo = null;
// Error manager is already initialized in kimi-error-manager.js
try {
kimiDB = new KimiDatabase();
await kimiDB.init();
// Expose globally as soon as available
window.kimiDB = kimiDB;
const selectedCharacter = await kimiDB.getPreference("selectedCharacter", "kimi");
const favorabilityLabel = window.KimiDOMUtils.get("#favorability-label");
if (favorabilityLabel && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]) {
favorabilityLabel.setAttribute("data-i18n", "affection_level_of");
favorabilityLabel.setAttribute(
"data-i18n-params",
JSON.stringify({ name: window.KIMI_CHARACTERS[selectedCharacter].name })
);
favorabilityLabel.textContent = `💖 Affection level of ${window.KIMI_CHARACTERS[selectedCharacter].name}`;
}
const chatHeaderName = window.KimiDOMUtils.get(".chat-header span[data-i18n]");
if (chatHeaderName && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]) {
chatHeaderName.setAttribute("data-i18n", `chat_with_${selectedCharacter}`);
}
const systemPromptInput = window.KimiDOMUtils.get("#system-prompt");
if (systemPromptInput && kimiDB.getSystemPromptForCharacter) {
const prompt = await kimiDB.getSystemPromptForCharacter(selectedCharacter);
systemPromptInput.value = prompt;
if (kimiLLM && kimiLLM.setSystemPrompt) kimiLLM.setSystemPrompt(prompt);
}
kimiLLM = new KimiLLMManager(kimiDB);
window.kimiLLM = kimiLLM;
await kimiLLM.init();
// Initialize unified emotion system
window.kimiEmotionSystem = new window.KimiEmotionSystem(kimiDB);
// Initialize the new memory system
window.kimiMemorySystem = new window.KimiMemorySystem(kimiDB);
await window.kimiMemorySystem.init();
// Initialize legacy memory for favorability
const kimiMemory = new KimiMemory(kimiDB);
await kimiMemory.init();
window.kimiMemory = kimiMemory;
// Expose globally (already set before init)
// Load available models now that LLM is ready
if (window.loadAvailableModels) {
setTimeout(() => window.loadAvailableModels(), 500);
}
isSystemReady = true;
window.isSystemReady = true;
// Hydrate API config UI from DB after systems ready
initializeApiConfigUI();
} catch (error) {
console.error("Initialization error:", error);
}
// Centralized helpers for API config UI
const ApiUi = {
presenceDot: () => document.getElementById("api-key-presence"),
presenceDotTest: () => document.getElementById("api-key-presence-test"),
apiKeyInput: () => document.getElementById("openrouter-api-key"),
toggleBtn: () => document.getElementById("toggle-api-key"),
providerSelect: () => document.getElementById("llm-provider"),
baseUrlInput: () => document.getElementById("llm-base-url"),
modelIdInput: () => document.getElementById("llm-model-id"),
savedBadge: () => document.getElementById("api-key-saved"),
statusSpan: () => document.getElementById("api-status"),
testBtn: () => document.getElementById("test-api"),
// Saved key indicator (left dot)
setPresence(color) {
const dot = this.presenceDot();
if (dot) dot.style.backgroundColor = color;
},
// Test result indicator (right dot)
setTestPresence(color) {
const dot2 = this.presenceDotTest();
if (dot2) dot2.style.backgroundColor = color;
},
clearStatus() {
const s = this.statusSpan();
if (s) {
s.textContent = "";
s.style.color = "";
}
},
setTestEnabled(enabled) {
const b = this.testBtn();
if (b) b.disabled = !enabled;
}
};
// Initial presence state based on current input value
{
const apiInputInit = ApiUi.apiKeyInput();
const currentVal = (apiInputInit || {}).value || "";
const colorInit = currentVal && currentVal.length > 0 ? "#4caf50" : "#9e9e9e";
ApiUi.setPresence(colorInit);
// On load, test status is unknown
ApiUi.setTestPresence("#9e9e9e");
// Enforce initial masking style to avoid UA text-security glitches
if (apiInputInit && apiInputInit.classList.contains("masked")) {
try {
apiInputInit.style.webkitTextSecurity = "disc";
} catch (_) {}
apiInputInit.style.filter = "blur(6px)";
}
}
// Initialize API config UI from saved preferences
async function initializeApiConfigUI() {
try {
if (!window.kimiDB) return;
const provider = await window.kimiDB.getPreference("llmProvider", "openrouter");
const baseUrl = await window.kimiDB.getPreference(
"llmBaseUrl",
provider === "openrouter"
? "https://openrouter.ai/api/v1/chat/completions"
: "https://api.openai.com/v1/chat/completions"
);
const modelId = await window.kimiDB.getPreference(
"llmModelId",
window.kimiLLM ? window.kimiLLM.currentModel : "model-id"
);
const providerSelect = ApiUi.providerSelect();
if (providerSelect) providerSelect.value = provider;
const baseUrlInput = ApiUi.baseUrlInput();
const modelIdInput = ApiUi.modelIdInput();
const apiKeyInput = ApiUi.apiKeyInput();
if (baseUrlInput) {
baseUrlInput.value = baseUrl || "";
const isFixed = ["openrouter", "openai", "groq", "together", "deepseek"].includes(provider);
baseUrlInput.readOnly = !!isFixed;
baseUrlInput.setAttribute("aria-readonly", isFixed ? "true" : "false");
// Anti-autofill & soft-readonly flags
baseUrlInput.autocomplete = "new-password";
baseUrlInput.removeAttribute("name");
baseUrlInput.setAttribute("data-lpignore", "true");
baseUrlInput.setAttribute("data-1p-ignore", "true");
baseUrlInput.setAttribute("data-bwignore", "true");
baseUrlInput.setAttribute("data-form-type", "other");
baseUrlInput.setAttribute("autocapitalize", "none");
baseUrlInput.setAttribute("autocorrect", "off");
// For editable providers, use a soft-readonly until focus to deter autofill prompts
if (!isFixed) {
baseUrlInput.setAttribute("data-soft-readonly", "true");
baseUrlInput.setAttribute("readonly", "true");
baseUrlInput.addEventListener(
"focus",
() => {
if (baseUrlInput.hasAttribute("data-soft-readonly")) {
baseUrlInput.removeAttribute("readonly");
}
},
{ once: true }
);
} else {
baseUrlInput.removeAttribute("data-soft-readonly");
}
}
// Only prefill model for OpenRouter, others should show placeholder only
if (modelIdInput) {
if (provider === "openrouter") {
modelIdInput.value = modelId || (window.kimiLLM ? window.kimiLLM.currentModel : "");
modelIdInput.readOnly = true;
modelIdInput.setAttribute("aria-readonly", "true");
} else {
modelIdInput.value = "";
modelIdInput.readOnly = false;
modelIdInput.setAttribute("aria-readonly", "false");
}
// Anti-autofill & soft-readonly flags for model id when editable
modelIdInput.autocomplete = "new-password";
modelIdInput.removeAttribute("name");
modelIdInput.setAttribute("data-lpignore", "true");
modelIdInput.setAttribute("data-1p-ignore", "true");
modelIdInput.setAttribute("data-bwignore", "true");
modelIdInput.setAttribute("data-form-type", "other");
modelIdInput.setAttribute("autocapitalize", "none");
modelIdInput.setAttribute("autocorrect", "off");
if (!modelIdInput.readOnly) {
modelIdInput.setAttribute("data-soft-readonly", "true");
modelIdInput.setAttribute("readonly", "true");
modelIdInput.addEventListener(
"focus",
() => {
if (modelIdInput.hasAttribute("data-soft-readonly")) {
modelIdInput.removeAttribute("readonly");
}
},
{ once: true }
);
} else {
modelIdInput.removeAttribute("data-soft-readonly");
}
}
// Load the provider-specific key
const keyPrefMap = {
openrouter: "openrouterApiKey",
openai: "apiKey_openai",
groq: "apiKey_groq",
together: "apiKey_together",
deepseek: "apiKey_deepseek",
"openai-compatible": "apiKey_custom"
};
const keyPref = keyPrefMap[provider] || "llmApiKey";
const storedKey = await window.kimiDB.getPreference(keyPref, "");
if (apiKeyInput) {
apiKeyInput.value = storedKey || "";
// Keep masking visuals consistent after programmatic value changes
if (apiKeyInput.classList.contains("masked")) {
try {
apiKeyInput.style.webkitTextSecurity = "disc";
} catch (_) {}
apiKeyInput.style.filter = "blur(6px)";
} else {
try {
apiKeyInput.style.webkitTextSecurity = "";
} catch (_) {}
apiKeyInput.style.filter = "none";
}
}
ApiUi.setPresence(storedKey ? "#4caf50" : "#9e9e9e");
ApiUi.setTestPresence("#9e9e9e");
const savedBadge = ApiUi.savedBadge();
if (savedBadge) savedBadge.style.display = storedKey ? "inline" : "none";
ApiUi.clearStatus();
// Enable/disable Test button according to validation (Ollama does not require API key)
const valid = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(storedKey || ""));
ApiUi.setTestEnabled(provider === "ollama" ? true : valid);
// Update dynamic label and placeholders using change handler logic
if (providerSelect && typeof providerSelect.dispatchEvent === "function") {
const ev = new Event("change");
providerSelect.dispatchEvent(ev);
}
} catch (e) {
console.warn("Failed to initialize API config UI:", e);
}
}
const providerSelectEl = document.getElementById("llm-provider");
if (providerSelectEl) {
providerSelectEl.addEventListener("change", async () => {
const provider = providerSelectEl.value;
const baseUrlInput = ApiUi.baseUrlInput();
const apiKeyInput = ApiUi.apiKeyInput();
const modelIdInput = ApiUi.modelIdInput();
const placeholders = {
openrouter: {
url: "https://openrouter.ai/api/v1/chat/completions",
keyPh: "sk-or-v1-...",
model: window.kimiLLM ? window.kimiLLM.currentModel : "model-id"
},
openai: {
url: "https://api.openai.com/v1/chat/completions",
keyPh: "sk-...",
model: "gpt-4o-mini"
},
groq: {
url: "https://api.groq.com/openai/v1/chat/completions",
keyPh: "gsk_...",
model: "llama-3.1-8b-instant"
},
together: {
url: "https://api.together.xyz/v1/chat/completions",
keyPh: "together_...",
model: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
},
deepseek: {
url: "https://api.deepseek.com/chat/completions",
keyPh: "sk-...",
model: "deepseek-chat"
},
"openai-compatible": {
url: "https://your-endpoint/v1/chat/completions",
keyPh: "your-key",
model: "model-id"
},
ollama: {
url: "http://localhost:11434/api/chat",
keyPh: "",
model: "llama3"
}
};
const p = placeholders[provider] || placeholders.openai;
if (baseUrlInput) {
baseUrlInput.placeholder = p.url;
// Providers fixes: URL canonique, champ en lecture seule
const isFixed = ["openrouter", "openai", "groq", "together", "deepseek"].includes(provider);
if (isFixed) {
baseUrlInput.value = provider === "openrouter" ? "https://openrouter.ai/api/v1/chat/completions" : p.url;
baseUrlInput.readOnly = true;
baseUrlInput.setAttribute("aria-readonly", "true");
baseUrlInput.removeAttribute("data-soft-readonly");
} else {
// Custom & Ollama: laisser éditable et ne pas écraser l’URL utilisateur si déjà définie
let savedUrl = "";
if (window.kimiDB) {
savedUrl = await window.kimiDB.getPreference("llmBaseUrl", "");
}
baseUrlInput.value = savedUrl || p.url;
baseUrlInput.readOnly = false;
baseUrlInput.setAttribute("aria-readonly", "false");
baseUrlInput.setAttribute("data-soft-readonly", "true");
baseUrlInput.setAttribute("readonly", "true");
baseUrlInput.addEventListener(
"focus",
() => {
if (baseUrlInput.hasAttribute("data-soft-readonly")) {
baseUrlInput.removeAttribute("readonly");
}
},
{ once: true }
);
}
// Reassert anti-autofill attributes each switch
baseUrlInput.autocomplete = "new-password";
baseUrlInput.removeAttribute("name");
baseUrlInput.setAttribute("data-lpignore", "true");
baseUrlInput.setAttribute("data-1p-ignore", "true");
baseUrlInput.setAttribute("data-bwignore", "true");
baseUrlInput.setAttribute("data-form-type", "other");
baseUrlInput.setAttribute("autocapitalize", "none");
baseUrlInput.setAttribute("autocorrect", "off");
}
if (apiKeyInput) apiKeyInput.placeholder = p.keyPh;
if (modelIdInput) {
modelIdInput.placeholder = p.model;
// Value only for OpenRouter; others cleared to encourage provider-specific model naming
if (provider === "openrouter") {
const savedId = (window.kimiDB && (await window.kimiDB.getPreference("llmModelId", ""))) || "";
modelIdInput.value = savedId || (window.kimiLLM ? window.kimiLLM.currentModel : "");
modelIdInput.readOnly = true;
modelIdInput.setAttribute("aria-readonly", "true");
modelIdInput.removeAttribute("data-soft-readonly");
} else {
modelIdInput.value = "";
modelIdInput.readOnly = false;
modelIdInput.setAttribute("aria-readonly", "false");
modelIdInput.setAttribute("data-soft-readonly", "true");
modelIdInput.setAttribute("readonly", "true");
modelIdInput.addEventListener(
"focus",
() => {
if (modelIdInput.hasAttribute("data-soft-readonly")) {
modelIdInput.removeAttribute("readonly");
}
},
{ once: true }
);
}
// Reassert anti-autofill attributes each switch
modelIdInput.autocomplete = "new-password";
modelIdInput.removeAttribute("name");
modelIdInput.setAttribute("data-lpignore", "true");
modelIdInput.setAttribute("data-1p-ignore", "true");
modelIdInput.setAttribute("data-bwignore", "true");
modelIdInput.setAttribute("data-form-type", "other");
modelIdInput.setAttribute("autocapitalize", "none");
modelIdInput.setAttribute("autocorrect", "off");
}
if (window.kimiDB) {
await window.kimiDB.setPreference("llmProvider", provider);
// Éviter d’écraser l’URL personnalisée pour custom/ollama
if (["openrouter", "openai", "groq", "together", "deepseek"].includes(provider)) {
const canonical = provider === "openrouter" ? "https://openrouter.ai/api/v1/chat/completions" : p.url;
await window.kimiDB.setPreference("llmBaseUrl", canonical);
} else {
const existing = await window.kimiDB.getPreference("llmBaseUrl", "");
await window.kimiDB.setPreference("llmBaseUrl", existing || p.url);
}
const apiKeyLabel = document.getElementById("api-key-label");
// Load provider-specific key into the input for clarity
const keyPrefMap = {
openrouter: "openrouterApiKey",
openai: "apiKey_openai",
groq: "apiKey_groq",
together: "apiKey_together",
deepseek: "apiKey_deepseek",
"openai-compatible": "apiKey_custom"
};
const keyPref = keyPrefMap[provider] || "llmApiKey";
const storedKey = await window.kimiDB.getPreference(keyPref, "");
if (apiKeyInput) apiKeyInput.value = storedKey || "";
const color = provider === "ollama" ? "#9e9e9e" : storedKey && storedKey.length > 0 ? "#4caf50" : "#9e9e9e";
ApiUi.setPresence(color);
// Changing provider invalidates previous test state
ApiUi.setTestPresence("#9e9e9e");
ApiUi.setTestEnabled(
provider === "ollama"
? true
: !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(storedKey || ""))
);
// Dynamic label per provider
if (apiKeyLabel) {
const labelByProvider = {
openrouter: "OpenRouter API Key",
openai: "OpenAI API Key",
groq: "Groq API Key",
together: "Together API Key",
deepseek: "DeepSeek API Key",
"openai-compatible": "API Key",
ollama: "API Key"
};
apiKeyLabel.textContent = labelByProvider[provider] || "API Key";
}
const savedBadge = ApiUi.savedBadge();
if (savedBadge) savedBadge.style.display = "none";
ApiUi.clearStatus();
}
});
}
const loadingScreen = document.getElementById("loading-screen");
if (loadingScreen) {
setTimeout(() => {
loadingScreen.style.opacity = "0";
setTimeout(() => {
loadingScreen.style.display = "none";
}, 500);
}, 1500);
}
let video1 = window.KimiDOMUtils.get("#video1");
let video2 = window.KimiDOMUtils.get("#video2");
if (!video1 || !video2) {
console.error("Video elements not found! Creating them...");
const videoContainer = document.querySelector(".video-container");
if (videoContainer) {
video1 = document.createElement("video");
video1.id = "video1";
video1.className = "bg-video active";
video1.autoplay = true;
video1.muted = true;
video1.playsinline = true;
video1.preload = "auto";
video1.innerHTML =
'Your browser does not support the video tag.';
video2 = document.createElement("video");
video2.id = "video2";
video2.className = "bg-video";
video2.autoplay = true;
video2.muted = true;
video2.playsinline = true;
video2.preload = "auto";
video2.innerHTML =
'Your browser does not support the video tag.';
videoContainer.appendChild(video1);
videoContainer.appendChild(video2);
}
}
let activeVideo = video1;
let inactiveVideo = video2;
kimiVideo = new KimiVideoManager(video1, video2);
await kimiVideo.init(kimiDB);
window.kimiVideo = kimiVideo;
if (video1 && video2 && kimiDB && kimiDB.getSelectedCharacter) {
try {
const selectedCharacter = await kimiDB.getSelectedCharacter();
if (selectedCharacter && window.KIMI_CHARACTERS) {
kimiVideo.setCharacter(selectedCharacter);
const folder = window.KIMI_CHARACTERS[selectedCharacter].videoFolder;
const neutralVideo = `${folder}neutral/neutral-gentle-breathing.mp4`;
const video1Source = video1.querySelector("source");
if (video1Source) {
video1Source.setAttribute("src", neutralVideo);
video1.load();
}
}
if (kimiVideo && kimiVideo.switchToContext) {
kimiVideo.switchToContext("neutral");
}
} catch (e) {
console.warn("Error loading initial video:", e);
}
}
async function attachCharacterSection() {
let saveCharacterBtn = window.KimiDOMUtils.get("#save-character-btn");
if (saveCharacterBtn) {
saveCharacterBtn.addEventListener("click", async e => {
const settingsPanel = window.KimiDOMUtils.get(".settings-panel");
let scrollTop = settingsPanel ? settingsPanel.scrollTop : null;
const characterGrid = window.KimiDOMUtils.get("#character-grid");
const selectedCard = characterGrid ? characterGrid.querySelector(".character-card.selected") : null;
if (!selectedCard) return;
const charKey = selectedCard.dataset.character;
const savedBadge = document.getElementById("api-key-saved");
if (savedBadge) {
savedBadge.textContent = (window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
savedBadge.style.display = "inline";
}
const promptInput = window.KimiDOMUtils.get(`#prompt-${charKey}`);
const prompt = promptInput ? promptInput.value : "";
await window.kimiDB.setSelectedCharacter(charKey);
await window.kimiDB.setSystemPromptForCharacter(charKey, prompt);
if (window.kimiVideo && window.kimiVideo.setCharacter) {
window.kimiVideo.setCharacter(charKey);
if (window.kimiVideo.switchToContext) {
window.kimiVideo.switchToContext("neutral");
}
}
if (window.voiceManager && window.voiceManager.updateSelectedCharacter) {
await window.voiceManager.updateSelectedCharacter();
}
if (window.kimiLLM && window.kimiLLM.setSystemPrompt) {
// Only manage system prompt here. API key editing is handled globally to avoid duplicates.
// Clear API status when Base URL or Model ID change
const baseUrlInputEl = ApiUi.baseUrlInput();
if (baseUrlInputEl) {
baseUrlInputEl.addEventListener("input", () => {
ApiUi.clearStatus();
});
}
const modelIdInputEl = ApiUi.modelIdInput();
if (modelIdInputEl) {
modelIdInputEl.addEventListener("input", () => {
ApiUi.clearStatus();
});
}
window.kimiLLM.setSystemPrompt(prompt);
}
const systemPromptInput = window.KimiDOMUtils.get("#system-prompt");
if (systemPromptInput) systemPromptInput.value = prompt;
await window.loadCharacterSection();
if (settingsPanel && scrollTop !== null) {
requestAnimationFrame(() => {
settingsPanel.scrollTop = scrollTop;
});
}
saveCharacterBtn.setAttribute("data-i18n", "saved");
saveCharacterBtn.classList.add("success");
saveCharacterBtn.disabled = true;
setTimeout(() => {
saveCharacterBtn.setAttribute("data-i18n", "save");
saveCharacterBtn.classList.remove("success");
saveCharacterBtn.disabled = false;
}, 1500);
});
}
let settingsButton2 = window.KimiDOMUtils.get("#settings-button");
if (settingsButton2) {
settingsButton2.addEventListener("click", window.loadCharacterSection);
}
}
await attachCharacterSection();
const chatContainer = document.getElementById("chat-container");
const chatButton = document.getElementById("chat-button");
const chatToggle = document.getElementById("chat-toggle");
const chatMessages = document.getElementById("chat-messages");
const chatInput = document.getElementById("chat-input");
const sendButton = document.getElementById("send-button");
const chatDelete = document.getElementById("chat-delete");
const waitingIndicator = document.getElementById("waiting-indicator");
if (!chatContainer || !chatButton || !chatMessages) {
console.error("Critical chat elements missing from DOM");
return;
}
window.kimiOverlayManager = new window.KimiOverlayManager();
chatButton.addEventListener("click", () => {
window.kimiOverlayManager.toggle("chat-container");
if (window.kimiOverlayManager.isOpen("chat-container")) {
window.loadChatHistory();
}
});
if (chatToggle) {
chatToggle.addEventListener("click", () => {
window.kimiOverlayManager.close("chat-container");
});
}
// Setup chat input and send button event listeners
if (sendButton) {
sendButton.addEventListener("click", () => {
if (typeof window.sendMessage === "function") {
window.sendMessage();
} else {
console.error("sendMessage function not available");
}
});
console.log("Send button event listener attached");
} else {
console.error("Send button not found");
}
if (chatInput) {
chatInput.addEventListener("keydown", e => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
if (typeof window.sendMessage === "function") {
window.sendMessage();
} else {
console.error("sendMessage function not available");
}
}
});
console.log("Chat input event listener attached");
} else {
console.error("Chat input not found");
}
const settingsOverlay = document.getElementById("settings-overlay");
const settingsButton = document.getElementById("settings-button");
const settingsClose = document.getElementById("settings-close");
const helpOverlay = document.getElementById("help-overlay");
const helpButton = document.getElementById("help-button");
const helpClose = document.getElementById("help-close");
if (!settingsButton || !helpButton) {
console.error("Critical UI buttons missing from DOM");
return;
}
helpButton.addEventListener("click", () => {
window.kimiOverlayManager.open("help-overlay");
});
if (helpClose) {
helpClose.addEventListener("click", () => {
window.kimiOverlayManager.close("help-overlay");
});
}
settingsButton.addEventListener("click", () => {
window.kimiOverlayManager.open("settings-overlay");
// Prevent multiple settings loading
if (!window._settingsLoading) {
window._settingsLoading = true;
window.loadSettingsData();
setTimeout(() => {
window.updateTabsScrollIndicator();
if (window.initializeAllSliders) window.initializeAllSliders();
if (window.syncLLMMaxTokensSlider) window.syncLLMMaxTokensSlider();
if (window.syncLLMTemperatureSlider) window.syncLLMTemperatureSlider();
if (window.setupSettingsListeners) window.setupSettingsListeners(window.kimiDB, window.kimiMemory);
if (window.syncPersonalityTraits) window.syncPersonalityTraits();
if (window.ensureVideoContextConsistency) window.ensureVideoContextConsistency();
// Only retry loading models if not already done
if (window.loadAvailableModels && !loadAvailableModels._loading) {
setTimeout(() => window.loadAvailableModels(), 100);
}
window._settingsLoading = false;
}, 200);
}
});
if (settingsClose) {
settingsClose.addEventListener("click", () => {
window.kimiOverlayManager.close("settings-overlay");
});
}
// Initialisation unifiée de la gestion des tabs
window.kimiTabManager = new window.KimiTabManager({
onTabChange: async tabName => {
if (tabName === "llm" || tabName === "api") {
if (window.kimiDB) {
const selectedCharacter = await window.kimiDB.getSelectedCharacter();
const prompt = await window.kimiDB.getSystemPromptForCharacter(selectedCharacter);
const systemPromptInput = document.getElementById("system-prompt");
if (systemPromptInput) systemPromptInput.value = prompt;
}
}
if (tabName === "personality") {
await window.loadCharacterSection();
}
}
});
window.kimiUIEventManager = new window.KimiUIEventManager();
window.kimiUIEventManager.addEvent(window, "resize", window.updateTabsScrollIndicator);
const saveSystemPromptButton = document.getElementById("save-system-prompt");
if (saveSystemPromptButton) {
saveSystemPromptButton.addEventListener("click", async () => {
const selectedCharacter = await window.kimiDB.getPreference("selectedCharacter", "kimi");
const systemPromptInput = document.getElementById("system-prompt");
if (systemPromptInput && window.kimiDB.setSystemPromptForCharacter) {
await window.kimiDB.setSystemPromptForCharacter(selectedCharacter, systemPromptInput.value);
if (window.kimiLLM && window.kimiLLM.setSystemPrompt) window.kimiLLM.setSystemPrompt(systemPromptInput.value);
const originalText = saveSystemPromptButton.textContent;
saveSystemPromptButton.textContent = "Saved!";
saveSystemPromptButton.classList.add("success");
saveSystemPromptButton.disabled = true;
setTimeout(() => {
saveSystemPromptButton.setAttribute("data-i18n", "save");
applyTranslations();
// Re-enable the button after the success feedback
saveSystemPromptButton.disabled = false;
saveSystemPromptButton.classList.remove("success");
// Ensure text reflects i18n "save" state
// (applyTranslations above will set the text from locale)
}, 1500);
}
});
}
const resetSystemPromptButton = document.getElementById("reset-system-prompt");
const systemPromptInput = document.getElementById("system-prompt");
if (resetSystemPromptButton) {
resetSystemPromptButton.addEventListener("click", async () => {
const selectedCharacter = await window.kimiDB.getPreference("selectedCharacter", "kimi");
if (systemPromptInput && window.kimiDB && window.kimiLLM) {
await window.kimiDB.setSystemPromptForCharacter(selectedCharacter, DEFAULT_SYSTEM_PROMPT);
systemPromptInput.value = DEFAULT_SYSTEM_PROMPT;
window.kimiLLM.setSystemPrompt(DEFAULT_SYSTEM_PROMPT);
resetSystemPromptButton.textContent = "Reset!";
resetSystemPromptButton.classList.add("animated");
resetSystemPromptButton.setAttribute("data-i18n", "reset_done");
applyTranslations();
setTimeout(() => {
resetSystemPromptButton.setAttribute("data-i18n", "reset_to_default");
applyTranslations();
}, 1500);
// After a reset, allow saving again
if (saveSystemPromptButton) {
saveSystemPromptButton.disabled = false;
saveSystemPromptButton.classList.remove("success");
saveSystemPromptButton.setAttribute("data-i18n", "save");
applyTranslations();
}
}
});
}
// Enable the Save button whenever the prompt content changes
if (systemPromptInput && saveSystemPromptButton) {
systemPromptInput.addEventListener("input", () => {
if (saveSystemPromptButton.disabled) {
saveSystemPromptButton.disabled = false;
}
saveSystemPromptButton.classList.remove("success");
saveSystemPromptButton.setAttribute("data-i18n", "save");
applyTranslations();
});
}
window.kimiFormManager = new window.KimiFormManager({ db: window.kimiDB, memory: window.kimiMemory });
const testVoiceButton = document.getElementById("test-voice");
if (testVoiceButton) {
testVoiceButton.addEventListener("click", () => {
if (voiceManager) {
const rate = parseFloat(document.getElementById("voice-rate").value);
const pitch = parseFloat(document.getElementById("voice-pitch").value);
const volume = parseFloat(document.getElementById("voice-volume").value);
if (window.kimiMemory.preferences) {
window.kimiMemory.preferences.voiceRate = rate;
window.kimiMemory.preferences.voicePitch = pitch;
window.kimiMemory.preferences.voiceVolume = volume;
}
const testMessage =
window.kimiI18nManager?.t("voice_test_message") ||
"Hello my love! Here is my new voice configured with all the settings! Do you like it?";
voiceManager.speak(testMessage, {
rate,
pitch,
volume
});
} else {
console.warn("Voice manager not initialized");
}
});
}
const testApiButton = document.getElementById("test-api");
if (testApiButton) {
testApiButton.addEventListener("click", async () => {
const statusSpan = ApiUi.statusSpan();
const apiKeyInput = ApiUi.apiKeyInput();
const apiKey = apiKeyInput ? apiKeyInput.value.trim() : "";
const providerSelect = ApiUi.providerSelect();
const baseUrlInput = ApiUi.baseUrlInput();
const modelIdInput = ApiUi.modelIdInput();
const provider = providerSelect ? providerSelect.value : "openrouter";
const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : "";
const modelId = modelIdInput ? modelIdInput.value.trim() : "";
if (!statusSpan) return;
if (provider !== "ollama" && !apiKey) {
statusSpan.textContent = window.kimiI18nManager?.t("api_key_missing") || "API key missing";
statusSpan.style.color = "#ff6b6b";
return;
}
// Validate API key format before saving/testing
if (provider !== "ollama") {
const isValid = (window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(apiKey)) || false;
if (!isValid) {
statusSpan.textContent =
window.kimiI18nManager?.t("api_key_invalid_format") ||
"Invalid API key format (must start with sk-or-v1-)";
statusSpan.style.color = "#ff6b6b";
return;
}
}
if (window.kimiDB) {
// Save API key under provider-specific preference key (skip for Ollama)
if (provider !== "ollama") {
const keyPrefMap = {
openrouter: "openrouterApiKey",
openai: "apiKey_openai",
groq: "apiKey_groq",
together: "apiKey_together",
deepseek: "apiKey_deepseek",
"openai-compatible": "apiKey_custom"
};
const keyPref = keyPrefMap[provider] || "llmApiKey";
await window.kimiDB.setPreference(keyPref, apiKey);
}
await window.kimiDB.setPreference("llmProvider", provider);
if (baseUrl) await window.kimiDB.setPreference("llmBaseUrl", baseUrl);
if (modelId) await window.kimiDB.setPreference("llmModelId", modelId);
}
statusSpan.textContent = "Testing in progress...";
statusSpan.style.color = "#ffa726";
try {
if (window.kimiLLM) {
let result;
if (provider === "openrouter") {
result = await window.kimiLLM.testModel(window.kimiLLM.currentModel, "Bonjour");
} else if (provider === "ollama") {
const response = await window.kimiLLM.chatWithLocal("Bonjour", { maxTokens: 2 });
result = { success: true, response };
} else {
const response = await window.kimiLLM.chatWithOpenAICompatible("Bonjour", { maxTokens: 2 });
result = { success: true, response };
}
if (result.success) {
statusSpan.textContent = "Connection successful!";
statusSpan.style.color = "#4caf50";
const savedBadge = ApiUi.savedBadge();
if (savedBadge) {
savedBadge.textContent =
(window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
savedBadge.style.display = "inline";
}
if (result.response) {
setTimeout(() => {
statusSpan.textContent = `Test response: \"${result.response.substring(0, 50)}...\"`;
}, 1000);
}
// Mark test success explicitly
ApiUi.setTestPresence("#4caf50");
} else {
statusSpan.textContent = `${result.error}`;
statusSpan.style.color = "#ff6b6b";
ApiUi.setTestPresence("#9e9e9e");
if (result.error.includes("similaires disponibles")) {
setTimeout(() => {}, 1000);
}
}
} else {
statusSpan.textContent = "LLM manager not initialized";
statusSpan.style.color = "#ff6b6b";
ApiUi.setTestPresence("#9e9e9e");
}
} catch (error) {
console.error("Error while testing API:", error);
statusSpan.textContent = `Error: ${error.message}`;
statusSpan.style.color = "#ff6b6b";
ApiUi.setTestPresence("#9e9e9e");
if (error.message.includes("non disponible")) {
setTimeout(() => {}, 1000);
}
}
});
}
// Global, single handler for API key input to save and update presence in real-time
(function setupApiKeyInputHandler() {
const input = ApiUi.apiKeyInput();
if (!input) return;
let t;
input.addEventListener("input", () => {
clearTimeout(t);
t = setTimeout(async () => {
const providerEl = ApiUi.providerSelect();
const provider = providerEl ? providerEl.value : "openrouter";
const keyPrefMap = {
openrouter: "openrouterApiKey",
openai: "apiKey_openai",
groq: "apiKey_groq",
together: "apiKey_together",
deepseek: "apiKey_deepseek",
"openai-compatible": "apiKey_custom"
};
const keyPref = keyPrefMap[provider] || "llmApiKey";
const value = input.value.trim();
// Update Test button state immediately
const validNow = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(value));
ApiUi.setTestEnabled(provider === "ollama" ? true : validNow);
if (window.kimiDB) {
try {
await window.kimiDB.setPreference(keyPref, value);
const savedBadge = ApiUi.savedBadge();
if (savedBadge) {
savedBadge.textContent =
(window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
savedBadge.style.display = value ? "inline" : "none";
}
ApiUi.setPresence(value ? "#4caf50" : "#9e9e9e");
// Any key change invalidates previous test state
ApiUi.setTestPresence("#9e9e9e");
ApiUi.clearStatus();
} catch (e) {
// Validation error from DB
const s = ApiUi.statusSpan();
if (s) {
s.textContent = e?.message || "Invalid API key";
s.style.color = "#ff6b6b";
}
ApiUi.setTestEnabled(false);
ApiUi.setTestPresence("#9e9e9e");
}
}
}, window.KIMI_SECURITY_CONFIG?.DEBOUNCE_DELAY || 300);
});
})();
// Harden anti-autofill and masking on DOM ready
(function enforceApiInputSecurity() {
const input = ApiUi.apiKeyInput();
if (!input) return;
// Anti-autofill flags
input.autocomplete = "off";
input.removeAttribute("name");
input.setAttribute("data-lpignore", "true");
input.setAttribute("data-1p-ignore", "true");
input.setAttribute("data-bwignore", "true");
input.setAttribute("data-form-type", "other");
input.setAttribute("autocapitalize", "none");
input.setAttribute("autocorrect", "off");
// Keep masked by style as well
if (input.classList.contains("masked")) {
try {
input.style.webkitTextSecurity = "disc";
} catch (_) {}
input.style.filter = "blur(6px)";
}
// If the field gets focus, ensure it is editable (index.html sets readonly initially)
input.addEventListener(
"focus",
() => {
if (input.hasAttribute("readonly")) input.removeAttribute("readonly");
},
{ once: false }
);
})();
// Toggle show/hide for API key
(function setupToggleEye() {
const btn = ApiUi.toggleBtn();
const input = ApiUi.apiKeyInput();
if (!btn || !input) return;
btn.addEventListener("click", () => {
const wasMasked = input.classList.contains("masked");
// Toggle class first
if (wasMasked) {
input.classList.remove("masked");
} else {
input.classList.add("masked");
}
// Force style to avoid UA quirks where -webkit-text-security persists
const nowMasked = input.classList.contains("masked");
if (nowMasked) {
// Apply masking explicitly
try {
input.style.webkitTextSecurity = "disc"; // Chromium/WebKit
} catch (_) {}
input.style.filter = "blur(6px)"; // Fallback
} else {
// Remove any residual masking
try {
input.style.webkitTextSecurity = ""; // reset to CSS/default
} catch (_) {}
input.style.filter = "none";
}
btn.setAttribute("aria-pressed", String(!nowMasked));
const icon = btn.querySelector("i");
if (icon) {
icon.classList.toggle("fa-eye");
icon.classList.toggle("fa-eye-slash");
}
btn.setAttribute("aria-label", nowMasked ? "Show API key" : "Hide API key");
});
})();
kimiInit.register(
"appearanceManager",
async () => {
const manager = new KimiAppearanceManager(window.kimiDB);
await manager.init();
window.kimiAppearanceManager = manager;
return manager;
},
[],
500
);
kimiInit.register(
"dataManager",
async () => {
const manager = new KimiDataManager(window.kimiDB);
await manager.init();
window.kimiDataManager = manager;
return manager;
},
[],
600
);
kimiInit.register(
"voiceManager",
async () => {
if (window.KimiVoiceManager) {
const manager = new KimiVoiceManager(window.kimiDB, window.kimiMemory);
const success = await manager.init();
if (success) {
manager.setOnSpeechAnalysis(window.analyzeAndReact);
return manager;
}
}
return null;
},
[],
1000
);
try {
await kimiInit.initializeAll();
window.voiceManager = kimiInit.getInstance("voiceManager");
window.kimiMemory.updateFavorabilityBar();
} catch (error) {
console.error("Initialization error:", error);
}
// Setup unified event handlers to prevent duplicates
setupUnifiedEventHandlers();
// Global hardening against browser/extension autofill on all text-like fields
(function hardenAgainstAutofill() {
const ATTRS = {
autocomplete: "off",
autocorrect: "off",
autocapitalize: "none",
spellcheck: "false",
"data-lpignore": "true",
"data-1p-ignore": "true",
"data-bwignore": "true",
"data-form-type": "other"
};
const block = el => {
if (!el || !(el instanceof HTMLElement)) return;
const tag = el.tagName.toLowerCase();
const type = (el.getAttribute("type") || "").toLowerCase();
if (tag === "input" && (type === "text" || type === "search" || type === "email" || type === "url")) {
Object.entries(ATTRS).forEach(([k, v]) => el.setAttribute(k, v));
// Never keep a sensitive name attribute
const name = el.getAttribute("name") || "";
if (name) el.removeAttribute("name");
}
if (tag === "textarea") {
Object.entries(ATTRS).forEach(([k, v]) => el.setAttribute(k, v));
const name = el.getAttribute("name") || "";
if (name) el.removeAttribute("name");
}
};
document.querySelectorAll("input, textarea").forEach(block);
const mo = new MutationObserver(muts => {
for (const m of muts) {
m.addedNodes &&
m.addedNodes.forEach(n => {
if (n instanceof HTMLElement) {
if (n.matches && (n.matches("input") || n.matches("textarea"))) block(n);
n.querySelectorAll && n.querySelectorAll("input, textarea").forEach(block);
}
});
if (m.type === "attributes" && m.target instanceof HTMLElement) {
if (m.attributeName === "type" || m.attributeName === "name") block(m.target);
}
}
});
mo.observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["type", "name"]
});
})();
// Initialize language and UI
await initializeLanguageAndUI();
// Setup message handling
setupMessageHandling();
// Function definitions
function setupUnifiedEventHandlers() {
// Cleanup existing event handlers
if (window._kimiEventCleanup && Array.isArray(window._kimiEventCleanup)) {
window._kimiEventCleanup.forEach(cleanup => {
if (typeof cleanup === "function") cleanup();
});
}
window._kimiEventCleanup = [];
// Helper function to safely add event listeners
function safeAddEventListener(element, event, handler, identifier) {
if (element && !element[identifier]) {
element.addEventListener(event, handler);
element[identifier] = true;
window._kimiEventCleanup.push(() => {
element.removeEventListener(event, handler);
element[identifier] = false;
});
}
}
// Chat event handlers
const chatDelete = document.getElementById("chat-delete");
if (chatDelete) {
const handler = async () => {
if (confirm("Do you really want to delete all chat messages? This cannot be undone.")) {
const chatMessages = document.getElementById("chat-messages");
if (chatMessages) {
chatMessages.textContent = "";
}
if (window.kimiDB && window.kimiDB.db) {
try {
await window.kimiDB.db.conversations.clear();
} catch (error) {
console.error("Error deleting conversations:", error);
}
}
}
};
safeAddEventListener(chatDelete, "click", handler, "_kimiChatDeleteHandlerAttached");
}
}
async function initializeLanguageAndUI() {
// Language initialization
window.kimiI18nManager = new window.KimiI18nManager();
const lang = await kimiDB.getPreference("selectedLanguage", "en");
await window.kimiI18nManager.setLanguage(lang);
const langSelect = document.getElementById("language-selection");
if (langSelect) {
langSelect.value = lang;
langSelect.addEventListener("change", async function (e) {
const selectedLang = e.target.value;
await kimiDB.setPreference("selectedLanguage", selectedLang);
await window.kimiI18nManager.setLanguage(selectedLang);
if (window.voiceManager && window.voiceManager.handleLanguageChange) {
await window.voiceManager.handleLanguageChange({ target: { value: selectedLang } });
}
if (window.kimiLLM && window.kimiLLM.setSystemPrompt && kimiDB) {
const selectedCharacter = await kimiDB.getPreference("selectedCharacter", "kimi");
let prompt = await kimiDB.getSystemPromptForCharacter(selectedCharacter);
let langInstruction;
switch (selectedLang) {
case "fr":
langInstruction = "Always reply exclusively in French. Do not mix languages.";
break;
case "es":
langInstruction = "Always reply exclusively in Spanish. Do not mix languages.";
break;
case "de":
langInstruction = "Always reply exclusively in German. Do not mix languages.";
break;
case "it":
langInstruction = "Always reply exclusively in Italian. Do not mix languages.";
break;
case "ja":
langInstruction = "Always reply exclusively in Japanese. Do not mix languages.";
break;
case "zh":
langInstruction = "Always reply exclusively in Chinese. Do not mix languages.";
break;
default:
langInstruction = "Always reply exclusively in English. Do not mix languages.";
break;
}
if (prompt) {
prompt = langInstruction + "\n" + prompt;
} else {
prompt = langInstruction;
}
window.kimiLLM.setSystemPrompt(prompt);
const systemPromptInput = document.getElementById("system-prompt");
if (systemPromptInput) systemPromptInput.value = prompt;
}
});
}
window.kimiUIStateManager = new window.KimiUIStateManager();
}
function setupMessageHandling() {
// Chat event handlers are already attached in the main script
// No need to reattach them here to avoid duplicates
}
// Add personality change listener
window.addEventListener("personalityUpdated", async event => {
const { character, traits } = event.detail;
console.log(`🧠Personality updated for ${character}:`, traits);
// Update video context based on new traits
if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
window.kimiVideo.setMoodByPersonality(traits);
}
// Update voice modulation if available
if (window.voiceManager && window.voiceManager.updatePersonalityModulation) {
window.voiceManager.updatePersonalityModulation(traits);
}
// Update UI elements that depend on personality
// Favorability bar will be updated by KimiMemory system
});
// Add global keyboard event listener for microphone toggle (F8)
let f8KeyPressed = false;
document.addEventListener("keydown", function (event) {
// Check if F8 key is pressed and no input field is focused
if (event.key === "F8" && !f8KeyPressed) {
f8KeyPressed = true;
const activeElement = document.activeElement;
const isInputFocused =
activeElement &&
(activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable);
// Only trigger if no input field is focused
if (!isInputFocused && window.voiceManager && window.voiceManager.toggleMicrophone) {
event.preventDefault();
window.voiceManager.toggleMicrophone();
}
}
});
document.addEventListener("keyup", function (event) {
if (event.key === "F8") {
f8KeyPressed = false;
}
});
// Monitor for consistency and errors
setInterval(async () => {
if (window.ensureVideoContextConsistency) {
await window.ensureVideoContextConsistency();
}
}, 30000); // Check every 30 seconds
});