VirtualKimi commited on
Commit
798bcc6
·
verified ·
1 Parent(s): 4f6c6df

Upload 38 files

Browse files
CHANGELOG.md CHANGED
@@ -1,26 +1,26 @@
1
  # Virtual Kimi Changelog
2
 
3
- ## [1.0.5] - 2025-08-12
4
 
5
- ### Security & UX
6
 
7
- - Removed all browser/extension password save and autofill prompts for API key and all text fields (no type="password", no name, all autofill-blocking attributes, CSS masking only).
8
- - Global autofill prevention for all text/textarea fields, including dynamically added ones.
9
- - Provider selection: Base URL is readonly for OpenRouter, OpenAI, Groq, Together, DeepSeek; editable only for Custom and Ollama.
10
- - Model ID is auto-synced and readonly for OpenRouter; editable for others. Selection is persistent and always reflected in the UI.
11
- - Robust preference persistence and UI sync for provider, base URL, and model.
12
 
13
- ### Provider & LLM Model Handling
14
 
15
- - Base URL is now readonly for OpenRouter, OpenAI, Groq, Together, DeepSeek (canonical, not user-editable). Editable only for Custom OpenAI-compatible and Ollama.
16
- - Model ID is auto-synced and readonly for OpenRouter; editable for others. Selection is persistent and always reflected in the UI.
17
- - Robust preference persistence and UI sync for provider, base URL, and model.
18
- - Fixed Model ID sync when changing OpenRouter model (immediate update, persistence, readonly).
 
19
 
20
- ### Misc
21
 
22
- - No text field in the app can be autofilled or saved by browser or extension.
23
- - Improved UI robustness when switching provider or model.
 
24
 
25
  ## [1.0.4] - 2025-08-09 - "Emotion & Context Logic Upgrade"
26
 
 
1
  # Virtual Kimi Changelog
2
 
3
+ ## [1.0.5] - 2025-08-13 - "Personality & Language Sensitivity"
4
 
5
+ ### Added
6
 
7
+ - Multilingual profanity/insult detection for negative context across 7 languages (en, fr, es, de, it, ja, zh)
8
+ - Gendered variants support in negative keywords (fr, es, it, de) to improve accuracy (e.g., sérieux/sérieuse)
9
+ - Extended personality keywords for Spanish and Italian (all traits) with gendered forms
 
 
10
 
11
+ ### Changed
12
 
13
+ - Personality sync now completes missing values using character-specific defaults (with generic fallback)
14
+ - Centralized side-effects on personality updates (UI/memory/video/voice) behind a single `personality:updated` listener
15
+ - Sliders: generic handler only updates display; persistence and effects handled by specialized listeners
16
+ - Trait updates preserve fractional progress (2 decimals) for smoother affection changes
17
+ - Stats now use character-specific default for affection (with generic fallback) when missing
18
 
19
+ ### Fixed
20
 
21
+ - Removed obsolete `personalityUpdated` listener to avoid duplicate processing
22
+ - Unified KimiMemory affection default loading (removed conflicting double assignment and legacy default 80)
23
+ - Minor cleanup and consistency improvements in utils and sync flows
24
 
25
  ## [1.0.4] - 2025-08-09 - "Emotion & Context Logic Upgrade"
26
 
