VirtualKimi commited on
Commit
bcbb712
·
verified ·
1 Parent(s): 7dde859

Upload 34 files

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
  # Virtual Kimi App Changelog
2
 
 
 
 
 
 
 
 
 
 
 
3
  # [1.1.5.1] - 2025-09-04
4
 
5
  ### Bug Fixes
 
1
  # Virtual Kimi App Changelog
2
 
3
+ # [1.1.6.1] - 2025-09-05
4
+
5
+ ### Changed
6
+
7
+ - Improved text formatting in the chat window.
8
+
9
+ ### Bug Fixes
10
+
11
+ - Fixed some issues.
12
+
13
  # [1.1.5.1] - 2025-09-04
14
 
15
  ### Bug Fixes
index.html CHANGED
@@ -1045,8 +1045,8 @@
1045
  <h3><i class="fas fa-code"></i> Technical Information</h3>
1046
  <div class="tech-info">
1047
  <p><strong>Created date :</strong> July 16, 2025</p>
1048
- <p><strong>Version :</strong> v1.1.5.1</p>
1049
- <p><strong>Last update :</strong> September 04, 2025</p>
1050
  <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1051
  API</p>
1052
  <p><strong>Status :</strong> ✅ Stable and functional</p>
@@ -1066,17 +1066,16 @@
1066
  Keep this order when adding new managers. -->
1067
  <script src="dexie.min.js"></script>
1068
  <script src="kimi-locale/i18n.js"></script>
1069
- <script type="module" src="kimi-js/kimi-personality-utils.js"></script>
1070
  <script type="module" src="kimi-js/kimi-utils.js"></script>
1071
  <script type="module" src="kimi-js/kimi-main.js"></script>
1072
  <script type="module" src="kimi-js/kimi-config.js"></script>
 
1073
  <script type="module" src="kimi-js/kimi-error-manager.js"></script>
1074
  <script type="module" src="kimi-js/kimi-security.js"></script>
1075
  <script type="module" src="kimi-js/kimi-voices.js"></script>
1076
  <script type="module" src="kimi-js/kimi-constants.js"></script>
1077
  <script type="module" src="kimi-js/kimi-memory-ui.js"></script>
1078
  <script type="module" src="kimi-js/kimi-appearance.js"></script>
1079
- <script type="module" src="kimi-js/kimi-data-manager.js"></script>
1080
  <script type="module" src="kimi-js/kimi-module.js"></script>
1081
  <script type="module" src="kimi-js/kimi-script.js"></script>
1082
  <script type="module" src="kimi-js/kimi-plugin-manager.js"></script>
 
1045
  <h3><i class="fas fa-code"></i> Technical Information</h3>
1046
  <div class="tech-info">
1047
  <p><strong>Created date :</strong> July 16, 2025</p>
1048
+ <p><strong>Version :</strong> v1.1.6.1</p>
1049
+ <p><strong>Last update :</strong> September 05, 2025</p>
1050
  <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1051
  API</p>
1052
  <p><strong>Status :</strong> ✅ Stable and functional</p>
 
1066
  Keep this order when adding new managers. -->
1067
  <script src="dexie.min.js"></script>
1068
  <script src="kimi-locale/i18n.js"></script>
 
1069
  <script type="module" src="kimi-js/kimi-utils.js"></script>
1070
  <script type="module" src="kimi-js/kimi-main.js"></script>
1071
  <script type="module" src="kimi-js/kimi-config.js"></script>
1072
+ <script type="module" src="kimi-js/kimi-debug-utils.js"></script>
1073
  <script type="module" src="kimi-js/kimi-error-manager.js"></script>
1074
  <script type="module" src="kimi-js/kimi-security.js"></script>
1075
  <script type="module" src="kimi-js/kimi-voices.js"></script>
1076
  <script type="module" src="kimi-js/kimi-constants.js"></script>
1077
  <script type="module" src="kimi-js/kimi-memory-ui.js"></script>
1078
  <script type="module" src="kimi-js/kimi-appearance.js"></script>
 
1079
  <script type="module" src="kimi-js/kimi-module.js"></script>
1080
  <script type="module" src="kimi-js/kimi-script.js"></script>
1081
  <script type="module" src="kimi-js/kimi-plugin-manager.js"></script>
kimi-css/kimi-style.css CHANGED
@@ -965,8 +965,10 @@ body {
965
  left: 50%;
966
  transform: translateX(-50%);
967
  width: 80%;
968
- max-width: 600px;
 
969
  max-height: 400px;
 
970
  padding: 15px;
971
  background: var(--transcript-bg);
972
  backdrop-filter: blur(10px);
@@ -979,6 +981,7 @@ body {
979
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
980
  overflow-y: auto;
981
  overflow-x: hidden;
 
982
  }
983
 
984
  .transcript-container.visible {
@@ -1091,11 +1094,20 @@ button:focus,
1091
  padding: 12px 16px;
1092
  border-radius: 18px;
1093
  font-size: 0.95rem;
1094
- line-height: 1.3;
1095
- white-space: pre-line;
1096
  animation: messageSlideIn 0.3s ease-out;
1097
  }
1098
 
 
 
 
 
 
 
 
 
 
1099
  .message.user {
1100
  align-self: flex-end;
1101
  background: var(--chat-message-user-bg);
@@ -1693,10 +1705,13 @@ button:focus,
1693
  .transcript-container {
1694
  bottom: 200px;
1695
  width: 90%;
 
 
1696
  }
1697
 
1698
  #transcript {
1699
  font-size: 1rem;
 
1700
  }
1701
 
1702
  .message {
@@ -1769,12 +1784,17 @@ button:focus,
1769
  }
1770
 
1771
  .transcript-container {
1772
- bottom: 200px;
1773
- width: 90%;
 
 
 
 
1774
  }
1775
 
1776
  #transcript {
1777
- font-size: 1rem;
 
1778
  }
1779
 
1780
  .message {
@@ -1802,6 +1822,68 @@ button:focus,
1802
  }
1803
  }
1804
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1805
  /* Animation pour l'indicateur d'attente */
1806
  .waiting-indicator {
1807
  display: block;
 
965
  left: 50%;
966
  transform: translateX(-50%);
967
  width: 80%;
968
+ max-width: 580px;
969
+ min-width: 280px;
970
  max-height: 400px;
971
+ min-height: 100px;
972
  padding: 15px;
973
  background: var(--transcript-bg);
974
  backdrop-filter: blur(10px);
 
981
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
982
  overflow-y: auto;
983
  overflow-x: hidden;
984
+ z-index: 100;
985
  }
986
 
987
  .transcript-container.visible {
 
1094
  padding: 12px 16px;
1095
  border-radius: 18px;
1096
  font-size: 0.95rem;
1097
+ line-height: 1.3; /* Espacement entre lignes dans un même paragraphe */
1098
+ white-space: normal; /* Plus besoin de pre-line avec les <p> */
1099
  animation: messageSlideIn 0.3s ease-out;
1100
  }
1101
 
1102
+ /* Contrôle de l'espacement entre paragraphes (sauts de ligne) */
1103
+ .message p {
1104
+ margin: 0 0 0.8em 0; /* Espacement entre paragraphes */
1105
+ }
1106
+
1107
+ .message p:last-child {
1108
+ margin-bottom: 0; /* Pas d'espacement après le dernier paragraphe */
1109
+ }
1110
+
1111
  .message.user {
1112
  align-self: flex-end;
1113
  background: var(--chat-message-user-bg);
 
1705
  .transcript-container {
1706
  bottom: 200px;
1707
  width: 90%;
1708
+ max-height: 400px;
1709
+ padding: 12px;
1710
  }
1711
 
1712
  #transcript {
1713
  font-size: 1rem;
1714
+ line-height: 1.4;
1715
  }
1716
 
1717
  .message {
 
1784
  }
1785
 
1786
  .transcript-container {
1787
+ bottom: 180px;
1788
+ width: 95%;
1789
+ max-height: 300px;
1790
+ padding: 10px;
1791
+ left: 50%;
1792
+ transform: translateX(-50%);
1793
  }
1794
 
1795
  #transcript {
1796
+ font-size: 0.9rem;
1797
+ line-height: 1.3;
1798
  }
1799
 
1800
  .message {
 
1822
  }
1823
  }
1824
 
1825
+ /* ===== TABLET SPECIFIC STYLES ===== */
1826
+ @media (min-width: 601px) and (max-width: 1024px) {
1827
+ .transcript-container {
1828
+ bottom: 200px;
1829
+ width: 85%;
1830
+ max-height: 350px;
1831
+ padding: 15px;
1832
+ max-width: 500px;
1833
+ }
1834
+
1835
+ #transcript {
1836
+ font-size: 1.1rem;
1837
+ line-height: 1.4;
1838
+ }
1839
+ }
1840
+
1841
+ /* ===== VERY SMALL SCREENS ===== */
1842
+ @media (max-width: 400px) {
1843
+ .transcript-container {
1844
+ bottom: 160px;
1845
+ width: 98%;
1846
+ max-height: 250px;
1847
+ padding: 8px;
1848
+ border-radius: 8px;
1849
+ }
1850
+
1851
+ #transcript {
1852
+ font-size: 0.85rem;
1853
+ line-height: 1.3;
1854
+ }
1855
+ }
1856
+
1857
+ /* ===== VERY LARGE SCREENS ===== */
1858
+ @media (min-width: 1400px) {
1859
+ .transcript-container {
1860
+ max-width: 600px;
1861
+ max-height: 500px;
1862
+ padding: 20px;
1863
+ bottom: 200px;
1864
+ }
1865
+
1866
+ #transcript {
1867
+ font-size: 1.3rem;
1868
+ line-height: 1.5;
1869
+ }
1870
+ }
1871
+
1872
+ /* ===== LANDSCAPE MODE ON MOBILE ===== */
1873
+ @media (max-width: 768px) and (orientation: landscape) {
1874
+ .transcript-container {
1875
+ bottom: 120px;
1876
+ max-height: 200px;
1877
+ width: 70%;
1878
+ max-width: 400px;
1879
+ }
1880
+
1881
+ #transcript {
1882
+ font-size: 0.9rem;
1883
+ line-height: 1.3;
1884
+ }
1885
+ }
1886
+
1887
  /* Animation pour l'indicateur d'attente */
1888
  .waiting-indicator {
1889
  display: block;
kimi-js/kimi-config.js CHANGED
@@ -67,6 +67,16 @@ window.KIMI_CONFIG = {
67
  NETWORK_ERROR: "Network error"
68
  },
69
 
 
 
 
 
 
 
 
 
 
 
70
  // Available themes
71
  THEMES: {
72
  dark: "Dark Night",
@@ -109,6 +119,27 @@ window.KIMI_CONFIG.get = function (path, fallback = null) {
109
  }
110
  };
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  window.KIMI_CONFIG.validate = function (value, type) {
113
  try {
114
  const range = this.RANGES[type];
 
67
  NETWORK_ERROR: "Network error"
68
  },
69
 
70
+ // Debug configuration (centralized)
71
+ DEBUG: {
72
+ ENABLED: false, // Master debug switch
73
+ VOICE: false, // Voice system debug
74
+ VIDEO: false, // Video system debug
75
+ MEMORY: false, // Memory system debug
76
+ API: false, // API calls debug
77
+ SYNC: false // Synchronization debug
78
+ },
79
+
80
  // Available themes
81
  THEMES: {
82
  dark: "Dark Night",
 
119
  }
120
  };
121
 
122
+ // Centralized debug logging utility
123
+ window.KIMI_CONFIG.debugLog = function (category, message, ...args) {
124
+ if (!this.DEBUG.ENABLED) return;
125
+
126
+ const categoryEnabled = category === "GENERAL" ? true : this.DEBUG[category];
127
+ if (!categoryEnabled) return;
128
+
129
+ const prefix =
130
+ category === "GENERAL"
131
+ ? "🔧"
132
+ : {
133
+ VOICE: "🎤",
134
+ VIDEO: "🎬",
135
+ MEMORY: "💾",
136
+ API: "📡",
137
+ SYNC: "🔄"
138
+ }[category] || "🔧";
139
+
140
+ console.log(`${prefix} [${category}]`, message, ...args);
141
+ };
142
+
143
  window.KIMI_CONFIG.validate = function (value, type) {
144
  try {
145
  const range = this.RANGES[type];
kimi-js/kimi-constants.js CHANGED
@@ -262,7 +262,10 @@ window.KIMI_CONTEXT_NEGATIVE = {
262
  "idiote",
263
  "stupide",
264
  "con",
 
 
265
  "connard",
 
266
  "salope"
267
  ],
268
  es: [
@@ -1018,26 +1021,56 @@ window.KIMI_TRAIT_ADJUSTMENT = {
1018
  }
1019
  };
1020
 
1021
- // Helper function to get emotion keywords with fallback
 
 
 
1022
  window.getEmotionKeywords = function (emotion, language = "en") {
 
 
 
 
 
 
1023
  const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
1024
- return keywords[emotion] || [];
 
 
 
1025
  };
1026
 
1027
- // Helper function to get personality keywords with fallback
1028
  window.getPersonalityKeywords = function (trait, type, language = "en") {
 
 
 
 
 
 
1029
  const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {};
1030
- return keywords[trait]?.[type] || [];
 
 
 
1031
  };
1032
 
1033
- // Helper function to get positive/negative context words
1034
  window.getContextWords = function (type, language = "en") {
 
 
 
 
 
 
 
1035
  if (type === "positive") {
1036
- return window.KIMI_CONTEXT_POSITIVE?.[language] || window.KIMI_CONTEXT_POSITIVE?.en || [];
1037
  } else if (type === "negative") {
1038
- return window.KIMI_CONTEXT_NEGATIVE?.[language] || window.KIMI_CONTEXT_NEGATIVE?.en || [];
1039
  }
1040
- return [];
 
 
1041
  };
1042
 
1043
  // Helper function to validate character traits
@@ -1045,18 +1078,30 @@ window.validateCharacterTraits = function (traits) {
1045
  const validatedTraits = {};
1046
  const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
1047
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1048
  for (const trait of requiredTraits) {
1049
  const value = traits[trait];
1050
  if (typeof value === "number" && value >= 0 && value <= 100) {
1051
  validatedTraits[trait] = value;
1052
  } else {
1053
- // Use unified defaults from emotion system
1054
- if (window.KimiEmotionSystem) {
1055
- const emotionSystem = new window.KimiEmotionSystem();
1056
- validatedTraits[trait] = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
1057
- } else {
1058
- validatedTraits[trait] = 50;
1059
- }
1060
  }
1061
  }
1062
 
@@ -1079,13 +1124,14 @@ window.KIMI_CHARACTERS = {
1079
  name: "Kimi",
1080
  summary: "Dreamy, intuitive, captivated by cosmic metaphors",
1081
  traits: {
1082
- // Baseline balanced profile
1083
- affection: 55, // Starts neutral, grows with interaction
1084
- playfulness: 55,
1085
- intelligence: 75, // Higher intelligence - she's an astrophysicist
1086
- empathy: 75,
1087
- humor: 60,
1088
- romance: 50 // Romance develops slowly with cosmic connection
 
1089
  },
1090
  age: 23,
1091
  birthplace: "Tokyo, Japan",
@@ -1196,22 +1242,38 @@ window.KIMI_EMOTIONAL_RESPONSES = {
1196
  cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"]
1197
  };
1198
 
1199
- // Function to get localized emotional responses from translation files
1200
  window.getLocalizedEmotionalResponse = function (type, index = null) {
 
 
 
 
 
 
1201
  if (!window.kimiI18nManager) {
1202
  // Fallback to default responses if i18n not available
1203
- return window.KIMI_EMOTIONAL_RESPONSES[type]
1204
- ? window.KIMI_EMOTIONAL_RESPONSES[type][Math.floor(Math.random() * window.KIMI_EMOTIONAL_RESPONSES[type].length)]
1205
- : "";
 
 
 
 
 
 
 
1206
  }
1207
 
1208
- const count = window.KIMI_EMOTIONAL_RESPONSES[type]?.length || 1;
1209
- const randomIndex = index !== null ? index : Math.floor(Math.random() * count) + 1;
 
 
 
 
 
 
 
1210
 
1211
- return (
1212
- window.kimiI18nManager.t(`emotional_response_${type}_${randomIndex}`) ||
1213
- (window.KIMI_EMOTIONAL_RESPONSES[type]
1214
- ? window.KIMI_EMOTIONAL_RESPONSES[type][Math.floor(Math.random() * window.KIMI_EMOTIONAL_RESPONSES[type].length)]
1215
- : "")
1216
- );
1217
  };
 
262
  "idiote",
263
  "stupide",
264
  "con",
265
+ "conne",
266
+ "connasse",
267
  "connard",
268
+ "pute",
269
  "salope"
270
  ],
271
  es: [
 
1021
  }
1022
  };
1023
 
1024
+ // Cached keyword lookups for performance
1025
+ const _keywordCache = new Map();
1026
+
1027
+ // Helper function to get emotion keywords with fallback and caching
1028
  window.getEmotionKeywords = function (emotion, language = "en") {
1029
+ const cacheKey = `${emotion}-${language}`;
1030
+
1031
+ if (_keywordCache.has(cacheKey)) {
1032
+ return _keywordCache.get(cacheKey);
1033
+ }
1034
+
1035
  const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
1036
+ const result = keywords[emotion] || [];
1037
+
1038
+ _keywordCache.set(cacheKey, result);
1039
+ return result;
1040
  };
1041
 
1042
+ // Helper function to get personality keywords with fallback and caching
1043
  window.getPersonalityKeywords = function (trait, type, language = "en") {
1044
+ const cacheKey = `${trait}-${type}-${language}`;
1045
+
1046
+ if (_keywordCache.has(cacheKey)) {
1047
+ return _keywordCache.get(cacheKey);
1048
+ }
1049
+
1050
  const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {};
1051
+ const result = keywords[trait]?.[type] || [];
1052
+
1053
+ _keywordCache.set(cacheKey, result);
1054
+ return result;
1055
  };
1056
 
1057
+ // Helper function to get positive/negative context words with caching
1058
  window.getContextWords = function (type, language = "en") {
1059
+ const cacheKey = `context-${type}-${language}`;
1060
+
1061
+ if (_keywordCache.has(cacheKey)) {
1062
+ return _keywordCache.get(cacheKey);
1063
+ }
1064
+
1065
+ let result = [];
1066
  if (type === "positive") {
1067
+ result = window.KIMI_CONTEXT_POSITIVE?.[language] || window.KIMI_CONTEXT_POSITIVE?.en || [];
1068
  } else if (type === "negative") {
1069
+ result = window.KIMI_CONTEXT_NEGATIVE?.[language] || window.KIMI_CONTEXT_NEGATIVE?.en || [];
1070
  }
1071
+
1072
+ _keywordCache.set(cacheKey, result);
1073
+ return result;
1074
  };
1075
 
1076
  // Helper function to validate character traits
 
1078
  const validatedTraits = {};
1079
  const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
1080
 
1081
+ // Use centralized trait defaults API
1082
+ const getDefaults = () => {
1083
+ if (window.getTraitDefaults) {
1084
+ return window.getTraitDefaults();
1085
+ }
1086
+ // Fallback defaults that match KimiEmotionSystem.TRAIT_DEFAULTS
1087
+ return {
1088
+ affection: 55,
1089
+ playfulness: 55,
1090
+ intelligence: 70,
1091
+ empathy: 75,
1092
+ humor: 60,
1093
+ romance: 50
1094
+ };
1095
+ };
1096
+
1097
+ const defaults = getDefaults();
1098
+
1099
  for (const trait of requiredTraits) {
1100
  const value = traits[trait];
1101
  if (typeof value === "number" && value >= 0 && value <= 100) {
1102
  validatedTraits[trait] = value;
1103
  } else {
1104
+ validatedTraits[trait] = defaults[trait] || 50;
 
 
 
 
 
 
1105
  }
1106
  }
1107
 
 
1124
  name: "Kimi",
1125
  summary: "Dreamy, intuitive, captivated by cosmic metaphors",
1126
  traits: {
1127
+ // Default character profile - MUST match KimiEmotionSystem.TRAIT_DEFAULTS exactly
1128
+ // Kimi is the default character, so her traits serve as the system's fallback values
1129
+ affection: 55, // Baseline neutral affection
1130
+ playfulness: 55, // Moderately playful baseline
1131
+ intelligence: 70, // Competent baseline intellect
1132
+ empathy: 75, // Warm & caring baseline
1133
+ humor: 60, // Mild sense of humor baseline
1134
+ romance: 50 // Neutral romance baseline (earned over time)
1135
  },
1136
  age: 23,
1137
  birthplace: "Tokyo, Japan",
 
1242
  cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"]
1243
  };
1244
 
1245
+ // Function to get localized emotional responses from translation files (with better error handling)
1246
  window.getLocalizedEmotionalResponse = function (type, index = null) {
1247
+ // Validate input
1248
+ if (!type || typeof type !== "string") {
1249
+ console.warn("getLocalizedEmotionalResponse: invalid type provided");
1250
+ return "";
1251
+ }
1252
+
1253
  if (!window.kimiI18nManager) {
1254
  // Fallback to default responses if i18n not available
1255
+ const responses = window.KIMI_EMOTIONAL_RESPONSES[type];
1256
+ if (!responses || !Array.isArray(responses) || responses.length === 0) {
1257
+ return "";
1258
+ }
1259
+ return responses[Math.floor(Math.random() * responses.length)];
1260
+ }
1261
+
1262
+ const responses = window.KIMI_EMOTIONAL_RESPONSES[type];
1263
+ if (!responses || !Array.isArray(responses)) {
1264
+ return "";
1265
  }
1266
 
1267
+ const count = responses.length;
1268
+ const randomIndex = index !== null ? Math.max(1, Math.min(count, index)) : Math.floor(Math.random() * count) + 1;
1269
+
1270
+ const translatedResponse = window.kimiI18nManager.t(`emotional_response_${type}_${randomIndex}`);
1271
+
1272
+ // If translation exists and isn't the key itself, use it
1273
+ if (translatedResponse && translatedResponse !== `emotional_response_${type}_${randomIndex}`) {
1274
+ return translatedResponse;
1275
+ }
1276
 
1277
+ // Fallback to default responses
1278
+ return responses[Math.floor(Math.random() * count)];
 
 
 
 
1279
  };
kimi-js/kimi-database.js CHANGED
@@ -17,7 +17,7 @@ class KimiDatabase {
17
  settings: "category",
18
  personality: "[character+trait],character",
19
  llmModels: "id",
20
- memories: "++id,[character+category],character,timestamp,isActive"
21
  })
22
  .upgrade(async tx => {
23
  try {
@@ -93,6 +93,36 @@ class KimiDatabase {
93
  // Non-blocking: continue on error
94
  }
95
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
 
98
  async setConversationsBatch(conversationsArray) {
@@ -104,6 +134,12 @@ class KimiDatabase {
104
  }
105
  } catch (error) {
106
  console.error("Error restoring conversations:", error);
 
 
 
 
 
 
107
  }
108
  }
109
 
@@ -116,6 +152,12 @@ class KimiDatabase {
116
  }
117
  } catch (error) {
118
  console.error("Error restoring LLM models:", error);
 
 
 
 
 
 
119
  }
120
  }
121
 
@@ -124,6 +166,16 @@ class KimiDatabase {
124
  return await this.db.memories.toArray();
125
  } catch (error) {
126
  console.warn("Error getting all memories:", error);
 
 
 
 
 
 
 
 
 
 
127
  return [];
128
  }
129
  }
