VirtualKimi commited on
Commit
0bc2a8a
Β·
verified Β·
1 Parent(s): 79fe2c6

Upload 38 files

Browse files
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
- license: openrail
3
- title: Virtual Kimi - AI Companion App πŸ’–
4
- sdk: static
5
- emoji: 😻
6
- colorFrom: indigo
7
- colorTo: purple
8
- pinned: true
9
- short_description: AI companion girlfriends with adaptive personalities
10
- ---
11
- <div align="center">
12
-
13
- <b>Virtual Kimi</b>
14
-
15
- [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
16
- [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#license)
17
-
18
- </div>
19
-
20
- # Virtual Kimi - AI Companion Application πŸ’–
21
-
22
- Web-based AI companion girlfriends featuring adaptive personalities, intelligent memory systems, and immersive conversational experiences.
23
-
24
- ## Overview
25
-
26
- 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.
27
-
28
- - **Lightweight:** ~600 KB of pure JavaScript, HTML, and CSS (no frameworks)
29
- - **Local-first:** All data is stored in your browser's IndexedDB (managed by Dexie.js)
30
- - **No tracking:** The only external calls are to FontAwesome (for icons) and the OpenRouter API (for AI)
31
-
32
- Built with vanilla JavaScript and modern web APIs, it offers a rich, responsive experience across devices.
33
-
34
- ---
35
-
36
- ## 🌐 Support & Links
37
-
38
- - **Website**: [virtualkimi.com](https://virtualkimi.com)
39
- - **Email**: [ijohn@virtualkimi.com](ijohn@virtualkimi.com)
40
- - **X (Twitter)**: [x.com/virtualkimi](https://x.com/virtualkimi)
41
- - **GitHub**: [github.com/virtualkimi](https://github.com/virtualkimi)
42
- - **HuggingFace**: [huggingface.co/VirtualKimi](https://huggingface.co/VirtualKimi)
43
- - **YouTube**: [YouTube Channel](https://www.youtube.com/@VirtualKimi)
44
-
45
- - **Support the project**: [ko-fi.com/virtualkimi](https://ko-fi.com/virtualkimi)
46
- _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!_
47
-
48
- - **ETH Wallet**: 0x836C9D2e605f98Bc7144C62Bef837627b1a9C30c
49
-
50
- ---
51
-
52
- ## Key Features
53
-
54
- ### πŸ€– **Advanced AI Integration**
55
-
56
- **Available recommended models and pricing for Openrouter (per 1M tokens):**
57
-
58
- - **Mistral-small-3.2**: 0.05$ input, 0.1$ output (128k context)
59
- - **Nous Hermes Llama 3.1 70B**: 0.1$ input, 0.28$ output (131k context)
60
- - **Cohere Command-R-08-2024**: 0.15$ input, 0.6$ output (131k context)
61
- - **Qwen3-235b-a22b-think**: 0.13$ input, 0.6$ output (262k context)
62
- - **Grok 3 mini**: 0.3$ input, 0.5$ output (131k context)
63
- - **Nous Hermes Llama 3.1 405B**: 0.7$ input, 0.8$ output (131k context)
64
- - **Anthropic Claude 3 Haiku**: 0.25$ input, 1.25$ output (131k context)
65
- - **Local Model (Ollama)**: 0$ input, 0$ output (4k context, runs offline β€” _experimental, not fully functional yet_)
66
-
67
- ### πŸ‘₯ **Multiple AI Personalities**
68
-
69
- - **Kimi**: Cosmic dreamer and astrophysicist with ethereal sensibilities
70
- - **Bella**: Nurturing botanist who sees people as plants needing care
71
- - **Rosa**: Chaotic prankster thriving on controlled chaos
72
- - **Stella**: Digital artist transforming reality through pixelated vision
73
-
74
- ### Personality Trait Ranges
75
-
76
- All personality traits operate on a 0-100 scale:
77
-
78
- - **Affection**: Emotional warmth and attachment
79
- - **Playfulness**: Fun-loving and spontaneous behavior
80
- - **Intelligence**: Analytical and thoughtful responses
81
- - **Empathy**: Understanding and emotional support
82
- - **Humor**: Wit and lighthearted interactions
83
- - **Romance**: Romantic and intimate expressions
84
-
85
- ### 🧠 **Intelligent Memory System**
86
-
87
- - Automatic extraction and categorization of conversation memories
88
- - Seven memory categories: Personal, Preferences, Relationships, Activities, Goals, Experiences, Events
89
- - Persistent memory across sessions with search and management capabilities
90
- - Character-specific memory isolation
91
-
92
- ### πŸ’« **Dynamic Personality Evolution**
93
-
94
- - Six personality traits that evolve based on interactions:
95
- - Affection, Playfulness, Intelligence, Empathy, Humor, Romance
96
- - Real-time trait adjustments based on conversation tone and content
97
- - Visual personality indicators and progression tracking
98
- - Intelligent model selection and switching
99
- - Real-time emotion detection and analysis
100
- - Contextually-aware responses
101
-
102
- ### 🎬 **Emotion-Driven Visual Experience**
103
-
104
- - Real-time video responses matching detected emotions
105
- - Smooth transitions between emotional states
106
- - Character-specific visual libraries with 50+ video clips
107
- - Context-aware video selection system
108
-
109
- ### 🎨 **Customizable Interface**
110
-
111
- - Five professionally designed themes
112
- - Adjustable interface transparency
113
- - Responsive design optimized for desktop, tablet, and mobile
114
- - Accessibility features and keyboard navigation
115
-
116
- ### 🌍 **Multilingual Support**
117
-
118
- - Full localization in 7 languages: English, French, Spanish, German, Italian, Japanese, Chinese
119
- - Automatic language detection from user input
120
- - Culturally-aware responses and emotion keywords
121
-
122
- ### πŸ”Œ **Extensible Plugin System**
123
-
124
- - Theme plugins for visual customization (currently, only the color theme plugin is functional)
125
- - Voice plugins for speech synthesis options (planned)
126
- - Behavior plugins for personality modifications (planned)
127
- - Secure plugin loading with validation
128
-
129
- ### πŸ›‘οΈ **Security & Privacy**
130
-
131
- - Input validation and sanitization
132
- - Secure API key handling
133
- - Local data storage with IndexedDB
134
- - No server dependencies for core functionality
135
-
136
- ## πŸ—οΈ Technical Architecture
137
-
138
- ### 🧩 Core Technologies
139
-
140
- - **Frontend**: Vanilla JavaScript (ES6+), HTML5, CSS3
141
- - **Database**: IndexedDB with Dexie.js
142
- - **AI Integration**: OpenRouter API
143
- - **Speech**: Web Speech API
144
- - **Audio**: Web Audio API
145
-
146
- ---
147
-
148
- ## ✨ Inspiration & Assets
149
-
150
- This project was originally inspired by the [JackyWine GitHub repository](https://github.com/Jackywine).
151
- @Jackywine on X (Twitter)
152
-
153
- The four main characters are visually based on images from four creators on X (Twitter):
154
-
155
- - @JulyFox33 (Kimi)
156
- - @BelisariaNew (Bella)
157
- - @JuliAIkiko (Rosa and Stella)
158
-
159
- All character videos were generated using the image-to-video AI from Kling.ai, specifically with the Kling v2.1 model.
160
-
161
- 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)
162
-
163
- ---
164
-
165
- ### πŸ—‚οΈ Module Structure
166
-
167
- ```
168
-
169
- β”œβ”€β”€ Core System
170
- β”‚ β”œβ”€β”€ kimi-script.js # Main initialization
171
- β”‚ β”œβ”€β”€ kimi-database.js # Data persistence layer
172
- β”‚ β”œβ”€β”€ kimi-config.js # Configuration management
173
- β”‚ └── kimi-security.js # Security utilities
174
- β”œβ”€β”€ AI & Memory
175
- β”‚ β”œβ”€β”€ kimi-llm-manager.js # LLM integration
176
- β”‚ β”œβ”€β”€ kimi-emotion-system.js # Emotion analysis
177
- β”‚ β”œβ”€β”€ kimi-memory-system.js # Intelligent memory
178
- β”‚ └── kimi-memory-ui.js # Memory interface
179
- β”œβ”€β”€ Interface & Media
180
- β”‚ β”œβ”€β”€ kimi-appearance.js # Theme management
181
- β”‚ β”œβ”€β”€ kimi-voices.js # Speech synthesis
182
- β”‚ β”œβ”€β”€ kimi-utils.js # Utility classes
183
- β”‚ └── kimi-module.js # Core functions
184
- β”œβ”€β”€ Localization
185
- β”‚ └── kimi-locale/ # Translation files
186
- └── Extensions
187
- └── kimi-plugins/ # Plugin system
188
-
189
- ```
190
-
191
- ### Data Flow
192
-
193
- 1. **Input Processing**: User input β†’ Security validation β†’ Language detection
194
- 2. **AI Analysis**: Emotion detection β†’ Memory extraction β†’ LLM processing
195
- 3. **Response Generation**: Personality-aware response β†’ Emotion mapping β†’ Visual selection
196
- 4. **Memory Update**: Trait evolution β†’ Memory storage β†’ UI synchronization
197
-
198
- ## Installation & Setup
199
-
200
- ### Prerequisites
201
-
202
- - Modern web browser (Chrome, Edge, Firefox recommended)
203
- - OpenRouter API key (optional but recommended for full functionality)
204
-
205
- ### Quick Start
206
-
207
- 1. **Clone the repository**
208
-
209
- ```bash
210
- git clone https://github.com/virtualkimi/virtual-kimi.git
211
- cd virtual-kimi
212
- ```
213
-
214
- 2. **Open the application**
215
-
216
- - Open `index.html` in your web browser
217
- - Or serve via local web server for optimal performance:
218
- ```bash
219
- python -m http.server 8000
220
- # Navigate to http://localhost:8000
221
- ```
222
-
223
- 3. **Configure API access**
224
-
225
- - Open Settings β†’ AI & Models
226
- - Add your OpenRouter API key
227
- - Select preferred AI model
228
-
229
- 4. **Customize your experience**
230
- - Choose a character in Personality tab
231
- - Enable memory system in Data tab
232
- - Adjust themes in Appearance tab
233
-
234
- ### Production Deployment
235
-
236
- For production deployment, ensure:
237
-
238
- - HTTPS is enabled (required for microphone access)
239
- - Gzip compression for assets
240
- - Proper cache headers
241
- - CSP headers for enhanced security
242
-
243
- ## βš™οΈ Configuration
244
-
245
- ### API Integration
246
-
247
- The application supports multiple AI providers through OpenRouter:
248
-
249
- - Mistral models
250
- - Nous Hermes models
251
- - Qwen3 models
252
- - Open-source alternatives
253
-
254
- ### Memory System Configuration
255
-
256
- ```javascript
257
- // Memory categories can be customized
258
- const memoryCategories = [
259
- "personal", // Personal information
260
- "preferences", // Likes and dislikes
261
- "relationships", // People and connections
262
- "activities", // Hobbies and activities
263
- "goals", // Aspirations and plans
264
- "experiences", // Past events
265
- "important" // Significant moments
266
- ];
267
- ```
268
-
269
- ## πŸ› οΈ Development
270
-
271
- ### Project Structure
272
-
273
- ```
274
- virtual-kimi/
275
- β”œβ”€β”€ index.html # Main application
276
- β”œβ”€β”€ virtualkimi.html # Landing page
277
- β”œβ”€β”€ kimi-*.js # Core modules
278
- β”œβ”€β”€ kimi-locale/ # Localization
279
- β”œβ”€β”€ kimi-plugins/ # Plugin examples
280
- β”œβ”€β”€ kimi-videos/ # Character videos
281
- β”œβ”€β”€ kimi-icons/ # Character assets
282
- └── docs/ # Documentation
283
- ```
284
-
285
- ### Adding New Features
286
-
287
- #### Creating a New Plugin
288
-
289
- ```javascript
290
- // manifest.json
291
- {
292
- "name": "Custom Theme",
293
- "version": "1.0.0",
294
- "type": "theme",
295
- "style": "theme.css",
296
- "main": "theme.js",
297
- "enabled": true
298
- }
299
- ```
300
-
301
- > **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.
302
-
303
- #### Extending Memory Categories
304
-
305
- ```javascript
306
- // Add to kimi-memory-system.js
307
- const customCategory = {
308
- name: "custom",
309
- icon: "fas fa-star",
310
- keywords: ["keyword1", "keyword2"],
311
- confidence: 0.7
312
- };
313
- ```
314
-
315
- ### Health Check System
316
-
317
- The application includes a comprehensive health check system:
318
-
319
- ```javascript
320
- // Run health check
321
- const healthCheck = new KimiHealthCheck();
322
- const report = await healthCheck.runAllChecks();
323
- console.log(report.status); // 'HEALTHY' or 'NEEDS_ATTENTION'
324
- ```
325
-
326
- ## Browser Compatibility
327
-
328
- | Browser | Voice Recognition | Full Features | Notes |
329
- | ----------- | ----------------- | ------------- | ------------------------- |
330
- | Chrome 90+ | βœ… | βœ… | Recommended |
331
- | Edge 90+ | βœ… | βœ… | Optimal voice performance |
332
- | Firefox 88+ | ⚠️ | βœ… | Limited voice support |
333
- | Safari 14+ | ⚠️ | βœ… | iOS limitations |
334
-
335
- ## Performance
336
-
337
- ### Optimization Features
338
-
339
- - Lazy loading of non-critical modules
340
- - Efficient batch database operations
341
- - Debounced UI interactions
342
- - Memory management with cleanup
343
- - Optimized video preloading
344
-
345
- ### Resource Usage
346
-
347
- - Memory footprint: ~15-30MB active usage
348
- - Storage: Scales with conversation history
349
- - Network: API calls only, no tracking
350
- - CPU: Minimal background processing
351
-
352
- ## Privacy & Security
353
-
354
- ### Data Handling
355
-
356
- - All data stored locally in browser
357
- - No telemetry or analytics
358
- - API keys encrypted in local storage
359
- - User content never sent to external servers (except chosen AI provider)
360
-
361
- ### Security Measures
362
-
363
- - Input validation and sanitization
364
- - XSS protection
365
- - Safe plugin loading
366
- - Secure API communication
367
-
368
- ## Troubleshooting
369
-
370
- ### Common Issues
371
-
372
- - **Microphone not working**: Ensure HTTPS and browser permissions
373
- - **API errors**: Verify OpenRouter key and model availability
374
- - **Performance issues**: Clear browser cache, check available memory
375
- - **Memory system not learning**: Ensure system is enabled in Data tab
376
-
377
- ## Contributing
378
-
379
- We welcome contributions! Please see our contributing guidelines:
380
-
381
- 1. Fork the repository
382
- 2. Create a feature branch
383
- 3. Make your changes with appropriate tests
384
- 4. Submit a pull request with detailed description
385
-
386
- ### Development Guidelines
387
-
388
- - Follow existing code style and patterns
389
- - Add comments for complex functionality
390
- - Test across multiple browsers
391
- - Update documentation for new features
392
-
393
- ## πŸ”„ TODO / Roadmap
394
-
395
- - [ ] Full support for local models (Ollama integration, offline mode)
396
- - [ ] Voice plugin system (custom voices, TTS engines)
397
- - [ ] Behavior plugin system (custom AI behaviors)
398
- - [ ] Better advanced memory management UI
399
- - [ ] More character personalities and backgrounds
400
- - [ ] In-app onboarding and help system
401
- - [ ] Enhanced mobile experience (UI/UX)
402
- - [ ] More granular privacy controls
403
- - [ ] User profile and persistent settings sync (optional)
404
- - [ ] Community plugin/theme sharing platform
405
- - [ ] Improved error reporting and diagnostics
406
- - [ ] Accessibility improvements (screen reader, contrast, etc.)
407
- - [ ] Automated testing and CI/CD pipeline
408
- - [ ] Documentation in multiple languages
409
- - [ ] Performance profiling and optimization for large histories
410
- - [ ] Create new character videos better matching specific contexts
411
- - [ ] Improve emotion and context logic
412
- - [ ] Enhance memory management and logic
413
-
414
- ---
415
-
416
- ## πŸ“œ License
417
-
418
- 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.**
419
-
420
- See the [LICENSE](LICENSE) file for details.
421
-
422
- [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
423
- [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#license)
424
-
425
- ---
426
-
427
- **Virtual Kimi** - Creating meaningful connections between humans and AI, one conversation at a time.
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
+ [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
6
+ [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#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
+ [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
420
+ [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#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-29",
60
- "version": "v1.1.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
- <input type="text" id="chat-input" data-i18n-placeholder="write_something"
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
- <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,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.1</p>
1076
- <p><strong>Last update :</strong> August 29, 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,7 +1126,7 @@
1126
  "name": "Jean & Kimi"
1127
  },
1128
  "dateCreated": "2025-07-16",
1129
- "version": "v1.1.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: ["affection", "tendresse", "proche", "chaleur", "gentil", "attentionnΓ©", "cΓ’lin", "aimer", "adorer"],
 
 
 
 
 
 
 
 
 
 
 
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
- // Invalidate cache
658
- if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") {
659
- window.KimiCacheManager.delete(`trait_${character}_${trait}`);
660
- window.KimiCacheManager.delete(`all_traits_${character}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
662
 
663
- return this.db.personality.put({
664
- trait: trait,
665
- character: character,
666
- value: value,
667
- updated: new Date().toISOString()
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 = text.toLowerCase();
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 emotionChecks) {
117
- const hits = check.keywords.reduce((acc, word) => acc + (lowerText.includes(word.toLowerCase()) ? 1 : 0), 0);
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 = positiveWords.some(word => lowerText.includes(word.toLowerCase()));
132
- const hasNegative = negativeWords.some(word => lowerText.includes(word.toLowerCase()));
 
 
 
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.3))); // Empathy still grows (understanding pain)
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.3))); // Reduced from 0.4
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.3))); // Affection from shared activity
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.3))); // Reduced from 0.4
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.3))); // Reduced from 0.4
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.3))); // Listening builds empathy
259
  break;
260
  }
261
 
@@ -302,8 +317,16 @@ class KimiEmotionSystem {
302
  intelligence: to2(clamp(intelligence))
303
  };
304
 
305
- // Save to database
306
- await this.db.setPersonalityBatch(updatedTraits, selectedCharacter);
 
 
 
 
 
 
 
 
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 lowerUser = userMessage ? userMessage.toLowerCase() : "";
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.match(new RegExp(w, "g")) || []).length * 1.0;
339
- posCount += (lowerKimi.match(new RegExp(w, "g")) || []).length * 0.3;
340
  }
341
  for (const w of negWords) {
342
- negCount += (lowerUser.match(new RegExp(w, "g")) || []).length * 1.0;
343
- negCount += (lowerKimi.match(new RegExp(w, "g")) || []).length * 0.3;
344
  }
345
 
346
- const delta = (posCount - negCount) * 0.3; // Reduced from 0.4 - slower LLM-based progression
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
- await this.db.setPersonalityTrait(trait, value, character);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return fullPersonality + rankedSnapshot;
 
 
 
 
189
  }
190
 
191
  async generateKimiPersonality() {
@@ -281,7 +285,25 @@ class KimiLLMManager {
281
  console.warn("Error loading memories for personality:", error);
282
  }
283
  }
284
- const preferences = await this.db.getAllPreferences();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: ${preferences.totalInteractions || 0}`,
385
- `- Current affection level: ${preferences.favorabilityLevel || 50}%`,
386
- `- Last interaction: ${preferences.lastInteraction || "First time"}`,
387
- `- Favorite words: ${(preferences.favoriteWords || []).join(", ")}`,
 
 
 
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
- return await this.db.db.memories
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
- console.log(`Retrieved ${memories.length} memories for character: ${character}`);
 
 
 
 
 
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
- // If the number of memories exceeds the limit (this.maxMemoryEntries),
1151
- // delete the least important/oldest ones to keep only the most relevant.
1152
- if (memories.length > this.maxMemoryEntries) {
1153
- // Sort by importance (confidence) and recency (timestamp)
1154
- memories.sort((a, b) => {
1155
- // Score = confidence * age (the higher the score, the less priority the memory has)
1156
- const scoreA = a.confidence * (Date.now() - new Date(a.timestamp).getTime());
1157
- const scoreB = b.confidence * (Date.now() - new Date(b.timestamp).getTime());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1158
  return scoreB - scoreA;
1159
  });
1160
 
1161
- // Delete all memories beyond the limit
1162
- const toDelete = memories.slice(this.maxMemoryEntries);
1163
- for (const memory of toDelete) {
1164
- await this.deleteMemory(memory.id);
 
 
 
1165
  }
1166
  }
1167
  } catch (error) {
@@ -1180,7 +1243,10 @@ class KimiMemorySystem {
1180
 
1181
  if (!context) {
1182
  // Return most important and recent memories
1183
- return this.selectMostImportantMemories(allMemories, limit);
 
 
 
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
- return relevantMemories.slice(0, limit);
 
 
 
 
 
 
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.4;
1243
 
1244
- // Keyword matching bonus
1245
- let keywordMatches = 0;
1246
- for (const word of contextWords) {
1247
- if (memoryWords.includes(word)) {
1248
- keywordMatches++;
 
 
 
 
 
 
 
 
 
 
 
 
 
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 score = importance * 0.35 + recency * 0.2 + freq * 0.15 + confidence * 0.2 + freshness * 0.1;
 
 
 
 
 
 
 
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
- const memories = await this.memorySystem.getAllMemories();
187
- console.log("Loading memories into UI:", memories.length);
188
- this.renderMemories(memories);
 
 
 
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
- <div class="memory-actions">
330
- <button class="memory-edit-btn" onclick="kimiMemoryUI.editMemory('${memory.id}')" data-i18n-title="edit_memory_button_title">
331
- <i class="fas fa-edit"></i>
332
- </button>
333
- <button class="memory-delete-btn" onclick="kimiMemoryUI.deleteMemory('${memory.id}')" data-i18n-title="delete_memory_button_title">
334
- <i class="fas fa-trash"></i>
335
- </button>
336
- </div>
337
  </div>
338
  `;
339
  });
@@ -344,7 +384,24 @@ class KimiMemoryUI {
344
  `;
345
  });
346
 
347
- memoryList.innerHTML = html;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- await this.updateMemoryStats();
 
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
- <p>❌ Error loading models: ${error.message}</p>
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
- const isChrome = /Chrome\//.test(ua) && !isEdge && !isOpera;
 
 
 
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
- this.currentVoice = femaleVoice || filteredVoices[0] || null;
 
 
 
 
 
 
 
 
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
- if (filteredVoices.length === 0) {
381
- // Add a placeholder option when no voices are available
 
 
382
  const noVoicesOption = document.createElement("option");
383
  noVoicesOption.value = "none";
384
- noVoicesOption.textContent = "No voices available (loading...)";
385
  noVoicesOption.disabled = true;
386
  voiceSelect.appendChild(noVoicesOption);
387
  } else {
@@ -482,8 +496,8 @@ class KimiVoiceManager {
482
  );
483
  }
484
 
485
- // Last resort: return all voices
486
- if (filteredVoices.length === 0) filteredVoices = this.availableVoices;
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