VirtualKimi commited on
Commit
35443be
·
verified ·
1 Parent(s): a9fae17

Upload 59 files

Browse files
CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
  # Virtual Kimi App Changelog
2
 
 
 
 
 
 
 
 
3
  # [1.1.5] - 2025-09-03
4
 
5
  ### Bug Fixes
 
1
  # Virtual Kimi App Changelog
2
 
3
+ # [1.1.6] - 2025-09-04
4
+
5
+ ### Bug Fixes
6
+
7
+ - Fixed a bug where sliders refused the value 0 (0 was treated as falsy and reset to defaults).
8
+ - Removed crossfade transition from video playback to avoid visual glitches during video changes.
9
+
10
  # [1.1.5] - 2025-09-03
11
 
12
  ### Bug Fixes
index.html CHANGED
@@ -37,44 +37,25 @@
37
  content="Virtual AI companion with evolving personality and advanced voice recognition.">
38
  <meta property="twitter:image" content="kimi-icons/virtualkimi-logo.png">
39
 
40
- <!-- Schema.org consolidated JSON-LD (WebPage + mainEntity SoftwareApplication) -->
41
  <script type="application/ld+json">
42
- {
43
- "@context": "https://schema.org",
44
- "@type": "WebPage",
45
- "name": "Virtual Kimi - Virtual AI Companion",
46
- "description": "Virtual Kimi, your virtual AI girlfriend and companion with an evolving personality, multi-provider AI support, advanced voice recognition and immersive interface.",
47
- "url": "https://virtualkimi.com/virtual-kimi-app/index.html",
48
- "mainEntity": {
49
  "@type": "SoftwareApplication",
50
- "@id": "https://virtualkimi.com/virtual-kimi-app/#app",
51
  "name": "Virtual Kimi",
52
- "description": "Virtual Kimi, your virtual AI girlfriend and companion with an evolving personality, multi-provider AI support, voice recognition and immersive interface",
 
53
  "applicationCategory": "AI Companion",
54
  "operatingSystem": "Web Browser",
55
- "offers": {
56
- "@type": "Offer",
57
- "price": "0",
58
- "priceCurrency": "USD"
59
- },
60
- "creator": {
61
- "@type": "Person",
62
- "name": "Jean & Kimi"
63
- },
64
  "dateCreated": "2025-07-16",
65
- "dateModified": "2025-09-03",
66
- "version": "v1.1.5",
67
- "features": [
68
- "Advanced voice recognition",
69
- "Evolving personality with 6 adjustable traits",
70
- "Premium LLM integration",
71
- "5 customizable visual themes",
72
- "Persistent memory",
73
- "Intelligent affection system"
74
- ]
75
  }
76
- }
77
- </script>
78
 
79
  <!-- Favicon -->
80
  <link rel="icon" type="image/x-icon" href="favicon.ico">
@@ -1087,8 +1068,8 @@
1087
  <h3><i class="fas fa-code"></i> Technical Information</h3>
1088
  <div class="tech-info">
1089
  <p><strong>Created date :</strong> July 16, 2025</p>
1090
- <p><strong>Version :</strong> v1.1.5</p>
1091
- <p><strong>Last update :</strong> September 03, 2025</p>
1092
  <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1093
  API</p>
1094
  <p><strong>Status :</strong> ✅ Stable and functional</p>
 
37
  content="Virtual AI companion with evolving personality and advanced voice recognition.">
38
  <meta property="twitter:image" content="kimi-icons/virtualkimi-logo.png">
39
 
40
+ <!-- Minimal Schema.org JSON-LD for SoftwareApplication -->
41
  <script type="application/ld+json">
42
+ {
43
+ "@context": "https://schema.org",
 
 
 
 
 
44
  "@type": "SoftwareApplication",
 
45
  "name": "Virtual Kimi",
46
+ "url": "https://virtualkimi.com/virtual-kimi-app/index.html",
47
+ "description": "Virtual AI girlfriend and companion with evolving personality and voice interface.",
48
  "applicationCategory": "AI Companion",
49
  "operatingSystem": "Web Browser",
50
+ "author": { "@type": "Person", "name": "Jean & Kimi" },
 
 
 
 
 
 
 
 
51
  "dateCreated": "2025-07-16",
52
+ "dateModified": "2025-09-04",
53
+ "version": "v1.1.6",
54
+ "logo": { "@type": "ImageObject", "url": "https://virtualkimi.com/kimi-icons/virtualkimi-logo.png" },
55
+ "screenshot": ["https://virtualkimi.com/kimi-icons/virtualkimi-preview1.jpg","https://virtualkimi.com/kimi-icons/virtualkimi-preview2.jpg"],
56
+ "sameAs": ["https://x.com/virtualkimi","https://www.youtube.com/@VirtualKimi","https://github.com/virtualkimi"]
 
 
 
 
 
57
  }