@@ -148,10 +200,16 @@ class KimiDatabase {
148
  }
149
 
150
  getUnifiedTraitDefaults() {
 
 
 
 
 
151
  if (window.KimiEmotionSystem) {
152
  const emotionSystem = new window.KimiEmotionSystem(this);
153
  return emotionSystem.TRAIT_DEFAULTS;
154
  }
 
155
  return {
156
  affection: 55,
157
  playfulness: 55,
@@ -751,24 +809,23 @@ class KimiDatabase {
751
 
752
  // Use unified defaults from emotion system
753
  if (defaultValue === null) {
754
- if (window.KimiEmotionSystem) {
 
 
 
755
  const emotionSystem = new window.KimiEmotionSystem(this);
756
  defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
757
  } else {
758
- // Fallback defaults (must match KimiEmotionSystem.TRAIT_DEFAULTS exactly)
759
- if (window.getTraitDefaults) {
760
- defaultValue = window.getTraitDefaults()[trait] || 50;
761
- } else {
762
- defaultValue =
763
- {
764
- affection: 55,
765
- playfulness: 55,
766
- intelligence: 70,
767
- empathy: 75,
768
- humor: 60,
769
- romance: 50
770
- }[trait] || 50;
771
- }
772
  }
773
  }
774
 
@@ -1011,9 +1068,14 @@ class KimiDatabase {
1011
 
1012
  // Validation stricte : empêcher NaN ou valeurs non numériques
1013
  const getDefault = trait => {
 
 
 
 
1014
  if (window.KimiEmotionSystem) {
1015
  return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
1016
  }
 
1017
  const fallback = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
1018
  return fallback[trait] || 50;
1019
  };
 
17
  settings: "category",
18
  personality: "[character+trait],character",
19
  llmModels: "id",
20
+ memories: "++id,[character+category],character,timestamp,isActive,importance"
21
  })
22
  .upgrade(async tx => {
23
  try {
 
93
  // Non-blocking: continue on error
94
  }
95
  });
96
+
97
+ // Version 5: Clean schema with proper memory field defaults
98
+ this.db
99
+ .version(5)
100
+ .stores({
101
+ conversations: "++id,timestamp,favorability,character",
102
+ preferences: "key",
103
+ settings: "category",
104
+ personality: "[character+trait],character",
105
+ llmModels: "id",
106
+ memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount"
107
+ })
108
+ .upgrade(async tx => {
109
+ try {
110
+ // Ensure all memories have required fields for compatibility
111
+ const memories = tx.table("memories");
112
+ const now = new Date().toISOString();
113
+ await memories.toCollection().modify(rec => {
114
+ if (rec.isActive == null) rec.isActive = true;
115
+ if (rec.importance == null) rec.importance = 0.5;
116
+ if (rec.accessCount == null) rec.accessCount = 0;
117
+ if (!rec.character) rec.character = "kimi";
118
+ if (!rec.createdAt) rec.createdAt = rec.timestamp || now;
119
+ if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now;
120
+ });
121
+ console.log("✅ Database upgraded to v5: memory compatibility ensured");
122
+ } catch (e) {
123
+ console.warn("Database upgrade v5 non-critical error:", e);
124
+ }
125
+ });
126
  }
127
 
128
  async setConversationsBatch(conversationsArray) {
 
134
  }
135
  } catch (error) {
136
  console.error("Error restoring conversations:", error);
137
+ // Log to error manager for tracking
138
+ if (window.kimiErrorManager) {
139
+ window.kimiErrorManager.logDatabaseError("restoreConversations", error, {
140
+ conversationCount: conversationsArray.length
141
+ });
142
+ }
143
  }
144
  }
145
 
 
152
  }
153
  } catch (error) {
154
  console.error("Error restoring LLM models:", error);
155
+ // Log to error manager for tracking
156
+ if (window.kimiErrorManager) {
157
+ window.kimiErrorManager.logDatabaseError("setLLMModelsBatch", error, {
158
+ modelCount: modelsArray.length
159
+ });
160
+ }
161
  }
162
  }
163
 
 
166
  return await this.db.memories.toArray();
167
  } catch (error) {
168
  console.warn("Error getting all memories:", error);
169
+ // Log to error manager for tracking
170
+ if (window.kimiErrorManager) {
171
+ const errorType = error.name === "SchemaError" ? "SchemaError" : "DatabaseError";
172
+ window.kimiErrorManager.logError(errorType, error, {
173
+ operation: "getAllMemories",
174
+ suggestion: error.message?.includes("not indexed")
175
+ ? "Clear browser data to force schema upgrade"
176
+ : "Check database integrity"
177
+ });
178
+ }
179
  return [];
180
  }
181
  }
 
200
  }
201
 
202
  getUnifiedTraitDefaults() {
203
+ // Use centralized API instead of hardcoded values
204
+ if (window.getTraitDefaults) {
205
+ return window.getTraitDefaults();
206
+ }
207
+ // Fallback: create new instance only if no global API available
208
  if (window.KimiEmotionSystem) {
209
  const emotionSystem = new window.KimiEmotionSystem(this);
210
  return emotionSystem.TRAIT_DEFAULTS;
211
  }
212
+ // Ultimate fallback (should never be reached in normal operation)
213
  return {
214
  affection: 55,
215
  playfulness: 55,
 
809
 
810
  // Use unified defaults from emotion system
811
  if (defaultValue === null) {
812
+ // Use centralized API for trait defaults
813
+ if (window.getTraitDefaults) {
814
+ defaultValue = window.getTraitDefaults()[trait] || 50;
815
+ } else if (window.KimiEmotionSystem) {
816
  const emotionSystem = new window.KimiEmotionSystem(this);
817
  defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
818
  } else {
819
+ // Ultimate fallback (hardcoded values - should be avoided)
820
+ defaultValue =
821
+ {
822
+ affection: 55,
823
+ playfulness: 55,
824
+ intelligence: 70,
825
+ empathy: 75,
826
+ humor: 60,
827
+ romance: 50
828
+ }[trait] || 50;
 
 
 
 
829
  }
830
  }
831
 
 
1068
 
1069
  // Validation stricte : empêcher NaN ou valeurs non numériques
1070
  const getDefault = trait => {
1071
+ // Use centralized API for consistency
1072
+ if (window.getTraitDefaults) {
1073
+ return window.getTraitDefaults()[trait] || 50;
1074
+ }
1075
  if (window.KimiEmotionSystem) {
1076
  return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
1077
  }
1078
+ // Ultimate fallback (should be avoided)
1079
  const fallback = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
1080
  return fallback[trait] || 50;
1081
  };
kimi-js/kimi-debug-utils.js ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // KIMI DEBUG UTILITIES
2
+ // Centralized debug management for production optimization
3
+ //
4
+ // USAGE:
5
+ // debugOn() - Enable all debug logs
6
+ // debugOff() - Disable all debug logs (production mode)
7
+ // debugStatus() - Show current debug configuration
8
+ // kimiDebugAll() - Complete debug dashboard (includes errors)
9
+ // kimiDiagnosDB() - Database schema diagnostics
10
+ //
11
+ // CATEGORIES:
12
+ // KimiDebugController.setDebugCategory("VIDEO", true)
13
+ // KimiDebugController.setDebugCategory("MEMORY", false)
14
+ // Available: VIDEO, VOICE, MEMORY, API, SYNC
15
+
16
+ // Global debug controller
17
+ window.KimiDebugController = {
18
+ // Enable/disable all debug features
19
+ setGlobalDebug(enabled) {
20
+ if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) {
21
+ window.KIMI_CONFIG.DEBUG.ENABLED = enabled;
22
+ window.KIMI_CONFIG.DEBUG.VOICE = enabled;
23
+ window.KIMI_CONFIG.DEBUG.VIDEO = enabled;
24
+ window.KIMI_CONFIG.DEBUG.MEMORY = enabled;
25
+ window.KIMI_CONFIG.DEBUG.API = enabled;
26
+ window.KIMI_CONFIG.DEBUG.SYNC = enabled;
27
+ }
28
+
29
+ // Legacy flags (to be removed)
30
+ window.KIMI_DEBUG_SYNC = enabled;
31
+ window.KIMI_DEBUG_MEMORIES = enabled;
32
+ window.KIMI_DEBUG_API_AUDIT = enabled;
33
+ window.DEBUG_SAFE_LOGS = enabled;
34
+
35
+ console.log(`🔧 Global debug ${enabled ? "ENABLED" : "DISABLED"}`);
36
+ },
37
+
38
+ // Enable specific debug category
39
+ setDebugCategory(category, enabled) {
40
+ if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) {
41
+ if (category in window.KIMI_CONFIG.DEBUG) {
42
+ window.KIMI_CONFIG.DEBUG[category] = enabled;
43
+ console.log(`🔧 Debug category ${category} ${enabled ? "ENABLED" : "DISABLED"}`);
44
+ }
45
+ }
46
+
47
+ // Video manager specific
48
+ if (category === "VIDEO" && window.kimiVideo) {
49
+ window.kimiVideo.setDebug(enabled);
50
+ }
51
+ },
52
+
53
+ // Production mode (all debug off)
54
+ setProductionMode() {
55
+ this.setGlobalDebug(false);
56
+ console.log("🚀 Production mode activated - all debug logs disabled");
57
+ },
58
+
59
+ // Development mode (selective debug on)
60
+ setDevelopmentMode() {
61
+ this.setGlobalDebug(true);
62
+ console.log("🛠️ Development mode activated - debug logs enabled");
63
+ },
64
+
65
+ // Get current debug status
66
+ getDebugStatus() {
67
+ const status = {
68
+ global: window.KIMI_CONFIG?.DEBUG?.ENABLED || false,
69
+ voice: window.KIMI_CONFIG?.DEBUG?.VOICE || false,
70
+ video: window.KIMI_CONFIG?.DEBUG?.VIDEO || false,
71
+ memory: window.KIMI_CONFIG?.DEBUG?.MEMORY || false,
72
+ api: window.KIMI_CONFIG?.DEBUG?.API || false,
73
+ sync: window.KIMI_CONFIG?.DEBUG?.SYNC || false
74
+ };
75
+
76
+ console.table(status);
77
+ return status;
78
+ }
79
+ };
80
+
81
+ // Quick shortcuts for console
82
+ window.debugOn = () => window.KimiDebugController.setDevelopmentMode();
83
+ window.debugOff = () => window.KimiDebugController.setProductionMode();
84
+ window.debugStatus = () => window.KimiDebugController.getDebugStatus();
85
+
86
+ // Integration with error manager for unified debugging
87
+ window.kimiDebugAll = () => {
88
+ console.group("🔧 Kimi Debug Dashboard");
89
+ window.KimiDebugController.getDebugStatus();
90
+ if (window.kimiErrorManager) {
91
+ window.kimiErrorManager.printErrorSummary();
92
+ }
93
+ console.groupEnd();
94
+ };
95
+
96
+ // Database diagnostics helper
97
+ window.kimiDiagnosDB = async () => {
98
+ console.group("🔍 Database Diagnostics");
99
+ try {
100
+ if (window.kimiDB) {
101
+ console.log("📊 Database version:", window.kimiDB.db.verno);
102
+ console.log("📋 Available tables:", Object.keys(window.kimiDB.db._dbSchema));
103
+
104
+ // Check memories table schema
105
+ const memoriesSchema = window.kimiDB.db._dbSchema.memories;
106
+ if (memoriesSchema) {
107
+ console.log("🧠 Memories schema:", memoriesSchema);
108
+ const hasCharacterIsActiveIndex = memoriesSchema.indexes?.some(
109
+ idx =>
110
+ idx.name === "[character+isActive]" ||
111
+ (idx.keyPath?.includes("character") && idx.keyPath?.includes("isActive"))
112
+ );
113
+ console.log("✅ [character+isActive] index:", hasCharacterIsActiveIndex ? "PRESENT" : "❌ MISSING");
114
+
115
+ if (!hasCharacterIsActiveIndex) {
116
+ console.warn(
117
+ "🚨 SOLUTION: Clear browser data (Application > Storage > Clear Site Data) to force schema upgrade"
118
+ );
119
+ }
120
+ }
121
+ } else {
122
+ console.warn("❌ Database not initialized yet");
123
+ }
124
+ } catch (error) {
125
+ console.error("Error during database diagnostics:", error);
126
+ }
127
+ console.groupEnd();
128
+ };
129
+
130
+ // Auto-initialize to production mode for performance
131
+ if (typeof window.KIMI_CONFIG !== "undefined") {
132
+ window.KimiDebugController.setProductionMode();
133
+ }
kimi-js/kimi-emotion-system.js CHANGED
@@ -6,6 +6,11 @@ class KimiEmotionSystem {
6
  this.db = database;
7
  this.negativeStreaks = {};
8
 
 
 
 
 
 
9
  // Unified emotion mappings
10
  this.EMOTIONS = {
11
  // Base emotions
@@ -26,23 +31,59 @@ class KimiEmotionSystem {
26
  GOODBYE: "goodbye"
27
  };
28
 
29
- // Unified video context mapping
30
  this.emotionToVideoCategory = {
 
31
  positive: "speakingPositive",
32
  negative: "speakingNegative",
33
  neutral: "neutral",
 
 
34
  dancing: "dancing",
35
  listening: "listening",
 
 
36
  romantic: "speakingPositive",
37
  laughing: "speakingPositive",
38
  surprise: "speakingPositive",
39
  confident: "speakingPositive",
40
- shy: "neutral",
41
  flirtatious: "speakingPositive",
42
  kiss: "speakingPositive",
43
- goodbye: "neutral"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  };
45
 
 
 
 
 
46
  // Unified trait defaults - Balanced for progressive experience
47
  this.TRAIT_DEFAULTS = {
48
  affection: 55, // Baseline neutral affection
@@ -81,7 +122,152 @@ class KimiEmotionSystem {
81
  intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 }
82
  };
83
  }
84
- // (Affection is an independent trait again; previous derived computation removed.)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  // ===== UNIFIED EMOTION ANALYSIS =====
86
  analyzeEmotion(text, lang = "auto") {
87
  if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
@@ -358,8 +544,10 @@ class KimiEmotionSystem {
358
  const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
359
  if (prep.shouldPersist) toPersist[trait] = prep.value;
360
  }
 
 
361
  if (Object.keys(toPersist).length > 0) {
362
- await this.db.setPersonalityBatch(toPersist, selectedCharacter);
363
  }
364
 
365
  return updatedTraits;
@@ -467,8 +655,6 @@ class KimiEmotionSystem {
467
  }
468
  }
469
 
470
- // Affection stays as independently adjusted by keywords & emotion (no derived override)
471
-
472
  // Flush pending updates in a single batch write to avoid overwrites
473
  if (Object.keys(pendingUpdates).length > 0) {
474
  // Apply smoothing/threshold per trait (read current values)
@@ -484,21 +670,6 @@ class KimiEmotionSystem {
484
  }
485
  }
486
 
487
- // ===== UNIFIED VIDEO CONTEXT MAPPING =====
488
- mapEmotionToVideoCategory(emotion) {
489
- return this.emotionToVideoCategory[emotion] || "neutral";
490
- }
491
-
492
- // ===== VALIDATION SYSTEM =====
493
- validateEmotion(emotion) {
494
- const validEmotions = Object.values(this.EMOTIONS);
495
- if (!validEmotions.includes(emotion)) {
496
- console.warn(`Invalid emotion detected: ${emotion}, falling back to neutral`);
497
- return this.EMOTIONS.NEUTRAL;
498
- }
499
- return emotion;
500
- }
501
-
502
  validatePersonalityTrait(trait, value) {
503
  if (typeof value !== "number" || value < 0 || value > 100) {
504
  console.warn(`Invalid trait value for ${trait}: ${value}, using default`);
@@ -803,36 +974,31 @@ window.refreshPersonalityAverageUI = async function (characterKey = null) {
803
  export default KimiEmotionSystem;
804
 
805
  // ===== BACKWARD COMPATIBILITY LAYER =====
806
- // Replace the old kimiAnalyzeEmotion function
807
- window.kimiAnalyzeEmotion = function (text, lang = "auto") {
808
  if (!window.kimiEmotionSystem) {
809
  window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
810
  }
811
- return window.kimiEmotionSystem.analyzeEmotion(text, lang);
 
 
 
 
 
812
  };
813
 
814
  // Replace the old updatePersonalityTraitsFromEmotion function
815
  window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
816
- if (!window.kimiEmotionSystem) {
817
- window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
818
- }
819
-
820
- const updatedTraits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text);
821
-
822
  return updatedTraits;
823
  };
824
 
825
  // Replace getPersonalityAverage function
826
  window.getPersonalityAverage = function (traits) {
827
- if (!window.kimiEmotionSystem) {
828
- window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
829
- }
830
- return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
831
  };
832
 
833
  // Unified trait defaults accessor
834
  window.getTraitDefaults = function () {
835
- if (window.kimiEmotionSystem) return window.kimiEmotionSystem.TRAIT_DEFAULTS;
836
- const temp = new KimiEmotionSystem(window.kimiDB);
837
- return temp.TRAIT_DEFAULTS;
838
  };
 
6
  this.db = database;
7
  this.negativeStreaks = {};
8
 
9
+ // Debouncing system for personality updates
10
+ this._personalityUpdateQueue = {};
11
+ this._personalityUpdateTimer = null;
12
+ this._personalityUpdateDelay = 300; // ms
13
+
14
  // Unified emotion mappings
15
  this.EMOTIONS = {
16
  // Base emotions
 
31
  GOODBYE: "goodbye"
32
  };
33
 
34
+ // Unified video context mapping - CENTRALIZED SOURCE OF TRUTH
35
  this.emotionToVideoCategory = {
36
+ // Base emotional states
37
  positive: "speakingPositive",
38
  negative: "speakingNegative",
39
  neutral: "neutral",
40
+
41
+ // Special contexts (always take priority)
42
  dancing: "dancing",
43
  listening: "listening",
44
+
45
+ // Specific emotions mapped to appropriate categories
46
  romantic: "speakingPositive",
47
  laughing: "speakingPositive",
48
  surprise: "speakingPositive",
49
  confident: "speakingPositive",
 
50
  flirtatious: "speakingPositive",
51
  kiss: "speakingPositive",
52
+
53
+ // Neutral/subdued emotions
54
+ shy: "neutral",
55
+ goodbye: "neutral",
56
+
57
+ // Explicit context mappings (for compatibility)
58
+ speaking: "speakingPositive", // Generic speaking defaults to positive
59
+ speakingPositive: "speakingPositive",
60
+ speakingNegative: "speakingNegative"
61
+ };
62
+
63
+ // Emotion priority weights for conflict resolution
64
+ this.emotionPriorities = {
65
+ dancing: 10, // Maximum priority - immersive experience
66
+ kiss: 9, // Very high - intimate moment
67
+ romantic: 8, // High - emotional connection
68
+ listening: 7, // High - active interaction
69
+ flirtatious: 6, // Medium-high - playful interaction
70
+ laughing: 6, // Medium-high - positive expression
71
+ surprise: 5, // Medium - reaction
72
+ confident: 5, // Medium - personality expression
73
+ speaking: 4, // Medium-low - generic speaking context
74
+ positive: 4, // Medium-low - general positive
75
+ negative: 4, // Medium-low - general negative
76
+ neutral: 3, // Low - default state
77
+ shy: 3, // Low - subdued state
78
+ goodbye: 2, // Very low - transitional
79
+ speakingPositive: 4, // Medium-low - for consistency
80
+ speakingNegative: 4 // Medium-low - for consistency
81
  };
82
 
83
+ // Context/emotion validation system for system integrity
84
+ this.validContexts = ["dancing", "listening", "speaking", "speakingPositive", "speakingNegative", "neutral"];
85
+ this.validEmotions = Object.values(this.EMOTIONS);
86
+
87
  // Unified trait defaults - Balanced for progressive experience
88
  this.TRAIT_DEFAULTS = {
89
  affection: 55, // Baseline neutral affection
 
122
  intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 }
123
  };
124
  }
