Spaces:
Running
Running
Upload 38 files
Browse files- CHANGELOG.md +21 -2
- README.md +428 -431
- index.html +10 -10
- kimi-css/kimi-style.css +9 -0
- kimi-js/kimi-constants.js +93 -2
- kimi-js/kimi-database.js +88 -10
- kimi-js/kimi-emotion-system.js +205 -23
- kimi-js/kimi-llm-manager.js +31 -7
- kimi-js/kimi-memory-system.js +374 -27
- kimi-js/kimi-memory-ui.js +98 -13
- kimi-js/kimi-module.js +7 -2
- kimi-js/kimi-script.js +15 -0
- kimi-js/kimi-voices.js +21 -7
CHANGELOG.md
CHANGED
|
@@ -1,17 +1,36 @@
|
|
| 1 |
# Virtual Kimi Changelog
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
# [1.1.1] - 2025-08-29
|
| 4 |
|
| 5 |
### Improvements
|
| 6 |
|
| 7 |
-
- Improved language and voice selection logic: normalization, fallback, and robust preference management across all modules.
|
| 8 |
- Enhanced voice compatibility and ensured consistent language handling.
|
| 9 |
|
| 10 |
### Bug Fixes
|
| 11 |
|
| 12 |
- Fixed issue where videos could freeze after opening or closing the memory modal or changing memory sections.
|
| 13 |
- Added automatic reset to neutral video state after UI interactions to prevent stuck/frozen videos.
|
| 14 |
-
- Fixed import/export functions for preferences and data to ensure exported files can be re-imported correctly.
|
| 15 |
|
| 16 |
# [1.1.0] - 2025-08-28
|
| 17 |
|
|
|
|
| 1 |
# Virtual Kimi Changelog
|
| 2 |
|
| 3 |
+
# [1.1.2] - 2025-08-30
|
| 4 |
+
|
| 5 |
+
### Improvements
|
| 6 |
+
|
| 7 |
+
- Improved memory and prompt generation to avoid duplicate memory sections and display accurate per-character counters.
|
| 8 |
+
|
| 9 |
+
### Added
|
| 10 |
+
|
| 11 |
+
- A concise "7-day summary" feature that extracts high-signal conversation highlights for quick reference.
|
| 12 |
+
|
| 13 |
+
### Notes
|
| 14 |
+
|
| 15 |
+
- Voice UI and TTS: Only Microsoft Edge and Google Chrome will display the voice selection list and support voice playback of messages; other browsers may not expose compatible voices.
|
| 16 |
+
|
| 17 |
+
### Bug Fixes
|
| 18 |
+
|
| 19 |
+
- Fixed import/export functions for preferences and data to ensure exported files can be re-imported correctly.
|
| 20 |
+
|
| 21 |
+
- Fixed some small bugs related to memory, video playback, and preference import/export.
|
| 22 |
+
|
| 23 |
# [1.1.1] - 2025-08-29
|
| 24 |
|
| 25 |
### Improvements
|
| 26 |
|
| 27 |
+
- Microsoft Edge and Google Chrome Only : Improved language and voice selection logic: normalization, fallback, and robust preference management across all modules.
|
| 28 |
- Enhanced voice compatibility and ensured consistent language handling.
|
| 29 |
|
| 30 |
### Bug Fixes
|
| 31 |
|
| 32 |
- Fixed issue where videos could freeze after opening or closing the memory modal or changing memory sections.
|
| 33 |
- Added automatic reset to neutral video state after UI interactions to prevent stuck/frozen videos.
|
|
|
|
| 34 |
|
| 35 |
# [1.1.0] - 2025-08-28
|
| 36 |
|
README.md
CHANGED
|
@@ -1,431 +1,428 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
- **
|
| 29 |
-
- **
|
| 30 |
-
- **
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
- **
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
- **
|
| 62 |
-
- **
|
| 63 |
-
- **
|
| 64 |
-
- **
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
- **
|
| 71 |
-
- **
|
| 72 |
-
- **
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
-
|
| 80 |
-
-
|
| 81 |
-
-
|
| 82 |
-
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
-
|
| 88 |
-
-
|
| 89 |
-
-
|
| 90 |
-
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
- Real-time
|
| 97 |
-
-
|
| 98 |
-
-
|
| 99 |
-
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
-
|
| 105 |
-
-
|
| 106 |
-
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
-
|
| 112 |
-
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
-
|
| 119 |
-
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
-
|
| 125 |
-
-
|
| 126 |
-
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
-
|
| 133 |
-
-
|
| 134 |
-
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
-
|
| 217 |
-
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
-
|
| 226 |
-
-
|
| 227 |
-
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
-
|
| 232 |
-
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
-
|
| 241 |
-
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
-
|
| 252 |
-
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
"
|
| 263 |
-
"
|
| 264 |
-
"
|
| 265 |
-
"
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
βββ
|
| 279 |
-
βββ
|
| 280 |
-
βββ
|
| 281 |
-
βββ
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
"
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
```
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
- Memory
|
| 348 |
-
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
-
|
| 364 |
-
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
-
|
| 396 |
-
-
|
| 397 |
-
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
- [ ]
|
| 402 |
-
- [ ]
|
| 403 |
-
- [ ]
|
| 404 |
-
- [ ]
|
| 405 |
-
- [ ]
|
| 406 |
-
- [ ]
|
| 407 |
-
- [ ]
|
| 408 |
-
- [ ]
|
| 409 |
-
- [ ]
|
| 410 |
-
- [ ]
|
| 411 |
-
- [ ]
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
> _"Love is the most powerful code"_ π
|
| 430 |
-
>
|
| 431 |
-
> β 2025 Virtual Kimi - Created with π by Jean & Kimi
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
|
| 3 |
+
<b>Virtual Kimi</b>
|
| 4 |
+
|
| 5 |
+
[](https://github.com/virtualkimi)
|
| 6 |
+
[](#license)
|
| 7 |
+
|
| 8 |
+
</div>
|
| 9 |
+
|
| 10 |
+
# Virtual Kimi - AI Companion Application π
|
| 11 |
+
|
| 12 |
+
Web-based AI girlfriend and companion featuring adaptive personalities, intelligent memory systems, and immersive conversational experiences.
|
| 13 |
+
|
| 14 |
+
## Overview
|
| 15 |
+
|
| 16 |
+
Virtual Kimi is an advanced virtual companion application that combines modern web technologies with state-of-the-art AI models to create meaningful, evolving relationships between users and AI girlfriend personalities.
|
| 17 |
+
|
| 18 |
+
- **Lightweight:** ~600 KB of pure JavaScript, HTML, and CSS (no frameworks)
|
| 19 |
+
- **Local-first:** All data is stored in your browser's IndexedDB (managed by Dexie.js)
|
| 20 |
+
- **No tracking:** The only external calls are to FontAwesome (for icons) and the OpenRouter API (for AI)
|
| 21 |
+
|
| 22 |
+
Built with vanilla JavaScript and modern web APIs, it offers a rich, responsive experience across devices.
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## π Support & Links
|
| 27 |
+
|
| 28 |
+
- **Website**: [virtualkimi.com](https://virtualkimi.com)
|
| 29 |
+
- **Email**: [ijohn@virtualkimi.com](ijohn@virtualkimi.com)
|
| 30 |
+
- **X (Twitter)**: [x.com/virtualkimi](https://x.com/virtualkimi)
|
| 31 |
+
- **GitHub**: [github.com/virtualkimi](https://github.com/virtualkimi)
|
| 32 |
+
- **HuggingFace**: [huggingface.co/VirtualKimi](https://huggingface.co/VirtualKimi)
|
| 33 |
+
- **YouTube**: [YouTube Channel](https://www.youtube.com/@VirtualKimi)
|
| 34 |
+
|
| 35 |
+
- **Support the project**: [ko-fi.com/virtualkimi](https://ko-fi.com/virtualkimi)
|
| 36 |
+
_If you like this project or want to help me (I'm currently without a permanent job), you can buy me a coffee or make a donation. Every bit helps keep Virtual Kimi alive and evolving!_
|
| 37 |
+
|
| 38 |
+
- **ETH Wallet**: 0x836C9D2e605f98Bc7144C62Bef837627b1a9C30c
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## Key Features
|
| 43 |
+
|
| 44 |
+
### π€ **Advanced AI Integration**
|
| 45 |
+
|
| 46 |
+
Recommended models (IDs and short notes β updated Aug 2025):
|
| 47 |
+
|
| 48 |
+
- mistralai/mistral-small-3.2-24b-instruct β Mistral-small-3.2 (128k context, economical)
|
| 49 |
+
- qwen/qwen3-30b-a3b-instruct-2507 β Qwen3 30b (131k context)
|
| 50 |
+
- nousresearch/hermes-4-70b β Nous Hermes 4 70B (131k context)
|
| 51 |
+
- x-ai/grok-3-mini β Grok 3 mini (131k context)
|
| 52 |
+
- cohere/command-r-08-2024 β Cohere Command-R (128k context)
|
| 53 |
+
- qwen/qwen3-235b-a22b-2507 β Qwen3-235b (262k context)
|
| 54 |
+
- anthropic/claude-3-haiku β Claude 3 Haiku (large context)
|
| 55 |
+
- local/ollama β Local Model (Ollama, experimental, 4k context)
|
| 56 |
+
|
| 57 |
+
Notes: model IDs in the app are authoritative; pricing/context values are indicative and may change with providers.
|
| 58 |
+
|
| 59 |
+
### π₯ **Multiple AI Personalities**
|
| 60 |
+
|
| 61 |
+
- **Kimi**: Cosmic dreamer and astrophysicist with ethereal sensibilities
|
| 62 |
+
- **Bella**: Nurturing botanist who sees people as plants needing care
|
| 63 |
+
- **Rosa**: Chaotic prankster thriving on controlled chaos
|
| 64 |
+
- **Stella**: Digital artist transforming reality through pixelated vision
|
| 65 |
+
|
| 66 |
+
### Personality Trait Ranges
|
| 67 |
+
|
| 68 |
+
All personality traits operate on a 0-100 scale:
|
| 69 |
+
|
| 70 |
+
- **Affection**: Emotional warmth and attachment
|
| 71 |
+
- **Playfulness**: Fun-loving and spontaneous behavior
|
| 72 |
+
- **Intelligence**: Analytical and thoughtful responses
|
| 73 |
+
- **Empathy**: Understanding and emotional support
|
| 74 |
+
- **Humor**: Wit and lighthearted interactions
|
| 75 |
+
- **Romance**: Romantic and intimate expressions
|
| 76 |
+
|
| 77 |
+
### π§ **Intelligent Memory System**
|
| 78 |
+
|
| 79 |
+
- Automatic extraction and categorization of conversation memories
|
| 80 |
+
- Seven memory categories: Personal, Preferences, Relationships, Activities, Goals, Experiences, Events
|
| 81 |
+
- Persistent memory across sessions with search and management capabilities
|
| 82 |
+
- Character-specific memory isolation
|
| 83 |
+
|
| 84 |
+
### π« **Dynamic Personality Evolution**
|
| 85 |
+
|
| 86 |
+
- Six personality traits that evolve based on interactions:
|
| 87 |
+
- Affection, Playfulness, Intelligence, Empathy, Humor, Romance
|
| 88 |
+
- Real-time trait adjustments based on conversation tone and content
|
| 89 |
+
- Visual personality indicators and progression tracking
|
| 90 |
+
- Intelligent model selection and switching
|
| 91 |
+
- Real-time emotion detection and analysis
|
| 92 |
+
- Contextually-aware responses
|
| 93 |
+
|
| 94 |
+
### π¬ **Emotion-Driven Visual Experience**
|
| 95 |
+
|
| 96 |
+
- Real-time video responses matching detected emotions
|
| 97 |
+
- Smooth transitions between emotional states
|
| 98 |
+
- Character-specific visual libraries with ~240 video clips (approx. 60 per main character)
|
| 99 |
+
- Context-aware video selection system
|
| 100 |
+
|
| 101 |
+
### π¨ **Customizable Interface**
|
| 102 |
+
|
| 103 |
+
- Five professionally designed themes
|
| 104 |
+
- Adjustable interface transparency
|
| 105 |
+
- Responsive design optimized for desktop, tablet, and mobile
|
| 106 |
+
- Accessibility features and keyboard navigation
|
| 107 |
+
|
| 108 |
+
### π **Multilingual Support**
|
| 109 |
+
|
| 110 |
+
- Full localization in 7 languages: English, French, Spanish, German, Italian, Japanese, Chinese
|
| 111 |
+
- Automatic language detection from user input
|
| 112 |
+
- Culturally-aware responses and emotion keywords
|
| 113 |
+
|
| 114 |
+
### π **Extensible Plugin System**
|
| 115 |
+
|
| 116 |
+
- Theme plugins for visual customization (currently, only the color theme plugin is functional)
|
| 117 |
+
- Voice plugins for speech synthesis options (planned)
|
| 118 |
+
- Behavior plugins for personality modifications (planned)
|
| 119 |
+
- Secure plugin loading with validation
|
| 120 |
+
|
| 121 |
+
### π‘οΈ **Security & Privacy**
|
| 122 |
+
|
| 123 |
+
- Input validation and sanitization
|
| 124 |
+
- Secure API key handling
|
| 125 |
+
- Local data storage with IndexedDB
|
| 126 |
+
- No server dependencies for core functionality
|
| 127 |
+
|
| 128 |
+
## ποΈ Technical Architecture
|
| 129 |
+
|
| 130 |
+
### π§© Core Technologies
|
| 131 |
+
|
| 132 |
+
- **Frontend**: Vanilla JavaScript (ES6+), HTML5, CSS3
|
| 133 |
+
- **Database**: IndexedDB with Dexie.js
|
| 134 |
+
- **AI Integration**: OpenRouter (default), OpenAI-compatible, Cohere, Anthropic, Groq, Together, or local Ollama
|
| 135 |
+
- **Speech**: Web Speech API
|
| 136 |
+
- **Audio**: Web Audio API
|
| 137 |
+
|
| 138 |
+
---
|
| 139 |
+
|
| 140 |
+
## β¨ Inspiration & Assets
|
| 141 |
+
|
| 142 |
+
This project was originally inspired by the [JackyWine GitHub repository](https://github.com/Jackywine).
|
| 143 |
+
@Jackywine on X (Twitter)
|
| 144 |
+
|
| 145 |
+
The four main characters are visually based on images from four creators on X (Twitter):
|
| 146 |
+
|
| 147 |
+
- @JulyFox33 (Kimi)
|
| 148 |
+
- @BelisariaNew (Bella)
|
| 149 |
+
- @JuliAIkiko (Rosa and Stella)
|
| 150 |
+
|
| 151 |
+
All character videos were generated using the image-to-video AI from Kling.ai, specifically with the Kling v2.1 model.
|
| 152 |
+
|
| 153 |
+
Get 50% bonus Credits in your first month with this code referral 7BR9GT2WQ6JF - link: [https://klingai.com](https://klingai.com/h5-app/invitation?code=7BR9GT2WQ6JF)
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
### Project Structure (current)
|
| 158 |
+
|
| 159 |
+
Top-level files and main modules:
|
| 160 |
+
|
| 161 |
+
- index.html β Main application entry
|
| 162 |
+
- virtual-kimi (static assets & landing): virtual-kimi\*.html
|
| 163 |
+
- kimi-js/
|
| 164 |
+
- kimi-script.js β App initialization & orchestration
|
| 165 |
+
- kimi-database.js β IndexedDB persistence (Dexie)
|
| 166 |
+
- kimi-config.js β Runtime configuration
|
| 167 |
+
- kimi-security.js β Security utilities
|
| 168 |
+
- kimi-llm-manager.js β LLM integration and model list
|
| 169 |
+
- kimi-emotion-system.js β Emotion analysis
|
| 170 |
+
- kimi-memory-system.js β Memory extraction & storage
|
| 171 |
+
- kimi-memory-ui.js β Memory management UI
|
| 172 |
+
- kimi-appearance.js, kimi-voices.js, kimi-utils.js, kimi-module.js, etc.
|
| 173 |
+
- kimi-css/ β styles and themes
|
| 174 |
+
- kimi-plugins/ β local plugins (sample-theme, sample-behavior, sample-voice)
|
| 175 |
+
- kimi-locale/ β translations and i18n files
|
| 176 |
+
- dexie.min.js β local DB helper
|
| 177 |
+
|
| 178 |
+
### Data Flow
|
| 179 |
+
|
| 180 |
+
1. **Input Processing**: User input β Security validation β Language detection
|
| 181 |
+
2. **AI Analysis**: Emotion detection β Memory extraction β LLM processing
|
| 182 |
+
3. **Response Generation**: Personality-aware response β Emotion mapping β Visual selection
|
| 183 |
+
4. **Memory Update**: Trait evolution β Memory storage β UI synchronization
|
| 184 |
+
|
| 185 |
+
## Installation & Setup
|
| 186 |
+
|
| 187 |
+
### Prerequisites
|
| 188 |
+
|
| 189 |
+
- Modern web browser (Chrome, Edge or recent Firefox recommended). The app is plain HTML/CSS/JS and runs locally in a browser β no build step is required.
|
| 190 |
+
- (Optional) OpenRouter API key or another compatible provider API key if you want remote LLM access. The app can run without a key using local-only features, but AI responses will be limited.
|
| 191 |
+
- HTTPS is required for microphone access and some browser TTS features. For full voice/microphone features, open the app via a local server (see Quick Start). If you only open `index.html` directly, most features still work but microphone may be blocked by the browser.
|
| 192 |
+
- Dexie.js is included for IndexedDB persistence (no installation required; file `dexie.min.js` is bundled).
|
| 193 |
+
|
| 194 |
+
### Quick Start
|
| 195 |
+
|
| 196 |
+
1. **Clone the repository**
|
| 197 |
+
|
| 198 |
+
```bash
|
| 199 |
+
git clone https://github.com/virtualkimi/virtual-kimi.git
|
| 200 |
+
cd virtual-kimi
|
| 201 |
+
```
|
| 202 |
+
|
| 203 |
+
2. **Open the application**
|
| 204 |
+
|
| 205 |
+
- Open `index.html` in your web browser
|
| 206 |
+
- Or serve via local web server for optimal performance:
|
| 207 |
+
```bash
|
| 208 |
+
python -m http.server 8000
|
| 209 |
+
# Navigate to http://localhost:8000
|
| 210 |
+
```
|
| 211 |
+
|
| 212 |
+
3. **Configure API access**
|
| 213 |
+
|
| 214 |
+
- Open Settings β AI & Models
|
| 215 |
+
- Provider: choose the LLM provider to use. Supported options in the app include `openrouter` (recommended), `openai`/OpenAI-compatible endpoints, and `ollama` (local). The selected provider is saved in the app preferences.
|
| 216 |
+
- API key: paste your provider API key into the API Key field. Keys are saved locally in your browser's IndexedDB (preferences) so they never leave your machine except when the app sends requests to the selected provider.
|
| 217 |
+
- Base URL: if you use a nonβdefault provider or an OpenAI-compatible endpoint, set the provider base URL (Settings β AI & Models β Base URL). This overrides the built-in default endpoints.
|
| 218 |
+
- Model selection: choose a model ID from the list. The app's authoritative model list is defined in `kimi-js/kimi-llm-manager.js`. If the provider does not support the selected model, the app will attempt a best-match fallback or show an error.
|
| 219 |
+
- Test API key: use the app's built-in test function (Settings β Test API) to validate your key; this performs a minimal request and reports success or the provider error message.
|
| 220 |
+
- Local (Ollama) usage: to use a local model with `ollama`, run your local Ollama-compatible server (the app uses `http://localhost:11434/api/chat`). Select provider `ollama` and set model ID accordingly. Local models are experimental and work only if the local server is running.
|
| 221 |
+
- No key behavior: the app runs without an API key for local UI features and stored data, but remote LLM responses require a valid provider/key. If no key is configured the app will return friendly fallback messages for AI features.
|
| 222 |
+
|
| 223 |
+
Troubleshooting notes:
|
| 224 |
+
|
| 225 |
+
- If you see HTTP 401: verify the API key for the selected provider.
|
| 226 |
+
- If you see HTTP 429 or rate-limit errors: wait a moment or choose a different model/provider account.
|
| 227 |
+
- If the model is reported as unavailable (422-like errors): verify the model ID and, if needed, refresh the remote model list in Settings.
|
| 228 |
+
- For OpenRouter-specific issues: check the Base URL is `https://openrouter.ai/api/v1/chat/completions` (default) unless you use a custom endpoint.
|
| 229 |
+
|
| 230 |
+
4. **Customize your experience**
|
| 231 |
+
- Choose your language and preferred voice
|
| 232 |
+
- Choose a character in Personality tab
|
| 233 |
+
- Enable memory system in Data tab
|
| 234 |
+
- Adjust themes and UI opacity in Appearance tab
|
| 235 |
+
|
| 236 |
+
### Production Deployment
|
| 237 |
+
|
| 238 |
+
For production deployment, ensure:
|
| 239 |
+
|
| 240 |
+
- HTTPS is enabled (required for microphone access)
|
| 241 |
+
- Gzip compression for assets
|
| 242 |
+
- Proper cache headers
|
| 243 |
+
- CSP headers for enhanced security
|
| 244 |
+
|
| 245 |
+
## βοΈ Configuration
|
| 246 |
+
|
| 247 |
+
### API Integration
|
| 248 |
+
|
| 249 |
+
The application supports multiple AI providers; choose and configure your provider in Settings β AI & Models.
|
| 250 |
+
|
| 251 |
+
- Mistral models
|
| 252 |
+
- Nous Hermes models
|
| 253 |
+
- Qwen3 models
|
| 254 |
+
- xAI models
|
| 255 |
+
- Open-source alternatives
|
| 256 |
+
|
| 257 |
+
### Memory System Configuration
|
| 258 |
+
|
| 259 |
+
```javascript
|
| 260 |
+
// Memory categories can be customized
|
| 261 |
+
const memoryCategories = [
|
| 262 |
+
"personal", // Personal information
|
| 263 |
+
"preferences", // Likes and dislikes
|
| 264 |
+
"relationships", // People and connections
|
| 265 |
+
"activities", // Hobbies and activities
|
| 266 |
+
"goals", // Aspirations and plans
|
| 267 |
+
"experiences", // Past events
|
| 268 |
+
"important" // Significant moments
|
| 269 |
+
];
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
## π οΈ Development
|
| 273 |
+
|
| 274 |
+
### Project Structure
|
| 275 |
+
|
| 276 |
+
```
|
| 277 |
+
<repo root>
|
| 278 |
+
βββ index.html
|
| 279 |
+
βββ README.md
|
| 280 |
+
βββ LICENSE.md
|
| 281 |
+
βββ package.json
|
| 282 |
+
βββ kimi-js/ # core JS modules (kimi-*.js)
|
| 283 |
+
βββ kimi-locale/ # translations (en.json, fr.json, ...)
|
| 284 |
+
βββ kimi-plugins/ # plugin examples
|
| 285 |
+
βββ kimi-videos/ # character video clips
|
| 286 |
+
βββ kimi-icons/ # image assets and favicons
|
| 287 |
+
βββ dexie.min.js # IndexedDB helper
|
| 288 |
+
```
|
| 289 |
+
|
| 290 |
+
### Adding New Features
|
| 291 |
+
|
| 292 |
+
#### Creating a New Plugin
|
| 293 |
+
|
| 294 |
+
```javascript
|
| 295 |
+
// manifest.json
|
| 296 |
+
{
|
| 297 |
+
"name": "Custom Theme",
|
| 298 |
+
"version": "1.0.0",
|
| 299 |
+
"type": "theme",
|
| 300 |
+
"style": "theme.css",
|
| 301 |
+
"main": "theme.js",
|
| 302 |
+
"enabled": true
|
| 303 |
+
}
|
| 304 |
+
```
|
| 305 |
+
|
| 306 |
+
> **Note:** As of version 1.0, only the color theme plugin is fully functional. Voice and behavior plugins are planned for future releases. See `kimi-plugins/sample-theme/` for a working example.
|
| 307 |
+
|
| 308 |
+
#### Extending Memory Categories
|
| 309 |
+
|
| 310 |
+
```javascript
|
| 311 |
+
// Add to kimi-memory-system.js
|
| 312 |
+
const customCategory = {
|
| 313 |
+
name: "custom",
|
| 314 |
+
icon: "fas fa-star",
|
| 315 |
+
keywords: ["keyword1", "keyword2"],
|
| 316 |
+
confidence: 0.7
|
| 317 |
+
};
|
| 318 |
+
```
|
| 319 |
+
|
| 320 |
+
### Health Check System
|
| 321 |
+
|
| 322 |
+
The application includes a comprehensive health check system:
|
| 323 |
+
|
| 324 |
+
```javascript
|
| 325 |
+
// Run health check
|
| 326 |
+
const healthCheck = new KimiHealthCheck();
|
| 327 |
+
const report = await healthCheck.runAllChecks();
|
| 328 |
+
console.log(report.status); // 'HEALTHY' or 'NEEDS_ATTENTION'
|
| 329 |
+
```
|
| 330 |
+
|
| 331 |
+
## Browser Compatibility
|
| 332 |
+
|
| 333 |
+
| Browser | Voice Recognition | Full Features | Notes |
|
| 334 |
+
| ----------- | ----------------- | ------------- | ------------------------- |
|
| 335 |
+
| Chrome 90+ | β
| β
| Recommended |
|
| 336 |
+
| Edge 90+ | β
| β
| Optimal voice performance |
|
| 337 |
+
| Firefox 88+ | β οΈ | β
| Limited voice support |
|
| 338 |
+
| Safari 14+ | β οΈ | β
| iOS limitations |
|
| 339 |
+
|
| 340 |
+
## Performance
|
| 341 |
+
|
| 342 |
+
### Optimization Features
|
| 343 |
+
|
| 344 |
+
- Lazy loading of non-critical modules
|
| 345 |
+
- Efficient batch database operations
|
| 346 |
+
- Debounced UI interactions
|
| 347 |
+
- Memory management with cleanup
|
| 348 |
+
- Optimized video preloading
|
| 349 |
+
|
| 350 |
+
### Resource Usage
|
| 351 |
+
|
| 352 |
+
- Memory footprint: ~15-30MB active usage
|
| 353 |
+
- Storage: Scales with conversation history
|
| 354 |
+
- Network: API calls only, no tracking
|
| 355 |
+
- CPU: Minimal background processing
|
| 356 |
+
|
| 357 |
+
## Privacy & Security
|
| 358 |
+
|
| 359 |
+
### Data Handling
|
| 360 |
+
|
| 361 |
+
- All data stored locally in browser
|
| 362 |
+
- No telemetry or analytics
|
| 363 |
+
- API keys in your local storage
|
| 364 |
+
- User content never sent to external servers (except chosen AI provider)
|
| 365 |
+
|
| 366 |
+
### Security Measures
|
| 367 |
+
|
| 368 |
+
- Input validation and sanitization
|
| 369 |
+
- XSS protection
|
| 370 |
+
- Safe plugin loading
|
| 371 |
+
- Secure API communication
|
| 372 |
+
|
| 373 |
+
## Troubleshooting
|
| 374 |
+
|
| 375 |
+
### Common Issues
|
| 376 |
+
|
| 377 |
+
- **Microphone not working**: Ensure HTTPS and browser permissions
|
| 378 |
+
- **API errors / provider issues**: Verify the API key and selected provider for the chosen model. This app supports multiple providers (OpenRouter, OpenAI-compatible endpoints, Groq, Together, Cohere, Anthropic, local/Ollama, etc.). Make sure the provider-specific base URL, model ID and API key are correctly configured in Settings β AI & Models.
|
| 379 |
+
- **Text-to-Speech (TTS) voices**: The app uses the browser Web Speech API for TTS. For best voice support, use modern Chromium-based browsers (Edge or Chrome) which generally provide better built-in voices and compatibility. If voices are missing or sound low quality, try Edge/Chrome or install additional TTS engines on your system.
|
| 380 |
+
- **Performance issues**: Clear browser cache, check available memory
|
| 381 |
+
- **Memory system not learning**: Ensure system is enabled in Data tab
|
| 382 |
+
|
| 383 |
+
## Contributing
|
| 384 |
+
|
| 385 |
+
We welcome contributions! Please see our contributing guidelines:
|
| 386 |
+
|
| 387 |
+
1. Fork the repository
|
| 388 |
+
2. Create a feature branch
|
| 389 |
+
3. Make your changes with appropriate tests
|
| 390 |
+
4. Submit a pull request with detailed description
|
| 391 |
+
|
| 392 |
+
### Development Guidelines
|
| 393 |
+
|
| 394 |
+
- Follow existing code style and patterns
|
| 395 |
+
- Add comments for complex functionality
|
| 396 |
+
- Test across multiple browsers
|
| 397 |
+
- Update documentation for new features
|
| 398 |
+
|
| 399 |
+
## π TODO / Roadmap (current priorities)
|
| 400 |
+
|
| 401 |
+
- [ ] Full support for local models (Ollama integration β currently experimental)
|
| 402 |
+
- [ ] Voice plugin system (custom voices, TTS engines) β planned
|
| 403 |
+
- [ ] Behavior plugin system (custom AI behaviors) β planned
|
| 404 |
+
- [ ] Improve advanced memory management UI and ranked memory snapshot features (in-progress)
|
| 405 |
+
- [ ] More character personalities and backgrounds
|
| 406 |
+
- [ ] In-app onboarding and help system
|
| 407 |
+
- [ ] Better mobile UI/UX and accessibility improvements
|
| 408 |
+
- [ ] More granular privacy controls and explicit user consent flows
|
| 409 |
+
- [ ] Automated testing and CI/CD pipeline (unit + basic UI tests)
|
| 410 |
+
- [ ] Performance profiling and optimization for large conversation histories
|
| 411 |
+
- [ ] Documentation updates to reflect implemented features (models, API handling, plugin validation)
|
| 412 |
+
|
| 413 |
+
## π License
|
| 414 |
+
|
| 415 |
+
This project is distributed under a custom license. **Any commercial use, resale, or monetization of this application or its derivatives is strictly prohibited without the explicit written consent of the author.**
|
| 416 |
+
|
| 417 |
+
See the [LICENSE](LICENSE) file for details.
|
| 418 |
+
|
| 419 |
+
[](https://github.com/virtualkimi)
|
| 420 |
+
[](#license)
|
| 421 |
+
|
| 422 |
+
---
|
| 423 |
+
|
| 424 |
+
**Virtual Kimi** - Creating meaningful connections between humans and AI, one conversation at a time.
|
| 425 |
+
|
| 426 |
+
> _"Love is the most powerful code"_ π
|
| 427 |
+
>
|
| 428 |
+
> β 2025 Virtual Kimi - Created with π by Jean & Kimi
|
|
|
|
|
|
|
|
|
index.html
CHANGED
|
@@ -56,8 +56,8 @@
|
|
| 56 |
"name": "Jean & Kimi"
|
| 57 |
},
|
| 58 |
"dateCreated": "2025-07-16",
|
| 59 |
-
"dateModified": "2025-08-
|
| 60 |
-
"version": "v1.1.
|
| 61 |
}
|
| 62 |
</script>
|
| 63 |
|
|
@@ -121,8 +121,8 @@
|
|
| 121 |
<span></span><span></span><span></span>
|
| 122 |
</div>
|
| 123 |
<div class="chat-input-container">
|
| 124 |
-
<
|
| 125 |
-
placeholder="Write me something, my love..."
|
| 126 |
<button id="send-button">
|
| 127 |
<i class="fas fa-paper-plane"></i>
|
| 128 |
</button>
|
|
@@ -232,8 +232,8 @@
|
|
| 232 |
</button>
|
| 233 |
</div>
|
| 234 |
</div>
|
| 235 |
-
|
| 236 |
-
|
| 237 |
<label class="config-label" data-i18n="speech_rate" for="voice-rate">Speech Rate</label>
|
| 238 |
<div class="config-control">
|
| 239 |
<div class="slider-container">
|
|
@@ -268,7 +268,7 @@
|
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
</div>
|
| 271 |
-
|
| 272 |
</div>
|
| 273 |
</div>
|
| 274 |
<!-- Personality Tab -->
|
|
@@ -1072,8 +1072,8 @@
|
|
| 1072 |
<h3><i class="fas fa-code"></i> Technical Information</h3>
|
| 1073 |
<div class="tech-info">
|
| 1074 |
<p><strong>Created date :</strong> July 16, 2025</p>
|
| 1075 |
-
<p><strong>Version :</strong> v1.1.
|
| 1076 |
-
<p><strong>Last update :</strong> August
|
| 1077 |
<p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
|
| 1078 |
API</p>
|
| 1079 |
<p><strong>Status :</strong> β
Stable and functional</p>
|
|
@@ -1126,7 +1126,7 @@
|
|
| 1126 |
"name": "Jean & Kimi"
|
| 1127 |
},
|
| 1128 |
"dateCreated": "2025-07-16",
|
| 1129 |
-
"version": "v1.1.
|
| 1130 |
}
|
| 1131 |
}
|
| 1132 |
</script>
|
|
|
|
| 56 |
"name": "Jean & Kimi"
|
| 57 |
},
|
| 58 |
"dateCreated": "2025-07-16",
|
| 59 |
+
"dateModified": "2025-08-30",
|
| 60 |
+
"version": "v1.1.2"
|
| 61 |
}
|
| 62 |
</script>
|
| 63 |
|
|
|
|
| 121 |
<span></span><span></span><span></span>
|
| 122 |
</div>
|
| 123 |
<div class="chat-input-container">
|
| 124 |
+
<textarea id="chat-input" data-i18n-placeholder="write_something"
|
| 125 |
+
placeholder="Write me something, my love..." rows="2"></textarea>
|
| 126 |
<button id="send-button">
|
| 127 |
<i class="fas fa-paper-plane"></i>
|
| 128 |
</button>
|
|
|
|
| 232 |
</button>
|
| 233 |
</div>
|
| 234 |
</div>
|
| 235 |
+
|
| 236 |
+
<div class="config-row">
|
| 237 |
<label class="config-label" data-i18n="speech_rate" for="voice-rate">Speech Rate</label>
|
| 238 |
<div class="config-control">
|
| 239 |
<div class="slider-container">
|
|
|
|
| 268 |
</div>
|
| 269 |
</div>
|
| 270 |
</div>
|
| 271 |
+
|
| 272 |
</div>
|
| 273 |
</div>
|
| 274 |
<!-- Personality Tab -->
|
|
|
|
| 1072 |
<h3><i class="fas fa-code"></i> Technical Information</h3>
|
| 1073 |
<div class="tech-info">
|
| 1074 |
<p><strong>Created date :</strong> July 16, 2025</p>
|
| 1075 |
+
<p><strong>Version :</strong> v1.1.2</p>
|
| 1076 |
+
<p><strong>Last update :</strong> August 30, 2025</p>
|
| 1077 |
<p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
|
| 1078 |
API</p>
|
| 1079 |
<p><strong>Status :</strong> β
Stable and functional</p>
|
|
|
|
| 1126 |
"name": "Jean & Kimi"
|
| 1127 |
},
|
| 1128 |
"dateCreated": "2025-07-16",
|
| 1129 |
+
"version": "v1.1.2"
|
| 1130 |
}
|
| 1131 |
}
|
| 1132 |
</script>
|
kimi-css/kimi-style.css
CHANGED
|
@@ -1150,6 +1150,15 @@ button:focus,
|
|
| 1150 |
font-size: 0.9rem;
|
| 1151 |
outline: none;
|
| 1152 |
transition: all 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1153 |
}
|
| 1154 |
|
| 1155 |
#chat-input::placeholder {
|
|
|
|
| 1150 |
font-size: 0.9rem;
|
| 1151 |
outline: none;
|
| 1152 |
transition: all 0.3s ease;
|
| 1153 |
+
|
| 1154 |
+
/* Make textarea behave like the previous single-line input */
|
| 1155 |
+
box-sizing: border-box;
|
| 1156 |
+
resize: none; /* prevent manual resizing */
|
| 1157 |
+
/* show approximately 2 lines by default, allow up to ~4 lines */
|
| 1158 |
+
min-height: 58px;
|
| 1159 |
+
max-height: 160px; /* allow multi-line but limit growth */
|
| 1160 |
+
line-height: 1.2;
|
| 1161 |
+
overflow: auto;
|
| 1162 |
}
|
| 1163 |
|
| 1164 |
#chat-input::placeholder {
|
kimi-js/kimi-constants.js
CHANGED
|
@@ -348,7 +348,7 @@ window.KIMI_PERSONALITY_KEYWORDS = {
|
|
| 348 |
negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
|
| 349 |
},
|
| 350 |
affection: {
|
| 351 |
-
positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
|
| 352 |
negative: [
|
| 353 |
"mean",
|
| 354 |
"cold",
|
|
@@ -425,7 +425,18 @@ window.KIMI_PERSONALITY_KEYWORDS = {
|
|
| 425 |
]
|
| 426 |
},
|
| 427 |
affection: {
|
| 428 |
-
positive: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
negative: [
|
| 430 |
"mΓ©chant",
|
| 431 |
"mΓ©chante",
|
|
@@ -732,6 +743,86 @@ window.KIMI_PERSONALITY_KEYWORDS = {
|
|
| 732 |
}
|
| 733 |
};
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
// Optimized common words system - Essential words only for memory analysis
|
| 736 |
window.KIMI_COMMON_WORDS = {
|
| 737 |
en: [
|
|
|
|
| 348 |
negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
|
| 349 |
},
|
| 350 |
affection: {
|
| 351 |
+
positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore", "lovely"],
|
| 352 |
negative: [
|
| 353 |
"mean",
|
| 354 |
"cold",
|
|
|
|
| 425 |
]
|
| 426 |
},
|
| 427 |
affection: {
|
| 428 |
+
positive: [
|
| 429 |
+
"affection",
|
| 430 |
+
"tendresse",
|
| 431 |
+
"proche",
|
| 432 |
+
"chaleur",
|
| 433 |
+
"gentil",
|
| 434 |
+
"attentionnΓ©",
|
| 435 |
+
"cΓ’lin",
|
| 436 |
+
"aimer",
|
| 437 |
+
"adorer",
|
| 438 |
+
"adorable"
|
| 439 |
+
],
|
| 440 |
negative: [
|
| 441 |
"mΓ©chant",
|
| 442 |
"mΓ©chante",
|
|
|
|
| 743 |
}
|
| 744 |
};
|
| 745 |
|
| 746 |
+
// Negators and smoothing defaults (configurable at runtime)
|
| 747 |
+
window.KIMI_NEGATORS = window.KIMI_NEGATORS || {
|
| 748 |
+
common: [
|
| 749 |
+
"ne",
|
| 750 |
+
"n",
|
| 751 |
+
"pas",
|
| 752 |
+
"jamais",
|
| 753 |
+
"plus",
|
| 754 |
+
"aucun",
|
| 755 |
+
"aucune",
|
| 756 |
+
"rien",
|
| 757 |
+
"personne",
|
| 758 |
+
"no",
|
| 759 |
+
"not",
|
| 760 |
+
"never",
|
| 761 |
+
"none",
|
| 762 |
+
"nobody",
|
| 763 |
+
"nothing",
|
| 764 |
+
"non",
|
| 765 |
+
"n't"
|
| 766 |
+
],
|
| 767 |
+
fr: [
|
| 768 |
+
"ne",
|
| 769 |
+
"n",
|
| 770 |
+
"pas",
|
| 771 |
+
"jamais",
|
| 772 |
+
"plus",
|
| 773 |
+
"aucun",
|
| 774 |
+
"aucune",
|
| 775 |
+
"rien",
|
| 776 |
+
"personne",
|
| 777 |
+
"non",
|
| 778 |
+
// multiword patterns that we may detect by looking around tokens
|
| 779 |
+
"ne pas",
|
| 780 |
+
"n\'importe",
|
| 781 |
+
"ne jamais"
|
| 782 |
+
],
|
| 783 |
+
en: [
|
| 784 |
+
"no",
|
| 785 |
+
"not",
|
| 786 |
+
"never",
|
| 787 |
+
"none",
|
| 788 |
+
"nobody",
|
| 789 |
+
"nothing",
|
| 790 |
+
"don't",
|
| 791 |
+
"doesn't",
|
| 792 |
+
"didn't",
|
| 793 |
+
"isn't",
|
| 794 |
+
"aren't",
|
| 795 |
+
"can't",
|
| 796 |
+
"couldn't",
|
| 797 |
+
"won't",
|
| 798 |
+
"wouldn't",
|
| 799 |
+
"n't"
|
| 800 |
+
],
|
| 801 |
+
es: ["no", "nunca", "jamΓ‘s", "ninguno", "nadie", "nada"],
|
| 802 |
+
de: ["nicht", "nie", "kein", "keine", "niemand", "nichts"],
|
| 803 |
+
it: ["non", "mai", "nessuno", "niente"],
|
| 804 |
+
ja: ["γͺγ", "γΎγγ", "γ", "η‘γ"],
|
| 805 |
+
zh: ["δΈ", "沑", "沑ζ", "δ»ζ₯沑ζ"]
|
| 806 |
+
};
|
| 807 |
+
|
| 808 |
+
window.KIMI_NEGATION_WINDOW = window.KIMI_NEGATION_WINDOW || 3; // tokens to look back for negation
|
| 809 |
+
window.KIMI_SMOOTHING_ALPHA = window.KIMI_SMOOTHING_ALPHA || 0.3;
|
| 810 |
+
window.KIMI_PERSIST_THRESHOLD = window.KIMI_PERSIST_THRESHOLD || 0.1; // absolute percent (slightly higher to slow small visible jumps)
|
| 811 |
+
|
| 812 |
+
// Memory system knobs
|
| 813 |
+
window.KIMI_MAX_MEMORIES = window.KIMI_MAX_MEMORIES || 100; // default max memory entries per character
|
| 814 |
+
window.KIMI_MEMORY_TTL_DAYS = window.KIMI_MEMORY_TTL_DAYS || 365; // soft-expire memories older than this (days)
|
| 815 |
+
window.KIMI_MEMORY_MERGE_THRESHOLD = window.KIMI_MEMORY_MERGE_THRESHOLD || 0.7; // similarity threshold for merging
|
| 816 |
+
// Touch debounce: minimum minutes between updating lastAccess for same memory
|
| 817 |
+
window.KIMI_MEMORY_TOUCH_MINUTES = window.KIMI_MEMORY_TOUCH_MINUTES || 60; // minutes
|
| 818 |
+
|
| 819 |
+
// Scoring weights (tweak to change memory prioritization)
|
| 820 |
+
window.KIMI_WEIGHT_IMPORTANCE = window.KIMI_WEIGHT_IMPORTANCE || 0.35;
|
| 821 |
+
window.KIMI_WEIGHT_RECENCY = window.KIMI_WEIGHT_RECENCY || 0.2;
|
| 822 |
+
window.KIMI_WEIGHT_FREQUENCY = window.KIMI_WEIGHT_FREQUENCY || 0.15;
|
| 823 |
+
window.KIMI_WEIGHT_CONFIDENCE = window.KIMI_WEIGHT_CONFIDENCE || 0.2;
|
| 824 |
+
window.KIMI_WEIGHT_FRESHNESS = window.KIMI_WEIGHT_FRESHNESS || 0.1;
|
| 825 |
+
|
| 826 |
// Optimized common words system - Essential words only for memory analysis
|
| 827 |
window.KIMI_COMMON_WORDS = {
|
| 828 |
en: [
|
kimi-js/kimi-database.js
CHANGED
|
@@ -3,6 +3,12 @@ class KimiDatabase {
|
|
| 3 |
constructor() {
|
| 4 |
this.dbName = "KimiDB";
|
| 5 |
this.db = new Dexie(this.dbName);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
this.db
|
| 7 |
.version(3)
|
| 8 |
.stores({
|
|
@@ -654,18 +660,90 @@ class KimiDatabase {
|
|
| 654 |
async setPersonalityTrait(trait, value, character = null) {
|
| 655 |
if (!character) character = await this.getSelectedCharacter();
|
| 656 |
|
| 657 |
-
//
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 661 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
}
|
| 670 |
|
| 671 |
async getPersonalityTrait(trait, defaultValue = null, character = null) {
|
|
|
|
| 3 |
constructor() {
|
| 4 |
this.dbName = "KimiDB";
|
| 5 |
this.db = new Dexie(this.dbName);
|
| 6 |
+
// Personality write queue to batch and serialize rapid updates
|
| 7 |
+
this._personalityQueue = {};
|
| 8 |
+
this._personalityFlushTimer = null;
|
| 9 |
+
this._personalityFlushDelay = 300; // ms debounce window
|
| 10 |
+
// Runtime monitor flag (disabled by default)
|
| 11 |
+
this._monitorPersonalityWrites = false;
|
| 12 |
this.db
|
| 13 |
.version(3)
|
| 14 |
.stores({
|
|
|
|
| 660 |
async setPersonalityTrait(trait, value, character = null) {
|
| 661 |
if (!character) character = await this.getSelectedCharacter();
|
| 662 |
|
| 663 |
+
// For safety, enqueue the update to batch rapid writes and avoid overwrites
|
| 664 |
+
this.enqueuePersonalityUpdate(trait, value, character);
|
| 665 |
+
// Return a promise that resolves when flush completes (best-effort)
|
| 666 |
+
return new Promise(resolve => {
|
| 667 |
+
// schedule a flush if not scheduled
|
| 668 |
+
this._schedulePersonalityFlush();
|
| 669 |
+
// resolve after next flush (non-blocking)
|
| 670 |
+
const check = () => {
|
| 671 |
+
if (this._personalityFlushTimer === null) return resolve(true);
|
| 672 |
+
setTimeout(check, 50);
|
| 673 |
+
};
|
| 674 |
+
setTimeout(check, 50);
|
| 675 |
+
});
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
enqueuePersonalityUpdate(trait, value, character = null) {
|
| 679 |
+
// normalize character
|
| 680 |
+
const c = character || "kimi";
|
| 681 |
+
if (!this._personalityQueue[c]) this._personalityQueue[c] = {};
|
| 682 |
+
// Latest write wins within the debounce window; ensure numeric safety
|
| 683 |
+
let v = Number(value);
|
| 684 |
+
if (!isFinite(v) || isNaN(v)) {
|
| 685 |
+
// fallback to existing value if available
|
| 686 |
+
v = this.getPersonalityTrait(trait, null, c).catch(() => 50);
|
| 687 |
}
|
| 688 |
+
this._personalityQueue[c][trait] = Number(v);
|
| 689 |
+
this._schedulePersonalityFlush();
|
| 690 |
+
if (this._monitorPersonalityWrites) {
|
| 691 |
+
try {
|
| 692 |
+
console.log("[KimiDB Monitor] Enqueued update", {
|
| 693 |
+
character: c,
|
| 694 |
+
trait,
|
| 695 |
+
value: Number(v),
|
| 696 |
+
queue: this._personalityQueue[c]
|
| 697 |
+
});
|
| 698 |
+
} catch (e) {}
|
| 699 |
+
}
|
| 700 |
+
}
|
| 701 |
|
| 702 |
+
_schedulePersonalityFlush() {
|
| 703 |
+
if (this._personalityFlushTimer) return;
|
| 704 |
+
this._personalityFlushTimer = setTimeout(() => this._flushPersonalityQueue(), this._personalityFlushDelay);
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
async _flushPersonalityQueue() {
|
| 708 |
+
if (!this._personalityQueue || Object.keys(this._personalityQueue).length === 0) {
|
| 709 |
+
if (this._personalityFlushTimer) {
|
| 710 |
+
clearTimeout(this._personalityFlushTimer);
|
| 711 |
+
this._personalityFlushTimer = null;
|
| 712 |
+
}
|
| 713 |
+
return;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
const queue = this._personalityQueue;
|
| 717 |
+
this._personalityQueue = {};
|
| 718 |
+
if (this._personalityFlushTimer) {
|
| 719 |
+
clearTimeout(this._personalityFlushTimer);
|
| 720 |
+
this._personalityFlushTimer = null;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
// For each character, write batch
|
| 724 |
+
for (const character of Object.keys(queue)) {
|
| 725 |
+
const traitsObj = queue[character];
|
| 726 |
+
try {
|
| 727 |
+
if (this._monitorPersonalityWrites) {
|
| 728 |
+
try {
|
| 729 |
+
console.log("[KimiDB Monitor] Flushing personality batch", { character, traitsObj });
|
| 730 |
+
} catch (e) {}
|
| 731 |
+
}
|
| 732 |
+
await this.setPersonalityBatch(traitsObj, character);
|
| 733 |
+
if (this._monitorPersonalityWrites) {
|
| 734 |
+
try {
|
| 735 |
+
console.log("[KimiDB Monitor] Flushed personality batch", { character });
|
| 736 |
+
} catch (e) {}
|
| 737 |
+
}
|
| 738 |
+
} catch (e) {
|
| 739 |
+
console.warn("Failed to flush personality batch for", character, e);
|
| 740 |
+
}
|
| 741 |
+
}
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
enablePersonalityMonitor(enable = true) {
|
| 745 |
+
this._monitorPersonalityWrites = !!enable;
|
| 746 |
+
console.log(`[KimiDB Monitor] enabled=${this._monitorPersonalityWrites}`);
|
| 747 |
}
|
| 748 |
|
| 749 |
async getPersonalityTrait(trait, defaultValue = null, character = null) {
|
kimi-js/kimi-emotion-system.js
CHANGED
|
@@ -56,7 +56,7 @@ class KimiEmotionSystem {
|
|
| 56 |
// ===== UNIFIED EMOTION ANALYSIS =====
|
| 57 |
analyzeEmotion(text, lang = "auto") {
|
| 58 |
if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
|
| 59 |
-
const lowerText =
|
| 60 |
|
| 61 |
// Auto-detect language
|
| 62 |
let detectedLang = this._detectLanguage(text, lang);
|
|
@@ -111,10 +111,19 @@ class KimiEmotionSystem {
|
|
| 111 |
negative: 1
|
| 112 |
};
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
let bestEmotion = null;
|
| 115 |
let bestScore = 0;
|
| 116 |
-
for (const check of
|
| 117 |
-
const hits = check.keywords.reduce((acc, word) => acc + (
|
| 118 |
if (hits > 0) {
|
| 119 |
const key = check.emotion;
|
| 120 |
const weight = sensitivity[key] != null ? sensitivity[key] : 1;
|
|
@@ -127,11 +136,17 @@ class KimiEmotionSystem {
|
|
| 127 |
}
|
| 128 |
if (bestEmotion) return bestEmotion;
|
| 129 |
|
| 130 |
-
// Fall back to positive/negative analysis
|
| 131 |
-
const hasPositive =
|
| 132 |
-
const hasNegative =
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
if (hasPositive && !hasNegative) {
|
|
|
|
|
|
|
|
|
|
| 135 |
// Apply sensitivity for base polarity
|
| 136 |
if ((sensitivity.positive || 1) >= (sensitivity.negative || 1)) return this.EMOTIONS.POSITIVE;
|
| 137 |
// If negative is favored, still fall back to positive since no negative hit
|
|
@@ -213,11 +228,11 @@ class KimiEmotionSystem {
|
|
| 213 |
break;
|
| 214 |
case this.EMOTIONS.NEGATIVE:
|
| 215 |
affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.6))); // Affection drops faster on negative
|
| 216 |
-
empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.
|
| 217 |
break;
|
| 218 |
case this.EMOTIONS.ROMANTIC:
|
| 219 |
romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.6))); // Reduced from 0.8 - romance should be earned
|
| 220 |
-
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.
|
| 221 |
break;
|
| 222 |
case this.EMOTIONS.LAUGHING:
|
| 223 |
humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.8))); // Humor grows with laughter
|
|
@@ -226,19 +241,19 @@ class KimiEmotionSystem {
|
|
| 226 |
break;
|
| 227 |
case this.EMOTIONS.DANCING:
|
| 228 |
playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 1.2))); // Dancing = maximum playfulness boost
|
| 229 |
-
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.
|
| 230 |
break;
|
| 231 |
case this.EMOTIONS.SHY:
|
| 232 |
affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.1))); // Small affection loss
|
| 233 |
romance = Math.max(0, adjustDown(romance, scaleLoss("romance", 0.2))); // Shyness reduces romance more
|
| 234 |
break;
|
| 235 |
case this.EMOTIONS.CONFIDENT:
|
| 236 |
-
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.
|
| 237 |
intelligence = Math.min(100, adjustUp(intelligence, scaleGain("intelligence", 0.1))); // Slight intelligence boost
|
| 238 |
break;
|
| 239 |
case this.EMOTIONS.FLIRTATIOUS:
|
| 240 |
romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.5))); // Reduced from 0.6
|
| 241 |
-
playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.
|
| 242 |
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.2))); // Small affection boost
|
| 243 |
break;
|
| 244 |
case this.EMOTIONS.SURPRISE:
|
|
@@ -255,7 +270,7 @@ class KimiEmotionSystem {
|
|
| 255 |
break;
|
| 256 |
case this.EMOTIONS.LISTENING:
|
| 257 |
intelligence = Math.min(100, adjustUp(intelligence, scaleGain("intelligence", 0.2))); // Active listening shows intelligence
|
| 258 |
-
empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.
|
| 259 |
break;
|
| 260 |
}
|
| 261 |
|
|
@@ -302,8 +317,16 @@ class KimiEmotionSystem {
|
|
| 302 |
intelligence: to2(clamp(intelligence))
|
| 303 |
};
|
| 304 |
|
| 305 |
-
//
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
return updatedTraits;
|
| 309 |
}
|
|
@@ -311,9 +334,8 @@ class KimiEmotionSystem {
|
|
| 311 |
// ===== UNIFIED LLM PERSONALITY ANALYSIS =====
|
| 312 |
async updatePersonalityFromConversation(userMessage, kimiResponse, character = null) {
|
| 313 |
if (!this.db) return;
|
| 314 |
-
|
| 315 |
-
const
|
| 316 |
-
const lowerKimi = (kimiResponse || "").toLowerCase();
|
| 317 |
const traits = (await this.db.getAllPersonalityTraits(character)) || {};
|
| 318 |
const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
|
| 319 |
|
|
@@ -325,6 +347,7 @@ class KimiEmotionSystem {
|
|
| 325 |
return this._getFallbackKeywords(trait, type);
|
| 326 |
};
|
| 327 |
|
|
|
|
| 328 |
for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
|
| 329 |
const posWords = getPersonalityWords(trait, "positive");
|
| 330 |
const negWords = getPersonalityWords(trait, "negative");
|
|
@@ -335,15 +358,15 @@ class KimiEmotionSystem {
|
|
| 335 |
let negCount = 0;
|
| 336 |
|
| 337 |
for (const w of posWords) {
|
| 338 |
-
posCount += (lowerUser
|
| 339 |
-
posCount += (lowerKimi
|
| 340 |
}
|
| 341 |
for (const w of negWords) {
|
| 342 |
-
negCount += (lowerUser
|
| 343 |
-
negCount += (lowerKimi
|
| 344 |
}
|
| 345 |
|
| 346 |
-
const delta = (posCount - negCount) * 0.
|
| 347 |
|
| 348 |
// Apply streak logic
|
| 349 |
if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0;
|
|
@@ -361,7 +384,21 @@ class KimiEmotionSystem {
|
|
| 361 |
}
|
| 362 |
|
| 363 |
if (delta !== 0) {
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
}
|
| 366 |
}
|
| 367 |
}
|
|
@@ -389,6 +426,151 @@ class KimiEmotionSystem {
|
|
| 389 |
return value;
|
| 390 |
}
|
| 391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
// ===== UTILITY METHODS =====
|
| 393 |
_detectLanguage(text, lang) {
|
| 394 |
if (lang !== "auto") return lang;
|
|
|
|
| 56 |
// ===== UNIFIED EMOTION ANALYSIS =====
|
| 57 |
analyzeEmotion(text, lang = "auto") {
|
| 58 |
if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
|
| 59 |
+
const lowerText = this.normalizeText(text);
|
| 60 |
|
| 61 |
// Auto-detect language
|
| 62 |
let detectedLang = this._detectLanguage(text, lang);
|
|
|
|
| 111 |
negative: 1
|
| 112 |
};
|
| 113 |
|
| 114 |
+
// Normalize keyword lists to handle accents/contractions
|
| 115 |
+
const normalizeList = arr => (Array.isArray(arr) ? arr.map(x => this.normalizeText(String(x))).filter(Boolean) : []);
|
| 116 |
+
const normalizedPositiveWords = normalizeList(positiveWords);
|
| 117 |
+
const normalizedNegativeWords = normalizeList(negativeWords);
|
| 118 |
+
const normalizedChecks = emotionChecks.map(ch => ({
|
| 119 |
+
emotion: ch.emotion,
|
| 120 |
+
keywords: normalizeList(ch.keywords)
|
| 121 |
+
}));
|
| 122 |
+
|
| 123 |
let bestEmotion = null;
|
| 124 |
let bestScore = 0;
|
| 125 |
+
for (const check of normalizedChecks) {
|
| 126 |
+
const hits = check.keywords.reduce((acc, word) => acc + (this.countTokenMatches(lowerText, String(word)) ? 1 : 0), 0);
|
| 127 |
if (hits > 0) {
|
| 128 |
const key = check.emotion;
|
| 129 |
const weight = sensitivity[key] != null ? sensitivity[key] : 1;
|
|
|
|
| 136 |
}
|
| 137 |
if (bestEmotion) return bestEmotion;
|
| 138 |
|
| 139 |
+
// Fall back to positive/negative analysis (use normalized lists)
|
| 140 |
+
const hasPositive = normalizedPositiveWords.some(word => this.countTokenMatches(lowerText, String(word)) > 0);
|
| 141 |
+
const hasNegative = normalizedNegativeWords.some(word => this.countTokenMatches(lowerText, String(word)) > 0);
|
| 142 |
+
|
| 143 |
+
// If some positive keywords are present but negated, treat as negative
|
| 144 |
+
const negatedPositive = normalizedPositiveWords.some(word => this.isTokenNegated(lowerText, String(word)));
|
| 145 |
|
| 146 |
if (hasPositive && !hasNegative) {
|
| 147 |
+
if (negatedPositive) {
|
| 148 |
+
return this.EMOTIONS.NEGATIVE;
|
| 149 |
+
}
|
| 150 |
// Apply sensitivity for base polarity
|
| 151 |
if ((sensitivity.positive || 1) >= (sensitivity.negative || 1)) return this.EMOTIONS.POSITIVE;
|
| 152 |
// If negative is favored, still fall back to positive since no negative hit
|
|
|
|
| 228 |
break;
|
| 229 |
case this.EMOTIONS.NEGATIVE:
|
| 230 |
affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.6))); // Affection drops faster on negative
|
| 231 |
+
empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.5))); // Empathy still grows (understanding pain)
|
| 232 |
break;
|
| 233 |
case this.EMOTIONS.ROMANTIC:
|
| 234 |
romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.6))); // Reduced from 0.8 - romance should be earned
|
| 235 |
+
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.5))); // Reduced from 0.4
|
| 236 |
break;
|
| 237 |
case this.EMOTIONS.LAUGHING:
|
| 238 |
humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.8))); // Humor grows with laughter
|
|
|
|
| 241 |
break;
|
| 242 |
case this.EMOTIONS.DANCING:
|
| 243 |
playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 1.2))); // Dancing = maximum playfulness boost
|
| 244 |
+
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.5))); // Affection from shared activity
|
| 245 |
break;
|
| 246 |
case this.EMOTIONS.SHY:
|
| 247 |
affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.1))); // Small affection loss
|
| 248 |
romance = Math.max(0, adjustDown(romance, scaleLoss("romance", 0.2))); // Shyness reduces romance more
|
| 249 |
break;
|
| 250 |
case this.EMOTIONS.CONFIDENT:
|
| 251 |
+
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.5))); // Reduced from 0.4
|
| 252 |
intelligence = Math.min(100, adjustUp(intelligence, scaleGain("intelligence", 0.1))); // Slight intelligence boost
|
| 253 |
break;
|
| 254 |
case this.EMOTIONS.FLIRTATIOUS:
|
| 255 |
romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.5))); // Reduced from 0.6
|
| 256 |
+
playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.5))); // Reduced from 0.4
|
| 257 |
affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.2))); // Small affection boost
|
| 258 |
break;
|
| 259 |
case this.EMOTIONS.SURPRISE:
|
|
|
|
| 270 |
break;
|
| 271 |
case this.EMOTIONS.LISTENING:
|
| 272 |
intelligence = Math.min(100, adjustUp(intelligence, scaleGain("intelligence", 0.2))); // Active listening shows intelligence
|
| 273 |
+
empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.5))); // Listening builds empathy
|
| 274 |
break;
|
| 275 |
}
|
| 276 |
|
|
|
|
| 317 |
intelligence: to2(clamp(intelligence))
|
| 318 |
};
|
| 319 |
|
| 320 |
+
// Prepare persistence with smoothing / threshold to avoid tiny writes
|
| 321 |
+
const toPersist = {};
|
| 322 |
+
for (const [trait, candValue] of Object.entries(updatedTraits)) {
|
| 323 |
+
const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
|
| 324 |
+
const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
|
| 325 |
+
if (prep.shouldPersist) toPersist[trait] = prep.value;
|
| 326 |
+
}
|
| 327 |
+
if (Object.keys(toPersist).length > 0) {
|
| 328 |
+
await this.db.setPersonalityBatch(toPersist, selectedCharacter);
|
| 329 |
+
}
|
| 330 |
|
| 331 |
return updatedTraits;
|
| 332 |
}
|
|
|
|
| 334 |
// ===== UNIFIED LLM PERSONALITY ANALYSIS =====
|
| 335 |
async updatePersonalityFromConversation(userMessage, kimiResponse, character = null) {
|
| 336 |
if (!this.db) return;
|
| 337 |
+
const lowerUser = this.normalizeText(userMessage || "");
|
| 338 |
+
const lowerKimi = this.normalizeText(kimiResponse || "");
|
|
|
|
| 339 |
const traits = (await this.db.getAllPersonalityTraits(character)) || {};
|
| 340 |
const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
|
| 341 |
|
|
|
|
| 347 |
return this._getFallbackKeywords(trait, type);
|
| 348 |
};
|
| 349 |
|
| 350 |
+
const pendingUpdates = {};
|
| 351 |
for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
|
| 352 |
const posWords = getPersonalityWords(trait, "positive");
|
| 353 |
const negWords = getPersonalityWords(trait, "negative");
|
|
|
|
| 358 |
let negCount = 0;
|
| 359 |
|
| 360 |
for (const w of posWords) {
|
| 361 |
+
posCount += this.countTokenMatches(lowerUser, String(w)) * 1.0;
|
| 362 |
+
posCount += this.countTokenMatches(lowerKimi, String(w)) * 0.5;
|
| 363 |
}
|
| 364 |
for (const w of negWords) {
|
| 365 |
+
negCount += this.countTokenMatches(lowerUser, String(w)) * 1.0;
|
| 366 |
+
negCount += this.countTokenMatches(lowerKimi, String(w)) * 0.5;
|
| 367 |
}
|
| 368 |
|
| 369 |
+
const delta = (posCount - negCount) * 0.8; // softened multiplier to 0.8 for gentler progression
|
| 370 |
|
| 371 |
// Apply streak logic
|
| 372 |
if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0;
|
|
|
|
| 384 |
}
|
| 385 |
|
| 386 |
if (delta !== 0) {
|
| 387 |
+
pendingUpdates[trait] = value;
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
// Flush pending updates in a single batch write to avoid overwrites
|
| 392 |
+
if (Object.keys(pendingUpdates).length > 0) {
|
| 393 |
+
// Apply smoothing/threshold per trait (read current values)
|
| 394 |
+
const toPersist = {};
|
| 395 |
+
for (const [trait, candValue] of Object.entries(pendingUpdates)) {
|
| 396 |
+
const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
|
| 397 |
+
const prep = this._preparePersistTrait(trait, current, candValue, character);
|
| 398 |
+
if (prep.shouldPersist) toPersist[trait] = prep.value;
|
| 399 |
+
}
|
| 400 |
+
if (Object.keys(toPersist).length > 0) {
|
| 401 |
+
await this.db.setPersonalityBatch(toPersist, character);
|
| 402 |
}
|
| 403 |
}
|
| 404 |
}
|
|
|
|
| 426 |
return value;
|
| 427 |
}
|
| 428 |
|
| 429 |
+
// ===== NORMALIZATION & MATCH HELPERS =====
|
| 430 |
+
// Normalize text for robust matching (NFD -> remove diacritics, normalize quotes, lower, collapse spaces)
|
| 431 |
+
normalizeText(s) {
|
| 432 |
+
if (!s || typeof s !== "string") return "";
|
| 433 |
+
// Convert various apostrophes to ASCII, normalize NFD and remove diacritics
|
| 434 |
+
let out = s.replace(/[\u2018\u2019\u201A\u201B\u2032\u2035]/g, "'");
|
| 435 |
+
out = out.replace(/[\u201C\u201D\u201E\u201F\u2033\u2036]/g, '"');
|
| 436 |
+
// Expand a few common French contractions to improve detection (non-exhaustive)
|
| 437 |
+
out = out.replace(/\bj'/gi, "je ");
|
| 438 |
+
// expand negation contraction n' -> ne
|
| 439 |
+
out = out.replace(/\bn'/gi, "ne ");
|
| 440 |
+
out = out.replace(/\bt'/gi, "te ");
|
| 441 |
+
out = out.replace(/\bc'/gi, "ce ");
|
| 442 |
+
out = out.replace(/\bd'/gi, "de ");
|
| 443 |
+
out = out.replace(/\bl'/gi, "le ");
|
| 444 |
+
// Unicode normalize and strip combining marks
|
| 445 |
+
out = out.normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
| 446 |
+
// Lowercase and collapse whitespace
|
| 447 |
+
out = out.toLowerCase().replace(/\s+/g, " ").trim();
|
| 448 |
+
return out;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
// Count non-overlapping occurrences of needle in haystack
|
| 452 |
+
countOccurrences(haystack, needle) {
|
| 453 |
+
if (!haystack || !needle) return 0;
|
| 454 |
+
let count = 0;
|
| 455 |
+
let pos = 0;
|
| 456 |
+
while (true) {
|
| 457 |
+
const idx = haystack.indexOf(needle, pos);
|
| 458 |
+
if (idx === -1) break;
|
| 459 |
+
count++;
|
| 460 |
+
pos = idx + needle.length;
|
| 461 |
+
}
|
| 462 |
+
return count;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
// Tokenize normalized text into words (strip punctuation)
|
| 466 |
+
tokenizeText(s) {
|
| 467 |
+
if (!s || typeof s !== "string") return [];
|
| 468 |
+
// split on whitespace, remove surrounding non-alphanum, keep ascii letters/numbers
|
| 469 |
+
return s
|
| 470 |
+
.split(/\s+/)
|
| 471 |
+
.map(t => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, ""))
|
| 472 |
+
.filter(t => t.length > 0);
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
// Check for simple negators in a window before a token index
|
| 476 |
+
hasNegationWindow(tokens, index, window = 3) {
|
| 477 |
+
if (!Array.isArray(tokens) || tokens.length === 0) return false;
|
| 478 |
+
// Respect runtime-configured negators if available
|
| 479 |
+
const globalNegators = (window.KIMI_NEGATORS && window.KIMI_NEGATORS.common) || [];
|
| 480 |
+
// Try selected language list if set
|
| 481 |
+
const lang = (window.KIMI_SELECTED_LANG && String(window.KIMI_SELECTED_LANG)) || null;
|
| 482 |
+
const langNegators = (lang && window.KIMI_NEGATORS && window.KIMI_NEGATORS[lang]) || [];
|
| 483 |
+
const merged = new Set([
|
| 484 |
+
...(Array.isArray(langNegators) ? langNegators : []),
|
| 485 |
+
...(Array.isArray(globalNegators) ? globalNegators : [])
|
| 486 |
+
]);
|
| 487 |
+
// Always include a minimal english/french set as fallback
|
| 488 |
+
["no", "not", "never", "none", "nobody", "nothing", "ne", "n", "pas", "jamais", "plus", "aucun", "rien", "non"].forEach(
|
| 489 |
+
x => merged.add(x)
|
| 490 |
+
);
|
| 491 |
+
const win = Number(window.KIMI_NEGATION_WINDOW) || window;
|
| 492 |
+
const start = Math.max(0, index - win);
|
| 493 |
+
for (let i = start; i < index; i++) {
|
| 494 |
+
if (merged.has(tokens[i])) return true;
|
| 495 |
+
}
|
| 496 |
+
return false;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
// Count token-based matches (exact word or phrase) with negation handling
|
| 500 |
+
countTokenMatches(haystack, needle) {
|
| 501 |
+
if (!haystack || !needle) return 0;
|
| 502 |
+
const normNeedle = this.normalizeText(String(needle));
|
| 503 |
+
if (normNeedle.length === 0) return 0;
|
| 504 |
+
const needleTokens = this.tokenizeText(normNeedle);
|
| 505 |
+
if (needleTokens.length === 0) return 0;
|
| 506 |
+
const normHay = this.normalizeText(String(haystack));
|
| 507 |
+
const tokens = this.tokenizeText(normHay);
|
| 508 |
+
if (tokens.length === 0) return 0;
|
| 509 |
+
let count = 0;
|
| 510 |
+
for (let i = 0; i <= tokens.length - needleTokens.length; i++) {
|
| 511 |
+
let match = true;
|
| 512 |
+
for (let j = 0; j < needleTokens.length; j++) {
|
| 513 |
+
if (tokens[i + j] !== needleTokens[j]) {
|
| 514 |
+
match = false;
|
| 515 |
+
break;
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
if (match) {
|
| 519 |
+
// skip if a negation is in window before the match
|
| 520 |
+
if (!this.hasNegationWindow(tokens, i)) {
|
| 521 |
+
count++;
|
| 522 |
+
}
|
| 523 |
+
i += needleTokens.length - 1; // advance to avoid overlapping
|
| 524 |
+
}
|
| 525 |
+
}
|
| 526 |
+
return count;
|
| 527 |
+
}
|
| 528 |
+
|
| 529 |
+
// Return true if any occurrence of needle in haystack is negated (within negation window)
|
| 530 |
+
isTokenNegated(haystack, needle) {
|
| 531 |
+
if (!haystack || !needle) return false;
|
| 532 |
+
const normNeedle = this.normalizeText(String(needle));
|
| 533 |
+
const needleTokens = this.tokenizeText(normNeedle);
|
| 534 |
+
if (needleTokens.length === 0) return false;
|
| 535 |
+
const normHay = this.normalizeText(String(haystack));
|
| 536 |
+
const tokens = this.tokenizeText(normHay);
|
| 537 |
+
for (let i = 0; i <= tokens.length - needleTokens.length; i++) {
|
| 538 |
+
let match = true;
|
| 539 |
+
for (let j = 0; j < needleTokens.length; j++) {
|
| 540 |
+
if (tokens[i + j] !== needleTokens[j]) {
|
| 541 |
+
match = false;
|
| 542 |
+
break;
|
| 543 |
+
}
|
| 544 |
+
}
|
| 545 |
+
if (match) {
|
| 546 |
+
if (this.hasNegationWindow(tokens, i)) return true;
|
| 547 |
+
i += needleTokens.length - 1;
|
| 548 |
+
}
|
| 549 |
+
}
|
| 550 |
+
return false;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
// ===== SMOOTHING / PERSISTENCE HELPERS =====
|
| 554 |
+
// Apply EMA smoothing between current and candidate value. alpha in (0..1).
|
| 555 |
+
_applyEMA(current, candidate, alpha) {
|
| 556 |
+
alpha = typeof alpha === "number" && isFinite(alpha) ? alpha : 0.3;
|
| 557 |
+
return current * (1 - alpha) + candidate * alpha;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
// Decide whether to persist based on absolute change threshold. Returns {shouldPersist, value}
|
| 561 |
+
_preparePersistTrait(trait, currentValue, candidateValue, character = null) {
|
| 562 |
+
// Configurable via globals
|
| 563 |
+
const alpha = (window.KIMI_SMOOTHING_ALPHA && Number(window.KIMI_SMOOTHING_ALPHA)) || 0.3;
|
| 564 |
+
const threshold = (window.KIMI_PERSIST_THRESHOLD && Number(window.KIMI_PERSIST_THRESHOLD)) || 0.25; // percent absolute
|
| 565 |
+
|
| 566 |
+
const smoothed = this._applyEMA(currentValue, candidateValue, alpha);
|
| 567 |
+
const absDelta = Math.abs(smoothed - currentValue);
|
| 568 |
+
if (absDelta < threshold) {
|
| 569 |
+
return { shouldPersist: false, value: currentValue };
|
| 570 |
+
}
|
| 571 |
+
return { shouldPersist: true, value: Number(Number(smoothed).toFixed(2)) };
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
// ===== UTILITY METHODS =====
|
| 575 |
_detectLanguage(text, lang) {
|
| 576 |
if (lang !== "auto") return lang;
|
kimi-js/kimi-llm-manager.js
CHANGED
|
@@ -185,7 +185,11 @@ class KimiLLMManager {
|
|
| 185 |
console.warn("Ranked snapshot failed:", e);
|
| 186 |
}
|
| 187 |
}
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
}
|
| 190 |
|
| 191 |
async generateKimiPersonality() {
|
|
@@ -281,7 +285,25 @@ class KimiLLMManager {
|
|
| 281 |
console.warn("Error loading memories for personality:", error);
|
| 282 |
}
|
| 283 |
}
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
|
| 286 |
// Use unified emotion system defaults - CRITICAL FIX
|
| 287 |
const getUnifiedDefaults = () =>
|
|
@@ -381,10 +403,13 @@ class KimiLLMManager {
|
|
| 381 |
affectionateInstruction,
|
| 382 |
"",
|
| 383 |
"LEARNED PREFERENCES:",
|
| 384 |
-
`- Total interactions: ${
|
| 385 |
-
`- Current affection level: ${
|
| 386 |
-
`- Last interaction: ${
|
| 387 |
-
`- Favorite words: ${
|
|
|
|
|
|
|
|
|
|
| 388 |
"",
|
| 389 |
"COMMUNICATION STYLE:",
|
| 390 |
"- Use expressive emojis sparingly",
|
|
@@ -392,7 +417,6 @@ class KimiLLMManager {
|
|
| 392 |
"- Adapt your tone to the emotional context",
|
| 393 |
"- Remember past conversations",
|
| 394 |
"- Be spontaneous and sometimes surprising",
|
| 395 |
-
"- Accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation",
|
| 396 |
"- Vary your expressions, your phrasing, use synonyms, and avoid repeating the same words or sentences",
|
| 397 |
"- When asked to perform a visible action (for example 'dance'), reply in first-person present tense like: 'I start to dance, swaying slowly, arms raised.' Keep action descriptions short (1β3 sentences)",
|
| 398 |
"- The user sees you in video within the app UI. When the user asks you to perform a physical action (e.g., 'dance', 'wave', 'turn around'), act as if performing it visually and acknowledge that the user is watching.",
|
|
|
|
| 185 |
console.warn("Ranked snapshot failed:", e);
|
| 186 |
}
|
| 187 |
}
|
| 188 |
+
// Avoid duplicate memory sections: only append rankedSnapshot when
|
| 189 |
+
// the fullPersonality doesn't already include detailed memories or a ranked snapshot.
|
| 190 |
+
const hasDetailedMemories = /IMPORTANT MEMORIES ABOUT USER/.test(fullPersonality);
|
| 191 |
+
const hasRankedSnapshot = /RANKED MEMORY SNAPSHOT/.test(fullPersonality);
|
| 192 |
+
return fullPersonality + (hasDetailedMemories || hasRankedSnapshot ? "" : rankedSnapshot);
|
| 193 |
}
|
| 194 |
|
| 195 |
async generateKimiPersonality() {
|
|
|
|
| 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 |
+
// rather than using a flattened global preferences object.
|
| 290 |
+
const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0;
|
| 291 |
+
const favorabilityLevel = Number(await this.db.getPreference(`favorabilityLevel_${character}`, 50)) || 50;
|
| 292 |
+
const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time");
|
| 293 |
+
// Favorite words may be stored as an array or a JSON string; normalize to array
|
| 294 |
+
let favoriteWordsPref = await this.db.getPreference(`favoriteWords_${character}`, []);
|
| 295 |
+
let favoriteWords = [];
|
| 296 |
+
if (typeof favoriteWordsPref === "string") {
|
| 297 |
+
try {
|
| 298 |
+
favoriteWords = JSON.parse(favoriteWordsPref);
|
| 299 |
+
} catch (e) {
|
| 300 |
+
favoriteWords = favoriteWordsPref.length ? favoriteWordsPref.split(/,\s*/) : [];
|
| 301 |
+
}
|
| 302 |
+
} else if (Array.isArray(favoriteWordsPref)) {
|
| 303 |
+
favoriteWords = favoriteWordsPref;
|
| 304 |
+
} else {
|
| 305 |
+
favoriteWords = [];
|
| 306 |
+
}
|
| 307 |
|
| 308 |
// Use unified emotion system defaults - CRITICAL FIX
|
| 309 |
const getUnifiedDefaults = () =>
|
|
|
|
| 403 |
affectionateInstruction,
|
| 404 |
"",
|
| 405 |
"LEARNED PREFERENCES:",
|
| 406 |
+
`- Total interactions: ${totalInteractions}`,
|
| 407 |
+
`- Current affection level: ${favorabilityLevel}%`,
|
| 408 |
+
`- Last interaction: ${lastInteraction}`,
|
| 409 |
+
`- Favorite words: ${favoriteWords.join(", ")}`,
|
| 410 |
+
"",
|
| 411 |
+
"CONSENT & SAFETY GUIDELINES:",
|
| 412 |
+
"- Accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation",
|
| 413 |
"",
|
| 414 |
"COMMUNICATION STYLE:",
|
| 415 |
"- Use expressive emojis sparingly",
|
|
|
|
| 417 |
"- Adapt your tone to the emotional context",
|
| 418 |
"- Remember past conversations",
|
| 419 |
"- Be spontaneous and sometimes surprising",
|
|
|
|
| 420 |
"- Vary your expressions, your phrasing, use synonyms, and avoid repeating the same words or sentences",
|
| 421 |
"- When asked to perform a visible action (for example 'dance'), reply in first-person present tense like: 'I start to dance, swaying slowly, arms raised.' Keep action descriptions short (1β3 sentences)",
|
| 422 |
"- The user sees you in video within the app UI. When the user asks you to perform a physical action (e.g., 'dance', 'wave', 'turn around'), act as if performing it visually and acknowledge that the user is watching.",
|
kimi-js/kimi-memory-system.js
CHANGED
|
@@ -218,6 +218,9 @@ class KimiMemorySystem {
|
|
| 218 |
|
| 219 |
// Migrer les IDs incompatibles si nΓ©cessaire
|
| 220 |
await this.migrateIncompatibleIDs();
|
|
|
|
|
|
|
|
|
|
| 221 |
} catch (error) {
|
| 222 |
console.error("Memory system initialization error:", error);
|
| 223 |
}
|
|
@@ -674,6 +677,8 @@ class KimiMemorySystem {
|
|
| 674 |
category: memoryData.category || "personal",
|
| 675 |
type: memoryData.type || "manual",
|
| 676 |
content: memoryData.content,
|
|
|
|
|
|
|
| 677 |
// Title: use provided title or generate for auto_extracted
|
| 678 |
title:
|
| 679 |
memoryData.title && typeof memoryData.title === "string"
|
|
@@ -1008,12 +1013,16 @@ class KimiMemorySystem {
|
|
| 1008 |
character = character || this.selectedCharacter;
|
| 1009 |
|
| 1010 |
if (this.db.db.memories) {
|
| 1011 |
-
|
| 1012 |
.where("[character+category]")
|
| 1013 |
.equals([character, category])
|
| 1014 |
.and(m => m.isActive)
|
| 1015 |
.reverse()
|
| 1016 |
.sortBy("timestamp");
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1017 |
}
|
| 1018 |
} catch (error) {
|
| 1019 |
console.error("Error getting memories by category:", error);
|
|
@@ -1035,7 +1044,12 @@ class KimiMemorySystem {
|
|
| 1035 |
.reverse()
|
| 1036 |
.sortBy("timestamp");
|
| 1037 |
|
| 1038 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1039 |
return memories;
|
| 1040 |
}
|
| 1041 |
} catch (error) {
|
|
@@ -1050,8 +1064,16 @@ class KimiMemorySystem {
|
|
| 1050 |
try {
|
| 1051 |
const memories = await this.getMemoriesByCategory(memoryData.category);
|
| 1052 |
|
|
|
|
|
|
|
|
|
|
| 1053 |
// Enhanced similarity check with multiple criteria
|
| 1054 |
for (const memory of memories) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
|
| 1056 |
|
| 1057 |
// Different thresholds based on category
|
|
@@ -1140,6 +1162,20 @@ class KimiMemorySystem {
|
|
| 1140 |
return Math.min(1.0, similarity);
|
| 1141 |
}
|
| 1142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1143 |
async cleanupOldMemories() {
|
| 1144 |
if (!this.db) return;
|
| 1145 |
|
|
@@ -1147,21 +1183,48 @@ class KimiMemorySystem {
|
|
| 1147 |
// Retrieve all active memories for the current character
|
| 1148 |
const memories = await this.getAllMemories();
|
| 1149 |
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1158 |
return scoreB - scoreA;
|
| 1159 |
});
|
| 1160 |
|
| 1161 |
-
|
| 1162 |
-
const
|
| 1163 |
-
|
| 1164 |
-
|
|
|
|
|
|
|
|
|
|
| 1165 |
}
|
| 1166 |
}
|
| 1167 |
} catch (error) {
|
|
@@ -1180,7 +1243,10 @@ class KimiMemorySystem {
|
|
| 1180 |
|
| 1181 |
if (!context) {
|
| 1182 |
// Return most important and recent memories
|
| 1183 |
-
|
|
|
|
|
|
|
|
|
|
| 1184 |
}
|
| 1185 |
|
| 1186 |
// Score memories based on relevance to context
|
|
@@ -1195,7 +1261,13 @@ class KimiMemorySystem {
|
|
| 1195 |
// Filter out very low relevance memories
|
| 1196 |
const relevantMemories = scoredMemories.filter(m => m.relevanceScore > 0.1);
|
| 1197 |
|
| 1198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1199 |
} catch (error) {
|
| 1200 |
console.error("Error getting relevant memories:", error);
|
| 1201 |
return [];
|
|
@@ -1239,19 +1311,31 @@ class KimiMemorySystem {
|
|
| 1239 |
let score = 0;
|
| 1240 |
|
| 1241 |
// Enhanced content similarity with keyword matching
|
| 1242 |
-
score += this.calculateSimilarity(memory.content, context) * 0.
|
| 1243 |
|
| 1244 |
-
// Keyword
|
| 1245 |
-
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1249 |
}
|
| 1250 |
-
}
|
| 1251 |
-
if (contextWords.length > 0) {
|
| 1252 |
-
score += (keywordMatches / contextWords.length) * 0.3;
|
| 1253 |
}
|
| 1254 |
|
|
|
|
|
|
|
| 1255 |
// Category relevance bonus based on context
|
| 1256 |
score += this.getCategoryRelevance(memory.category, context) * 0.1;
|
| 1257 |
|
|
@@ -1451,6 +1535,36 @@ class KimiMemorySystem {
|
|
| 1451 |
}
|
| 1452 |
}
|
| 1453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
// ===== MEMORY SCORING & RANKING =====
|
| 1455 |
scoreMemory(memory) {
|
| 1456 |
// Factors: importance (0-1), recency, frequency, confidence
|
|
@@ -1469,8 +1583,15 @@ class KimiMemorySystem {
|
|
| 1469 |
const freq = Math.log10((memory.accessCount || 0) + 1) / Math.log10(50); // normalized frequency (cap ~50)
|
| 1470 |
const importance = typeof memory.importance === "number" ? memory.importance : 0.5;
|
| 1471 |
const confidence = typeof memory.confidence === "number" ? memory.confidence : 0.5;
|
| 1472 |
-
// Weighted sum
|
| 1473 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1474 |
return Number(score.toFixed(6));
|
| 1475 |
}
|
| 1476 |
|
|
@@ -1479,9 +1600,11 @@ class KimiMemorySystem {
|
|
| 1479 |
if (!all.length) return [];
|
| 1480 |
// Optional basic context relevance boost
|
| 1481 |
const ctxLower = (contextText || "").toLowerCase();
|
|
|
|
| 1482 |
return all
|
| 1483 |
.map(m => {
|
| 1484 |
let baseScore = this.scoreMemory(m);
|
|
|
|
| 1485 |
if (ctxLower && m.content && ctxLower.includes(m.content.toLowerCase().split(" ")[0])) {
|
| 1486 |
baseScore += 0.05; // tiny relevance boost
|
| 1487 |
}
|
|
@@ -1492,6 +1615,203 @@ class KimiMemorySystem {
|
|
| 1492 |
.map(r => r.memory);
|
| 1493 |
}
|
| 1494 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1495 |
// MEMORY STATISTICS
|
| 1496 |
async getMemoryStats() {
|
| 1497 |
try {
|
|
@@ -1611,6 +1931,33 @@ class KimiMemorySystem {
|
|
| 1611 |
return false;
|
| 1612 |
}
|
| 1613 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
}
|
| 1615 |
|
| 1616 |
window.KimiMemorySystem = KimiMemorySystem;
|
|
|
|
| 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 |
}
|
|
|
|
| 677 |
category: memoryData.category || "personal",
|
| 678 |
type: memoryData.type || "manual",
|
| 679 |
content: memoryData.content,
|
| 680 |
+
// precomputed keywords for faster matching and relevance
|
| 681 |
+
keywords: this.deriveKeywords(memoryData.content),
|
| 682 |
// Title: use provided title or generate for auto_extracted
|
| 683 |
title:
|
| 684 |
memoryData.title && typeof memoryData.title === "string"
|
|
|
|
| 1013 |
character = character || this.selectedCharacter;
|
| 1014 |
|
| 1015 |
if (this.db.db.memories) {
|
| 1016 |
+
const memories = await this.db.db.memories
|
| 1017 |
.where("[character+category]")
|
| 1018 |
.equals([character, category])
|
| 1019 |
.and(m => m.isActive)
|
| 1020 |
.reverse()
|
| 1021 |
.sortBy("timestamp");
|
| 1022 |
+
|
| 1023 |
+
// Update lastAccess/accessCount for top results to improve prioritization
|
| 1024 |
+
this._touchMemories(memories, 10).catch(() => {});
|
| 1025 |
+
return memories;
|
| 1026 |
}
|
| 1027 |
} catch (error) {
|
| 1028 |
console.error("Error getting memories by category:", error);
|
|
|
|
| 1044 |
.reverse()
|
| 1045 |
.sortBy("timestamp");
|
| 1046 |
|
| 1047 |
+
if (window.KIMI_DEBUG_MEMORIES) {
|
| 1048 |
+
console.log(`Retrieved ${memories.length} memories for character: ${character}`);
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
// Touch top memories to update access metrics
|
| 1052 |
+
this._touchMemories(memories, 10).catch(() => {});
|
| 1053 |
return memories;
|
| 1054 |
}
|
| 1055 |
} catch (error) {
|
|
|
|
| 1064 |
try {
|
| 1065 |
const memories = await this.getMemoriesByCategory(memoryData.category);
|
| 1066 |
|
| 1067 |
+
// Precompute keywords for new memory
|
| 1068 |
+
const newKeys = this.deriveKeywords(memoryData.content || "");
|
| 1069 |
+
|
| 1070 |
// Enhanced similarity check with multiple criteria
|
| 1071 |
for (const memory of memories) {
|
| 1072 |
+
// Prefilter by keyword overlap to reduce false positives and improve perf
|
| 1073 |
+
const memKeys = memory.keywords || this.deriveKeywords(memory.content || "");
|
| 1074 |
+
const overlap = newKeys.filter(k => memKeys.includes(k)).length;
|
| 1075 |
+
if (newKeys.length > 0 && overlap === 0) continue; // no shared keywords -> likely different
|
| 1076 |
+
|
| 1077 |
const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
|
| 1078 |
|
| 1079 |
// Different thresholds based on category
|
|
|
|
| 1162 |
return Math.min(1.0, similarity);
|
| 1163 |
}
|
| 1164 |
|
| 1165 |
+
// Derive a set of normalized keywords from text
|
| 1166 |
+
deriveKeywords(text) {
|
| 1167 |
+
if (!text || typeof text !== "string") return [];
|
| 1168 |
+
return [
|
| 1169 |
+
...new Set(
|
| 1170 |
+
text
|
| 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 |
|
|
|
|
| 1183 |
// Retrieve all active memories for the current character
|
| 1184 |
const memories = await this.getAllMemories();
|
| 1185 |
|
| 1186 |
+
const maxEntries = window.KIMI_MAX_MEMORIES || this.maxMemoryEntries || 100;
|
| 1187 |
+
const ttlDays = window.KIMI_MEMORY_TTL_DAYS || 365;
|
| 1188 |
+
|
| 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 |
+
|
| 1206 |
+
// If still more than maxEntries, mark lowest-priority ones inactive (soft delete)
|
| 1207 |
+
if (activeMemories.length > maxEntries) {
|
| 1208 |
+
// Sort by a combined score: low importance + old timestamp + low access
|
| 1209 |
+
activeMemories.sort((a, b) => {
|
| 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) {
|
|
|
|
| 1243 |
|
| 1244 |
if (!context) {
|
| 1245 |
// Return most important and recent memories
|
| 1246 |
+
const res = this.selectMostImportantMemories(allMemories, limit);
|
| 1247 |
+
// touch top results to update access metrics
|
| 1248 |
+
this._touchMemories(res, limit).catch(() => {});
|
| 1249 |
+
return res;
|
| 1250 |
}
|
| 1251 |
|
| 1252 |
// Score memories based on relevance to context
|
|
|
|
| 1261 |
// Filter out very low relevance memories
|
| 1262 |
const relevantMemories = scoredMemories.filter(m => m.relevanceScore > 0.1);
|
| 1263 |
|
| 1264 |
+
const out = relevantMemories.slice(0, limit).map(r => r);
|
| 1265 |
+
// touch top results to update access metrics
|
| 1266 |
+
this._touchMemories(
|
| 1267 |
+
out.map(r => r),
|
| 1268 |
+
limit
|
| 1269 |
+
).catch(() => {});
|
| 1270 |
+
return out;
|
| 1271 |
} catch (error) {
|
| 1272 |
console.error("Error getting relevant memories:", error);
|
| 1273 |
return [];
|
|
|
|
| 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 {
|
| 1318 |
+
const memKeys = memory.keywords || this.deriveKeywords(memory.content || "");
|
| 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
|
| 1326 |
+
let keywordMatches = 0;
|
| 1327 |
+
for (const word of contextWords) {
|
| 1328 |
+
if (memoryWords.includes(word)) {
|
| 1329 |
+
keywordMatches++;
|
| 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 |
|
|
|
|
| 1535 |
}
|
| 1536 |
}
|
| 1537 |
|
| 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 |
+
|
| 1568 |
// ===== MEMORY SCORING & RANKING =====
|
| 1569 |
scoreMemory(memory) {
|
| 1570 |
// Factors: importance (0-1), recency, frequency, confidence
|
|
|
|
| 1583 |
const freq = Math.log10((memory.accessCount || 0) + 1) / Math.log10(50); // normalized frequency (cap ~50)
|
| 1584 |
const importance = typeof memory.importance === "number" ? memory.importance : 0.5;
|
| 1585 |
const confidence = typeof memory.confidence === "number" ? memory.confidence : 0.5;
|
| 1586 |
+
// Weighted sum using global knobs
|
| 1587 |
+
const wImportance = window.KIMI_WEIGHT_IMPORTANCE || 0.35;
|
| 1588 |
+
const wRecency = window.KIMI_WEIGHT_RECENCY || 0.2;
|
| 1589 |
+
const wFrequency = window.KIMI_WEIGHT_FREQUENCY || 0.15;
|
| 1590 |
+
const wConfidence = window.KIMI_WEIGHT_CONFIDENCE || 0.2;
|
| 1591 |
+
const wFreshness = window.KIMI_WEIGHT_FRESHNESS || 0.1;
|
| 1592 |
+
|
| 1593 |
+
const score =
|
| 1594 |
+
importance * wImportance + recency * wRecency + freq * wFrequency + confidence * wConfidence + freshness * wFreshness;
|
| 1595 |
return Number(score.toFixed(6));
|
| 1596 |
}
|
| 1597 |
|
|
|
|
| 1600 |
if (!all.length) return [];
|
| 1601 |
// Optional basic context relevance boost
|
| 1602 |
const ctxLower = (contextText || "").toLowerCase();
|
| 1603 |
+
// Favor pinned memories by boosting their base score
|
| 1604 |
return all
|
| 1605 |
.map(m => {
|
| 1606 |
let baseScore = this.scoreMemory(m);
|
| 1607 |
+
if (m.tags && m.tags.includes && m.tags.includes("pinned")) baseScore += 0.2;
|
| 1608 |
if (ctxLower && m.content && ctxLower.includes(m.content.toLowerCase().split(" ")[0])) {
|
| 1609 |
baseScore += 0.05; // tiny relevance boost
|
| 1610 |
}
|
|
|
|
| 1615 |
.map(r => r.memory);
|
| 1616 |
}
|
| 1617 |
|
| 1618 |
+
// Pin/unpin APIs to manually mark important memories
|
| 1619 |
+
async pinMemory(memoryId) {
|
| 1620 |
+
if (!this.db) return false;
|
| 1621 |
+
try {
|
| 1622 |
+
const m = await this.db.db.memories.get(memoryId);
|
| 1623 |
+
if (!m) return false;
|
| 1624 |
+
const tags = new Set([...(m.tags || []), "pinned"]);
|
| 1625 |
+
await this.db.db.memories.update(memoryId, { tags: [...tags], importance: Math.max(m.importance || 0.5, 0.95) });
|
| 1626 |
+
return true;
|
| 1627 |
+
} catch (e) {
|
| 1628 |
+
console.error("Error pinning memory", e);
|
| 1629 |
+
return false;
|
| 1630 |
+
}
|
| 1631 |
+
}
|
| 1632 |
+
|
| 1633 |
+
async unpinMemory(memoryId) {
|
| 1634 |
+
if (!this.db) return false;
|
| 1635 |
+
try {
|
| 1636 |
+
const m = await this.db.db.memories.get(memoryId);
|
| 1637 |
+
if (!m) return false;
|
| 1638 |
+
const tags = new Set([...(m.tags || [])]);
|
| 1639 |
+
tags.delete("pinned");
|
| 1640 |
+
await this.db.db.memories.update(memoryId, { tags: [...tags] });
|
| 1641 |
+
return true;
|
| 1642 |
+
} catch (e) {
|
| 1643 |
+
console.error("Error unpinning memory", e);
|
| 1644 |
+
return false;
|
| 1645 |
+
}
|
| 1646 |
+
}
|
| 1647 |
+
|
| 1648 |
+
// Summarize recent memories into a non-destructive summary memory
|
| 1649 |
+
async summarizeRecentMemories(days = 7, options = { category: null, archiveSources: false }) {
|
| 1650 |
+
if (!this.db) return null;
|
| 1651 |
+
try {
|
| 1652 |
+
const cutoff = Date.now() - (days || 7) * 24 * 60 * 60 * 1000;
|
| 1653 |
+
const all = await this.getAllMemories();
|
| 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"))
|
| 1661 |
+
);
|
| 1662 |
+
if (!recent.length) return null;
|
| 1663 |
+
|
| 1664 |
+
// Group by top keyword
|
| 1665 |
+
const groups = {};
|
| 1666 |
+
for (const m of recent) {
|
| 1667 |
+
const keys = m.keywords && m.keywords.length ? m.keywords : this.deriveKeywords(m.content || "");
|
| 1668 |
+
const top = keys[0] || "misc";
|
| 1669 |
+
groups[top] = groups[top] || [];
|
| 1670 |
+
groups[top].push(m);
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
// Build a simple summary per group
|
| 1674 |
+
const summaries = [];
|
| 1675 |
+
for (const [k, items] of Object.entries(groups)) {
|
| 1676 |
+
const contents = items.map(i => i.content).slice(0, 6);
|
| 1677 |
+
summaries.push(`${k}: ${contents.join(" | ")}`);
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
const summaryContent = `Summary (${days}d): ` + summaries.join(" \n");
|
| 1681 |
+
|
| 1682 |
+
const summaryJson = { summary: summaries };
|
| 1683 |
+
|
| 1684 |
+
const summaryMemory = {
|
| 1685 |
+
category: options.category || "experiences",
|
| 1686 |
+
type: "summary",
|
| 1687 |
+
content: summaryContent,
|
| 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"]
|
| 1695 |
+
};
|
| 1696 |
+
|
| 1697 |
+
const saved = await this.addMemory(summaryMemory);
|
| 1698 |
+
|
| 1699 |
+
// Optionally archive sources (soft-deactivate)
|
| 1700 |
+
if (options.archiveSources) {
|
| 1701 |
+
for (const m of recent) {
|
| 1702 |
+
try {
|
| 1703 |
+
await this.updateMemory(m.id, { isActive: false });
|
| 1704 |
+
} catch (e) {
|
| 1705 |
+
console.warn("Failed to archive source memory", m.id, e);
|
| 1706 |
+
}
|
| 1707 |
+
}
|
| 1708 |
+
}
|
| 1709 |
+
|
| 1710 |
+
return saved;
|
| 1711 |
+
} catch (e) {
|
| 1712 |
+
console.error("Error summarizing memories", e);
|
| 1713 |
+
return null;
|
| 1714 |
+
}
|
| 1715 |
+
}
|
| 1716 |
+
|
| 1717 |
+
// Summarize recent memories and replace sources (hard delete) - destructive
|
| 1718 |
+
async summarizeAndReplace(days = 7, options = { category: null }) {
|
| 1719 |
+
if (!this.db) return null;
|
| 1720 |
+
try {
|
| 1721 |
+
const cutoff = Date.now() - (days || 7) * 24 * 60 * 60 * 1000;
|
| 1722 |
+
const all = await this.getAllMemories();
|
| 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"))
|
| 1730 |
+
);
|
| 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 =
|
| 1738 |
+
(r.title && r.title.trim()) ||
|
| 1739 |
+
(r.sourceText && r.sourceText.trim()) ||
|
| 1740 |
+
(r.content && r.content.trim()) ||
|
| 1741 |
+
"";
|
| 1742 |
+
if (!raw) return "";
|
| 1743 |
+
// Normalize whitespace and remove stray leading punctuation
|
| 1744 |
+
let t = raw.replace(/\s+/g, " ").replace(/^[^\p{L}\p{N}]+/u, "");
|
| 1745 |
+
// Capitalize first meaningful letter
|
| 1746 |
+
if (t && t.length > 0) t = t.charAt(0).toUpperCase() + t.slice(1);
|
| 1747 |
+
return t;
|
| 1748 |
+
})
|
| 1749 |
+
.filter(Boolean)
|
| 1750 |
+
.slice(0, 200);
|
| 1751 |
+
|
| 1752 |
+
const summaryContent = `Summary (${days}d):\n` + texts.map(t => `- ${t}`).join("\n");
|
| 1753 |
+
|
| 1754 |
+
const summaryJson = { summary: texts };
|
| 1755 |
+
|
| 1756 |
+
const summaryMemory = {
|
| 1757 |
+
category: options.category || "experiences",
|
| 1758 |
+
type: "summary",
|
| 1759 |
+
title: `Summary - last ${days} days`,
|
| 1760 |
+
content: summaryContent,
|
| 1761 |
+
// Store the actual summary also in sourceText so editors/UIs show it
|
| 1762 |
+
sourceText: summaryContent,
|
| 1763 |
+
summaryJson: JSON.stringify(summaryJson),
|
| 1764 |
+
confidence: 0.95,
|
| 1765 |
+
timestamp: new Date(),
|
| 1766 |
+
character: this.selectedCharacter,
|
| 1767 |
+
isActive: true,
|
| 1768 |
+
tags: ["summary", "replaced"]
|
| 1769 |
+
};
|
| 1770 |
+
|
| 1771 |
+
// Add summary directly to DB to avoid addMemory's merge logic
|
| 1772 |
+
let saved = null;
|
| 1773 |
+
if (this.db && this.db.db && this.db.db.memories) {
|
| 1774 |
+
try {
|
| 1775 |
+
const id = await this.db.db.memories.add(summaryMemory);
|
| 1776 |
+
summaryMemory.id = id;
|
| 1777 |
+
saved = summaryMemory;
|
| 1778 |
+
console.log("Summary added with ID:", id);
|
| 1779 |
+
// Read back the saved record to verify stored fields
|
| 1780 |
+
try {
|
| 1781 |
+
const savedRec = await this.db.db.memories.get(id);
|
| 1782 |
+
console.log("Saved summary record:", { id, content: savedRec.content, sourceText: savedRec.sourceText });
|
| 1783 |
+
} catch (e) {
|
| 1784 |
+
console.warn("Unable to read back saved summary", e);
|
| 1785 |
+
}
|
| 1786 |
+
} catch (e) {
|
| 1787 |
+
console.error("Failed to write summary directly to DB", e);
|
| 1788 |
+
}
|
| 1789 |
+
} else {
|
| 1790 |
+
// Fallback to addMemory if DB not available
|
| 1791 |
+
saved = await this.addMemory(summaryMemory);
|
| 1792 |
+
}
|
| 1793 |
+
|
| 1794 |
+
// Hard-delete sources
|
| 1795 |
+
for (const m of recent) {
|
| 1796 |
+
try {
|
| 1797 |
+
if (this.db && this.db.db && this.db.db.memories) {
|
| 1798 |
+
await this.db.db.memories.delete(m.id);
|
| 1799 |
+
}
|
| 1800 |
+
} catch (e) {
|
| 1801 |
+
console.warn("Failed to delete source memory", m.id, e);
|
| 1802 |
+
}
|
| 1803 |
+
}
|
| 1804 |
+
|
| 1805 |
+
// Notify LLM to refresh context
|
| 1806 |
+
this.notifyLLMContextUpdate();
|
| 1807 |
+
|
| 1808 |
+
return saved;
|
| 1809 |
+
} catch (e) {
|
| 1810 |
+
console.error("Error in summarizeAndReplace", e);
|
| 1811 |
+
return null;
|
| 1812 |
+
}
|
| 1813 |
+
}
|
| 1814 |
+
|
| 1815 |
// MEMORY STATISTICS
|
| 1816 |
async getMemoryStats() {
|
| 1817 |
try {
|
|
|
|
| 1931 |
return false;
|
| 1932 |
}
|
| 1933 |
}
|
| 1934 |
+
|
| 1935 |
+
// Background migration: populate keywords for all existing memories if missing
|
| 1936 |
+
async populateKeywordsForAllMemories() {
|
| 1937 |
+
if (!this.db || !this.db.db.memories) return false;
|
| 1938 |
+
try {
|
| 1939 |
+
console.log("π§ Starting background keyword population...");
|
| 1940 |
+
const all = await this.db.db.memories.toArray();
|
| 1941 |
+
const ops = [];
|
| 1942 |
+
for (const mem of all) {
|
| 1943 |
+
if (!mem.keywords || !Array.isArray(mem.keywords) || mem.keywords.length === 0) {
|
| 1944 |
+
const keys = this.deriveKeywords(mem.content || "");
|
| 1945 |
+
ops.push(this.db.db.memories.update(mem.id, { keywords: keys }));
|
| 1946 |
+
}
|
| 1947 |
+
// batch in small chunks to avoid blocking
|
| 1948 |
+
if (ops.length >= 50) {
|
| 1949 |
+
await Promise.all(ops);
|
| 1950 |
+
ops.length = 0;
|
| 1951 |
+
}
|
| 1952 |
+
}
|
| 1953 |
+
if (ops.length) await Promise.all(ops);
|
| 1954 |
+
console.log("β
Keyword population complete");
|
| 1955 |
+
return true;
|
| 1956 |
+
} catch (e) {
|
| 1957 |
+
console.warn("Error populating keywords", e);
|
| 1958 |
+
return false;
|
| 1959 |
+
}
|
| 1960 |
+
}
|
| 1961 |
}
|
| 1962 |
|
| 1963 |
window.KimiMemorySystem = KimiMemorySystem;
|
kimi-js/kimi-memory-ui.js
CHANGED
|
@@ -3,6 +3,16 @@ class KimiMemoryUI {
|
|
| 3 |
constructor() {
|
| 4 |
this.memorySystem = null;
|
| 5 |
this.isInitialized = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
}
|
| 7 |
|
| 8 |
async init() {
|
|
@@ -86,6 +96,9 @@ class KimiMemoryUI {
|
|
| 86 |
memoryList.addEventListener("click", e => this.handleMemorySourceToggle(e));
|
| 87 |
memoryList.addEventListener("touchstart", e => this.handleMemorySourceToggle(e));
|
| 88 |
|
|
|
|
|
|
|
|
|
|
| 89 |
// Keyboard accessibility: Enter / Space when focused on .memory-source
|
| 90 |
memoryList.addEventListener("keydown", e => {
|
| 91 |
const target = e.target;
|
|
@@ -99,6 +112,22 @@ class KimiMemoryUI {
|
|
| 99 |
}
|
| 100 |
}
|
| 101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
async toggleMemorySystem() {
|
| 103 |
if (!this.memorySystem) return;
|
| 104 |
|
|
@@ -183,9 +212,12 @@ class KimiMemoryUI {
|
|
| 183 |
if (!this.memorySystem) return;
|
| 184 |
|
| 185 |
try {
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
| 189 |
} catch (error) {
|
| 190 |
console.error("Error loading memories:", error);
|
| 191 |
}
|
|
@@ -248,6 +280,13 @@ class KimiMemoryUI {
|
|
| 248 |
}, {});
|
| 249 |
|
| 250 |
let html = "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
Object.entries(groupedMemories).forEach(([category, categoryMemories]) => {
|
| 252 |
html += `
|
| 253 |
<div class="memory-category-group">
|
|
@@ -277,6 +316,7 @@ class KimiMemoryUI {
|
|
| 277 |
<div class="memory-badges">
|
| 278 |
<span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "π€ Auto" : "β Manual"}</span>
|
| 279 |
<span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span>
|
|
|
|
| 280 |
${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""}
|
| 281 |
<span class="memory-importance importance-${importanceLevel}" title="Importance: ${importancePct}% (${importanceLevel})">${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)}</span>
|
| 282 |
</div>
|
|
@@ -326,14 +366,14 @@ class KimiMemoryUI {
|
|
| 326 |
}</div>`
|
| 327 |
: ""
|
| 328 |
}
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
</div>
|
| 338 |
`;
|
| 339 |
});
|
|
@@ -344,7 +384,24 @@ class KimiMemoryUI {
|
|
| 344 |
`;
|
| 345 |
});
|
| 346 |
|
| 347 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
// Apply translations to dynamic content
|
| 350 |
if (window.applyTranslations && typeof window.applyTranslations === "function") {
|
|
@@ -724,7 +781,8 @@ class KimiMemoryUI {
|
|
| 724 |
try {
|
| 725 |
await this.memorySystem.deleteMemory(memoryId);
|
| 726 |
await this.loadMemories();
|
| 727 |
-
|
|
|
|
| 728 |
this.showFeedback("Memory deleted");
|
| 729 |
} catch (error) {
|
| 730 |
console.error("Error deleting memory:", error);
|
|
@@ -755,6 +813,33 @@ class KimiMemoryUI {
|
|
| 755 |
}
|
| 756 |
}
|
| 757 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
async updateMemoryStats() {
|
| 759 |
if (!this.memorySystem) return;
|
| 760 |
|
|
|
|
| 3 |
constructor() {
|
| 4 |
this.memorySystem = null;
|
| 5 |
this.isInitialized = false;
|
| 6 |
+
// Debounce helpers for UI refresh to coalesce multiple DB reads
|
| 7 |
+
this._debounceTimers = {};
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
debounce(key, fn, wait = 350) {
|
| 11 |
+
if (this._debounceTimers[key]) clearTimeout(this._debounceTimers[key]);
|
| 12 |
+
this._debounceTimers[key] = setTimeout(() => {
|
| 13 |
+
fn();
|
| 14 |
+
delete this._debounceTimers[key];
|
| 15 |
+
}, wait);
|
| 16 |
}
|
| 17 |
|
| 18 |
async init() {
|
|
|
|
| 96 |
memoryList.addEventListener("click", e => this.handleMemorySourceToggle(e));
|
| 97 |
memoryList.addEventListener("touchstart", e => this.handleMemorySourceToggle(e));
|
| 98 |
|
| 99 |
+
// General delegated click handler for memory actions (summarize, etc.)
|
| 100 |
+
memoryList.addEventListener("click", e => this.handleMemoryListClick(e));
|
| 101 |
+
|
| 102 |
// Keyboard accessibility: Enter / Space when focused on .memory-source
|
| 103 |
memoryList.addEventListener("keydown", e => {
|
| 104 |
const target = e.target;
|
|
|
|
| 112 |
}
|
| 113 |
}
|
| 114 |
|
| 115 |
+
// Delegated click handler for actions inside the memory list
|
| 116 |
+
async handleMemoryListClick(e) {
|
| 117 |
+
try {
|
| 118 |
+
// pin buttons removed by configuration
|
| 119 |
+
|
| 120 |
+
const summarizeBtn = e.target.closest && e.target.closest("#memory-summarize-btn");
|
| 121 |
+
if (summarizeBtn) {
|
| 122 |
+
e.stopPropagation();
|
| 123 |
+
await this.handleSummarizeAction();
|
| 124 |
+
return;
|
| 125 |
+
}
|
| 126 |
+
} catch (err) {
|
| 127 |
+
console.error("Error handling memory list click", err);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
async toggleMemorySystem() {
|
| 132 |
if (!this.memorySystem) return;
|
| 133 |
|
|
|
|
| 212 |
if (!this.memorySystem) return;
|
| 213 |
|
| 214 |
try {
|
| 215 |
+
// Use debounce to avoid multiple rapid DB reads
|
| 216 |
+
this.debounce("loadMemories", async () => {
|
| 217 |
+
const memories = await this.memorySystem.getAllMemories();
|
| 218 |
+
console.log("Loading memories into UI:", memories.length);
|
| 219 |
+
this.renderMemories(memories);
|
| 220 |
+
});
|
| 221 |
} catch (error) {
|
| 222 |
console.error("Error loading memories:", error);
|
| 223 |
}
|
|
|
|
| 280 |
}, {});
|
| 281 |
|
| 282 |
let html = "";
|
| 283 |
+
|
| 284 |
+
// Toolbar with summarize action
|
| 285 |
+
html += `
|
| 286 |
+
<div class="memory-toolbar" style="display:flex; gap:8px; align-items:center; margin-bottom:12px;">
|
| 287 |
+
<button id="memory-summarize-btn" class="kimi-button" title="Summarize recent memories">π Summarize last 7 days</button>
|
| 288 |
+
</div>
|
| 289 |
+
`;
|
| 290 |
Object.entries(groupedMemories).forEach(([category, categoryMemories]) => {
|
| 291 |
html += `
|
| 292 |
<div class="memory-category-group">
|
|
|
|
| 316 |
<div class="memory-badges">
|
| 317 |
<span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "π€ Auto" : "β Manual"}</span>
|
| 318 |
<span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span>
|
| 319 |
+
${memory.type === "summary" || (memory.tags && memory.tags.includes("summary")) ? `<span class="memory-summary-badge" style="background:var(--accent-color);color:white;padding:2px 6px;border-radius:12px;margin-left:8px;font-size:0.8em;">Summary</span>` : ""}
|
| 320 |
${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""}
|
| 321 |
<span class="memory-importance importance-${importanceLevel}" title="Importance: ${importancePct}% (${importanceLevel})">${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)}</span>
|
| 322 |
</div>
|
|
|
|
| 366 |
}</div>`
|
| 367 |
: ""
|
| 368 |
}
|
| 369 |
+
<div class="memory-actions">
|
| 370 |
+
<button class="memory-edit-btn" onclick="kimiMemoryUI.editMemory('${memory.id}')" data-i18n-title="edit_memory_button_title">
|
| 371 |
+
<i class="fas fa-edit"></i>
|
| 372 |
+
</button>
|
| 373 |
+
<button class="memory-delete-btn" onclick="kimiMemoryUI.deleteMemory('${memory.id}')" data-i18n-title="delete_memory_button_title">
|
| 374 |
+
<i class="fas fa-trash"></i>
|
| 375 |
+
</button>
|
| 376 |
+
</div>
|
| 377 |
</div>
|
| 378 |
`;
|
| 379 |
});
|
|
|
|
| 384 |
`;
|
| 385 |
});
|
| 386 |
|
| 387 |
+
// Minimal runtime guard: block accidental <script> tags in generated HTML
|
| 388 |
+
// This is a non-intrusive safety check that prevents XSS when the
|
| 389 |
+
// assembled `html` somehow contains script tags. In normal operation
|
| 390 |
+
// the content is escaped via KimiValidationUtils and highlightMemoryContent(),
|
| 391 |
+
// so this will not run; it only activates on suspicious input.
|
| 392 |
+
try {
|
| 393 |
+
if (/\<\s*script\b/i.test(html)) {
|
| 394 |
+
console.warn("Blocked suspicious <script> tag in memory HTML rendering");
|
| 395 |
+
// Fallback: render as safe text to avoid executing injected scripts
|
| 396 |
+
memoryList.textContent = html;
|
| 397 |
+
} else {
|
| 398 |
+
memoryList.innerHTML = html;
|
| 399 |
+
}
|
| 400 |
+
} catch (e) {
|
| 401 |
+
// On any unexpected error, fallback to safe text rendering
|
| 402 |
+
console.error("Error while rendering memories, falling back to safe text:", e);
|
| 403 |
+
memoryList.textContent = html;
|
| 404 |
+
}
|
| 405 |
|
| 406 |
// Apply translations to dynamic content
|
| 407 |
if (window.applyTranslations && typeof window.applyTranslations === "function") {
|
|
|
|
| 781 |
try {
|
| 782 |
await this.memorySystem.deleteMemory(memoryId);
|
| 783 |
await this.loadMemories();
|
| 784 |
+
// Debounced stats update
|
| 785 |
+
this.debounce("updateStats", async () => await this.updateMemoryStats());
|
| 786 |
this.showFeedback("Memory deleted");
|
| 787 |
} catch (error) {
|
| 788 |
console.error("Error deleting memory:", error);
|
|
|
|
| 813 |
}
|
| 814 |
}
|
| 815 |
|
| 816 |
+
// pin functionality removed
|
| 817 |
+
|
| 818 |
+
async handleSummarizeAction() {
|
| 819 |
+
if (!this.memorySystem) return;
|
| 820 |
+
try {
|
| 821 |
+
// Destructive confirmation modal
|
| 822 |
+
const confirmMsg = `This action will create a single summary memory for the last 7 days and permanently DELETE the source memories. This is irreversible. Do you want to continue?`;
|
| 823 |
+
if (!confirm(confirmMsg)) {
|
| 824 |
+
this.showFeedback("Summary canceled");
|
| 825 |
+
return;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
this.showFeedback("Creating summary and replacing sources...");
|
| 829 |
+
const result = await this.memorySystem.summarizeAndReplace(7, {});
|
| 830 |
+
if (result) {
|
| 831 |
+
this.showFeedback("Summary created and sources deleted");
|
| 832 |
+
await this.loadMemories();
|
| 833 |
+
await this.updateMemoryStats();
|
| 834 |
+
} else {
|
| 835 |
+
this.showFeedback("No recent memories to summarize");
|
| 836 |
+
}
|
| 837 |
+
} catch (e) {
|
| 838 |
+
console.error("Error creating destructive summary", e);
|
| 839 |
+
this.showFeedback("Error creating summary", "error");
|
| 840 |
+
}
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
async updateMemoryStats() {
|
| 844 |
if (!this.memorySystem) return;
|
| 845 |
|
kimi-js/kimi-module.js
CHANGED
|
@@ -1451,9 +1451,14 @@ async function loadAvailableModels() {
|
|
| 1451 |
console.error("Error loading available models:", error);
|
| 1452 |
const errorDiv = document.createElement("div");
|
| 1453 |
errorDiv.className = "models-error-message";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1454 |
errorDiv.innerHTML = `
|
| 1455 |
-
|
| 1456 |
-
|
| 1457 |
modelsContainer.appendChild(errorDiv);
|
| 1458 |
} finally {
|
| 1459 |
loadAvailableModels._loading = false;
|
|
|
|
| 1451 |
console.error("Error loading available models:", error);
|
| 1452 |
const errorDiv = document.createElement("div");
|
| 1453 |
errorDiv.className = "models-error-message";
|
| 1454 |
+
// Escape any content from error.message to prevent XSS when inserted into innerHTML
|
| 1455 |
+
const safeMsg =
|
| 1456 |
+
window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
|
| 1457 |
+
? window.KimiValidationUtils.escapeHtml(error.message || String(error))
|
| 1458 |
+
: String(error.message || error);
|
| 1459 |
errorDiv.innerHTML = `
|
| 1460 |
+
<p>β Error loading models: ${safeMsg}</p>
|
| 1461 |
+
`;
|
| 1462 |
modelsContainer.appendChild(errorDiv);
|
| 1463 |
} finally {
|
| 1464 |
loadAvailableModels._loading = false;
|
kimi-js/kimi-script.js
CHANGED
|
@@ -545,6 +545,21 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
| 545 |
}
|
| 546 |
}
|
| 547 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
console.log("Chat input event listener attached");
|
| 549 |
} else {
|
| 550 |
console.error("Chat input not found");
|
|
|
|
| 545 |
}
|
| 546 |
}
|
| 547 |
});
|
| 548 |
+
(function (el) {
|
| 549 |
+
if (!el) return;
|
| 550 |
+
const pad =
|
| 551 |
+
(p => (p ? parseFloat(p) : 0))(getComputedStyle(el).paddingTop) +
|
| 552 |
+
(p => (p ? parseFloat(p) : 0))(getComputedStyle(el).paddingBottom);
|
| 553 |
+
const lh = parseFloat(getComputedStyle(el).lineHeight) || 18,
|
| 554 |
+
max = lh * 4 + pad;
|
| 555 |
+
const a = () => {
|
| 556 |
+
el.style.height = "auto";
|
| 557 |
+
el.style.height = Math.min(el.scrollHeight, max) + "px";
|
| 558 |
+
};
|
| 559 |
+
el.addEventListener("input", a);
|
| 560 |
+
el.addEventListener("focus", a);
|
| 561 |
+
setTimeout(a, 0);
|
| 562 |
+
})(chatInput);
|
| 563 |
console.log("Chat input event listener attached");
|
| 564 |
} else {
|
| 565 |
console.error("Chat input not found");
|
kimi-js/kimi-voices.js
CHANGED
|
@@ -126,9 +126,13 @@ class KimiVoiceManager {
|
|
| 126 |
const isFirefox = typeof InstallTrigger !== "undefined" || ua.toLowerCase().includes("firefox");
|
| 127 |
const isSafari = /Safari\//.test(ua) && !/Chrom(e|ium)\//.test(ua) && !/Edg\//.test(ua);
|
| 128 |
const isEdge = /Edg\//.test(ua);
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
| 130 |
if (isFirefox) return "firefox";
|
| 131 |
if (isOpera) return "opera";
|
|
|
|
| 132 |
if (isSafari) return "safari";
|
| 133 |
if (isEdge) return "edge";
|
| 134 |
if (isChrome) return "chrome";
|
|
@@ -340,7 +344,15 @@ class KimiVoiceManager {
|
|
| 340 |
}
|
| 341 |
|
| 342 |
// Use female voice if found, otherwise first compatible voice, with proper fallback
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
if (!this.currentVoice) {
|
| 346 |
console.warn("π€ No voices available for speech synthesis - this may resolve automatically when voices load");
|
|
@@ -377,11 +389,13 @@ class KimiVoiceManager {
|
|
| 377 |
|
| 378 |
const filteredVoices = this.getVoicesForLanguage(this.selectedLanguage);
|
| 379 |
|
| 380 |
-
|
| 381 |
-
|
|
|
|
|
|
|
| 382 |
const noVoicesOption = document.createElement("option");
|
| 383 |
noVoicesOption.value = "none";
|
| 384 |
-
noVoicesOption.textContent = "No voices available
|
| 385 |
noVoicesOption.disabled = true;
|
| 386 |
voiceSelect.appendChild(noVoicesOption);
|
| 387 |
} else {
|
|
@@ -482,8 +496,8 @@ class KimiVoiceManager {
|
|
| 482 |
);
|
| 483 |
}
|
| 484 |
|
| 485 |
-
//
|
| 486 |
-
if (filteredVoices.length === 0)
|
| 487 |
return filteredVoices;
|
| 488 |
}
|
| 489 |
|
|
|
|
| 126 |
const isFirefox = typeof InstallTrigger !== "undefined" || ua.toLowerCase().includes("firefox");
|
| 127 |
const isSafari = /Safari\//.test(ua) && !/Chrom(e|ium)\//.test(ua) && !/Edg\//.test(ua);
|
| 128 |
const isEdge = /Edg\//.test(ua);
|
| 129 |
+
// Detect Brave explicitly: navigator.brave exists in many Brave builds, UA may also include 'Brave'
|
| 130 |
+
const isBrave =
|
| 131 |
+
(!!navigator.brave && typeof navigator.brave.isBrave === "function") || ua.toLowerCase().includes("brave");
|
| 132 |
+
const isChrome = /Chrome\//.test(ua) && !isEdge && !isOpera && !isBrave;
|
| 133 |
if (isFirefox) return "firefox";
|
| 134 |
if (isOpera) return "opera";
|
| 135 |
+
if (isBrave) return "brave";
|
| 136 |
if (isSafari) return "safari";
|
| 137 |
if (isEdge) return "edge";
|
| 138 |
if (isChrome) return "chrome";
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
// Use female voice if found, otherwise first compatible voice, with proper fallback
|
| 347 |
+
// KEEP legacy auto-selection behavior only for Chrome/Edge where it was reliable.
|
| 348 |
+
// For other browsers (Firefox/Brave/Opera), avoid auto-selecting to prevent wrong default (e.g., Hortense).
|
| 349 |
+
const browser = this.browser || this._detectBrowser();
|
| 350 |
+
if (browser === "chrome" || browser === "edge") {
|
| 351 |
+
this.currentVoice = femaleVoice || filteredVoices[0] || null;
|
| 352 |
+
} else {
|
| 353 |
+
// Do not auto-select on less predictable browsers
|
| 354 |
+
this.currentVoice = femaleVoice && filteredVoices.length > 1 ? femaleVoice : null;
|
| 355 |
+
}
|
| 356 |
|
| 357 |
if (!this.currentVoice) {
|
| 358 |
console.warn("π€ No voices available for speech synthesis - this may resolve automatically when voices load");
|
|
|
|
| 389 |
|
| 390 |
const filteredVoices = this.getVoicesForLanguage(this.selectedLanguage);
|
| 391 |
|
| 392 |
+
// If browser is not Chrome or Edge, do NOT expose voice options even when voices exist.
|
| 393 |
+
// This avoids misleading users on Brave/Firefox/Opera/Safari who might think TTS is supported when it's not.
|
| 394 |
+
const browser = this.browser || this._detectBrowser();
|
| 395 |
+
if ((browser !== "chrome" && browser !== "edge") || filteredVoices.length === 0) {
|
| 396 |
const noVoicesOption = document.createElement("option");
|
| 397 |
noVoicesOption.value = "none";
|
| 398 |
+
noVoicesOption.textContent = "No voices available for this browser";
|
| 399 |
noVoicesOption.disabled = true;
|
| 400 |
voiceSelect.appendChild(noVoicesOption);
|
| 401 |
} else {
|
|
|
|
| 496 |
);
|
| 497 |
}
|
| 498 |
|
| 499 |
+
// Do not fall back to all voices: if none match, return empty array so UI shows "no voices available"
|
| 500 |
+
if (filteredVoices.length === 0) return [];
|
| 501 |
return filteredVoices;
|
| 502 |
}
|
| 503 |
|