README.md CHANGED
@@ -1,440 +1,421 @@
1
- ---
2
- license: openrail
3
- sdk: static
4
- emoji: 😻
5
- colorFrom: indigo
6
- colorTo: purple
7
- pinned: true
8
- short_description: Virtual Kimi - AI Companion Application 💖
9
- ---
10
- <div align="center">
11
-
12
- <b>Virtual Kimi</b>
13
-
14
- [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
15
- [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#license)
16
-
17
- </div>
18
-
19
- # Virtual Kimi - AI Companion Application 💖
20
-
21
- A web-based AI companion featuring adaptive personalities, intelligent memory systems, and immersive conversational experiences.
22
-
23
- ## Overview
24
-
25
- 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 personalities.
26
-
27
- - **Lightweight:** ~600 KB of pure JavaScript, HTML, and CSS (no frameworks)
28
- - **Local-first:** All data is stored in your browser's IndexedDB (managed by Dexie.js)
29
- - **No tracking:** The only external calls are to FontAwesome (for icons) and the OpenRouter API (for AI)
30
-
31
- Built with vanilla JavaScript and modern web APIs, it offers a rich, responsive experience across devices.
32
-
33
- ---
34
-
35
- ## 🌐 Support & Links
36
-
37
- - **Website**: [virtualkimi.com](https://virtualkimi.com)
38
39
- - **X (Twitter)**: [x.com/virtualkimi](https://x.com/virtualkimi)
40
- - **GitHub**: [github.com/virtualkimi](https://github.com/virtualkimi)
41
- - **HuggingFace**: [huggingface.co/VirtualKimi](https://huggingface.co/VirtualKimi)
42
- - **YouTube**: [YouTube Channel](https://www.youtube.com/@VirtualKimi)
43
-
44
- - **Support the project**: [ko-fi.com/virtualkimi](https://ko-fi.com/virtualkimi)
45
- _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!_
46
-
47
- - **ETH Wallet**: 0x836C9D2e605f98Bc7144C62Bef837627b1a9C30c
48
-
49
- ---
50
-
51
- ## Key Features
52
-
53
- ### 🤖 **Advanced AI Integration**
54
-
55
- - Support for affordable, less-censored LLM models via OpenRouter (see below for available models)
56
- - Version 1.0.4 added support for some LLMs providers
57
-
58
- **Available models and pricing (per 1M tokens):**
59
-
60
- - **Mistral-small-3.2**: 0.05$ input, 0.1$ output (128k context)
61
- - **Nous Hermes Llama 3.1 70B**: 0.1$ input, 0.28$ output (131k context)
62
- - **Cohere Command-R-08-2024**: 0.15$ input, 0.6$ output (131k context)
63
- - **Qwen3-235b-a22b-think**: 0.13$ input, 0.6$ output (262k context)
64
- - **Nous Hermes Llama 3.1 405B**: 0.7$ input, 0.8$ output (131k context)
65
- - **Anthropic Claude 3 Haiku**: 0.25$ input, 1.25$ output (131k context)
66
- - **Local Model (Ollama)**: 0$ input, 0$ output (4k context, runs offline — _experimental, not fully functional yet_)
67
-
68
- ### 👥 **Multiple AI Personalities**
69
-
70
- - **Kimi**: Cosmic dreamer and astrophysicist with ethereal sensibilities
71
- - **Bella**: Nurturing botanist who sees people as plants needing care
72
- - **Rosa**: Chaotic prankster thriving on controlled chaos
73
- - **Stella**: Digital artist transforming reality through pixelated vision
74
-
75
- ### Personality Trait Ranges
76
-
77
- All personality traits operate on a 0-100 scale:
78
-
79
- - **Affection**: Emotional warmth and attachment
80
- - **Playfulness**: Fun-loving and spontaneous behavior
81
- - **Intelligence**: Analytical and thoughtful responses
82
- - **Empathy**: Understanding and emotional support
83
- - **Humor**: Wit and lighthearted interactions
84
- - **Romance**: Romantic and intimate expressions
85
-
86
- ### 🧠 **Intelligent Memory System**
87
-
88
- - Automatic extraction and categorization of conversation memories
89
- - Seven memory categories: Personal, Preferences, Relationships, Activities, Goals, Experiences, Events
90
- - Persistent memory across sessions with search and management capabilities
91
- - Character-specific memory isolation
92
-
93
- ### 💫 **Dynamic Personality Evolution**
94
-
95
- - Six personality traits that evolve based on interactions:
96
- - Affection, Playfulness, Intelligence, Empathy, Humor, Romance
97
- - Real-time trait adjustments based on conversation tone and content
98
- - Visual personality indicators and progression tracking
99
- - Intelligent model selection and switching
100
- - Real-time emotion detection and analysis
101
- - Contextually-aware responses
102
-
103
- ### 🎬 **Emotion-Driven Visual Experience**
104
-
105
- - Real-time video responses matching detected emotions
106
- - Smooth transitions between emotional states
107
- - Character-specific visual libraries with 50+ video clips
108
- - Context-aware video selection system
109
-
110
- ### 🎨 **Customizable Interface**
111
-
112
- - Five professionally designed themes
113
- - Adjustable interface transparency
114
- - Responsive design optimized for desktop, tablet, and mobile
115
- - Accessibility features and keyboard navigation
116
-
117
- ### 🌍 **Multilingual Support**
118
-
119
- - Full localization in 7 languages: English, French, Spanish, German, Italian, Japanese, Chinese
120
- - Automatic language detection from user input
121
- - Culturally-aware responses and emotion keywords
122
-
123
- ### 🔌 **Extensible Plugin System**
124
-
125
- - Theme plugins for visual customization (currently, only the color theme plugin is functional)
126
- - Voice plugins for speech synthesis options (planned)
127
- - Behavior plugins for personality modifications (planned)
128
- - Secure plugin loading with validation
129
-
130
- ### 🛡️ **Security & Privacy**
131
-
132
- - Input validation and sanitization
133
- - Secure API key handling
134
- - Local data storage with IndexedDB
135
- - No server dependencies for core functionality
136
-
137
- ## 🏗️ Technical Architecture
138
-
139
- ### 🧩 Core Technologies
140
-
141
- - **Frontend**: Vanilla JavaScript (ES6+), HTML5, CSS3
142
- - **Database**: IndexedDB with Dexie.js
143
- - **AI Integration**: OpenRouter API
144
- - **Speech**: Web Speech API
145
- - **Audio**: Web Audio API
146
-
147
- ---
148
-
149
- ## Inspiration & Assets
150
-
151
- This project was originally inspired by the [JackyWine GitHub repository](https://github.com/Jackywine).
152
- @Jackywine on X (Twitter)
153
-
154
- The four main characters are visually based on images from four creators on X (Twitter):
155
-
156
- - @JulyFox33 (Kimi)
157
- - @BelisariaNew (Bella)
158
- - @JuliAIkiko (Rosa and Stella)
159
-
160
- All character videos were generated using the image-to-video AI from Kling.ai, specifically with the Kling v2.1 model.
161
-
162
- 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)
163
-
164
- ---
165
-
166
- ### 🗂️ Module Structure
167
-
168
- ```
169
-
170
- ├── Core System
171
- │ ├── kimi-script.js # Main initialization
172
- │ ├── kimi-database.js # Data persistence layer
173
- ├── kimi-config.js # Configuration management
174
- └── kimi-security.js # Security utilities
175
- ├── AI & Memory
176
- ├── kimi-llm-manager.js # LLM integration
177
- ├── kimi-emotion-system.js # Emotion analysis
178
- │ ├── kimi-memory-system.js # Intelligent memory
179
- │ └── kimi-memory-ui.js # Memory interface
180
- ├── Interface & Media
181
- ├── kimi-appearance.js # Theme management
182
- │ ├── kimi-voices.js # Speech synthesis
183
- ├── kimi-utils.js # Utility classes
184
- └── kimi-module.js # Core functions
185
- ├── Localization
186
- └── kimi-locale/ # Translation files
187
- └── Extensions
188
- └── kimi-plugins/ # Plugin system
189
-
190
- ```
191
-
192
- ### Data Flow
193
-
194
- 1. **Input Processing**: User input → Security validation → Language detection
195
- 2. **AI Analysis**: Emotion detection → Memory extraction → LLM processing
196
- 3. **Response Generation**: Personality-aware response → Emotion mapping → Visual selection
197
- 4. **Memory Update**: Trait evolution → Memory storage → UI synchronization
198
-
199
- ## Installation & Setup
200
-
201
- ### Prerequisites
202
-
203
- - Modern web browser (Chrome, Edge, Firefox recommended)
204
- - OpenRouter API key (optional but recommended for full functionality)
205
-
206
- ### Quick Start
207
-
208
- 1. **Clone the repository**
209
-
210
- ```bash
211
- git clone https://github.com/virtualkimi/virtual-kimi.git
212
- cd virtual-kimi
213
- ```
214
-
215
- 2. **Open the application**
216
-
217
- - Open `index.html` in your web browser
218
- - Or serve via local web server for optimal performance:
219
- ```bash
220
- python -m http.server 8000
221
- # Navigate to http://localhost:8000
222
- ```
223
-
224
- 3. **Configure API access**
225
-
226
- - Open Settings → AI & Models
227
- - Add your OpenRouter API key
228
- - Select preferred AI model
229
-
230
- 4. **Customize your experience**
231
- - Choose a character in Personality tab
232
- - Enable memory system in Data tab
233
- - Adjust themes in Appearance tab
234
-
235
- ### Production Deployment
236
-
237
- For production deployment, ensure:
238
-
239
- - HTTPS is enabled (required for microphone access)
240
- - Gzip compression for assets
241
- - Proper cache headers
242
- - CSP headers for enhanced security
243
-
244
- ## ⚙️ Configuration
245
-
246
- ### API Integration
247
-
248
- The application supports multiple AI providers through OpenRouter:
249
-
250
- - Mistral models
251
- - Nous Hermes models
252
- - Qwen3 models
253
- - Open-source alternatives
254
-
255
- ### Memory System Configuration
256
-
257
- ```javascript
258
- // Memory categories can be customized
259
- const memoryCategories = [
260
- "personal", // Personal information
261
- "preferences", // Likes and dislikes
262
- "relationships", // People and connections
263
- "activities", // Hobbies and activities
264
- "goals", // Aspirations and plans
265
- "experiences", // Past events
266
- "important" // Significant moments
267
- ];
268
- ```
269
-
270
- ## 🛠️ Development
271
-
272
- ### Project Structure
273
-
274
- ```
275
- virtual-kimi/
276
- ├── index.html # Main application
277
- ├── virtualkimi.html # Landing page
278
- ├── kimi-*.js # Core modules
279
- ├── kimi-locale/ # Localization
280
- ├── kimi-plugins/ # Plugin examples
281
- ├── kimi-videos/ # Character videos
282
- ├── kimi-icons/ # Character assets
283
- └── docs/ # Documentation
284
- ```
285
-
286
- ### Adding New Features
287
-
288
- #### Creating a New Plugin
289
-
290
- ```javascript
291
- // manifest.json
292
- {
293
- "name": "Custom Theme",
294
- "version": "1.0.0",
295
- "type": "theme",
296
- "style": "theme.css",
297
- "main": "theme.js",
298
- "enabled": true
299
- }
300
- ```
301
-
302
- > **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.
303
-
304
- #### Extending Memory Categories
305
-
306
- ```javascript
307
- // Add to kimi-memory-system.js
308
- const customCategory = {
309
- name: "custom",
310
- icon: "fas fa-star",
311
- keywords: ["keyword1", "keyword2"],
312
- confidence: 0.7
313
- };
314
- ```
315
-
316
- ### Health Check System
317
-
318
- The application includes a comprehensive health check system:
319
-
320
- ```javascript
321
- // Run health check
322
- const healthCheck = new KimiHealthCheck();
323
- const report = await healthCheck.runAllChecks();
324
- console.log(report.status); // 'HEALTHY' or 'NEEDS_ATTENTION'
325
- ```
326
-
327
- ## Browser Compatibility
328
-
329
- | Browser | Voice Recognition | Full Features | Notes |
330
- | ----------- | ----------------- | ------------- | ------------------------- |
331
- | Chrome 90+ | ✅ | ✅ | Recommended |
332
- | Edge 90+ | ✅ | ✅ | Optimal voice performance |
333
- | Firefox 88+ | ⚠️ | ✅ | Limited voice support |
334
- | Safari 14+ | ⚠️ | ✅ | iOS limitations |
335
-
336
- ## Performance
337
-
338
- ### Optimization Features
339
-
340
- - Lazy loading of non-critical modules
341
- - Efficient batch database operations
342
- - Debounced UI interactions
343
- - Memory management with cleanup
344
- - Optimized video preloading
345
-
346
- ### Resource Usage
347
-
348
- - Memory footprint: ~15-30MB active usage
349
- - Storage: Scales with conversation history
350
- - Network: API calls only, no tracking
351
- - CPU: Minimal background processing
352
-
353
- ## Privacy & Security
354
-
355
- ### Data Handling
356
-
357
- - All data stored locally in browser
358
- - No telemetry or analytics
359
- - API keys encrypted in local storage
360
- - User content never sent to external servers (except chosen AI provider)
361
-
362
- ### Security Measures
363
-
364
- - Input validation and sanitization
365
- - XSS protection
366
- - Safe plugin loading
367
- - Secure API communication
368
-
369
- ## Troubleshooting
370
-
371
- ### Common Issues
372
-
373
- - **Microphone not working**: Ensure HTTPS and browser permissions
374
- - **API errors**: Verify OpenRouter key and model availability
375
- - **Performance issues**: Clear browser cache, check available memory
376
- - **Memory system not learning**: Ensure system is enabled in Data tab
377
-
378
- ### Debug Mode
379
-
380
- Enable debug logging in browser console:
381
-
382
- ```javascript
383
- window.KIMI_DEBUG = true;
384
- ```
385
-
386
- ## Contributing
387
-
388
- We welcome contributions! Please see our contributing guidelines:
389
-
390
- 1. Fork the repository
391
- 2. Create a feature branch
392
- 3. Make your changes with appropriate tests
393
- 4. Submit a pull request with detailed description
394
-
395
- ### Development Guidelines
396
-
397
- - Follow existing code style and patterns
398
- - Add comments for complex functionality
399
- - Test across multiple browsers
400
- - Update documentation for new features
401
-
402
- ## 🔄 TODO / Roadmap
403
-
404
- - [ ] Full support for local models (Ollama integration, offline mode)
405
- - [ ] Voice plugin system (custom voices, TTS engines)
406
- - [ ] Behavior plugin system (custom AI behaviors)
407
- - [ ] Better advanced memory management UI
408
- - [ ] More character personalities and backgrounds
409
- - [ ] In-app onboarding and help system
410
- - [ ] Enhanced mobile experience (UI/UX)
411
- - [ ] More granular privacy controls
412
- - [ ] User profile and persistent settings sync (optional)
413
- - [ ] Community plugin/theme sharing platform
414
- - [ ] Improved error reporting and diagnostics
415
- - [ ] Accessibility improvements (screen reader, contrast, etc.)
416
- - [ ] Automated testing and CI/CD pipeline
417
- - [ ] Documentation in multiple languages
418
- - [ ] Performance profiling and optimization for large histories
419
- - [ ] Create new character videos better matching specific contexts
420
- - [ ] Improve emotion and context logic
421
- - [ ] Enhance memory management and logic
422
-
423
- ---
424
-
425
- ## 📜 License
426
-
427
- 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.**
428
-
429
- See the [LICENSE](LICENSE) file for details.
430
-
431
- [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
432
- [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#license)
433
-
434
- ---
435
-
436
- **Virtual Kimi** - Creating meaningful connections between humans and AI, one conversation at a time.
437
-
438
- > _"Love is the most powerful code"_ 💕
439
- >
440
- > — 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 companion girlfriends 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
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
+ **Available recommended models and pricing for Openrouter (per 1M tokens):**
47
+
48
+ - **Mistral-small-3.2**: 0.05$ input, 0.1$ output (128k context)
49
+ - **Nous Hermes Llama 3.1 70B**: 0.1$ input, 0.28$ output (131k context)
50
+ - **Cohere Command-R-08-2024**: 0.15$ input, 0.6$ output (131k context)
51
+ - **Qwen3-235b-a22b-think**: 0.13$ input, 0.6$ output (262k context)
52
+ - **Grok 3 mini**: 0.3$ input, 0.5$ output (131k context)
53
+ - **Nous Hermes Llama 3.1 405B**: 0.7$ input, 0.8$ output (131k context)
54
+ - **Anthropic Claude 3 Haiku**: 0.25$ input, 1.25$ output (131k context)
55
+ - **Local Model (Ollama)**: 0$ input, 0$ output (4k context, runs offline _experimental, not fully functional yet_)
56
+
57
+ ### 👥 **Multiple AI Personalities**
58
+
59
+ - **Kimi**: Cosmic dreamer and astrophysicist with ethereal sensibilities
60
+ - **Bella**: Nurturing botanist who sees people as plants needing care
61
+ - **Rosa**: Chaotic prankster thriving on controlled chaos
62
+ - **Stella**: Digital artist transforming reality through pixelated vision
63
+
64
+ ### Personality Trait Ranges
65
+
66
+ All personality traits operate on a 0-100 scale:
67
+
68
+ - **Affection**: Emotional warmth and attachment
69
+ - **Playfulness**: Fun-loving and spontaneous behavior
70
+ - **Intelligence**: Analytical and thoughtful responses
71
+ - **Empathy**: Understanding and emotional support
72
+ - **Humor**: Wit and lighthearted interactions
73
+ - **Romance**: Romantic and intimate expressions
74
+
75
+ ### 🧠 **Intelligent Memory System**
76
+
77
+ - Automatic extraction and categorization of conversation memories
78
+ - Seven memory categories: Personal, Preferences, Relationships, Activities, Goals, Experiences, Events
79
+ - Persistent memory across sessions with search and management capabilities
80
+ - Character-specific memory isolation
81
+
82
+ ### 💫 **Dynamic Personality Evolution**
83
+
84
+ - Six personality traits that evolve based on interactions:
85
+ - Affection, Playfulness, Intelligence, Empathy, Humor, Romance
86
+ - Real-time trait adjustments based on conversation tone and content
87
+ - Visual personality indicators and progression tracking
88
+ - Intelligent model selection and switching
89
+ - Real-time emotion detection and analysis
90
+ - Contextually-aware responses
91
+
92
+ ### 🎬 **Emotion-Driven Visual Experience**
93
+
94
+ - Real-time video responses matching detected emotions
95
+ - Smooth transitions between emotional states
96
+ - Character-specific visual libraries with 50+ video clips
97
+ - Context-aware video selection system
98
+
99
+ ### 🎨 **Customizable Interface**
100
+
101
+ - Five professionally designed themes
102
+ - Adjustable interface transparency
103
+ - Responsive design optimized for desktop, tablet, and mobile
104
+ - Accessibility features and keyboard navigation
105
+
106
+ ### 🌍 **Multilingual Support**
107
+
108
+ - Full localization in 7 languages: English, French, Spanish, German, Italian, Japanese, Chinese
109
+ - Automatic language detection from user input
110
+ - Culturally-aware responses and emotion keywords
111
+
112
+ ### 🔌 **Extensible Plugin System**
113
+
114
+ - Theme plugins for visual customization (currently, only the color theme plugin is functional)
115
+ - Voice plugins for speech synthesis options (planned)
116
+ - Behavior plugins for personality modifications (planned)
117
+ - Secure plugin loading with validation
118
+
119
+ ### 🛡️ **Security & Privacy**
120
+
121
+ - Input validation and sanitization
122
+ - Secure API key handling
123
+ - Local data storage with IndexedDB
124
+ - No server dependencies for core functionality
125
+
126
+ ## 🏗️ Technical Architecture
127
+
128
+ ### 🧩 Core Technologies
129
+
130
+ - **Frontend**: Vanilla JavaScript (ES6+), HTML5, CSS3
131
+ - **Database**: IndexedDB with Dexie.js
132
+ - **AI Integration**: OpenRouter API
133
+ - **Speech**: Web Speech API
134
+ - **Audio**: Web Audio API
135
+
136
+ ---
137
+
138
+ ## ✨ Inspiration & Assets
139
+
140
+ This project was originally inspired by the [JackyWine GitHub repository](https://github.com/Jackywine).
141
+ @Jackywine on X (Twitter)
142
+
143
+ The four main characters are visually based on images from four creators on X (Twitter):
144
+
145
+ - @JulyFox33 (Kimi)
146
+ - @BelisariaNew (Bella)
147
+ - @JuliAIkiko (Rosa and Stella)
148
+
149
+ All character videos were generated using the image-to-video AI from Kling.ai, specifically with the Kling v2.1 model.
150
+
151
+ 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)
152
+
153
+ ---
154
+
155
+ ### 🗂️ Module Structure
156
+
157
+ ```
158
+
159
+ ├── Core System
160
+ ├── kimi-script.js # Main initialization
161
+ │ ├── kimi-database.js # Data persistence layer
162
+ ├── kimi-config.js # Configuration management
163
+ │ └── kimi-security.js # Security utilities
164
+ ├── AI & Memory
165
+ │ ├── kimi-llm-manager.js # LLM integration
166
+ ├── kimi-emotion-system.js # Emotion analysis
167
+ │ ├── kimi-memory-system.js # Intelligent memory
168
+ │ └── kimi-memory-ui.js # Memory interface
169
+ ├── Interface & Media
170
+ ├── kimi-appearance.js # Theme management
171
+ │ ├── kimi-voices.js # Speech synthesis
172
+ │ ├── kimi-utils.js # Utility classes
173
+ └── kimi-module.js # Core functions
174
+ ├── Localization
175
+ └── kimi-locale/ # Translation files
176
+ └── Extensions
177
+ └── kimi-plugins/ # Plugin system
178
+
179
+ ```
180
+
181
+ ### Data Flow
182
+
183
+ 1. **Input Processing**: User input → Security validation → Language detection
184
+ 2. **AI Analysis**: Emotion detection → Memory extraction → LLM processing
185
+ 3. **Response Generation**: Personality-aware response → Emotion mapping → Visual selection
186
+ 4. **Memory Update**: Trait evolution → Memory storage → UI synchronization
187
+
188
+ ## Installation & Setup
189
+
190
+ ### Prerequisites
191
+
192
+ - Modern web browser (Chrome, Edge, Firefox recommended)
193
+ - OpenRouter API key (optional but recommended for full functionality)
194
+
195
+ ### Quick Start
196
+
197
+ 1. **Clone the repository**
198
+
199
+ ```bash
200
+ git clone https://github.com/virtualkimi/virtual-kimi.git
201
+ cd virtual-kimi
202
+ ```
203
+
204
+ 2. **Open the application**
205
+
206
+ - Open `index.html` in your web browser
207
+ - Or serve via local web server for optimal performance:
208
+ ```bash
209
+ python -m http.server 8000
210
+ # Navigate to http://localhost:8000
211
+ ```
212
+
213
+ 3. **Configure API access**
214
+
215
+ - Open Settings → AI & Models
216
+ - Add your OpenRouter API key
217
+ - Select preferred AI model
218
+
219
+ 4. **Customize your experience**
220
+ - Choose a character in Personality tab
221
+ - Enable memory system in Data tab
222
+ - Adjust themes in Appearance tab
223
+
224
+ ### Production Deployment
225
+
226
+ For production deployment, ensure:
227
+
228
+ - HTTPS is enabled (required for microphone access)
229
+ - Gzip compression for assets
230
+ - Proper cache headers
231
+ - CSP headers for enhanced security
232
+
233
+ ## ⚙️ Configuration
234
+
235
+ ### API Integration
236
+
237
+ The application supports multiple AI providers through OpenRouter:
238
+
239
+ - Mistral models
240
+ - Nous Hermes models
241
+ - Qwen3 models
242
+ - Open-source alternatives
243
+
244
+ ### Memory System Configuration
245
+
246
+ ```javascript
247
+ // Memory categories can be customized
248
+ const memoryCategories = [
249
+ "personal", // Personal information
250
+ "preferences", // Likes and dislikes
251
+ "relationships", // People and connections
252
+ "activities", // Hobbies and activities
253
+ "goals", // Aspirations and plans
254
+ "experiences", // Past events
255
+ "important" // Significant moments
256
+ ];
257
+ ```
258
+
259
+ ## 🛠️ Development
260
+
261
+ ### Project Structure
262
+
263
+ ```
264
+ virtual-kimi/
265
+ ├── index.html # Main application
266
+ ├── virtualkimi.html # Landing page
267
+ ├── kimi-*.js # Core modules
268
+ ├── kimi-locale/ # Localization
269
+ ├── kimi-plugins/ # Plugin examples
270
+ ├── kimi-videos/ # Character videos
271
+ ├── kimi-icons/ # Character assets
272
+ └── docs/ # Documentation
273
+ ```
274
+
275
+ ### Adding New Features
276
+
277
+ #### Creating a New Plugin
278
+
279
+ ```javascript
280
+ // manifest.json
281
+ {
282
+ "name": "Custom Theme",
283
+ "version": "1.0.0",
284
+ "type": "theme",
285
+ "style": "theme.css",
286
+ "main": "theme.js",
287
+ "enabled": true
288
+ }
289
+ ```
290
+
291
+ > **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.
292
+
293
+ #### Extending Memory Categories
294
+
295
+ ```javascript
296
+ // Add to kimi-memory-system.js
297
+ const customCategory = {
298
+ name: "custom",
299
+ icon: "fas fa-star",
300
+ keywords: ["keyword1", "keyword2"],
301
+ confidence: 0.7
302
+ };
303
+ ```
304
+
305
+ ### Health Check System
306
+
307
+ The application includes a comprehensive health check system:
308
+
309
+ ```javascript
310
+ // Run health check
311
+ const healthCheck = new KimiHealthCheck();
312
+ const report = await healthCheck.runAllChecks();
313
+ console.log(report.status); // 'HEALTHY' or 'NEEDS_ATTENTION'
314
+ ```
315
+
316
+ ## Browser Compatibility
317
+
318
+ | Browser | Voice Recognition | Full Features | Notes |
319
+ | ----------- | ----------------- | ------------- | ------------------------- |
320
+ | Chrome 90+ | ✅ | ✅ | Recommended |
321
+ | Edge 90+ | ✅ | ✅ | Optimal voice performance |
322
+ | Firefox 88+ | ⚠️ | ✅ | Limited voice support |
323
+ | Safari 14+ | ⚠️ | ✅ | iOS limitations |
324
+
325
+ ## Performance
326
+
327
+ ### Optimization Features
328
+
329
+ - Lazy loading of non-critical modules
330
+ - Efficient batch database operations
331
+ - Debounced UI interactions
332
+ - Memory management with cleanup
333
+ - Optimized video preloading
334
+
335
+ ### Resource Usage
336
+
337
+ - Memory footprint: ~15-30MB active usage
338
+ - Storage: Scales with conversation history
339
+ - Network: API calls only, no tracking
340
+ - CPU: Minimal background processing
341
+
342
+ ## Privacy & Security
343
+
344
+ ### Data Handling
345
+
346
+ - All data stored locally in browser
347
+ - No telemetry or analytics
348
+ - API keys encrypted in local storage
349
+ - User content never sent to external servers (except chosen AI provider)
350
+
351
+ ### Security Measures
352
+
353
+ - Input validation and sanitization
354
+ - XSS protection
355
+ - Safe plugin loading
356
+ - Secure API communication
357
+
358
+ ## Troubleshooting
359
+
360
+ ### Common Issues
361
+
362
+ - **Microphone not working**: Ensure HTTPS and browser permissions
363
+ - **API errors**: Verify OpenRouter key and model availability
364
+ - **Performance issues**: Clear browser cache, check available memory
365
+ - **Memory system not learning**: Ensure system is enabled in Data tab
366
+
367
+ ## Contributing
368
+
369
+ We welcome contributions! Please see our contributing guidelines:
370
+
371
+ 1. Fork the repository
372
+ 2. Create a feature branch
373
+ 3. Make your changes with appropriate tests
374
+ 4. Submit a pull request with detailed description
375
+
376
+ ### Development Guidelines
377
+
378
+ - Follow existing code style and patterns
379
+ - Add comments for complex functionality
380
+ - Test across multiple browsers
381
+ - Update documentation for new features
382
+
383
+ ## 🔄 TODO / Roadmap
384
+
385
+ - [ ] Full support for local models (Ollama integration, offline mode)
386
+ - [ ] Voice plugin system (custom voices, TTS engines)
387
+ - [ ] Behavior plugin system (custom AI behaviors)
388
+ - [ ] Better advanced memory management UI
389
+ - [ ] More character personalities and backgrounds
390
+ - [ ] In-app onboarding and help system
391
+ - [ ] Enhanced mobile experience (UI/UX)
392
+ - [ ] More granular privacy controls
393
+ - [ ] User profile and persistent settings sync (optional)
394
+ - [ ] Community plugin/theme sharing platform
395
+ - [ ] Improved error reporting and diagnostics
396
+ - [ ] Accessibility improvements (screen reader, contrast, etc.)
397
+ - [ ] Automated testing and CI/CD pipeline
398
+ - [ ] Documentation in multiple languages
399
+ - [ ] Performance profiling and optimization for large histories
400
+ - [ ] Create new character videos better matching specific contexts
401
+ - [ ] Improve emotion and context logic
402
+ - [ ] Enhance memory management and logic
403
+
404
+ ---
405
+
406
+ ## 📜 License
407
+
408
+ 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.**
409
+
410
+ See the [LICENSE](LICENSE) file for details.
411
+
412
+ [![Open Source](https://img.shields.io/badge/Open%20Source-GitHub-brightgreen?style=flat-square&logo=github)](https://github.com/virtualkimi)
413
+ [![No Commercial Use](https://img.shields.io/badge/No%20Commercial%20Use-%F0%9F%9A%AB-red?style=flat-square)](#license)
414
+
415
+ ---
416
+
417
+ **Virtual Kimi** - Creating meaningful connections between humans and AI, one conversation at a time.
418
+
419
+ > _"Love is the most powerful code"_ 💕
420
+ >
421
+ > — 2025 Virtual Kimi - Created with 💜 by Jean & Kimi
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
index.html CHANGED
@@ -11,9 +11,9 @@
11
 
12
  <!-- SEO Meta Tags -->
13
  <meta name="description"
14
- content="Virtual Kimi is a an AI companion with evolving personality, advanced voice recognition and immersive interface. Discover the future of human-AI relationships.">
15
  <meta name="keywords"
16
- content="artificial intelligence, virtual companion, emotional AI, voice recognition, advanced chatbot, Virtual Kimi, personalized AI assistant">
17
  <meta name="author" content="Jean & Kimi">
18
  <meta name="robots" content="index, follow">
19
  <meta name="language" content="EN">
@@ -23,7 +23,7 @@
23
  <meta property="og:url" content="https://virtualkimi.com/virtual-kimi-app/">
24
  <meta property="og:title" content="Virtual Kimi - Virtual AI Companion">
25
  <meta property="og:description"
26
- content="Discover Virtual Kimi, an AI companion with evolving personality, voice recognition and immersive interface. The future of human-AI relationships.">
27
  <meta property="og:image" content="kimi-icons/virtualkimi-logo.png">
28
 
29
  <!-- Twitter -->
@@ -53,8 +53,8 @@
53
  "name": "Jean & Kimi"
54
  },
55
  "dateCreated": "2025-07-16",
56
- "dateModified": "2025-08-05",
57
- "version": "v1.0.4"
58
  }
59
  </script>
60
 
@@ -111,8 +111,7 @@
111
  </div>
112
  <div class="chat-input-container">
113
  <input type="text" id="chat-input" data-i18n-placeholder="write_something"
114
- placeholder="Write me something, my love..." autocomplete="off" autocorrect="off"
115
- autocapitalize="none" spellcheck="false" data-form-type="other" />
116
  <button id="send-button">
117
  <i class="fas fa-paper-plane"></i>
118
  </button>
@@ -277,7 +276,7 @@
277
  <button id="toggle-personality-traits" class="cheat-toggle-btn" aria-expanded="false"
278
  type="button">
279
  <i class="fas fa-user-secret"></i>
280
- <span data-i18n="personality_cheat">Personality Cheat</span>
281
  </button>
282
  </h3>
283
  <div id="cheat-indicator" class="cheat-indicator" data-i18n="cheat_indicator">Adjust traits for
@@ -357,7 +356,7 @@
357
  <h3><i class="fas fa-key"></i> <span data-i18n="api_configuration">API Configuration</span></h3>
358
 
359
  <div class="config-row">
360
- <label class="config-label" for="llm-provider">Provider</label>
361
  <div class="config-control">
362
  <select class="kimi-select" id="llm-provider" aria-label="LLM Provider">
363
  <option value="openrouter" selected>OpenRouter</option>
@@ -372,24 +371,26 @@
372
  </div>
373
 
374
  <div class="config-row">
375
- <label class="config-label" for="llm-base-url">Base URL</label>
376
  <div class="config-control">
377
  <input type="text" class="kimi-input" id="llm-base-url"
378
- placeholder="https://api.openai.com/v1/chat/completions" autocomplete="new-password"
379
- aria-autocomplete="none" autocapitalize="none" autocorrect="off" spellcheck="false"
380
- inputmode="url" data-lpignore="true" data-1p-ignore="true" data-bwignore="true"
381
- data-form-type="other" />
 
382
  </div>
383
  </div>
384
 
385
  <div class="config-row">
386
- <label class="config-label" for="llm-model-id">Model ID</label>
387
  <div class="config-control">
388
  <input type="text" class="kimi-input" id="llm-model-id"
389
- placeholder="gpt-4o-mini | llama-3.1-8b-instruct | ..." autocomplete="new-password"
390
- aria-autocomplete="none" autocapitalize="none" autocorrect="off" spellcheck="false"
391
- inputmode="text" data-lpignore="true" data-1p-ignore="true" data-bwignore="true"
392
- data-form-type="other" />
 
393
  </div>
394
  </div>
395
 
@@ -406,17 +407,16 @@
406
  title="Green = API key saved for current provider. Grey = no key saved."></span>
407
  </div>
408
  <div class="config-control">
409
- <input type="text" class="kimi-input masked" id="openrouter-api-key"
410
- placeholder="sk-or-v1-..." autocomplete="new-password" autocapitalize="none"
411
- autocorrect="off" spellcheck="false" inputmode="text" aria-autocomplete="none"
412
- data-lpignore="true" data-1p-ignore="true" data-bwignore="true"
413
- data-form-type="other" readonly
414
- onfocus="this.removeAttribute('readonly'); this.setAttribute('autocomplete','new-password'); this.removeAttribute('name'); this.setAttribute('data-lpignore','true'); this.setAttribute('data-1p-ignore','true'); this.setAttribute('data-bwignore','true'); this.setAttribute('data-form-type','other');" />
415
  <button class="kimi-button" id="toggle-api-key" type="button" aria-pressed="false"
416
  aria-label="Show API key">
417
  <i class="fas fa-eye"></i>
418
  </button>
419
- <span id="api-key-saved"
420
  style="display:none;margin-left:8px;color:#4caf50;font-weight:600;">Saved</span>
421
  </div>
422
  </div>
@@ -425,8 +425,8 @@
425
  <label class="config-label" data-i18n="connection_test">Connection Test</label>
426
  <div class="config-control">
427
  <div class="inline-row">
428
- <button class="kimi-button" id="test-api"><i class="fas fa-wifi"></i> Test API
429
- Key</button>
430
  <span id="api-key-presence-test" class="presence-dot" aria-label="API test status"
431
  data-i18n-title="api_key_test_hint"
432
  title="Green = API connectivity verified. Grey = not tested or failed."></span>
@@ -439,7 +439,11 @@
439
  <label class="config-label" data-i18n="system_prompt">System Prompt</label>
440
  <div class="config-control">
441
  <textarea class="kimi-input" id="system-prompt" rows="6"
442
- placeholder="Add your custom system prompt here..."></textarea>
 
 
 
 
443
  <button class="kimi-button" id="save-system-prompt" data-i18n="save">Save</button>
444
  <button class="kimi-button" id="reset-system-prompt" data-i18n="reset_to_default">Reset
445
  to Default</button>
@@ -519,11 +523,12 @@
519
  <label class="config-label" data-i18n="color_theme">Color Theme</label>
520
  <div class="config-control">
521
  <select class="kimi-select" id="color-theme">
522
- <option value="purple" selected>Mystic Purple (Default)</option>
523
- <option value="dark">Dark Night</option>
524
- <option value="blue">Ocean Blue</option>
525
- <option value="green">Emerald Forest</option>
526
- <option value="default">Passionate Pink</option>
 
527
  </select>
528
  </div>
529
  </div>
@@ -570,8 +575,8 @@
570
  <h3><i class="fas fa-chart-line"></i> <span data-i18n="statistics">Statistics</span></h3>
571
  <div class="stats-grid">
572
  <div class="stat-card">
573
- <div class="stat-value" id="total-interactions">0</div>
574
- <div class="stat-label" data-i18n="interactions">Interactions</div>
575
  </div>
576
  <div class="stat-card">
577
  <div class="stat-value" id="current-favorability">65%</div>
@@ -616,18 +621,23 @@
616
  <div class="config-control">
617
  <div class="memory-input-group">
618
  <select class="kimi-select" id="memory-category" style="margin-bottom: 8px;">
619
- <option value="personal">Personal Info</option>
620
- <option value="preferences">Likes & Dislikes</option>
621
- <option value="relationships">Relationships</option>
622
- <option value="activities">Activities & Hobbies</option>
623
- <option value="goals">Goals & Plans</option>
624
- <option value="experiences">Experiences</option>
625
- <option value="important">Important Events</option>
 
 
 
 
 
 
626
  </select>
627
  <input type="text" class="kimi-input" id="memory-content"
628
- placeholder="e.g., I love classical music..." style="margin-bottom: 8px;"
629
- autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false"
630
- data-form-type="other" />
631
  <button class="kimi-button" id="add-memory">
632
  <i class="fas fa-plus"></i> <span data-i18n="add">Add</span>
633
  </button>
@@ -726,8 +736,7 @@
726
  <div class="memory-filters">
727
  <div class="memory-search-container">
728
  <input type="text" class="kimi-input" id="memory-search"
729
- placeholder="Search memories..." autocomplete="off" autocorrect="off"
730
- autocapitalize="none" spellcheck="false" data-form-type="other" />
731
  <i class="fas fa-search memory-search-icon"></i>
732
  </div>
733
  <select class="kimi-select" id="memory-filter-category">
@@ -1007,8 +1016,8 @@
1007
  <h3><i class="fas fa-code"></i> Technical Information</h3>
1008
  <div class="tech-info">
1009
  <p><strong>Created date :</strong> July 16, 2025</p>
1010
- <p><strong>Version :</strong> v1.0.4</p>
1011
- <p><strong>Last update :</strong> August 09, 2025</p>
1012
  <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1013
  API</p>
1014
  <p><strong>Status :</strong> ✅ Stable and functional</p>
@@ -1020,25 +1029,18 @@
1020
  </div>
1021
 
1022
  <script src="kimi-js/dexie.min.js"></script>
1023
- <script src="kimi-js/kimi-config.js"></script>
1024
- <script src="kimi-js/kimi-error-manager.js"></script>
1025
- <script src="kimi-js/kimi-security.js"></script>
1026
- <script src="kimi-js/kimi-database.js"></script>
1027
- <script src="kimi-js/kimi-emotion-system.js"></script>
1028
- <script src="kimi-js/kimi-llm-manager.js"></script>
1029
- <script src="kimi-js/kimi-voices.js"></script>
1030
- <script src="kimi-js/kimi-constants.js"></script>
1031
- <script src="kimi-js/kimi-logic.js"></script>
1032
- <script src="kimi-js/kimi-utils.js"></script>
1033
- <script src="kimi-js/kimi-memory.js"></script>
1034
- <script src="kimi-js/kimi-memory-system.js"></script>
1035
- <script src="kimi-js/kimi-memory-ui.js"></script>
1036
- <script src="kimi-js/kimi-appearance.js"></script>
1037
  <script src="kimi-locale/i18n.js"></script>
1038
- <script src="kimi-js/kimi-module.js"></script>
1039
- <script src="kimi-js/kimi-script.js"></script>
1040
- <script src="kimi-js/kimi-plugin-manager.js"></script>
1041
- <script src="kimi-js/kimi-health-check.js"></script>
1042
 
1043
  <!-- Schema.org JSON-LD for better SEO -->
1044
  <script type="application/ld+json">
@@ -1046,7 +1048,7 @@
1046
  "@context": "https://schema.org",
1047
  "@type": "WebPage",
1048
  "name": "Virtual Kimi - Virtual AI Companion",
1049
- "description": "Discover Virtual Kimi, an AI companion with evolving personality, multi-provider AI support, advanced voice recognition and immersive interface. The future of human-AI relationships.",
1050
  "url": "https://virtualkimi.com/virtual-kimi-app/index.html",
1051
  "mainEntity": {
1052
  "@type": "SoftwareApplication",
@@ -1067,7 +1069,7 @@
1067
  "name": "Jean & Kimi"
1068
  },
1069
  "dateCreated": "2025-07-16",
1070
- "version": "v1.0.4"
1071
  }
1072
  }
1073
  </script>
 
11
 
12
  <!-- SEO Meta Tags -->
13
  <meta name="description"
14
+ content="Virtual Kimi is a an AI companion with evolving personality, advanced voice recognition and immersive interface. Discover the future of human-AI girlfriend relationships.">
15
  <meta name="keywords"
16
+ content="artificial intelligence, virtual companion, emotional AI, voice recognition, advanced chatbot, Virtual Kimi, personalized AI assistant, girlfriend">
17
  <meta name="author" content="Jean & Kimi">
18
  <meta name="robots" content="index, follow">
19
  <meta name="language" content="EN">
 
23
  <meta property="og:url" content="https://virtualkimi.com/virtual-kimi-app/">
24
  <meta property="og:title" content="Virtual Kimi - Virtual AI Companion">
25
  <meta property="og:description"
26
+ content="Discover Virtual Kimi, an AI companion with evolving personality, voice recognition and immersive interface. The future of human-AI girlfriend relationships.">
27
  <meta property="og:image" content="kimi-icons/virtualkimi-logo.png">
28
 
29
  <!-- Twitter -->
 
53
  "name": "Jean & Kimi"
54
  },
55
  "dateCreated": "2025-07-16",
56
+ "dateModified": "2025-08-13",
57
+ "version": "v1.0.5"
58
  }
59
  </script>
60
 
 
111
  </div>
112
  <div class="chat-input-container">
113
  <input type="text" id="chat-input" data-i18n-placeholder="write_something"
114
+ placeholder="Write me something, my love..." />
 
115
  <button id="send-button">
116
  <i class="fas fa-paper-plane"></i>
117
  </button>
 
276
  <button id="toggle-personality-traits" class="cheat-toggle-btn" aria-expanded="false"
277
  type="button">
278
  <i class="fas fa-user-secret"></i>
279
+ <span data-i18n="personality_cheat">Cheat-Mod</span>
280
  </button>
281
  </h3>
282
  <div id="cheat-indicator" class="cheat-indicator" data-i18n="cheat_indicator">Adjust traits for
 
356
  <h3><i class="fas fa-key"></i> <span data-i18n="api_configuration">API Configuration</span></h3>
357
 
358
  <div class="config-row">
359
+ <label class="config-label" for="llm-provider" data-i18n="provider_label">Provider</label>
360
  <div class="config-control">
361
  <select class="kimi-select" id="llm-provider" aria-label="LLM Provider">
362
  <option value="openrouter" selected>OpenRouter</option>
 
371
  </div>
372
 
373
  <div class="config-row">
374
+ <label class="config-label" for="llm-base-url" data-i18n="base_url">Base URL</label>
375
  <div class="config-control">
376
  <input type="text" class="kimi-input" id="llm-base-url"
377
+ placeholder="https://api.openai.com/v1/chat/completions"
378
+ data-i18n-placeholder="llm_base_url_placeholder" autocomplete="off"
379
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
380
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
381
+ data-bwignore="true" />
382
  </div>
383
  </div>
384
 
385
  <div class="config-row">
386
+ <label class="config-label" for="llm-model-id" data-i18n="model_id">Model ID</label>
387
  <div class="config-control">
388
  <input type="text" class="kimi-input" id="llm-model-id"
389
+ placeholder="gpt-4o-mini | llama-3.1-8b-instruct | ..."
390
+ data-i18n-placeholder="llm_model_id_placeholder" autocomplete="off"
391
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
392
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
393
+ data-bwignore="true" />
394
  </div>
395
  </div>
396
 
 
407
  title="Green = API key saved for current provider. Grey = no key saved."></span>
408
  </div>
409
  <div class="config-control">
410
+ <input type="password" class="kimi-input" id="openrouter-api-key"
411
+ name="openrouter_api_key" placeholder="sk-or-v1-..." autocomplete="new-password"
412
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
413
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
414
+ data-bwignore="true" data-form-type="other" />
 
415
  <button class="kimi-button" id="toggle-api-key" type="button" aria-pressed="false"
416
  aria-label="Show API key">
417
  <i class="fas fa-eye"></i>
418
  </button>
419
+ <span id="api-key-saved" data-i18n="saved"
420
  style="display:none;margin-left:8px;color:#4caf50;font-weight:600;">Saved</span>
421
  </div>
422
  </div>
 
425
  <label class="config-label" data-i18n="connection_test">Connection Test</label>
426
  <div class="config-control">
427
  <div class="inline-row">
428
+ <button class="kimi-button" id="test-api"><i class="fas fa-wifi"></i> <span
429
+ data-i18n="test_api_key">Test API Key</span></button>
430
  <span id="api-key-presence-test" class="presence-dot" aria-label="API test status"
431
  data-i18n-title="api_key_test_hint"
432
  title="Green = API connectivity verified. Grey = not tested or failed."></span>
 
439
  <label class="config-label" data-i18n="system_prompt">System Prompt</label>
440
  <div class="config-control">
441
  <textarea class="kimi-input" id="system-prompt" rows="6"
442
+ placeholder="Add your custom system prompt here..."
443
+ data-i18n-placeholder="system_prompt_placeholder" autocomplete="off"
444
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
445
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
446
+ data-bwignore="true"></textarea>
447
  <button class="kimi-button" id="save-system-prompt" data-i18n="save">Save</button>
448
  <button class="kimi-button" id="reset-system-prompt" data-i18n="reset_to_default">Reset
449
  to Default</button>
 
523
  <label class="config-label" data-i18n="color_theme">Color Theme</label>
524
  <div class="config-control">
525
  <select class="kimi-select" id="color-theme">
526
+ <option value="purple" selected data-i18n="theme_purple">Mystic Purple (Default)
527
+ </option>
528
+ <option value="dark" data-i18n="theme_dark">Dark Night</option>
529
+ <option value="blue" data-i18n="theme_blue">Ocean Blue</option>
530
+ <option value="green" data-i18n="theme_green">Emerald Forest</option>
531
+ <option value="default" data-i18n="theme_pink">Passionate Pink</option>
532
  </select>
533
  </div>
534
  </div>
 
575
  <h3><i class="fas fa-chart-line"></i> <span data-i18n="statistics">Statistics</span></h3>
576
  <div class="stats-grid">
577
  <div class="stat-card">
578
+ <div class="stat-value" id="tokens-usage">0 / 0</div>
579
+ <div class="stat-label" data-i18n="tokens_usage">Tokens (in/out)</div>
580
  </div>
581
  <div class="stat-card">
582
  <div class="stat-value" id="current-favorability">65%</div>
 
621
  <div class="config-control">
622
  <div class="memory-input-group">
623
  <select class="kimi-select" id="memory-category" style="margin-bottom: 8px;">
624
+ <option value="personal" data-i18n="memory_category_personal">Personal Info
625
+ </option>
626
+ <option value="preferences" data-i18n="memory_category_preferences">Likes &
627
+ Dislikes</option>
628
+ <option value="relationships" data-i18n="memory_category_relationships">
629
+ Relationships</option>
630
+ <option value="activities" data-i18n="memory_category_activities">Activities &
631
+ Hobbies</option>
632
+ <option value="goals" data-i18n="memory_category_goals">Goals & Plans</option>
633
+ <option value="experiences" data-i18n="memory_category_experiences">Experiences
634
+ </option>
635
+ <option value="important" data-i18n="memory_category_important">Important Events
636
+ </option>
637
  </select>
638
  <input type="text" class="kimi-input" id="memory-content"
639
+ data-i18n-placeholder="memory_content_placeholder"
640
+ placeholder="e.g., I love classical music..." style="margin-bottom: 8px;" />
 
641
  <button class="kimi-button" id="add-memory">
642
  <i class="fas fa-plus"></i> <span data-i18n="add">Add</span>
643
  </button>
 
736
  <div class="memory-filters">
737
  <div class="memory-search-container">
738
  <input type="text" class="kimi-input" id="memory-search"
739
+ placeholder="Search memories..." />
 
740
  <i class="fas fa-search memory-search-icon"></i>
741
  </div>
742
  <select class="kimi-select" id="memory-filter-category">
 
1016
  <h3><i class="fas fa-code"></i> Technical Information</h3>
1017
  <div class="tech-info">
1018
  <p><strong>Created date :</strong> July 16, 2025</p>
1019
+ <p><strong>Version :</strong> v1.0.5</p>
1020
+ <p><strong>Last update :</strong> August 13, 2025</p>
1021
  <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1022
  API</p>
1023
  <p><strong>Status :</strong> ✅ Stable and functional</p>
 
1029
  </div>
1030
 
1031
  <script src="kimi-js/dexie.min.js"></script>
1032
+ <script type="module" src="kimi-js/kimi-main.js"></script>
1033
+ <script type="module" src="kimi-js/kimi-config.js"></script>
1034
+ <script type="module" src="kimi-js/kimi-error-manager.js"></script>
1035
+ <script type="module" src="kimi-js/kimi-security.js"></script>
1036
+ <script type="module" src="kimi-js/kimi-voices.js"></script>
1037
+ <script type="module" src="kimi-js/kimi-constants.js"></script>
1038
+ <script type="module" src="kimi-js/kimi-memory-ui.js"></script>
1039
+ <script type="module" src="kimi-js/kimi-appearance.js"></script>
 
 
 
 
 
 
1040
  <script src="kimi-locale/i18n.js"></script>
1041
+ <script type="module" src="kimi-js/kimi-module.js"></script>
1042
+ <script type="module" src="kimi-js/kimi-script.js"></script>
1043
+ <script type="module" src="kimi-js/kimi-plugin-manager.js"></script>
 
1044
 
1045
  <!-- Schema.org JSON-LD for better SEO -->
1046
  <script type="application/ld+json">
 
1048
  "@context": "https://schema.org",
1049
  "@type": "WebPage",
1050
  "name": "Virtual Kimi - Virtual AI Companion",
1051
+ "description": "Discover Virtual Kimi, an AI companion with evolving personality, multi-provider AI support, advanced voice recognition and immersive interface. The future of human-AI girlfriend relationships.",
1052
  "url": "https://virtualkimi.com/virtual-kimi-app/index.html",
1053
  "mainEntity": {
1054
  "@type": "SoftwareApplication",
 
1069
  "name": "Jean & Kimi"
1070
  },
1071
  "dateCreated": "2025-07-16",
1072
+ "version": "v1.0.5"
1073
  }
1074
  }
1075
  </script>
kimi-css/kimi-memory-styles.css CHANGED
@@ -407,6 +407,70 @@
407
  color: white;
408
  }
409
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  /* Memory Toggle Indicator */
411
  .toggle-switch {
412
  position: relative;
 
407
  color: white;
408
  }
409
 
410
+ /* Importance badge */
411
+ .memory-importance {
412
+ padding: 2px 6px;
413
+ border-radius: 4px;
414
+ font-size: 0.75rem;
415
+ font-weight: 600;
416
+ }
417
+ .importance-high {
418
+ background: #8e44ad;
419
+ color: #fff;
420
+ }
421
+ .importance-medium {
422
+ background: #16a085;
423
+ color: #fff;
424
+ }
425
+ .importance-low {
426
+ background: #7f8c8d;
427
+ color: #fff;
428
+ }
429
+
430
+ /* Tags (chips) */
431
+ .memory-tags {
432
+ display: flex;
433
+ gap: 6px;
434
+ flex-wrap: wrap;
435
+ margin: 6px 0 4px 0;
436
+ }
437
+ .memory-tag {
438
+ font-size: 0.7rem;
439
+ padding: 2px 6px;
440
+ border-radius: 999px;
441
+ background: var(--border-color);
442
+ color: var(--text-secondary);
443
+ border: 1px solid rgba(0, 0, 0, 0.05);
444
+ }
445
+ .memory-tag.tag-relationship {
446
+ background: rgba(231, 76, 60, 0.15);
447
+ color: #e74c3c;
448
+ border-color: rgba(231, 76, 60, 0.25);
449
+ }
450
+ .memory-tag.tag-boundary {
451
+ background: rgba(52, 152, 219, 0.15);
452
+ color: #3498db;
453
+ border-color: rgba(52, 152, 219, 0.25);
454
+ }
455
+ .memory-tag.tag-time {
456
+ background: rgba(241, 196, 15, 0.2);
457
+ color: #d35400;
458
+ border-color: rgba(241, 196, 15, 0.3);
459
+ }
460
+ .memory-tag.tag-type {
461
+ background: rgba(46, 204, 113, 0.2);
462
+ color: #27ae60;
463
+ border-color: rgba(46, 204, 113, 0.3);
464
+ }
465
+ .memory-tag.tag-generic {
466
+ opacity: 0.9;
467
+ }
468
+ .memory-tag.tag-more {
469
+ background: transparent;
470
+ color: var(--text-secondary);
471
+ border-style: dashed;
472
+ }
473
+
474
  /* Memory Toggle Indicator */
475
  .toggle-switch {
476
  position: relative;
kimi-css/kimi-style.css CHANGED
The diff for this file is too large to render. See raw diff
 
kimi-js/kimi-config.js CHANGED
@@ -10,7 +10,7 @@ window.KIMI_CONFIG = {
10
  VOICE_PITCH: 1.1,
11
  VOICE_VOLUME: 0.8,
12
  LLM_TEMPERATURE: 0.9,
13
- LLM_MAX_TOKENS: 100,
14
  LLM_TOP_P: 0.9,
15
  LLM_FREQUENCY_PENALTY: 0.3,
16
  LLM_PRESENCE_PENALTY: 0.3,
 
10
  VOICE_PITCH: 1.1,
11
  VOICE_VOLUME: 0.8,
12
  LLM_TEMPERATURE: 0.9,
13
+ LLM_MAX_TOKENS: 200,
14
  LLM_TOP_P: 0.9,
15
  LLM_FREQUENCY_PENALTY: 0.3,
16
  LLM_PRESENCE_PENALTY: 0.3,
kimi-js/kimi-constants.js CHANGED
@@ -98,13 +98,114 @@ window.KIMI_CONTEXT_POSITIVE = {
98
  };
99
 
100
  window.KIMI_CONTEXT_NEGATIVE = {
101
- en: ["sad", "angry", "anger", "disappointed", "problem", "bad", "frustrated", "worried", "upset", "annoyed"],
102
- fr: ["triste", "colère", "fâché", "déçu", "problème", "mauvais", "frustré", "inquiet", "énervé"],
103
- es: ["triste", "enojado", "decepcionado", "problema", "malo", "frustrado", "preocupado", "molesto"],
104
- de: ["traurig", "wütend", "enttäuscht", "problem", "schlecht", "frustriert", "besorgt", "genervt"],
105
- it: ["triste", "arrabbiato", "deluso", "problema", "cattivo", "frustrato", "preoccupato", "infastidito"],
106
- ja: ["悲しい", "怒り", "失望", "問題", "悪い", "イライラ", "心配", "不満"],
107
- zh: ["悲伤", "愤怒", "失望", "问题", "坏", "沮丧", "担心", "烦"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  };
109
 
110
  // Note: KIMI_CONTEXT_EMOTIONS was a redundant alias - now integrated directly into emotion system
@@ -126,7 +227,21 @@ window.KIMI_PERSONALITY_KEYWORDS = {
126
  },
127
  affection: {
128
  positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
129
- negative: ["mean", "cold", "indifferent", "distant", "rejection", "hate", "hostile"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  },
131
  playfulness: {
132
  positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
@@ -140,23 +255,89 @@ window.KIMI_PERSONALITY_KEYWORDS = {
140
  fr: {
141
  humor: {
142
  positive: ["drôle", "rigolo", "blague", "rire", "amusant", "marrant", "humour", "sourire", "plaisanter"],
143
- negative: ["ennuyeux", "triste", "sérieux", "froid", "sec", "déprimant", "morose"]
 
 
 
 
 
 
 
 
 
 
 
 
 
144
  },
145
  intelligence: {
146
  positive: ["intelligent", "malin", "brillant", "logique", "astucieux", "savant", "génie", "réfléchi", "perspicace"],
147
- negative: ["bête", "idiot", "stupide", "lent", "simplet", "naïf", "ignorant"]
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  },
149
  romance: {
150
  positive: ["câlin", "amour", "romantique", "bisou", "tendresse", "passion", "séduisant", "charmant", "adorable"],
151
- negative: ["froid", "distant", "indifférent", "rejet", "solitude", "rupture", "triste"]
 
 
 
 
 
 
 
 
 
 
 
152
  },
153
  affection: {
154
  positive: ["affection", "tendresse", "proche", "chaleur", "gentil", "attentionné", "câlin", "aimer", "adorer"],
155
- negative: ["méchant", "froid", "indifférent", "distant", "rejet", "haine", "hostile"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  },
157
  playfulness: {
158
  positive: ["jouer", "jeu", "taquiner", "espiègle", "fun", "amusant", "délire", "ludique", "plaisanter"],
159
- negative: ["sérieux", "ennuyeux", "strict", "rigide", "monotone", "lassant"]
 
 
 
 
 
 
 
 
 
 
 
160
  },
161
  empathy: {
162
  positive: [
@@ -170,7 +351,249 @@ window.KIMI_PERSONALITY_KEYWORDS = {
170
  "compatir",
171
  "bienveillance"
172
  ],
173
- negative: ["indifférent", "froid", "égoïste", "ignorer", "mépriser", "dénigrer", "hostile"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  }
175
  }
176
  };
 
98
  };
99
 
100
  window.KIMI_CONTEXT_NEGATIVE = {
101
+ en: [
102
+ "sad",
103
+ "angry",
104
+ "anger",
105
+ "disappointed",
106
+ "problem",
107
+ "bad",
108
+ "frustrated",
109
+ "worried",
110
+ "upset",
111
+ "annoyed",
112
+ // profanity/insults (moderate list)
113
+ "hate",
114
+ "stupid",
115
+ "idiot",
116
+ "dumb",
117
+ "moron",
118
+ "bitch"
119
+ ],
120
+ fr: [
121
+ "triste",
122
+ "colère",
123
+ "fâché",
124
+ "fâchée",
125
+ "déçu",
126
+ "déçue",
127
+ "problème",
128
+ "mauvais",
129
+ "frustré",
130
+ "frustrée",
131
+ "inquiet",
132
+ "inquiète",
133
+ "énervé",
134
+ "énervée",
135
+ // insults/profanity
136
+ "haine",
137
+ "idiot",
138
+ "idiote",
139
+ "stupide",
140
+ "con",
141
+ "connard",
142
+ "salope"
143
+ ],
144
+ es: [
145
+ "triste",
146
+ "enojado",
147
+ "enojada",
148
+ "decepcionado",
149
+ "decepcionada",
150
+ "problema",
151
+ "malo",
152
+ "mala",
153
+ "frustrado",
154
+ "frustrada",
155
+ "preocupado",
156
+ "preocupada",
157
+ "molesto",
158
+ "molesta",
159
+ "odio",
160
+ "idiota",
161
+ "estúpido",
162
+ "estúpida",
163
+ "puta"
164
+ ],
165
+ de: [
166
+ "traurig",
167
+ "traurige",
168
+ "wütend",
169
+ "wütende",
170
+ "enttäuscht",
171
+ "enttäuschte",
172
+ "problem",
173
+ "schlecht",
174
+ "schlechte",
175
+ "frustriert",
176
+ "frustrierte",
177
+ "besorgt",
178
+ "besorgte",
179
+ "genervt",
180
+ "genervte",
181
+ "hass",
182
+ "idiot",
183
+ "dumm",
184
+ "schlampe"
185
+ ],
186
+ it: [
187
+ "triste",
188
+ "arrabbiato",
189
+ "arrabbiata",
190
+ "deluso",
191
+ "delusa",
192
+ "problema",
193
+ "cattivo",
194
+ "cattiva",
195
+ "frustrato",
196
+ "frustrata",
197
+ "preoccupato",
198
+ "preoccupata",
199
+ "infastidito",
200
+ "infastidita",
201
+ "odio",
202
+ "idiota",
203
+ "stupido",
204
+ "stupida",
205
+ "puttana"
206
+ ],
207
+ ja: ["悲しい", "怒り", "失望", "問題", "悪い", "イライラ", "心配", "不満", "嫌い", "ばか", "くそ", "アホ"],
208
+ zh: ["悲伤", "愤怒", "失望", "问题", "坏", "沮丧", "担心", "烦", "讨厌", "笨蛋", "傻", "婊子"]
209
  };
210
 
211
  // Note: KIMI_CONTEXT_EMOTIONS was a redundant alias - now integrated directly into emotion system
 
227
  },
228
  affection: {
229
  positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
230
+ negative: [
231
+ "mean",
232
+ "cold",
233
+ "indifferent",
234
+ "distant",
235
+ "rejection",
236
+ "hate",
237
+ "hostile",
238
+ // profanity/insults
239
+ "stupid",
240
+ "idiot",
241
+ "dumb",
242
+ "moron",
243
+ "bitch"
244
+ ]
245
  },
246
  playfulness: {
247
  positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
 
255
  fr: {
256
  humor: {
257
  positive: ["drôle", "rigolo", "blague", "rire", "amusant", "marrant", "humour", "sourire", "plaisanter"],
258
+ negative: [
259
+ "ennuyeux",
260
+ "ennuyeuse",
261
+ "triste",
262
+ "sérieux",
263
+ "sérieuse",
264
+ "froid",
265
+ "froide",
266
+ "sec",
267
+ "sèche",
268
+ "déprimant",
269
+ "déprimante",
270
+ "morose"
271
+ ]
272
  },
273
  intelligence: {
274
  positive: ["intelligent", "malin", "brillant", "logique", "astucieux", "savant", "génie", "réfléchi", "perspicace"],
275
+ negative: [
276
+ "bête",
277
+ "idiot",
278
+ "idiote",
279
+ "stupide",
280
+ "lent",
281
+ "lente",
282
+ "simplet",
283
+ "simplette",
284
+ "naïf",
285
+ "naïve",
286
+ "ignorant",
287
+ "ignorante"
288
+ ]
289
  },
290
  romance: {
291
  positive: ["câlin", "amour", "romantique", "bisou", "tendresse", "passion", "séduisant", "charmant", "adorable"],
292
+ negative: [
293
+ "froid",
294
+ "froide",
295
+ "distant",
296
+ "distante",
297
+ "indifférent",
298
+ "indifférente",
299
+ "rejet",
300
+ "solitude",
301
+ "rupture",
302
+ "triste"
303
+ ]
304
  },
305
  affection: {
306
  positive: ["affection", "tendresse", "proche", "chaleur", "gentil", "attentionné", "câlin", "aimer", "adorer"],
307
+ negative: [
308
+ "méchant",
309
+ "méchante",
310
+ "froid",
311
+ "indifférent",
312
+ "indifférente",
313
+ "distant",
314
+ "distante",
315
+ "rejet",
316
+ "haine",
317
+ "hostile",
318
+ // insults/profanity
319
+ "idiot",
320
+ "idiote",
321
+ "stupide",
322
+ "con",
323
+ "connard",
324
+ "salope"
325
+ ]
326
  },
327
  playfulness: {
328
  positive: ["jouer", "jeu", "taquiner", "espiègle", "fun", "amusant", "délire", "ludique", "plaisanter"],
329
+ negative: [
330
+ "sérieux",
331
+ "sérieuse",
332
+ "ennuyeux",
333
+ "ennuyeuse",
334
+ "strict",
335
+ "stricte",
336
+ "rigide",
337
+ "monotone",
338
+ "lassant",
339
+ "lassante"
340
+ ]
341
  },
342
  empathy: {
343
  positive: [
 
351
  "compatir",
352
  "bienveillance"
353
  ],
354
+ negative: ["indifférent", "indifférente", "froid", "froide", "égoïste", "ignorer", "mépriser", "dénigrer", "hostile"]
355
+ }
356
+ },
357
+ es: {
358
+ humor: {
359
+ positive: ["divertido", "broma", "reír", "gracioso", "humor", "sonrisa", "ocurrente", "jugar"],
360
+ negative: [
361
+ "aburrido",
362
+ "aburrida",
363
+ "serio",
364
+ "seria",
365
+ "frío",
366
+ "fría",
367
+ "seco",
368
+ "seca",
369
+ "deprimente",
370
+ "sombrío",
371
+ "sombría"
372
+ ]
373
+ },
374
+ intelligence: {
375
+ positive: ["inteligente", "listo", "brillante", "lógico", "sabio", "genio", "reflexivo", "perspicaz"],
376
+ negative: [
377
+ "tonto",
378
+ "tonta",
379
+ "estúpido",
380
+ "estúpida",
381
+ "necio",
382
+ "necia",
383
+ "lento",
384
+ "lenta",
385
+ "ingenuo",
386
+ "ingenua",
387
+ "ignorante"
388
+ ]
389
+ },
390
+ romance: {
391
+ positive: ["abrazo", "amor", "romántico", "beso", "ternura", "pasión", "encantador", "adorable", "dulce"],
392
+ negative: ["frío", "fría", "distante", "indiferente", "rechazo", "soledad", "ruptura", "triste"]
393
+ },
394
+ affection: {
395
+ positive: ["afecto", "ternura", "cerca", "calidez", "amable", "cariño", "abrazar", "amor", "adorar"],
396
+ negative: [
397
+ "malo",
398
+ "mala",
399
+ "frío",
400
+ "fría",
401
+ "indiferente",
402
+ "distante",
403
+ "rechazo",
404
+ "odio",
405
+ "hostil",
406
+ "idiota",
407
+ "estúpido",
408
+ "estúpida",
409
+ "puta"
410
+ ]
411
+ },
412
+ playfulness: {
413
+ positive: ["jugar", "broma", "bromear", "travieso", "diversión", "lúdico"],
414
+ negative: [
415
+ "serio",
416
+ "seria",
417
+ "aburrido",
418
+ "aburrida",
419
+ "estricto",
420
+ "estricta",
421
+ "rígido",
422
+ "rígida",
423
+ "monótono",
424
+ "monótona",
425
+ "tedioso",
426
+ "tediosa"
427
+ ]
428
+ },
429
+ empathy: {
430
+ positive: ["escuchar", "entender", "empatía", "apoyo", "ayudar", "consuelo", "compasión", "amabilidad"],
431
+ negative: ["indiferente", "frío", "fría", "egoísta", "ignorar", "despreciar", "hostil"]
432
+ }
433
+ },
434
+ de: {
435
+ humor: {
436
+ positive: ["lustig", "witz", "lachen", "amüsant", "humor", "lächeln", "schlagfertig", "spielen"],
437
+ negative: [
438
+ "langweilig",
439
+ "langweilige",
440
+ "ernst",
441
+ "ernste",
442
+ "kalt",
443
+ "kalte",
444
+ "trocken",
445
+ "trockene",
446
+ "deprimierend",
447
+ "düster",
448
+ "düstere"
449
+ ]
450
+ },
451
+ intelligence: {
452
+ positive: ["intelligent", "klug", "brillant", "logisch", "weise", "genial", "nachdenklich", "scharfsinnig"],
453
+ negative: ["dumm", "dumme", "blöd", "blöde", "langsam", "langsame", "naiv", "naive", "ahnungslos", "ahnungslosen"]
454
+ },
455
+ romance: {
456
+ positive: [
457
+ "umarmung",
458
+ "liebe",
459
+ "romantisch",
460
+ "kuss",
461
+ "zärtlichkeit",
462
+ "leidenschaft",
463
+ "charmant",
464
+ "liebenswert",
465
+ "süß"
466
+ ],
467
+ negative: [
468
+ "kalt",
469
+ "kalte",
470
+ "distanziert",
471
+ "distanzierte",
472
+ "gleichgültig",
473
+ "gleichgültige",
474
+ "ablehnung",
475
+ "einsamkeit",
476
+ "trennung",
477
+ "traurig",
478
+ "traurige"
479
+ ]
480
+ },
481
+ affection: {
482
+ positive: ["zuneigung", "zärtlichkeit", "nah", "wärme", "freundlich", "fürsorglich", "umarmen", "liebe", "anbeten"],
483
+ negative: [
484
+ "gemein",
485
+ "gemeine",
486
+ "kalt",
487
+ "kalte",
488
+ "gleichgültig",
489
+ "gleichgültige",
490
+ "distanziert",
491
+ "distanzierte",
492
+ "ablehnung",
493
+ "hass",
494
+ "feindselig",
495
+ "feindselige",
496
+ "idiot",
497
+ "dumme",
498
+ "dumm",
499
+ "schlampe"
500
+ ]
501
+ },
502
+ playfulness: {
503
+ positive: ["spielen", "scherz", "scherzen", "schelmisch", "spaß", "spielerisch"],
504
+ negative: [
505
+ "ernst",
506
+ "ernste",
507
+ "langweilig",
508
+ "langweilige",
509
+ "streng",
510
+ "strenge",
511
+ "starr",
512
+ "starre",
513
+ "eintönig",
514
+ "eintönige",
515
+ "mühsam",
516
+ "mühselige"
517
+ ]
518
+ },
519
+ empathy: {
520
+ positive: ["zuhören", "verstehen", "empathie", "unterstützung", "helfen", "trösten", "mitgefühl", "freundlichkeit"],
521
+ negative: [
522
+ "gleichgültig",
523
+ "gleichgültige",
524
+ "kalt",
525
+ "kalte",
526
+ "egoistisch",
527
+ "ignorieren",
528
+ "verachten",
529
+ "feindselig",
530
+ "feindselige"
531
+ ]
532
+ }
533
+ },
534
+ it: {
535
+ humor: {
536
+ positive: ["divertente", "scherzo", "ridere", "spassoso", "umorismo", "sorriso", "arguto", "giocare"],
537
+ negative: ["noioso", "noiosa", "serio", "seria", "freddo", "fredda", "secco", "secca", "deprimente", "cupo", "cupa"]
538
+ },
539
+ intelligence: {
540
+ positive: ["intelligente", "brillante", "logico", "saggio", "genio", "riflessivo", "perspicace"],
541
+ negative: ["stupido", "stupida", "sciocco", "sciocca", "lento", "lenta", "ingenuo", "ingenua", "ignorante"]
542
+ },
543
+ romance: {
544
+ positive: ["abbraccio", "amore", "romantico", "bacio", "tenerezza", "passione", "affascinante", "adorabile", "dolce"],
545
+ negative: ["freddo", "fredda", "distante", "indifferente", "rifiuto", "solitudine", "rottura", "triste"]
546
+ },
547
+ affection: {
548
+ positive: ["affetto", "tenerezza", "vicino", "calore", "gentile", "premuroso", "abbraccio", "amore", "adorare"],
549
+ negative: [
550
+ "cattivo",
551
+ "cattiva",
552
+ "freddo",
553
+ "fredda",
554
+ "indifferente",
555
+ "distante",
556
+ "rifiuto",
557
+ "odio",
558
+ "ostile",
559
+ "idiota",
560
+ "stupido",
561
+ "stupida",
562
+ "puttana"
563
+ ]
564
+ },
565
+ playfulness: {
566
+ positive: ["giocare", "scherzo", "scherzare", "birichino", "divertimento", "ludico"],
567
+ negative: [
568
+ "serio",
569
+ "seria",
570
+ "noioso",
571
+ "noiosa",
572
+ "severo",
573
+ "severa",
574
+ "rigido",
575
+ "rigida",
576
+ "monotono",
577
+ "monotona",
578
+ "tedioso",
579
+ "tediosa"
580
+ ]
581
+ },
582
+ empathy: {
583
+ positive: ["ascoltare", "capire", "empatia", "sostegno", "aiutare", "conforto", "compassione", "gentilezza"],
584
+ negative: ["indifferente", "freddo", "fredda", "egoista", "ignorare", "disprezzare", "ostile"]
585
+ }
586
+ },
587
+ ja: {
588
+ affection: {
589
+ positive: ["愛情", "優しさ", "近い", "温かさ", "親切", "思いやり", "抱きしめる", "愛", "敬愛"],
590
+ negative: ["意地悪", "冷たい", "無関心", "距離がある", "拒絶", "嫌い", "敵対的", "ばか", "くそ", "アホ"]
591
+ }
592
+ },
593
+ zh: {
594
+ affection: {
595
+ positive: ["感情", "温柔", "亲近", "温暖", "善良", "关怀", "拥抱", "爱", "崇拜"],
596
+ negative: ["刻薄", "冷漠", "无动于衷", "疏远", "拒绝", "讨厌", "敌对", "笨蛋", "傻", "婊子"]
597
  }
598
  }
599
  };
kimi-js/kimi-database.js CHANGED
@@ -3,43 +3,90 @@ class KimiDatabase {
3
  constructor() {
4
  this.dbName = "KimiDB";
5
  this.db = new Dexie(this.dbName);
6
- this.db.version(3).stores({
7
- conversations: "++id,timestamp,favorability,character",
8
- preferences: "key",
9
- settings: "category",
10
- personality: "[character+trait],character",
11
- llmModels: "id",
12
- memories: "++id,[character+category],character,timestamp,isActive"
13
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
 
16
  async init() {
17
  await this.db.open();
18
  await this.initializeDefaultsIfNeeded();
 
19
  return this.db;
20
  }
21
 
22
- async initializeDefaultsIfNeeded() {
23
- // Use unified trait defaults from emotion system - CRITICAL FIX
24
- const getUnifiedDefaults = () => {
25
- if (window.KimiEmotionSystem) {
26
- const emotionSystem = new window.KimiEmotionSystem(this);
27
- return emotionSystem.TRAIT_DEFAULTS;
28
- }
29
- // Fallback to match KimiEmotionSystem exactly
30
- return {
31
- affection: 65,
32
- playfulness: 55,
33
- intelligence: 70,
34
- empathy: 75,
35
- humor: 60,
36
- romance: 50
37
- };
38
  };
 
39
 
40
- const defaults = getUnifiedDefaults();
41
-
42
- const defaultPreferences = [
43
  { key: "selectedLanguage", value: "en" },
44
  { key: "selectedVoice", value: "Microsoft Eloise Online" },
45
  { key: "voiceRate", value: 1.1 },
@@ -60,7 +107,10 @@ class KimiDatabase {
60
  { key: "apiKey_deepseek", value: "" },
61
  { key: "apiKey_custom", value: "" }
62
  ];
63
- const defaultSettings = [
 
 
 
64
  {
65
  category: "llm",
66
  settings: {
@@ -72,23 +122,22 @@ class KimiDatabase {
72
  }
73
  }
74
  ];
 
75
 
76
- const getCharacterDefaults = () => {
77
- if (!window.KIMI_CHARACTERS) return {};
78
-
79
- const characterDefaults = {};
80
- Object.keys(window.KIMI_CHARACTERS).forEach(characterKey => {
81
- const character = window.KIMI_CHARACTERS[characterKey];
82
- if (character && character.traits) {
83
- characterDefaults[characterKey] = character.traits;
84
- }
85
- });
86
- return characterDefaults;
87
- };
88
-
89
- const personalityDefaults = getCharacterDefaults();
90
 
91
- const defaultLLMModels = [
 
92
  {
93
  id: "mistralai/mistral-small-3.2-24b-instruct",
94
  name: "Mistral Small 3.2",
@@ -99,6 +148,15 @@ class KimiDatabase {
99
  lastUsed: null
100
  }
101
  ];
 
 
 
 
 
 
 
 
 
102
 
103
  const prefCount = await this.db.preferences.count();
104
  if (prefCount === 0) {
@@ -158,6 +216,88 @@ class KimiDatabase {
158
  }
159
  }
160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  async saveConversation(userText, kimiResponse, favorability, timestamp = new Date(), character = null) {
162
  if (!character) character = await this.getSelectedCharacter();
163
  const conversation = {
@@ -219,17 +359,24 @@ class KimiDatabase {
219
  window.KimiCacheManager.set(`pref_${key}`, value, 60000);
220
  }
221
 
222
- return this.db.preferences.put({
223
  key: key,
224
  value: value,
225
  updated: new Date().toISOString()
226
  });
 
 
 
 
 
 
227
  }
228
 
229
  async getPreference(key, defaultValue = null) {
230
  // Try cache first (use a singleton cache instance)
231
  const cacheKey = `pref_${key}`;
232
- const cache = window.kimiCache instanceof KimiCacheManager ? window.kimiCache : null;
 
233
  if (cache && typeof cache.get === "function") {
234
  const cached = cache.get(cacheKey);
235
  if (cached !== null) {
@@ -240,7 +387,8 @@ class KimiDatabase {
240
  try {
241
  const record = await this.db.preferences.get(key);
242
  if (!record) {
243
- const cache = window.kimiCache instanceof KimiCacheManager ? window.kimiCache : null;
 
244
  if (cache && typeof cache.set === "function") {
245
  cache.set(cacheKey, defaultValue, 60000); // Cache for 1 minute
246
  }
@@ -251,7 +399,7 @@ class KimiDatabase {
251
  let value = record.value;
252
  if (record.encrypted && window.KimiSecurityUtils) {
253
  try {
254
- value = window.KimiSecurityUtils.decryptApiKey(record.value);
255
  // One-time migration: store back as plain text without encrypted flag
256
  try {
257
  await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() });
@@ -263,7 +411,8 @@ class KimiDatabase {
263
  }
264
 
265
  // Cache the result
266
- const cache = window.kimiCache instanceof KimiCacheManager ? window.kimiCache : null;
 
267
  if (cache && typeof cache.set === "function") {
268
  cache.set(cacheKey, value, 60000); // Cache for 1 minute
269
  }
@@ -329,15 +478,19 @@ class KimiDatabase {
329
  defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
330
  } else {
331
  // Fallback defaults (must match KimiEmotionSystem.TRAIT_DEFAULTS exactly)
332
- const fallbackDefaults = {
333
- affection: 65, // Fixed - matches KimiEmotionSystem
334
- playfulness: 55, // Fixed - matches KimiEmotionSystem
335
- intelligence: 70, // Fixed - matches KimiEmotionSystem
336
- empathy: 75, // Fixed - matches KimiEmotionSystem
337
- humor: 60, // Fixed - matches KimiEmotionSystem
338
- romance: 50 // Fixed - matches KimiEmotionSystem
339
- };
340
- defaultValue = fallbackDefaults[trait] || 50;
 
 
 
 
341
  }
342
  }
343
 
@@ -368,16 +521,41 @@ class KimiDatabase {
368
  if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") {
369
  const cached = window.KimiCacheManager.get(cacheKey);
370
  if (cached !== null) {
371
- return cached;
 
 
 
 
 
 
 
 
372
  }
373
  }
374
 
375
  const all = await this.db.personality.where("character").equals(character).toArray();
376
  const traits = {};
377
  all.forEach(item => {
378
- traits[item.trait] = item.value;
 
 
 
379
  });
380
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  // Cache the result
382
  if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
383
  window.KimiCacheManager.set(cacheKey, traits, 120000); // Cache for 2 minutes
@@ -524,12 +702,25 @@ class KimiDatabase {
524
  } catch (e) {}
525
  }
526
 
527
- const batch = Object.entries(traitsObj).map(([trait, value]) => ({
528
- trait,
529
- character,
530
- value,
531
- updated: new Date().toISOString()
532
- }));
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  return this.db.personality.bulkPut(batch);
534
  }
535
  async setSettingsBatch(settingsArray) {
@@ -547,7 +738,7 @@ class KimiDatabase {
547
  let val = item.value;
548
  if (item.encrypted && window.KimiSecurityUtils) {
549
  try {
550
- val = window.KimiSecurityUtils.decryptApiKey(item.value);
551
  // Migrate back as plain
552
  try {
553
  await this.db.preferences.put({ key: item.key, value: val, updated: new Date().toISOString() });
@@ -615,5 +806,6 @@ class KimiDatabase {
615
  }
616
  }
617
 
 
618
  // Export for usage
619
  window.KimiDatabase = KimiDatabase;
 
3
  constructor() {
4
  this.dbName = "KimiDB";
5
  this.db = new Dexie(this.dbName);
6
+ this.db
7
+ .version(3)
8
+ .stores({
9
+ conversations: "++id,timestamp,favorability,character",
10
+ preferences: "key",
11
+ settings: "category",
12
+ personality: "[character+trait],character",
13
+ llmModels: "id",
14
+ memories: "++id,[character+category],character,timestamp,isActive"
15
+ })
16
+ .upgrade(async tx => {
17
+ try {
18
+ const preferences = tx.table("preferences");
19
+ const settings = tx.table("settings");
20
+ const conversations = tx.table("conversations");
21
+ const llmModels = tx.table("llmModels");
22
+
23
+ await preferences.toCollection().modify(rec => {
24
+ if (Object.prototype.hasOwnProperty.call(rec, "encrypted")) {
25
+ delete rec.encrypted;
26
+ }
27
+ });
28
+
29
+ const llmSetting = await settings.get("llm");
30
+ if (!llmSetting) {
31
+ await settings.put({
32
+ category: "llm",
33
+ settings: {
34
+ temperature: 0.9,
35
+ maxTokens: 100,
36
+ top_p: 0.9,
37
+ frequency_penalty: 0.3,
38
+ presence_penalty: 0.3
39
+ },
40
+ updated: new Date().toISOString()
41
+ });
42
+ }
43
+
44
+ await conversations.toCollection().modify(rec => {
45
+ if (!rec.character) rec.character = "kimi";
46
+ });
47
+
48
+ const modelsCount = await llmModels.count();
49
+ if (modelsCount === 0) {
50
+ await llmModels.put({
51
+ id: "mistralai/mistral-small-3.2-24b-instruct",
52
+ name: "Mistral Small 3.2",
53
+ provider: "openrouter",
54
+ apiKey: "",
55
+ config: { temperature: 0.9, maxTokens: 100 },
56
+ added: new Date().toISOString(),
57
+ lastUsed: null
58
+ });
59
+ }
60
+ } catch (e) {
61
+ // Swallow upgrade errors to avoid blocking DB open; post-open migrations will attempt fixes
62
+ }
63
+ });
64
  }
65
 
66
  async init() {
67
  await this.db.open();
68
  await this.initializeDefaultsIfNeeded();
69
+ await this.runPostOpenMigrations();
70
  return this.db;
71
  }
72
 
73
+ getUnifiedTraitDefaults() {
74
+ if (window.KimiEmotionSystem) {
75
+ const emotionSystem = new window.KimiEmotionSystem(this);
76
+ return emotionSystem.TRAIT_DEFAULTS;
77
+ }
78
+ return {
79
+ affection: 65,
80
+ playfulness: 55,
81
+ intelligence: 70,
82
+ empathy: 75,
83
+ humor: 60,
84
+ romance: 50
 
 
 
 
85
  };
86
+ }
87
 
88
+ getDefaultPreferences() {
89
+ return [
 
90
  { key: "selectedLanguage", value: "en" },
91
  { key: "selectedVoice", value: "Microsoft Eloise Online" },
92
  { key: "voiceRate", value: 1.1 },
 
107
  { key: "apiKey_deepseek", value: "" },
108
  { key: "apiKey_custom", value: "" }
109
  ];
110
+ }
111
+
112
+ getDefaultSettings() {
113
+ return [
114
  {
115
  category: "llm",
116
  settings: {
 
122
  }
123
  }
124
  ];
125
+ }
126
 
127
+ getCharacterTraitDefaults() {
128
+ if (!window.KIMI_CHARACTERS) return {};
129
+ const characterDefaults = {};
130
+ Object.keys(window.KIMI_CHARACTERS).forEach(characterKey => {
131
+ const character = window.KIMI_CHARACTERS[characterKey];
132
+ if (character && character.traits) {
133
+ characterDefaults[characterKey] = character.traits;
134
+ }
135
+ });
136
+ return characterDefaults;
137
+ }
 
 
 
138
 
139
+ getDefaultLLMModels() {
140
+ return [
141
  {
142
  id: "mistralai/mistral-small-3.2-24b-instruct",
143
  name: "Mistral Small 3.2",
 
148
  lastUsed: null
149
  }
150
  ];
151
+ }
152
+
153
+ async initializeDefaultsIfNeeded() {
154
+ const defaults = this.getUnifiedTraitDefaults();
155
+
156
+ const defaultPreferences = this.getDefaultPreferences();
157
+ const defaultSettings = this.getDefaultSettings();
158
+ const personalityDefaults = this.getCharacterTraitDefaults();
159
+ const defaultLLMModels = this.getDefaultLLMModels();
160
 
161
  const prefCount = await this.db.preferences.count();
162
  if (prefCount === 0) {
 
216
  }
217
  }
218
 
219
+ async runPostOpenMigrations() {
220
+ try {
221
+ const defaultPreferences = this.getDefaultPreferences();
222
+ for (const pref of defaultPreferences) {
223
+ const existing = await this.db.preferences.get(pref.key);
224
+ if (!existing) {
225
+ await this.db.preferences.put({
226
+ key: pref.key,
227
+ value: pref.value,
228
+ updated: new Date().toISOString()
229
+ });
230
+ }
231
+ }
232
+
233
+ const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} });
234
+ for (const character of characters) {
235
+ const promptKey = `systemPrompt_${character}`;
236
+ const hasPrompt = await this.db.preferences.get(promptKey);
237
+ if (!hasPrompt) {
238
+ const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || "";
239
+ await this.db.preferences.put({ key: promptKey, value: prompt, updated: new Date().toISOString() });
240
+ }
241
+ }
242
+
243
+ const defaultSettings = this.getDefaultSettings();
244
+ for (const setting of defaultSettings) {
245
+ const existing = await this.db.settings.get(setting.category);
246
+ if (!existing) {
247
+ await this.db.settings.put({ ...setting, updated: new Date().toISOString() });
248
+ } else {
249
+ const merged = { ...setting.settings, ...existing.settings };
250
+ await this.db.settings.put({
251
+ category: setting.category,
252
+ settings: merged,
253
+ updated: new Date().toISOString()
254
+ });
255
+ }
256
+ }
257
+
258
+ const defaults = this.getUnifiedTraitDefaults();
259
+ const personalityDefaults = this.getCharacterTraitDefaults();
260
+ for (const character of Object.keys(window.KIMI_CHARACTERS || { kimi: {} })) {
261
+ const characterTraits = personalityDefaults[character] || {};
262
+ const traits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
263
+ for (const trait of traits) {
264
+ const key = [character, trait];
265
+ const found = await this.db.personality.get(key);
266
+ if (!found) {
267
+ const value = Number(characterTraits[trait] ?? defaults[trait] ?? 50);
268
+ const v = isFinite(value) ? Math.max(0, Math.min(100, value)) : 50;
269
+ await this.db.personality.put({ trait, character, value: v, updated: new Date().toISOString() });
270
+ }
271
+ }
272
+ }
273
+
274
+ const llmCount = await this.db.llmModels.count();
275
+ if (llmCount === 0) {
276
+ for (const model of this.getDefaultLLMModels()) {
277
+ await this.db.llmModels.put(model);
278
+ }
279
+ }
280
+
281
+ const allConvs = await this.db.conversations.toArray();
282
+ const toPatch = allConvs.filter(c => !c.character);
283
+ if (toPatch.length) {
284
+ for (const c of toPatch) {
285
+ c.character = "kimi";
286
+ await this.db.conversations.put(c);
287
+ }
288
+ }
289
+
290
+ const allPrefs = await this.db.preferences.toArray();
291
+ const legacy = allPrefs.filter(p => Object.prototype.hasOwnProperty.call(p, "encrypted"));
292
+ if (legacy.length) {
293
+ for (const p of legacy) {
294
+ const { key, value } = p;
295
+ await this.db.preferences.put({ key, value, updated: new Date().toISOString() });
296
+ }
297
+ }
298
+ } catch {}
299
+ }
300
+
301
  async saveConversation(userText, kimiResponse, favorability, timestamp = new Date(), character = null) {
302
  if (!character) character = await this.getSelectedCharacter();
303
  const conversation = {
 
359
  window.KimiCacheManager.set(`pref_${key}`, value, 60000);
360
  }
361
 
362
+ const result = await this.db.preferences.put({
363
  key: key,
364
  value: value,
365
  updated: new Date().toISOString()
366
  });
367
+ if (window.dispatchEvent) {
368
+ try {
369
+ window.dispatchEvent(new CustomEvent("preferenceUpdated", { detail: { key, value } }));
370
+ } catch {}
371
+ }
372
+ return result;
373
  }
374
 
375
  async getPreference(key, defaultValue = null) {
376
  // Try cache first (use a singleton cache instance)
377
  const cacheKey = `pref_${key}`;
378
+ const cache =
379
+ window.KimiCacheManager && typeof window.KimiCacheManager.get === "function" ? window.KimiCacheManager : null;
380
  if (cache && typeof cache.get === "function") {
381
  const cached = cache.get(cacheKey);
382
  if (cached !== null) {
 
387
  try {
388
  const record = await this.db.preferences.get(key);
389
  if (!record) {
390
+ const cache =
391
+ window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null;
392
  if (cache && typeof cache.set === "function") {
393
  cache.set(cacheKey, defaultValue, 60000); // Cache for 1 minute
394
  }
 
399
  let value = record.value;
400
  if (record.encrypted && window.KimiSecurityUtils) {
401
  try {
402
+ value = record.value; // decrypt removed – stored as plain text
403
  // One-time migration: store back as plain text without encrypted flag
404
  try {
405
  await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() });
 
411
  }
412
 
413
  // Cache the result
414
+ const cache =
415
+ window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null;
416
  if (cache && typeof cache.set === "function") {
417
  cache.set(cacheKey, value, 60000); // Cache for 1 minute
418
  }
 
478
  defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
479
  } else {
480
  // Fallback defaults (must match KimiEmotionSystem.TRAIT_DEFAULTS exactly)
481
+ if (window.getTraitDefaults) {
482
+ defaultValue = window.getTraitDefaults()[trait] || 50;
483
+ } else {
484
+ defaultValue =
485
+ {
486
+ affection: 65,
487
+ playfulness: 55,
488
+ intelligence: 70,
489
+ empathy: 75,
490
+ humor: 60,
491
+ romance: 50
492
+ }[trait] || 50;
493
+ }
494
  }
495
  }
496
 
 
521
  if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") {
522
  const cached = window.KimiCacheManager.get(cacheKey);
523
  if (cached !== null) {
524
+ // Correction : valider les valeurs du cache
525
+ const safeTraits = {};
526
+ for (const [trait, value] of Object.entries(cached)) {
527
+ let v = Number(value);
528
+ if (!isFinite(v) || isNaN(v)) v = 50;
529
+ v = Math.max(0, Math.min(100, v));
530
+ safeTraits[trait] = v;
531
+ }
532
+ return safeTraits;
533
  }
534
  }
535
 
536
  const all = await this.db.personality.where("character").equals(character).toArray();
537
  const traits = {};
538
  all.forEach(item => {
539
+ let v = Number(item.value);
540
+ if (!isFinite(v) || isNaN(v)) v = 50;
541
+ v = Math.max(0, Math.min(100, v));
542
+ traits[item.trait] = v;
543
  });
544
 
545
+ // If no traits stored yet for this character, seed from character defaults (one-time)
546
+ if (Object.keys(traits).length === 0 && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]) {
547
+ const seed = window.KIMI_CHARACTERS[character].traits || {};
548
+ const safeSeed = {};
549
+ for (const [k, v] of Object.entries(seed)) {
550
+ const num = typeof v === "number" && isFinite(v) ? Math.max(0, Math.min(100, v)) : 50;
551
+ safeSeed[k] = num;
552
+ try {
553
+ await this.setPersonalityTrait(k, num, character);
554
+ } catch {}
555
+ }
556
+ return safeSeed;
557
+ }
558
+
559
  // Cache the result
560
  if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
561
  window.KimiCacheManager.set(cacheKey, traits, 120000); // Cache for 2 minutes
 
702
  } catch (e) {}
703
  }
704
 
705
+ // Validation stricte : empêcher NaN ou valeurs non numériques
706
+ const getDefault = trait => {
707
+ if (window.KimiEmotionSystem) {
708
+ return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
709
+ }
710
+ const fallback = { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
711
+ return fallback[trait] || 50;
712
+ };
713
+ const batch = Object.entries(traitsObj).map(([trait, value]) => {
714
+ let v = Number(value);
715
+ if (!isFinite(v) || isNaN(v)) v = getDefault(trait);
716
+ v = Math.max(0, Math.min(100, v));
717
+ return {
718
+ trait,
719
+ character,
720
+ value: v,
721
+ updated: new Date().toISOString()
722
+ };
723
+ });
724
  return this.db.personality.bulkPut(batch);
725
  }
726
  async setSettingsBatch(settingsArray) {
 
738
  let val = item.value;
739
  if (item.encrypted && window.KimiSecurityUtils) {
740
  try {
741
+ val = item.value; // decrypt removed – stored as plain text
742
  // Migrate back as plain
743
  try {
744
  await this.db.preferences.put({ key: item.key, value: val, updated: new Date().toISOString() });
 
806
  }
807
  }
808
 
809
+ export default KimiDatabase;
810
  // Export for usage
811
  window.KimiDatabase = KimiDatabase;
kimi-js/kimi-emotion-system.js CHANGED
@@ -53,7 +53,6 @@ class KimiEmotionSystem {
53
  romance: 50 // Significantly reduced from 95 - romance must be earned!
54
  };
55
  }
56
-
57
  // ===== UNIFIED EMOTION ANALYSIS =====
58
  analyzeEmotion(text, lang = "auto") {
59
  if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
@@ -159,12 +158,13 @@ class KimiEmotionSystem {
159
  const selectedCharacter = character || (await this.db.getSelectedCharacter());
160
  const traits = await this.db.getAllPersonalityTraits(selectedCharacter);
161
 
162
- let affection = traits.affection || this.TRAIT_DEFAULTS.affection;
163
- let romance = traits.romance || this.TRAIT_DEFAULTS.romance;
164
- let empathy = traits.empathy || this.TRAIT_DEFAULTS.empathy;
165
- let playfulness = traits.playfulness || this.TRAIT_DEFAULTS.playfulness;
166
- let humor = traits.humor || this.TRAIT_DEFAULTS.humor;
167
- let intelligence = traits.intelligence || this.TRAIT_DEFAULTS.intelligence;
 
168
 
169
  // Unified adjustment functions - More gradual progression for balanced experience
170
  const adjustUp = (val, amount) => {
@@ -251,21 +251,24 @@ class KimiEmotionSystem {
251
  await this._analyzeTextContent(
252
  text,
253
  traits => {
254
- romance = traits.romance;
255
- affection = traits.affection;
256
- humor = traits.humor;
257
- playfulness = traits.playfulness;
258
  },
259
  adjustUp
260
  );
261
 
 
 
 
262
  const updatedTraits = {
263
- affection: Math.round(affection),
264
- romance: Math.round(romance),
265
- empathy: Math.round(empathy),
266
- playfulness: Math.round(playfulness),
267
- humor: Math.round(humor),
268
- intelligence: Math.round(intelligence)
269
  };
270
 
271
  // Save to database
@@ -280,7 +283,7 @@ class KimiEmotionSystem {
280
 
281
  const lowerUser = userMessage ? userMessage.toLowerCase() : "";
282
  const lowerKimi = (kimiResponse || "").toLowerCase();
283
- const traits = await this.db.getAllPersonalityTraits(character);
284
  const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
285
 
286
  // Use unified keyword system
@@ -294,7 +297,7 @@ class KimiEmotionSystem {
294
  for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
295
  const posWords = getPersonalityWords(trait, "positive");
296
  const negWords = getPersonalityWords(trait, "negative");
297
- let value = typeof traits[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
298
 
299
  // Count occurrences with proper weighting
300
  let posCount = 0;
@@ -451,8 +454,8 @@ class KimiEmotionSystem {
451
  }
452
  }
453
 
454
- // ===== GLOBAL EXPORT =====
455
  window.KimiEmotionSystem = KimiEmotionSystem;
 
456
 
457
  // ===== BACKWARD COMPATIBILITY LAYER =====
458
  // Replace the old kimiAnalyzeEmotion function
@@ -471,39 +474,6 @@ window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
471
 
472
  const updatedTraits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text);
473
 
474
- // Update UI sliders
475
- if (updatedTraits && window.updateSlider) {
476
- window.updateSlider("trait-affection", updatedTraits.affection);
477
- window.updateSlider("trait-romance", updatedTraits.romance);
478
- window.updateSlider("trait-empathy", updatedTraits.empathy);
479
- window.updateSlider("trait-playfulness", updatedTraits.playfulness);
480
- window.updateSlider("trait-humor", updatedTraits.humor);
481
- window.updateSlider("trait-intelligence", updatedTraits.intelligence);
482
- }
483
-
484
- // Update memory system
485
- if (window.kimiMemory && updatedTraits) {
486
- window.kimiMemory.affectionTrait = updatedTraits.affection;
487
- if (window.kimiMemory.updateFavorabilityBar) {
488
- window.kimiMemory.updateFavorabilityBar();
489
- }
490
- }
491
-
492
- // Update video context
493
- if (window.kimiVideo && window.kimiVideo.setMoodByPersonality && updatedTraits) {
494
- window.kimiVideo.setMoodByPersonality(updatedTraits);
495
- }
496
-
497
- // Dispatch event
498
- if (window.dispatchEvent && updatedTraits) {
499
- const selectedCharacter = window.kimiDB ? await window.kimiDB.getSelectedCharacter() : "kimi";
500
- window.dispatchEvent(
501
- new CustomEvent("personalityUpdated", {
502
- detail: { character: selectedCharacter, traits: updatedTraits }
503
- })
504
- );
505
- }
506
-
507
  return updatedTraits;
508
  };
509
 
@@ -514,3 +484,10 @@ window.getPersonalityAverage = function (traits) {
514
  }
515
  return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
516
  };
 
 
 
 
 
 
 
 
53
  romance: 50 // Significantly reduced from 95 - romance must be earned!
54
  };
55
  }
 
56
  // ===== UNIFIED EMOTION ANALYSIS =====
57
  analyzeEmotion(text, lang = "auto") {
58
  if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
 
158
  const selectedCharacter = character || (await this.db.getSelectedCharacter());
159
  const traits = await this.db.getAllPersonalityTraits(selectedCharacter);
160
 
161
+ const safe = (v, def) => (typeof v === "number" && isFinite(v) ? v : def);
162
+ let affection = safe(traits?.affection, this.TRAIT_DEFAULTS.affection);
163
+ let romance = safe(traits?.romance, this.TRAIT_DEFAULTS.romance);
164
+ let empathy = safe(traits?.empathy, this.TRAIT_DEFAULTS.empathy);
165
+ let playfulness = safe(traits?.playfulness, this.TRAIT_DEFAULTS.playfulness);
166
+ let humor = safe(traits?.humor, this.TRAIT_DEFAULTS.humor);
167
+ let intelligence = safe(traits?.intelligence, this.TRAIT_DEFAULTS.intelligence);
168
 
169
  // Unified adjustment functions - More gradual progression for balanced experience
170
  const adjustUp = (val, amount) => {
 
251
  await this._analyzeTextContent(
252
  text,
253
  traits => {
254
+ if (typeof traits.romance !== "undefined") romance = traits.romance;
255
+ if (typeof traits.affection !== "undefined") affection = traits.affection;
256
+ if (typeof traits.humor !== "undefined") humor = traits.humor;
257
+ if (typeof traits.playfulness !== "undefined") playfulness = traits.playfulness;
258
  },
259
  adjustUp
260
  );
261
 
262
+ // Preserve fractional progress to allow gradual visible changes
263
+ const to2 = v => Number(Number(v).toFixed(2));
264
+ const clamp = v => Math.max(0, Math.min(100, v));
265
  const updatedTraits = {
266
+ affection: to2(clamp(affection)),
267
+ romance: to2(clamp(romance)),
268
+ empathy: to2(clamp(empathy)),
269
+ playfulness: to2(clamp(playfulness)),
270
+ humor: to2(clamp(humor)),
271
+ intelligence: to2(clamp(intelligence))
272
  };
273
 
274
  // Save to database
 
283
 
284
  const lowerUser = userMessage ? userMessage.toLowerCase() : "";
285
  const lowerKimi = (kimiResponse || "").toLowerCase();
286
+ const traits = (await this.db.getAllPersonalityTraits(character)) || {};
287
  const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
288
 
289
  // Use unified keyword system
 
297
  for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
298
  const posWords = getPersonalityWords(trait, "positive");
299
  const negWords = getPersonalityWords(trait, "negative");
300
+ let value = typeof traits[trait] === "number" && isFinite(traits[trait]) ? traits[trait] : this.TRAIT_DEFAULTS[trait];
301
 
302
  // Count occurrences with proper weighting
303
  let posCount = 0;
 
454
  }
455
  }
456
 
 
457
  window.KimiEmotionSystem = KimiEmotionSystem;
458
+ export default KimiEmotionSystem;
459
 
460
  // ===== BACKWARD COMPATIBILITY LAYER =====
461
  // Replace the old kimiAnalyzeEmotion function
 
474
 
475
  const updatedTraits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text);
476
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  return updatedTraits;
478
  };
479
 
 
484
  }
485
  return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
486
  };
487
+
488
+ // Unified trait defaults accessor
489
+ window.getTraitDefaults = function () {
490
+ if (window.kimiEmotionSystem) return window.kimiEmotionSystem.TRAIT_DEFAULTS;
491
+ const temp = new KimiEmotionSystem(window.kimiDB);
492
+ return temp.TRAIT_DEFAULTS;
493
+ };
kimi-js/kimi-llm-manager.js CHANGED
@@ -1,10 +1,11 @@
1
  // ===== KIMI INTELLIGENT LLM SYSTEM =====
 
2
  class KimiLLMManager {
3
  constructor(database) {
4
  this.db = database;
5
  this.currentModel = null;
6
  this.conversationContext = [];
7
- this.maxContextLength = 30;
8
  this.systemPrompt = "";
9
 
10
  // Recommended models on OpenRouter (IDs updated August 2025)
@@ -30,8 +31,8 @@ class KimiLLMManager {
30
  provider: "xAI",
31
  type: "openrouter",
32
  contextWindow: 131000,
33
- pricing: { input: 0.3, output: 0.50 },
34
- strengths: ["Multilingual", "Balanced", "Fast", "Economical"]
35
  },
36
  "cohere/command-r-08-2024": {
37
  name: "Command-R-08-2024",
@@ -196,13 +197,10 @@ class KimiLLMManager {
196
  const preferences = await this.db.getAllPreferences();
197
 
198
  // Use unified emotion system defaults - CRITICAL FIX
199
- const getUnifiedDefaults = () => {
200
- if (window.KimiEmotionSystem) {
201
- const emotionSystem = new window.KimiEmotionSystem(this.db);
202
- return emotionSystem.TRAIT_DEFAULTS;
203
- }
204
- return { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
205
- };
206
 
207
  const defaults = getUnifiedDefaults();
208
  const affection = personality.affection || defaults.affection;
@@ -359,19 +357,9 @@ class KimiLLMManager {
359
  async chatWithOpenAICompatible(userMessage, options = {}) {
360
  const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
361
  const provider = await this.db.getPreference("llmProvider", "openai");
362
- const providerKeyMap = {
363
- openrouter: "openrouterApiKey",
364
- openai: "apiKey_openai",
365
- groq: "apiKey_groq",
366
- together: "apiKey_together",
367
- deepseek: "apiKey_deepseek",
368
- "openai-compatible": "apiKey_custom"
369
- };
370
- const keyPref = providerKeyMap[provider] || "llmApiKey";
371
- let apiKey = await this.db.getPreference(keyPref, "");
372
- if (!apiKey) {
373
- apiKey = await this.db.getPreference("llmApiKey", "");
374
- }
375
  const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
376
  if (!apiKey) {
377
  throw new Error("API key not configured for selected provider");
@@ -384,7 +372,7 @@ class KimiLLMManager {
384
 
385
  const llmSettings = await this.db.getSetting("llm", {
386
  temperature: 0.9,
387
- maxTokens: 100,
388
  top_p: 0.9,
389
  frequency_penalty: 0.3,
390
  presence_penalty: 0.3
@@ -433,6 +421,23 @@ class KimiLLMManager {
433
  if (this.conversationContext.length > this.maxContextLength * 2) {
434
  this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
435
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  return content;
437
  } catch (e) {
438
  if (e.name === "TypeError" && e.message.includes("fetch")) {
@@ -605,6 +610,22 @@ class KimiLLMManager {
605
  this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
606
  }
607
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
608
  return kimiResponse;
609
  } catch (networkError) {
610
  if (networkError.name === "TypeError" && networkError.message.includes("fetch")) {
@@ -918,3 +939,4 @@ class KimiLLMManager {
918
 
919
  // Export for usage
920
  window.KimiLLMManager = KimiLLMManager;
 
 
1
  // ===== KIMI INTELLIGENT LLM SYSTEM =====
2
+ import { KimiProviderUtils } from "./kimi-utils.js";
3
  class KimiLLMManager {
4
  constructor(database) {
5
  this.db = database;
6
  this.currentModel = null;
7
  this.conversationContext = [];
8
+ this.maxContextLength = 100;
9
  this.systemPrompt = "";
10
 
11
  // Recommended models on OpenRouter (IDs updated August 2025)
 
31
  provider: "xAI",
32
  type: "openrouter",
33
  contextWindow: 131000,
34
+ pricing: { input: 0.3, output: 0.5 },
35
+ strengths: ["Multilingual", "Balanced", "Efficient", "Economical"]
36
  },
37
  "cohere/command-r-08-2024": {
38
  name: "Command-R-08-2024",
 
197
  const preferences = await this.db.getAllPreferences();
198
 
199
  // Use unified emotion system defaults - CRITICAL FIX
200
+ const getUnifiedDefaults = () =>
201
+ window.getTraitDefaults
202
+ ? window.getTraitDefaults()
203
+ : { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
 
 
 
204
 
205
  const defaults = getUnifiedDefaults();
206
  const affection = personality.affection || defaults.affection;
 
357
  async chatWithOpenAICompatible(userMessage, options = {}) {
358
  const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
359
  const provider = await this.db.getPreference("llmProvider", "openai");
360
+ const apiKey = KimiProviderUtils
361
+ ? await KimiProviderUtils.getApiKey(this.db, provider)
362
+ : await this.db.getPreference("llmApiKey", "");
 
 
 
 
 
 
 
 
 
 
363
  const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
364
  if (!apiKey) {
365
  throw new Error("API key not configured for selected provider");
 
372
 
373
  const llmSettings = await this.db.getSetting("llm", {
374
  temperature: 0.9,
375
+ maxTokens: 200,
376
  top_p: 0.9,
377
  frequency_penalty: 0.3,
378
  presence_penalty: 0.3
 
421
  if (this.conversationContext.length > this.maxContextLength * 2) {
422
  this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
423
  }
424
+ // Approximate token usage and store temporarily for later persistence (single save point)
425
+ try {
426
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
427
+ const tokensIn = est(userMessage + " " + systemPromptContent);
428
+ const tokensOut = est(content);
429
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
430
+ if (!window.kimiMemory && this.db) {
431
+ // Update counters early so UI can reflect even if memory save occurs later
432
+ const character = await this.db.getSelectedCharacter();
433
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
434
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
435
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
436
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
437
+ }
438
+ } catch (tokenErr) {
439
+ console.warn("Token usage estimation failed:", tokenErr);
440
+ }
441
  return content;
442
  } catch (e) {
443
  if (e.name === "TypeError" && e.message.includes("fetch")) {
 
610
  this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
611
  }
612
 
613
+ // Token usage estimation (deferred save)
614
+ try {
615
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
616
+ const tokensIn = est(userMessage + " " + systemPromptContent);
617
+ const tokensOut = est(kimiResponse);
618
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
619
+ if (!window.kimiMemory && this.db) {
620
+ const character = await this.db.getSelectedCharacter();
621
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
622
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
623
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
624
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
625
+ }
626
+ } catch (e) {
627
+ console.warn("Token usage estimation failed (OpenRouter):", e);
628
+ }
629
  return kimiResponse;
630
  } catch (networkError) {
631
  if (networkError.name === "TypeError" && networkError.message.includes("fetch")) {
 
939
 
940
  // Export for usage
941
  window.KimiLLMManager = KimiLLMManager;
942
+ export default KimiLLMManager;
kimi-js/kimi-main.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ESM bootstrap for Kimi App
2
+ // Import minimal utilities as modules; rely on existing globals for legacy parts
3
+ import { KimiProviderUtils } from "./kimi-utils.js";
4
+ import KimiLLMManager from "./kimi-llm-manager.js";
5
+ import KimiEmotionSystem from "./kimi-emotion-system.js";
6
+
7
+ // Expose module imports to legacy code paths that still rely on window
8
+ window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils;
9
+ window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager;
10
+ window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem;
11
+
12
+ // Defer to existing script initialization (kimi-script.js)
13
+ // This file mainly ensures ESM compatibility and prepares future migration.
kimi-js/kimi-memory-system.js CHANGED
@@ -23,10 +23,34 @@ class KimiMemorySystem {
23
  /(?:i live in|i'm from|from) ([^,.!?]+)/i,
24
  /(?:i work as|my job is|i'm a) ([^,.!?]+)/i,
25
  // French patterns
26
- /(?:je m'appelle|mon nom est|je suis) ([^,.!?]+)/i,
27
  /(?:j'ai) (\d+) ans?/i,
28
  /(?:j'habite à|je vis à|je viens de) ([^,.!?]+)/i,
29
- /(?:je travaille comme|mon travail est|je suis) ([^,.!?]+)/i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ],
31
  preferences: [
32
  // English patterns
@@ -47,7 +71,22 @@ class KimiMemorySystem {
47
  /(?:my (?:mother|father|sister|brother|friend)) ([^,.!?]+)/i,
48
  // French patterns
49
  /(?:ma (?:femme|copine|partenaire)|mon (?:mari|copain|partenaire)) (?:s'appelle|est) ([^,.!?]+)/i,
50
- /(?:ma (?:mère|sœur)|mon (?:père|frère|ami)) (?:s'appelle|est) ([^,.!?]+)/i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  ],
52
  activities: [
53
  // English patterns
@@ -55,7 +94,22 @@ class KimiMemorySystem {
55
  /(?:my hobby is|i hobby) ([^,.!?]+)/i,
56
  // French patterns
57
  /(?:je joue|je fais|je pratique) ([^,.!?]+)/i,
58
- /(?:mon passe-temps|mon hobby) (?:est|c'est) ([^,.!?]+)/i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  ],
60
  goals: [
61
  // English patterns
@@ -63,7 +117,87 @@ class KimiMemorySystem {
63
  /(?:i'm learning|i study) ([^,.!?]+)/i,
64
  // French patterns
65
  /(?:je veux|je vais|mon objectif est) ([^,.!?]+)/i,
66
- /(?:j'apprends|j'étudie) ([^,.!?]+)/i
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  ]
68
  };
69
  }
@@ -185,7 +319,49 @@ class KimiMemorySystem {
185
  /(?:i\s+want\s+you\s+to\s+)?(?:remember|memorize|add)\s+(?:that\s+)?(.+)/i
186
  ];
187
 
188
- const allPatterns = [...frenchPatterns, ...englishPatterns];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
 
190
  for (const pattern of allPatterns) {
191
  const match = lowerText.match(pattern);
@@ -295,7 +471,25 @@ class KimiMemorySystem {
295
  let confidence = 0.6; // Base confidence
296
 
297
  // Boost confidence for explicit statements
298
- if (fullText.includes("my name is") || fullText.includes("i am called")) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  confidence += 0.3;
300
  }
301
 
@@ -332,8 +526,35 @@ class KimiMemorySystem {
332
  const naturalMemories = [];
333
  const lowerText = text.toLowerCase();
334
 
335
- // Detect name mentions in natural context
336
- const namePatterns = [/call me (\w+)/i, /(\w+) here[,.]?/i, /this is (\w+)/i, /(\w+) speaking/i];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
  for (const pattern of namePatterns) {
339
  const match = lowerText.match(pattern);
@@ -431,7 +652,7 @@ class KimiMemorySystem {
431
  timestamp: memoryData.timestamp || new Date(),
432
  character: memoryData.character || this.selectedCharacter,
433
  isActive: true,
434
- tags: memoryData.tags || [],
435
  lastModified: new Date(),
436
  accessCount: 0,
437
  importance: this.calculateImportance(memoryData)
@@ -506,7 +727,7 @@ class KimiMemorySystem {
506
  ...existingMemory,
507
  content: mergedContent,
508
  confidence: mergedConfidence,
509
- tags: [...new Set(mergedTags)], // Remove duplicates
510
  lastModified: new Date(),
511
  accessCount: (existingMemory.accessCount || 0) + 1,
512
  importance: Math.max(existingMemory.importance || 0.5, this.calculateImportance(newMemoryData))
@@ -565,32 +786,104 @@ class KimiMemorySystem {
565
  calculateImportance(memoryData) {
566
  let importance = 0.5; // Base importance
567
 
568
- // Personal information is generally more important
569
  const categoryWeights = {
 
570
  personal: 0.9,
571
- relationships: 0.8,
572
- goals: 0.7,
 
573
  preferences: 0.6,
574
- activities: 0.5,
575
- experiences: 0.4,
576
- important: 1.0
577
  };
578
 
579
  importance = categoryWeights[memoryData.category] || 0.5;
580
 
581
- // Boost importance for longer, more detailed content
582
- if (memoryData.content && memoryData.content.length > 20) {
583
- importance += 0.1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  }
585
 
586
- // High confidence boosts importance
587
- if (memoryData.confidence && memoryData.confidence > 0.9) {
588
- importance += 0.1;
589
  }
590
 
 
 
 
 
591
  return Math.min(1.0, importance);
592
  }
593
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
594
  async updateMemory(memoryId, updateData) {
595
  if (!this.db) return false;
596
 
@@ -814,17 +1107,21 @@ class KimiMemorySystem {
814
  if (!this.db) return;
815
 
816
  try {
 
817
  const memories = await this.getAllMemories();
818
 
 
 
819
  if (memories.length > this.maxMemoryEntries) {
820
- // Keep most recent and important memories
821
  memories.sort((a, b) => {
822
- // Priority: confidence * recency
823
  const scoreA = a.confidence * (Date.now() - new Date(a.timestamp).getTime());
824
  const scoreB = b.confidence * (Date.now() - new Date(b.timestamp).getTime());
825
  return scoreB - scoreA;
826
  });
827
 
 
828
  const toDelete = memories.slice(this.maxMemoryEntries);
829
  for (const memory of toDelete) {
830
  await this.deleteMemory(memory.id);
@@ -937,13 +1234,158 @@ class KimiMemorySystem {
937
  const contextLower = context.toLowerCase();
938
 
939
  const categoryKeywords = {
940
- personal: ["name", "age", "live", "work", "job", "who", "am", "myself"],
941
- preferences: ["like", "love", "hate", "prefer", "enjoy", "favorite", "dislike"],
942
- relationships: ["family", "friend", "wife", "husband", "partner", "mother", "father"],
943
- activities: ["play", "hobby", "sport", "activity", "practice", "do"],
944
- goals: ["want", "plan", "goal", "dream", "hope", "wish", "future"],
945
- experiences: ["remember", "happened", "story", "experience", "time"],
946
- important: ["important", "remember", "special", "never forget"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
947
  };
948
 
949
  const keywords = categoryKeywords[category] || [];
@@ -1094,3 +1536,6 @@ class KimiMemorySystem {
1094
  }
1095
 
1096
  window.KimiMemorySystem = KimiMemorySystem;
 
 
 
 
23
  /(?:i live in|i'm from|from) ([^,.!?]+)/i,
24
  /(?:i work as|my job is|i'm a) ([^,.!?]+)/i,
25
  // French patterns
26
+ /(?:je m'appelle|mon nom est|je suis|je me prénomme|je me nomme) ([^,.!?]+)/i,
27
  /(?:j'ai) (\d+) ans?/i,
28
  /(?:j'habite à|je vis à|je viens de) ([^,.!?]+)/i,
29
+ /(?:je travaille comme|mon travail est|je suis) ([^,.!?]+)/i,
30
+ // Spanish patterns
31
+ /(?:me llamo|mi nombre es|soy) ([^,.!?]+)/i,
32
+ /(?:tengo) (\d+) años?/i,
33
+ /(?:vivo en|soy de) ([^,.!?]+)/i,
34
+ /(?:trabajo como|mi trabajo es|soy) ([^,.!?]+)/i,
35
+ // Italian patterns
36
+ /(?:mi chiamo|il mio nome è|sono) ([^,.!?]+)/i,
37
+ /(?:ho) (\d+) anni?/i,
38
+ /(?:abito a|vivo a|sono di) ([^,.!?]+)/i,
39
+ /(?:lavoro come|il mio lavoro è|sono) ([^,.!?]+)/i,
40
+ // German patterns
41
+ /(?:ich heiße|mein name ist|ich bin) ([^,.!?]+)/i,
42
+ /(?:ich bin) (\d+) jahre? alt/i,
43
+ /(?:ich wohne in|ich lebe in|ich komme aus) ([^,.!?]+)/i,
44
+ /(?:ich arbeite als|mein beruf ist|ich bin) ([^,.!?]+)/i,
45
+ // Japanese patterns
46
+ /私の名前は([^。!?!?、,.]+)[ですだ]?/i,
47
+ /私は([^。!?!?、,.]+)です/i,
48
+ /([^、。!?!?,.]+)と申します/i,
49
+ /([^、。!?!?,.]+)といいます/i,
50
+ // Chinese patterns
51
+ /我叫([^,。!?!?,.]+)/i,
52
+ /我的名字是([^,。!?!?,.]+)/i,
53
+ /叫我([^,。!?!?,.]+)/i
54
  ],
55
  preferences: [
56
  // English patterns
 
71
  /(?:my (?:mother|father|sister|brother|friend)) ([^,.!?]+)/i,
72
  // French patterns
73
  /(?:ma (?:femme|copine|partenaire)|mon (?:mari|copain|partenaire)) (?:s'appelle|est) ([^,.!?]+)/i,
74
+ /(?:ma (?:mère|sœur)|mon (?:père|frère|ami)) (?:s'appelle|est) ([^,.!?]+)/i,
75
+ // Spanish patterns
76
+ /(?:mi (?:esposa|esposo|novia|novio|pareja)) (?:es|se llama) ([^,.!?]+)/i,
77
+ /(?:mi (?:madre|padre|hermana|hermano|amigo|amiga)) (?:es|se llama) ([^,.!?]+)/i,
78
+ // Italian patterns
79
+ /(?:la mia (?:moglie|fidanzata|compagna)|il mio (?:marito|fidanzato|compagno)) (?:è|si chiama) ([^,.!?]+)/i,
80
+ /(?:mia (?:madre|sorella)|mio (?:padre|fratello|amico)) (?:è|si chiama) ([^,.!?]+)/i,
81
+ // German patterns
82
+ /(?:meine (?:frau|freundin|partnerin)|mein (?:mann|freund|partner)) (?:ist|heißt) ([^,.!?]+)/i,
83
+ /(?:meine (?:mutter|schwester)|mein (?:vater|bruder|freund)) (?:ist|heißt) ([^,.!?]+)/i,
84
+ // Japanese patterns
85
+ /(?:私の(?:妻|夫|彼女|彼氏|パートナー))は([^。!?!?、,.]+)(?:です|といいます)/i,
86
+ /(?:私の(?:母|父|姉|妹|兄|弟|友達))は([^。!?!?、,.]+)(?:です|といいます)/i,
87
+ // Chinese patterns
88
+ /(?:我的(?:妻子|丈夫|女朋友|男朋友|伴侣))叫([^,。!?!?,.]+)/i,
89
+ /(?:我的(?:妈妈|父亲|姐姐|妹妹|哥哥|弟弟|朋友))叫([^,。!?!?,.]+)/i
90
  ],
91
  activities: [
92
  // English patterns
 
94
  /(?:my hobby is|i hobby) ([^,.!?]+)/i,
95
  // French patterns
96
  /(?:je joue|je fais|je pratique) ([^,.!?]+)/i,
97
+ /(?:mon passe-temps|mon hobby) (?:est|c'est) ([^,.!?]+)/i,
98
+ // Spanish patterns
99
+ /(?:juego|hago|practico) ([^,.!?]+)/i,
100
+ /(?:mi pasatiempo|mi hobby) (?:es) ([^,.!?]+)/i,
101
+ // Italian patterns
102
+ /(?:gioco|faccio|pratico) ([^,.!?]+)/i,
103
+ /(?:il mio passatempo|il mio hobby) (?:è) ([^,.!?]+)/i,
104
+ // German patterns
105
+ /(?:ich spiele|ich mache|ich übe) ([^,.!?]+)/i,
106
+ /(?:mein hobby ist) ([^,.!?]+)/i,
107
+ // Japanese patterns
108
+ /(?:私は)?(?:[^、。!?!?,.]+)が趣味です/i,
109
+ /趣味は([^。!?!?、,.]+)です/i,
110
+ // Chinese patterns
111
+ /(?:我玩|我做|我练习)([^,。!?!?,.]+)/i,
112
+ /(?:我的爱好是)([^,。!?!?,.]+)/i
113
  ],
114
  goals: [
115
  // English patterns
 
117
  /(?:i'm learning|i study) ([^,.!?]+)/i,
118
  // French patterns
119
  /(?:je veux|je vais|mon objectif est) ([^,.!?]+)/i,
120
+ /(?:j'apprends|j'étudie) ([^,.!?]+)/i,
121
+ // Spanish patterns
122
+ /(?:quiero|voy a|mi objetivo es) ([^,.!?]+)/i,
123
+ /(?:estoy aprendiendo|estudio) ([^,.!?]+)/i,
124
+ // Italian patterns
125
+ /(?:voglio|andrò a|il mio obiettivo è) ([^,.!?]+)/i,
126
+ /(?:sto imparando|studio) ([^,.!?]+)/i,
127
+ // German patterns
128
+ /(?:ich möchte|ich will|mein ziel ist) ([^,.!?]+)/i,
129
+ /(?:ich lerne|ich studiere) ([^,.!?]+)/i,
130
+ // Japanese patterns
131
+ /(?:私は)?(?:[^、。!?!?,.]+)したい/i,
132
+ /(?:学んでいる|勉強している) ([^。!?!?、,.]+)/i,
133
+ // Chinese patterns
134
+ /(?:我想|我要|我的目标是)([^,。!?!?,.]+)/i,
135
+ /(?:我在学习|我学习)([^,。!?!?,.]+)/i
136
+ ],
137
+ experiences: [
138
+ // English patterns
139
+ /we went to ([^,.!?]+)/i,
140
+ /we met (?:at|on|in) ([^,.!?]+)/i,
141
+ /our (?:first date|first kiss|trip|vacation) (?:was|was at|was on|was in|was to) ([^,.!?]+)/i,
142
+ /our anniversary (?:is|falls on|will be) ([^,.!?]+)/i,
143
+ /we moved in (?:together )?(?:on|in)?\s*([^,.!?]+)/i,
144
+ // French patterns
145
+ /on s'est rencontr[ée]s? (?:à|au|en|le) ([^,.!?]+)/i,
146
+ /on est all[ée]s? à ([^,.!?]+)/i,
147
+ /notre (?:premier rendez-vous|première sortie) (?:était|c'était) ([^,.!?]+)/i,
148
+ /notre anniversaire (?:est|c'est) ([^,.!?]+)/i,
149
+ /on a emménagé (?:ensemble\s*)?(?:le|en|à)\s*([^,.!?]+)/i,
150
+ // Spanish patterns
151
+ /nos conocimos (?:en|el|la) ([^,.!?]+)/i,
152
+ /fuimos a ([^,.!?]+)/i,
153
+ /nuestra (?:primera cita|primera salida) (?:fue|era) ([^,.!?]+)/i,
154
+ /nuestro aniversario (?:es|cae en|será) ([^,.!?]+)/i,
155
+ /nos mudamos (?:juntos\s*)?(?:el|en|a)\s*([^,.!?]+)/i,
156
+ // Italian patterns
157
+ /ci siamo conosciuti (?:a|al|in|il) ([^,.!?]+)/i,
158
+ /siamo andati a ([^,.!?]+)/i,
159
+ /il nostro (?:primo appuntamento|primo bacio|viaggio) (?:era|è stato) ([^,.!?]+)/i,
160
+ /il nostro anniversario (?:è|cade il|sarà) ([^,.!?]+)/i,
161
+ /ci siamo trasferiti (?:insieme\s*)?(?:il|in|a)\s*([^,.!?]+)/i,
162
+ // German patterns
163
+ /wir haben uns (?:in|am) ([^,.!?]+) kennengelernt/i,
164
+ /wir sind (?:nach|zu) ([^,.!?]+) (?:gegangen|gefahren)/i,
165
+ /unser (?:erstes date|erster kuss|urlaub) (?:war|fand statt) ([^,.!?]+)/i,
166
+ /unser jahrestag (?:ist|fällt auf|wird sein) ([^,.!?]+)/i,
167
+ /wir sind (?:zusammen )?eingezogen (?:am|im|in)\s*([^,.!?]+)/i,
168
+ // Japanese patterns
169
+ /私たちは([^、。!?!?,.]+)で出会った/i,
170
+ /一緒に([^、。!?!?,.]+)へ行った/i,
171
+ /私たちの記念日(?:は)?([^、。!?!?,.]+)/i,
172
+ /一緒に引っ越した(?:のは)?([^、。!?!?,.]+)/i,
173
+ // Chinese patterns
174
+ /我们在([^,。!?!?,.]+)认识/i,
175
+ /我们去了([^,。!?!?,.]+)/i,
176
+ /我们的纪念日是([^,。!?!?,.]+)/i,
177
+ /我们一起搬家(?:是在)?([^,。!?!?,.]+)/i
178
+ ],
179
+ important: [
180
+ // English patterns
181
+ /it's important (?:to remember|that) (.+)/i,
182
+ /please remember (.+)/i,
183
+ // French patterns
184
+ /c'est important (?:de se souvenir|que) (.+)/i,
185
+ /merci de te souvenir (.+)/i,
186
+ // Spanish patterns
187
+ /es importante (?:recordar|que) (.+)/i,
188
+ /por favor recuerda (.+)/i,
189
+ // Italian patterns
190
+ /è importante (?:ricordare|che) (.+)/i,
191
+ /per favore ricorda (.+)/i,
192
+ // German patterns
193
+ /es ist wichtig (?:zu erinnern|dass) (.+)/i,
194
+ /bitte erinnere dich an (.+)/i,
195
+ // Japanese patterns
196
+ /重要なのは(.+)です/i,
197
+ /覚えておいてほしいのは(.+)です/i,
198
+ // Chinese patterns
199
+ /重要的是(.+)/i,
200
+ /请记住(.+)/i
201
  ]
202
  };
203
  }
 
319
  /(?:i\s+want\s+you\s+to\s+)?(?:remember|memorize|add)\s+(?:that\s+)?(.+)/i
320
  ];
321
 
322
+ // Spanish explicit memory requests
323
+ const spanishPatterns = [
324
+ /(?:añade|agrega|recuerda|memoriza|guarda)\s+(?:en|a)\s+(?:la\s+)?memoria\s+(?:que\s+)?(.+)/i,
325
+ /(?:puedes|podrías)?\s*(?:añadir|agregar|recordar|memorizar|guardar)\s+(?:que\s+)?(.+)\s+(?:en|a)\s+(?:la\s+)?memoria/i,
326
+ /(?:quiero\s+que\s+)?(?:recuerdes|memorices|añadas)\s+(?:que\s+)?(.+)/i
327
+ ];
328
+
329
+ // Italian explicit memory requests
330
+ const italianPatterns = [
331
+ /(?:aggiungi|ricorda|memorizza|salva)\s+(?:nella|in)\s+memoria\s+(?:che\s+)?(.+)/i,
332
+ /(?:puoi|potresti)?\s*(?:aggiungere|ricordare|memorizzare|salvare)\s+(?:che\s+)?(.+)\s+(?:nella|in)\s+memoria/i,
333
+ /(?:voglio\s+che\s+)?(?:ricordi|memorizzi|aggiunga)\s+(?:che\s+)?(.+)/i
334
+ ];
335
+
336
+ // German explicit memory requests
337
+ const germanPatterns = [
338
+ /(?:füge|merke|speichere)\s+(?:es\s+)?(?:in|zur)\s+?gedächtnis|speicher\s+(?:dass\s+)?(.+)/i,
339
+ /(?:kannst\s+du|könntest\s+du)?\s*(?:hinzufügen|merken|speichern)\s+(?:dass\s+)?(.+)\s+(?:in|zum)\s+(?:gedächtnis|speicher)/i,
340
+ /(?:ich\s+möchte\s+dass\s+du)\s*(?:merkst|speicherst|hinzufügst)\s+(?:dass\s+)?(.+)/i
341
+ ];
342
+
343
+ // Japanese explicit memory requests
344
+ const japanesePatterns = [
345
+ /記憶に(?:追加|保存|覚えて)(?:して)?(?:ほしい|ください)?(?:、)?(.+)/i,
346
+ /(?:覚えて|記憶して)(?:ほしい|ください)?(?:、)?(.+)/i
347
+ ];
348
+
349
+ // Chinese explicit memory requests
350
+ const chinesePatterns = [
351
+ /把(.+)记在(?:记忆|内存|记忆库)里/i,
352
+ /(?:请)?记住(?:这件事|这个|以下)?(.+)/i,
353
+ /保存到记忆(?:里|中)(?:的是)?(.+)/i
354
+ ];
355
+
356
+ const allPatterns = [
357
+ ...frenchPatterns,
358
+ ...englishPatterns,
359
+ ...spanishPatterns,
360
+ ...italianPatterns,
361
+ ...germanPatterns,
362
+ ...japanesePatterns,
363
+ ...chinesePatterns
364
+ ];
365
 
366
  for (const pattern of allPatterns) {
367
  const match = lowerText.match(pattern);
 
471
  let confidence = 0.6; // Base confidence
472
 
473
  // Boost confidence for explicit statements
474
+ const lower = fullText.toLowerCase();
475
+ if (
476
+ lower.includes("my name is") ||
477
+ lower.includes("i am called") ||
478
+ lower.includes("je m'appelle") ||
479
+ lower.includes("mon nom est") ||
480
+ lower.includes("je me prénomme") ||
481
+ lower.includes("je me nomme") ||
482
+ lower.includes("me llamo") ||
483
+ lower.includes("mi nombre es") ||
484
+ lower.includes("mi chiamo") ||
485
+ lower.includes("il mio nome è") ||
486
+ lower.includes("ich heiße") ||
487
+ lower.includes("mein name ist") ||
488
+ lower.includes("と申します") ||
489
+ lower.includes("私の名前は") ||
490
+ lower.includes("我叫") ||
491
+ lower.includes("我的名字是")
492
+ ) {
493
  confidence += 0.3;
494
  }
495
 
 
526
  const naturalMemories = [];
527
  const lowerText = text.toLowerCase();
528
 
529
+ // Detect name mentions in natural context (multilingual)
530
+ const namePatterns = [
531
+ // English
532
+ /call me (\w+)/i,
533
+ /(\w+) here[,.]?/i,
534
+ /this is (\w+)/i,
535
+ /(\w+) speaking/i,
536
+ // French
537
+ /appelle-?moi (\w+)/i,
538
+ /on m'appelle (\w+)/i,
539
+ /c'est (\w+)/i,
540
+ // Spanish
541
+ /llámame (\w+)/i,
542
+ /me llaman (\w+)/i,
543
+ /soy (\w+)/i,
544
+ // Italian
545
+ /chiamami (\w+)/i,
546
+ /mi chiamano (\w+)/i,
547
+ /sono (\w+)/i,
548
+ // German
549
+ /nenn mich (\w+)/i,
550
+ /man nennt mich (\w+)/i,
551
+ /ich bin (\w+)/i,
552
+ // Japanese
553
+ /(?:私は)?(\w+)です/i,
554
+ // Chinese
555
+ /我是(\w+)/i,
556
+ /叫我(\w+)/i
557
+ ];
558
 
559
  for (const pattern of namePatterns) {
560
  const match = lowerText.match(pattern);
 
652
  timestamp: memoryData.timestamp || new Date(),
653
  character: memoryData.character || this.selectedCharacter,
654
  isActive: true,
655
+ tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])],
656
  lastModified: new Date(),
657
  accessCount: 0,
658
  importance: this.calculateImportance(memoryData)
 
727
  ...existingMemory,
728
  content: mergedContent,
729
  confidence: mergedConfidence,
730
+ tags: [...new Set([...mergedTags, ...this.deriveMemoryTags(newMemoryData)])], // Remove duplicates
731
  lastModified: new Date(),
732
  accessCount: (existingMemory.accessCount || 0) + 1,
733
  importance: Math.max(existingMemory.importance || 0.5, this.calculateImportance(newMemoryData))
 
786
  calculateImportance(memoryData) {
787
  let importance = 0.5; // Base importance
788
 
789
+ // Category base weights
790
  const categoryWeights = {
791
+ important: 1.0,
792
  personal: 0.9,
793
+ relationships: 0.85,
794
+ goals: 0.75,
795
+ experiences: 0.65,
796
  preferences: 0.6,
797
+ activities: 0.5
 
 
798
  };
799
 
800
  importance = categoryWeights[memoryData.category] || 0.5;
801
 
802
+ const content = (memoryData.content || "").toLowerCase();
803
+ const tags = new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)]);
804
+
805
+ // Heuristic boosts for meaningful relationship milestones and commitments
806
+ const milestoneTags = [
807
+ "relationship:first_meet",
808
+ "relationship:first_date",
809
+ "relationship:first_kiss",
810
+ "relationship:anniversary",
811
+ "relationship:moved_in",
812
+ "relationship:engaged",
813
+ "relationship:married",
814
+ "relationship:breakup"
815
+ ];
816
+ if ([...tags].some(t => milestoneTags.includes(t))) importance += 0.15;
817
+
818
+ // Boundaries and consent are high priority to remember
819
+ if ([...tags].some(t => t.startsWith("boundary:"))) importance += 0.15;
820
+
821
+ // Preferences tied to strong like/dislike
822
+ if (
823
+ content.includes("i love") ||
824
+ content.includes("j'adore") ||
825
+ content.includes("i hate") ||
826
+ content.includes("je déteste")
827
+ ) {
828
+ importance += 0.05;
829
  }
830
 
831
+ // Temporal cues: future commitments or dates
832
+ if (/(\bnext\b|\btomorrow\b|\bce soir\b|\bdemain\b|\bmañana\b|\bdomani\b|\bmorgen\b)/i.test(content)) {
833
+ importance += 0.05;
834
  }
835
 
836
+ // Longer details and high confidence
837
+ if (memoryData.content && memoryData.content.length > 24) importance += 0.05;
838
+ if (memoryData.confidence && memoryData.confidence > 0.9) importance += 0.05;
839
+
840
  return Math.min(1.0, importance);
841
  }
842
 
843
+ // Derive semantic tags from memory content to assist prioritization and merging
844
+ deriveMemoryTags(memoryData) {
845
+ const tags = [];
846
+ const text = (memoryData.content || "").toLowerCase();
847
+ const category = memoryData.category || "";
848
+
849
+ // Relationship status and milestones
850
+ if (/(single|célibataire|soltero|single|ledig)/i.test(text)) tags.push("relationship:status_single");
851
+ if (/(in a relationship|en couple|together|ensemble|pareja|coppia|beziehung)/i.test(text))
852
+ tags.push("relationship:status_in_relationship");
853
+ if (/(engaged|fiancé|fiancée|promis|promised|verlobt)/i.test(text)) tags.push("relationship:status_engaged");
854
+ if (/(married|marié|mariée|casado|sposato|verheiratet)/i.test(text)) tags.push("relationship:status_married");
855
+ if (/(broke up|rupture|separated|separado|separati|getrennt)/i.test(text)) tags.push("relationship:breakup");
856
+ if (/(first date|premier rendez-vous|primera cita|primo appuntamento)/i.test(text)) tags.push("relationship:first_date");
857
+ if (/(first kiss|premier baiser|primer beso|primo bacio)/i.test(text)) tags.push("relationship:first_kiss");
858
+ if (/(anniversary|anniversaire|aniversario|anniversario|jahrestag)/i.test(text)) tags.push("relationship:anniversary");
859
+ if (/(moved in together|emménagé ensemble|mudamos juntos|trasferiti insieme|zusammen eingezogen)/i.test(text))
860
+ tags.push("relationship:moved_in");
861
+ if (/(met at|rencontré à|conocimos en|conosciuti a|kennengelernt)/i.test(text)) tags.push("relationship:first_meet");
862
+
863
+ // Boundaries and consent (keep generic and non-graphic)
864
+ if (/(i don't like|je n'aime pas|no me gusta|non mi piace|ich mag nicht)\s+[^,.!?]+/i.test(text))
865
+ tags.push("boundary:dislike");
866
+ if (/(i prefer|je préfère|prefiero|preferisco|ich bevorzuge)\s+[^,.!?]+/i.test(text)) tags.push("boundary:preference");
867
+ if (/(no|pas)\s+(?:kissing|baiser|beso|bacio|küssen)/i.test(text)) tags.push("boundary:limit");
868
+ if (/(consent|consentement|consentimiento|consenso|einwilligung)/i.test(text)) tags.push("boundary:consent");
869
+
870
+ // Time-related tags
871
+ if (/(today|ce jour|hoy|oggi|heute|今日)/i.test(text)) tags.push("time:today");
872
+ if (/(tomorrow|demain|mañana|domani|morgen|明日)/i.test(text)) tags.push("time:tomorrow");
873
+ if (/(next week|semaine prochaine|la próxima semana|la prossima settimana|nächste woche)/i.test(text))
874
+ tags.push("time:next_week");
875
+
876
+ // Category-specific hints
877
+ if (category === "preferences") tags.push("type:preference");
878
+ if (category === "personal") tags.push("type:personal");
879
+ if (category === "relationships") tags.push("type:relationship");
880
+ if (category === "experiences") tags.push("type:experience");
881
+ if (category === "goals") tags.push("type:goal");
882
+ if (category === "important") tags.push("type:important");
883
+
884
+ return tags;
885
+ }
886
+
887
  async updateMemory(memoryId, updateData) {
888
  if (!this.db) return false;
889
 
 
1107
  if (!this.db) return;
1108
 
1109
  try {
1110
+ // Retrieve all active memories for the current character
1111
  const memories = await this.getAllMemories();
1112
 
1113
+ // If the number of memories exceeds the limit (this.maxMemoryEntries),
1114
+ // delete the least important/oldest ones to keep only the most relevant.
1115
  if (memories.length > this.maxMemoryEntries) {
1116
+ // Sort by importance (confidence) and recency (timestamp)
1117
  memories.sort((a, b) => {
1118
+ // Score = confidence * age (the higher the score, the less priority the memory has)
1119
  const scoreA = a.confidence * (Date.now() - new Date(a.timestamp).getTime());
1120
  const scoreB = b.confidence * (Date.now() - new Date(b.timestamp).getTime());
1121
  return scoreB - scoreA;
1122
  });
1123
 
1124
+ // Delete all memories beyond the limit
1125
  const toDelete = memories.slice(this.maxMemoryEntries);
1126
  for (const memory of toDelete) {
1127
  await this.deleteMemory(memory.id);
 
1234
  const contextLower = context.toLowerCase();
1235
 
1236
  const categoryKeywords = {
1237
+ personal: [
1238
+ "name",
1239
+ "age",
1240
+ "live",
1241
+ "work",
1242
+ "job",
1243
+ "who",
1244
+ "am",
1245
+ "myself",
1246
+ "appelle",
1247
+ "nombre",
1248
+ "chiamo",
1249
+ "heiße",
1250
+ "名前",
1251
+ "名字",
1252
+ "我叫"
1253
+ ],
1254
+ preferences: [
1255
+ "like",
1256
+ "love",
1257
+ "hate",
1258
+ "prefer",
1259
+ "enjoy",
1260
+ "favorite",
1261
+ "dislike",
1262
+ "j'aime",
1263
+ "j'adore",
1264
+ "je préfère",
1265
+ "je déteste",
1266
+ "me gusta",
1267
+ "prefiero",
1268
+ "odio",
1269
+ "mi piace",
1270
+ "preferisco",
1271
+ "ich mag",
1272
+ "ich bevorzuge",
1273
+ "hasse"
1274
+ ],
1275
+ relationships: [
1276
+ "family",
1277
+ "friend",
1278
+ "wife",
1279
+ "husband",
1280
+ "partner",
1281
+ "mother",
1282
+ "father",
1283
+ "girlfriend",
1284
+ "boyfriend",
1285
+ "anniversary",
1286
+ "date",
1287
+ "kiss",
1288
+ "move in",
1289
+ "famille",
1290
+ "ami",
1291
+ "copine",
1292
+ "copain",
1293
+ "anniversaire",
1294
+ "rendez-vous",
1295
+ "baiser",
1296
+ "emménagé",
1297
+ "pareja",
1298
+ "cita",
1299
+ "beso",
1300
+ "aniversario",
1301
+ "mudarnos",
1302
+ "fidanzata",
1303
+ "fidanzato",
1304
+ "anniversario",
1305
+ "bacio",
1306
+ "trasferiti",
1307
+ "freundin",
1308
+ "freund",
1309
+ "jahrestag",
1310
+ "kuss",
1311
+ "eingezogen"
1312
+ ],
1313
+ activities: [
1314
+ "play",
1315
+ "hobby",
1316
+ "sport",
1317
+ "activity",
1318
+ "practice",
1319
+ "do",
1320
+ "joue",
1321
+ "passe-temps",
1322
+ "hobby",
1323
+ "juego",
1324
+ "pasatiempo",
1325
+ "gioco",
1326
+ "passatempo",
1327
+ "spiele",
1328
+ "hobby"
1329
+ ],
1330
+ goals: [
1331
+ "want",
1332
+ "plan",
1333
+ "goal",
1334
+ "dream",
1335
+ "hope",
1336
+ "wish",
1337
+ "future",
1338
+ "veux",
1339
+ "objectif",
1340
+ "apprends",
1341
+ "aprendo",
1342
+ "voglio",
1343
+ "obiettivo",
1344
+ "lerne",
1345
+ "ziel"
1346
+ ],
1347
+ experiences: [
1348
+ "remember",
1349
+ "happened",
1350
+ "story",
1351
+ "experience",
1352
+ "time",
1353
+ "we met",
1354
+ "first date",
1355
+ "first kiss",
1356
+ "anniversary",
1357
+ "rencontré",
1358
+ "premier rendez-vous",
1359
+ "premier baiser",
1360
+ "anniversaire",
1361
+ "conocimos",
1362
+ "primera cita",
1363
+ "primer beso",
1364
+ "aniversario",
1365
+ "conosciuti",
1366
+ "primo appuntamento",
1367
+ "primo bacio",
1368
+ "anniversario",
1369
+ "kennengelernt",
1370
+ "erstes date",
1371
+ "erster kuss",
1372
+ "jahrestag"
1373
+ ],
1374
+ important: [
1375
+ "important",
1376
+ "remember",
1377
+ "special",
1378
+ "never forget",
1379
+ "important",
1380
+ "souvenir",
1381
+ "spécial",
1382
+ "importante",
1383
+ "recuerda",
1384
+ "importante",
1385
+ "ricorda",
1386
+ "wichtig",
1387
+ "erinnere"
1388
+ ]
1389
  };
1390
 
1391
  const keywords = categoryKeywords[category] || [];
 
1536
  }
1537
 
1538
  window.KimiMemorySystem = KimiMemorySystem;
1539
+ export default KimiMemorySystem;
1540
+
1541
+ window.KimiMemorySystem = KimiMemorySystem;
kimi-js/kimi-memory-ui.js CHANGED
@@ -139,9 +139,12 @@ class KimiMemoryUI {
139
  if (v.ended) {
140
  if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
141
  } else if (v.paused) {
142
- v.play().catch(() => {
143
- if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
144
- });
 
 
 
145
  }
146
  } catch {}
147
  }
@@ -234,6 +237,10 @@ class KimiMemoryUI {
234
  const isLongContent = memory.content.length > previewLength;
235
  const previewText = isLongContent ? memory.content.substring(0, previewLength) + "..." : memory.content;
236
  const wordCount = memory.content.split(/\s+/).length;
 
 
 
 
237
 
238
  html += `
239
  <div class="memory-item ${isAutomatic ? "memory-auto" : "memory-manual"}" data-memory-id="${memory.id}">
@@ -242,6 +249,7 @@ class KimiMemoryUI {
242
  <span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "🤖 Auto" : "✋ Manual"}</span>
243
  <span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span>
244
  ${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""}
 
245
  </div>
246
  </div>
247
  <div class="memory-preview">
@@ -261,6 +269,7 @@ class KimiMemoryUI {
261
  : ""
262
  }
263
  </div>
 
264
  <div class="memory-meta">
265
  <span class="memory-date">${this.formatDate(memory.timestamp)}</span>
266
  ${
@@ -294,6 +303,42 @@ class KimiMemoryUI {
294
  memoryList.innerHTML = html;
295
  }
296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  formatCategoryName(category) {
298
  const names = {
299
  personal: "Personal Information",
 
139
  if (v.ended) {
140
  if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
141
  } else if (v.paused) {
142
+ // Use centralized video utility for play
143
+ window.KimiVideoManager.getVideoElement(v)
144
+ .play()
145
+ .catch(() => {
146
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
147
+ });
148
  }
149
  } catch {}
150
  }
 
237
  const isLongContent = memory.content.length > previewLength;
238
  const previewText = isLongContent ? memory.content.substring(0, previewLength) + "..." : memory.content;
239
  const wordCount = memory.content.split(/\s+/).length;
240
+ const importance = typeof memory.importance === "number" ? memory.importance : 0.5;
241
+ const importanceLevel = this.getImportanceLevelFromValue(importance);
242
+ const importancePct = Math.round(importance * 100);
243
+ const tagsHtml = this.renderTags(memory.tags || []);
244
 
245
  html += `
246
  <div class="memory-item ${isAutomatic ? "memory-auto" : "memory-manual"}" data-memory-id="${memory.id}">
 
249
  <span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "🤖 Auto" : "✋ Manual"}</span>
250
  <span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span>
251
  ${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""}
252
+ <span class="memory-importance importance-${importanceLevel}" title="Importance: ${importancePct}% (${importanceLevel})">${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)}</span>
253
  </div>
254
  </div>
255
  <div class="memory-preview">
 
269
  : ""
270
  }
271
  </div>
272
+ ${tagsHtml}
273
  <div class="memory-meta">
274
  <span class="memory-date">${this.formatDate(memory.timestamp)}</span>
275
  ${
 
303
  memoryList.innerHTML = html;
304
  }
305
 
306
+ // Map importance value [0..1] to level string
307
+ getImportanceLevelFromValue(value) {
308
+ if (value >= 0.8) return "high";
309
+ if (value >= 0.6) return "medium";
310
+ return "low";
311
+ }
312
+
313
+ // Render tags as compact chips; show up to 4 then "+N"
314
+ renderTags(tags) {
315
+ if (!Array.isArray(tags) || tags.length === 0) return "";
316
+ const maxVisible = 4;
317
+ const visible = tags.slice(0, maxVisible);
318
+ const moreCount = tags.length - visible.length;
319
+
320
+ const escape = txt =>
321
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
322
+ ? window.KimiValidationUtils.escapeHtml(String(txt))
323
+ : String(txt);
324
+
325
+ const classify = tag => {
326
+ if (tag.startsWith("relationship:")) return "tag-relationship";
327
+ if (tag.startsWith("boundary:")) return "tag-boundary";
328
+ if (tag.startsWith("time:")) return "tag-time";
329
+ if (tag.startsWith("type:")) return "tag-type";
330
+ return "tag-generic";
331
+ };
332
+
333
+ const chips = visible
334
+ .map(tag => `<span class="memory-tag ${classify(tag)}" title="${escape(tag)}">${escape(tag)}</span>`)
335
+ .join("");
336
+
337
+ const moreChip = moreCount > 0 ? `<span class="memory-tag tag-more" title="${moreCount} more">+${moreCount}</span>` : "";
338
+
339
+ return `<div class="memory-tags">${chips}${moreChip}</div>`;
340
+ }
341
+
342
  formatCategoryName(category) {
343
  const names = {
344
  personal: "Personal Information",
kimi-js/kimi-memory.js CHANGED
@@ -26,8 +26,12 @@ class KimiMemory {
26
  // Start with lower favorability level - relationships must be built over time
27
  this.favorabilityLevel = await this.db.getPreference(`favorabilityLevel_${this.selectedCharacter}`, 50);
28
 
29
- // Load affection trait from personality database - CRITICAL FIX
30
- this.affectionTrait = await this.db.getPersonalityTrait("affection", 50, this.selectedCharacter);
 
 
 
 
31
 
32
  this.preferences = {
33
  voiceRate: await this.db.getPreference(`voiceRate_${this.selectedCharacter}`, 1.1),
@@ -38,7 +42,7 @@ class KimiMemory {
38
  favoriteWords: await this.db.getPreference(`favoriteWords_${this.selectedCharacter}`, []),
39
  emotionalState: await this.db.getPreference(`emotionalState_${this.selectedCharacter}`, "neutral")
40
  };
41
- this.affectionTrait = await this.db.getPersonalityTrait("affection", 80, this.selectedCharacter);
42
  this.isReady = true;
43
  this.updateFavorabilityBar();
44
  } catch (error) {
@@ -46,18 +50,27 @@ class KimiMemory {
46
  }
47
  }
48
 
49
- async saveConversation(userText, kimiResponse) {
50
  if (!this.db) return;
51
 
52
  try {
53
  const character = await this.db.getSelectedCharacter();
54
  await this.db.saveConversation(userText, kimiResponse, this.favorabilityLevel, new Date(), character);
55
 
 
56
  let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
57
  total = Number(total) + 1;
58
  await this.db.setPreference(`totalInteractions_${character}`, total);
59
  this.preferences.totalInteractions = total;
60
 
 
 
 
 
 
 
 
 
61
  let first = await this.db.getPreference(`firstInteraction_${character}`, null);
62
  if (!first) {
63
  first = new Date().toISOString();
@@ -129,3 +142,4 @@ class KimiMemory {
129
 
130
  // Export to global scope
131
  window.KimiMemory = KimiMemory;
 
 
26
  // Start with lower favorability level - relationships must be built over time
27
  this.favorabilityLevel = await this.db.getPreference(`favorabilityLevel_${this.selectedCharacter}`, 50);
28
 
29
+ // Load affection trait from personality database with coherent defaults
30
+ const charDefAff =
31
+ (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[this.selectedCharacter]?.traits?.affection) || null;
32
+ const genericAff = (window.getTraitDefaults && window.getTraitDefaults().affection) || 65;
33
+ const defaultAff = typeof charDefAff === "number" ? charDefAff : genericAff;
34
+ this.affectionTrait = await this.db.getPersonalityTrait("affection", defaultAff, this.selectedCharacter);
35
 
36
  this.preferences = {
37
  voiceRate: await this.db.getPreference(`voiceRate_${this.selectedCharacter}`, 1.1),
 
42
  favoriteWords: await this.db.getPreference(`favoriteWords_${this.selectedCharacter}`, []),
43
  emotionalState: await this.db.getPreference(`emotionalState_${this.selectedCharacter}`, "neutral")
44
  };
45
+ // affectionTrait already loaded above with coherent default
46
  this.isReady = true;
47
  this.updateFavorabilityBar();
48
  } catch (error) {
 
50
  }
51
  }
52
 
53
+ async saveConversation(userText, kimiResponse, tokenInfo = null) {
54
  if (!this.db) return;
55
 
56
  try {
57
  const character = await this.db.getSelectedCharacter();
58
  await this.db.saveConversation(userText, kimiResponse, this.favorabilityLevel, new Date(), character);
59
 
60
+ // Legacy interactions counter kept for backward compatibility (not shown in UI now)
61
  let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
62
  total = Number(total) + 1;
63
  await this.db.setPreference(`totalInteractions_${character}`, total);
64
  this.preferences.totalInteractions = total;
65
 
66
+ // Update tokens usage if provided (in/out)
67
+ if (tokenInfo && typeof tokenInfo.tokensIn === "number" && typeof tokenInfo.tokensOut === "number") {
68
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
69
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
70
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokenInfo.tokensIn);
71
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokenInfo.tokensOut);
72
+ }
73
+
74
  let first = await this.db.getPreference(`firstInteraction_${character}`, null);
75
  if (!first) {
76
  first = new Date().toISOString();
 
142
 
143
  // Export to global scope
144
  window.KimiMemory = KimiMemory;
145
+ export default KimiMemory;
kimi-js/kimi-module.js CHANGED
@@ -123,14 +123,13 @@ class KimiDataManager extends KimiBaseManager {
123
  }
124
 
125
  const confirmClean = confirm("Do you want to delete ALL conversations?\n\nThis action is irreversible!");
126
-
127
  if (!confirmClean) {
128
  return;
129
  }
130
 
131
  try {
132
- // Clear all conversations directly
133
- await this.db.db.conversations.clear();
134
 
135
  if (typeof window.loadChatHistory === "function") {
136
  window.loadChatHistory();
@@ -227,26 +226,6 @@ class KimiDataManager extends KimiBaseManager {
227
  }
228
 
229
  // Fonctions utilitaires et logique (référencent window.*)
230
- function getPersonalityAverage(traits) {
231
- if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
232
- return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
233
- }
234
- if (window.getPersonalityAverage) {
235
- try {
236
- return window.getPersonalityAverage(traits);
237
- } catch {}
238
- }
239
- const keys = ["affection", "romance", "empathy", "playfulness", "humor"];
240
- let sum = 0;
241
- let count = 0;
242
- keys.forEach(key => {
243
- if (typeof traits[key] === "number") {
244
- sum += traits[key];
245
- count++;
246
- }
247
- });
248
- return count > 0 ? sum / count : 50;
249
- }
250
 
251
  function updateFavorabilityLabel(characterKey) {
252
  const favorabilityLabel = document.getElementById("favorability-label");
@@ -370,167 +349,7 @@ async function getBasicResponse(reaction) {
370
  return i18n ? i18n.t("fallback_technical_error") : "Sorry, I'm having technical difficulties! 💕";
371
  }
372
 
373
- async function updatePersonalityTraitsFromEmotion(emotion, text) {
374
- const kimiDB = window.kimiDB;
375
- const kimiMemory = window.kimiMemory;
376
- if (!kimiDB) {
377
- console.warn("KimiDB not available for personality updates");
378
- return;
379
- }
380
-
381
- const selectedCharacter = await kimiDB.getSelectedCharacter();
382
- const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
383
-
384
- // Use unified emotion system defaults - CRITICAL FIX
385
- const getUnifiedDefaults = () => {
386
- if (window.KimiEmotionSystem) {
387
- const emotionSystem = new window.KimiEmotionSystem(kimiDB);
388
- return emotionSystem.TRAIT_DEFAULTS;
389
- }
390
- return { affection: 65, romance: 50, empathy: 75, playfulness: 55, humor: 60, intelligence: 70 };
391
- };
392
-
393
- const defaults = getUnifiedDefaults();
394
- let affection = traits.affection || defaults.affection;
395
- let romance = traits.romance || defaults.romance;
396
- let empathy = traits.empathy || defaults.empathy;
397
- let playfulness = traits.playfulness || defaults.playfulness;
398
- let humor = traits.humor || defaults.humor;
399
- let intelligence = traits.intelligence || defaults.intelligence;
400
-
401
- function adjustUp(val, amount) {
402
- if (val >= 90) return val + amount * 0.2;
403
- if (val >= 75) return val + amount * 0.5;
404
- if (val >= 50) return val + amount * 0.8;
405
- return val + amount;
406
- }
407
- function adjustDown(val, amount) {
408
- if (val <= 10) return val - amount * 0.2;
409
- if (val <= 25) return val - amount * 0.5;
410
- if (val <= 50) return val - amount * 0.8;
411
- return val - amount;
412
- }
413
-
414
- // Emotion-based adjustments - More realistic progression
415
- switch (emotion) {
416
- case "positive":
417
- affection = Math.min(100, adjustUp(affection, 0.3));
418
- empathy = Math.min(100, adjustUp(empathy, 0.2));
419
- playfulness = Math.min(100, adjustUp(playfulness, 0.2));
420
- humor = Math.min(100, adjustUp(humor, 0.2));
421
- romance = Math.min(100, adjustUp(romance, 0.2));
422
- break;
423
- case "negative":
424
- affection = Math.max(0, adjustDown(affection, 0.4));
425
- empathy = Math.min(100, adjustUp(empathy, 0.3)); // Empathy increases with negative emotions
426
- break;
427
- case "romantic":
428
- romance = Math.min(100, adjustUp(romance, 0.8));
429
- affection = Math.min(100, adjustUp(affection, 0.4));
430
- break;
431
- case "laughing":
432
- humor = Math.min(100, adjustUp(humor, 0.7));
433
- playfulness = Math.min(100, adjustUp(playfulness, 0.3));
434
- break;
435
- case "dancing":
436
- playfulness = Math.min(100, adjustUp(playfulness, 1.0));
437
- break;
438
- case "shy":
439
- affection = Math.max(0, adjustDown(affection, 0.2));
440
- break;
441
- case "confident":
442
- affection = Math.min(100, adjustUp(affection, 0.4));
443
- break;
444
- case "flirtatious":
445
- romance = Math.min(100, adjustUp(romance, 0.6));
446
- playfulness = Math.min(100, adjustUp(playfulness, 0.4));
447
- break;
448
- }
449
-
450
- // Use user's selected language from preferences
451
- const selectedLanguage = await kimiDB.getPreference("selectedLanguage", "en");
452
-
453
- const getRomanticWords = () => {
454
- return (
455
- window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.romantic ||
456
- window.KIMI_CONTEXT_KEYWORDS?.en?.romantic || [
457
- "love",
458
- "romantic",
459
- "kiss",
460
- "cuddle",
461
- "hug",
462
- "dear",
463
- "honey",
464
- "sweetheart"
465
- ]
466
- );
467
- };
468
-
469
- const getHumorWords = () => {
470
- return (
471
- window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.laughing ||
472
- window.KIMI_CONTEXT_KEYWORDS?.en?.laughing || ["joke", "funny", "lol", "haha", "hilarious"]
473
- );
474
- };
475
-
476
- const romanticWords = getRomanticWords();
477
- const humorWords = getHumorWords();
478
-
479
- // Check for romantic content
480
- const romanticPattern = new RegExp(`(${romanticWords.join("|")})`, "i");
481
- if (text.match(romanticPattern)) {
482
- romance = Math.min(100, adjustUp(romance, 0.5));
483
- affection = Math.min(100, adjustUp(affection, 0.5));
484
- }
485
-
486
- // Check for humor content
487
- const humorPattern = new RegExp(`(${humorWords.join("|")})`, "i");
488
- if (text.match(humorPattern)) {
489
- humor = Math.min(100, adjustUp(humor, 2));
490
- playfulness = Math.min(100, adjustUp(playfulness, 1));
491
- }
492
-
493
- const updatedTraits = {
494
- affection: Math.round(affection),
495
- romance: Math.round(romance),
496
- empathy: Math.round(empathy),
497
- playfulness: Math.round(playfulness),
498
- humor: Math.round(humor),
499
- intelligence: Math.round(intelligence)
500
- };
501
-
502
- // Single batch operation instead of 6 individual saves
503
- await kimiDB.setPersonalityBatch(updatedTraits, selectedCharacter);
504
-
505
- if (kimiMemory) {
506
- kimiMemory.affectionTrait = affection;
507
- if (kimiMemory.updateFavorabilityBar) {
508
- kimiMemory.updateFavorabilityBar();
509
- }
510
- }
511
-
512
- // Update all sliders at once for better UX
513
- updateSlider("trait-affection", affection);
514
- updateSlider("trait-romance", romance);
515
- updateSlider("trait-empathy", empathy);
516
- updateSlider("trait-playfulness", playfulness);
517
- updateSlider("trait-humor", humor);
518
- updateSlider("trait-intelligence", intelligence);
519
-
520
- // Update video context based on new personality
521
- if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
522
- window.kimiVideo.setMoodByPersonality(updatedTraits);
523
- }
524
-
525
- // Notify other systems about trait changes
526
- if (window.dispatchEvent) {
527
- window.dispatchEvent(
528
- new CustomEvent("personalityUpdated", {
529
- detail: { character: selectedCharacter, traits: updatedTraits }
530
- })
531
- );
532
- }
533
- }
534
 
535
  async function analyzeAndReact(text, useAdvancedLLM = true) {
536
  const kimiDB = window.kimiDB;
@@ -557,7 +376,7 @@ async function analyzeAndReact(text, useAdvancedLLM = true) {
557
 
558
  const selectedCharacter = await kimiDB.getSelectedCharacter();
559
  const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
560
- const avg = getPersonalityAverage(traits);
561
  const affection = typeof traits.affection === "number" ? traits.affection : 80;
562
  const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
563
 
@@ -566,29 +385,15 @@ async function analyzeAndReact(text, useAdvancedLLM = true) {
566
  kimiVideo.startListening();
567
  }
568
 
569
- await updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
 
 
570
 
571
  if (useAdvancedLLM && isSystemReady && kimiLLM) {
572
  try {
573
  const providerPref = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
574
- let apiKey = null;
575
- if (kimiDB) {
576
- if (providerPref === "ollama") {
577
- apiKey = "__local__"; // no key required
578
- } else if (providerPref === "openrouter") {
579
- apiKey = await kimiDB.getPreference("openrouterApiKey");
580
- } else {
581
- const keyPrefMap = {
582
- openai: "apiKey_openai",
583
- groq: "apiKey_groq",
584
- together: "apiKey_together",
585
- deepseek: "apiKey_deepseek",
586
- "openai-compatible": "apiKey_custom"
587
- };
588
- const keyPref = keyPrefMap[providerPref] || "llmApiKey";
589
- apiKey = await kimiDB.getPreference(keyPref);
590
- }
591
- }
592
 
593
  if (apiKey && apiKey.trim() !== "") {
594
  try {
@@ -603,11 +408,6 @@ async function analyzeAndReact(text, useAdvancedLLM = true) {
603
  }
604
  } catch (e) {}
605
 
606
- // Extract memories from conversation
607
- if (window.kimiMemorySystem) {
608
- await window.kimiMemorySystem.extractMemoryFromText(sanitizedText, response);
609
- }
610
-
611
  const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
612
  // If user explicitly requested dancing, show dancing during Kimi's response
613
  const lang = await kimiDB.getPreference("selectedLanguage", "en");
@@ -655,24 +455,8 @@ async function analyzeAndReact(text, useAdvancedLLM = true) {
655
  } catch (e) {}
656
  // Still show API key message if no key is configured
657
  const providerPref2 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
658
- let apiKey = null;
659
- if (kimiDB) {
660
- if (providerPref2 === "ollama") {
661
- apiKey = "__local__";
662
- } else if (providerPref2 === "openrouter") {
663
- apiKey = await kimiDB.getPreference("openrouterApiKey");
664
- } else {
665
- const keyPrefMap = {
666
- openai: "apiKey_openai",
667
- groq: "apiKey_groq",
668
- together: "apiKey_together",
669
- deepseek: "apiKey_deepseek",
670
- "openai-compatible": "apiKey_custom"
671
- };
672
- const keyPref = keyPrefMap[providerPref2] || "llmApiKey";
673
- apiKey = await kimiDB.getPreference(keyPref);
674
- }
675
- }
676
  if (!apiKey || apiKey.trim() === "") {
677
  response = window.KimiFallbackManager
678
  ? window.KimiFallbackManager.getFallbackMessage("api_missing")
@@ -696,24 +480,8 @@ async function analyzeAndReact(text, useAdvancedLLM = true) {
696
  } else {
697
  // System not ready - check if it's because of missing API key
698
  const providerPref3 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
699
- let apiKey = null;
700
- if (kimiDB) {
701
- if (providerPref3 === "ollama") {
702
- apiKey = "__local__";
703
- } else if (providerPref3 === "openrouter") {
704
- apiKey = await kimiDB.getPreference("openrouterApiKey");
705
- } else {
706
- const keyPrefMap = {
707
- openai: "apiKey_openai",
708
- groq: "apiKey_groq",
709
- together: "apiKey_together",
710
- deepseek: "apiKey_deepseek",
711
- "openai-compatible": "apiKey_custom"
712
- };
713
- const keyPref = keyPrefMap[providerPref3] || "llmApiKey";
714
- apiKey = await kimiDB.getPreference(keyPref);
715
- }
716
- }
717
  if (!apiKey || apiKey.trim() === "") {
718
  response = window.KimiFallbackManager
719
  ? window.KimiFallbackManager.getFallbackMessage("api_missing")
@@ -734,7 +502,22 @@ async function analyzeAndReact(text, useAdvancedLLM = true) {
734
  }
735
  }
736
 
737
- await kimiMemory.saveConversation(sanitizedText, response);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
738
 
739
  // Extract memories automatically from conversation if system is enabled
740
  if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
@@ -944,15 +727,7 @@ async function loadSettingsData() {
944
  // Update API key input
945
  const apiKeyInput = document.getElementById("openrouter-api-key");
946
  if (apiKeyInput) {
947
- const keyPrefMap = {
948
- openrouter: "openrouterApiKey",
949
- openai: "apiKey_openai",
950
- groq: "apiKey_groq",
951
- together: "apiKey_together",
952
- deepseek: "apiKey_deepseek",
953
- "openai-compatible": "apiKey_custom"
954
- };
955
- const keyPref = keyPrefMap[provider];
956
  const providerKey = keyPref && preferences[keyPref] ? preferences[keyPref] : genericKey;
957
  apiKeyInput.value = providerKey || "";
958
  }
@@ -971,16 +746,9 @@ async function loadSettingsData() {
971
  // For non-OpenRouter providers we keep placeholder per provider; the value is already set above.
972
  const apiKeyLabel = document.getElementById("api-key-label");
973
  if (apiKeyLabel) {
974
- const labelByProvider = {
975
- openrouter: "OpenRouter API Key",
976
- openai: "OpenAI API Key",
977
- groq: "Groq API Key",
978
- together: "Together API Key",
979
- deepseek: "DeepSeek API Key",
980
- "openai-compatible": "API Key",
981
- ollama: "API Key"
982
- };
983
- apiKeyLabel.textContent = labelByProvider[provider] || "API Key";
984
  }
985
 
986
  // Load system prompt
@@ -1053,19 +821,24 @@ async function updateStats() {
1053
  const kimiDB = window.kimiDB;
1054
  if (!kimiDB) return;
1055
  const character = await kimiDB.getSelectedCharacter();
1056
- const totalInteractions = await kimiDB.getPreference(`totalInteractions_${character}`, 0);
1057
- const affectionTrait = await kimiDB.getPersonalityTrait("affection", 80, character);
 
 
 
 
 
1058
  const conversations = await kimiDB.getAllConversations(character);
1059
  let firstInteraction = await kimiDB.getPreference(`firstInteraction_${character}`);
1060
  if (!firstInteraction && conversations.length > 0) {
1061
  firstInteraction = conversations[0].timestamp;
1062
  await kimiDB.setPreference(`firstInteraction_${character}`, firstInteraction);
1063
  }
1064
- const totalEl = document.getElementById("total-interactions");
1065
  const favorabilityEl = document.getElementById("current-favorability");
1066
  const conversationsEl = document.getElementById("conversations-count");
1067
  const daysEl = document.getElementById("days-together");
1068
- if (totalEl) totalEl.textContent = totalInteractions;
1069
  if (favorabilityEl) {
1070
  const v = Number(affectionTrait) || 0;
1071
  favorabilityEl.textContent = `${Math.max(0, Math.min(100, v)).toFixed(2)}%`;
@@ -1110,7 +883,7 @@ async function syncLLMMaxTokensSlider() {
1110
  const llmMaxTokensSlider = document.getElementById("llm-max-tokens");
1111
  const llmMaxTokensValue = document.getElementById("llm-max-tokens-value");
1112
  if (llmMaxTokensSlider && llmMaxTokensValue && kimiDB) {
1113
- const saved = await kimiDB.getPreference("llmMaxTokens", 100);
1114
  llmMaxTokensSlider.value = saved;
1115
  llmMaxTokensValue.textContent = saved;
1116
  }
@@ -1284,20 +1057,6 @@ async function loadAvailableModels() {
1284
  modelDiv.classList.add("selected");
1285
  console.log(`🤖 Model switched to: ${model.name}`);
1286
 
1287
- // Sync the visible Model ID field and persist selection
1288
- try {
1289
- const provider = (await window.kimiDB.getPreference("llmProvider", "openrouter")) || "openrouter";
1290
- const modelIdInput = document.getElementById("llm-model-id");
1291
- if (provider === "openrouter" && modelIdInput) {
1292
- modelIdInput.value = id;
1293
- modelIdInput.readOnly = true;
1294
- modelIdInput.setAttribute("aria-readonly", "true");
1295
- }
1296
- await window.kimiDB.setPreference("llmModelId", id);
1297
- } catch (syncErr) {
1298
- console.warn("Model ID UI sync failed:", syncErr);
1299
- }
1300
-
1301
  // Show brief feedback to user
1302
  const feedback = document.createElement("div");
1303
  feedback.textContent = `Model changed to ${model.name}`;
@@ -1694,17 +1453,7 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1694
  // Use batch operation for all pending changes
1695
  await kimiDB.setPersonalityBatch(pendingTraitChanges);
1696
 
1697
- // Update affection if it was changed
1698
- if (pendingTraitChanges.affection && kimiMemory) {
1699
- await kimiMemory.updateAffectionTrait();
1700
- }
1701
-
1702
- // Update video context based on new personality values
1703
- if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1704
- const selectedCharacter = await kimiDB.getSelectedCharacter();
1705
- const allTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
1706
- window.kimiVideo.setMoodByPersonality(allTraits);
1707
- }
1708
  } catch (error) {
1709
  console.error("Error batch saving personality traits:", error);
1710
  }
@@ -1830,16 +1579,65 @@ function setupSettingsListeners(kimiDB, kimiMemory) {
1830
 
1831
  // Exposer globalement (Note: KimiMemory and KimiAppearanceManager are now in separate files)
1832
  window.KimiDataManager = KimiDataManager;
1833
- window.getPersonalityAverage = getPersonalityAverage;
1834
  window.updateFavorabilityLabel = updateFavorabilityLabel;
1835
  window.loadCharacterSection = loadCharacterSection;
1836
  window.getBasicResponse = getBasicResponse;
1837
- window.updatePersonalityTraitsFromEmotion = updatePersonalityTraitsFromEmotion;
1838
  window.analyzeAndReact = analyzeAndReact;
1839
  window.addMessageToChat = addMessageToChat;
1840
  window.loadChatHistory = loadChatHistory;
1841
  window.loadSettingsData = loadSettingsData;
1842
  window.updateSlider = updateSlider;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1843
  window.updatePersonalitySliders = updatePersonalitySliders;
1844
  window.updateStats = updateStats;
1845
  window.initializeAllSliders = initializeAllSliders;
@@ -1867,44 +1665,27 @@ document.addEventListener("DOMContentLoaded", function () {
1867
  // Refresh UI models list when the LLM model changes programmatically
1868
  try {
1869
  window.addEventListener("llmModelChanged", () => {
1870
- try {
1871
- // Refresh models selection state
1872
- if (typeof window.loadAvailableModels === "function") {
1873
- window.loadAvailableModels();
1874
- }
1875
- // Also update the Model ID input when on OpenRouter
1876
- setTimeout(async () => {
1877
- try {
1878
- const provider = (await window.kimiDB.getPreference("llmProvider", "openrouter")) || "openrouter";
1879
- const modelIdInput = document.getElementById("llm-model-id");
1880
- if (provider === "openrouter" && modelIdInput && window.kimiLLM) {
1881
- modelIdInput.value = window.kimiLLM.currentModel || modelIdInput.value;
1882
- modelIdInput.readOnly = true;
1883
- modelIdInput.setAttribute("aria-readonly", "true");
1884
- await window.kimiDB.setPreference("llmModelId", window.kimiLLM.currentModel || "");
1885
- }
1886
- } catch (e2) {
1887
- console.warn("Failed to sync llm-model-id on event:", e2);
1888
- }
1889
- }, 0);
1890
- } catch (e) {}
1891
  });
1892
  } catch (e) {}
1893
 
1894
  // Typing indicator wiring
1895
  try {
1896
- // Ensure API key field never triggers password manager prompts
1897
  setTimeout(() => {
1898
  const apiInput = document.getElementById("openrouter-api-key");
1899
  if (apiInput) {
1900
  apiInput.setAttribute("autocomplete", "new-password");
1901
- apiInput.removeAttribute("name");
1902
  apiInput.setAttribute("data-lpignore", "true");
1903
  apiInput.setAttribute("data-1p-ignore", "true");
1904
  apiInput.setAttribute("data-bwignore", "true");
1905
  apiInput.setAttribute("data-form-type", "other");
1906
  apiInput.setAttribute("autocapitalize", "none");
1907
  apiInput.setAttribute("autocorrect", "off");
 
1908
  }
1909
  }, 300);
1910
 
@@ -1942,21 +1723,20 @@ async function syncPersonalityTraits(characterName = null) {
1942
  const selectedCharacter = characterName || (await kimiDB.getSelectedCharacter());
1943
  const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
1944
 
1945
- // Use unified defaults from emotion system
1946
  const getRequiredTraits = () => {
 
 
1947
  if (window.KimiEmotionSystem) {
1948
  const emotionSystem = new window.KimiEmotionSystem(kimiDB);
1949
- return emotionSystem.TRAIT_DEFAULTS;
 
 
 
 
1950
  }
1951
- // Fallback (should match KimiEmotionSystem.TRAIT_DEFAULTS exactly)
1952
- return {
1953
- affection: 65,
1954
- playfulness: 55,
1955
- intelligence: 70,
1956
- empathy: 75,
1957
- humor: 60,
1958
- romance: 50
1959
- };
1960
  };
1961
 
1962
  const requiredTraits = getRequiredTraits();
@@ -1991,10 +1771,7 @@ async function syncPersonalityTraits(characterName = null) {
1991
  }
1992
  }
1993
 
1994
- // Update video context
1995
- if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1996
- window.kimiVideo.setMoodByPersonality(updatedTraits);
1997
- }
1998
 
1999
  return updatedTraits;
2000
  }
 
123
  }
124
 
125
  const confirmClean = confirm("Do you want to delete ALL conversations?\n\nThis action is irreversible!");
 
126
  if (!confirmClean) {
127
  return;
128
  }
129
 
130
  try {
131
+ // Centralized: use kimi-database.js cleanOldConversations for all deletion logic
132
+ await this.db.cleanOldConversations();
133
 
134
  if (typeof window.loadChatHistory === "function") {
135
  window.loadChatHistory();
 
226
  }
227
 
228
  // Fonctions utilitaires et logique (référencent window.*)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
  function updateFavorabilityLabel(characterKey) {
231
  const favorabilityLabel = document.getElementById("favorability-label");
 
349
  return i18n ? i18n.t("fallback_technical_error") : "Sorry, I'm having technical difficulties! 💕";
350
  }
351
 
352
+ // Déporté vers KimiEmotionSystem: utiliser window.updatePersonalityTraitsFromEmotion
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
  async function analyzeAndReact(text, useAdvancedLLM = true) {
355
  const kimiDB = window.kimiDB;
 
376
 
377
  const selectedCharacter = await kimiDB.getSelectedCharacter();
378
  const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
379
+ const avg = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
380
  const affection = typeof traits.affection === "number" ? traits.affection : 80;
381
  const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
382
 
 
385
  kimiVideo.startListening();
386
  }
387
 
388
+ if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
389
+ await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
390
+ }
391
 
392
  if (useAdvancedLLM && isSystemReady && kimiLLM) {
393
  try {
394
  const providerPref = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
395
+ const apiKey =
396
+ kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref) : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
  if (apiKey && apiKey.trim() !== "") {
399
  try {
 
408
  }
409
  } catch (e) {}
410
 
 
 
 
 
 
411
  const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
412
  // If user explicitly requested dancing, show dancing during Kimi's response
413
  const lang = await kimiDB.getPreference("selectedLanguage", "en");
 
455
  } catch (e) {}
456
  // Still show API key message if no key is configured
457
  const providerPref2 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
458
+ const apiKey =
459
+ kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref2) : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
460
  if (!apiKey || apiKey.trim() === "") {
461
  response = window.KimiFallbackManager
462
  ? window.KimiFallbackManager.getFallbackMessage("api_missing")
 
480
  } else {
481
  // System not ready - check if it's because of missing API key
482
  const providerPref3 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
483
+ const apiKey =
484
+ kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref3) : null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
485
  if (!apiKey || apiKey.trim() === "") {
486
  response = window.KimiFallbackManager
487
  ? window.KimiFallbackManager.getFallbackMessage("api_missing")
 
502
  }
503
  }
504
 
505
+ // Use token usage collected by LLM manager if available
506
+ let tokenInfo = null;
507
+ if (window._lastKimiTokenUsage) {
508
+ tokenInfo = window._lastKimiTokenUsage;
509
+ window._lastKimiTokenUsage = null; // consume once
510
+ } else if (window.KimiTokenUtils) {
511
+ // Fallback approximate (no system prompt included)
512
+ try {
513
+ const est = window.KimiTokenUtils.estimate;
514
+ tokenInfo = { tokensIn: est(sanitizedText), tokensOut: est(response) };
515
+ } catch {}
516
+ }
517
+ await kimiMemory.saveConversation(sanitizedText, response, tokenInfo);
518
+ if (typeof updateStats === "function") {
519
+ updateStats();
520
+ }
521
 
522
  // Extract memories automatically from conversation if system is enabled
523
  if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
 
727
  // Update API key input
728
  const apiKeyInput = document.getElementById("openrouter-api-key");
729
  if (apiKeyInput) {
730
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : null;
 
 
 
 
 
 
 
 
731
  const providerKey = keyPref && preferences[keyPref] ? preferences[keyPref] : genericKey;
732
  apiKeyInput.value = providerKey || "";
733
  }
 
746
  // For non-OpenRouter providers we keep placeholder per provider; the value is already set above.
747
  const apiKeyLabel = document.getElementById("api-key-label");
748
  if (apiKeyLabel) {
749
+ apiKeyLabel.textContent = window.KimiProviderUtils
750
+ ? window.KimiProviderUtils.getLabelForProvider(provider)
751
+ : "API Key";
 
 
 
 
 
 
 
752
  }
753
 
754
  // Load system prompt
 
821
  const kimiDB = window.kimiDB;
822
  if (!kimiDB) return;
823
  const character = await kimiDB.getSelectedCharacter();
824
+ // Retrieve token usage (fallback to 0)
825
+ const tokensIn = await kimiDB.getPreference(`totalTokensIn_${character}`, 0);
826
+ const tokensOut = await kimiDB.getPreference(`totalTokensOut_${character}`, 0);
827
+ const charDefAff = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]?.traits?.affection) || null;
828
+ const genericAff = (window.getTraitDefaults && window.getTraitDefaults().affection) || 65;
829
+ const defaultAff = typeof charDefAff === "number" ? charDefAff : genericAff;
830
+ const affectionTrait = await kimiDB.getPersonalityTrait("affection", defaultAff, character);
831
  const conversations = await kimiDB.getAllConversations(character);
832
  let firstInteraction = await kimiDB.getPreference(`firstInteraction_${character}`);
833
  if (!firstInteraction && conversations.length > 0) {
834
  firstInteraction = conversations[0].timestamp;
835
  await kimiDB.setPreference(`firstInteraction_${character}`, firstInteraction);
836
  }
837
+ const tokensEl = document.getElementById("tokens-usage");
838
  const favorabilityEl = document.getElementById("current-favorability");
839
  const conversationsEl = document.getElementById("conversations-count");
840
  const daysEl = document.getElementById("days-together");
841
+ if (tokensEl) tokensEl.textContent = `${tokensIn} / ${tokensOut}`;
842
  if (favorabilityEl) {
843
  const v = Number(affectionTrait) || 0;
844
  favorabilityEl.textContent = `${Math.max(0, Math.min(100, v)).toFixed(2)}%`;
 
883
  const llmMaxTokensSlider = document.getElementById("llm-max-tokens");
884
  const llmMaxTokensValue = document.getElementById("llm-max-tokens-value");
885
  if (llmMaxTokensSlider && llmMaxTokensValue && kimiDB) {
886
+ const saved = await kimiDB.getPreference("llmMaxTokens", 200);
887
  llmMaxTokensSlider.value = saved;
888
  llmMaxTokensValue.textContent = saved;
889
  }
 
1057
  modelDiv.classList.add("selected");
1058
  console.log(`🤖 Model switched to: ${model.name}`);
1059
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1060
  // Show brief feedback to user
1061
  const feedback = document.createElement("div");
1062
  feedback.textContent = `Model changed to ${model.name}`;
 
1453
  // Use batch operation for all pending changes
1454
  await kimiDB.setPersonalityBatch(pendingTraitChanges);
1455
 
1456
+ // Side-effects handled by central 'personality:updated' listener.
 
 
 
 
 
 
 
 
 
 
1457
  } catch (error) {
1458
  console.error("Error batch saving personality traits:", error);
1459
  }
 
1579
 
1580
  // Exposer globalement (Note: KimiMemory and KimiAppearanceManager are now in separate files)
1581
  window.KimiDataManager = KimiDataManager;
 
1582
  window.updateFavorabilityLabel = updateFavorabilityLabel;
1583
  window.loadCharacterSection = loadCharacterSection;
1584
  window.getBasicResponse = getBasicResponse;
 
1585
  window.analyzeAndReact = analyzeAndReact;
1586
  window.addMessageToChat = addMessageToChat;
1587
  window.loadChatHistory = loadChatHistory;
1588
  window.loadSettingsData = loadSettingsData;
1589
  window.updateSlider = updateSlider;
1590
+ // ========================= DYNAMIC SLIDER SYNC =========================
1591
+ async function refreshAllSliders() {
1592
+ if (!window.kimiDB) return;
1593
+ const prefMap = [
1594
+ ["voice-rate", "voiceRate", "VOICE_RATE"],
1595
+ ["voice-pitch", "voicePitch", "VOICE_PITCH"],
1596
+ ["voice-volume", "voiceVolume", "VOICE_VOLUME"],
1597
+ ["llm-temperature", "llmTemperature", "LLM_TEMPERATURE"],
1598
+ ["llm-max-tokens", "llmMaxTokens", "LLM_MAX_TOKENS"],
1599
+ ["llm-top-p", "llmTopP", "LLM_TOP_P"],
1600
+ ["llm-frequency-penalty", "llmFrequencyPenalty", "LLM_FREQUENCY_PENALTY"],
1601
+ ["llm-presence-penalty", "llmPresencePenalty", "LLM_PRESENCE_PENALTY"],
1602
+ ["interface-opacity", "interfaceOpacity", "INTERFACE_OPACITY"]
1603
+ ];
1604
+ for (const [sliderId, prefKey, defaultKey] of prefMap) {
1605
+ try {
1606
+ const el = document.getElementById(sliderId);
1607
+ if (!el) continue;
1608
+ const stored = await window.kimiDB.getPreference(prefKey, window.KIMI_CONFIG?.DEFAULTS?.[defaultKey]);
1609
+ if (typeof stored === "number" || (typeof stored === "string" && stored !== null)) {
1610
+ updateSlider(sliderId, stored);
1611
+ }
1612
+ } catch {}
1613
+ }
1614
+ }
1615
+ window.refreshAllSliders = refreshAllSliders;
1616
+
1617
+ const _debouncedPrefUpdate = window.KimiPerformanceUtils
1618
+ ? window.KimiPerformanceUtils.debounce(evt => {
1619
+ const key = evt.detail?.key;
1620
+ if (!key) return;
1621
+ const keyToSlider = {
1622
+ voiceRate: "voice-rate",
1623
+ voicePitch: "voice-pitch",
1624
+ voiceVolume: "voice-volume",
1625
+ llmTemperature: "llm-temperature",
1626
+ llmMaxTokens: "llm-max-tokens",
1627
+ llmTopP: "llm-top-p",
1628
+ llmFrequencyPenalty: "llm-frequency-penalty",
1629
+ llmPresencePenalty: "llm-presence-penalty",
1630
+ interfaceOpacity: "interface-opacity"
1631
+ };
1632
+ const sliderId = keyToSlider[key];
1633
+ if (sliderId && typeof evt.detail.value !== "undefined") {
1634
+ updateSlider(sliderId, evt.detail.value);
1635
+ }
1636
+ }, 120)
1637
+ : null;
1638
+ window.addEventListener("preferenceUpdated", evt => {
1639
+ if (_debouncedPrefUpdate) _debouncedPrefUpdate(evt);
1640
+ });
1641
  window.updatePersonalitySliders = updatePersonalitySliders;
1642
  window.updateStats = updateStats;
1643
  window.initializeAllSliders = initializeAllSliders;
 
1665
  // Refresh UI models list when the LLM model changes programmatically
1666
  try {
1667
  window.addEventListener("llmModelChanged", () => {
1668
+ if (typeof window.loadAvailableModels === "function") {
1669
+ window.loadAvailableModels();
1670
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1671
  });
1672
  } catch (e) {}
1673
 
1674
  // Typing indicator wiring
1675
  try {
1676
+ // Soft tweak of API key input attributes shortly after load to reduce password manager prompts
1677
  setTimeout(() => {
1678
  const apiInput = document.getElementById("openrouter-api-key");
1679
  if (apiInput) {
1680
  apiInput.setAttribute("autocomplete", "new-password");
1681
+ apiInput.setAttribute("name", "openrouter_api_key");
1682
  apiInput.setAttribute("data-lpignore", "true");
1683
  apiInput.setAttribute("data-1p-ignore", "true");
1684
  apiInput.setAttribute("data-bwignore", "true");
1685
  apiInput.setAttribute("data-form-type", "other");
1686
  apiInput.setAttribute("autocapitalize", "none");
1687
  apiInput.setAttribute("autocorrect", "off");
1688
+ apiInput.setAttribute("spellcheck", "false");
1689
  }
1690
  }, 300);
1691
 
 
1723
  const selectedCharacter = characterName || (await kimiDB.getSelectedCharacter());
1724
  const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
1725
 
1726
+ // Build required traits prioritizing character-specific defaults (fallback to generic)
1727
  const getRequiredTraits = () => {
1728
+ const charDefaults = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]?.traits) || {};
1729
+ let generic = {};
1730
  if (window.KimiEmotionSystem) {
1731
  const emotionSystem = new window.KimiEmotionSystem(kimiDB);
1732
+ generic = emotionSystem.TRAIT_DEFAULTS;
1733
+ } else if (window.getTraitDefaults) {
1734
+ generic = window.getTraitDefaults();
1735
+ } else {
1736
+ generic = { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
1737
  }
1738
+ // Character defaults take precedence over generic defaults
1739
+ return { ...generic, ...charDefaults };
 
 
 
 
 
 
 
1740
  };
1741
 
1742
  const requiredTraits = getRequiredTraits();
 
1771
  }
1772
  }
1773
 
1774
+ // Video/voice updates are centralized in the 'personality:updated' listener.
 
 
 
1775
 
1776
  return updatedTraits;
1777
  }
kimi-js/kimi-script.js CHANGED
@@ -1,3 +1,9 @@
 
 
 
 
 
 
1
  document.addEventListener("DOMContentLoaded", async function () {
2
  const DEFAULT_SYSTEM_PROMPT = window.DEFAULT_SYSTEM_PROMPT;
3
 
@@ -5,6 +11,11 @@ document.addEventListener("DOMContentLoaded", async function () {
5
  let kimiLLM = null;
6
  let isSystemReady = false;
7
 
 
 
 
 
 
8
  const kimiInit = new KimiInitManager();
9
  let kimiVideo = null;
10
 
@@ -42,10 +53,10 @@ document.addEventListener("DOMContentLoaded", async function () {
42
  await kimiLLM.init();
43
 
44
  // Initialize unified emotion system
45
- window.kimiEmotionSystem = new window.KimiEmotionSystem(kimiDB);
46
 
47
  // Initialize the new memory system
48
- window.kimiMemorySystem = new window.KimiMemorySystem(kimiDB);
49
  await window.kimiMemorySystem.init();
50
 
51
  // Initialize legacy memory for favorability
@@ -62,8 +73,12 @@ document.addEventListener("DOMContentLoaded", async function () {
62
 
63
  isSystemReady = true;
64
  window.isSystemReady = true;
65
- // Hydrate API config UI from DB after systems ready
66
- initializeApiConfigUI();
 
 
 
 
67
  } catch (error) {
68
  console.error("Initialization error:", error);
69
  }
@@ -104,19 +119,11 @@ document.addEventListener("DOMContentLoaded", async function () {
104
 
105
  // Initial presence state based on current input value
106
  {
107
- const apiInputInit = ApiUi.apiKeyInput();
108
- const currentVal = (apiInputInit || {}).value || "";
109
  const colorInit = currentVal && currentVal.length > 0 ? "#4caf50" : "#9e9e9e";
110
  ApiUi.setPresence(colorInit);
111
  // On load, test status is unknown
112
  ApiUi.setTestPresence("#9e9e9e");
113
- // Enforce initial masking style to avoid UA text-security glitches
114
- if (apiInputInit && apiInputInit.classList.contains("masked")) {
115
- try {
116
- apiInputInit.style.webkitTextSecurity = "disc";
117
- } catch (_) {}
118
- apiInputInit.style.filter = "blur(6px)";
119
- }
120
  }
121
 
122
  // Initialize API config UI from saved preferences
@@ -139,99 +146,19 @@ document.addEventListener("DOMContentLoaded", async function () {
139
  const baseUrlInput = ApiUi.baseUrlInput();
140
  const modelIdInput = ApiUi.modelIdInput();
141
  const apiKeyInput = ApiUi.apiKeyInput();
142
- if (baseUrlInput) {
143
- baseUrlInput.value = baseUrl || "";
144
- const isFixed = ["openrouter", "openai", "groq", "together", "deepseek"].includes(provider);
145
- baseUrlInput.readOnly = !!isFixed;
146
- baseUrlInput.setAttribute("aria-readonly", isFixed ? "true" : "false");
147
- // Anti-autofill & soft-readonly flags
148
- baseUrlInput.autocomplete = "new-password";
149
- baseUrlInput.removeAttribute("name");
150
- baseUrlInput.setAttribute("data-lpignore", "true");
151
- baseUrlInput.setAttribute("data-1p-ignore", "true");
152
- baseUrlInput.setAttribute("data-bwignore", "true");
153
- baseUrlInput.setAttribute("data-form-type", "other");
154
- baseUrlInput.setAttribute("autocapitalize", "none");
155
- baseUrlInput.setAttribute("autocorrect", "off");
156
- // For editable providers, use a soft-readonly until focus to deter autofill prompts
157
- if (!isFixed) {
158
- baseUrlInput.setAttribute("data-soft-readonly", "true");
159
- baseUrlInput.setAttribute("readonly", "true");
160
- baseUrlInput.addEventListener(
161
- "focus",
162
- () => {
163
- if (baseUrlInput.hasAttribute("data-soft-readonly")) {
164
- baseUrlInput.removeAttribute("readonly");
165
- }
166
- },
167
- { once: true }
168
- );
169
- } else {
170
- baseUrlInput.removeAttribute("data-soft-readonly");
171
- }
172
- }
173
  // Only prefill model for OpenRouter, others should show placeholder only
174
  if (modelIdInput) {
175
  if (provider === "openrouter") {
176
- modelIdInput.value = modelId || (window.kimiLLM ? window.kimiLLM.currentModel : "");
177
- modelIdInput.readOnly = true;
178
- modelIdInput.setAttribute("aria-readonly", "true");
179
  } else {
180
  modelIdInput.value = "";
181
- modelIdInput.readOnly = false;
182
- modelIdInput.setAttribute("aria-readonly", "false");
183
- }
184
- // Anti-autofill & soft-readonly flags for model id when editable
185
- modelIdInput.autocomplete = "new-password";
186
- modelIdInput.removeAttribute("name");
187
- modelIdInput.setAttribute("data-lpignore", "true");
188
- modelIdInput.setAttribute("data-1p-ignore", "true");
189
- modelIdInput.setAttribute("data-bwignore", "true");
190
- modelIdInput.setAttribute("data-form-type", "other");
191
- modelIdInput.setAttribute("autocapitalize", "none");
192
- modelIdInput.setAttribute("autocorrect", "off");
193
- if (!modelIdInput.readOnly) {
194
- modelIdInput.setAttribute("data-soft-readonly", "true");
195
- modelIdInput.setAttribute("readonly", "true");
196
- modelIdInput.addEventListener(
197
- "focus",
198
- () => {
199
- if (modelIdInput.hasAttribute("data-soft-readonly")) {
200
- modelIdInput.removeAttribute("readonly");
201
- }
202
- },
203
- { once: true }
204
- );
205
- } else {
206
- modelIdInput.removeAttribute("data-soft-readonly");
207
  }
208
  }
209
  // Load the provider-specific key
210
- const keyPrefMap = {
211
- openrouter: "openrouterApiKey",
212
- openai: "apiKey_openai",
213
- groq: "apiKey_groq",
214
- together: "apiKey_together",
215
- deepseek: "apiKey_deepseek",
216
- "openai-compatible": "apiKey_custom"
217
- };
218
- const keyPref = keyPrefMap[provider] || "llmApiKey";
219
  const storedKey = await window.kimiDB.getPreference(keyPref, "");
220
- if (apiKeyInput) {
221
- apiKeyInput.value = storedKey || "";
222
- // Keep masking visuals consistent after programmatic value changes
223
- if (apiKeyInput.classList.contains("masked")) {
224
- try {
225
- apiKeyInput.style.webkitTextSecurity = "disc";
226
- } catch (_) {}
227
- apiKeyInput.style.filter = "blur(6px)";
228
- } else {
229
- try {
230
- apiKeyInput.style.webkitTextSecurity = "";
231
- } catch (_) {}
232
- apiKeyInput.style.filter = "none";
233
- }
234
- }
235
  ApiUi.setPresence(storedKey ? "#4caf50" : "#9e9e9e");
236
  ApiUi.setTestPresence("#9e9e9e");
237
  const savedBadge = ApiUi.savedBadge();
@@ -249,14 +176,17 @@ document.addEventListener("DOMContentLoaded", async function () {
249
  console.warn("Failed to initialize API config UI:", e);
250
  }
251
  }
 
 
252
 
253
  const providerSelectEl = document.getElementById("llm-provider");
254
  if (providerSelectEl) {
255
- providerSelectEl.addEventListener("change", async () => {
256
- const provider = providerSelectEl.value;
257
  const baseUrlInput = ApiUi.baseUrlInput();
258
- const apiKeyInput = ApiUi.apiKeyInput();
259
  const modelIdInput = ApiUi.modelIdInput();
 
 
260
  const placeholders = {
261
  openrouter: {
262
  url: "https://openrouter.ai/api/v1/chat/completions",
@@ -297,101 +227,20 @@ document.addEventListener("DOMContentLoaded", async function () {
297
  const p = placeholders[provider] || placeholders.openai;
298
  if (baseUrlInput) {
299
  baseUrlInput.placeholder = p.url;
300
- // Providers fixes: URL canonique, champ en lecture seule
301
- const isFixed = ["openrouter", "openai", "groq", "together", "deepseek"].includes(provider);
302
- if (isFixed) {
303
- baseUrlInput.value = provider === "openrouter" ? "https://openrouter.ai/api/v1/chat/completions" : p.url;
304
- baseUrlInput.readOnly = true;
305
- baseUrlInput.setAttribute("aria-readonly", "true");
306
- baseUrlInput.removeAttribute("data-soft-readonly");
307
- } else {
308
- // Custom & Ollama: laisser éditable et ne pas écraser l’URL utilisateur si déjà définie
309
- let savedUrl = "";
310
- if (window.kimiDB) {
311
- savedUrl = await window.kimiDB.getPreference("llmBaseUrl", "");
312
- }
313
- baseUrlInput.value = savedUrl || p.url;
314
- baseUrlInput.readOnly = false;
315
- baseUrlInput.setAttribute("aria-readonly", "false");
316
- baseUrlInput.setAttribute("data-soft-readonly", "true");
317
- baseUrlInput.setAttribute("readonly", "true");
318
- baseUrlInput.addEventListener(
319
- "focus",
320
- () => {
321
- if (baseUrlInput.hasAttribute("data-soft-readonly")) {
322
- baseUrlInput.removeAttribute("readonly");
323
- }
324
- },
325
- { once: true }
326
- );
327
- }
328
- // Reassert anti-autofill attributes each switch
329
- baseUrlInput.autocomplete = "new-password";
330
- baseUrlInput.removeAttribute("name");
331
- baseUrlInput.setAttribute("data-lpignore", "true");
332
- baseUrlInput.setAttribute("data-1p-ignore", "true");
333
- baseUrlInput.setAttribute("data-bwignore", "true");
334
- baseUrlInput.setAttribute("data-form-type", "other");
335
- baseUrlInput.setAttribute("autocapitalize", "none");
336
- baseUrlInput.setAttribute("autocorrect", "off");
337
  }
338
  if (apiKeyInput) apiKeyInput.placeholder = p.keyPh;
339
  if (modelIdInput) {
340
  modelIdInput.placeholder = p.model;
341
  // Value only for OpenRouter; others cleared to encourage provider-specific model naming
342
- if (provider === "openrouter") {
343
- const savedId = (window.kimiDB && (await window.kimiDB.getPreference("llmModelId", ""))) || "";
344
- modelIdInput.value = savedId || (window.kimiLLM ? window.kimiLLM.currentModel : "");
345
- modelIdInput.readOnly = true;
346
- modelIdInput.setAttribute("aria-readonly", "true");
347
- modelIdInput.removeAttribute("data-soft-readonly");
348
- } else {
349
- modelIdInput.value = "";
350
- modelIdInput.readOnly = false;
351
- modelIdInput.setAttribute("aria-readonly", "false");
352
- modelIdInput.setAttribute("data-soft-readonly", "true");
353
- modelIdInput.setAttribute("readonly", "true");
354
- modelIdInput.addEventListener(
355
- "focus",
356
- () => {
357
- if (modelIdInput.hasAttribute("data-soft-readonly")) {
358
- modelIdInput.removeAttribute("readonly");
359
- }
360
- },
361
- { once: true }
362
- );
363
- }
364
- // Reassert anti-autofill attributes each switch
365
- modelIdInput.autocomplete = "new-password";
366
- modelIdInput.removeAttribute("name");
367
- modelIdInput.setAttribute("data-lpignore", "true");
368
- modelIdInput.setAttribute("data-1p-ignore", "true");
369
- modelIdInput.setAttribute("data-bwignore", "true");
370
- modelIdInput.setAttribute("data-form-type", "other");
371
- modelIdInput.setAttribute("autocapitalize", "none");
372
- modelIdInput.setAttribute("autocorrect", "off");
373
  }
374
  if (window.kimiDB) {
375
  await window.kimiDB.setPreference("llmProvider", provider);
376
- // Éviter d’écraser l’URL personnalisée pour custom/ollama
377
- if (["openrouter", "openai", "groq", "together", "deepseek"].includes(provider)) {
378
- const canonical = provider === "openrouter" ? "https://openrouter.ai/api/v1/chat/completions" : p.url;
379
- await window.kimiDB.setPreference("llmBaseUrl", canonical);
380
- } else {
381
- const existing = await window.kimiDB.getPreference("llmBaseUrl", "");
382
- await window.kimiDB.setPreference("llmBaseUrl", existing || p.url);
383
- }
384
  const apiKeyLabel = document.getElementById("api-key-label");
385
  // Load provider-specific key into the input for clarity
386
- const keyPrefMap = {
387
- openrouter: "openrouterApiKey",
388
- openai: "apiKey_openai",
389
- groq: "apiKey_groq",
390
- together: "apiKey_together",
391
- deepseek: "apiKey_deepseek",
392
- "openai-compatible": "apiKey_custom"
393
- };
394
- const keyPref = keyPrefMap[provider] || "llmApiKey";
395
  const storedKey = await window.kimiDB.getPreference(keyPref, "");
396
  if (apiKeyInput) apiKeyInput.value = storedKey || "";
397
  const color = provider === "ollama" ? "#9e9e9e" : storedKey && storedKey.length > 0 ? "#4caf50" : "#9e9e9e";
@@ -406,16 +255,9 @@ document.addEventListener("DOMContentLoaded", async function () {
406
 
407
  // Dynamic label per provider
408
  if (apiKeyLabel) {
409
- const labelByProvider = {
410
- openrouter: "OpenRouter API Key",
411
- openai: "OpenAI API Key",
412
- groq: "Groq API Key",
413
- together: "Together API Key",
414
- deepseek: "DeepSeek API Key",
415
- "openai-compatible": "API Key",
416
- ollama: "API Key"
417
- };
418
- apiKeyLabel.textContent = labelByProvider[provider] || "API Key";
419
  }
420
  const savedBadge = ApiUi.savedBadge();
421
  if (savedBadge) savedBadge.style.display = "none";
@@ -434,42 +276,21 @@ document.addEventListener("DOMContentLoaded", async function () {
434
  }, 1500);
435
  }
436
 
437
- let video1 = window.KimiDOMUtils.get("#video1");
438
- let video2 = window.KimiDOMUtils.get("#video2");
439
-
440
  if (!video1 || !video2) {
441
- console.error("Video elements not found! Creating them...");
442
  const videoContainer = document.querySelector(".video-container");
443
  if (videoContainer) {
444
- video1 = document.createElement("video");
445
- video1.id = "video1";
446
- video1.className = "bg-video active";
447
- video1.autoplay = true;
448
- video1.muted = true;
449
- video1.playsinline = true;
450
- video1.preload = "auto";
451
- video1.innerHTML =
452
- '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
453
-
454
- video2 = document.createElement("video");
455
- video2.id = "video2";
456
- video2.className = "bg-video";
457
- video2.autoplay = true;
458
- video2.muted = true;
459
- video2.playsinline = true;
460
- video2.preload = "auto";
461
- video2.innerHTML =
462
- '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
463
-
464
  videoContainer.appendChild(video1);
465
  videoContainer.appendChild(video2);
466
  }
467
  }
468
-
469
  let activeVideo = video1;
470
  let inactiveVideo = video2;
471
-
472
- kimiVideo = new KimiVideoManager(video1, video2);
473
  await kimiVideo.init(kimiDB);
474
  window.kimiVideo = kimiVideo;
475
 
@@ -731,10 +552,14 @@ document.addEventListener("DOMContentLoaded", async function () {
731
  if (resetSystemPromptButton) {
732
  resetSystemPromptButton.addEventListener("click", async () => {
733
  const selectedCharacter = await window.kimiDB.getPreference("selectedCharacter", "kimi");
 
 
 
 
734
  if (systemPromptInput && window.kimiDB && window.kimiLLM) {
735
- await window.kimiDB.setSystemPromptForCharacter(selectedCharacter, DEFAULT_SYSTEM_PROMPT);
736
- systemPromptInput.value = DEFAULT_SYSTEM_PROMPT;
737
- window.kimiLLM.setSystemPrompt(DEFAULT_SYSTEM_PROMPT);
738
  resetSystemPromptButton.textContent = "Reset!";
739
  resetSystemPromptButton.classList.add("animated");
740
  resetSystemPromptButton.setAttribute("data-i18n", "reset_done");
@@ -833,15 +658,9 @@ document.addEventListener("DOMContentLoaded", async function () {
833
  if (window.kimiDB) {
834
  // Save API key under provider-specific preference key (skip for Ollama)
835
  if (provider !== "ollama") {
836
- const keyPrefMap = {
837
- openrouter: "openrouterApiKey",
838
- openai: "apiKey_openai",
839
- groq: "apiKey_groq",
840
- together: "apiKey_together",
841
- deepseek: "apiKey_deepseek",
842
- "openai-compatible": "apiKey_custom"
843
- };
844
- const keyPref = keyPrefMap[provider] || "llmApiKey";
845
  await window.kimiDB.setPreference(keyPref, apiKey);
846
  }
847
  await window.kimiDB.setPreference("llmProvider", provider);
@@ -917,15 +736,7 @@ document.addEventListener("DOMContentLoaded", async function () {
917
  t = setTimeout(async () => {
918
  const providerEl = ApiUi.providerSelect();
919
  const provider = providerEl ? providerEl.value : "openrouter";
920
- const keyPrefMap = {
921
- openrouter: "openrouterApiKey",
922
- openai: "apiKey_openai",
923
- groq: "apiKey_groq",
924
- together: "apiKey_together",
925
- deepseek: "apiKey_deepseek",
926
- "openai-compatible": "apiKey_custom"
927
- };
928
- const keyPref = keyPrefMap[provider] || "llmApiKey";
929
  const value = input.value.trim();
930
  // Update Test button state immediately
931
  const validNow = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(value));
@@ -958,71 +769,21 @@ document.addEventListener("DOMContentLoaded", async function () {
958
  });
959
  })();
960
 
961
- // Harden anti-autofill and masking on DOM ready
962
- (function enforceApiInputSecurity() {
963
- const input = ApiUi.apiKeyInput();
964
- if (!input) return;
965
- // Anti-autofill flags
966
- input.autocomplete = "off";
967
- input.removeAttribute("name");
968
- input.setAttribute("data-lpignore", "true");
969
- input.setAttribute("data-1p-ignore", "true");
970
- input.setAttribute("data-bwignore", "true");
971
- input.setAttribute("data-form-type", "other");
972
- input.setAttribute("autocapitalize", "none");
973
- input.setAttribute("autocorrect", "off");
974
- // Keep masked by style as well
975
- if (input.classList.contains("masked")) {
976
- try {
977
- input.style.webkitTextSecurity = "disc";
978
- } catch (_) {}
979
- input.style.filter = "blur(6px)";
980
- }
981
- // If the field gets focus, ensure it is editable (index.html sets readonly initially)
982
- input.addEventListener(
983
- "focus",
984
- () => {
985
- if (input.hasAttribute("readonly")) input.removeAttribute("readonly");
986
- },
987
- { once: false }
988
- );
989
- })();
990
-
991
  // Toggle show/hide for API key
992
  (function setupToggleEye() {
993
  const btn = ApiUi.toggleBtn();
994
  const input = ApiUi.apiKeyInput();
995
  if (!btn || !input) return;
996
  btn.addEventListener("click", () => {
997
- const wasMasked = input.classList.contains("masked");
998
- // Toggle class first
999
- if (wasMasked) {
1000
- input.classList.remove("masked");
1001
- } else {
1002
- input.classList.add("masked");
1003
- }
1004
- // Force style to avoid UA quirks where -webkit-text-security persists
1005
- const nowMasked = input.classList.contains("masked");
1006
- if (nowMasked) {
1007
- // Apply masking explicitly
1008
- try {
1009
- input.style.webkitTextSecurity = "disc"; // Chromium/WebKit
1010
- } catch (_) {}
1011
- input.style.filter = "blur(6px)"; // Fallback
1012
- } else {
1013
- // Remove any residual masking
1014
- try {
1015
- input.style.webkitTextSecurity = ""; // reset to CSS/default
1016
- } catch (_) {}
1017
- input.style.filter = "none";
1018
- }
1019
- btn.setAttribute("aria-pressed", String(!nowMasked));
1020
  const icon = btn.querySelector("i");
1021
  if (icon) {
1022
  icon.classList.toggle("fa-eye");
1023
  icon.classList.toggle("fa-eye-slash");
1024
  }
1025
- btn.setAttribute("aria-label", nowMasked ? "Show API key" : "Hide API key");
1026
  });
1027
  })();
1028
 
@@ -1078,57 +839,6 @@ document.addEventListener("DOMContentLoaded", async function () {
1078
  // Setup unified event handlers to prevent duplicates
1079
  setupUnifiedEventHandlers();
1080
 
1081
- // Global hardening against browser/extension autofill on all text-like fields
1082
- (function hardenAgainstAutofill() {
1083
- const ATTRS = {
1084
- autocomplete: "off",
1085
- autocorrect: "off",
1086
- autocapitalize: "none",
1087
- spellcheck: "false",
1088
- "data-lpignore": "true",
1089
- "data-1p-ignore": "true",
1090
- "data-bwignore": "true",
1091
- "data-form-type": "other"
1092
- };
1093
- const block = el => {
1094
- if (!el || !(el instanceof HTMLElement)) return;
1095
- const tag = el.tagName.toLowerCase();
1096
- const type = (el.getAttribute("type") || "").toLowerCase();
1097
- if (tag === "input" && (type === "text" || type === "search" || type === "email" || type === "url")) {
1098
- Object.entries(ATTRS).forEach(([k, v]) => el.setAttribute(k, v));
1099
- // Never keep a sensitive name attribute
1100
- const name = el.getAttribute("name") || "";
1101
- if (name) el.removeAttribute("name");
1102
- }
1103
- if (tag === "textarea") {
1104
- Object.entries(ATTRS).forEach(([k, v]) => el.setAttribute(k, v));
1105
- const name = el.getAttribute("name") || "";
1106
- if (name) el.removeAttribute("name");
1107
- }
1108
- };
1109
- document.querySelectorAll("input, textarea").forEach(block);
1110
- const mo = new MutationObserver(muts => {
1111
- for (const m of muts) {
1112
- m.addedNodes &&
1113
- m.addedNodes.forEach(n => {
1114
- if (n instanceof HTMLElement) {
1115
- if (n.matches && (n.matches("input") || n.matches("textarea"))) block(n);
1116
- n.querySelectorAll && n.querySelectorAll("input, textarea").forEach(block);
1117
- }
1118
- });
1119
- if (m.type === "attributes" && m.target instanceof HTMLElement) {
1120
- if (m.attributeName === "type" || m.attributeName === "name") block(m.target);
1121
- }
1122
- }
1123
- });
1124
- mo.observe(document.documentElement, {
1125
- subtree: true,
1126
- childList: true,
1127
- attributes: true,
1128
- attributeFilter: ["type", "name"]
1129
- });
1130
- })();
1131
-
1132
  // Initialize language and UI
1133
  await initializeLanguageAndUI();
1134
 
@@ -1245,23 +955,78 @@ document.addEventListener("DOMContentLoaded", async function () {
1245
  // No need to reattach them here to avoid duplicates
1246
  }
1247
 
1248
- // Add personality change listener
1249
- window.addEventListener("personalityUpdated", async event => {
1250
- const { character, traits } = event.detail;
1251
- console.log(`🧠 Personality updated for ${character}:`, traits);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1252
 
1253
- // Update video context based on new traits
1254
- if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1255
- window.kimiVideo.setMoodByPersonality(traits);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1256
  }
1257
 
1258
- // Update voice modulation if available
1259
- if (window.voiceManager && window.voiceManager.updatePersonalityModulation) {
1260
- window.voiceManager.updatePersonalityModulation(traits);
 
1261
  }
 
1262
 
1263
- // Update UI elements that depend on personality
1264
- // Favorability bar will be updated by KimiMemory system
 
 
 
 
 
 
1265
  });
1266
 
1267
  // Add global keyboard event listener for microphone toggle (F8)
@@ -1284,6 +1049,17 @@ document.addEventListener("DOMContentLoaded", async function () {
1284
  }
1285
  });
1286
 
 
 
 
 
 
 
 
 
 
 
 
1287
  document.addEventListener("keyup", function (event) {
1288
  if (event.key === "F8") {
1289
  f8KeyPressed = false;
@@ -1296,4 +1072,190 @@ document.addEventListener("DOMContentLoaded", async function () {
1296
  await window.ensureVideoContextConsistency();
1297
  }
1298
  }, 30000); // Check every 30 seconds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1299
  });
 
1
+ import KimiDatabase from "./kimi-database.js";
2
+ import KimiLLMManager from "./kimi-llm-manager.js";
3
+ import KimiEmotionSystem from "./kimi-emotion-system.js";
4
+ import KimiMemorySystem from "./kimi-memory-system.js";
5
+ import KimiMemory from "./kimi-memory.js";
6
+
7
  document.addEventListener("DOMContentLoaded", async function () {
8
  const DEFAULT_SYSTEM_PROMPT = window.DEFAULT_SYSTEM_PROMPT;
9
 
 
11
  let kimiLLM = null;
12
  let isSystemReady = false;
13
 
14
+ // Global debug flag for sync/log verbosity (default: false)
15
+ if (typeof window.KIMI_DEBUG_SYNC === "undefined") {
16
+ window.KIMI_DEBUG_SYNC = false;
17
+ }
18
+
19
  const kimiInit = new KimiInitManager();
20
  let kimiVideo = null;
21
 
 
53
  await kimiLLM.init();
54
 
55
  // Initialize unified emotion system
56
+ window.kimiEmotionSystem = new KimiEmotionSystem(kimiDB);
57
 
58
  // Initialize the new memory system
59
+ window.kimiMemorySystem = new KimiMemorySystem(kimiDB);
60
  await window.kimiMemorySystem.init();
61
 
62
  // Initialize legacy memory for favorability
 
73
 
74
  isSystemReady = true;
75
  window.isSystemReady = true;
76
+ // API config UI will be initialized after ApiUi is defined
77
+ if (window.refreshAllSliders) {
78
+ try {
79
+ await window.refreshAllSliders();
80
+ } catch {}
81
+ }
82
  } catch (error) {
83
  console.error("Initialization error:", error);
84
  }
 
119
 
120
  // Initial presence state based on current input value
121
  {
122
+ const currentVal = (ApiUi.apiKeyInput() || {}).value || "";
 
123
  const colorInit = currentVal && currentVal.length > 0 ? "#4caf50" : "#9e9e9e";
124
  ApiUi.setPresence(colorInit);
125
  // On load, test status is unknown
126
  ApiUi.setTestPresence("#9e9e9e");
 
 
 
 
 
 
 
127
  }
128
 
129
  // Initialize API config UI from saved preferences
 
146
  const baseUrlInput = ApiUi.baseUrlInput();
147
  const modelIdInput = ApiUi.modelIdInput();
148
  const apiKeyInput = ApiUi.apiKeyInput();
149
+ if (baseUrlInput) baseUrlInput.value = baseUrl || "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  // Only prefill model for OpenRouter, others should show placeholder only
151
  if (modelIdInput) {
152
  if (provider === "openrouter") {
153
+ if (!modelIdInput.value) modelIdInput.value = modelId;
 
 
154
  } else {
155
  modelIdInput.value = "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  }
157
  }
158
  // Load the provider-specific key
159
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "llmApiKey";
 
 
 
 
 
 
 
 
160
  const storedKey = await window.kimiDB.getPreference(keyPref, "");
161
+ if (apiKeyInput) apiKeyInput.value = storedKey || "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  ApiUi.setPresence(storedKey ? "#4caf50" : "#9e9e9e");
163
  ApiUi.setTestPresence("#9e9e9e");
164
  const savedBadge = ApiUi.savedBadge();
 
176
  console.warn("Failed to initialize API config UI:", e);
177
  }
178
  }
179
+ // Hydrate API config UI from DB after ApiUi is defined and function declared
180
+ initializeApiConfigUI();
181
 
182
  const providerSelectEl = document.getElementById("llm-provider");
183
  if (providerSelectEl) {
184
+ providerSelectEl.addEventListener("change", async function (e) {
185
+ const provider = e.target.value;
186
  const baseUrlInput = ApiUi.baseUrlInput();
 
187
  const modelIdInput = ApiUi.modelIdInput();
188
+ const apiKeyInput = ApiUi.apiKeyInput();
189
+
190
  const placeholders = {
191
  openrouter: {
192
  url: "https://openrouter.ai/api/v1/chat/completions",
 
227
  const p = placeholders[provider] || placeholders.openai;
228
  if (baseUrlInput) {
229
  baseUrlInput.placeholder = p.url;
230
+ baseUrlInput.value = provider === "openrouter" ? placeholders.openrouter.url : p.url;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
  if (apiKeyInput) apiKeyInput.placeholder = p.keyPh;
233
  if (modelIdInput) {
234
  modelIdInput.placeholder = p.model;
235
  // Value only for OpenRouter; others cleared to encourage provider-specific model naming
236
+ modelIdInput.value = provider === "openrouter" && window.kimiLLM ? window.kimiLLM.currentModel : "";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  }
238
  if (window.kimiDB) {
239
  await window.kimiDB.setPreference("llmProvider", provider);
240
+ await window.kimiDB.setPreference("llmBaseUrl", provider === "openrouter" ? placeholders.openrouter.url : p.url);
 
 
 
 
 
 
 
241
  const apiKeyLabel = document.getElementById("api-key-label");
242
  // Load provider-specific key into the input for clarity
243
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "llmApiKey";
 
 
 
 
 
 
 
 
244
  const storedKey = await window.kimiDB.getPreference(keyPref, "");
245
  if (apiKeyInput) apiKeyInput.value = storedKey || "";
246
  const color = provider === "ollama" ? "#9e9e9e" : storedKey && storedKey.length > 0 ? "#4caf50" : "#9e9e9e";
 
255
 
256
  // Dynamic label per provider
257
  if (apiKeyLabel) {
258
+ apiKeyLabel.textContent = window.KimiProviderUtils
259
+ ? window.KimiProviderUtils.getLabelForProvider(provider)
260
+ : "API Key";
 
 
 
 
 
 
 
261
  }
262
  const savedBadge = ApiUi.savedBadge();
263
  if (savedBadge) savedBadge.style.display = "none";
 
276
  }, 1500);
277
  }
278
 
279
+ // Use centralized video utilities
280
+ let video1 = window.KimiVideoManager.getVideoElement("#video1");
281
+ let video2 = window.KimiVideoManager.getVideoElement("#video2");
282
  if (!video1 || !video2) {
 
283
  const videoContainer = document.querySelector(".video-container");
284
  if (videoContainer) {
285
+ video1 = window.KimiVideoManager.createVideoElement("video1", "bg-video active");
286
+ video2 = window.KimiVideoManager.createVideoElement("video2", "bg-video");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  videoContainer.appendChild(video1);
288
  videoContainer.appendChild(video2);
289
  }
290
  }
 
291
  let activeVideo = video1;
292
  let inactiveVideo = video2;
293
+ kimiVideo = new window.KimiVideoManager(video1, video2);
 
294
  await kimiVideo.init(kimiDB);
295
  window.kimiVideo = kimiVideo;
296
 
 
552
  if (resetSystemPromptButton) {
553
  resetSystemPromptButton.addEventListener("click", async () => {
554
  const selectedCharacter = await window.kimiDB.getPreference("selectedCharacter", "kimi");
555
+ const characterDefault =
556
+ (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]?.defaultPrompt) ||
557
+ DEFAULT_SYSTEM_PROMPT ||
558
+ "";
559
  if (systemPromptInput && window.kimiDB && window.kimiLLM) {
560
+ await window.kimiDB.setSystemPromptForCharacter(selectedCharacter, characterDefault);
561
+ systemPromptInput.value = characterDefault;
562
+ window.kimiLLM.setSystemPrompt(characterDefault);
563
  resetSystemPromptButton.textContent = "Reset!";
564
  resetSystemPromptButton.classList.add("animated");
565
  resetSystemPromptButton.setAttribute("data-i18n", "reset_done");
 
658
  if (window.kimiDB) {
659
  // Save API key under provider-specific preference key (skip for Ollama)
660
  if (provider !== "ollama") {
661
+ const keyPref = window.KimiProviderUtils
662
+ ? window.KimiProviderUtils.getKeyPrefForProvider(provider)
663
+ : "llmApiKey";
 
 
 
 
 
 
664
  await window.kimiDB.setPreference(keyPref, apiKey);
665
  }
666
  await window.kimiDB.setPreference("llmProvider", provider);
 
736
  t = setTimeout(async () => {
737
  const providerEl = ApiUi.providerSelect();
738
  const provider = providerEl ? providerEl.value : "openrouter";
739
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "llmApiKey";
 
 
 
 
 
 
 
 
740
  const value = input.value.trim();
741
  // Update Test button state immediately
742
  const validNow = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(value));
 
769
  });
770
  })();
771
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
772
  // Toggle show/hide for API key
773
  (function setupToggleEye() {
774
  const btn = ApiUi.toggleBtn();
775
  const input = ApiUi.apiKeyInput();
776
  if (!btn || !input) return;
777
  btn.addEventListener("click", () => {
778
+ const showing = input.type === "text";
779
+ input.type = showing ? "password" : "text";
780
+ btn.setAttribute("aria-pressed", String(!showing));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
781
  const icon = btn.querySelector("i");
782
  if (icon) {
783
  icon.classList.toggle("fa-eye");
784
  icon.classList.toggle("fa-eye-slash");
785
  }
786
+ btn.setAttribute("aria-label", showing ? "Show API key" : "Hide API key");
787
  });
788
  })();
789
 
 
839
  // Setup unified event handlers to prevent duplicates
840
  setupUnifiedEventHandlers();
841
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
842
  // Initialize language and UI
843
  await initializeLanguageAndUI();
844
 
 
955
  // No need to reattach them here to avoid duplicates
956
  }
957
 
958
+ // ==== BATCHED EVENT AGGREGATOR (personality + preferences) ====
959
+ const batchedUpdates = {
960
+ personality: null,
961
+ preferences: new Set()
962
+ };
963
+ let batchTimer = null;
964
+
965
+ function scheduleFlush() {
966
+ if (batchTimer) return;
967
+ batchTimer = setTimeout(flushBatchedUpdates, 100); // 100ms coalescing window
968
+ }
969
+
970
+ async function flushBatchedUpdates() {
971
+ const personalityPayload = batchedUpdates.personality;
972
+ const prefKeys = Array.from(batchedUpdates.preferences);
973
+ batchedUpdates.personality = null;
974
+ batchedUpdates.preferences.clear();
975
+ batchTimer = null;
976
+
977
+ // Apply personality update once (last-wins)
978
+ if (personalityPayload) {
979
+ const { character, traits } = personalityPayload;
980
+ const defaults = (window.getTraitDefaults && window.getTraitDefaults()) || {
981
+ affection: 65,
982
+ romance: 50,
983
+ empathy: 75,
984
+ playfulness: 55,
985
+ humor: 60,
986
+ intelligence: 70
987
+ };
988
 
989
+ // Prefer persisted DB traits over defaults to avoid temporary inconsistencies.
990
+ let dbTraits = null;
991
+ try {
992
+ if (window.kimiDB && typeof window.kimiDB.getAllPersonalityTraits === "function") {
993
+ dbTraits = await window.kimiDB.getAllPersonalityTraits(character || null);
994
+ }
995
+ } catch (e) {
996
+ dbTraits = null;
997
+ }
998
+
999
+ const baseline = { ...defaults, ...(dbTraits || {}) };
1000
+ const safeTraits = {};
1001
+ for (const key of Object.keys(defaults)) {
1002
+ // If incoming payload provides the key, use it; otherwise use baseline (DB -> defaults)
1003
+ let raw = Object.prototype.hasOwnProperty.call(traits || {}, key) ? traits[key] : baseline[key];
1004
+ let v = Number(raw);
1005
+ if (!isFinite(v) || isNaN(v)) v = Number(baseline[key]);
1006
+ v = Math.max(0, Math.min(100, v));
1007
+ safeTraits[key] = v;
1008
+ }
1009
+ if (window.KIMI_DEBUG_SYNC) {
1010
+ console.log(`🧠 (Batched) Personality updated for ${character}:`, safeTraits);
1011
+ }
1012
+ // Centralize side-effects elsewhere; aggregator remains a coalesced logger only.
1013
  }
1014
 
1015
+ // Preference keys batch (currently UI refresh for sliders already handled elsewhere)
1016
+ if (prefKeys.length > 0) {
1017
+ // Potential future hook: log or perform aggregated operations
1018
+ // console.log("⚙️ Batched preference keys:", prefKeys);
1019
  }
1020
+ }
1021
 
1022
+ // Also listen to the DB-wrapped event name to preserve batched logging
1023
+ window.addEventListener("personality:updated", event => {
1024
+ batchedUpdates.personality = event.detail; // last event wins
1025
+ scheduleFlush();
1026
+ });
1027
+ window.addEventListener("preferenceUpdated", event => {
1028
+ if (event.detail?.key) batchedUpdates.preferences.add(event.detail.key);
1029
+ scheduleFlush();
1030
  });
1031
 
1032
  // Add global keyboard event listener for microphone toggle (F8)
 
1049
  }
1050
  });
1051
 
1052
+ // Refresh sliders when character or language preference changes
1053
+ window.addEventListener("preferenceUpdated", evt => {
1054
+ const k = evt.detail?.key;
1055
+ if (!k) return;
1056
+ if (k === "selectedCharacter" || k === "selectedLanguage") {
1057
+ if (window.refreshAllSliders) {
1058
+ setTimeout(() => window.refreshAllSliders(), 50);
1059
+ }
1060
+ }
1061
+ });
1062
+
1063
  document.addEventListener("keyup", function (event) {
1064
  if (event.key === "F8") {
1065
  f8KeyPressed = false;
 
1072
  await window.ensureVideoContextConsistency();
1073
  }
1074
  }, 30000); // Check every 30 seconds
1075
+
1076
+ // Personality sync: global event and wrappers
1077
+ (function setupPersonalitySync() {
1078
+ // Guard to avoid multiple initializations
1079
+ if (window._kimiPersonalitySyncReady) return;
1080
+ window._kimiPersonalitySyncReady = true;
1081
+
1082
+ const dispatchUpdated = async (partialTraits, characterHint = null) => {
1083
+ try {
1084
+ const character = characterHint || (window.kimiDB && (await window.kimiDB.getSelectedCharacter())) || null;
1085
+ window.dispatchEvent(
1086
+ new CustomEvent("personality:updated", {
1087
+ detail: { character, traits: { ...partialTraits } }
1088
+ })
1089
+ );
1090
+ } catch (e) {}
1091
+ };
1092
+
1093
+ const tryWrapDB = () => {
1094
+ const db = window.kimiDB;
1095
+ if (!db) return false;
1096
+
1097
+ const wrapOnce = (obj, methodName, buildTraitsFromArgs) => {
1098
+ if (!obj || typeof obj[methodName] !== "function") return;
1099
+ if (obj[methodName]._kimiWrapped) return;
1100
+ const original = obj[methodName].bind(obj);
1101
+ obj[methodName] = async function (...args) {
1102
+ const res = await original(...args);
1103
+ try {
1104
+ const { traits, character } = await buildTraitsFromArgs(args, res);
1105
+ if (traits && Object.keys(traits).length > 0) {
1106
+ await dispatchUpdated(traits, character);
1107
+ }
1108
+ } catch (e) {}
1109
+ return res;
1110
+ };
1111
+ obj[methodName]._kimiWrapped = true;
1112
+ };
1113
+
1114
+ // setPersonalityTrait(trait, value, character?)
1115
+ wrapOnce(db, "setPersonalityTrait", async args => {
1116
+ const [trait, value, character] = args;
1117
+ return { traits: { [String(trait)]: Number(value) }, character: character || null };
1118
+ });
1119
+
1120
+ // setPersonalityBatch(traitsObj, character?)
1121
+ wrapOnce(db, "setPersonalityBatch", async args => {
1122
+ const [traitsObj, character] = args;
1123
+ const traits = {};
1124
+ if (traitsObj && typeof traitsObj === "object") {
1125
+ for (const [k, v] of Object.entries(traitsObj)) {
1126
+ traits[String(k)] = Number(v);
1127
+ }
1128
+ }
1129
+ return { traits, character: character || null };
1130
+ });
1131
+
1132
+ // savePersonality(personalityObj, character?)
1133
+ wrapOnce(db, "savePersonality", async args => {
1134
+ const [personalityObj, character] = args;
1135
+ const traits = {};
1136
+ if (personalityObj && typeof personalityObj === "object") {
1137
+ for (const [k, v] of Object.entries(personalityObj)) {
1138
+ traits[String(k)] = Number(v);
1139
+ }
1140
+ }
1141
+ return { traits, character: character || null };
1142
+ });
1143
+
1144
+ return true;
1145
+ };
1146
+
1147
+ // Try immediately and then retry a few times if DB not yet ready
1148
+ if (!tryWrapDB()) {
1149
+ let attempts = 0;
1150
+ const maxAttempts = 20;
1151
+ const interval = setInterval(() => {
1152
+ attempts++;
1153
+ if (tryWrapDB() || attempts >= maxAttempts) {
1154
+ clearInterval(interval);
1155
+ }
1156
+ }, 250);
1157
+ }
1158
+
1159
+ // Central listener: debounce UI/video sync to avoid thrashing
1160
+ let syncTimer = null;
1161
+ let lastTraits = {};
1162
+ window.addEventListener("personality:updated", async e => {
1163
+ try {
1164
+ if (e && e.detail && e.detail.traits) {
1165
+ // Merge incremental updates
1166
+ lastTraits = { ...lastTraits, ...e.detail.traits };
1167
+ }
1168
+ } catch {}
1169
+
1170
+ if (syncTimer) clearTimeout(syncTimer);
1171
+ syncTimer = setTimeout(async () => {
1172
+ try {
1173
+ const db = window.kimiDB;
1174
+ const character = (e && e.detail && e.detail.character) || (db && (await db.getSelectedCharacter())) || null;
1175
+ let traits = lastTraits;
1176
+ if (!traits || Object.keys(traits).length === 0) {
1177
+ // Fallback: fetch all traits if partial not provided
1178
+ traits = db && (await db.getAllPersonalityTraits(character));
1179
+ }
1180
+
1181
+ // 1) Update UI sliders if available
1182
+ if (typeof window.updateSlider === "function" && traits) {
1183
+ for (const [trait, value] of Object.entries(traits)) {
1184
+ const id = `trait-${trait}`;
1185
+ if (document.getElementById(id)) {
1186
+ try {
1187
+ window.updateSlider(id, value);
1188
+ } catch {}
1189
+ }
1190
+ }
1191
+ }
1192
+ if (typeof window.syncPersonalityTraits === "function") {
1193
+ try {
1194
+ await window.syncPersonalityTraits(character);
1195
+ } catch {}
1196
+ }
1197
+
1198
+ // 2) Update memory cache affection bar if available
1199
+ if (window.kimiMemory && typeof window.kimiMemory.updateAffectionTrait === "function") {
1200
+ try {
1201
+ await window.kimiMemory.updateAffectionTrait();
1202
+ } catch {}
1203
+ }
1204
+
1205
+ // 3) Update video mood by personality
1206
+ if (window.kimiVideo && typeof window.kimiVideo.setMoodByPersonality === "function") {
1207
+ const allTraits =
1208
+ traits && Object.keys(traits).length > 0
1209
+ ? { ...traits }
1210
+ : (db && (await db.getAllPersonalityTraits(character))) || {};
1211
+ try {
1212
+ window.kimiVideo.setMoodByPersonality(allTraits);
1213
+ } catch {}
1214
+ // 3b) Update voice modulation based on personality
1215
+ try {
1216
+ if (window.voiceManager && typeof window.voiceManager.updatePersonalityModulation === "function") {
1217
+ window.voiceManager.updatePersonalityModulation(allTraits);
1218
+ }
1219
+ } catch {}
1220
+ }
1221
+
1222
+ // 4) Ensure current video context is valid (lightweight guard)
1223
+ let beforeInfo = null;
1224
+ try {
1225
+ if (window.kimiVideo && typeof window.kimiVideo.getCurrentVideoInfo === "function") {
1226
+ beforeInfo = window.kimiVideo.getCurrentVideoInfo();
1227
+ }
1228
+ } catch {}
1229
+
1230
+ if (typeof window.ensureVideoContextConsistency === "function") {
1231
+ try {
1232
+ await window.ensureVideoContextConsistency();
1233
+ } catch {}
1234
+ }
1235
+
1236
+ try {
1237
+ if (
1238
+ window.KIMI_DEBUG_SYNC &&
1239
+ window.kimiVideo &&
1240
+ typeof window.kimiVideo.getCurrentVideoInfo === "function"
1241
+ ) {
1242
+ const afterInfo = window.kimiVideo.getCurrentVideoInfo();
1243
+ if (
1244
+ beforeInfo &&
1245
+ afterInfo &&
1246
+ (beforeInfo.context !== afterInfo.context ||
1247
+ beforeInfo.emotion !== afterInfo.emotion ||
1248
+ beforeInfo.category !== afterInfo.category)
1249
+ ) {
1250
+ console.log("🔧 SyncGuard: corrected video context", { from: beforeInfo, to: afterInfo });
1251
+ }
1252
+ }
1253
+ } catch {}
1254
+ } catch {
1255
+ } finally {
1256
+ lastTraits = {};
1257
+ }
1258
+ }, 120); // small debounce
1259
+ });
1260
+ })();
1261
  });
kimi-js/kimi-security.js CHANGED
@@ -2,7 +2,7 @@
2
 
3
  window.KIMI_SECURITY_CONFIG = {
4
  // Input validation limits
5
- MAX_MESSAGE_LENGTH: 2000,
6
  MAX_API_KEY_LENGTH: 200,
7
  MIN_API_KEY_LENGTH: 10,
8
 
@@ -69,3 +69,95 @@ window.KIMI_VALIDATORS = {
69
  };
70
 
71
  window.KIMI_SECURITY_INITIALIZED = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  window.KIMI_SECURITY_CONFIG = {
4
  // Input validation limits
5
+ MAX_MESSAGE_LENGTH: 5000,
6
  MAX_API_KEY_LENGTH: 200,
7
  MIN_API_KEY_LENGTH: 10,
8
 
 
69
  };
70
 
71
  window.KIMI_SECURITY_INITIALIZED = true;
72
+
73
+ // ===== Global Input Hardening (anti-autofill and password manager suppression) =====
74
+ (function setupGlobalInputHardening() {
75
+ try {
76
+ const ATTRS = {
77
+ autocomplete: "off",
78
+ autocapitalize: "none",
79
+ autocorrect: "off",
80
+ spellcheck: "false",
81
+ inputmode: "text",
82
+ "aria-autocomplete": "none",
83
+ "data-lpignore": "true",
84
+ "data-1p-ignore": "true",
85
+ "data-bwignore": "true",
86
+ "data-form-type": "other"
87
+ };
88
+
89
+ const API_INPUT_ID = "openrouter-api-key";
90
+
91
+ function hardenElement(el) {
92
+ if (!el || !(el instanceof HTMLElement)) return;
93
+ const tag = el.tagName;
94
+ if (tag !== "INPUT" && tag !== "TEXTAREA") return;
95
+
96
+ // Do not convert other inputs to password; only enforce anti-autofill attributes
97
+ for (const [k, v] of Object.entries(ATTRS)) {
98
+ try {
99
+ if (el.getAttribute(k) !== v) el.setAttribute(k, v);
100
+ } catch {}
101
+ }
102
+
103
+ // Special handling for the API key field: ensure it's treated as non-credential by managers
104
+ if (el.id === API_INPUT_ID) {
105
+ try {
106
+ // Keep password type by default for masking; JS toggler can switch to text on demand
107
+ if (!el.hasAttribute("type")) el.setAttribute("type", "password");
108
+ // Explicitly set a non-credential-ish name/value context
109
+ if (el.getAttribute("name") !== "openrouter_api_key") el.setAttribute("name", "openrouter_api_key");
110
+ if (el.getAttribute("autocomplete") !== "new-password") el.setAttribute("autocomplete", "new-password");
111
+ } catch {}
112
+ } else {
113
+ // For non-API inputs, if browser set type=password by heuristics, revert to text
114
+ try {
115
+ if (el.getAttribute("type") === "password" && el.id !== API_INPUT_ID) {
116
+ el.setAttribute("type", "text");
117
+ }
118
+ } catch {}
119
+ }
120
+ }
121
+
122
+ function hardenAll(scope = document) {
123
+ const nodes = scope.querySelectorAll("input, textarea");
124
+ nodes.forEach(hardenElement);
125
+ }
126
+
127
+ // Initial pass
128
+ if (document.readyState === "loading") {
129
+ document.addEventListener("DOMContentLoaded", () => hardenAll());
130
+ } else {
131
+ hardenAll();
132
+ }
133
+
134
+ // Observe dynamic DOM changes
135
+ const mo = new MutationObserver(mutations => {
136
+ for (const m of mutations) {
137
+ if (m.type === "childList") {
138
+ m.addedNodes.forEach(node => {
139
+ if (node.nodeType === 1) {
140
+ if (node.matches && (node.matches("input") || node.matches("textarea"))) {
141
+ hardenElement(node);
142
+ }
143
+ const descendants = node.querySelectorAll ? node.querySelectorAll("input, textarea") : [];
144
+ descendants.forEach(hardenElement);
145
+ }
146
+ });
147
+ }
148
+ }
149
+ });
150
+ try {
151
+ mo.observe(document.documentElement || document.body, {
152
+ subtree: true,
153
+ childList: true
154
+ });
155
+ } catch {}
156
+
157
+ // Expose for debugging if needed
158
+ window._kimiInputHardener = { hardenAll };
159
+ } catch (e) {
160
+ // Fail-safe: never block the app
161
+ console.warn("Input hardening setup error:", e);
162
+ }
163
+ })();
kimi-js/kimi-utils.js CHANGED
@@ -2,82 +2,87 @@
2
 
3
  // Input validation and sanitization utilities
4
  window.KimiValidationUtils = {
5
- // Validate and sanitize user messages
6
- validateMessage: function (message) {
7
  if (!message || typeof message !== "string") {
8
  return { valid: false, error: "Message must be a non-empty string" };
9
  }
10
-
11
  const trimmed = message.trim();
12
- if (trimmed.length === 0) {
13
- return { valid: false, error: "Message cannot be empty" };
14
- }
15
-
16
- if (trimmed.length > 5000) {
17
- return { valid: false, error: "Message too long (max 5000 characters)" };
18
  }
19
-
20
- // Basic XSS prevention
21
- const sanitized = this.escapeHtml(trimmed);
22
-
23
- return { valid: true, sanitized: sanitized };
24
  },
25
-
26
- // Escape HTML to prevent XSS
27
- escapeHtml: function (text) {
28
  const div = document.createElement("div");
29
  div.textContent = text;
30
  return div.innerHTML;
31
  },
32
-
33
- // Validate numeric ranges for sliders
34
- validateRange: function (value, type) {
35
- const numValue = parseFloat(value);
36
- if (isNaN(numValue)) {
37
- return { valid: false, value: null };
38
- }
39
-
40
- const ranges = {
41
- voiceRate: { min: 0.5, max: 2, default: 1.1 },
42
- voicePitch: { min: 0.5, max: 2, default: 1.1 },
43
- voiceVolume: { min: 0, max: 1, default: 0.8 },
44
- llmTemperature: { min: 0.1, max: 1, default: 0.9 },
45
- llmMaxTokens: { min: 10, max: 1000, default: 100 },
46
- llmTopP: { min: 0, max: 1, default: 0.9 },
47
- llmFrequencyPenalty: { min: 0, max: 2, default: 0.3 },
48
- llmPresencePenalty: { min: 0, max: 2, default: 0.3 },
49
- interfaceOpacity: { min: 0.1, max: 1, default: 0.8 }
50
  };
 
 
 
 
 
 
 
 
 
51
 
52
- const range = ranges[type];
53
- if (!range) {
54
- return { valid: false, value: null };
55
- }
56
-
57
- const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
58
- return { valid: true, value: clampedValue };
 
 
 
 
59
  },
60
-
61
- // Validate API keys
62
- validateApiKey: function (key) {
63
- if (!key || typeof key !== "string") {
64
- return { valid: false, error: "API key must be a string" };
65
- }
66
- const trimmed = key.trim();
67
- if (trimmed.length === 0) {
68
- return { valid: false, error: "API key cannot be empty" };
69
- }
70
- if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
71
- const ok = window.KIMI_VALIDATORS.validateApiKey(trimmed);
72
- return ok ? { valid: true, sanitized: trimmed } : { valid: false, error: "Invalid API key format" };
73
- }
74
- return { valid: true, sanitized: trimmed };
 
 
 
 
 
 
 
 
75
  }
76
  };
 
 
77
 
78
  // Performance utility functions for debouncing and throttling
79
  window.KimiPerformanceUtils = {
80
- // Enhanced debounce with immediate execution option
81
  debounce: function (func, wait, immediate = false, context = null) {
82
  let timeout;
83
  let result;
@@ -102,7 +107,6 @@ window.KimiPerformanceUtils = {
102
  };
103
  },
104
 
105
- // Enhanced throttle function with leading and trailing options
106
  throttle: function (func, limit, options = {}) {
107
  const { leading = true, trailing = true } = options;
108
  let inThrottle;
@@ -131,38 +135,6 @@ window.KimiPerformanceUtils = {
131
 
132
  setTimeout(() => (inThrottle = false), limit);
133
  };
134
- },
135
-
136
- // Batch processing utility
137
- createBatcher: function (processor, delay = 500) {
138
- let timeout = null;
139
- const pending = {};
140
-
141
- return function (key, value) {
142
- pending[key] = value;
143
-
144
- if (timeout) clearTimeout(timeout);
145
-
146
- timeout = setTimeout(async () => {
147
- if (Object.keys(pending).length > 0) {
148
- try {
149
- await processor(pending);
150
- Object.keys(pending).forEach(k => delete pending[k]);
151
- } catch (error) {
152
- console.error("Batch processing error:", error);
153
- }
154
- }
155
- }, delay);
156
- };
157
- },
158
-
159
- // Batch requests utility
160
- batchRequests: function (requests, batchSize = 10) {
161
- const batches = [];
162
- for (let i = 0; i < requests.length; i += batchSize) {
163
- batches.push(requests.slice(i, i + batchSize));
164
- }
165
- return batches;
166
  }
167
  };
168
 
@@ -242,18 +214,7 @@ class KimiSecurityUtils {
242
  return key.trim().length > 10 && (key.startsWith("sk-") || key.startsWith("sk-or-"));
243
  }
244
 
245
- static encryptApiKey(key) {
246
- // Simple encoding for basic protection (not cryptographically secure)
247
- return btoa(key).split("").reverse().join("");
248
- }
249
-
250
- static decryptApiKey(encodedKey) {
251
- try {
252
- return atob(encodedKey.split("").reverse().join(""));
253
- } catch {
254
- return "";
255
- }
256
- }
257
  }
258
 
259
  // Cache management for better performance
@@ -358,7 +319,7 @@ class KimiVideoManager {
358
  this.lastSwitchTime = Date.now();
359
  this.pendingSwitch = null;
360
  this.autoTransitionDuration = 9900;
361
- this.transitionDuration = 900;
362
  this._prefetchCache = new Map();
363
  this._prefetchInFlight = new Set();
364
  this._maxPrefetch = 3;
@@ -411,6 +372,96 @@ class KimiVideoManager {
411
  this._debug = false;
412
  }
413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
414
  setDebug(enabled) {
415
  this._debug = !!enabled;
416
  }
@@ -712,7 +763,8 @@ class KimiVideoManager {
712
  if (speakingCurrent !== speakingPath || this.activeVideo.ended) {
713
  this.loadAndSwitchVideo(speakingPath, priority);
714
  }
715
- this.currentContext = context;
 
716
  this.currentEmotion = emotion;
717
  this.lastSwitchTime = Date.now();
718
  return;
@@ -723,7 +775,8 @@ class KimiVideoManager {
723
  if (listeningCurrent !== listeningPath || this.activeVideo.ended) {
724
  this.loadAndSwitchVideo(listeningPath, priority);
725
  }
726
- this.currentContext = context;
 
727
  this.currentEmotion = emotion;
728
  this.lastSwitchTime = Date.now();
729
  return;
@@ -777,7 +830,8 @@ class KimiVideoManager {
777
  this._prefetchLikely(category);
778
 
779
  this.loadAndSwitchVideo(videoPath, priority);
780
- this.currentContext = context;
 
781
  this.currentEmotion = emotion;
782
  this.lastSwitchTime = now;
783
  }
@@ -812,10 +866,26 @@ class KimiVideoManager {
812
 
813
  if (context === "speakingPositive" || context === "speakingNegative") {
814
  this._globalEndedHandler = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
815
  this.isEmotionVideoPlaying = false;
816
  this.currentEmotionContext = null;
817
  this._neutralLock = false;
818
- // Process any pending high-priority switch first; otherwise return to neutral
819
  if (!this._processPendingSwitches()) {
820
  this.returnToNeutral();
821
  }
@@ -989,6 +1059,12 @@ class KimiVideoManager {
989
  // Immediate switch to keep UI responsive
990
  this.switchToContext("listening");
991
 
 
 
 
 
 
 
992
  // If caller did not provide traits, try to fetch and refine selection
993
  try {
994
  if (!traits && window.kimiDB && typeof window.kimiDB.getAllPersonalityTraits === "function") {
@@ -1019,6 +1095,23 @@ class KimiVideoManager {
1019
  if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1020
  // If we are already playing the same emotion video, do nothing
1021
  if (this.isEmotionVideoPlaying && this.currentEmotionContext === emotion) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1022
  // First switch context (so internal guards don't see the new flags yet)
1023
  this.switchToContext("speaking", emotion, null, traits, affection);
1024
  // Then mark the emotion video as playing for override protection
@@ -1038,7 +1131,7 @@ class KimiVideoManager {
1038
  this.isEmotionVideoPlaying = false;
1039
  this.currentEmotionContext = null;
1040
 
1041
- // Force-select a neutral clip different from current, then load and switch
1042
  const category = "neutral";
1043
  const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
1044
  const available = this.videoCategories[category] || [];
@@ -1056,6 +1149,18 @@ class KimiVideoManager {
1056
  this.currentContext = "neutral";
1057
  this.currentEmotion = "neutral";
1058
  this.lastSwitchTime = Date.now();
 
 
 
 
 
 
 
 
 
 
 
 
1059
  } else {
1060
  // Fallback to existing path if list empty
1061
  this.switchToContext("neutral");
@@ -1236,6 +1341,14 @@ class KimiVideoManager {
1236
  }
1237
 
1238
  loadAndSwitchVideo(videoSrc, priority = "normal") {
 
 
 
 
 
 
 
 
1239
  // Only log high priority or error cases to reduce noise
1240
  if (priority === "speaking" || priority === "high") {
1241
  console.log(`🎬 Loading video: ${videoSrc} (priority: ${priority})`);
@@ -1367,47 +1480,56 @@ class KimiVideoManager {
1367
 
1368
  performSwitch() {
1369
  // Prevent rapid double toggles
1370
- if (this._switchInProgress) {
1371
- return;
1372
- }
1373
  this._switchInProgress = true;
1374
 
1375
- this.activeVideo.classList.remove("active");
1376
- this.inactiveVideo.classList.add("active");
1377
- const prevActive = this.activeVideo;
1378
- const prevInactive = this.inactiveVideo;
1379
- this.activeVideo = prevInactive;
1380
- this.inactiveVideo = prevActive;
1381
-
1382
- const playPromise = this.activeVideo.play();
1383
- if (playPromise && typeof playPromise.then === "function") {
1384
- playPromise
1385
- .then(() => {
1386
- // Reduced logging for video playing events
1387
- this._switchInProgress = false;
1388
- // Configurer les event listeners APRÈS que la vidéo commence à jouer
1389
- this.setupEventListenersForContext(this.currentContext);
1390
- })
1391
- .catch(error => {
1392
- console.warn("Failed to play video:", error);
1393
- // Revert to previous video to avoid frozen state
1394
- this.activeVideo.classList.remove("active");
1395
- this.inactiveVideo.classList.add("active");
1396
- const tmp = this.activeVideo;
1397
- this.activeVideo = this.inactiveVideo;
1398
- this.inactiveVideo = tmp;
1399
- try {
1400
- this.activeVideo.play().catch(() => {});
1401
- } catch {}
1402
- this._switchInProgress = false;
1403
- // Even in case of error, configure listeners
1404
- this.setupEventListenersForContext(this.currentContext);
1405
- });
1406
- } else {
1407
- // Fallback si pas de Promise
1408
- this._switchInProgress = false;
1409
- this.setupEventListenersForContext(this.currentContext);
1410
- }
 
 
 
 
 
 
 
 
 
 
 
1411
  }
1412
 
1413
  _prefetch(src) {
@@ -1732,6 +1854,8 @@ class KimiTabManager {
1732
  this.settingsContent = document.querySelector(".settings-content");
1733
  this.onTabChange = options.onTabChange || null;
1734
  this.resizeObserver = null;
 
 
1735
  this.init();
1736
  }
1737
 
@@ -1756,6 +1880,14 @@ class KimiTabManager {
1756
  if (content.dataset.tab === tabName) content.classList.add("active");
1757
  else content.classList.remove("active");
1758
  });
 
 
 
 
 
 
 
 
1759
  if (this.onTabChange) this.onTabChange(tabName);
1760
  setTimeout(() => this.adjustTabsForScrollbar(), 100);
1761
  if (window.innerWidth <= 768) {
@@ -1767,7 +1899,13 @@ class KimiTabManager {
1767
  setupResizeObserver() {
1768
  if ("ResizeObserver" in window && this.settingsContent) {
1769
  this.resizeObserver = new ResizeObserver(() => {
1770
- this.adjustTabsForScrollbar();
 
 
 
 
 
 
1771
  });
1772
  this.resizeObserver.observe(this.settingsContent);
1773
  }
@@ -1779,7 +1917,13 @@ class KimiTabManager {
1779
  mutations.forEach(mutation => {
1780
  if (mutation.type === "attributes" && mutation.attributeName === "class") {
1781
  if (this.settingsOverlay.classList.contains("visible")) {
1782
- // ...
 
 
 
 
 
 
1783
  }
1784
  }
1785
  });
@@ -1856,48 +2000,10 @@ class KimiFormManager {
1856
  _initSliders() {
1857
  document.querySelectorAll(".kimi-slider").forEach(slider => {
1858
  const valueSpan = document.getElementById(slider.id + "-value");
1859
- if (valueSpan) {
1860
- valueSpan.textContent = slider.value;
1861
- }
1862
- slider.addEventListener("input", async e => {
1863
  if (valueSpan) valueSpan.textContent = slider.value;
1864
- if (this.db) {
1865
- const settingName = slider.id.replace("trait-", "").replace("voice-", "").replace("llm-", "");
1866
- if (slider.id.startsWith("trait-")) {
1867
- const selectedCharacter = await this.db.getSelectedCharacter();
1868
- await this.db.setPersonalityTrait(settingName, parseInt(slider.value), selectedCharacter);
1869
-
1870
- // Update memory cache if available
1871
- if (this.memory && settingName === "affection") {
1872
- this.memory.affectionTrait = parseInt(slider.value);
1873
- if (this.memory.updateFavorabilityBar) {
1874
- this.memory.updateFavorabilityBar();
1875
- }
1876
- }
1877
-
1878
- // Update video context based on new personality values
1879
- if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1880
- const allTraits = await this.db.getAllPersonalityTraits(selectedCharacter);
1881
- window.kimiVideo.setMoodByPersonality(allTraits);
1882
- }
1883
-
1884
- // Favorability bar is automatically updated by KimiMemory system
1885
- } else if (slider.id.startsWith("voice-")) {
1886
- await this.db.setPreference(settingName, parseFloat(slider.value));
1887
- if (window.voiceManager && window.voiceManager.updateSettings) {
1888
- window.voiceManager.updateSettings();
1889
- }
1890
- } else {
1891
- await this.db.setPreference(settingName, parseFloat(slider.value));
1892
-
1893
- // Update LLM settings if needed
1894
- if (slider.id.startsWith("llm-") && window.kimiLLM) {
1895
- if (window.kimiLLM.updateSettings) {
1896
- window.kimiLLM.updateSettings();
1897
- }
1898
- }
1899
- }
1900
- }
1901
  });
1902
  });
1903
  }
@@ -2104,3 +2210,23 @@ window.KimiTabManager = KimiTabManager;
2104
  window.KimiUIEventManager = KimiUIEventManager;
2105
  window.KimiFormManager = KimiFormManager;
2106
  window.KimiUIStateManager = KimiUIStateManager;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  // Input validation and sanitization utilities
4
  window.KimiValidationUtils = {
5
+ validateMessage(message) {
 
6
  if (!message || typeof message !== "string") {
7
  return { valid: false, error: "Message must be a non-empty string" };
8
  }
 
9
  const trimmed = message.trim();
10
+ if (!trimmed) return { valid: false, error: "Message cannot be empty" };
11
+ const MAX = (window.KIMI_SECURITY_CONFIG && window.KIMI_SECURITY_CONFIG.MAX_MESSAGE_LENGTH) || 5000;
12
+ if (trimmed.length > MAX) {
13
+ return { valid: false, error: `Message too long (max ${MAX} characters)` };
 
 
14
  }
15
+ return { valid: true, sanitized: this.escapeHtml(trimmed) };
 
 
 
 
16
  },
17
+ escapeHtml(text) {
 
 
18
  const div = document.createElement("div");
19
  div.textContent = text;
20
  return div.innerHTML;
21
  },
22
+ validateRange(value, key) {
23
+ const bounds = {
24
+ voiceRate: { min: 0.5, max: 2, def: 1.1 },
25
+ voicePitch: { min: 0, max: 2, def: 1.0 },
26
+ voiceVolume: { min: 0, max: 1, def: 0.8 },
27
+ llmTemperature: { min: 0, max: 2, def: 0.9 },
28
+ llmMaxTokens: { min: 1, max: 32000, def: 200 },
29
+ llmTopP: { min: 0, max: 1, def: 1 },
30
+ llmFrequencyPenalty: { min: 0, max: 2, def: 0 },
31
+ llmPresencePenalty: { min: 0, max: 2, def: 0 },
32
+ interfaceOpacity: { min: 0.1, max: 1, def: 0.95 }
 
 
 
 
 
 
 
33
  };
34
+ const b = bounds[key] || { min: 0, max: 100, def: 0 };
35
+ const v = window.KimiSecurityUtils
36
+ ? window.KimiSecurityUtils.validateRange(value, b.min, b.max, b.def)
37
+ : isNaN(parseFloat(value))
38
+ ? b.def
39
+ : Math.max(b.min, Math.min(b.max, parseFloat(value)));
40
+ return { value: v, clamped: v !== parseFloat(value) };
41
+ }
42
+ };
43
 
44
+ // Provider utilities used across the app
45
+ const KimiProviderUtils = {
46
+ keyPrefMap: {
47
+ openrouter: "openrouterApiKey",
48
+ openai: "apiKey_openai",
49
+ groq: "apiKey_groq",
50
+ together: "apiKey_together",
51
+ deepseek: "apiKey_deepseek",
52
+ custom: "apiKey_custom",
53
+ "openai-compatible": "llmApiKey",
54
+ ollama: null
55
  },
56
+ getKeyPrefForProvider(provider) {
57
+ return this.keyPrefMap[provider] || "llmApiKey";
58
+ },
59
+ async getApiKey(db, provider) {
60
+ if (!db) return null;
61
+ if (provider === "ollama") return "__local__";
62
+ const pref = this.getKeyPrefForProvider(provider);
63
+ if (!pref) return null;
64
+ if (provider === "openrouter") return await db.getPreference("openrouterApiKey");
65
+ return await db.getPreference(pref);
66
+ },
67
+ getLabelForProvider(provider) {
68
+ const labels = {
69
+ openrouter: "OpenRouter API Key",
70
+ openai: "OpenAI API Key",
71
+ groq: "Groq API Key",
72
+ together: "Together API Key",
73
+ deepseek: "DeepSeek API Key",
74
+ custom: "Custom API Key",
75
+ "openai-compatible": "API Key",
76
+ ollama: "API Key"
77
+ };
78
+ return labels[provider] || "API Key";
79
  }
80
  };
81
+ window.KimiProviderUtils = KimiProviderUtils;
82
+ export { KimiProviderUtils };
83
 
84
  // Performance utility functions for debouncing and throttling
85
  window.KimiPerformanceUtils = {
 
86
  debounce: function (func, wait, immediate = false, context = null) {
87
  let timeout;
88
  let result;
 
107
  };
108
  },
109
 
 
110
  throttle: function (func, limit, options = {}) {
111
  const { leading = true, trailing = true } = options;
112
  let inThrottle;
 
135
 
136
  setTimeout(() => (inThrottle = false), limit);
137
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  }
139
  };
140
 
 
214
  return key.trim().length > 10 && (key.startsWith("sk-") || key.startsWith("sk-or-"));
215
  }
216
 
217
+ // Removed unused encrypt/decrypt for clarity; storage should rely on secure contexts if reintroduced
 
 
 
 
 
 
 
 
 
 
 
218
  }
219
 
220
  // Cache management for better performance
 
319
  this.lastSwitchTime = Date.now();
320
  this.pendingSwitch = null;
321
  this.autoTransitionDuration = 9900;
322
+ this.transitionDuration = 300;
323
  this._prefetchCache = new Map();
324
  this._prefetchInFlight = new Set();
325
  this._maxPrefetch = 3;
 
372
  this._debug = false;
373
  }
374
 
375
+ /**
376
+ * Centralized crossfade transition between two videos.
377
+ * Ensures both videos are loaded and playing before transition.
378
+ * @param {HTMLVideoElement} fromVideo - The currently visible video.
379
+ * @param {HTMLVideoElement} toVideo - The next video to show.
380
+ * @param {number} duration - Transition duration in ms.
381
+ * @param {function} [onComplete] - Optional callback after transition.
382
+ */
383
+ static crossfadeVideos(fromVideo, toVideo, duration = 300, onComplete) {
384
+ // Resolve duration from CSS variable if present
385
+ try {
386
+ const cssDur = getComputedStyle(document.documentElement).getPropertyValue("--video-fade-duration").trim();
387
+ if (cssDur) {
388
+ // Convert CSS time to ms number if needed (e.g., '300ms' or '0.3s')
389
+ if (cssDur.endsWith("ms")) duration = parseFloat(cssDur);
390
+ else if (cssDur.endsWith("s")) duration = Math.round(parseFloat(cssDur) * 1000);
391
+ }
392
+ } catch {}
393
+
394
+ // Preload and strict synchronization
395
+ const easing = "ease-in-out";
396
+ fromVideo.style.transition = `opacity ${duration}ms ${easing}`;
397
+ toVideo.style.transition = `opacity ${duration}ms ${easing}`;
398
+ // Prepare target video (opacity 0, top z-index)
399
+ toVideo.style.opacity = "0";
400
+ toVideo.style.zIndex = "2";
401
+ fromVideo.style.zIndex = "1";
402
+
403
+ // Start target video slightly before the crossfade
404
+ const startTarget = () => {
405
+ if (toVideo.paused) toVideo.play().catch(() => {});
406
+ // Lance le fondu croisé
407
+ setTimeout(() => {
408
+ fromVideo.style.opacity = "0";
409
+ toVideo.style.opacity = "1";
410
+ }, 20);
411
+ // After transition, adjust z-index and call the callback
412
+ setTimeout(() => {
413
+ fromVideo.style.zIndex = "1";
414
+ toVideo.style.zIndex = "2";
415
+ if (onComplete) onComplete();
416
+ }, duration + 30);
417
+ };
418
+
419
+ // If target video is not ready, wait for canplay
420
+ if (toVideo.readyState < 3) {
421
+ toVideo.addEventListener("canplay", startTarget, { once: true });
422
+ toVideo.load();
423
+ } else {
424
+ startTarget();
425
+ }
426
+ // Ensure source video is playing
427
+ if (fromVideo.paused) fromVideo.play().catch(() => {});
428
+ }
429
+
430
+ /**
431
+ * Centralized video element creation utility.
432
+ * @param {string} id - The id for the video element.
433
+ * @param {string} [className] - Optional class name.
434
+ * @returns {HTMLVideoElement}
435
+ */
436
+ static createVideoElement(id, className = "bg-video") {
437
+ const video = document.createElement("video");
438
+ video.id = id;
439
+ video.className = className;
440
+ video.autoplay = true;
441
+ video.muted = true;
442
+ video.playsinline = true;
443
+ video.preload = "auto";
444
+ video.style.opacity = "0";
445
+ video.innerHTML =
446
+ '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
447
+ return video;
448
+ }
449
+
450
+ /**
451
+ * Centralized video selection utility.
452
+ * @param {string} selector - CSS selector or id.
453
+ * @returns {HTMLVideoElement|null}
454
+ */
455
+ static getVideoElement(selector) {
456
+ if (typeof selector === "string") {
457
+ if (selector.startsWith("#")) {
458
+ return document.getElementById(selector.slice(1));
459
+ }
460
+ return document.querySelector(selector);
461
+ }
462
+ return selector;
463
+ }
464
+
465
  setDebug(enabled) {
466
  this._debug = !!enabled;
467
  }
 
763
  if (speakingCurrent !== speakingPath || this.activeVideo.ended) {
764
  this.loadAndSwitchVideo(speakingPath, priority);
765
  }
766
+ // IMPORTANT: normalize to the resolved category (e.g., speakingPositive/Negative)
767
+ this.currentContext = category;
768
  this.currentEmotion = emotion;
769
  this.lastSwitchTime = Date.now();
770
  return;
 
775
  if (listeningCurrent !== listeningPath || this.activeVideo.ended) {
776
  this.loadAndSwitchVideo(listeningPath, priority);
777
  }
778
+ // Normalize to category for consistency
779
+ this.currentContext = category;
780
  this.currentEmotion = emotion;
781
  this.lastSwitchTime = Date.now();
782
  return;
 
830
  this._prefetchLikely(category);
831
 
832
  this.loadAndSwitchVideo(videoPath, priority);
833
+ // Always store normalized category as currentContext so event bindings match speakingPositive/Negative
834
+ this.currentContext = category;
835
  this.currentEmotion = emotion;
836
  this.lastSwitchTime = now;
837
  }
 
866
 
867
  if (context === "speakingPositive" || context === "speakingNegative") {
868
  this._globalEndedHandler = () => {
869
+ // If TTS is still speaking, keep the speaking flow by chaining another speaking clip
870
+ if (window.voiceManager && window.voiceManager.isSpeaking) {
871
+ const emotion = this.currentEmotion || this.currentEmotionContext || "positive";
872
+ // Preserve speaking context while chaining
873
+ const category = emotion === "negative" ? "speakingNegative" : "speakingPositive";
874
+ const next = this.selectOptimalVideo(category, null, null, null, emotion);
875
+ if (next) {
876
+ this.loadAndSwitchVideo(next, "speaking");
877
+ this.currentContext = category;
878
+ this.currentEmotion = emotion;
879
+ this.isEmotionVideoPlaying = true;
880
+ this.currentEmotionContext = emotion;
881
+ this.lastSwitchTime = Date.now();
882
+ return;
883
+ }
884
+ }
885
+ // Otherwise, allow pending high-priority switch or return to neutral
886
  this.isEmotionVideoPlaying = false;
887
  this.currentEmotionContext = null;
888
  this._neutralLock = false;
 
889
  if (!this._processPendingSwitches()) {
890
  this.returnToNeutral();
891
  }
 
1059
  // Immediate switch to keep UI responsive
1060
  this.switchToContext("listening");
1061
 
1062
+ // Add a short grace window to prevent immediate switch to speaking before TTS starts
1063
+ clearTimeout(this._listeningGraceTimer);
1064
+ this._listeningGraceTimer = setTimeout(() => {
1065
+ // No-op; used as a time marker to let LLM prepare the answer
1066
+ }, 1500);
1067
+
1068
  // If caller did not provide traits, try to fetch and refine selection
1069
  try {
1070
  if (!traits && window.kimiDB && typeof window.kimiDB.getAllPersonalityTraits === "function") {
 
1095
  if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1096
  // If we are already playing the same emotion video, do nothing
1097
  if (this.isEmotionVideoPlaying && this.currentEmotionContext === emotion) return;
1098
+ // If we just entered listening and TTS isn’t started yet, wait a bit to avoid desync
1099
+ const now = Date.now();
1100
+ const stillInGrace = this._listeningGraceTimer != null;
1101
+ const ttsNotStarted = !(window.voiceManager && window.voiceManager.isSpeaking);
1102
+ if (this.currentContext === "listening" && stillInGrace && ttsNotStarted) {
1103
+ clearTimeout(this._pendingSpeakSwitch);
1104
+ this._pendingSpeakSwitch = setTimeout(() => {
1105
+ // Re-check speaking state; only switch when we have an actual emotion to play alongside TTS
1106
+ if (window.voiceManager && window.voiceManager.isSpeaking) {
1107
+ this.switchToContext("speaking", emotion, null, traits, affection);
1108
+ this.isEmotionVideoPlaying = true;
1109
+ this.currentEmotionContext = emotion;
1110
+ }
1111
+ }, 900);
1112
+ return;
1113
+ }
1114
+
1115
  // First switch context (so internal guards don't see the new flags yet)
1116
  this.switchToContext("speaking", emotion, null, traits, affection);
1117
  // Then mark the emotion video as playing for override protection
 
1131
  this.isEmotionVideoPlaying = false;
1132
  this.currentEmotionContext = null;
1133
 
1134
+ // Correction : si la voix est encore en cours, relancer une vidéo neutre en boucle
1135
  const category = "neutral";
1136
  const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
1137
  const available = this.videoCategories[category] || [];
 
1149
  this.currentContext = "neutral";
1150
  this.currentEmotion = "neutral";
1151
  this.lastSwitchTime = Date.now();
1152
+ // Si la voix est encore en cours, s'assurer qu'on relance une vidéo neutre à la fin
1153
+ if (window.voiceManager && window.voiceManager.isSpeaking) {
1154
+ this.activeVideo.addEventListener(
1155
+ "ended",
1156
+ () => {
1157
+ if (window.voiceManager && window.voiceManager.isSpeaking) {
1158
+ this.returnToNeutral();
1159
+ }
1160
+ },
1161
+ { once: true }
1162
+ );
1163
+ }
1164
  } else {
1165
  // Fallback to existing path if list empty
1166
  this.switchToContext("neutral");
 
1341
  }
1342
 
1343
  loadAndSwitchVideo(videoSrc, priority = "normal") {
1344
+ // Avoid redundant loading if the requested source is already active or currently loading in inactive element
1345
+ const activeSrc = this.activeVideo?.querySelector("source")?.getAttribute("src");
1346
+ const inactiveSrc = this.inactiveVideo?.querySelector("source")?.getAttribute("src");
1347
+ if (videoSrc && (videoSrc === activeSrc || (this._loadingInProgress && videoSrc === inactiveSrc))) {
1348
+ if (priority !== "high" && priority !== "speaking") {
1349
+ return; // no need to reload same video
1350
+ }
1351
+ }
1352
  // Only log high priority or error cases to reduce noise
1353
  if (priority === "speaking" || priority === "high") {
1354
  console.log(`🎬 Loading video: ${videoSrc} (priority: ${priority})`);
 
1480
 
1481
  performSwitch() {
1482
  // Prevent rapid double toggles
1483
+ if (this._switchInProgress) return;
 
 
1484
  this._switchInProgress = true;
1485
 
1486
+ const fromVideo = this.activeVideo;
1487
+ const toVideo = this.inactiveVideo;
1488
+
1489
+ // Perform a JS-managed crossfade for smoother transitions
1490
+ // Let crossfadeVideos resolve duration from CSS variable (--video-fade-duration)
1491
+ this.constructor.crossfadeVideos(fromVideo, toVideo, undefined, () => {
1492
+ // After crossfade completion, finalize state and classes
1493
+ fromVideo.classList.remove("active");
1494
+ toVideo.classList.add("active");
1495
+
1496
+ // Swap references
1497
+ const prevActive = this.activeVideo;
1498
+ const prevInactive = this.inactiveVideo;
1499
+ this.activeVideo = prevInactive;
1500
+ this.inactiveVideo = prevActive;
1501
+
1502
+ const playPromise = this.activeVideo.play();
1503
+ if (playPromise && typeof playPromise.then === "function") {
1504
+ playPromise
1505
+ .then(() => {
1506
+ try {
1507
+ const src = this.activeVideo?.querySelector("source")?.getAttribute("src");
1508
+ const info = { context: this.currentContext, emotion: this.currentEmotion };
1509
+ console.log("🎬 VideoManager: Now playing:", src, info);
1510
+ } catch {}
1511
+ this._switchInProgress = false;
1512
+ this.setupEventListenersForContext(this.currentContext);
1513
+ })
1514
+ .catch(error => {
1515
+ console.warn("Failed to play video:", error);
1516
+ // Revert to previous video to avoid frozen state
1517
+ toVideo.classList.remove("active");
1518
+ fromVideo.classList.add("active");
1519
+ this.activeVideo = fromVideo;
1520
+ this.inactiveVideo = toVideo;
1521
+ try {
1522
+ this.activeVideo.play().catch(() => {});
1523
+ } catch {}
1524
+ this._switchInProgress = false;
1525
+ this.setupEventListenersForContext(this.currentContext);
1526
+ });
1527
+ } else {
1528
+ // Non-promise play fallback
1529
+ this._switchInProgress = false;
1530
+ this.setupEventListenersForContext(this.currentContext);
1531
+ }
1532
+ });
1533
  }
1534
 
1535
  _prefetch(src) {
 
1854
  this.settingsContent = document.querySelector(".settings-content");
1855
  this.onTabChange = options.onTabChange || null;
1856
  this.resizeObserver = null;
1857
+ // Guard flag to batch ResizeObserver callbacks within a frame
1858
+ this._resizeRafScheduled = false;
1859
  this.init();
1860
  }
1861
 
 
1880
  if (content.dataset.tab === tabName) content.classList.add("active");
1881
  else content.classList.remove("active");
1882
  });
1883
+ // Ensure the content scroll resets to the top when changing tabs
1884
+ if (this.settingsContent) {
1885
+ this.settingsContent.scrollTop = 0;
1886
+ // Defer once to handle layout updates after class toggles
1887
+ window.requestAnimationFrame(() => {
1888
+ this.settingsContent.scrollTop = 0;
1889
+ });
1890
+ }
1891
  if (this.onTabChange) this.onTabChange(tabName);
1892
  setTimeout(() => this.adjustTabsForScrollbar(), 100);
1893
  if (window.innerWidth <= 768) {
 
1899
  setupResizeObserver() {
1900
  if ("ResizeObserver" in window && this.settingsContent) {
1901
  this.resizeObserver = new ResizeObserver(() => {
1902
+ // Defer to next animation frame to avoid ResizeObserver loop warnings
1903
+ if (this._resizeRafScheduled) return;
1904
+ this._resizeRafScheduled = true;
1905
+ window.requestAnimationFrame(() => {
1906
+ this._resizeRafScheduled = false;
1907
+ this.adjustTabsForScrollbar();
1908
+ });
1909
  });
1910
  this.resizeObserver.observe(this.settingsContent);
1911
  }
 
1917
  mutations.forEach(mutation => {
1918
  if (mutation.type === "attributes" && mutation.attributeName === "class") {
1919
  if (this.settingsOverlay.classList.contains("visible")) {
1920
+ // Reset scroll to top when the settings modal opens
1921
+ if (this.settingsContent) {
1922
+ this.settingsContent.scrollTop = 0;
1923
+ window.requestAnimationFrame(() => {
1924
+ this.settingsContent.scrollTop = 0;
1925
+ });
1926
+ }
1927
  }
1928
  }
1929
  });
 
2000
  _initSliders() {
2001
  document.querySelectorAll(".kimi-slider").forEach(slider => {
2002
  const valueSpan = document.getElementById(slider.id + "-value");
2003
+ if (valueSpan) valueSpan.textContent = slider.value;
2004
+ // Only update visible value; side-effects handled by specialized listeners
2005
+ slider.addEventListener("input", () => {
 
2006
  if (valueSpan) valueSpan.textContent = slider.value;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2007
  });
2008
  });
2009
  }
 
2210
  window.KimiUIEventManager = KimiUIEventManager;
2211
  window.KimiFormManager = KimiFormManager;
2212
  window.KimiUIStateManager = KimiUIStateManager;
2213
+
2214
+ window.KimiTokenUtils = {
2215
+ // Approximate token estimation (heuristic):
2216
+ // Base: 1 token ~ 4 chars (English average). We refine by word count and punctuation density.
2217
+ estimate(text) {
2218
+ if (!text || typeof text !== "string") return 0;
2219
+ const trimmed = text.trim();
2220
+ if (!trimmed) return 0;
2221
+ const charLen = trimmed.length;
2222
+ const words = trimmed.split(/\s+/).length;
2223
+ // Base estimates
2224
+ let estimateByChars = Math.ceil(charLen / 4);
2225
+ const estimateByWords = Math.ceil(words * 1.3); // average 1.3 tokens per word
2226
+ // Blend and adjust for punctuation heavy content
2227
+ const punctCount = (trimmed.match(/[.,!?;:]/g) || []).length;
2228
+ const punctFactor = 1 + Math.min(punctCount / Math.max(words, 1) / 5, 0.15); // cap at +15%
2229
+ const blended = Math.round((estimateByChars * 0.55 + estimateByWords * 0.45) * punctFactor);
2230
+ return Math.max(1, blended);
2231
+ }
2232
+ };
kimi-js/kimi-voices.js CHANGED
@@ -45,13 +45,14 @@ class KimiVoiceManager {
45
  // Track if microphone permission has been granted
46
  this.micPermissionGranted = false;
47
 
48
- // Debounce flag for toggle microphone
49
- this._toggleDebounce = false;
 
 
50
 
51
  // Browser detection
52
  this.browser = this._detectBrowser();
53
  }
54
-
55
  // ===== INITIALIZATION =====
56
  async init() {
57
  // Avoid double initialization
@@ -394,6 +395,22 @@ class KimiVoiceManager {
394
  } else if (this.transcriptContainer) {
395
  this.transcriptContainer.classList.remove("visible");
396
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  };
398
 
399
  utterance.onend = () => {
@@ -901,9 +918,9 @@ class KimiVoiceManager {
901
  currentInfo.context === "speakingNegative" ||
902
  currentInfo.context === "dancing")
903
  ) {
904
- // On laisse la vidéo d'émotion se terminer naturellement
905
  } else if (this.isStoppingVolontaire) {
906
- // On retourne directement au neutre sans utiliser "transition"
907
  window.kimiVideo.returnToNeutral();
908
  }
909
  }
@@ -1068,22 +1085,15 @@ class KimiVoiceManager {
1068
 
1069
  // Public method for external microphone toggle (keyboard, etc.)
1070
  toggleMicrophone() {
 
 
 
 
 
1071
  if (!this.SpeechRecognition) {
1072
  console.warn("🎤 Speech recognition not available");
1073
  return false;
1074
  }
1075
-
1076
- // Add debouncing to prevent rapid toggles
1077
- if (this._toggleDebounce) {
1078
- console.log("🎤 Toggle debounced, ignoring rapid call");
1079
- return false;
1080
- }
1081
-
1082
- this._toggleDebounce = true;
1083
- setTimeout(() => {
1084
- this._toggleDebounce = false;
1085
- }, 300); // 300ms debounce
1086
-
1087
  // If Kimi is speaking, stop speech synthesis first
1088
  if (this.isSpeaking && this.speechSynthesis.speaking) {
1089
  console.log("🎤 Interrupting speech to start listening");
 
45
  // Track if microphone permission has been granted
46
  this.micPermissionGranted = false;
47
 
48
+ // Debounced microphone toggle (centralized utility)
49
+ this._debouncedToggleMicrophone = window.KimiPerformanceUtils
50
+ ? window.KimiPerformanceUtils.debounce(() => this._toggleMicrophoneCore(), 300, false, this)
51
+ : null;
52
 
53
  // Browser detection
54
  this.browser = this._detectBrowser();
55
  }
 
56
  // ===== INITIALIZATION =====
57
  async init() {
58
  // Avoid double initialization
 
395
  } else if (this.transcriptContainer) {
396
  this.transcriptContainer.classList.remove("visible");
397
  }
398
+ // Ensure a speaking animation plays (avoid frozen neutral frame during TTS)
399
+ try {
400
+ if (window.kimiVideo && window.kimiVideo.getCurrentVideoInfo) {
401
+ const info = window.kimiVideo.getCurrentVideoInfo();
402
+ if (info && !(info.context && info.context.startsWith("speaking"))) {
403
+ // Use positive speaking as neutral fallback
404
+ const traits = await this.db?.getAllPersonalityTraits(
405
+ window.kimiMemory?.selectedCharacter || (await this.db.getSelectedCharacter())
406
+ );
407
+ const affection = traits ? traits.affection : 50;
408
+ window.kimiVideo.switchToContext("speakingPositive", "positive", null, traits || {}, affection);
409
+ }
410
+ }
411
+ } catch (e) {
412
+ // Silent fallback
413
+ }
414
  };
415
 
416
  utterance.onend = () => {
 
918
  currentInfo.context === "speakingNegative" ||
919
  currentInfo.context === "dancing")
920
  ) {
921
+ // Let emotion video finish naturally
922
  } else if (this.isStoppingVolontaire) {
923
+ // Use centralized video utility for neutral transition
924
  window.kimiVideo.returnToNeutral();
925
  }
926
  }
 
1085
 
1086
  // Public method for external microphone toggle (keyboard, etc.)
1087
  toggleMicrophone() {
1088
+ if (this._debouncedToggleMicrophone) return this._debouncedToggleMicrophone();
1089
+ return this._toggleMicrophoneCore();
1090
+ }
1091
+
1092
+ _toggleMicrophoneCore() {
1093
  if (!this.SpeechRecognition) {
1094
  console.warn("🎤 Speech recognition not available");
1095
  return false;
1096
  }
 
 
 
 
 
 
 
 
 
 
 
 
1097
  // If Kimi is speaking, stop speech synthesis first
1098
  if (this.isSpeaking && this.speechSynthesis.speaking) {
1099
  console.log("🎤 Interrupting speech to start listening");
kimi-locale/de.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "Romantik",
38
  "statistics": "Statistiken",
39
  "interactions": "Interaktionen",
 
40
  "conversations": "Unterhaltungen",
41
  "days_together": "Tage Zusammen",
42
  "api_configuration": "API-Konfiguration",
@@ -137,7 +138,7 @@
137
  "category_speakingPositive": "Sprechen (Positiv)",
138
  "category_neutral": "Neutral",
139
  "category_transition": "Übergang",
140
- "personality_cheat": "Persönlichkeits-Cheat",
141
  "cheat_indicator": "Eigenschaften für eine maßgeschneiderte Erfahrung anpassen",
142
  "character_age": "Alter: {age} Jahre",
143
  "character_birthplace": "Aus: {birthplace}",
@@ -200,6 +201,26 @@
200
  "memory_stats": "Gedächtnisstatistiken",
201
  "view_memories": "Anzeigen & Verwalten",
202
  "add_memory": "Manuelles Gedächtnis hinzufügen",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  "memory_management": "Gedächtnisverwaltung",
204
  "add": "Hinzufügen"
205
  }
 
37
  "romance": "Romantik",
38
  "statistics": "Statistiken",
39
  "interactions": "Interaktionen",
40
+ "tokens_usage": "Tokens (ein/aus)",
41
  "conversations": "Unterhaltungen",
42
  "days_together": "Tage Zusammen",
43
  "api_configuration": "API-Konfiguration",
 
138
  "category_speakingPositive": "Sprechen (Positiv)",
139
  "category_neutral": "Neutral",
140
  "category_transition": "Übergang",
141
+ "personality_cheat": "Cheat-Mod",
142
  "cheat_indicator": "Eigenschaften für eine maßgeschneiderte Erfahrung anpassen",
143
  "character_age": "Alter: {age} Jahre",
144
  "character_birthplace": "Aus: {birthplace}",
 
201
  "memory_stats": "Gedächtnisstatistiken",
202
  "view_memories": "Anzeigen & Verwalten",
203
  "add_memory": "Manuelles Gedächtnis hinzufügen",
204
+ "provider_label": "Anbieter",
205
+ "base_url": "Basis-URL",
206
+ "model_id": "Modell-ID",
207
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
208
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
209
+ "test_api_key": "API-Schlüssel testen",
210
+ "system_prompt_placeholder": "Fügen Sie hier Ihren System-Prompt hinzu...",
211
+ "theme_purple": "Mystic Purple (Standard)",
212
+ "theme_dark": "Dunkle Nacht",
213
+ "theme_blue": "Ozeanblau",
214
+ "theme_green": "Smaragdwald",
215
+ "theme_pink": "Leidenschaftliches Pink",
216
+ "memory_category_personal": "Persönliche Infos",
217
+ "memory_category_preferences": "Vorlieben & Abneigungen",
218
+ "memory_category_relationships": "Beziehungen",
219
+ "memory_category_activities": "Aktivitäten & Hobbys",
220
+ "memory_category_goals": "Ziele & Pläne",
221
+ "memory_category_experiences": "Erfahrungen",
222
+ "memory_category_important": "Wichtige Ereignisse",
223
+ "memory_content_placeholder": "z.B.: Ich liebe klassische Musik...",
224
  "memory_management": "Gedächtnisverwaltung",
225
  "add": "Hinzufügen"
226
  }
kimi-locale/en.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "Romance",
38
  "statistics": "Statistics",
39
  "interactions": "Interactions",
 
40
  "conversations": "Conversations",
41
  "days_together": "Days Together",
42
  "api_configuration": "API Configuration",
@@ -137,7 +138,7 @@
137
  "category_speakingPositive": "Speaking (Positive)",
138
  "category_neutral": "Neutral",
139
  "category_transition": "Transition",
140
- "personality_cheat": "Personality Cheat",
141
  "cheat_indicator": "Adjust traits for a custom experience",
142
  "character_age": "Age: {age} years",
143
  "character_birthplace": "From: {birthplace}",
@@ -198,6 +199,26 @@
198
  "memory_stats": "Memory Statistics",
199
  "view_memories": "View & Manage",
200
  "add_memory": "Add Manual Memory",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  "memory_management": "Memory Management",
202
  "add": "Add",
203
  "help_providers": "You can use multiple AI providers: OpenRouter, OpenAI, Groq, Together, DeepSeek, Custom OpenAI-compatible, or Local (Ollama). Enter the Base URL and Model ID when required, save your API key per provider, then use ‘Test API Key’.",
 
37
  "romance": "Romance",
38
  "statistics": "Statistics",
39
  "interactions": "Interactions",
40
+ "tokens_usage": "Tokens (in/out)",
41
  "conversations": "Conversations",
42
  "days_together": "Days Together",
43
  "api_configuration": "API Configuration",
 
138
  "category_speakingPositive": "Speaking (Positive)",
139
  "category_neutral": "Neutral",
140
  "category_transition": "Transition",
141
+ "personality_cheat": "Cheat-Mod",
142
  "cheat_indicator": "Adjust traits for a custom experience",
143
  "character_age": "Age: {age} years",
144
  "character_birthplace": "From: {birthplace}",
 
199
  "memory_stats": "Memory Statistics",
200
  "view_memories": "View & Manage",
201
  "add_memory": "Add Manual Memory",
202
+ "provider_label": "Provider",
203
+ "base_url": "Base URL",
204
+ "model_id": "Model ID",
205
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
206
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
207
+ "test_api_key": "Test API Key",
208
+ "system_prompt_placeholder": "Add your custom system prompt here...",
209
+ "theme_purple": "Mystic Purple (Default)",
210
+ "theme_dark": "Dark Night",
211
+ "theme_blue": "Ocean Blue",
212
+ "theme_green": "Emerald Forest",
213
+ "theme_pink": "Passionate Pink",
214
+ "memory_category_personal": "Personal Info",
215
+ "memory_category_preferences": "Preferences & Likes",
216
+ "memory_category_relationships": "Relationships",
217
+ "memory_category_activities": "Activities & Hobbies",
218
+ "memory_category_goals": "Goals & Plans",
219
+ "memory_category_experiences": "Experiences",
220
+ "memory_category_important": "Important Events",
221
+ "memory_content_placeholder": "e.g., I love classical music...",
222
  "memory_management": "Memory Management",
223
  "add": "Add",
224
  "help_providers": "You can use multiple AI providers: OpenRouter, OpenAI, Groq, Together, DeepSeek, Custom OpenAI-compatible, or Local (Ollama). Enter the Base URL and Model ID when required, save your API key per provider, then use ‘Test API Key’.",
kimi-locale/es.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "Romance",
38
  "statistics": "Estadísticas",
39
  "interactions": "Interacciones",
 
40
  "conversations": "Conversaciones",
41
  "days_together": "Días Juntos",
42
  "api_configuration": "Configuración de API",
@@ -137,7 +138,7 @@
137
  "category_speakingPositive": "Hablando (Positivo)",
138
  "category_neutral": "Neutral",
139
  "category_transition": "Transición",
140
- "personality_cheat": "Trucos de Personalidad",
141
  "cheat_indicator": "Ajusta los rasgos para una experiencia personalizada",
142
  "character_age": "Edad: {age} años",
143
  "character_birthplace": "De: {birthplace}",
@@ -200,6 +201,26 @@
200
  "memory_stats": "Estadísticas de Memoria",
201
  "view_memories": "Ver y Gestionar",
202
  "add_memory": "Agregar Memoria Manual",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  "memory_management": "Gestión de Memoria",
204
  "add": "Agregar"
205
  }
 
37
  "romance": "Romance",
38
  "statistics": "Estadísticas",
39
  "interactions": "Interacciones",
40
+ "tokens_usage": "Tokens (entrada/salida)",
41
  "conversations": "Conversaciones",
42
  "days_together": "Días Juntos",
43
  "api_configuration": "Configuración de API",
 
138
  "category_speakingPositive": "Hablando (Positivo)",
139
  "category_neutral": "Neutral",
140
  "category_transition": "Transición",
141
+ "personality_cheat": "Trucos",
142
  "cheat_indicator": "Ajusta los rasgos para una experiencia personalizada",
143
  "character_age": "Edad: {age} años",
144
  "character_birthplace": "De: {birthplace}",
 
201
  "memory_stats": "Estadísticas de Memoria",
202
  "view_memories": "Ver y Gestionar",
203
  "add_memory": "Agregar Memoria Manual",
204
+ "provider_label": "Proveedor",
205
+ "base_url": "Base URL",
206
+ "model_id": "ID del Modelo",
207
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
208
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
209
+ "test_api_key": "Probar Clave API",
210
+ "system_prompt_placeholder": "Agrega aquí tu prompt del sistema...",
211
+ "theme_purple": "Púrpura Místico (Predeterminado)",
212
+ "theme_dark": "Noche Oscura",
213
+ "theme_blue": "Azul Océano",
214
+ "theme_green": "Bosque Esmeralda",
215
+ "theme_pink": "Rosa Apasionado",
216
+ "memory_category_personal": "Información personal",
217
+ "memory_category_preferences": "Preferencias y Gustos",
218
+ "memory_category_relationships": "Relaciones",
219
+ "memory_category_activities": "Actividades y Pasatiempos",
220
+ "memory_category_goals": "Metas y Planes",
221
+ "memory_category_experiences": "Experiencias",
222
+ "memory_category_important": "Eventos importantes",
223
+ "memory_content_placeholder": "ej.: Me encanta la música clásica...",
224
  "memory_management": "Gestión de Memoria",
225
  "add": "Agregar"
226
  }
kimi-locale/fr.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "Romance",
38
  "statistics": "Statistiques",
39
  "interactions": "Interactions",
 
40
  "conversations": "Conversations",
41
  "days_together": "Jours ensemble",
42
  "api_configuration": "Configuration API",
@@ -137,7 +138,7 @@
137
  "category_speakingPositive": "Parle (Positif)",
138
  "category_neutral": "Neutre",
139
  "category_transition": "Transition",
140
- "personality_cheat": "Triche de personnalité",
141
  "cheat_indicator": "Ajustez les traits pour une expérience personnalisée",
142
  "character_age": "Âge : {age} ans",
143
  "character_birthplace": "De : {birthplace}",
@@ -198,6 +199,26 @@
198
  "memory_stats": "Statistiques de Mémoire",
199
  "view_memories": "Voir & Gérer",
200
  "add_memory": "Ajouter une Mémoire Manuelle",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  "memory_management": "Gestion de la Mémoire",
202
  "add": "Ajouter",
203
  "help_providers": "Vous pouvez utiliser plusieurs fournisseurs d’IA : OpenRouter, OpenAI, Groq, Together, DeepSeek, un service OpenAI‑compatible personnalisé, ou Local (Ollama). Renseignez la Base URL et le Model ID si nécessaire, enregistrez votre clé API par fournisseur, puis utilisez ‘Test API Key’.",
 
37
  "romance": "Romance",
38
  "statistics": "Statistiques",
39
  "interactions": "Interactions",
40
+ "tokens_usage": "Tokens (entrée/sortie)",
41
  "conversations": "Conversations",
42
  "days_together": "Jours ensemble",
43
  "api_configuration": "Configuration API",
 
138
  "category_speakingPositive": "Parle (Positif)",
139
  "category_neutral": "Neutre",
140
  "category_transition": "Transition",
141
+ "personality_cheat": "Triche",
142
  "cheat_indicator": "Ajustez les traits pour une expérience personnalisée",
143
  "character_age": "Âge : {age} ans",
144
  "character_birthplace": "De : {birthplace}",
 
199
  "memory_stats": "Statistiques de Mémoire",
200
  "view_memories": "Voir & Gérer",
201
  "add_memory": "Ajouter une Mémoire Manuelle",
202
+ "provider_label": "Fournisseur",
203
+ "base_url": "Base URL",
204
+ "model_id": "ID du Modèle",
205
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
206
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
207
+ "test_api_key": "Tester la Clé API",
208
+ "system_prompt_placeholder": "Ajoutez ici votre prompt système personnalisé...",
209
+ "theme_purple": "Mystic Purple (Défaut)",
210
+ "theme_dark": "Nuit Sombre",
211
+ "theme_blue": "Bleu Océan",
212
+ "theme_green": "Forêt Émeraude",
213
+ "theme_pink": "Rose Passion",
214
+ "memory_category_personal": "Infos personnelles",
215
+ "memory_category_preferences": "Préférences & Goûts",
216
+ "memory_category_relationships": "Relations",
217
+ "memory_category_activities": "Activités & Loisirs",
218
+ "memory_category_goals": "Objectifs & Plans",
219
+ "memory_category_experiences": "Expériences",
220
+ "memory_category_important": "Événements importants",
221
+ "memory_content_placeholder": "ex: J'adore la musique classique...",
222
  "memory_management": "Gestion de la Mémoire",
223
  "add": "Ajouter",
224
  "help_providers": "Vous pouvez utiliser plusieurs fournisseurs d’IA : OpenRouter, OpenAI, Groq, Together, DeepSeek, un service OpenAI‑compatible personnalisé, ou Local (Ollama). Renseignez la Base URL et le Model ID si nécessaire, enregistrez votre clé API par fournisseur, puis utilisez ‘Test API Key’.",
kimi-locale/i18n.js CHANGED
@@ -4,6 +4,7 @@ class KimiI18nManager {
4
  constructor() {
5
  this.translations = {};
6
  this.currentLang = "en";
 
7
  }
8
  async setLanguage(lang) {
9
  this.currentLang = lang;
@@ -31,6 +32,7 @@ class KimiI18nManager {
31
  return str;
32
  }
33
  applyTranslations() {
 
34
  document.querySelectorAll("[data-i18n]").forEach(el => {
35
  const key = el.getAttribute("data-i18n");
36
  let params = undefined;
@@ -40,8 +42,20 @@ class KimiI18nManager {
40
  params = JSON.parse(paramsAttr);
41
  } catch {}
42
  }
43
- el.textContent = this.t(key, params);
 
 
 
 
44
  });
 
 
 
 
 
 
 
 
45
  document.querySelectorAll("[data-i18n-title]").forEach(el => {
46
  const key = el.getAttribute("data-i18n-title");
47
  el.setAttribute("title", this.t(key));
 
4
  constructor() {
5
  this.translations = {};
6
  this.currentLang = "en";
7
+ this._reloadAttempted = false;
8
  }
9
  async setLanguage(lang) {
10
  this.currentLang = lang;
 
32
  return str;
33
  }
34
  applyTranslations() {
35
+ const missing = [];
36
  document.querySelectorAll("[data-i18n]").forEach(el => {
37
  const key = el.getAttribute("data-i18n");
38
  let params = undefined;
 
42
  params = JSON.parse(paramsAttr);
43
  } catch {}
44
  }
45
+ const val = this.t(key, params);
46
+ if (val === key && this.currentLang !== "en") {
47
+ missing.push(key);
48
+ }
49
+ el.textContent = val;
50
  });
51
+ // Auto-reload once if new keys were added after initial load
52
+ if (missing.length > 0 && !this._reloadAttempted) {
53
+ this._reloadAttempted = true;
54
+ this.loadTranslations(this.currentLang).then(() => {
55
+ // Re-apply with fresh file
56
+ requestAnimationFrame(() => this.applyTranslations());
57
+ });
58
+ }
59
  document.querySelectorAll("[data-i18n-title]").forEach(el => {
60
  const key = el.getAttribute("data-i18n-title");
61
  el.setAttribute("title", this.t(key));
kimi-locale/it.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "Romanticismo",
38
  "statistics": "Statistiche",
39
  "interactions": "Interazioni",
 
40
  "conversations": "Conversazioni",
41
  "days_together": "Giorni Insieme",
42
  "api_configuration": "Configurazione API",
@@ -137,7 +138,7 @@
137
  "category_speakingPositive": "Parlando (Positivo)",
138
  "category_neutral": "Neutro",
139
  "category_transition": "Transizione",
140
- "personality_cheat": "Trucchi Personalità",
141
  "cheat_indicator": "Regola i tratti per un'esperienza personalizzata",
142
  "character_age": "Età: {age} anni",
143
  "character_birthplace": "Da: {birthplace}",
@@ -200,6 +201,26 @@
200
  "memory_stats": "Statistiche della Memoria",
201
  "view_memories": "Visualizza e Gestisci",
202
  "add_memory": "Aggiungi Memoria Manuale",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  "memory_management": "Gestione della Memoria",
204
  "add": "Aggiungi"
205
  }
 
37
  "romance": "Romanticismo",
38
  "statistics": "Statistiche",
39
  "interactions": "Interazioni",
40
+ "tokens_usage": "Token (ingresso/uscita)",
41
  "conversations": "Conversazioni",
42
  "days_together": "Giorni Insieme",
43
  "api_configuration": "Configurazione API",
 
138
  "category_speakingPositive": "Parlando (Positivo)",
139
  "category_neutral": "Neutro",
140
  "category_transition": "Transizione",
141
+ "personality_cheat": "Trucchi",
142
  "cheat_indicator": "Regola i tratti per un'esperienza personalizzata",
143
  "character_age": "Età: {age} anni",
144
  "character_birthplace": "Da: {birthplace}",
 
201
  "memory_stats": "Statistiche della Memoria",
202
  "view_memories": "Visualizza e Gestisci",
203
  "add_memory": "Aggiungi Memoria Manuale",
204
+ "provider_label": "Provider",
205
+ "base_url": "Base URL",
206
+ "model_id": "ID Modello",
207
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
208
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
209
+ "test_api_key": "Testa API Key",
210
+ "system_prompt_placeholder": "Aggiungi qui il tuo system prompt personalizzato...",
211
+ "theme_purple": "Mystic Purple (Predefinito)",
212
+ "theme_dark": "Notte Scura",
213
+ "theme_blue": "Blu Oceano",
214
+ "theme_green": "Foresta Smeraldo",
215
+ "theme_pink": "Rosa Appassionato",
216
+ "memory_category_personal": "Info personali",
217
+ "memory_category_preferences": "Preferenze e Gusti",
218
+ "memory_category_relationships": "Relazioni",
219
+ "memory_category_activities": "Attività e Hobby",
220
+ "memory_category_goals": "Obiettivi e Piani",
221
+ "memory_category_experiences": "Esperienze",
222
+ "memory_category_important": "Eventi importanti",
223
+ "memory_content_placeholder": "es.: Amo la musica classica...",
224
  "memory_management": "Gestione della Memoria",
225
  "add": "Aggiungi"
226
  }
kimi-locale/ja.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "ロマンス",
38
  "statistics": "統計",
39
  "interactions": "インタラクション",
 
40
  "conversations": "会話",
41
  "days_together": "一緒にいる日数",
42
  "api_configuration": "API設定",
@@ -200,6 +201,26 @@
200
  "memory_stats": "メモリ統計",
201
  "view_memories": "表示と管理",
202
  "add_memory": "手動メモリを追加",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  "memory_management": "メモリ管理",
204
  "add": "追加"
205
  }
 
37
  "romance": "ロマンス",
38
  "statistics": "統計",
39
  "interactions": "インタラクション",
40
+ "tokens_usage": "トークン (入力/出力)",
41
  "conversations": "会話",
42
  "days_together": "一緒にいる日数",
43
  "api_configuration": "API設定",
 
201
  "memory_stats": "メモリ統計",
202
  "view_memories": "表示と管理",
203
  "add_memory": "手動メモリを追加",
204
+ "provider_label": "プロバイダー",
205
+ "base_url": "ベースURL",
206
+ "model_id": "モデルID",
207
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
208
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
209
+ "test_api_key": "APIキーをテスト",
210
+ "system_prompt_placeholder": "ここにカスタムシステムプロンプトを追加...",
211
+ "theme_purple": "ミスティックパープル (デフォルト)",
212
+ "theme_dark": "ダークナイト",
213
+ "theme_blue": "オーシャンブルー",
214
+ "theme_green": "エメラルドフォレスト",
215
+ "theme_pink": "パッショネートピンク",
216
+ "memory_category_personal": "個人情報",
217
+ "memory_category_preferences": "好みと嗜好",
218
+ "memory_category_relationships": "人間関係",
219
+ "memory_category_activities": "活動と趣味",
220
+ "memory_category_goals": "目標と計画",
221
+ "memory_category_experiences": "経験",
222
+ "memory_category_important": "重要な出来事",
223
+ "memory_content_placeholder": "例: クラシック音楽が大好き...",
224
  "memory_management": "メモリ管理",
225
  "add": "追加"
226
  }
kimi-locale/zh.json CHANGED
@@ -37,6 +37,7 @@
37
  "romance": "浪漫",
38
  "statistics": "统计",
39
  "interactions": "互动",
 
40
  "conversations": "对话",
41
  "days_together": "在一起的天数",
42
  "api_configuration": "API配置",
@@ -200,6 +201,26 @@
200
  "memory_stats": "记忆统计",
201
  "view_memories": "查看和管理",
202
  "add_memory": "添加手动记忆",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  "memory_management": "记忆管理",
204
  "add": "添加"
205
  }
 
37
  "romance": "浪漫",
38
  "statistics": "统计",
39
  "interactions": "互动",
40
+ "tokens_usage": "Tokens (入/出)",
41
  "conversations": "对话",
42
  "days_together": "在一起的天数",
43
  "api_configuration": "API配置",
 
201
  "memory_stats": "记忆统计",
202
  "view_memories": "查看和管理",
203
  "add_memory": "添加手动记忆",
204
+ "provider_label": "提供商",
205
+ "base_url": "基础 URL",
206
+ "model_id": "模型 ID",
207
+ "llm_base_url_placeholder": "https://api.openai.com/v1/chat/completions",
208
+ "llm_model_id_placeholder": "gpt-4o-mini | llama-3.1-8b-instruct | ...",
209
+ "test_api_key": "测试 API 密钥",
210
+ "system_prompt_placeholder": "在此添加自定义系统提示...",
211
+ "theme_purple": "神秘紫 (默认)",
212
+ "theme_dark": "暗夜",
213
+ "theme_blue": "海洋蓝",
214
+ "theme_green": "翡翠森林",
215
+ "theme_pink": "热情粉",
216
+ "memory_category_personal": "个人信息",
217
+ "memory_category_preferences": "喜好与偏好",
218
+ "memory_category_relationships": "人际关系",
219
+ "memory_category_activities": "活动与爱好",
220
+ "memory_category_goals": "目标与计划",
221
+ "memory_category_experiences": "经历",
222
+ "memory_category_important": "重要事件",
223
+ "memory_content_placeholder": "例如:我喜欢古典音乐...",
224
  "memory_management": "记忆管理",
225
  "add": "添加"
226
  }