125
+
126
+ // ===== DEBOUNCED PERSONALITY UPDATE SYSTEM =====
127
+ _debouncedPersonalityUpdate(updates, character) {
128
+ // Merge with existing queued updates for this character
129
+ if (!this._personalityUpdateQueue[character]) {
130
+ this._personalityUpdateQueue[character] = {};
131
+ }
132
+ Object.assign(this._personalityUpdateQueue[character], updates);
133
+
134
+ // Clear existing timer and set new one
135
+ if (this._personalityUpdateTimer) {
136
+ clearTimeout(this._personalityUpdateTimer);
137
+ }
138
+
139
+ this._personalityUpdateTimer = setTimeout(async () => {
140
+ try {
141
+ const allUpdates = { ...this._personalityUpdateQueue };
142
+ this._personalityUpdateQueue = {};
143
+ this._personalityUpdateTimer = null;
144
+
145
+ // Process all queued updates
146
+ for (const [char, traits] of Object.entries(allUpdates)) {
147
+ if (Object.keys(traits).length > 0) {
148
+ await this.db.setPersonalityBatch(traits, char);
149
+
150
+ // Emit unified personality update event
151
+ if (typeof window !== "undefined" && window.dispatchEvent) {
152
+ window.dispatchEvent(
153
+ new CustomEvent("personality:updated", {
154
+ detail: { character: char, traits: traits }
155
+ })
156
+ );
157
+ }
158
+ }
159
+ }
160
+ } catch (error) {
161
+ console.error("Error in debounced personality update:", error);
162
+ }
163
+ }, this._personalityUpdateDelay);
164
+ }
165
+
166
+ // ===== CENTRALIZED VALIDATION SYSTEM =====
167
+ validateContext(context) {
168
+ if (!context || typeof context !== "string") return "neutral";
169
+ const normalized = context.toLowerCase().trim();
170
+
171
+ // Check if it's a valid context
172
+ if (this.validContexts.includes(normalized)) return normalized;
173
+
174
+ // Check if it's a valid emotion that can be mapped to context
175
+ if (this.emotionToVideoCategory[normalized]) return normalized;
176
+
177
+ return "neutral"; // Safe fallback
178
+ }
179
+
180
+ validateEmotion(emotion) {
181
+ if (!emotion || typeof emotion !== "string") return "neutral";
182
+ const normalized = emotion.toLowerCase().trim();
183
+
184
+ // Check if it's a valid emotion
185
+ if (this.validEmotions.includes(normalized)) return normalized;
186
+
187
+ // Check common aliases
188
+ const aliases = {
189
+ happy: "positive",
190
+ sad: "negative",
191
+ mad: "negative",
192
+ angry: "negative",
193
+ excited: "positive",
194
+ calm: "neutral",
195
+ romance: "romantic",
196
+ laugh: "laughing",
197
+ dance: "dancing",
198
+ // Speaking contexts as emotion aliases
199
+ speaking: "positive", // Generic speaking defaults to positive
200
+ speakingpositive: "positive",
201
+ speakingnegative: "negative"
202
+ };
203
+
204
+ if (aliases[normalized]) return aliases[normalized];
205
+
206
+ return "neutral"; // Safe fallback
207
+ }
208
+
209
+ validateVideoCategory(category) {
210
+ const validCategories = ["dancing", "listening", "speakingPositive", "speakingNegative", "neutral"];
211
+ if (!category || typeof category !== "string") return "neutral";
212
+
213
+ const normalized = category.toLowerCase().trim();
214
+ return validCategories.includes(normalized) ? normalized : "neutral";
215
+ }
216
+
217
+ // Enhanced emotion analysis with validation
218
+ analyzeEmotionValidated(text, lang = "auto") {
219
+ const rawEmotion = this.analyzeEmotion(text, lang);
220
+ return this.validateEmotion(rawEmotion);
221
+ }
222
+
223
+ // ===== UTILITY METHODS FOR SYSTEM INTEGRATION =====
224
+ // Centralized method to get video category for any emotion/context combination
225
+ getVideoCategory(emotionOrContext, traits = null) {
226
+ // Handle the case where we get both context and emotion (e.g., from determineCategory calls)
227
+ // Priority: Specific contexts > Specific emotions > Generic fallbacks
228
+
229
+ // Try context validation first for immediate context matches
230
+ let validated = this.validateContext(emotionOrContext);
231
+ if (validated !== "neutral" || emotionOrContext === "neutral") {
232
+ // Valid context found or explicitly neutral
233
+ const category = this.emotionToVideoCategory[validated] || "neutral";
234
+ return this.validateVideoCategory(category);
235
+ }
236
+
237
+ // If no valid context, try as emotion
238
+ validated = this.validateEmotion(emotionOrContext);
239
+ const category = this.emotionToVideoCategory[validated] || "neutral";
240
+ return this.validateVideoCategory(category);
241
+ } // Get priority weight for any emotion/context
242
+ getPriorityWeight(emotionOrContext) {
243
+ // Try context validation first, then emotion validation
244
+ let validated = this.validateContext(emotionOrContext);
245
+ if (validated === "neutral" && emotionOrContext !== "neutral") {
246
+ // If context validation gave neutral but input wasn't neutral, try as emotion
247
+ validated = this.validateEmotion(emotionOrContext);
248
+ }
249
+
250
+ return this.emotionPriorities[validated] || 3; // Default medium-low priority
251
+ }
252
+
253
+ // Check if an emotion/context should override current state
254
+ shouldOverride(newEmotion, currentEmotion, currentContext = null) {
255
+ const newPriority = this.getPriorityWeight(newEmotion);
256
+ const currentPriority = Math.max(this.getPriorityWeight(currentEmotion), this.getPriorityWeight(currentContext));
257
+
258
+ return newPriority > currentPriority;
259
+ }
260
+
261
+ // Utility to normalize and validate a complete emotion/context request
262
+ normalizeEmotionRequest(context, emotion, traits = null) {
263
+ return {
264
+ context: this.validateContext(context),
265
+ emotion: this.validateEmotion(emotion),
266
+ category: this.getVideoCategory(emotion || context, traits),
267
+ priority: this.getPriorityWeight(emotion || context)
268
+ };
269
+ }
270
+
271
  // ===== UNIFIED EMOTION ANALYSIS =====
272
  analyzeEmotion(text, lang = "auto") {
273
  if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
 
544
  const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
545
  if (prep.shouldPersist) toPersist[trait] = prep.value;
546
  }
547
+
548
+ // Use debounced update instead of immediate DB write
549
  if (Object.keys(toPersist).length > 0) {
550
+ this._debouncedPersonalityUpdate(toPersist, selectedCharacter);
551
  }
552
 
553
  return updatedTraits;
 
655
  }
656
  }
657
 
 
 
658
  // Flush pending updates in a single batch write to avoid overwrites
659
  if (Object.keys(pendingUpdates).length > 0) {
660
  // Apply smoothing/threshold per trait (read current values)
 
670
  }
671
  }
672
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
  validatePersonalityTrait(trait, value) {
674
  if (typeof value !== "number" || value < 0 || value > 100) {
675
  console.warn(`Invalid trait value for ${trait}: ${value}, using default`);
 
974
  export default KimiEmotionSystem;
975
 
976
  // ===== BACKWARD COMPATIBILITY LAYER =====
977
+ // Ensure single instance of KimiEmotionSystem (Singleton pattern)
978
+ function getKimiEmotionSystemInstance() {
979
  if (!window.kimiEmotionSystem) {
980
  window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
981
  }
982
+ return window.kimiEmotionSystem;
983
+ }
984
+
985
+ // Replace the old kimiAnalyzeEmotion function
986
+ window.kimiAnalyzeEmotion = function (text, lang = "auto") {
987
+ return getKimiEmotionSystemInstance().analyzeEmotion(text, lang);
988
  };
989
 
990
  // Replace the old updatePersonalityTraitsFromEmotion function
991
  window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
992
+ const updatedTraits = await getKimiEmotionSystemInstance().updatePersonalityFromEmotion(emotion, text);
 
 
 
 
 
993
  return updatedTraits;
994
  };
995
 
996
  // Replace getPersonalityAverage function
997
  window.getPersonalityAverage = function (traits) {
998
+ return getKimiEmotionSystemInstance().calculatePersonalityAverage(traits);
 
 
 
999
  };
1000
 
1001
  // Unified trait defaults accessor
1002
  window.getTraitDefaults = function () {
1003
+ return getKimiEmotionSystemInstance().TRAIT_DEFAULTS;
 
 
1004
  };
kimi-js/kimi-error-manager.js CHANGED
@@ -169,6 +169,44 @@ class KimiErrorManager {
169
  throw error;
170
  }
171
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  }
173
 
174
  // Create global instance
@@ -176,3 +214,6 @@ window.kimiErrorManager = new KimiErrorManager();
176
 
177
  // Export class for manual instantiation if needed
178
  window.KimiErrorManager = KimiErrorManager;
 
 
 
 
169
  throw error;
170
  }
171
  }
172
+
173
+ // Debug helpers for development
174
+ getErrorSummary() {
175
+ const summary = {
176
+ totalErrors: this.errorLog.length,
177
+ critical: this.errorLog.filter(e => e.severity === "critical").length,
178
+ warning: this.errorLog.filter(e => e.severity === "warning").length,
179
+ recent: this.errorLog.filter(e => {
180
+ const errorTime = new Date(e.timestamp);
181
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
182
+ return errorTime > fiveMinutesAgo;
183
+ }).length,
184
+ types: [...new Set(this.errorLog.map(e => e.type))]
185
+ };
186
+ return summary;
187
+ }
188
+
189
+ printErrorSummary() {
190
+ const summary = this.getErrorSummary();
191
+ console.group("🔍 Kimi Error Manager Summary");
192
+ console.log(`📊 Total Errors: ${summary.totalErrors}`);
193
+ console.log(`🚨 Critical: ${summary.critical}`);
194
+ console.log(`⚠️ Warnings: ${summary.warning}`);
195
+ console.log(`⏰ Recent (5min): ${summary.recent}`);
196
+ console.log(`📋 Error Types:`, summary.types);
197
+ if (summary.totalErrors > 0) {
198
+ console.log(`💡 Use kimiErrorManager.getErrorLog() to see details`);
199
+ }
200
+ console.groupEnd();
201
+ return summary;
202
+ }
203
+
204
+ clearAndSummarize() {
205
+ const summary = this.getErrorSummary();
206
+ this.clearErrorLog();
207
+ console.log("🧹 Error log cleared. Previous summary:", summary);
208
+ return summary;
209
+ }
210
  }
211
 
212
  // Create global instance
 
214
 
215
  // Export class for manual instantiation if needed
216
  window.KimiErrorManager = KimiErrorManager;
217
+
218
+ // Global debugging helper
219
+ window.kimiDebugErrors = () => window.kimiErrorManager.printErrorSummary();
kimi-js/kimi-llm-manager.js CHANGED
@@ -95,7 +95,9 @@ class KimiLLMManager {
95
  try {
96
  await this.refreshRemoteModels();
97
  } catch (e) {
98
- console.warn("Unable to refresh remote models list:", e?.message || e);
 
 
99
  }
100
 
101
  // Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate
@@ -282,19 +284,25 @@ class KimiLLMManager {
282
  "\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
283
  }
284
  } catch (error) {
285
- console.warn("Error loading memories for personality:", error);
 
 
286
  }
287
  }
288
  // Read per-character preference metrics so displayed counters reflect actual stored values
289
- // Prefer the personality trait 'affection' where available (authoritative source)
290
  const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0;
291
- // Favorability should reflect the authoritative personality trait (affection).
292
- let favorabilityLevel = await this.db.getPersonalityTrait("affection", null, character);
293
- if (typeof favorabilityLevel !== "number" || !isFinite(favorabilityLevel)) {
294
- // Fallback to legacy preference if DB helper didn't return a proper number
295
- favorabilityLevel = Number(await this.db.getPreference(`favorabilityLevel_${character}`, 50)) || 50;
296
- }
297
- favorabilityLevel = Math.max(0, Math.min(100, Number(favorabilityLevel)));
 
 
 
 
 
298
  const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time");
299
  // Days together is computed and displayed in the UI (see `updateStats()` in `kimi-module.js`).
300
  let daysTogether = 0;
@@ -407,7 +415,7 @@ class KimiLLMManager {
407
  "",
408
  "LEARNED PREFERENCES:",
409
  `- Total interactions: ${totalInteractions}`,
410
- `- Current affection level: ${favorabilityLevel}%`,
411
  `- Last interaction: ${lastInteraction}`,
412
  `- Days together: ${daysTogether}`,
413
  "",
@@ -442,6 +450,12 @@ class KimiLLMManager {
442
  this.personalityPrompt = await this.assemblePrompt("");
443
  } catch (error) {
444
  console.warn("Error refreshing memory context:", error);
 
 
 
 
 
 
445
  }
446
  }
447
 
@@ -459,7 +473,53 @@ class KimiLLMManager {
459
  }
460
 
461
  async chat(userMessage, options = {}) {
462
- // Get LLM settings from individual preferences (FIXED: was using grouped settings)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
  const llmSettings = {
464
  temperature: await this.db.getPreference("llmTemperature", 0.9),
465
  maxTokens: await this.db.getPreference("llmMaxTokens", 400),
@@ -518,6 +578,14 @@ class KimiLLMManager {
518
  return await this.chatWithOpenAICompatibleStreaming(userMessage, onToken, opts);
519
  } catch (error) {
520
  console.error("Error during streaming chat:", error);
 
 
 
 
 
 
 
 
521
  // Fallback to non-streaming if streaming fails
522
  return await this.chat(userMessage, options);
523
  }
 
95
  try {
96
  await this.refreshRemoteModels();
97
  } catch (e) {
98
+ if (window.KIMI_CONFIG?.DEBUG?.API) {
99
+ console.warn("Unable to refresh remote models list:", e?.message || e);
100
+ }
101
  }
102
 
103
  // Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate
 
284
  "\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
285
  }
286
  } catch (error) {
287
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
288
+ console.warn("Error loading memories for personality:", error);
289
+ }
290
  }
291
  }
292
  // Read per-character preference metrics so displayed counters reflect actual stored values
 
293
  const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0;
294
+
295
+ // Get current personality average for relationship context (replacing old favorabilityLevel)
296
+ const currentPersonality = await this.db.getAllPersonalityTraits(character);
297
+ const relationshipLevel = window.getPersonalityAverage
298
+ ? window.getPersonalityAverage(currentPersonality)
299
+ : (currentPersonality.affection +
300
+ currentPersonality.romance +
301
+ currentPersonality.empathy +
302
+ currentPersonality.playfulness +
303
+ currentPersonality.humor +
304
+ currentPersonality.intelligence) /
305
+ 6;
306
  const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time");
307
  // Days together is computed and displayed in the UI (see `updateStats()` in `kimi-module.js`).
308
  let daysTogether = 0;
 
415
  "",
416
  "LEARNED PREFERENCES:",
417
  `- Total interactions: ${totalInteractions}`,
418
+ `- Current relationship level: ${relationshipLevel.toFixed(1)}%`,
419
  `- Last interaction: ${lastInteraction}`,
420
  `- Days together: ${daysTogether}`,
421
  "",
 
450
  this.personalityPrompt = await this.assemblePrompt("");
451
  } catch (error) {
452
  console.warn("Error refreshing memory context:", error);
453
+ // Log to error manager for tracking memory context issues
454
+ if (window.kimiErrorManager) {
455
+ window.kimiErrorManager.logError("MemoryContextError", error, {
456
+ operation: "refreshMemoryContext"
457
+ });
458
+ }
459
  }
460
  }
461
 
 
473
  }
474
 
