Spaces:
Running
Running
import KimiDatabase from "./kimi-database.js"; | |
import KimiLLMManager from "./kimi-llm-manager.js"; | |
import KimiEmotionSystem from "./kimi-emotion-system.js"; | |
import KimiMemorySystem from "./kimi-memory-system.js"; | |
import KimiMemory from "./kimi-memory.js"; | |
document.addEventListener("DOMContentLoaded", async function () { | |
const DEFAULT_SYSTEM_PROMPT = window.DEFAULT_SYSTEM_PROMPT; | |
let kimiDB = null; | |
let kimiLLM = null; | |
let isSystemReady = false; | |
// Global debug flag for sync/log verbosity (default: false) | |
if (typeof window.KIMI_DEBUG_SYNC === "undefined") { | |
window.KIMI_DEBUG_SYNC = 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}`); | |
} | |
kimiLLM = new KimiLLMManager(kimiDB); | |
window.kimiLLM = kimiLLM; | |
await kimiLLM.init(); | |
// Initialize unified emotion system | |
window.kimiEmotionSystem = new KimiEmotionSystem(kimiDB); | |
// Initialize the new memory system | |
window.kimiMemorySystem = new 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; | |
// API config UI will be initialized after ApiUi is defined | |
if (window.refreshAllSliders) { | |
try { | |
await window.refreshAllSliders(); | |
} catch {} | |
} | |
} 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("provider-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 currentVal = (ApiUi.apiKeyInput() || {}).value || ""; | |
const colorInit = currentVal && currentVal.length > 0 ? "#4caf50" : "#9e9e9e"; | |
ApiUi.setPresence(colorInit); | |
// On load, test status is unknown | |
ApiUi.setTestPresence("#9e9e9e"); | |
} | |
// 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(); | |
// Set base URL based on modifiability | |
if (baseUrlInput) { | |
const isModifiable = isUrlModifiable(provider); | |
baseUrlInput.value = baseUrl || ""; | |
baseUrlInput.disabled = !isModifiable; | |
baseUrlInput.style.opacity = isModifiable ? "1" : "0.6"; | |
} | |
// Only prefill model for OpenRouter, others should show placeholder only | |
if (modelIdInput) { | |
if (provider === "openrouter") { | |
if (!modelIdInput.value) modelIdInput.value = modelId; | |
} else { | |
modelIdInput.value = ""; | |
} | |
} | |
// Load the provider-specific key | |
const keyPref = window.KimiProviderUtils | |
? window.KimiProviderUtils.getKeyPrefForProvider(provider) | |
: "providerApiKey"; | |
const storedKey = await window.kimiDB.getPreference(keyPref, ""); | |
if (apiKeyInput) apiKeyInput.value = storedKey || ""; | |
ApiUi.setPresence(storedKey ? "#4caf50" : "#9e9e9e"); | |
ApiUi.setTestPresence("#9e9e9e"); | |
const savedBadge = ApiUi.savedBadge(); | |
if (savedBadge) { | |
// Show only if provider requires a key and key exists | |
if (provider !== "ollama" && storedKey) { | |
savedBadge.style.display = "inline"; | |
} else { | |
savedBadge.style.display = "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); | |
} | |
} | |
// Hydrate API config UI from DB after ApiUi is defined and function declared | |
initializeApiConfigUI(); | |
// Listen for model changes and update the UI only for OpenRouter | |
window.addEventListener("llmModelChanged", function (event) { | |
const modelIdInput = ApiUi.modelIdInput(); | |
const providerSelect = ApiUi.providerSelect(); | |
// Only update the field if current provider is OpenRouter | |
if (modelIdInput && event.detail && event.detail.id && providerSelect && providerSelect.value === "openrouter") { | |
modelIdInput.value = event.detail.id; | |
} | |
}); | |
// Helper function to check if URL is modifiable for current provider | |
function isUrlModifiable(provider) { | |
return provider === "openai-compatible" || provider === "ollama"; | |
} | |
const providerSelectEl = document.getElementById("llm-provider"); | |
if (providerSelectEl) { | |
providerSelectEl.addEventListener("change", async function (e) { | |
const provider = e.target.value; | |
const baseUrlInput = ApiUi.baseUrlInput(); | |
const modelIdInput = ApiUi.modelIdInput(); | |
const apiKeyInput = ApiUi.apiKeyInput(); | |
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; | |
// Only allow URL modification for custom and ollama providers | |
const isModifiable = isUrlModifiable(provider); | |
if (isModifiable) { | |
// For custom and ollama: load saved URL or use default | |
const savedUrl = await window.kimiDB.getPreference("llmBaseUrl", p.url); | |
baseUrlInput.value = savedUrl; | |
baseUrlInput.disabled = false; | |
baseUrlInput.style.opacity = "1"; | |
} else { | |
// For other providers: fixed URL, not modifiable | |
baseUrlInput.value = p.url; | |
baseUrlInput.disabled = true; | |
baseUrlInput.style.opacity = "0.6"; | |
} | |
} | |
if (apiKeyInput) { | |
apiKeyInput.placeholder = p.keyPh; | |
// Masquer/dΓ©sactiver le champ pour Ollama/local | |
if (provider === "ollama") { | |
apiKeyInput.value = ""; | |
apiKeyInput.disabled = true; | |
apiKeyInput.style.display = "none"; | |
} else { | |
apiKeyInput.disabled = false; | |
apiKeyInput.style.display = ""; | |
} | |
} | |
if (modelIdInput) { | |
modelIdInput.placeholder = p.model; | |
// Only populate the field for OpenRouter since those are the models we have in the list | |
// For other providers, user must manually enter the provider-specific model ID | |
modelIdInput.value = provider === "openrouter" && window.kimiLLM ? window.kimiLLM.currentModel : ""; | |
} | |
if (window.kimiDB) { | |
await window.kimiDB.setPreference("llmProvider", provider); | |
const apiKeyLabel = document.getElementById("api-key-label"); | |
// Load provider-specific key into the input for clarity | |
const keyPref = window.KimiProviderUtils | |
? window.KimiProviderUtils.getKeyPrefForProvider(provider) | |
: "providerApiKey"; | |
const storedKey = await window.kimiDB.getPreference(keyPref, ""); | |
if (apiKeyInput && provider !== "ollama") 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) { | |
apiKeyLabel.textContent = window.KimiProviderUtils | |
? window.KimiProviderUtils.getLabelForProvider(provider) | |
: "API Key"; | |
} | |
const savedBadge = ApiUi.savedBadge(); | |
if (savedBadge) { | |
if (provider !== "ollama" && storedKey) { | |
savedBadge.style.display = "inline"; | |
} else { | |
savedBadge.style.display = "none"; | |
} | |
} | |
ApiUi.clearStatus(); | |
// Save URL after all UI updates are complete | |
const isModifiable = isUrlModifiable(provider); | |
if (isModifiable && baseUrlInput) { | |
await window.kimiDB.setPreference("llmBaseUrl", baseUrlInput.value); | |
} else { | |
// For fixed providers, save the standard URL | |
await window.kimiDB.setPreference("llmBaseUrl", p.url); | |
} | |
} | |
}); | |
// Listen for model ID changes and update the current model | |
const modelIdInput = ApiUi.modelIdInput(); | |
if (modelIdInput) { | |
modelIdInput.addEventListener("blur", async function (e) { | |
const newModelId = e.target.value.trim(); | |
if (newModelId && window.kimiLLM && newModelId !== window.kimiLLM.currentModel) { | |
try { | |
await window.kimiLLM.setCurrentModel(newModelId); | |
} catch (error) { | |
console.warn("Failed to set model:", error.message); | |
// Reset to current model if setting failed | |
e.target.value = window.kimiLLM.currentModel || ""; | |
} | |
} | |
}); | |
} | |
// Listen for Base URL changes and save for modifiable providers | |
const baseUrlInput = ApiUi.baseUrlInput(); | |
if (baseUrlInput) { | |
baseUrlInput.addEventListener("blur", async function (e) { | |
const providerSelect = ApiUi.providerSelect(); | |
const provider = providerSelect ? providerSelect.value : "openrouter"; | |
const isModifiable = isUrlModifiable(provider); | |
if (isModifiable && window.kimiDB) { | |
const newUrl = e.target.value.trim(); | |
if (newUrl) { | |
try { | |
await window.kimiDB.setPreference("llmBaseUrl", newUrl); | |
} catch (error) { | |
console.warn("Failed to save base URL:", error.message); | |
} | |
} | |
} | |
}); | |
} | |
} | |
// Loading screen management | |
const hideLoadingScreen = () => { | |
const loadingScreen = document.getElementById("loading-screen"); | |
if (loadingScreen) { | |
loadingScreen.style.opacity = "0"; | |
setTimeout(() => { | |
loadingScreen.style.display = "none"; | |
}, 500); | |
} | |
}; | |
// Hide loading screen when resources are loaded | |
if (document.readyState === "complete") { | |
setTimeout(hideLoadingScreen, 1000); | |
} else { | |
window.addEventListener("load", () => { | |
setTimeout(hideLoadingScreen, 1000); | |
}); | |
} | |
// Use centralized video utilities | |
let video1 = window.KimiVideoManager.getVideoElement("#video1"); | |
let video2 = window.KimiVideoManager.getVideoElement("#video2"); | |
if (!video1 || !video2) { | |
const videoContainer = document.querySelector(".video-container"); | |
if (videoContainer) { | |
video1 = window.KimiVideoManager.createVideoElement("video1", "bg-video active"); | |
video2 = window.KimiVideoManager.createVideoElement("video2", "bg-video"); | |
videoContainer.appendChild(video1); | |
videoContainer.appendChild(video2); | |
} | |
} | |
let activeVideo = video1; | |
let inactiveVideo = video2; | |
kimiVideo = new window.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; | |
// Removed incorrect usage of the API key saved badge here. | |
// Character save should not toggle the API key saved indicator. | |
const promptInput = window.KimiDOMUtils.get(`#prompt-${charKey}`); | |
const prompt = promptInput ? promptInput.value : ""; | |
await window.kimiDB.setSelectedCharacter(charKey); | |
await window.kimiDB.setSystemPromptForCharacter(charKey, prompt); | |
// Ensure memory system uses the correct character | |
if (window.kimiMemorySystem) { | |
window.kimiMemorySystem.selectedCharacter = charKey; | |
} | |
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(); | |
} | |
await window.loadCharacterSection(); | |
if (settingsPanel && scrollTop !== null) { | |
requestAnimationFrame(() => { | |
settingsPanel.scrollTop = scrollTop; | |
}); | |
} | |
// Refresh memory tab after character selection | |
if (window.kimiMemoryUI && typeof window.kimiMemoryUI.updateMemoryStats === "function") { | |
await window.kimiMemoryUI.updateMemoryStats(); | |
} | |
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 === "personality") { | |
await window.loadCharacterSection(); | |
} | |
} | |
}); | |
window.kimiUIEventManager = new window.KimiUIEventManager(); | |
window.kimiUIEventManager.addEvent(window, "resize", window.updateTabsScrollIndicator); | |
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 keyPref = window.KimiProviderUtils | |
? window.KimiProviderUtils.getKeyPrefForProvider(provider) | |
: "providerApiKey"; | |
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) { | |
// Test API minimal et centralisΓ© pour tous les providers | |
const result = await window.kimiLLM.testApiKeyMinimal(modelId); | |
if (result.success) { | |
statusSpan.textContent = "Connection successful!"; | |
statusSpan.style.color = "#4caf50"; | |
// Only show saved badge if an actual non-empty API key is stored and provider requires one | |
const savedBadge = ApiUi.savedBadge(); | |
if (savedBadge) { | |
const apiKeyInputEl = ApiUi.apiKeyInput(); | |
const hasKey = apiKeyInputEl && apiKeyInputEl.value.trim().length > 0; | |
if (provider !== "ollama" && hasKey) { | |
savedBadge.textContent = | |
(window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved"; | |
savedBadge.style.display = "inline"; | |
} else { | |
savedBadge.style.display = "none"; | |
} | |
} | |
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 keyPref = window.KimiProviderUtils | |
? window.KimiProviderUtils.getKeyPrefForProvider(provider) | |
: "providerApiKey"; | |
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) { | |
if (value) { | |
savedBadge.textContent = | |
(window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved"; | |
savedBadge.style.display = "inline"; | |
} else { | |
savedBadge.style.display = "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); | |
}); | |
})(); | |
// Toggle show/hide for API key | |
(function setupToggleEye() { | |
const btn = ApiUi.toggleBtn(); | |
const input = ApiUi.apiKeyInput(); | |
if (!btn || !input) return; | |
btn.addEventListener("click", () => { | |
const showing = input.type === "text"; | |
input.type = showing ? "password" : "text"; | |
btn.setAttribute("aria-pressed", String(!showing)); | |
const icon = btn.querySelector("i"); | |
if (icon) { | |
icon.classList.toggle("fa-eye"); | |
icon.classList.toggle("fa-eye-slash"); | |
} | |
btn.setAttribute("aria-label", showing ? "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(); | |
// Initialize language and UI | |
await initializeLanguageAndUI(); | |
// Setup message handling | |
setupMessageHandling(); | |
// Function definitions | |
function setupUnifiedEventHandlers() { | |
// SIMPLE FIX: Initialize _kimiEventCleanup to prevent undefined error | |
if (!window._kimiEventCleanup) { | |
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; | |
// Simple cleanup system | |
const cleanupFn = () => { | |
element.removeEventListener(event, handler); | |
element[identifier] = false; | |
}; | |
// Store cleanup function in the simple array | |
window._kimiEventCleanup.push(cleanupFn); | |
} | |
} | |
// 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); | |
// Note: Language selector event listener is now handled by VoiceManager.setupLanguageSelector() | |
// This prevents duplicate event listeners and ensures proper coordination between voice and i18n systems | |
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 | |
} | |
// ==== BATCHED EVENT AGGREGATOR (personality + preferences) ==== | |
const batchedUpdates = { | |
personality: null, | |
preferences: new Set() | |
}; | |
let batchTimer = null; | |
function scheduleFlush() { | |
if (batchTimer) return; | |
batchTimer = setTimeout(flushBatchedUpdates, 100); // 100ms coalescing window | |
} | |
async function flushBatchedUpdates() { | |
const personalityPayload = batchedUpdates.personality; | |
const prefKeys = Array.from(batchedUpdates.preferences); | |
batchedUpdates.personality = null; | |
batchedUpdates.preferences.clear(); | |
batchTimer = null; | |
// Apply personality update once (last-wins) | |
if (personalityPayload) { | |
const { character, traits } = personalityPayload; | |
const defaults = (window.getTraitDefaults && window.getTraitDefaults()) || { | |
affection: 55, // Lowered to match emotion system defaults | |
romance: 50, | |
empathy: 75, | |
playfulness: 55, | |
humor: 60, | |
intelligence: 70 | |
}; | |
// Prefer persisted DB traits over defaults to avoid temporary inconsistencies. | |
let dbTraits = null; | |
try { | |
if (window.kimiDB && typeof window.kimiDB.getAllPersonalityTraits === "function") { | |
dbTraits = await window.kimiDB.getAllPersonalityTraits(character || null); | |
} | |
} catch (e) { | |
dbTraits = null; | |
} | |
const baseline = { ...defaults, ...(dbTraits || {}) }; | |
const safeTraits = {}; | |
for (const key of Object.keys(defaults)) { | |
// If incoming payload provides the key, use it; otherwise use baseline (DB -> defaults) | |
let raw = Object.prototype.hasOwnProperty.call(traits || {}, key) ? traits[key] : baseline[key]; | |
let v = Number(raw); | |
if (!isFinite(v) || isNaN(v)) v = Number(baseline[key]); | |
v = Math.max(0, Math.min(100, v)); | |
safeTraits[key] = v; | |
} | |
if (window.KIMI_DEBUG_SYNC) { | |
console.log(`π§ (Batched) Personality updated for ${character}:`, safeTraits); | |
} | |
// Centralize side-effects elsewhere; aggregator remains a coalesced logger only. | |
} | |
// Preference keys batch (currently UI refresh for sliders already handled elsewhere) | |
if (prefKeys.length > 0) { | |
// Potential future hook: log or perform aggregated operations | |
// console.log("βοΈ Batched preference keys:", prefKeys); | |
} | |
} | |
// Also listen to the DB-wrapped event name to preserve batched logging | |
window.addEventListener("personality:updated", event => { | |
batchedUpdates.personality = event.detail; // last event wins | |
scheduleFlush(); | |
}); | |
window.addEventListener("preferenceUpdated", event => { | |
if (event.detail?.key) batchedUpdates.preferences.add(event.detail.key); | |
scheduleFlush(); | |
}); | |
// 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(); | |
} | |
} | |
}); | |
// Refresh sliders when character or language preference changes | |
window.addEventListener("preferenceUpdated", evt => { | |
const k = evt.detail?.key; | |
if (!k) return; | |
if (k === "selectedCharacter" || k === "selectedLanguage") { | |
if (window.refreshAllSliders) { | |
setTimeout(() => window.refreshAllSliders(), 50); | |
} | |
} | |
}); | |
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 | |
// Personality sync: global event and wrappers | |
(function setupPersonalitySync() { | |
// Guard to avoid multiple initializations | |
if (window._kimiPersonalitySyncReady) return; | |
window._kimiPersonalitySyncReady = true; | |
const dispatchUpdated = async (partialTraits, characterHint = null) => { | |
try { | |
const character = characterHint || (window.kimiDB && (await window.kimiDB.getSelectedCharacter())) || null; | |
window.dispatchEvent( | |
new CustomEvent("personality:updated", { | |
detail: { character, traits: { ...partialTraits } } | |
}) | |
); | |
} catch (e) {} | |
}; | |
const tryWrapDB = () => { | |
const db = window.kimiDB; | |
if (!db) return false; | |
const wrapOnce = (obj, methodName, buildTraitsFromArgs) => { | |
if (!obj || typeof obj[methodName] !== "function") return; | |
if (obj[methodName]._kimiWrapped) return; | |
const original = obj[methodName].bind(obj); | |
obj[methodName] = async function (...args) { | |
const res = await original(...args); | |
try { | |
const { traits, character } = await buildTraitsFromArgs(args, res); | |
if (traits && Object.keys(traits).length > 0) { | |
await dispatchUpdated(traits, character); | |
} | |
} catch (e) {} | |
return res; | |
}; | |
obj[methodName]._kimiWrapped = true; | |
}; | |
// setPersonalityTrait(trait, value, character?) | |
wrapOnce(db, "setPersonalityTrait", async args => { | |
const [trait, value, character] = args; | |
return { traits: { [String(trait)]: Number(value) }, character: character || null }; | |
}); | |
// setPersonalityBatch(traitsObj, character?) | |
wrapOnce(db, "setPersonalityBatch", async args => { | |
const [traitsObj, character] = args; | |
const traits = {}; | |
if (traitsObj && typeof traitsObj === "object") { | |
for (const [k, v] of Object.entries(traitsObj)) { | |
traits[String(k)] = Number(v); | |
} | |
} | |
return { traits, character: character || null }; | |
}); | |
// savePersonality(personalityObj, character?) | |
wrapOnce(db, "savePersonality", async args => { | |
const [personalityObj, character] = args; | |
const traits = {}; | |
if (personalityObj && typeof personalityObj === "object") { | |
for (const [k, v] of Object.entries(personalityObj)) { | |
traits[String(k)] = Number(v); | |
} | |
} | |
return { traits, character: character || null }; | |
}); | |
return true; | |
}; | |
// Try immediately and then retry a few times if DB not yet ready | |
if (!tryWrapDB()) { | |
let attempts = 0; | |
const maxAttempts = 20; | |
const interval = setInterval(() => { | |
attempts++; | |
if (tryWrapDB() || attempts >= maxAttempts) { | |
clearInterval(interval); | |
} | |
}, 250); | |
} | |
// Central listener: debounce UI/video sync to avoid thrashing | |
let syncTimer = null; | |
let lastTraits = {}; | |
window.addEventListener("personality:updated", async e => { | |
try { | |
if (e && e.detail && e.detail.traits) { | |
// Merge incremental updates | |
lastTraits = { ...lastTraits, ...e.detail.traits }; | |
} | |
} catch {} | |
if (syncTimer) clearTimeout(syncTimer); | |
syncTimer = setTimeout(async () => { | |
try { | |
const db = window.kimiDB; | |
const character = (e && e.detail && e.detail.character) || (db && (await db.getSelectedCharacter())) || null; | |
let traits = lastTraits; | |
if (!traits || Object.keys(traits).length === 0) { | |
// Fallback: fetch all traits if partial not provided | |
traits = db && (await db.getAllPersonalityTraits(character)); | |
} | |
// 1) Update UI sliders if available | |
if (typeof window.updateSlider === "function" && traits) { | |
for (const [trait, value] of Object.entries(traits)) { | |
const id = `trait-${trait}`; | |
if (document.getElementById(id)) { | |
try { | |
window.updateSlider(id, value); | |
} catch {} | |
} | |
} | |
} | |
if (typeof window.syncPersonalityTraits === "function") { | |
try { | |
await window.syncPersonalityTraits(character); | |
} catch {} | |
} | |
// 2) Update memory cache affection bar if available | |
if (window.kimiMemory && typeof window.kimiMemory.updateAffectionTrait === "function") { | |
try { | |
await window.kimiMemory.updateAffectionTrait(); | |
} catch {} | |
} | |
// 3) Update video mood by personality | |
if (window.kimiVideo && typeof window.kimiVideo.setMoodByPersonality === "function") { | |
const allTraits = | |
traits && Object.keys(traits).length > 0 | |
? { ...traits } | |
: (db && (await db.getAllPersonalityTraits(character))) || {}; | |
try { | |
window.kimiVideo.setMoodByPersonality(allTraits); | |
} catch {} | |
// 3b) Update voice modulation based on personality | |
try { | |
if (window.voiceManager && typeof window.voiceManager.updatePersonalityModulation === "function") { | |
window.voiceManager.updatePersonalityModulation(allTraits); | |
} | |
} catch {} | |
} | |
// 4) Ensure current video context is valid (lightweight guard) | |
let beforeInfo = null; | |
try { | |
if (window.kimiVideo && typeof window.kimiVideo.getCurrentVideoInfo === "function") { | |
beforeInfo = window.kimiVideo.getCurrentVideoInfo(); | |
} | |
} catch {} | |
if (typeof window.ensureVideoContextConsistency === "function") { | |
try { | |
await window.ensureVideoContextConsistency(); | |
} catch {} | |
} | |
try { | |
if ( | |
window.KIMI_DEBUG_SYNC && | |
window.kimiVideo && | |
typeof window.kimiVideo.getCurrentVideoInfo === "function" | |
) { | |
const afterInfo = window.kimiVideo.getCurrentVideoInfo(); | |
if ( | |
beforeInfo && | |
afterInfo && | |
(beforeInfo.context !== afterInfo.context || | |
beforeInfo.emotion !== afterInfo.emotion || | |
beforeInfo.category !== afterInfo.category) | |
) { | |
console.log("π§ SyncGuard: corrected video context", { from: beforeInfo, to: afterInfo }); | |
} | |
} | |
} catch {} | |
} catch { | |
} finally { | |
lastTraits = {}; | |
} | |
}, 120); // small debounce | |
}); | |
})(); | |
}); | |