58
+ </script>
 
59
 
60
  <!-- Favicon -->
61
  <link rel="icon" type="image/x-icon" href="favicon.ico">
 
1068
  <h3><i class="fas fa-code"></i> Technical Information</h3>
1069
  <div class="tech-info">
1070
  <p><strong>Created date :</strong> July 16, 2025</p>
1071
+ <p><strong>Version :</strong> v1.1.6</p>
1072
+ <p><strong>Last update :</strong> September 04, 2025</p>
1073
  <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1074
  API</p>
1075
  <p><strong>Status :</strong> ✅ Stable and functional</p>
kimi-css/kimi-style.css CHANGED
@@ -119,9 +119,6 @@
119
  --mic-pulse-color: rgba(39, 174, 96, 0.5);
120
  --mic-pulse-listening-color: rgba(39, 174, 96, 0.4);
121
 
122
- /* Video crossfade timing */
123
- --video-fade-duration: 400ms;
124
-
125
  /* Cards & Stats */
126
  --card-bg: rgba(255, 255, 255, 0.02);
127
  --card-border: rgba(255, 255, 255, 0.05);
@@ -899,16 +896,9 @@ body {
899
  height: 100%;
900
  object-fit: contain;
901
  opacity: 0;
902
- transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1);
903
  background-color: #1a1a1a;
904
- will-change: opacity;
905
  backface-visibility: hidden;
906
- }
907
-
908
- .bg-video.transitioning {
909
- opacity: 0;
910
- transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1);
911
- pointer-events: none;
912
  }
913
 
914
  .content-overlay {
 
119
  --mic-pulse-color: rgba(39, 174, 96, 0.5);
120
  --mic-pulse-listening-color: rgba(39, 174, 96, 0.4);
121
 
 
 
 
122
  /* Cards & Stats */
123
  --card-bg: rgba(255, 255, 255, 0.02);
124
  --card-border: rgba(255, 255, 255, 0.05);
 
896
  height: 100%;
897
  object-fit: contain;
898
  opacity: 0;
 
899
  background-color: #1a1a1a;
 
900
  backface-visibility: hidden;
901
+ transition: opacity 300ms cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
 
902
  }
903
 
904
  .content-overlay {
kimi-icons/preview1.jpg ADDED
kimi-icons/preview2.jpg ADDED
kimi-icons/virtualkimi-preview1.jpg ADDED
kimi-icons/virtualkimi-preview2.jpg ADDED
kimi-js/kimi-config.js CHANGED
@@ -113,10 +113,8 @@ window.KIMI_CONFIG.validate = function (value, type) {
113
  try {
114
  const range = this.RANGES[type];
115
  if (!range) return { valid: true, value };
116
-
117
  const numValue = parseFloat(value);
118
  if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] };
119
-
120
  const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
121
  return { valid: true, value: clampedValue };
122
  } catch (error) {
 
113
  try {
114
  const range = this.RANGES[type];
115
  if (!range) return { valid: true, value };
 
116
  const numValue = parseFloat(value);
117
  if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] };
 
118
  const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
119
  return { valid: true, value: clampedValue };