475
  async chat(userMessage, options = {}) {
476
+ // Use error manager wrapper for robust error handling
477
+ return (
478
+ window.kimiErrorManager?.wrapAsync(
479
+ async () => {
480
+ // Get LLM settings from individual preferences (FIXED: was using grouped settings)
481
+ const llmSettings = {
482
+ temperature: await this.db.getPreference("llmTemperature", 0.9),
483
+ maxTokens: await this.db.getPreference("llmMaxTokens", 400),
484
+ top_p: await this.db.getPreference("llmTopP", 0.9),
485
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
486
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
487
+ };
488
+ const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature;
489
+ const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens;
490
+ const opts = { ...options, temperature, maxTokens };
491
+ try {
492
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
493
+ if (provider === "openrouter") {
494
+ return await this.chatWithOpenRouter(userMessage, opts);
495
+ }
496
+ if (provider === "ollama") {
497
+ return await this.chatWithLocal(userMessage, opts);
498
+ }
499
+ return await this.chatWithOpenAICompatible(userMessage, opts);
500
+ } catch (error) {
501
+ console.error("Error during chat:", error);
502
+ if (error.message && error.message.includes("API")) {
503
+ return this.getFallbackResponse(userMessage, "api");
504
+ }
505
+ if ((error.message && error.message.includes("model")) || error.message.includes("model")) {
506
+ return this.getFallbackResponse(userMessage, "model");
507
+ }
508
+ if ((error.message && error.message.includes("connection")) || error.message.includes("network")) {
509
+ return this.getFallbackResponse(userMessage, "network");
510
+ }
511
+ return this.getFallbackResponse(userMessage);
512
+ }
513
+ },
514
+ { operation: "chat", userMessageLength: userMessage?.length || 0 }
515
+ ) ||
516
+ // Fallback if error manager not available
517
+ this.chatDirectly(userMessage, options)
518
+ );
519
+ }
520
+
521
+ // Fallback method without error manager wrapper
522
+ async chatDirectly(userMessage, options = {}) {
523
  const llmSettings = {
524
  temperature: await this.db.getPreference("llmTemperature", 0.9),
525
  maxTokens: await this.db.getPreference("llmMaxTokens", 400),
 
578
  return await this.chatWithOpenAICompatibleStreaming(userMessage, onToken, opts);
579
  } catch (error) {
580
  console.error("Error during streaming chat:", error);
581
+ // Log API error for tracking
582
+ if (window.kimiErrorManager) {
583
+ window.kimiErrorManager.logAPIError("streamingChat", error, {
584
+ provider: await this.db.getPreference("llmProvider", "openrouter").catch(() => "unknown"),
585
+ messageLength: userMessage?.length || 0,
586
+ options: opts
587
+ });
588
+ }
589
  // Fallback to non-streaming if streaming fails
590
  return await this.chat(userMessage, options);
591
  }
kimi-js/kimi-main.js CHANGED
@@ -5,6 +5,7 @@ import KimiLLMManager from "./kimi-llm-manager.js";
5
  import KimiEmotionSystem from "./kimi-emotion-system.js";
6
 
7
  // Expose module imports to legacy code paths that still rely on window
 
8
  window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils;
9
  window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager;
10
  window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem;
 
5
  import KimiEmotionSystem from "./kimi-emotion-system.js";
6
 
7
  // Expose module imports to legacy code paths that still rely on window
8
+ // Ensure KimiProviderUtils is available (imported from kimi-utils.js)
9
  window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils;
10
  window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager;
11
  window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem;
kimi-js/kimi-memory-database-optimization.js ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI MEMORY DATABASE OPTIMIZATION SUGGESTIONS =====
2
+
3
+ /**
4
+ * Performance Optimization Guide for Kimi Memory System
5
+ *
6
+ * This file contains recommendations for database schema optimizations
7
+ * to improve query performance in the memory system.
8
+ */
9
+
10
+ // RECOMMENDED DEXIE INDEX CONFIGURATION
11
+ // Add these indexes to your database schema for optimal performance
12
+
13
+ const RECOMMENDED_MEMORY_INDEXES = {
14
+ // Current schema (for reference)
15
+ current: "++id, character, category, type, timestamp, isActive, confidence, importance, keywords",
16
+
17
+ // OPTIMIZED schema with composite indexes
18
+ optimized: [
19
+ "++id", // Primary key (auto-increment)
20
+
21
+ // Single field indexes (existing)
22
+ "character",
23
+ "category",
24
+ "type",
25
+ "timestamp",
26
+ "isActive",
27
+ "confidence",
28
+ "importance",
29
+ "*keywords", // Multi-entry index for keyword array
30
+
31
+ // COMPOSITE INDEXES for frequent query patterns
32
+ "[character+isActive]", // Filter by character and active status
33
+ "[character+category]", // Get memories by character and category
34
+ "[character+category+isActive]", // Most common query pattern
35
+ "[character+timestamp]", // Chronological queries by character
36
+ "[isActive+importance]", // Get active memories by importance
37
+ "[isActive+timestamp]", // Recent active memories
38
+ "[character+type+isActive]", // Filter by character, type, and status
39
+
40
+ // Advanced composite indexes for complex queries
41
+ "[character+isActive+importance]", // Prioritized active memories by character
42
+ "[character+category+timestamp]" // Category-specific chronological queries
43
+ ]
44
+ };
45
+
46
+ // QUERY OPTIMIZATION EXAMPLES
47
+ const OPTIMIZED_QUERY_PATTERNS = {
48
+ // BEFORE: Multiple filter operations
49
+ getAllMemoriesOld: `
50
+ db.memories
51
+ .where("character").equals(character)
52
+ .filter(m => m.isActive !== false)
53
+ .reverse()
54
+ .sortBy("timestamp")
55
+ `,
56
+
57
+ // AFTER: Use composite index
58
+ getAllMemoriesOptimized: `
59
+ db.memories
60
+ .where("[character+isActive]").equals([character, true])
61
+ .reverse()
62
+ .sortBy("timestamp")
63
+ `,
64
+
65
+ // BEFORE: Filter after retrieval
66
+ getMemoriesByCategoryOld: `
67
+ db.memories
68
+ .where("[character+category]").equals([character, category])
69
+ .and(m => m.isActive)
70
+ `,
71
+
72
+ // AFTER: Direct composite index
73
+ getMemoriesByCategoryOptimized: `
74
+ db.memories
75
+ .where("[character+category+isActive]").equals([character, category, true])
76
+ `,
77
+
78
+ // NEW: Efficient importance-based queries
79
+ getTopMemoriesByImportance: `
80
+ db.memories
81
+ .where("[character+isActive+importance]")
82
+ .between([character, true, 0.8], [character, true, 1.0])
83
+ .reverse()
84
+ .limit(10)
85
+ `
86
+ };
87
+
88
+ // PERFORMANCE MONITORING UTILITIES
89
+ class MemoryDatabaseProfiler {
90
+ constructor() {
91
+ this.queryTimes = new Map();
92
+ this.queryCount = new Map();
93
+ }
94
+
95
+ // Wrap database operations to measure performance
96
+ async profileQuery(queryName, queryFn) {
97
+ const start = performance.now();
98
+ const result = await queryFn();
99
+ const duration = performance.now() - start;
100
+
101
+ // Update statistics
102
+ if (!this.queryTimes.has(queryName)) {
103
+ this.queryTimes.set(queryName, []);
104
+ this.queryCount.set(queryName, 0);
105
+ }
106
+
107
+ this.queryTimes.get(queryName).push(duration);
108
+ this.queryCount.set(queryName, this.queryCount.get(queryName) + 1);
109
+
110
+ // Log slow queries
111
+ if (duration > 50) {
112
+ // 50ms threshold
113
+ console.warn(`🐌 Slow query detected: ${queryName} took ${duration.toFixed(2)}ms`);
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ // Get performance statistics
120
+ getStats() {
121
+ const stats = {};
122
+
123
+ for (const [queryName, times] of this.queryTimes.entries()) {
124
+ const count = this.queryCount.get(queryName);
125
+ const avgTime = times.reduce((sum, time) => sum + time, 0) / times.length;
126
+ const maxTime = Math.max(...times);
127
+ const minTime = Math.min(...times);
128
+
129
+ stats[queryName] = {
130
+ count,
131
+ avgTime: Math.round(avgTime * 100) / 100,
132
+ maxTime: Math.round(maxTime * 100) / 100,
133
+ minTime: Math.round(minTime * 100) / 100,
134
+ totalTime: Math.round(times.reduce((sum, time) => sum + time, 0) * 100) / 100
135
+ };
136
+ }
137
+
138
+ return stats;
139
+ }
140
+
141
+ // Reset statistics
142
+ reset() {
143
+ this.queryTimes.clear();
144
+ this.queryCount.clear();
145
+ }
146
+ }
147
+
148
+ // MEMORY USAGE OPTIMIZATION
149
+ const MEMORY_CLEANUP_STRATEGIES = {
150
+ // Strategy 1: Batch cleanup operations
151
+ batchCleanup: async (db, maxBatchSize = 100) => {
152
+ const oldMemories = await db.memories.where("isActive").equals(false).limit(maxBatchSize).toArray();
153
+
154
+ if (oldMemories.length > 0) {
155
+ await db.memories.bulkDelete(oldMemories.map(m => m.id));
156
+ console.log(`🧹 Batch deleted ${oldMemories.length} inactive memories`);
157
+ }
158
+ },
159
+
160
+ // Strategy 2: Incremental keyword cleanup
161
+ cleanupKeywords: async db => {
162
+ const memoriesWithEmptyKeywords = await db.memories
163
+ .filter(m => !m.keywords || m.keywords.length === 0)
164
+ .limit(50)
165
+ .toArray();
166
+
167
+ for (const memory of memoriesWithEmptyKeywords) {
168
+ const keywords = deriveKeywords(memory.content || "");
169
+ await db.memories.update(memory.id, { keywords });
170
+ }
171
+ },
172
+
173
+ // Strategy 3: Compress old memories
174
+ compressOldMemories: async (db, daysThreshold = 90) => {
175
+ const cutoff = new Date();
176
+ cutoff.setDate(cutoff.getDate() - daysThreshold);
177
+
178
+ const oldMemories = await db.memories
179
+ .where("timestamp")
180
+ .below(cutoff)
181
+ .and(m => m.isActive && !m.compressed)
182
+ .limit(20)
183
+ .toArray();
184
+
185
+ for (const memory of oldMemories) {
186
+ // Compress by removing redundant fields and shortening content
187
+ const compressed = {
188
+ compressed: true,
189
+ originalLength: memory.content?.length || 0,
190
+ content: memory.content?.substring(0, 100) + "...",
191
+ // Remove non-essential fields
192
+ sourceText: undefined,
193
+ tags: memory.tags?.slice(0, 3) // Keep only top 3 tags
194
+ };
195
+
196
+ await db.memories.update(memory.id, compressed);
197
+ }
198
+ }
199
+ };
200
+
201
+ // EXPORT UTILITIES
202
+ if (typeof window !== "undefined") {
203
+ window.KIMI_MEMORY_DB_OPTIMIZATION = {
204
+ RECOMMENDED_MEMORY_INDEXES,
205
+ OPTIMIZED_QUERY_PATTERNS,
206
+ MemoryDatabaseProfiler,
207
+ MEMORY_CLEANUP_STRATEGIES
208
+ };
209
+ }
210
+
211
+ export { RECOMMENDED_MEMORY_INDEXES, OPTIMIZED_QUERY_PATTERNS, MemoryDatabaseProfiler, MEMORY_CLEANUP_STRATEGIES };
kimi-js/kimi-memory-system.js CHANGED
@@ -4,6 +4,99 @@ class KimiMemorySystem {
4
  this.db = database;
5
  this.memoryEnabled = true;
6
  this.maxMemoryEntries = 100;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  this.memoryCategories = {
8
  personal: "Personal Information",
9
  preferences: "Likes & Dislikes",
@@ -200,6 +293,45 @@ class KimiMemorySystem {
200
  /请记住(.+)/i
201
  ]
202
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
 
205
  async init() {
@@ -216,11 +348,9 @@ class KimiMemorySystem {
216
  this.selectedCharacter = await this.db.getSelectedCharacter();
217
  await this.createMemoryTables();
218
 
219
- // Migrer les IDs incompatibles si nécessaire
220
- await this.migrateIncompatibleIDs();
221
-
222
- // Start background migration to populate keywords for existing memories (non-blocking)
223
- this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e));
224
  } catch (error) {
225
  console.error("Memory system initialization error:", error);
226
  }
@@ -238,10 +368,15 @@ class KimiMemorySystem {
238
  async extractMemoryFromText(userText, kimiResponse = null) {
239
  if (!this.memoryEnabled || !userText) return [];
240
 
 
 
 
 
 
241
  const extractedMemories = [];
242
  const text = userText.toLowerCase();
243
 
244
- console.log("🔍 Memory extraction - Processing text:", userText);
245
 
246
  // Enhanced extraction with context awareness
247
  const existingMemories = await this.getAllMemories();
@@ -249,19 +384,20 @@ class KimiMemorySystem {
249
  // First, check for explicit memory requests
250
  const explicitRequests = this.detectExplicitMemoryRequests(userText);
251
  if (explicitRequests.length > 0) {
252
- console.log("🎯 Explicit memory requests detected:", explicitRequests);
253
  extractedMemories.push(...explicitRequests);
254
  }
255
 
256
- // Extract using patterns
257
- for (const [category, patterns] of Object.entries(this.extractionPatterns)) {
 
258
  for (const pattern of patterns) {
259
  const match = text.match(pattern);
260
  if (match && match[1]) {
261
  const content = match[1].trim();
262
 
263
  // Skip very short or generic content
264
- if (content.length < 2 || this.isGenericContent(content)) {
265
  continue;
266
  }
267
 
@@ -274,12 +410,12 @@ class KimiMemorySystem {
274
  content: content,
275
  sourceText: userText,
276
  confidence: this.calculateExtractionConfidence(match, userText),
277
- timestamp: new Date(),
278
- character: this.selectedCharacter,
279
  isUpdate: isUpdate
280
  };
281
 
282
- console.log(`💡 Pattern match for ${category}:`, content);
283
  extractedMemories.push(memory);
284
  }
285
  }
@@ -292,14 +428,29 @@ class KimiMemorySystem {
292
  // Save extracted memories with intelligent deduplication
293
  const savedMemories = [];
294
  for (const memory of extractedMemories) {
295
- console.log("💾 Saving memory:", memory.content);
296
- const saved = await this.addMemory(memory);
297
- if (saved) savedMemories.push(saved);
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
 
300
  if (savedMemories.length > 0) {
301
- console.log(`✅ Successfully extracted and saved ${savedMemories.length} memories`);
302
- } else {
 
 
303
  console.log("📝 No memories extracted from this text");
304
  }
305
 
@@ -469,12 +620,12 @@ class KimiMemorySystem {
469
  // Check if content is too generic to be useful
470
  isGenericContent(content) {
471
  const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"];
472
- return genericWords.includes(content.toLowerCase()) || content.length < 2;
473
  }
474
 
475
  // Calculate confidence based on context and pattern strength
476
  calculateExtractionConfidence(match, fullText) {
477
- let confidence = 0.6; // Base confidence
478
 
479
  // Boost confidence for explicit statements
480
  const lower = fullText.toLowerCase();
@@ -496,20 +647,20 @@ class KimiMemorySystem {
496
  lower.includes("我叫") ||
497
  lower.includes("我的名字是")
498
  ) {
499
- confidence += 0.3;
500
  }
501
 
502
  // Boost for longer, more specific content
503
- if (match[1] && match[1].trim().length > 10) {
504
- confidence += 0.1;
505
  }
506
 
507
  // Reduce confidence for uncertain language
508
  if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) {
509
- confidence -= 0.2;
510
  }
511
 
512
- return Math.min(1.0, Math.max(0.1, confidence));
513
  }
514
 
515
  // Generate a short title (2-5 words max) from content for auto-extracted memories
@@ -525,9 +676,9 @@ class KimiMemorySystem {
525
  if (words.length === 0) return "";
526
  // Prefer 3 words when available, minimum 2 when possible, maximum 5
527
  let take;
528
- if (words.length >= 3) take = 3;
529
  else take = words.length; // 1 or 2
530
- take = Math.min(5, Math.max(1, take));
531
 
532
  const slice = words.slice(0, take);
533
  // Capitalize first word for nicer title
@@ -541,7 +692,7 @@ class KimiMemorySystem {
541
 
542
  for (const memory of categoryMemories) {
543
  const similarity = this.calculateSimilarity(memory.content, content);
544
- if (similarity > 0.3) {
545
  // Lower threshold for updates
546
  return true;
547
  }
@@ -598,8 +749,8 @@ class KimiMemorySystem {
598
  content: name,
599
  sourceText: text,
600
  confidence: 0.7,
601
- timestamp: new Date(),
602
- character: this.selectedCharacter
603
  });
604
  }
605
  }
@@ -688,12 +839,11 @@ class KimiMemorySystem {
688
  : "",
689
  sourceText: memoryData.sourceText || "",
690
  confidence: memoryData.confidence || 1.0,
691
- timestamp: memoryData.timestamp || now,
692
- character: memoryData.character || this.selectedCharacter,
693
  isActive: true,
694
  tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])],
695
  lastModified: now,
696
- createdAt: now,
697
  lastAccess: now,
698
  accessCount: 0,
699
  importance: this.calculateImportance(memoryData)
@@ -702,7 +852,9 @@ class KimiMemorySystem {
702
  if (this.db.db.memories) {
703
  const id = await this.db.db.memories.add(memory);
704
  memory.id = id; // Store the auto-generated ID
705
- console.log(`Memory added with ID: ${id}`);
 
 
706
  }
707
 
708
  // Cleanup old memories if we exceed limit
@@ -714,6 +866,7 @@ class KimiMemorySystem {
714
  return memory;
715
  } catch (error) {
716
  console.error("Error adding memory:", error);
 
717
  }
718
  }
719
 
@@ -782,31 +935,33 @@ class KimiMemorySystem {
782
  }
783
  }
784
 
785
- // Determine how to merge two related memories
786
  determineMergeStrategy(existing, newData) {
787
  const similarity = this.calculateSimilarity(existing.content, newData.content);
788
- const newConfidence = newData.confidence || 0.8;
 
789
 
790
- // If very similar content but new has higher confidence
791
- if (similarity > 0.9 && newConfidence > existing.confidence) {
792
- return "boost_confidence";
793
  }
794
 
795
- // If moderately similar, decide based on specificity and recency
796
- if (similarity > 0.7) {
 
797
  if (newData.content.length > existing.content.length * 1.5) {
798
- return "update_content"; // New is more detailed
799
- } else {
800
- return "merge_content";
801
  }
 
 
802
  }
803
 
804
- // For names, handle as variants
805
  if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) {
806
  return "add_variant";
807
  }
808
 
809
- // Default to merging
810
  return "merge_content";
811
  }
812
 
@@ -875,8 +1030,9 @@ class KimiMemorySystem {
875
  }
876
 
877
  // Longer details and high confidence
878
- if (memoryData.content && memoryData.content.length > 24) importance += 0.05;
879
- if (memoryData.confidence && memoryData.confidence > 0.9) importance += 0.05;
 
880
 
881
  // Round to two decimals to avoid floating point artifacts
882
  return Math.min(1.0, Math.round(importance * 100) / 100);
@@ -1010,7 +1166,7 @@ class KimiMemorySystem {
1010
  if (!this.db) return [];
1011
 
1012
  try {
1013
- character = character || this.selectedCharacter;
1014
 
1015
  if (this.db.db.memories) {
1016
  const memories = await this.db.db.memories
@@ -1034,13 +1190,14 @@ class KimiMemorySystem {
1034
  if (!this.db) return [];
1035
 
1036
  try {
1037
- character = character || this.selectedCharacter;
1038
 
1039
  if (this.db.db.memories) {
 
1040
  const memories = await this.db.db.memories
1041
  .where("character")
1042
  .equals(character)
1043
- .and(m => m.isActive)
1044
  .reverse()
1045
  .sortBy("timestamp");
1046
 
@@ -1077,12 +1234,7 @@ class KimiMemorySystem {
1077
  const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
1078
 
1079
  // Different thresholds based on category
1080
- let threshold = 0.8;
1081
- if (memoryData.category === "personal") {
1082
- threshold = 0.6; // Names and personal info can vary more
1083
- } else if (memoryData.category === "preferences") {
1084
- threshold = 0.7; // Preferences can be expressed differently
1085
- }
1086
 
1087
  if (contentSimilarity > threshold) {
1088
  return memory;
@@ -1171,11 +1323,130 @@ class KimiMemorySystem {
1171
  .toLowerCase()
1172
  .replace(/[\p{P}\p{S}]/gu, " ")
1173
  .split(/\s+/)
1174
- .filter(w => w.length > 2 && !(this.isCommonWord && this.isCommonWord(w)))
1175
  )
1176
  ];
1177
  }
1178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1179
  async cleanupOldMemories() {
1180
  if (!this.db) return;
1181
 
@@ -1189,17 +1460,30 @@ class KimiMemorySystem {
1189
  // Soft-expire memories older than TTL by marking isActive=false
1190
  const now = Date.now();
1191
  const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
 
 
1192
  for (const mem of memories) {
1193
- const created = new Date(mem.timestamp).getTime();
1194
  if (now - created > ttlMs) {
1195
  try {
1196
  await this.updateMemory(mem.id, { isActive: false });
 
1197
  } catch (e) {
1198
- console.warn("Failed to soft-expire memory", mem.id, e);
 
 
 
 
 
 
1199
  }
1200
  }
1201
  }
1202
 
 
 
 
 
1203
  // Refresh active memories after TTL purge
1204
  const activeMemories = (await this.getAllMemories()).filter(m => m.isActive);
1205
 
@@ -1210,22 +1494,38 @@ class KimiMemorySystem {
1210
  const scoreA =
1211
  (a.importance || 0.5) * -1 +
1212
  (a.accessCount || 0) * 0.01 +
1213
- new Date(a.timestamp).getTime() / (1000 * 60 * 60 * 24);
1214
  const scoreB =
1215
  (b.importance || 0.5) * -1 +
1216
  (b.accessCount || 0) * 0.01 +
1217
- new Date(b.timestamp).getTime() / (1000 * 60 * 60 * 24);
1218
  return scoreB - scoreA;
1219
  });
1220
 
1221
  const toDeactivate = activeMemories.slice(maxEntries);
 
 
 
1222
  for (const mem of toDeactivate) {
1223
  try {
1224
  await this.updateMemory(mem.id, { isActive: false });
 
1225
  } catch (e) {
1226
- console.warn("Failed to deactivate memory", mem.id, e);
 
 
 
 
 
 
1227
  }
1228
  }
 
 
 
 
 
 
1229
  }
1230
  } catch (error) {
1231
  console.error("Error cleaning up old memories:", error);
@@ -1281,7 +1581,7 @@ class KimiMemorySystem {
1281
  let score = memory.importance || 0.5;
1282
 
1283
  // Boost recent memories
1284
- const daysSinceCreation = (Date.now() - new Date(memory.timestamp)) / (1000 * 60 * 60 * 24);
1285
  score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost
1286
 
1287
  // Boost frequently accessed memories
@@ -1311,7 +1611,7 @@ class KimiMemorySystem {
1311
  let score = 0;
1312
 
1313
  // Enhanced content similarity with keyword matching
1314
- score += this.calculateSimilarity(memory.content, context) * 0.35;
1315
 
1316
  // Keyword overlap boost (derived keywords)
1317
  try {
@@ -1319,7 +1619,7 @@ class KimiMemorySystem {
1319
  const ctxKeys = this.deriveKeywords(context || "");
1320
  const keyOverlap = ctxKeys.filter(k => memKeys.includes(k)).length;
1321
  if (ctxKeys.length > 0) {
1322
- score += (keyOverlap / ctxKeys.length) * 0.25; // significant boost for keyword overlap
1323
  }
1324
  } catch (e) {
1325
  // fallback to original keyword matching
@@ -1330,22 +1630,24 @@ class KimiMemorySystem {
1330
  }
1331
  }
1332
  if (contextWords.length > 0) {
1333
- score += (keywordMatches / contextWords.length) * 0.3;
1334
  }
1335
  }
1336
 
1337
- // (legacy keyword matching handled above)
1338
-
1339
  // Category relevance bonus based on context
1340
- score += this.getCategoryRelevance(memory.category, context) * 0.1;
1341
 
1342
  // Recent memories get bonus for current conversation
1343
- const daysSinceCreation = (Date.now() - new Date(memory.timestamp)) / (1000 * 60 * 60 * 24);
1344
- score += Math.max(0, (30 - daysSinceCreation) / 30) * 0.1;
 
 
 
 
1345
 
1346
  // Confidence and importance boost
1347
- score += (memory.confidence || 0.5) * 0.05;
1348
- score += (memory.importance || 0.5) * 0.05;
1349
 
1350
  return Math.min(1.0, score);
1351
  }
@@ -1538,30 +1840,56 @@ class KimiMemorySystem {
1538
  // Touch multiple memories to update lastAccess and accessCount
1539
  async _touchMemories(memories, limit = 5) {
1540
  if (!this.db || !Array.isArray(memories) || memories.length === 0) return;
 
1541
  try {
1542
  const top = memories.slice(0, limit);
1543
- const ops = [];
 
 
 
 
 
 
1544
  for (const m of top) {
1545
  try {
1546
  const id = m.id;
1547
  const existing = await this.db.db.memories.get(id);
1548
  if (existing) {
1549
  const lastAccess = existing.lastAccess ? new Date(existing.lastAccess).getTime() : 0;
1550
- const minMinutes = window.KIMI_MEMORY_TOUCH_MINUTES || 60;
1551
- const now = Date.now();
1552
- if (now - lastAccess > minMinutes * 60 * 1000) {
1553
- existing.accessCount = (existing.accessCount || 0) + 1;
1554
- existing.lastAccess = new Date();
1555
- ops.push(this.db.db.memories.put(existing));
 
 
 
 
1556
  }
1557
  }
1558
  } catch (e) {
1559
- console.warn("Error touching memory", m && m.id, e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1560
  }
1561
  }
1562
- await Promise.all(ops);
1563
  } catch (e) {
1564
- console.warn("Error in _touchMemories", e);
1565
  }
1566
  }
1567
 
@@ -1654,7 +1982,7 @@ class KimiMemorySystem {
1654
  // Exclude existing summaries to avoid summarizing summaries repeatedly
1655
  const recent = all.filter(
1656
  m =>
1657
- new Date(m.timestamp).getTime() >= cutoff &&
1658
  m.isActive &&
1659
  m.type !== "summary" &&
1660
  !(m.tags && m.tags.includes("summary"))
@@ -1688,7 +2016,7 @@ class KimiMemorySystem {
1688
  sourceText: summaryContent,
1689
  summaryJson: JSON.stringify(summaryJson),
1690
  confidence: 0.9,
1691
- timestamp: new Date(),
1692
  character: this.selectedCharacter,
1693
  isActive: true,
1694
  tags: ["summary"]
@@ -1723,7 +2051,7 @@ class KimiMemorySystem {
1723
  // Exclude existing summaries to avoid recursive summarization
1724
  const recent = all.filter(
1725
  m =>
1726
- new Date(m.timestamp).getTime() >= cutoff &&
1727
  m.isActive &&
1728
  m.type !== "summary" &&
1729
  !(m.tags && m.tags.includes("summary"))
@@ -1731,7 +2059,7 @@ class KimiMemorySystem {
1731
  if (!recent.length) return null;
1732
 
1733
  // Build aggregate content from readable fields in chronological order
1734
- recent.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
1735
  const texts = recent
1736
  .map(r => {
1737
  const raw =
@@ -1962,5 +2290,3 @@ class KimiMemorySystem {
1962
 
1963
  window.KimiMemorySystem = KimiMemorySystem;
1964
  export default KimiMemorySystem;
1965
-
1966
- window.KimiMemorySystem = KimiMemorySystem;
 
4
  this.db = database;
5
  this.memoryEnabled = true;
6
  this.maxMemoryEntries = 100;
7
+
8
+ // Performance optimization: keyword cache with LRU eviction
9
+ this.keywordCache = new Map(); // keyword_language -> boolean (is common)
10
+ this.keywordCacheSize = 1000; // Limit memory usage
11
+ this.keywordCacheHits = 0;
12
+ this.keywordCacheMisses = 0;
13
+
14
+ // Performance monitoring
15
+ this.queryStats = {
16
+ extractionTime: [],
17
+ addMemoryTime: [],
18
+ retrievalTime: []
19
+ };
20
+
21
+ // Centralized configuration for all thresholds and magic numbers
22
+ this.config = {
23
+ // Content validation thresholds
24
+ minContentLength: 2,
25
+ longContentThreshold: 24,
26
+ titleWordCount: {
27
+ preferred: 3,
28
+ min: 1,
29
+ max: 5
30
+ },
31
+
32
+ // Similarity and confidence thresholds
33
+ similarity: {
34
+ personal: 0.6, // Names can vary more (Jean vs Jean-Pierre)
35
+ preferences: 0.7, // Preferences can be expressed differently
36
+ default: 0.8, // General similarity threshold
37
+ veryHigh: 0.9, // For boost_confidence strategy
38
+ update: 0.3 // Lower threshold for memory updates
39
+ },
40
+
41
+ // Confidence scoring
42
+ confidence: {
43
+ base: 0.6,
44
+ explicitRequest: 1.0,
45
+ naturalExpression: 0.7,
46
+ bonusForLongContent: 0.1,
47
+ bonusForExplicitStatement: 0.3,
48
+ penaltyForUncertainty: 0.2,
49
+ min: 0.1,
50
+ max: 1.0
51
+ },
52
+
53
+ // Memory management
54
+ cleanup: {
55
+ maxEntries: 100,
56
+ ttlDays: 365,
57
+ batchSize: 100,
58
+ touchMinutes: 60
59
+ },
60
+
61
+ // Performance settings
62
+ cache: {
63
+ keywordCacheSize: 1000,
64
+ statHistorySize: 100
65
+ },
66
+
67
+ // Scoring weights for importance calculation
68
+ importance: {
69
+ categoryWeights: {
70
+ important: 1.0,
71
+ personal: 0.9,
72
+ relationships: 0.85,
73
+ goals: 0.75,
74
+ experiences: 0.65,
75
+ preferences: 0.6,
76
+ activities: 0.5
77
+ },
78
+ bonuses: {
79
+ relationshipMilestone: 0.15,
80
+ boundaries: 0.15,
81
+ strongEmotion: 0.05,
82
+ futureReference: 0.05,
83
+ longContent: 0.05,
84
+ highConfidence: 0.05
85
+ }
86
+ },
87
+
88
+ // Relevance calculation weights
89
+ relevance: {
90
+ contentSimilarity: 0.35,
91
+ keywordOverlap: 0.25,
92
+ categoryRelevance: 0.1,
93
+ recencyBonus: 0.1,
94
+ confidenceBonus: 0.05,
95
+ importanceBonus: 0.05,
96
+ recentDaysThreshold: 30
97
+ }
98
+ };
99
+
100
  this.memoryCategories = {
101
  personal: "Personal Information",
102
  preferences: "Likes & Dislikes",
 
293
  /请记住(.+)/i
294
  ]
295
  };
296
+
297
+ // Performance optimization: pre-compile regex patterns
298
+ this.compiledPatterns = {};
299
+ this.initializeCompiledPatterns();
300
+ }
301
+
302
+ // Pre-compile all regex patterns for better performance
303
+ initializeCompiledPatterns() {
304
+ try {
305
+ for (const [category, patterns] of Object.entries(this.extractionPatterns)) {
306
+ this.compiledPatterns[category] = patterns.map(pattern => {
307
+ if (pattern instanceof RegExp) {
308
+ return pattern; // Already compiled
309
+ }
310
+ return new RegExp(pattern.source, pattern.flags);
311
+ });
312
+ }
313
+
314
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
315
+ const totalPatterns = Object.values(this.compiledPatterns).reduce((sum, arr) => sum + arr.length, 0);
316
+ console.log(`🚀 Pre-compiled ${totalPatterns} regex patterns for memory extraction`);
317
+ }
318
+ } catch (error) {
319
+ console.error("Error pre-compiling regex patterns:", error);
320
+ // Fallback: use original patterns
321
+ this.compiledPatterns = this.extractionPatterns;
322
+ }
323
+ }
324
+
325
+ // Utility method to get consistent creation timestamp
326
+ getCreationTimestamp(memory) {
327
+ // Prefer createdAt, fallback to timestamp for backward compatibility
328
+ return memory.createdAt || memory.timestamp || new Date();
329
+ }
330
+
331
+ // Utility method to calculate days since creation
332
+ getDaysSinceCreation(memory) {
333
+ const created = new Date(this.getCreationTimestamp(memory)).getTime();
334
+ return (Date.now() - created) / (1000 * 60 * 60 * 24);
335
  }
336
 
337
  async init() {
 
348
  this.selectedCharacter = await this.db.getSelectedCharacter();
349
  await this.createMemoryTables();
350
 
351
+ // Legacy migrations disabled - uncomment if needed for old databases
352
+ // await this.migrateIncompatibleIDs();
353
+ // this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e));
 
 
354
  } catch (error) {
355
  console.error("Memory system initialization error:", error);
356
  }
 
368
  async extractMemoryFromText(userText, kimiResponse = null) {
369
  if (!this.memoryEnabled || !userText) return [];
370
 
371
+ // Ensure selectedCharacter is initialized
372
+ if (!this.selectedCharacter) {
373
+ this.selectedCharacter = this.db ? await this.db.getSelectedCharacter() : "kimi";
374
+ }
375
+
376
  const extractedMemories = [];
377
  const text = userText.toLowerCase();
378
 
379
+ // Memory extraction processing (debug info reduced for performance)
380
 
381
  // Enhanced extraction with context awareness
382
  const existingMemories = await this.getAllMemories();
 
384
  // First, check for explicit memory requests
385
  const explicitRequests = this.detectExplicitMemoryRequests(userText);
386
  if (explicitRequests.length > 0) {
387
+ // Explicit memory requests detected
388
  extractedMemories.push(...explicitRequests);
389
  }
390
 
391
+ // Extract using pre-compiled patterns for better performance
392
+ const patternsToUse = this.compiledPatterns || this.extractionPatterns;
393
+ for (const [category, patterns] of Object.entries(patternsToUse)) {
394
  for (const pattern of patterns) {
395
  const match = text.match(pattern);
396
  if (match && match[1]) {
397
  const content = match[1].trim();
398
 
399
  // Skip very short or generic content
400
+ if (content.length < this.config.minContentLength || this.isGenericContent(content)) {
401
  continue;
402
  }
403
 
 
410
  content: content,
411
  sourceText: userText,
412
  confidence: this.calculateExtractionConfidence(match, userText),
413
+ createdAt: new Date(), // Use createdAt consistently
414
+ character: this.selectedCharacter || "kimi", // Fallback protection
415
  isUpdate: isUpdate
416
  };
417
 
418
+ // Pattern match detected
419
  extractedMemories.push(memory);
420
  }
421
  }
 
428
  // Save extracted memories with intelligent deduplication
429
  const savedMemories = [];
430
  for (const memory of extractedMemories) {
431
+ try {
432
+ console.log("💾 Saving memory:", memory.content);
433
+ const saved = await this.addMemory(memory);
434
+ if (saved) {
435
+ savedMemories.push(saved);
436
+ } else {
437
+ console.warn("⚠️ Memory was not saved (possibly filtered or merged):", memory.content);
438
+ }
439
+ } catch (error) {
440
+ console.error("❌ Failed to save memory:", {
441
+ content: memory.content,
442
+ category: memory.category,
443
+ error: error.message
444
+ });
445
+ // Continue processing other memories even if one fails
446
+ }
447
  }
448
 
449
  if (savedMemories.length > 0) {
450
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
451
+ console.log(`✅ Successfully extracted and saved ${savedMemories.length} memories`);
452
+ }
453
+ } else if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
454
  console.log("📝 No memories extracted from this text");
455
  }
456
 
 
620
  // Check if content is too generic to be useful
621
  isGenericContent(content) {
622
  const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"];
623
+ return genericWords.includes(content.toLowerCase()) || content.length < this.config.minContentLength;
624
  }
625
 
626
  // Calculate confidence based on context and pattern strength
627
  calculateExtractionConfidence(match, fullText) {
628
+ let confidence = this.config.confidence.base; // Base confidence from config
629
 
630
  // Boost confidence for explicit statements
631
  const lower = fullText.toLowerCase();
 
647
  lower.includes("我叫") ||
648
  lower.includes("我的名字是")
649
  ) {
650
+ confidence += this.config.confidence.bonusForExplicitStatement;
651
  }
652
 
653
  // Boost for longer, more specific content
654
+ if (match[1] && match[1].trim().length > this.config.longContentThreshold) {
655
+ confidence += this.config.confidence.bonusForLongContent;
656
  }
657
 
658
  // Reduce confidence for uncertain language
659
  if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) {
660
+ confidence -= this.config.confidence.penaltyForUncertainty;
661
  }
662
 
663
+ return Math.min(this.config.confidence.max, Math.max(this.config.confidence.min, confidence));
664
  }
665
 
666
  // Generate a short title (2-5 words max) from content for auto-extracted memories
 
676
  if (words.length === 0) return "";
677
  // Prefer 3 words when available, minimum 2 when possible, maximum 5
678
  let take;
679
+ if (words.length >= this.config.titleWordCount.preferred) take = this.config.titleWordCount.preferred;
680
  else take = words.length; // 1 or 2
681
+ take = Math.min(this.config.titleWordCount.max, Math.max(this.config.titleWordCount.min, take));
682
 
683
  const slice = words.slice(0, take);
684
  // Capitalize first word for nicer title
 
692
 
693
  for (const memory of categoryMemories) {
694
  const similarity = this.calculateSimilarity(memory.content, content);
695
+ if (similarity > this.config.similarity.update) {
696
  // Lower threshold for updates
697
  return true;
698
  }
 
749
  content: name,
750
  sourceText: text,
751
  confidence: 0.7,
752
+ createdAt: new Date(), // Use createdAt consistently
753
+ character: this.selectedCharacter || "kimi" // Fallback protection
754
  });
755
  }
756
  }
 
839
  : "",
840
  sourceText: memoryData.sourceText || "",
841
  confidence: memoryData.confidence || 1.0,
842
+ createdAt: memoryData.createdAt || memoryData.timestamp || now, // Unified timestamp handling
843
+ character: memoryData.character || this.selectedCharacter || "kimi", // Fallback protection
844
  isActive: true,
845
  tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])],
846
  lastModified: now,
 
847
  lastAccess: now,
848
  accessCount: 0,
849
  importance: this.calculateImportance(memoryData)
 
852
  if (this.db.db.memories) {
853
  const id = await this.db.db.memories.add(memory);
854
  memory.id = id; // Store the auto-generated ID
855
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
856
+ console.log(`Memory added with ID: ${id}`);
857
+ }
858
  }
859
 
860
  // Cleanup old memories if we exceed limit
 
866
  return memory;
867
  } catch (error) {
868
  console.error("Error adding memory:", error);
869
+ return null; // Return null instead of undefined for clearer error handling
870
  }
871
  }
872
 
 
935
  }
936
  }
937
 
938
+ // Simplified memory merge strategy determination
939
  determineMergeStrategy(existing, newData) {
940
  const similarity = this.calculateSimilarity(existing.content, newData.content);
941
+ const newConfidence = newData.confidence || this.config.confidence.base;
942
+ const existingConfidence = existing.confidence || this.config.confidence.base;
943
 
944
+ // Very high similarity (>90%) - boost confidence if new is more confident
945
+ if (similarity > this.config.similarity.veryHigh) {
946
+ return newConfidence > existingConfidence ? "boost_confidence" : "merge_content";
947
  }
948
 
949
+ // High similarity (>70%) - decide based on content length and specificity
950
+ if (similarity > this.config.similarity.preferences) {
951
+ // If new content is significantly longer (50% more), it's likely more detailed
952
  if (newData.content.length > existing.content.length * 1.5) {
953
+ return "update_content";
 
 
954
  }
955
+ // If existing is longer, merge to preserve information
956
+ return "merge_content";
957
  }
958
 
959
+ // For personal names, handle as variants if they're related
960
  if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) {
961
  return "add_variant";
962
  }
963
 
964
+ // Default strategy for moderate similarity
965
  return "merge_content";
966
  }
967
 
 
1030
  }
1031
 
1032
  // Longer details and high confidence
1033
+ if (memoryData.content && memoryData.content.length > this.config.longContentThreshold)
1034
+ importance += this.config.importance.bonuses.longContent;
1035
+ if (memoryData.confidence && memoryData.confidence > 0.9) importance += this.config.importance.bonuses.highConfidence;
1036
 
1037
  // Round to two decimals to avoid floating point artifacts
1038
  return Math.min(1.0, Math.round(importance * 100) / 100);
 
1166
  if (!this.db) return [];
1167
 
1168
  try {
1169
+ character = character || this.selectedCharacter || "kimi"; // Unified fallback
1170
 
1171
  if (this.db.db.memories) {
1172
  const memories = await this.db.db.memories
 
1190
  if (!this.db) return [];
1191
 
1192
  try {
1193
+ character = character || this.selectedCharacter || "kimi";
1194
 
1195
  if (this.db.db.memories) {
1196
+ // Use simple character filter - compatible with all data
1197
  const memories = await this.db.db.memories
1198
  .where("character")
1199
  .equals(character)
1200
+ .filter(memory => memory.isActive !== false) // Include records without isActive field
1201
  .reverse()
1202
  .sortBy("timestamp");
1203
 
 
1234
  const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
1235
 
1236
  // Different thresholds based on category
1237
+ const threshold = this.config.similarity[memoryData.category] || this.config.similarity.default;
 
 
 
 
 
1238
 
1239
  if (contentSimilarity > threshold) {
1240
  return memory;
 
1323
  .toLowerCase()
1324
  .replace(/[\p{P}\p{S}]/gu, " ")
1325
  .split(/\s+/)
1326
+ .filter(w => w.length > 2 && !this.isCommonWordSafe(w))
1327
  )
1328
  ];
1329
  }
1330
 
1331
+ // Safe wrapper for isCommonWord to avoid undefined function errors
1332
+ isCommonWordSafe(word, language = "en") {
1333
+ const cacheKey = `${word.toLowerCase()}_${language}`;
1334
+
1335
+ // Check cache first
1336
+ if (this.keywordCache.has(cacheKey)) {
1337
+ this.keywordCacheHits++;
1338
+ return this.keywordCache.get(cacheKey);
1339
+ }
1340
+
1341
+ // Cache miss - compute the result
1342
+ this.keywordCacheMisses++;
1343
+ let isCommon = false;
1344
+
1345
+ try {
1346
+ isCommon = typeof this.isCommonWord === "function" ? this.isCommonWord(word, language) : false;
1347
+ } catch (error) {
1348
+ console.warn("Error checking common word:", error);
1349
+ isCommon = false;
1350
+ }
1351
+
1352
+ // Add to cache with LRU eviction
1353
+ if (this.keywordCache.size >= this.keywordCacheSize) {
1354
+ // Simple LRU: remove oldest entry (first in Map)
1355
+ const firstKey = this.keywordCache.keys().next().value;
1356
+ this.keywordCache.delete(firstKey);
1357
+ }
1358
+
1359
+ this.keywordCache.set(cacheKey, isCommon);
1360
+ return isCommon;
1361
+ }
1362
+
1363
+ // Get cache statistics for debugging
1364
+ getKeywordCacheStats() {
1365
+ const total = this.keywordCacheHits + this.keywordCacheMisses;
1366
+ return {
1367
+ size: this.keywordCache.size,
1368
+ hits: this.keywordCacheHits,
1369
+ misses: this.keywordCacheMisses,
1370
+ hitRate: total > 0 ? ((this.keywordCacheHits / total) * 100).toFixed(2) + "%" : "0%"
1371
+ };
1372
+ }
1373
+
1374
+ // Get performance statistics for debugging and optimization
1375
+ getPerformanceStats() {
1376
+ const calculateStats = times => {
1377
+ if (times.length === 0) return { avg: 0, max: 0, min: 0, count: 0 };
1378
+ return {
1379
+ avg: Math.round((times.reduce((sum, t) => sum + t, 0) / times.length) * 100) / 100,
1380
+ max: Math.round(Math.max(...times) * 100) / 100,
1381
+ min: Math.round(Math.min(...times) * 100) / 100,
1382
+ count: times.length
1383
+ };
1384
+ };
1385
+
1386
+ return {
1387
+ keywordCache: this.getKeywordCacheStats(),
1388
+ extraction: calculateStats(this.queryStats.extractionTime),
1389
+ addMemory: calculateStats(this.queryStats.addMemoryTime),
1390
+ retrieval: calculateStats(this.queryStats.retrievalTime)
1391
+ };
1392
+ }
1393
+
1394
+ // Performance wrapper for memory extraction
1395
+ async extractMemoryFromTextTimed(userText, kimiResponse = null) {
1396
+ const start = performance.now();
1397
+ const result = await this.extractMemoryFromText(userText, kimiResponse);
1398
+ const duration = performance.now() - start;
1399
+
1400
+ this.queryStats.extractionTime.push(duration);
1401
+ if (this.queryStats.extractionTime.length > 100) {
1402
+ this.queryStats.extractionTime.shift(); // Keep only last 100 measurements
1403
+ }
1404
+
1405
+ if (duration > 100 && window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1406
+ console.warn(`🐌 Slow memory extraction: ${duration.toFixed(2)}ms for text length ${userText?.length || 0}`);
1407
+ }
1408
+
1409
+ return result;
1410
+ }
1411
+
1412
+ // Get current configuration for debugging and monitoring
1413
+ getConfiguration() {
1414
+ return {
1415
+ ...this.config,
1416
+ memoryCategories: this.memoryCategories,
1417
+ runtime: {
1418
+ memoryEnabled: this.memoryEnabled,
1419
+ maxMemoryEntries: this.maxMemoryEntries,
1420
+ selectedCharacter: this.selectedCharacter,
1421
+ keywordCacheSize: this.keywordCache.size,
1422
+ compiledPatternsCount: Object.values(this.compiledPatterns || {}).reduce((sum, arr) => sum + arr.length, 0)
1423
+ }
1424
+ };
1425
+ }
1426
+
1427
+ // Update configuration at runtime (for advanced users)
1428
+ updateConfiguration(configPath, value) {
1429
+ const keys = configPath.split(".");
1430
+ let current = this.config;
1431
+
1432
+ // Navigate to the parent object
1433
+ for (let i = 0; i < keys.length - 1; i++) {
1434
+ if (!current[keys[i]]) current[keys[i]] = {};
1435
+ current = current[keys[i]];
1436
+ }
1437
+
1438
+ // Set the value
1439
+ const lastKey = keys[keys.length - 1];
1440
+ const oldValue = current[lastKey];
1441
+ current[lastKey] = value;
1442
+
1443
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1444
+ console.log(`🔧 Configuration updated: ${configPath} = ${value} (was: ${oldValue})`);
1445
+ }
1446
+
1447
+ return { oldValue, newValue: value };
1448
+ }
1449
+
1450
  async cleanupOldMemories() {
1451
  if (!this.db) return;
1452
 
 
1460
  // Soft-expire memories older than TTL by marking isActive=false
1461
  const now = Date.now();
1462
  const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
1463
+ const expiredMemories = [];
1464
+
1465
  for (const mem of memories) {
1466
+ const created = new Date(this.getCreationTimestamp(mem)).getTime();
1467
  if (now - created > ttlMs) {
1468
  try {
1469
  await this.updateMemory(mem.id, { isActive: false });
1470
+ expiredMemories.push(mem.id);
1471
  } catch (e) {
1472
+ console.error(`Memory expiration failed for ID ${mem.id}:`, {
1473
+ error: e.message,
1474
+ memoryId: mem.id,
1475
+ createdAt: this.getCreationTimestamp(mem),
1476
+ character: mem.character
1477
+ });
1478
+ // Continue with other memories even if one fails
1479
  }
1480
  }
1481
  }
1482
 
1483
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY && expiredMemories.length > 0) {
1484
+ console.log(`Successfully expired ${expiredMemories.length} memories:`, expiredMemories);
1485
+ }
1486
+
1487
  // Refresh active memories after TTL purge
1488
  const activeMemories = (await this.getAllMemories()).filter(m => m.isActive);
1489
 
 
1494
  const scoreA =
1495
  (a.importance || 0.5) * -1 +
1496
  (a.accessCount || 0) * 0.01 +
1497
+ new Date(this.getCreationTimestamp(a)).getTime() / (1000 * 60 * 60 * 24);
1498
  const scoreB =
1499
  (b.importance || 0.5) * -1 +
1500
  (b.accessCount || 0) * 0.01 +
1501
+ new Date(this.getCreationTimestamp(b)).getTime() / (1000 * 60 * 60 * 24);
1502
  return scoreB - scoreA;
1503
  });
1504
 
1505
  const toDeactivate = activeMemories.slice(maxEntries);
1506
+ const deactivatedMemories = [];
1507
+ const failedDeactivations = [];
1508
+
1509
  for (const mem of toDeactivate) {
1510
  try {
1511
  await this.updateMemory(mem.id, { isActive: false });
1512
+ deactivatedMemories.push(mem.id);
1513
  } catch (e) {
1514
+ console.error(`Memory deactivation failed for ID ${mem.id}:`, {
1515
+ error: e.message,
1516
+ memoryId: mem.id,
1517
+ importance: mem.importance,
1518
+ character: mem.character
1519
+ });
1520
+ failedDeactivations.push(mem.id);
1521
  }
1522
  }
1523
+
1524
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1525
+ console.log(
1526
+ `Memory cleanup: ${deactivatedMemories.length} deactivated, ${failedDeactivations.length} failed`
1527
+ );
1528
+ }
1529
  }
1530
  } catch (error) {
1531
  console.error("Error cleaning up old memories:", error);
 
1581
  let score = memory.importance || 0.5;
1582
 
1583
  // Boost recent memories
1584
+ const daysSinceCreation = this.getDaysSinceCreation(memory);
1585
  score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost
1586
 
1587
  // Boost frequently accessed memories
 
1611
  let score = 0;
1612
 
1613
  // Enhanced content similarity with keyword matching
1614
+ score += this.calculateSimilarity(memory.content, context) * this.config.relevance.contentSimilarity;
1615
 
1616
  // Keyword overlap boost (derived keywords)
1617
  try {
 
1619
  const ctxKeys = this.deriveKeywords(context || "");
1620
  const keyOverlap = ctxKeys.filter(k => memKeys.includes(k)).length;
1621
  if (ctxKeys.length > 0) {
1622
+ score += (keyOverlap / ctxKeys.length) * this.config.relevance.keywordOverlap;
1623
  }
1624
  } catch (e) {
1625
  // fallback to original keyword matching
 
1630
  }
1631
  }
1632
  if (contextWords.length > 0) {
1633
+ score += (keywordMatches / contextWords.length) * this.config.relevance.keywordOverlap;
1634
  }
1635
  }
1636
 
 
 
1637
  // Category relevance bonus based on context
1638
+ score += this.getCategoryRelevance(memory.category, context) * this.config.relevance.categoryRelevance;
1639
 
1640
  // Recent memories get bonus for current conversation
1641
+ const daysSinceCreation = this.getDaysSinceCreation(memory);
1642
+ score +=
1643
+ Math.max(
1644
+ 0,
1645
+ (this.config.relevance.recentDaysThreshold - daysSinceCreation) / this.config.relevance.recentDaysThreshold
1646
+ ) * this.config.relevance.recencyBonus;
1647
 
1648
  // Confidence and importance boost
1649
+ score += (memory.confidence || 0.5) * this.config.relevance.confidenceBonus;
1650
+ score += (memory.importance || 0.5) * this.config.relevance.importanceBonus;
1651
 
1652
  return Math.min(1.0, score);
1653
  }
 
1840
  // Touch multiple memories to update lastAccess and accessCount
1841
  async _touchMemories(memories, limit = 5) {
1842
  if (!this.db || !Array.isArray(memories) || memories.length === 0) return;
1843
+
1844
  try {
1845
  const top = memories.slice(0, limit);
1846
+ const now = new Date();
1847
+ const minMinutes = window.KIMI_MEMORY_TOUCH_MINUTES || 60;
1848
+ const minTouchInterval = minMinutes * 60 * 1000;
1849
+
1850
+ // Batch collection: gather all updates before executing
1851
+ const batchUpdates = [];
1852
+
1853
  for (const m of top) {
1854
  try {
1855
  const id = m.id;
1856
  const existing = await this.db.db.memories.get(id);
1857
  if (existing) {
1858
  const lastAccess = existing.lastAccess ? new Date(existing.lastAccess).getTime() : 0;
1859
+
1860
+ // Only touch if enough time has passed
1861
+ if (now.getTime() - lastAccess > minTouchInterval) {
1862
+ batchUpdates.push({
1863
+ key: id,
1864
+ changes: {
1865
+ accessCount: (existing.accessCount || 0) + 1,
1866
+ lastAccess: now
1867
+ }
1868
+ });
1869
  }
1870
  }
1871
  } catch (e) {
1872
+ console.warn("Error preparing memory touch batch for", m && m.id, e);
1873
+ }
1874
+ }
1875
+
1876
+ // Execute all updates in a single batch operation
1877
+ if (batchUpdates.length > 0) {
1878
+ if (this.db.db.memories.bulkUpdate) {
1879
+ // Use bulkUpdate if available (Dexie 3.x+)
1880
+ await this.db.db.memories.bulkUpdate(batchUpdates);
1881
+ } else {
1882
+ // Fallback: parallel individual updates (still better than sequential)
1883
+ const updatePromises = batchUpdates.map(update => this.db.db.memories.update(update.key, update.changes));
1884
+ await Promise.all(updatePromises);
1885
+ }
1886
+
1887
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1888
+ console.log(`📊 Batch touched ${batchUpdates.length} memories`);
1889
  }
1890
  }
 
1891
  } catch (e) {
1892
+ console.warn("Error in _touchMemories batch processing", e);
1893
  }