120
  } catch (error) {
kimi-js/kimi-module.js CHANGED
@@ -1442,6 +1442,23 @@ async function sendMessage() {
1442
  }
1443
 
1444
  function setupSettingsListeners(kimiDB, kimiMemory) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1445
  const voiceRateSlider = document.getElementById("voice-rate");
1446
  const voicePitchSlider = document.getElementById("voice-pitch");
1447
  const voiceVolumeSlider = document.getElementById("voice-volume");
@@ -1526,7 +1543,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1526
  if (voiceRateSlider) {
1527
  const listener = e => {
1528
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate");
1529
- const value = validation?.value || parseFloat(e.target.value) || 1.1;
1530
 
1531
  document.getElementById("voice-rate-value").textContent = value;
1532
  e.target.value = value; // Ensure slider shows validated value
@@ -1538,7 +1555,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1538
  if (voicePitchSlider) {
1539
  const listener = e => {
1540
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch");
1541
- const value = validation?.value || parseFloat(e.target.value) || 1.1;
1542
 
1543
  document.getElementById("voice-pitch-value").textContent = value;
1544
  e.target.value = value;
@@ -1550,7 +1567,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1550
  if (voiceVolumeSlider) {
1551
  const listener = e => {
1552
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume");
1553
- const value = validation?.value || parseFloat(e.target.value) || 0.8;
1554
 
1555
  document.getElementById("voice-volume-value").textContent = value;
1556
  e.target.value = value;
@@ -1613,7 +1630,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1613
  if (llmTemperatureSlider) {
1614
  const listener = e => {
1615
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature");
1616
- const value = validation?.value || parseFloat(e.target.value) || 0.9;
1617
 
1618
  document.getElementById("llm-temperature-value").textContent = value;
1619
  e.target.value = value;
@@ -1625,7 +1642,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1625
  if (llmMaxTokensSlider) {
1626
  const listener = e => {
1627
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens");
1628
- const value = validation?.value || parseInt(e.target.value) || 400;
1629
 
1630
  document.getElementById("llm-max-tokens-value").textContent = value;
1631
  e.target.value = value;
@@ -1637,7 +1654,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1637
  if (llmTopPSlider) {
1638
  const listener = e => {
1639
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP");
1640
- const value = validation?.value || parseFloat(e.target.value) || 0.9;
1641
 
1642
  document.getElementById("llm-top-p-value").textContent = value;
1643
  e.target.value = value;
@@ -1649,7 +1666,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1649
  if (llmFrequencyPenaltySlider) {
1650
  const listener = e => {
1651
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty");
1652
- const value = validation?.value || parseFloat(e.target.value) || 0.9;
1653
 
1654
  document.getElementById("llm-frequency-penalty-value").textContent = value;
1655
  e.target.value = value;
@@ -1661,7 +1678,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1661
  if (llmPresencePenaltySlider) {
1662
  const listener = e => {
1663
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty");
1664
- const value = validation?.value || parseFloat(e.target.value) || 0.8;
1665
 
1666
  document.getElementById("llm-presence-penalty-value").textContent = value;
1667
  e.target.value = value;
@@ -1697,7 +1714,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1697
  if (interfaceOpacitySlider) {
1698
  const listener = e => {
1699
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity");
1700
- const value = validation?.value || parseFloat(e.target.value) || 0.8;
1701
 
1702
  document.getElementById("interface-opacity-value").textContent = value;
1703
  e.target.value = value;
 
1442
  }
1443
 
1444
  function setupSettingsListeners(kimiDB, kimiMemory) {
1445
+ // ---------------------------------------------------------------------------
1446
+ // Slider value coercion utilities
1447
+ // Ensures that numeric sliders preserve explicit 0 instead of falling back
1448
+ // to defaults via the logical OR (||) operator. We only fall back when the
1449
+ // parsed value is NaN or validation returns undefined (never when value === 0).
1450
+ // Use coerceFloat / coerceInt in all handlers to standardize behavior.
1451
+ // ---------------------------------------------------------------------------
1452
+ const coerceFloat = (raw, fallback, validationValue) => {
1453
+ if (validationValue !== undefined) return validationValue;
1454
+ const parsed = parseFloat(raw);
1455
+ return Number.isNaN(parsed) ? fallback : parsed;
1456
+ };
1457
+ const coerceInt = (raw, fallback, validationValue) => {
1458
+ if (validationValue !== undefined) return validationValue;
1459
+ const parsed = parseInt(raw, 10);
1460
+ return Number.isNaN(parsed) ? fallback : parsed;
1461
+ };
1462
  const voiceRateSlider = document.getElementById("voice-rate");
1463
  const voicePitchSlider = document.getElementById("voice-pitch");
1464
  const voiceVolumeSlider = document.getElementById("voice-volume");
 
1543
  if (voiceRateSlider) {
1544
  const listener = e => {
1545
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate");
1546
+ const value = coerceFloat(e.target.value, 1.1, validation?.value);
1547
 
1548
  document.getElementById("voice-rate-value").textContent = value;
1549
  e.target.value = value; // Ensure slider shows validated value
 
1555
  if (voicePitchSlider) {
1556
  const listener = e => {
1557
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch");
1558
+ const value = coerceFloat(e.target.value, 1.1, validation?.value);
1559
 
1560
  document.getElementById("voice-pitch-value").textContent = value;
1561
  e.target.value = value;
 
1567
  if (voiceVolumeSlider) {
1568
  const listener = e => {
1569
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume");
1570
+ const value = coerceFloat(e.target.value, 0.8, validation?.value);
1571
 
1572
  document.getElementById("voice-volume-value").textContent = value;
1573
  e.target.value = value;
 
1630
  if (llmTemperatureSlider) {
1631
  const listener = e => {
1632
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature");
1633
+ const value = coerceFloat(e.target.value, 0.9, validation?.value);
1634
 
1635
  document.getElementById("llm-temperature-value").textContent = value;
1636
  e.target.value = value;
 
1642
  if (llmMaxTokensSlider) {
1643
  const listener = e => {
1644
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens");
1645
+ const value = coerceInt(e.target.value, 400, validation?.value);
1646
 
1647
  document.getElementById("llm-max-tokens-value").textContent = value;
1648
  e.target.value = value;
 
1654
  if (llmTopPSlider) {
1655
  const listener = e => {
1656
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP");
1657
+ const value = coerceFloat(e.target.value, 0.9, validation?.value);
1658
 
1659
  document.getElementById("llm-top-p-value").textContent = value;
1660
  e.target.value = value;
 
1666
  if (llmFrequencyPenaltySlider) {
1667
  const listener = e => {
1668
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty");
1669
+ const value = coerceFloat(e.target.value, 0.9, validation?.value);
1670
 
1671
  document.getElementById("llm-frequency-penalty-value").textContent = value;
1672
  e.target.value = value;
 
1678
  if (llmPresencePenaltySlider) {
1679
  const listener = e => {
1680
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty");
1681
+ const value = coerceFloat(e.target.value, 0.8, validation?.value);
1682
 
1683
  document.getElementById("llm-presence-penalty-value").textContent = value;
1684
  e.target.value = value;
 
1714
  if (interfaceOpacitySlider) {
1715
  const listener = e => {
1716
  const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity");
1717
+ const value = coerceFloat(e.target.value, 0.8, validation?.value);
1718
 
1719
  document.getElementById("interface-opacity-value").textContent = value;
1720
  e.target.value = value;
kimi-js/kimi-plugin-manager.js CHANGED
@@ -15,6 +15,16 @@ class KimiPluginManager {
15
  path.startsWith("kimi-plugins/")
16
  );
17
  }
 
 
 
 
 
 
 
 
 
 
18
  async loadPlugins() {
19
  const pluginDirs = await this.getPluginDirs();
20
  this.plugins = [];
@@ -27,22 +37,17 @@ class KimiPluginManager {
27
 
28
  // Basic manifest validation and path sanitization (deny external or absolute URLs)
29
  const validTypes = new Set(["theme", "voice", "behavior"]);
30
- const isSafePath = p =>
31
- typeof p === "string" &&
32
- /^[-a-zA-Z0-9_\/.]+$/.test(p) &&
33
- !p.startsWith("/") &&
34
- !p.includes("..") &&
35
- !/^https?:\/\//i.test(p);
36
 
37
  if (!manifest.name || !manifest.type || !validTypes.has(manifest.type)) {
38
  console.warn(`Invalid plugin manifest in ${dir}: missing name or invalid type`);
39
  continue;
40
  }
41
- if (manifest.style && !isSafePath(manifest.style)) {
42
  console.warn(`Blocked unsafe style path in ${dir}: ${manifest.style}`);
43
  delete manifest.style;
44
  }
45
- if (manifest.main && !isSafePath(manifest.main)) {
46
  console.warn(`Blocked unsafe main path in ${dir}: ${manifest.main}`);
47
  delete manifest.main;
48
  }
 
15
  path.startsWith("kimi-plugins/")
16
  );
17
  }
18
+ // New: validate file name inside a plugin directory (relative path only)
19
+ isValidPluginFileName(file) {
20
+ return (
21
+ typeof file === "string" &&
22
+ /^[-a-zA-Z0-9_\/.]+$/.test(file) &&
23
+ !file.startsWith("/") &&
24
+ !file.includes("..") &&
25
+ !/^https?:\/:/i.test(file)
26
+ );
27
+ }
28
  async loadPlugins() {
29
  const pluginDirs = await this.getPluginDirs();
30
  this.plugins = [];
 
37
 
38
  // Basic manifest validation and path sanitization (deny external or absolute URLs)
39
  const validTypes = new Set(["theme", "voice", "behavior"]);
40
+ // DEPRECATION: inlined isSafePath replaced by isValidPluginFileName()
 
 
 
 
 
41
 
42
  if (!manifest.name || !manifest.type || !validTypes.has(manifest.type)) {
43
  console.warn(`Invalid plugin manifest in ${dir}: missing name or invalid type`);
44
  continue;
45
  }
46
+ if (manifest.style && !this.isValidPluginFileName(manifest.style)) {
47
  console.warn(`Blocked unsafe style path in ${dir}: ${manifest.style}`);
48
  delete manifest.style;
49
  }
50
+ if (manifest.main && !this.isValidPluginFileName(manifest.main)) {
51
  console.warn(`Blocked unsafe main path in ${dir}: ${manifest.main}`);
52
  delete manifest.main;
53
  }
kimi-js/kimi-utils.js CHANGED
@@ -20,24 +20,10 @@ window.KimiValidationUtils = {
20
  return div.innerHTML;
21
  },
22
  validateRange(value, key) {
23
- const bounds = {
24
- voiceRate: { min: 0.5, max: 2, def: 1.1 },
25
- voicePitch: { min: 0.5, max: 2, def: 1.1 },
26
- voiceVolume: { min: 0, max: 1, def: 0.8 },
27
- llmTemperature: { min: 0, max: 1, def: 0.9 },
28
- llmMaxTokens: { min: 1, max: 8192, def: 400 },
29
- llmTopP: { min: 0, max: 1, def: 0.9 },
30
- llmFrequencyPenalty: { min: 0, max: 2, def: 0.9 },
31
- llmPresencePenalty: { min: 0, max: 2, def: 0.8 },
32
- interfaceOpacity: { min: 0.1, max: 1, def: 0.8 }
33
- };
34
- const b = bounds[key] || { min: 0, max: 100, def: 0 };
35
- const v = window.KimiSecurityUtils
36
- ? window.KimiSecurityUtils.validateRange(value, b.min, b.max, b.def)
37
- : isNaN(parseFloat(value))
38
- ? b.def
39
- : Math.max(b.min, Math.min(b.max, parseFloat(value)));
40
- return { value: v, clamped: v !== parseFloat(value) };
41
  }
42
  };
43
 
@@ -90,6 +76,40 @@ const KimiProviderPlaceholders = {
90
  window.KimiProviderPlaceholders = KimiProviderPlaceholders;
91
  export { KimiProviderUtils, KimiProviderPlaceholders };
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  // Performance utility functions for debouncing and throttling
94
  window.KimiPerformanceUtils = {
95
  debounce: function (func, wait, immediate = false, context = null) {
@@ -201,12 +221,8 @@ class KimiSecurityUtils {
201
 
202
  switch (type) {
203
  case "html":
204
- return input
205
- .replace(/&/g, "&amp;")
206
- .replace(/</g, "&lt;")
207
- .replace(/>/g, "&gt;")
208
- .replace(/"/g, "&quot;")
209
- .replace(/'/g, "&#x27;");
210
  case "number":
211
  const num = parseFloat(input);
212
  return isNaN(num) ? 0 : num;
@@ -225,12 +241,6 @@ class KimiSecurityUtils {
225
  }
226
  }
227
 
228
- static validateRange(value, min, max, defaultValue = 0) {
229
- const num = parseFloat(value);
230
- if (isNaN(num)) return defaultValue;
231
- return Math.max(min, Math.min(max, num));
232
- }
233
-
234
  static validateApiKey(key) {
235
  if (!key || typeof key !== "string") return false;
236
  if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
@@ -521,6 +531,42 @@ class KimiOverlayManager {
521
  open(name) {
522
  const el = this.overlays[name];
523
  if (el) el.classList.add("visible");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  }
525
  close(name) {
526
  const el = this.overlays[name];
 
20
  return div.innerHTML;
21
  },
22
  validateRange(value, key) {
23
+ if (!window.KimiRange) {
24
+ throw new Error("KimiRange not initialized before validateRange call");
25
+ }
26
+ return window.KimiRange.clamp(key, value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
  };
29
 
 
76
  window.KimiProviderPlaceholders = KimiProviderPlaceholders;
77
  export { KimiProviderUtils, KimiProviderPlaceholders };
78
 
79
+ // Unified range management (central source of truth for numeric clamping)
80
+ // Keys map UI/logic identifiers to CONFIG constant names.
81
+ window.KimiRange = {
82
+ KEY_MAP: {
83
+ voiceRate: "VOICE_RATE",
84
+ voicePitch: "VOICE_PITCH",
85
+ voiceVolume: "VOICE_VOLUME",
86
+ llmTemperature: "LLM_TEMPERATURE",
87
+ llmMaxTokens: "LLM_MAX_TOKENS",
88
+ llmTopP: "LLM_TOP_P",
89
+ llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY",
90
+ llmPresencePenalty: "LLM_PRESENCE_PENALTY",
91
+ interfaceOpacity: "INTERFACE_OPACITY"
92
+ },
93
+ getBounds(key) {
94
+ try {
95
+ const configKey = this.KEY_MAP[key];
96
+ if (configKey && window.KIMI_CONFIG && window.KIMI_CONFIG.RANGES && window.KIMI_CONFIG.RANGES[configKey]) {
97
+ const range = window.KIMI_CONFIG.RANGES[configKey];
98
+ const def = window.KIMI_CONFIG.DEFAULTS?.[configKey] ?? range.min;
99
+ return { min: range.min, max: range.max, def };
100
+ }
101
+ } catch {}
102
+ return { min: 0, max: 100, def: 0 };
103
+ },
104
+ clamp(key, value) {
105
+ const b = this.getBounds(key);
106
+ const num = parseFloat(value);
107
+ if (isNaN(num)) return { value: b.def, clamped: true };
108
+ const v = Math.max(b.min, Math.min(b.max, num));
109
+ return { value: v, clamped: v !== num };
110
+ }
111
+ };
112
+
113
  // Performance utility functions for debouncing and throttling
114
  window.KimiPerformanceUtils = {
115
  debounce: function (func, wait, immediate = false, context = null) {
 
221
 
222
  switch (type) {
223
  case "html":
224
+ // Reuse centralized escape logic (removes duplication with KimiValidationUtils.escapeHtml)
225
+ return window.KimiValidationUtils?.escapeHtml(input) || input;
 
 
 
 
226
  case "number":
227
  const num = parseFloat(input);
228
  return isNaN(num) ? 0 : num;
 
241
  }
242
  }
243
 
 
 
 
 
 
 
244
  static validateApiKey(key) {
245
  if (!key || typeof key !== "string") return false;
246
  if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
 
531
  open(name) {
532
  const el = this.overlays[name];
533
  if (el) el.classList.add("visible");
534
+ // Special handling: opening settings overlay sometimes causes active video to freeze (browser rendering stall)
535
+ if (name === "settings-overlay") {
536
+ const kv = window.kimiVideo;
537
+ if (kv && kv.activeVideo) {
538
+ // Short delay so layout / repaint settles before forcing playback
539
+ setTimeout(() => {
540
+ try {
541
+ const v = kv.activeVideo;
542
+ if (!v) return;
543
+ // If ended -> immediately cycle neutral to avoid static frame
544
+ if (v.ended) {
545
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
546
+ } else {
547
+ // Near-end ( <400ms rest ) -> preemptively rotate to avoid stuck on last frame
548
+ if (v.duration && !isNaN(v.duration) && v.duration - v.currentTime < 0.4) {
549
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
550
+ } else if (v.paused) {
551
+ v.play().catch(() => {});
552
+ }
553
+ }
554
+ // Restart freeze watchdog if available
555
+ if (typeof kv._startFreezeWatchdog === "function") kv._startFreezeWatchdog();
556
+ } catch {}
557
+ }, 50);
558
+ // Deferred recheck (covers cases where autoplay is blocked after overlay animation)
559
+ setTimeout(() => {
560
+ try {
561
+ const v = kv.activeVideo;
562
+ if (!v) return;
563
+ if (!v.ended && (v.paused || v.readyState < 2)) {
564
+ v.play().catch(() => {});
565
+ }
566
+ } catch {}
567
+ }, 600);
568
+ }
569
+ }
570
  }
571
  close(name) {
572
  const el = this.overlays[name];
kimi-js/kimi-videos.js CHANGED
@@ -60,54 +60,13 @@ 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.
66
- static crossfadeVideos(fromVideo, toVideo, duration = 300, onComplete) {
67
- // Resolve duration from CSS variable if present
68
  try {
69
- const cssDur = getComputedStyle(document.documentElement).getPropertyValue("--video-fade-duration").trim();
70
- if (cssDur) {
71
- // Convert CSS time to ms number if needed (e.g., '300ms' or '0.3s')
72
- if (cssDur.endsWith("ms")) duration = parseFloat(cssDur);
73
- else if (cssDur.endsWith("s")) duration = Math.round(parseFloat(cssDur) * 1000);
74
  }
75
  } catch {}
76
-
77
- // Preload and strict synchronization
78
- const easing = "ease-in-out";
79
- fromVideo.style.transition = `opacity ${duration}ms ${easing}`;
80
- toVideo.style.transition = `opacity ${duration}ms ${easing}`;
81
- // Prepare target video (opacity 0, top z-index)
82
- toVideo.style.opacity = "0";
83
- toVideo.style.zIndex = "2";
84
- fromVideo.style.zIndex = "1";
85
-
86
- // Start target video slightly before the crossfade
87
- const startTarget = () => {
88
- if (toVideo.paused) toVideo.play().catch(() => {});
89
- // Lance le fondu croisé
90
- setTimeout(() => {
91
- fromVideo.style.opacity = "0";
92
- toVideo.style.opacity = "1";
93
- }, 20);
94
- // After transition, adjust z-index and call the callback
95
- setTimeout(() => {
96
- fromVideo.style.zIndex = "1";
97
- toVideo.style.zIndex = "2";
98
- if (onComplete) onComplete();
99
- }, duration + 30);
100
- };
101
-
102
- // If target video is not ready, wait for canplay
103
- if (toVideo.readyState < 3) {
104
- toVideo.addEventListener("canplay", startTarget, { once: true });
105
- toVideo.load();
106
- } else {
107
- startTarget();
108
- }
109
- // Ensure source video is playing
110
- if (fromVideo.paused) fromVideo.play().catch(() => {});
111
  }
112
 
113
  //Centralized video element creation utility.
@@ -119,7 +78,6 @@ class KimiVideoManager {
119
  video.muted = true;
120
  video.playsinline = true;
121
  video.preload = "auto";
122
- video.style.opacity = "0";
123
  video.innerHTML =
124
  '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
125
  return video;
@@ -1376,10 +1334,11 @@ class KimiVideoManager {
1376
  const fromVideo = this.activeVideo;
1377
  const toVideo = this.inactiveVideo;
1378
 
1379
- // Perform a JS-managed crossfade for smoother transitions
1380
- // Let crossfadeVideos resolve duration from CSS variable (--video-fade-duration)
1381
- this.constructor.crossfadeVideos(fromVideo, toVideo, undefined, () => {
1382
- // After crossfade completion, finalize state and classes
 
1383
  fromVideo.classList.remove("active");
1384
  toVideo.classList.add("active");
1385
 
@@ -1397,22 +1356,20 @@ class KimiVideoManager {
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;
1403
  if (!isNaN(d) && d > 0.5) {
1404
- // Keep 1s headroom before natural end for auto scheduling
1405
  const target = Math.max(1000, d * 1000 - 1100);
1406
  this.autoTransitionDuration = target;
1407
  } else {
1408
- this.autoTransitionDuration = 9900; // fallback for 10s clips
1409
  }
1410
- // Dynamic neutral prefetch to widen diversity without burst
1411
  this._prefetchNeutralDynamic();
1412
  } catch {}
1413
  } catch {}
1414
  this._switchInProgress = false;
1415
  this.setupEventListenersForContext(this.currentContext);
 
1416
  })
1417
  .catch(error => {
1418
  console.warn("Failed to play video:", error);
@@ -1428,7 +1385,7 @@ class KimiVideoManager {
1428
  this.setupEventListenersForContext(this.currentContext);
1429
  });
1430
  } else {
1431
- // Non-promise play fallback
1432
  this._switchInProgress = false;
1433
  try {
1434
  const d = this.activeVideo.duration;
@@ -1441,8 +1398,82 @@ class KimiVideoManager {
1441
  this._prefetchNeutralDynamic();
1442
  } catch {}
1443
  this.setupEventListenersForContext(this.currentContext);
 
1444
  }
1445
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1446
  }
1447
 
1448
  _prefetchNeutralDynamic() {
 
60
  this._consecutiveErrorCount = 0;
61
  // Track per-video load attempts to adapt timeouts & avoid faux échecs
62
  this._videoAttempts = new Map();
 
63
 
64
+ // Ensure the initially active video is visible (remove any stale inline opacity)
 
 
65
  try {
66
+ if (this.activeVideo && this.activeVideo.style && this.activeVideo.classList.contains("active")) {
67
+ this.activeVideo.style.opacity = ""; // rely purely on CSS class
 
 
 
68
  }
69
  } catch {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
71
 
72
  //Centralized video element creation utility.
 
78
  video.muted = true;
79
  video.playsinline = true;
80
  video.preload = "auto";
 
81
  video.innerHTML =
82
  '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
83
  return video;
 
1334
  const fromVideo = this.activeVideo;
1335
  const toVideo = this.inactiveVideo;
1336
 
1337
+ const finalizeSwap = () => {
1338
+ // Clear any inline opacity to rely solely on class-based visibility
1339
+ fromVideo.style.opacity = "";
1340
+ toVideo.style.opacity = "";
1341
+
1342
  fromVideo.classList.remove("active");
1343
  toVideo.classList.add("active");
1344
 
 
1356
  const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
1357
  const info = { context: this.currentContext, emotion: this.currentEmotion };
1358
  console.log("🎬 VideoManager: Now playing:", src, info);
 
1359
  try {
1360
  const d = this.activeVideo.duration;
1361
  if (!isNaN(d) && d > 0.5) {
 
1362
  const target = Math.max(1000, d * 1000 - 1100);
1363
  this.autoTransitionDuration = target;
1364
  } else {
1365
+ this.autoTransitionDuration = 9900;
1366
  }
 
1367
  this._prefetchNeutralDynamic();
1368
  } catch {}
1369
  } catch {}
1370
  this._switchInProgress = false;
1371
  this.setupEventListenersForContext(this.currentContext);
1372
+ this._startFreezeWatchdog();
1373
  })
1374
  .catch(error => {
1375
  console.warn("Failed to play video:", error);
 
1385
  this.setupEventListenersForContext(this.currentContext);
1386
  });
1387
  } else {
1388
+ // Non-promise fallback
1389
  this._switchInProgress = false;
1390
  try {
1391
  const d = this.activeVideo.duration;
 
1398
  this._prefetchNeutralDynamic();
1399
  } catch {}
1400
  this.setupEventListenersForContext(this.currentContext);
1401
+ this._startFreezeWatchdog();
1402
  }
1403
+ };
1404
+
1405
+ // Ensure target video is at start and attempt playback ahead of swap
1406
+ try {
1407
+ toVideo.currentTime = 0;
1408
+ } catch {}
1409
+ const ready = toVideo.readyState >= 2; // HAVE_CURRENT_DATA
1410
+ if (!ready) {
1411
+ const onReady = () => {
1412
+ toVideo.removeEventListener("canplay", onReady);
1413
+ finalizeSwap();
1414
+ };
1415
+ toVideo.addEventListener("canplay", onReady, { once: true });
1416
+ // Trigger load if not already
1417
+ try {
1418
+ toVideo.load();
1419
+ } catch {}
1420
+ // Also try to play (some browsers will start buffering more aggressively)
1421
+ toVideo.play().catch(() => {});
1422
+ } else {
1423
+ // Already ready -> swap immediately
1424
+ toVideo.play().catch(() => {});
1425
+ finalizeSwap();
1426
+ }
1427
+ }
1428
+
1429
+ // Watchdog to detect freeze when a 10s clip reaches end but 'ended' listener may not fire (browser quirk)
1430
+ _startFreezeWatchdog() {
1431
+ clearInterval(this._freezeInterval);
1432
+ const v = this.activeVideo;
1433
+ if (!v) return;
1434
+ const CHECK_MS = 1000;
1435
+ this._lastProgressTime = Date.now();
1436
+ let lastTime = v.currentTime;
1437
+ // Stalled detection via progress event
1438
+ const onStalled = () => {
1439
+ this._lastProgressTime = Date.now();
1440
+ };
1441
+ v.addEventListener("timeupdate", onStalled);
1442
+ v.addEventListener("progress", onStalled);
1443
+ this._freezeInterval = setInterval(() => {
1444
+ if (v !== this.activeVideo) return; // switched
1445
+ const dur = v.duration || 9.9; // assume 9.9s
1446
+ const nearEnd = v.currentTime >= dur - 0.25; // last 250ms
1447
+ const progressed = v.currentTime !== lastTime;
1448
+ if (progressed) {
1449
+ lastTime = v.currentTime;
1450
+ this._lastProgressTime = Date.now();
1451
+ }
1452
+ // If near end and not auto-transitioned within 500ms, trigger manual neutral
1453
+ if (nearEnd && Date.now() - this._lastProgressTime > 600) {
1454
+ // Ensure we are not already neutral cycling
1455
+ if (this.currentContext === "neutral") {
1456
+ // Pick another neutral to animate
1457
+ try {
1458
+ this.returnToNeutral();
1459
+ } catch {}
1460
+ } else {
1461
+ if (!this._processPendingSwitches()) this.returnToNeutral();
1462
+ }
1463
+ }
1464
+ // Extra safety: if video paused unexpectedly before end
1465
+ if (!v.paused && !v.ended && Date.now() - this._lastProgressTime > 4000) {
1466
+ try {
1467
+ v.play().catch(() => {});
1468
+ } catch {}
1469
+ }
1470
+ // Cleanup if naturally ended (ended handler will schedule next)
1471
+ if (v.ended) {
1472
+ clearInterval(this._freezeInterval);
1473
+ v.removeEventListener("timeupdate", onStalled);
1474
+ v.removeEventListener("progress", onStalled);
1475
+ }
1476
+ }, CHECK_MS);
1477
  }
1478
 
1479
  _prefetchNeutralDynamic() {
kimi-js/kimi-voices.js CHANGED
@@ -498,9 +498,10 @@ class KimiVoiceManager {
498
  getVoicePreference(paramType, options = {}) {
499
  // Hierarchy: options > memory.preferences > kimiMemory.preferences > DOM element > default
500
  const defaults = {
501
- rate: window.KIMI_CONFIG?.DEFAULTS?.VOICE_RATE || 1.1,
502
- pitch: window.KIMI_CONFIG?.DEFAULTS?.VOICE_PITCH || 1.1,
503
- volume: window.KIMI_CONFIG?.DEFAULTS?.VOICE_VOLUME || 0.8
 
504
  };
505
 
506
  const elementIds = {
 
498
  getVoicePreference(paramType, options = {}) {
499
  // Hierarchy: options > memory.preferences > kimiMemory.preferences > DOM element > default
500
  const defaults = {
501
+ // Use nullish coalescing to preserve explicit 0 values in config
502
+ rate: window.KIMI_CONFIG?.DEFAULTS?.VOICE_RATE ?? 1.1,
503
+ pitch: window.KIMI_CONFIG?.DEFAULTS?.VOICE_PITCH ?? 1.1,
504
+ volume: window.KIMI_CONFIG?.DEFAULTS?.VOICE_VOLUME ?? 0.8
505
  };
506
 
507
  const elementIds = {