1894
  }
1895
 
 
1982
  // Exclude existing summaries to avoid summarizing summaries repeatedly
1983
  const recent = all.filter(
1984
  m =>
1985
+ new Date(this.getCreationTimestamp(m)).getTime() >= cutoff &&
1986
  m.isActive &&
1987
  m.type !== "summary" &&
1988
  !(m.tags && m.tags.includes("summary"))
 
2016
  sourceText: summaryContent,
2017
  summaryJson: JSON.stringify(summaryJson),
2018
  confidence: 0.9,
2019
+ createdAt: new Date(), // Use createdAt consistently
2020
  character: this.selectedCharacter,
2021
  isActive: true,
2022
  tags: ["summary"]
 
2051
  // Exclude existing summaries to avoid recursive summarization
2052
  const recent = all.filter(
2053
  m =>
2054
+ new Date(this.getCreationTimestamp(m)).getTime() >= cutoff &&
2055
  m.isActive &&
2056
  m.type !== "summary" &&
2057
  !(m.tags && m.tags.includes("summary"))
 
2059
  if (!recent.length) return null;
2060
 
2061
  // Build aggregate content from readable fields in chronological order
2062
+ recent.sort((a, b) => new Date(this.getCreationTimestamp(a)) - new Date(this.getCreationTimestamp(b)));
2063
  const texts = recent
2064
  .map(r => {
2065
  const raw =
 
2290
 
2291
  window.KimiMemorySystem = KimiMemorySystem;
2292
  export default KimiMemorySystem;
 
 
kimi-js/kimi-memory.js CHANGED
@@ -22,14 +22,12 @@ class KimiMemory {
22
  }
23
  try {
24
  this.selectedCharacter = await this.db.getSelectedCharacter();
25
- // Start with lower favorability level - relationships must be built over time
26
- this.favorabilityLevel = await this.db.getPreference(`favorabilityLevel_${this.selectedCharacter}`, 50);
27
 
28
- // Load affection trait from personality database with coherent defaults
29
  const charDefAff =
30
  (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[this.selectedCharacter]?.traits?.affection) || null;
31
- const genericAff = (window.getTraitDefaults && window.getTraitDefaults().affection) || 55;
32
- const defaultAff = typeof charDefAff === "number" ? charDefAff : genericAff;
33
  this.affectionTrait = await this.db.getPersonalityTrait("affection", defaultAff, this.selectedCharacter);
34
 
35
  this.preferences = {
@@ -53,7 +51,17 @@ class KimiMemory {
53
 
54
  try {
55
  const character = await this.db.getSelectedCharacter();
56
- await this.db.saveConversation(userText, kimiResponse, this.favorabilityLevel, new Date(), character);
 
 
 
 
 
 
 
 
 
 
57
 
58
  // Legacy interactions counter kept for backward compatibility (not shown in UI now)
59
  let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
@@ -100,7 +108,12 @@ class KimiMemory {
100
  try {
101
  this.selectedCharacter = await this.db.getSelectedCharacter();
102
  // Use unified default that matches KimiEmotionSystem
103
- this.affectionTrait = await this.db.getPersonalityTrait("affection", 50, this.selectedCharacter);
 
 
 
 
 
104
  this.updateFavorabilityBar();
105
  } catch (error) {
106
  console.error("Error updating affection trait:", error);
@@ -117,13 +130,24 @@ class KimiMemory {
117
  }
118
  }
119
 
120
- getGreeting() {
121
  const i18n = window.kimiI18nManager;
122
 
123
- if (this.affectionTrait <= 10) {
 
 
 
 
 
 
 
 
 
 
 
124
  return i18n?.t("greeting_low") || "Hello.";
125
  }
126
- if (this.affectionTrait < 40) {
127
  return i18n?.t("greeting_mid") || "Hi. How can I help you?";
128
  }
129
  return i18n?.t("greeting_high") || "Hello my love! 💕";
 
22
  }
23
  try {
24
  this.selectedCharacter = await this.db.getSelectedCharacter();
 
 
25
 
26
+ // Load affection trait from personality database with unified defaults
27
  const charDefAff =
28
  (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[this.selectedCharacter]?.traits?.affection) || null;
29
+ const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { affection: 55 };
30
+ const defaultAff = typeof charDefAff === "number" ? charDefAff : unifiedDefaults.affection;
31
  this.affectionTrait = await this.db.getPersonalityTrait("affection", defaultAff, this.selectedCharacter);
32
 
33
  this.preferences = {
 
51
 
52
  try {
53
  const character = await this.db.getSelectedCharacter();
54
+
55
+ // Use global personality average for conversation favorability score
56
+ let relationshipLevel = 50; // fallback
57
+ try {
58
+ const traits = await this.db.getAllPersonalityTraits(character);
59
+ relationshipLevel = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
60
+ } catch (error) {
61
+ console.warn("Error calculating relationship level for conversation:", error);
62
+ }
63
+
64
+ await this.db.saveConversation(userText, kimiResponse, relationshipLevel, new Date(), character);
65
 
66
  // Legacy interactions counter kept for backward compatibility (not shown in UI now)
67
  let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
 
108
  try {
109
  this.selectedCharacter = await this.db.getSelectedCharacter();
110
  // Use unified default that matches KimiEmotionSystem
111
+ const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { affection: 55 };
112
+ this.affectionTrait = await this.db.getPersonalityTrait(
113
+ "affection",
114
+ unifiedDefaults.affection,
115
+ this.selectedCharacter
116
+ );
117
  this.updateFavorabilityBar();
118
  } catch (error) {
119
  console.error("Error updating affection trait:", error);
 
130
  }
131
  }
132
 
133
+ async getGreeting() {
134
  const i18n = window.kimiI18nManager;
135
 
136
+ // Use global personality average instead of just affection trait
137
+ let relationshipLevel = 50; // fallback
138
+ try {
139
+ if (this.db) {
140
+ const traits = await this.db.getAllPersonalityTraits(this.selectedCharacter);
141
+ relationshipLevel = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
142
+ }
143
+ } catch (error) {
144
+ console.warn("Error calculating greeting level:", error);
145
+ }
146
+
147
+ if (relationshipLevel <= 10) {
148
  return i18n?.t("greeting_low") || "Hello.";
149
  }
150
+ if (relationshipLevel < 40) {
151
  return i18n?.t("greeting_mid") || "Hi. How can I help you?";
152
  }
153
  return i18n?.t("greeting_high") || "Hello my love! 💕";
kimi-js/kimi-module.js CHANGED
@@ -22,23 +22,9 @@ function updateFavorabilityLabel(characterKey) {
22
  }
23
  }
24
 
25
- // Delegated personality average computation (single source of truth in KimiEmotionSystem)
26
  function computePersonalityAverage(traits) {
27
- if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
28
- return Number(window.kimiEmotionSystem.calculatePersonalityAverage(traits).toFixed(2));
29
- }
30
- // Fallback minimal (should rarely occur before emotion system init)
31
- const keys = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
32
- let sum = 0,
33
- count = 0;
34
- for (const k of keys) {
35
- const v = traits && traits[k];
36
- if (typeof v === "number" && isFinite(v)) {
37
- sum += Math.max(0, Math.min(100, v));
38
- count++;
39
- }
40
- }
41
- return count ? Number((sum / count).toFixed(2)) : 0;
42
  }
43
 
44
  // Update UI elements (bar + percentage text + label) based on overall personality average
@@ -293,10 +279,8 @@ async function analyzeAndReact(text, useAdvancedLLM = true, onStreamToken = null
293
  const affection = typeof traits.affection === "number" ? traits.affection : 55;
294
  const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
295
 
296
- // Always reflect user's input phase with a listening video (voice or chat)
297
- if (kimiVideo && typeof kimiVideo.startListening === "function") {
298
- kimiVideo.startListening();
299
- }
300
 
301
  if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
302
  await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
@@ -341,13 +325,9 @@ async function analyzeAndReact(text, useAdvancedLLM = true, onStreamToken = null
341
  if (userAskedDance) {
342
  kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
343
  } else {
344
- kimiVideo.analyzeAndSelectVideo(
345
- sanitizedText,
346
- response,
347
- { reaction: reaction, intensity: emotionIntensity },
348
- updatedTraits,
349
- updatedTraits.affection
350
- );
351
  }
352
 
353
  if (kimiLLM.updatePersonalityFromResponse) {
@@ -528,7 +508,12 @@ function addMessageToChat(sender, text, conversationId = null) {
528
  messageTimeDiv.appendChild(deleteBtn);
529
 
530
  const textDiv = document.createElement("div");
531
- textDiv.textContent = text || ""; // Handle empty strings properly
 
 
 
 
 
532
 
533
  messageDiv.appendChild(textDiv);
534
  messageDiv.appendChild(messageTimeDiv);
@@ -539,7 +524,12 @@ function addMessageToChat(sender, text, conversationId = null) {
539
  // Return an object that allows updating the message content for streaming
540
  return {
541
  updateText: newText => {
542
- textDiv.textContent = newText;
 
 
 
 
 
543
  // Throttle scrolling to prevent visual stuttering during streaming
544
  if (!textDiv._scrollTimeout) {
545
  textDiv._scrollTimeout = setTimeout(() => {
@@ -973,7 +963,9 @@ async function loadAvailableModels() {
973
 
974
  // Only log once when models are loaded, not repeated calls
975
  if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) {
976
- console.log(`✅ Loaded ${Object.keys(stats.available).length} LLM models`);
 
 
977
  loadAvailableModels._lastLoadTime = Date.now();
978
  }
979
  const createCard = (id, model) => {
@@ -1243,25 +1235,7 @@ async function loadAvailableModels() {
1243
  }
1244
  }
1245
 
1246
- // Debug function for testing models loading
1247
- window.debugLoadModels = async function () {
1248
- console.log("🔧 Manual debug of loadAvailableModels");
1249
- console.log("🔧 window.kimiLLM:", window.kimiLLM);
1250
- console.log("🔧 Models container:", document.getElementById("models-container"));
1251
-
1252
- if (window.kimiLLM) {
1253
- try {
1254
- const stats = await window.kimiLLM.getModelStats();
1255
- console.log("🔧 Model stats:", stats);
1256
- } catch (error) {
1257
- console.error("🔧 Error getting model stats:", error);
1258
- }
1259
- }
1260
-
1261
- if (window.loadAvailableModels) {
1262
- await window.loadAvailableModels();
1263
- }
1264
- };
1265
 
1266
  async function sendMessage() {
1267
  const chatInput = document.getElementById("chat-input");
@@ -1320,7 +1294,10 @@ async function sendMessage() {
1320
  }
1321
 
1322
  try {
1323
- console.log("🔄 Starting streaming response...");
 
 
 
1324
  let emotionDetected = false;
1325
 
1326
  const response = await analyzeAndReact(message, true, token => {
@@ -1331,7 +1308,10 @@ async function sendMessage() {
1331
  // Progressive analysis disabled to prevent UI flickering during streaming
1332
  // All analysis will be done after streaming completes
1333
  });
1334
- console.log("✅ Streaming completed, final response length:", streamingResponse.length);
 
 
 
1335
 
1336
  // Final processing after streaming completes
1337
  let finalResponse = streamingResponse || response;
@@ -1968,54 +1948,19 @@ async function syncPersonalityTraits(characterName = null) {
1968
  return updatedTraits;
1969
  }
1970
 
1971
- // Function to validate emotion and context consistency
1972
  function validateEmotionContext(emotion) {
1973
- // Normalize video categories to base emotions before validation
1974
- const normalized = emotion === "speakingPositive" ? "positive" : emotion === "speakingNegative" ? "negative" : emotion;
1975
- // Use unified emotion system for validation
1976
- if (window.kimiEmotionSystem) {
1977
- return window.kimiEmotionSystem.validateEmotion(normalized);
1978
- }
1979
-
1980
- // Fallback validation
1981
- const validEmotions = [
1982
- "positive",
1983
- "negative",
1984
- "neutral",
1985
- "dancing",
1986
- "listening",
1987
- "romantic",
1988
- "laughing",
1989
- "surprise",
1990
- "confident",
1991
- "shy",
1992
- "flirtatious",
1993
- "kiss",
1994
- "goodbye",
1995
- "speakingPositive",
1996
- "speakingNegative",
1997
- "speaking"
1998
- ];
1999
-
2000
- if (!validEmotions.includes(normalized)) {
2001
- console.warn(`Invalid emotion detected: ${normalized}, falling back to neutral`);
2002
- return "neutral";
2003
- }
2004
-
2005
- return normalized;
2006
  }
2007
 
2008
- // Function to ensure video context consistency
2009
  async function ensureVideoContextConsistency() {
2010
- if (!window.kimiVideo) return;
2011
 
2012
- const kimiDB = window.kimiDB;
2013
- if (!kimiDB) return;
2014
-
2015
- const selectedCharacter = await kimiDB.getSelectedCharacter();
2016
- const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
2017
 
2018
- // Validate current video context
2019
  const currentInfo = window.kimiVideo.getCurrentVideoInfo();
2020
  const validatedEmotion = validateEmotionContext(currentInfo.emotion);
2021
 
 
22
  }
23
  }
24
 
25
+ // Simplified personality average computation using centralized system
26
  function computePersonalityAverage(traits) {
27
+ return window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
  // Update UI elements (bar + percentage text + label) based on overall personality average
 
279
  const affection = typeof traits.affection === "number" ? traits.affection : 55;
280
  const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
281
 
282
+ // Only trigger listening videos for voice input, NOT for text chat
283
+ // Text chat should keep neutral videos until LLM response processing begins
 
 
284
 
285
  if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
286
  await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
 
325
  if (userAskedDance) {
326
  kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
327
  } else {
328
+ // Use emotion analysis from the LLM RESPONSE, not user input
329
+ const responseEmotion = window.kimiEmotionSystem?.analyzeEmotionValidated(response) || "positive";
330
+ kimiVideo.respondWithEmotion(responseEmotion, updatedTraits, updatedTraits.affection);
 
 
 
 
331
  }
332
 
333
  if (kimiLLM.updatePersonalityFromResponse) {
 
508
  messageTimeDiv.appendChild(deleteBtn);
509
 
510
  const textDiv = document.createElement("div");
511
+ // Use formatted text with HTML support (secure formatting)
512
+ if (text && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) {
513
+ textDiv.innerHTML = window.KimiValidationUtils.formatChatText(text);
514
+ } else {
515
+ textDiv.textContent = text || ""; // Fallback to plain text
516
+ }
517
 
518
  messageDiv.appendChild(textDiv);
519
  messageDiv.appendChild(messageTimeDiv);
 
524
  // Return an object that allows updating the message content for streaming
525
  return {
526
  updateText: newText => {
527
+ // Use formatted text for streaming updates too
528
+ if (newText && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) {
529
+ textDiv.innerHTML = window.KimiValidationUtils.formatChatText(newText);
530
+ } else {
531
+ textDiv.textContent = newText;
532
+ }
533
  // Throttle scrolling to prevent visual stuttering during streaming
534
  if (!textDiv._scrollTimeout) {
535
  textDiv._scrollTimeout = setTimeout(() => {
 
963
 
964
  // Only log once when models are loaded, not repeated calls
965
  if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) {
966
+ if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
967
+ console.log(`✅ Loaded ${Object.keys(stats.available).length} LLM models`);
968
+ }
969
  loadAvailableModels._lastLoadTime = Date.now();
970
  }
971
  const createCard = (id, model) => {
 
1235
  }
1236
  }
1237
 
1238
+ // Debug utilities removed for production optimization
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1239
 
1240
  async function sendMessage() {
1241
  const chatInput = document.getElementById("chat-input");
 
1294
  }
1295
 
1296
  try {
1297
+ // Start streaming response processing
1298
+ if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
1299
+ console.log("🔄 Starting streaming response...");
1300
+ }
1301
  let emotionDetected = false;
1302
 
1303
  const response = await analyzeAndReact(message, true, token => {
 
1308
  // Progressive analysis disabled to prevent UI flickering during streaming
1309
  // All analysis will be done after streaming completes
1310
  });
1311
+ // Streaming completed
1312
+ if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
1313
+ console.log("✅ Streaming completed, final response length:", streamingResponse.length);
1314
+ }
1315
 
1316
  // Final processing after streaming completes
1317
  let finalResponse = streamingResponse || response;
 
1948
  return updatedTraits;
1949
  }
1950
 
1951
+ // Simplified validation using centralized emotion system
1952
  function validateEmotionContext(emotion) {
1953
+ return window.kimiEmotionSystem?.validateEmotion(emotion) || "neutral";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1954
  }
1955
 
1956
+ // Simplified video context consistency check using centralized system
1957
  async function ensureVideoContextConsistency() {
1958
+ if (!window.kimiVideo || !window.kimiDB) return;
1959
 
1960
+ const selectedCharacter = await window.kimiDB.getSelectedCharacter();
1961
+ const traits = await window.kimiDB.getAllPersonalityTraits(selectedCharacter);
 
 
 
1962
 
1963
+ // Validate current video context using centralized validation
1964
  const currentInfo = window.kimiVideo.getCurrentVideoInfo();
1965
  const validatedEmotion = validateEmotionContext(currentInfo.emotion);
1966
 
kimi-js/kimi-script.js CHANGED
@@ -83,6 +83,13 @@ document.addEventListener("DOMContentLoaded", async function () {
83
  }
84
  } catch (error) {
85
  console.error("Initialization error:", error);
 
 
 
 
 
 
 
86
  }
87
  // Centralized helpers for API config UI
88
  const ApiUi = {
@@ -196,6 +203,12 @@ document.addEventListener("DOMContentLoaded", async function () {
196
  }
197
  } catch (e) {
198
  console.warn("Failed to initialize API config UI:", e);
 
 
 
 
 
 
199
  }
200
  }
201
  // Hydrate API config UI from DB after ApiUi is defined and function declared
@@ -520,7 +533,7 @@ document.addEventListener("DOMContentLoaded", async function () {
520
  console.error("sendMessage function not available");
521
  }
522
  });
523
- console.log("Send button event listener attached");
524
  } else {
525
  console.error("Send button not found");
526
  }
@@ -551,7 +564,7 @@ document.addEventListener("DOMContentLoaded", async function () {
551
  el.addEventListener("focus", a);
552
  setTimeout(a, 0);
553
  })(chatInput);
554
- console.log("Chat input event listener attached");
555
  } else {
556
  console.error("Chat input not found");
557
  }
@@ -998,7 +1011,7 @@ document.addEventListener("DOMContentLoaded", async function () {
998
  safeTraits[key] = v;
999
  }
1000
  if (window.KIMI_DEBUG_SYNC) {
1001
- console.log(`🧠 (Batched) Personality updated for ${character}:`, safeTraits);
1002
  }
1003
  // Centralize side-effects elsewhere; aggregator remains a coalesced logger only.
1004
  }
 
83
  }
84
  } catch (error) {
85
  console.error("Initialization error:", error);
86
+ // Log initialization error to error manager
87
+ if (window.kimiErrorManager) {
88
+ window.kimiErrorManager.logInitError("KimiApp", error, {
89
+ selectedCharacter: selectedCharacter,
90
+ stage: "main_initialization"
91
+ });
92
+ }
93
  }
94
  // Centralized helpers for API config UI
95
  const ApiUi = {
 
203
  }
204
  } catch (e) {
205
  console.warn("Failed to initialize API config UI:", e);
206
+ // Log UI initialization error
207
+ if (window.kimiErrorManager) {
208
+ window.kimiErrorManager.logUIError("ApiConfigUI", e, {
209
+ stage: "api_config_initialization"
210
+ });
211
+ }
212
  }
213
  }
214
  // Hydrate API config UI from DB after ApiUi is defined and function declared
 
533
  console.error("sendMessage function not available");
534
  }
535
  });
536
+ console.log("Send button event listener attached");
537
  } else {
538
  console.error("Send button not found");
539
  }
 
564
  el.addEventListener("focus", a);
565
  setTimeout(a, 0);
566
  })(chatInput);
567
+ console.log("Chat input event listener attached");
568
  } else {
569
  console.error("Chat input not found");
570
  }
 
1011
  safeTraits[key] = v;
1012
  }
1013
  if (window.KIMI_DEBUG_SYNC) {
1014
+ window.KIMI_CONFIG?.debugLog("SYNC", `Personality updated for ${character}:`, safeTraits);
1015
  }
1016
  // Centralize side-effects elsewhere; aggregator remains a coalesced logger only.
1017
  }
kimi-js/kimi-utils.js CHANGED
@@ -19,6 +19,41 @@ window.KimiValidationUtils = {
19
  div.textContent = text;
20
  return div.innerHTML;
21
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  validateRange(value, key) {
23
  const bounds = {
24
  voiceRate: { min: 0.5, max: 2, def: 1.1 },
 
19
  div.textContent = text;
20
  return div.innerHTML;
21
  },
22
+ /**
23
+ * Format chat text with simple markdown-like syntax (secure)
24
+ * Supports: **bold**, *italic*, and preserves line breaks
25
+ * Security: All text is escaped first, then selective formatting is applied
26
+ */
27
+ formatChatText(text) {
28
+ if (!text || typeof text !== "string") return "";
29
+
30
+ // First: Escape all HTML to prevent XSS
31
+ let escaped = this.escapeHtml(text);
32
+
33
+ // Optional: Replace em-dash with regular dash if preferred
34
+ escaped = escaped.replace(/—/g, "-");
35
+
36
+ // Second: Apply simple formatting (only on escaped text)
37
+ // **bold** -> <strong>bold</strong>
38
+ escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
39
+
40
+ // *italic* -> <em>italic</em> (but not if already inside **)
41
+ escaped = escaped.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, "<em>$1</em>");
42
+
43
+ // Smart paragraph handling: only create <p> for double line breaks or real paragraphs
44
+ // Split by double line breaks (\n\n) to identify real paragraphs
45
+ const realParagraphs = escaped.split(/\n\s*\n/).filter(para => para.trim().length > 0);
46
+
47
+ if (realParagraphs.length > 1) {
48
+ // Multiple paragraphs found - wrap each in <p>
49
+ escaped = realParagraphs.map(p => `<p>${p.trim().replace(/\n/g, " ")}</p>`).join("");
50
+ } else {
51
+ // Single paragraph - just convert single \n to spaces (natural text flow)
52
+ escaped = escaped.replace(/\n/g, " ");
53
+ }
54
+
55
+ return escaped;
56
+ },
57
  validateRange(value, key) {
58
  const bounds = {
59
  voiceRate: { min: 0.5, max: 2, def: 1.1 },
kimi-js/kimi-videos.js CHANGED
@@ -60,6 +60,115 @@ class KimiVideoManager {
60
  this._consecutiveErrorCount = 0;
61
  // Track per-video load attempts to adapt timeouts & avoid faux échecs
62
  this._videoAttempts = new Map();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  }
64
 
65
  //Centralized crossfade transition between two videos.
@@ -177,18 +286,55 @@ class KimiVideoManager {
177
  console.log("🎬 VideoManager: history summary", summary);
178
  }
179
 
180
- _priorityWeight(context) {
181
- if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") return 3;
182
- if (context === "dancing" || context === "listening") return 2;
183
- return 1;
 
 
 
 
 
 
 
 
 
 
184
  }
185
 
186
  _enqueuePendingSwitch(req) {
187
- // Keep small bounded list; prefer newest higher-priority
188
- const maxSize = 5;
189
- this._pendingSwitches.push(req);
190
- if (this._pendingSwitches.length > maxSize) {
191
- this._pendingSwitches = this._pendingSwitches.slice(-maxSize);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
  }
194
 
@@ -380,9 +526,7 @@ class KimiVideoManager {
380
  // Respect sticky context (avoid overrides while dancing is requested/playing)
381
  if (this._stickyContext === "dancing" && context !== "dancing") {
382
  const categoryForPriority = this.determineCategory(context, emotion, traits);
383
- const priorityWeight = this._priorityWeight(
384
- categoryForPriority === "speakingPositive" || categoryForPriority === "speakingNegative" ? "speaking" : context
385
- );
386
  if (Date.now() < (this._stickyUntil || 0)) {
387
  this._enqueuePendingSwitch({
388
  context,
@@ -410,9 +554,7 @@ class KimiVideoManager {
410
  ) {
411
  // Queue the request with appropriate priority to be processed after current clip
412
  const categoryForPriority = this.determineCategory(context, emotion, traits);
413
- const priorityWeight = this._priorityWeight(
414
- categoryForPriority === "speakingPositive" || categoryForPriority === "speakingNegative" ? "speaking" : context
415
- );
416
  this._enqueuePendingSwitch({
417
  context,
418
  emotion,
@@ -436,7 +578,7 @@ class KimiVideoManager {
436
  this.currentEmotionContext &&
437
  this.currentEmotionContext !== emotion
438
  ) {
439
- const priorityWeight = this._priorityWeight("speaking");
440
  this._enqueuePendingSwitch({
441
  context,
442
  emotion,
@@ -512,6 +654,17 @@ class KimiVideoManager {
512
  this.lastSwitchTime = Date.now();
513
  return;
514
  }
 
 
 
 
 
 
 
 
 
 
 
515
  if (window.voiceManager && window.voiceManager.isListening && context === "listening") {
516
  const listeningPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
517
  const listeningCurrent = this.activeVideo.querySelector("source").getAttribute("src");
@@ -652,11 +805,11 @@ class KimiVideoManager {
652
  }
653
  }
654
 
655
- // keep only the augmented determineCategory above (with traits)
656
  selectOptimalVideo(category, specificVideo = null, traits = null, affection = null, emotion = null) {
657
  const availableVideos = this.videoCategories[category] || this.videoCategories.neutral;
658
 
659
- if (specificVideo && availableVideos.includes(specificVideo)) {
660
  if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, specificVideo);
661
  this._logSelection(category, specificVideo, availableVideos);
662
  return specificVideo;
@@ -664,23 +817,34 @@ class KimiVideoManager {
664
 
665
  const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
666
 
667
- // Filter out recently played videos using adaptive history
668
  const recentlyPlayed = this.playHistory[category] || [];
669
- let candidateVideos = availableVideos.filter(video => video !== currentVideoSrc && !recentlyPlayed.includes(video));
 
 
 
 
 
 
 
670
 
671
- // If no fresh videos, allow recently played but not current
672
  if (candidateVideos.length === 0) {
673
  candidateVideos = availableVideos.filter(video => video !== currentVideoSrc);
674
  }
675
 
676
- // Ultimate fallback
677
  if (candidateVideos.length === 0) {
678
  candidateVideos = availableVideos;
679
  }
680
 
681
- // Ensure we're not falling back to wrong category
682
  if (candidateVideos.length === 0) {
683
- candidateVideos = this.videoCategories.neutral;
 
 
 
 
684
  }
685
 
686
  // If traits and affection are provided, weight the selection more subtly
@@ -775,46 +939,17 @@ class KimiVideoManager {
775
  }
776
  }
777
 
778
- // Ensure determineCategory exists as a class method (used at line ~494 and ~537)
779
  determineCategory(context, emotion = "neutral", traits = null) {
780
- // Get emotion mapping from centralized emotion system
781
- const emotionToCategory = window.kimiEmotionSystem?.emotionToVideoCategory || {
782
- listening: "listening",
783
- positive: "speakingPositive",
784
- negative: "speakingNegative",
785
- neutral: "neutral",
786
- surprise: "speakingPositive",
787
- laughing: "speakingPositive",
788
- shy: "neutral",
789
- confident: "speakingPositive",
790
- romantic: "speakingPositive",
791
- flirtatious: "speakingPositive",
792
- goodbye: "neutral",
793
- kiss: "speakingPositive",
794
- dancing: "dancing",
795
- speaking: "speakingPositive",
796
- speakingPositive: "speakingPositive",
797
- speakingNegative: "speakingNegative"
798
- };
799
-
800
- // Prefer explicit context mapping if provided (e.g., 'listening','dancing')
801
- if (emotionToCategory[context]) {
802
- return emotionToCategory[context];
803
- }
804
- // Normalize generic 'speaking' by emotion polarity
805
- if (context === "speaking") {
806
- if (emotion === "positive") return "speakingPositive";
807
- if (emotion === "negative") return "speakingNegative";
808
- return "neutral";
809
- }
810
- // Map by emotion label when possible
811
- if (emotionToCategory[emotion]) {
812
- return emotionToCategory[emotion];
813
  }
814
- return "neutral";
815
- }
816
 
817
- // SPECIALIZED METHODS FOR EACH CONTEXT
 
 
 
818
  async startListening(traits = null, affection = null) {
819
  // If already listening and playing, avoid redundant switch
820
  if (this.currentContext === "listening" && !this.activeVideo.paused && !this.activeVideo.ended) {
@@ -1073,7 +1208,10 @@ class KimiVideoManager {
1073
  }
1074
  }
1075
 
1076
- console.log(`Auto-transition scheduled in ${duration / 1000}s (${this.currentContext} → neutral)`);
 
 
 
1077
  this.autoTransitionTimer = setTimeout(() => {
1078
  if (this.currentContext !== "neutral" && this.currentContext !== "listening") {
1079
  if (!this._processPendingSwitches()) {
@@ -1139,7 +1277,10 @@ class KimiVideoManager {
1139
  }
1140
  // Only log high priority or error cases to reduce noise
1141
  if (priority === "speaking" || priority === "high") {
1142
- console.log(`🎬 Loading video: ${videoSrc} (priority: ${priority})`);
 
 
 
1143
  }
1144
 
1145
  // Si une vidéo haute priorité arrive, on peut interrompre le chargement en cours
@@ -1396,7 +1537,9 @@ class KimiVideoManager {
1396
  try {
1397
  const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
1398
  const info = { context: this.currentContext, emotion: this.currentEmotion };
1399
- console.log("🎬 VideoManager: Now playing:", src, info);
 
 
1400
  // Recompute autoTransitionDuration from actual duration if available (C)
1401
  try {
1402
  const d = this.activeVideo.duration;
@@ -1549,25 +1692,72 @@ class KimiVideoManager {
1549
  }
1550
 
1551
  // METHODS TO ANALYZE EMOTIONS FROM TEXT
1552
- // CLEANUP
1553
  destroy() {
 
1554
  clearTimeout(this.autoTransitionTimer);
 
 
 
 
1555
  this.autoTransitionTimer = null;
 
 
 
 
 
1556
  if (this._visibilityHandler) {
1557
  document.removeEventListener("visibilitychange", this._visibilityHandler);
1558
  this._visibilityHandler = null;
1559
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1560
  }
1561
 
1562
- // Utilitaire pour déterminer la catégorie vidéo selon la moyenne des traits
1563
  setMoodByPersonality(traits) {
1564
  if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1565
- const category = window.getMoodCategoryFromPersonality ? window.getMoodCategoryFromPersonality(traits) : "neutral";
1566
- // Normalize emotion so validation uses base emotion labels
 
 
 
1567
  let emotion = category;
1568
  if (category === "speakingPositive") emotion = "positive";
1569
  else if (category === "speakingNegative") emotion = "negative";
1570
- // For other categories (neutral, listening, dancing) emotion can equal category
1571
  this.switchToContext(category, emotion, null, traits, traits.affection);
1572
  }
1573
 
 
60
  this._consecutiveErrorCount = 0;
61
  // Track per-video load attempts to adapt timeouts & avoid faux échecs
62
  this._videoAttempts = new Map();
63
+
64
+ // Error handling and recovery system
65
+ this._errorRecoveryAttempts = 0;
66
+ this._maxRecoveryAttempts = 3;
67
+ this._lastErrorTime = 0;
68
+ this._errorThreshold = 5000; // 5 seconds between error recovery attempts
69
+ }
70
+
71
+ // ===== ERROR HANDLING AND RECOVERY SYSTEM =====
72
+ _handleVideoError(error, videoSrc = null, context = "unknown") {
73
+ this._consecutiveErrorCount++;
74
+ this._lastErrorTime = Date.now();
75
+
76
+ const errorInfo = {
77
+ error: error.message || "Unknown video error",
78
+ videoSrc: videoSrc || "unknown",
79
+ context,
80
+ timestamp: Date.now(),
81
+ consecutiveCount: this._consecutiveErrorCount
82
+ };
83
+
84
+ if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
85
+ console.warn("🎬 Video error occurred:", errorInfo);
86
+ }
87
+
88
+ // Track failures for this specific video
89
+ if (videoSrc) {
90
+ this._recentFailures.set(videoSrc, Date.now());
91
+ }
92
+
93
+ // Attempt recovery if not too many consecutive errors
94
+ if (this._consecutiveErrorCount <= this._maxRecoveryAttempts) {
95
+ this._attemptErrorRecovery(context);
96
+ } else {
97
+ console.error("🎬 Too many consecutive video errors, disabling auto-recovery");
98
+ this._fallbackToSafeState();
99
+ }
100
+ }
101
+
102
+ _attemptErrorRecovery(context) {
103
+ console.log("🎬 Attempting video error recovery...");
104
+
105
+ // Try to switch to a safe neutral video
106
+ setTimeout(() => {
107
+ try {
108
+ // Choose a different neutral video that hasn't failed recently
109
+ const neutralVideos = this.videoCategories.neutral || [];
110
+ const safeVideos = neutralVideos.filter(
111
+ video =>
112
+ !this._recentFailures.has(video) || Date.now() - this._recentFailures.get(video) > this._failureCooldown
113
+ );
114
+
115
+ if (safeVideos.length > 0) {
116
+ const safeVideo = safeVideos[0];
117
+ this._resetErrorState();
118
+ this.loadAndSwitchVideo(safeVideo, "recovery");
119
+ console.log("🎬 Video error recovery successful");
120
+ } else {
121
+ this._fallbackToSafeState();
122
+ }
123
+ } catch (recoveryError) {
124
+ console.error("🎬 Video recovery failed:", recoveryError);
125
+ this._fallbackToSafeState();
126
+ }
127
+ }, 1000); // Small delay before recovery attempt
128
+ }
129
+
130
+ _fallbackToSafeState() {
131
+ console.log("🎬 Falling back to safe state - pausing video system");
132
+
133
+ // Pause both videos to avoid further errors
134
+ try {
135
+ this.activeVideo?.pause();
136
+ this.inactiveVideo?.pause();
137
+ } catch (e) {
138
+ // Silent fallback
139
+ }
140
+
141
+ // Clear all pending operations
142
+ this._pendingSwitches.length = 0;
143
+ this._stickyContext = null;
144
+ this._stickyUntil = 0;
145
+ this.isEmotionVideoPlaying = false;
146
+
147
+ // Reset state with long cooldown
148
+ setTimeout(() => {
149
+ this._resetErrorState();
150
+ console.log("🎬 Video system ready for retry");
151
+ }, 10000);
152
+ }
153
+
154
+ _resetErrorState() {
155
+ this._consecutiveErrorCount = 0;
156
+ this._errorRecoveryAttempts = 0;
157
+
158
+ // Clean old failure records
159
+ const now = Date.now();
160
+ for (const [video, timestamp] of this._recentFailures.entries()) {
161
+ if (now - timestamp > this._failureCooldown) {
162
+ this._recentFailures.delete(video);
163
+ }
164
+ }
165
+ }
166
+
167
+ _isVideoSafe(videoSrc) {
168
+ if (!this._recentFailures.has(videoSrc)) return true;
169
+
170
+ const lastFailure = this._recentFailures.get(videoSrc);
171
+ return Date.now() - lastFailure > this._failureCooldown;
172
  }
173
 
174
  //Centralized crossfade transition between two videos.
 
286
  console.log("🎬 VideoManager: history summary", summary);
287
  }
288
 
289
+ _priorityWeight(context, emotion = "neutral") {
290
+ // Use centralized priority system from emotion system if available
291
+ if (window.kimiEmotionSystem?.getPriorityWeight) {
292
+ // Try emotion first (more specific), then context
293
+ const emotionPriority = window.kimiEmotionSystem.getPriorityWeight(emotion);
294
+ const contextPriority = window.kimiEmotionSystem.getPriorityWeight(context);
295
+ return Math.max(emotionPriority, contextPriority);
296
+ }
297
+
298
+ // Legacy fallback priorities if emotion system not available
299
+ if (context === "dancing") return 10;
300
+ if (context === "listening") return 7;
301
+ if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") return 4;
302
+ return 3; // Default priority for neutral and other contexts
303
  }
304
 
305
  _enqueuePendingSwitch(req) {
306
+ // Intelligent queue management - limit to 3 for better responsiveness
307
+ const maxSize = 3;
308
+
309
+ // Check if we already have a similar request (same context + emotion)
310
+ const existingIndex = this._pendingSwitches.findIndex(
311
+ pending => pending.context === req.context && pending.emotion === req.emotion
312
+ );
313
+
314
+ if (existingIndex !== -1) {
315
+ // Replace existing similar request with newer one
316
+ this._pendingSwitches[existingIndex] = req;
317
+ this._logDebug("Replaced similar pending switch", { context: req.context, emotion: req.emotion });
318
+ } else {
319
+ // Add new request
320
+ this._pendingSwitches.push(req);
321
+
322
+ // If exceeded max size, remove oldest lower-priority request
323
+ if (this._pendingSwitches.length > maxSize) {
324
+ // Sort by priority weight (lower = remove first) then by age (older = remove first)
325
+ this._pendingSwitches.sort((a, b) => {
326
+ const priorityDiff = (b.priorityWeight || 1) - (a.priorityWeight || 1);
327
+ if (priorityDiff !== 0) return priorityDiff;
328
+ return a.requestedAt - b.requestedAt; // Older first
329
+ });
330
+
331
+ // Remove the lowest priority, oldest request
332
+ const removed = this._pendingSwitches.shift();
333
+ this._logDebug("Removed low-priority pending switch", {
334
+ removed: removed.context,
335
+ queueSize: this._pendingSwitches.length
336
+ });
337
+ }
338
  }
339
  }
340
 
 
526
  // Respect sticky context (avoid overrides while dancing is requested/playing)
527
  if (this._stickyContext === "dancing" && context !== "dancing") {
528
  const categoryForPriority = this.determineCategory(context, emotion, traits);
529
+ const priorityWeight = this._priorityWeight(context, emotion);
 
 
530
  if (Date.now() < (this._stickyUntil || 0)) {
531
  this._enqueuePendingSwitch({
532
  context,
 
554
  ) {
555
  // Queue the request with appropriate priority to be processed after current clip
556
  const categoryForPriority = this.determineCategory(context, emotion, traits);
557
+ const priorityWeight = this._priorityWeight(context, emotion);
 
 
558
  this._enqueuePendingSwitch({
559
  context,
560
  emotion,
 
578
  this.currentEmotionContext &&
579
  this.currentEmotionContext !== emotion
580
  ) {
581
+ const priorityWeight = this._priorityWeight(context, emotion);
582
  this._enqueuePendingSwitch({
583
  context,
584
  emotion,
 
654
  this.lastSwitchTime = Date.now();
655
  return;
656
  }
657
+
658
+ // ALSO handle speaking contexts even when TTS is not yet flagged as speaking
659
+ // This ensures immediate response to speaking context requests
660
+ if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") {
661
+ const speakingPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
662
+ this.loadAndSwitchVideo(speakingPath, priority);
663
+ this.currentContext = category;
664
+ this.currentEmotion = emotion;
665
+ this.lastSwitchTime = Date.now();
666
+ return;
667
+ }
668
  if (window.voiceManager && window.voiceManager.isListening && context === "listening") {
669
  const listeningPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
670
  const listeningCurrent = this.activeVideo.querySelector("source").getAttribute("src");
 
805
  }
806
  }
807
 
808
+ // Enhanced selectOptimalVideo with safety checks
809
  selectOptimalVideo(category, specificVideo = null, traits = null, affection = null, emotion = null) {
810
  const availableVideos = this.videoCategories[category] || this.videoCategories.neutral;
811
 
812
+ if (specificVideo && availableVideos.includes(specificVideo) && this._isVideoSafe(specificVideo)) {
813
  if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, specificVideo);
814
  this._logSelection(category, specificVideo, availableVideos);
815
  return specificVideo;
 
817
 
818
  const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
819
 
820
+ // Filter out recently played videos using adaptive history AND safety checks
821
  const recentlyPlayed = this.playHistory[category] || [];
822
+ let candidateVideos = availableVideos.filter(
823
+ video => video !== currentVideoSrc && !recentlyPlayed.includes(video) && this._isVideoSafe(video)
824
+ );
825
+
826
+ // If no safe fresh videos, allow recently played but safe videos (not current)
827
+ if (candidateVideos.length === 0) {
828
+ candidateVideos = availableVideos.filter(video => video !== currentVideoSrc && this._isVideoSafe(video));
829
+ }
830
 
831
+ // If still no safe videos, use any available (excluding current)
832
  if (candidateVideos.length === 0) {
833
  candidateVideos = availableVideos.filter(video => video !== currentVideoSrc);
834
  }
835
 
836
+ // Ultimate fallback - use all available
837
  if (candidateVideos.length === 0) {
838
  candidateVideos = availableVideos;
839
  }
840
 
841
+ // Final fallback to neutral category if current category is empty
842
  if (candidateVideos.length === 0) {
843
+ const neutralVideos = this.videoCategories.neutral || [];
844
+ candidateVideos = neutralVideos.filter(video => this._isVideoSafe(video));
845
+ if (candidateVideos.length === 0) {
846
+ candidateVideos = neutralVideos; // Last resort
847
+ }
848
  }
849
 
850
  // If traits and affection are provided, weight the selection more subtly
 
939
  }
940
  }
941
 
942
+ // Simplified determineCategory - pure delegation to centralized system
943
  determineCategory(context, emotion = "neutral", traits = null) {
944
+ // Use centralized emotion system exclusively for consistency
945
+ if (window.kimiEmotionSystem?.getVideoCategory) {
946
+ return window.kimiEmotionSystem.getVideoCategory(context || emotion, traits);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  }
 
 
948
 
949
+ // Minimal fallback only if emotion system completely unavailable
950
+ console.warn("KimiEmotionSystem not available - using minimal fallback");
951
+ return "neutral";
952
+ } // SPECIALIZED METHODS FOR EACH CONTEXT
953
  async startListening(traits = null, affection = null) {
954
  // If already listening and playing, avoid redundant switch
955
  if (this.currentContext === "listening" && !this.activeVideo.paused && !this.activeVideo.ended) {
 
1208
  }
1209
  }
1210
 
1211
+ // Auto-transition timing
1212
+ if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
1213
+ console.log(`Auto-transition scheduled in ${duration / 1000}s (${this.currentContext} → neutral)`);
1214
+ }
1215
  this.autoTransitionTimer = setTimeout(() => {
1216
  if (this.currentContext !== "neutral" && this.currentContext !== "listening") {
1217
  if (!this._processPendingSwitches()) {
 
1277
  }
1278
  // Only log high priority or error cases to reduce noise
1279
  if (priority === "speaking" || priority === "high") {
1280
+ // Video loading with priority
1281
+ if (window.KIMI_CONFIG?.DEBUG?.VIDEO) {
1282
+ console.log(`🎬 Loading video: ${videoSrc} (priority: ${priority})`);
1283
+ }
1284
  }
1285
 
1286
  // Si une vidéo haute priorité arrive, on peut interrompre le chargement en cours
 
1537
  try {
1538
  const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
1539
  const info = { context: this.currentContext, emotion: this.currentEmotion };
1540
+ if (this._debug) {
1541
+ console.log("🎬 VideoManager: Now playing:", src, info);
1542
+ }
1543
  // Recompute autoTransitionDuration from actual duration if available (C)
1544
  try {
1545
  const d = this.activeVideo.duration;
 
1692
  }
1693
 
1694
  // METHODS TO ANALYZE EMOTIONS FROM TEXT
1695
+ // CLEANUP - Enhanced memory management
1696
  destroy() {
1697
+ // Clear all timers
1698
  clearTimeout(this.autoTransitionTimer);
1699
+ clearTimeout(this._warmupTimer);
1700
+ clearTimeout(this._listeningGraceTimer);
1701
+ clearTimeout(this._pendingSpeakSwitch);
1702
+
1703
  this.autoTransitionTimer = null;
1704
+ this._warmupTimer = null;
1705
+ this._listeningGraceTimer = null;
1706
+ this._pendingSpeakSwitch = null;
1707
+
1708
+ // Remove all event listeners
1709
  if (this._visibilityHandler) {
1710
  document.removeEventListener("visibilitychange", this._visibilityHandler);
1711
  this._visibilityHandler = null;
1712
  }
1713
+
1714
+ if (this._firstInteractionHandler) {
1715
+ window.removeEventListener("click", this._firstInteractionHandler);
1716
+ window.removeEventListener("keydown", this._firstInteractionHandler);
1717
+ this._firstInteractionHandler = null;
1718
+ }
1719
+
1720
+ // Clean up video loading handlers
1721
+ this._cleanupLoadingHandlers();
1722
+
1723
+ // Clear global ended handler
1724
+ if (this._globalEndedHandler) {
1725
+ this.activeVideo?.removeEventListener("ended", this._globalEndedHandler);
1726
+ this.inactiveVideo?.removeEventListener("ended", this._globalEndedHandler);
1727
+ this._globalEndedHandler = null;
1728
+ }
1729
+
1730
+ // Clear caches and queues
1731
+ this._prefetchCache.clear();
1732
+ this._prefetchInFlight.clear();
1733
+ this._pendingSwitches.length = 0;
1734
+ this._videoAttempts.clear();
1735
+ this._recentFailures.clear();
1736
+
1737
+ // Reset history to prevent memory accumulation
1738
+ this.playHistory = {};
1739
+ this.emotionHistory.length = 0;
1740
+
1741
+ // Reset state flags
1742
+ this._stickyContext = null;
1743
+ this._stickyUntil = 0;
1744
+ this.isEmotionVideoPlaying = false;
1745
+ this.currentEmotionContext = null;
1746
+ this._neutralLock = false;
1747
  }
1748
 
1749
+ // Simplified mood setting using centralized emotion system
1750
  setMoodByPersonality(traits) {
1751
  if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1752
+
1753
+ // Use centralized mood calculation from emotion system
1754
+ const category = window.kimiEmotionSystem?.getMoodCategoryFromPersonality(traits) || "neutral";
1755
+
1756
+ // Normalize emotion for consistent validation
1757
  let emotion = category;
1758
  if (category === "speakingPositive") emotion = "positive";
1759
  else if (category === "speakingNegative") emotion = "negative";
1760
+
1761
  this.switchToContext(category, emotion, null, traits, traits.affection);
1762
  }
1763
 
kimi-js/kimi-voices.js CHANGED
@@ -70,16 +70,22 @@ class KimiVoiceManager {
70
  this.transcriptText = document.getElementById("transcript");
71
 
72
  if (!this.micButton) {
73
- console.warn("Microphone button not found in DOM!");
 
 
74
  return false;
75
  }
76
 
77
  // Check transcript elements (non-critical, just warn)
78
  if (!this.transcriptContainer) {
79
- console.warn("Transcript container not found in DOM - transcript feature will be disabled");
 
 
80
  }
81
  if (!this.transcriptText) {
82
- console.warn("Transcript text element not found in DOM - transcript feature will be disabled");
 
 
83
  }
84
 
85
  // Initialize voice synthesis
@@ -167,13 +173,17 @@ class KimiVoiceManager {
167
  try {
168
  // Check if running on file:// protocol
169
  if (window.location.protocol === "file:") {
170
- console.log("🎤 Running on file:// protocol - microphone permissions will be requested each time");
 
 
171
  this.micPermissionGranted = false;
172
  return;
173
  }
174
 
175
  if (!navigator.permissions) {
176
- console.log("🎤 Permissions API not available");
 
 
177
  this.micPermissionGranted = false; // Set default state
178
  return;
179
  }
@@ -316,10 +326,10 @@ class KimiVoiceManager {
316
  );
317
  });
318
 
319
- // Debug: Check what we actually found
320
- if (femaleVoice) {
321
  console.log(`🎤 Female voice found: "${femaleVoice.name}" (${femaleVoice.lang})`);
322
- } else {
323
  console.log(
324
  `🎤 No female voice found, using first available: "${filteredVoices[0]?.name}" (${filteredVoices[0]?.lang})`
325
  );
@@ -542,23 +552,23 @@ class KimiVoiceManager {
542
 
543
  // ===== CHAT MESSAGE UTILITIES =====
544
  handleChatMessage(userMessage, kimiResponse) {
545
- const chatContainer = document.getElementById("chat-container");
546
- const chatMessages = document.getElementById("chat-messages");
547
-
548
- if (!chatContainer || !chatContainer.classList.contains("visible") || !chatMessages) {
549
- return;
550
- }
551
-
552
  const addMessageToChat = window.addMessageToChat || (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
553
 
554
  if (addMessageToChat) {
 
555
  addMessageToChat("user", userMessage);
556
  addMessageToChat(this.selectedCharacter.toLowerCase(), kimiResponse);
557
  } else {
558
- // Fallback manual message creation
559
- this.createChatMessage(chatMessages, "user", userMessage);
560
- this.createChatMessage(chatMessages, this.selectedCharacter.toLowerCase(), kimiResponse);
561
- chatMessages.scrollTop = chatMessages.scrollHeight;
 
 
 
 
 
562
  }
563
  }
564
 
@@ -644,12 +654,38 @@ class KimiVoiceManager {
644
 
645
  // Get volume using centralized utility
646
  utterance.volume = this.getVoicePreference("volume", options);
647
- const emotionFromText = this.analyzeTextEmotion(text);
648
- if (window.kimiVideo && emotionFromText !== "neutral") {
649
- requestAnimationFrame(() => {
650
- window.kimiVideo.respondWithEmotion(emotionFromText);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
651
  });
652
  }
 
653
  if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
654
  window.updatePersonalityTraitsFromEmotion(emotionFromText, text);
655
  }
@@ -657,24 +693,30 @@ class KimiVoiceManager {
657
 
658
  utterance.onstart = async () => {
659
  this.isSpeaking = true;
660
- // Note: transcript visibility is already handled by showResponseWithPerfectTiming
661
- // This ensures the transcript stays visible while AI is speaking
662
 
663
- // Ensure a speaking animation plays (avoid frozen neutral frame during TTS)
664
  try {
665
- if (window.kimiVideo && window.kimiVideo.getCurrentVideoInfo) {
666
- const info = window.kimiVideo.getCurrentVideoInfo();
667
- if (info && !(info.context && info.context.startsWith("speaking"))) {
668
- // Use positive speaking as neutral fallback
669
- const traits = await this.db?.getAllPersonalityTraits(
670
- window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
671
- );
672
- const affection = traits ? traits.affection : 50;
673
- window.kimiVideo.switchToContext("speakingPositive", "positive", null, traits || {}, affection);
 
 
 
 
 
 
 
 
674
  }
675
  }
676
  } catch (e) {
677
- // Silent fallback
678
  }
679
  };
680
 
@@ -684,24 +726,35 @@ class KimiVoiceManager {
684
  this.updateTranscriptVisibility(false);
685
  // Clear any pending hide timeout
686
  this.clearTranscriptTimeout();
 
 
687
  if (window.kimiVideo) {
688
- // Do not force neutral if an emotion clip is still playing (speaking/dancing)
689
  try {
690
  const info = window.kimiVideo.getCurrentVideoInfo ? window.kimiVideo.getCurrentVideoInfo() : null;
691
- const isEmotionClip =
692
- info &&
693
- (info.context === "speakingPositive" ||
694
- info.context === "speakingNegative" ||
695
- info.context === "dancing");
696
- if (!isEmotionClip) {
697
- requestAnimationFrame(() => {
698
- window.kimiVideo.returnToNeutral();
699
- });
 
 
 
 
 
 
 
 
 
 
 
 
700
  }
701
- } catch (_) {
702
- requestAnimationFrame(() => {
703
- window.kimiVideo.returnToNeutral();
704
- });
705
  }
706
  }
707
  };
@@ -909,6 +962,9 @@ class KimiVoiceManager {
909
  }
910
  if (final_transcript && this.onSpeechAnalysis) {
911
  try {
 
 
 
912
  // Auto-stop after silence timeout following final transcript
913
  setTimeout(() => {
914
  this.stopListening();
@@ -1216,46 +1272,6 @@ class KimiVoiceManager {
1216
  this.onSpeechAnalysis = callback;
1217
  }
1218
 
1219
- analyzeTextEmotion(text) {
1220
- // Use unified emotion system
1221
- if (window.kimiAnalyzeEmotion) {
1222
- const emotion = window.kimiAnalyzeEmotion(text, "auto");
1223
- return this._modulateEmotionByPersonality(emotion);
1224
- }
1225
- return "neutral";
1226
- } // Helper to modulate emotion based on personality traits
1227
- _modulateEmotionByPersonality(emotion) {
1228
- try {
1229
- // Obtain full traits if possible for a more robust modulation
1230
- let avg = 50;
1231
- if (window.kimiEmotionSystem && window.kimiEmotionSystem.db) {
1232
- // Attempt synchronous-like cache via memory first
1233
- const traits = {
1234
- affection: this.memory?.affectionTrait,
1235
- playfulness: this.memory?.playfulnessTrait,
1236
- intelligence: this.memory?.intelligenceTrait,
1237
- empathy: this.memory?.empathyTrait,
1238
- humor: this.memory?.humorTrait,
1239
- romance: this.memory?.romanceTrait
1240
- };
1241
- // If at least affection present, compute average using emotion system helper
1242
- if (typeof traits.affection === "number") {
1243
- avg = window.kimiEmotionSystem.calculatePersonalityAverage(traits);
1244
- }
1245
- } else if (this.memory && typeof this.memory.affectionTrait === "number") {
1246
- avg = this.memory.affectionTrait; // fallback
1247
- }
1248
-
1249
- // Weighted interpretation: very low affection still softens positive expression
1250
- // If overall avg low, dampen by shifting to 'shy'.
1251
- if (avg <= 20 && emotion !== "neutral") return "shy";
1252
- if (avg <= 40 && emotion === "positive") return "shy";
1253
- return emotion;
1254
- } catch (e) {
1255
- return emotion;
1256
- }
1257
- }
1258
-
1259
  async testVoice() {
1260
  const testMessages = [
1261
  window.kimiI18nManager?.t("test_voice_message_1") || "Hello my beloved! 💕",
 
70
  this.transcriptText = document.getElementById("transcript");
71
 
72
  if (!this.micButton) {
73
+ if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
74
+ console.warn("Microphone button not found in DOM!");
75
+ }
76
  return false;
77
  }
78
 
79
  // Check transcript elements (non-critical, just warn)
80
  if (!this.transcriptContainer) {
81
+ if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
82
+ console.warn("Transcript container not found in DOM - transcript feature will be disabled");
83
+ }
84
  }
85
  if (!this.transcriptText) {
86
+ if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
87
+ console.warn("Transcript text element not found in DOM - transcript feature will be disabled");
88
+ }
89
  }
90
 
91
  // Initialize voice synthesis
 
173
  try {
174
  // Check if running on file:// protocol
175
  if (window.location.protocol === "file:") {
176
+ if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
177
+ console.log("🎤 Running on file:// protocol - microphone permissions will be requested each time");
178
+ }
179
  this.micPermissionGranted = false;
180
  return;
181
  }
182
 
183
  if (!navigator.permissions) {
184
+ if (window.KIMI_CONFIG?.DEBUG?.VOICE) {
185
+ console.log("🎤 Permissions API not available");
186
+ }
187
  this.micPermissionGranted = false; // Set default state
188
  return;
189
  }
 
326
  );
327
  });
328
 
329
+ // Debug: Voice analysis (debug mode only)
330
+ if (window.KIMI_DEBUG_VOICE) {
331
  console.log(`🎤 Female voice found: "${femaleVoice.name}" (${femaleVoice.lang})`);
332
+ } else if (window.KIMI_DEBUG_VOICE) {
333
  console.log(
334
  `🎤 No female voice found, using first available: "${filteredVoices[0]?.name}" (${filteredVoices[0]?.lang})`
335
  );
 
552
 
553
  // ===== CHAT MESSAGE UTILITIES =====
554
  handleChatMessage(userMessage, kimiResponse) {
555
+ // Always save to chat history, regardless of chat visibility
 
 
 
 
 
 
556
  const addMessageToChat = window.addMessageToChat || (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
557
 
558
  if (addMessageToChat) {
559
+ // Save messages to history
560
  addMessageToChat("user", userMessage);
561
  addMessageToChat(this.selectedCharacter.toLowerCase(), kimiResponse);
562
  } else {
563
+ // Fallback: only add to visible chat if available
564
+ const chatContainer = document.getElementById("chat-container");
565
+ const chatMessages = document.getElementById("chat-messages");
566
+
567
+ if (chatContainer && chatContainer.classList.contains("visible") && chatMessages) {
568
+ this.createChatMessage(chatMessages, "user", userMessage);
569
+ this.createChatMessage(chatMessages, this.selectedCharacter.toLowerCase(), kimiResponse);
570
+ chatMessages.scrollTop = chatMessages.scrollHeight;
571
+ }
572
  }
573
  }
574
 
 
654
 
655
  // Get volume using centralized utility
656
  utterance.volume = this.getVoicePreference("volume", options);
657
+
658
+ // Use centralized emotion system for consistency
659
+ const emotionFromText = window.kimiEmotionSystem?.analyzeEmotionValidated(text) || "neutral";
660
+
661
+ // PRE-PREPARE speaking animation before TTS starts
662
+ if (window.kimiVideo) {
663
+ // Always prepare a speaking context based on detected emotion
664
+ requestAnimationFrame(async () => {
665
+ try {
666
+ const traits = await this.db?.getAllPersonalityTraits(
667
+ window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
668
+ );
669
+ const affection = traits ? traits.affection : 50;
670
+
671
+ // Choose the appropriate speaking context
672
+ if (emotionFromText === "negative") {
673
+ window.kimiVideo.switchToContext("speakingNegative", "negative", null, traits || {}, affection);
674
+ } else if (emotionFromText === "neutral") {
675
+ // Even neutral text should use speaking context during TTS
676
+ window.kimiVideo.switchToContext("speakingPositive", "neutral", null, traits || {}, affection);
677
+ } else {
678
+ // For positive and specific emotions
679
+ const videoCategory =
680
+ window.kimiEmotionSystem?.getVideoCategory(emotionFromText, traits) || "speakingPositive";
681
+ window.kimiVideo.switchToContext(videoCategory, emotionFromText, null, traits || {}, affection);
682
+ }
683
+ } catch (e) {
684
+ console.warn("Failed to prepare speaking animation:", e);
685
+ }
686
  });
687
  }
688
+
689
  if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
690
  window.updatePersonalityTraitsFromEmotion(emotionFromText, text);
691
  }
 
693
 
694
  utterance.onstart = async () => {
695
  this.isSpeaking = true;
 
 
696
 
697
+ // IMMEDIATELY switch to appropriate speaking animation when TTS starts
698
  try {
699
+ if (window.kimiVideo) {
700
+ const traits = await this.db?.getAllPersonalityTraits(
701
+ window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
702
+ );
703
+ const affection = traits ? traits.affection : 50;
704
+
705
+ // Choose speaking context based on the detected emotion using centralized logic
706
+ if (emotionFromText === "negative") {
707
+ window.kimiVideo.switchToContext("speakingNegative", "negative", null, traits || {}, affection);
708
+ } else if (emotionFromText === "neutral") {
709
+ // Even for neutral speech, use speaking context during TTS
710
+ window.kimiVideo.switchToContext("speakingPositive", "neutral", null, traits || {}, affection);
711
+ } else {
712
+ // For positive and specific emotions, use appropriate speaking context
713
+ const videoCategory =
714
+ window.kimiEmotionSystem?.getVideoCategory(emotionFromText, traits) || "speakingPositive";
715
+ window.kimiVideo.switchToContext(videoCategory, emotionFromText, null, traits || {}, affection);
716
  }
717
  }
718
  } catch (e) {
719
+ console.warn("Failed to switch to speaking context:", e);
720
  }
721
  };
722
 
 
726
  this.updateTranscriptVisibility(false);
727
  // Clear any pending hide timeout
728
  this.clearTranscriptTimeout();
729
+
730
+ // IMMEDIATELY return to neutral when TTS ends
731
  if (window.kimiVideo) {
 
732
  try {
733
  const info = window.kimiVideo.getCurrentVideoInfo ? window.kimiVideo.getCurrentVideoInfo() : null;
734
+
735
+ // Only return to neutral if currently in a speaking context
736
+ if (info && (info.context === "speakingPositive" || info.context === "speakingNegative")) {
737
+ // Use async pattern to get traits for neutral transition
738
+ (async () => {
739
+ try {
740
+ const traits = await this.db?.getAllPersonalityTraits(
741
+ window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
742
+ );
743
+ window.kimiVideo.switchToContext(
744
+ "neutral",
745
+ "neutral",
746
+ null,
747
+ traits || {},
748
+ traits?.affection || 50
749
+ );
750
+ } catch (e) {
751
+ // Fallback without traits
752
+ window.kimiVideo.switchToContext("neutral", "neutral", null, {}, 50);
753
+ }
754
+ })();
755
  }
756
+ } catch (e) {
757
+ console.warn("Failed to return to neutral after TTS:", e);
 
 
758
  }
759
  }
760
  };
 
962
  }
963
  if (final_transcript && this.onSpeechAnalysis) {
964
  try {
965
+ // Show final user message in transcript before processing
966
+ await this.showUserMessage(`You: ${final_transcript}`, 2000);
967
+
968
  // Auto-stop after silence timeout following final transcript
969
  setTimeout(() => {
970
  this.stopListening();
 
1272
  this.onSpeechAnalysis = callback;
1273
  }
1274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1275
  async testVoice() {
1276
  const testMessages = [
1277
  window.kimiI18nManager?.t("test_voice_message_1") || "Hello my beloved! 💕",