VirtualKimi commited on
Commit
9aa357b
·
verified ·
1 Parent(s): 4c9913f

Upload 69 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +10 -0
  2. CHANGELOG.md +487 -0
  3. CONTRIBUTING.md +364 -0
  4. LICENSE.md +23 -0
  5. Launch-local-kimi-app.bat +9 -0
  6. dexie-js/dexie.min.js +2 -0
  7. dexie-js/dexie.min.js.map +0 -0
  8. favicon.ico +0 -0
  9. index.html +1102 -0
  10. kimi-css/kimi-memory-styles.css +691 -0
  11. kimi-css/kimi-settings.css +1632 -0
  12. kimi-css/kimi-style.css +2075 -0
  13. kimi-icons/2blanche.jpg +3 -0
  14. kimi-icons/bella.jpg +3 -0
  15. kimi-icons/favicons/apple-touch-icon-180x180.png +0 -0
  16. kimi-icons/favicons/favicon-128x128.png +0 -0
  17. kimi-icons/favicons/favicon-16x16.png +0 -0
  18. kimi-icons/favicons/favicon-192x192.png +0 -0
  19. kimi-icons/favicons/favicon-32x32.png +0 -0
  20. kimi-icons/favicons/favicon-48x48.png +0 -0
  21. kimi-icons/favicons/favicon-64x64.png +0 -0
  22. kimi-icons/favicons/favicon-96x96.png +0 -0
  23. kimi-icons/jasmine.jpg +3 -0
  24. kimi-icons/july.jpg +3 -0
  25. kimi-icons/kimi-loading.png +3 -0
  26. kimi-icons/kimi.jpg +3 -0
  27. kimi-icons/rosa.jpg +3 -0
  28. kimi-icons/stella.jpg +3 -0
  29. kimi-icons/virtual-kimi-banners.jpg +3 -0
  30. kimi-icons/virtualkimi-logo.png +3 -0
  31. kimi-icons/virtualkimi-preview1.jpg +0 -0
  32. kimi-icons/virtualkimi-preview2.jpg +0 -0
  33. kimi-js/kimi-appearance.js +148 -0
  34. kimi-js/kimi-config.js +157 -0
  35. kimi-js/kimi-constants.js +1233 -0
  36. kimi-js/kimi-data-manager.js +318 -0
  37. kimi-js/kimi-database.js +1226 -0
  38. kimi-js/kimi-debug-utils.js +133 -0
  39. kimi-js/kimi-emotion-system.js +1060 -0
  40. kimi-js/kimi-error-manager.js +219 -0
  41. kimi-js/kimi-llm-manager.js +1729 -0
  42. kimi-js/kimi-main.js +11 -0
  43. kimi-js/kimi-memory-system.js +2257 -0
  44. kimi-js/kimi-memory-ui.js +966 -0
  45. kimi-js/kimi-memory.js +156 -0
  46. kimi-js/kimi-module.js +1949 -0
  47. kimi-js/kimi-plugin-manager.js +260 -0
  48. kimi-js/kimi-script.js +1250 -0
  49. kimi-js/kimi-security.js +163 -0
  50. kimi-js/kimi-utils.js +1118 -0
.gitattributes CHANGED
@@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ kimi-icons/2blanche.jpg filter=lfs diff=lfs merge=lfs -text
37
+ kimi-icons/bella.jpg filter=lfs diff=lfs merge=lfs -text
38
+ kimi-icons/jasmine.jpg filter=lfs diff=lfs merge=lfs -text
39
+ kimi-icons/july.jpg filter=lfs diff=lfs merge=lfs -text
40
+ kimi-icons/kimi-loading.png filter=lfs diff=lfs merge=lfs -text
41
+ kimi-icons/kimi.jpg filter=lfs diff=lfs merge=lfs -text
42
+ kimi-icons/rosa.jpg filter=lfs diff=lfs merge=lfs -text
43
+ kimi-icons/stella.jpg filter=lfs diff=lfs merge=lfs -text
44
+ kimi-icons/virtual-kimi-banners.jpg filter=lfs diff=lfs merge=lfs -text
45
+ kimi-icons/virtualkimi-logo.png filter=lfs diff=lfs merge=lfs -text
CHANGELOG.md ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual Kimi App Changelog
2
+
3
+ # [1.1.7.1] - 2025-11-12 (HuggingFace and Github version)
4
+
5
+ ### Added
6
+
7
+ - **New Characters**: Introduced 2 new AI personalities:
8
+ - **2Blanche**: Stoic YoRHa android combat unit with deep emotional vulnerability hidden beneath military protocols
9
+ - **Jasmine**: Divine goddess of love, sensuality, and Kamasutra; inspires passion, intimacy, and pleasure in every encounter
10
+ - **Character-Specific Emotions**: Added ANDROID, SENSUAL, and LOVE emotion types with specialized responses
11
+ - **Enhanced Emotion System**: New contextual keywords and emotional responses for android and sensual personas
12
+ - **Complete Internationalization**: Full translations for both new characters across all 8 supported languages (English, French, Spanish, German, Italian, Portuguese, Japanese, Chinese)
13
+
14
+ ### Changed
15
+
16
+ - **Total Characters**: Expanded from 4 to 6 unique AI personalities
17
+ - **Emotion Mapping**: New emotions mapped to existing video categories for seamless integration
18
+ - **Character Variety**: Enhanced personality spectrum from cosmic/nurturing/chaotic/artistic to include technological/rebellious themes
19
+
20
+ ### Technical Details
21
+
22
+ - Added comprehensive character profiles with unique trait progressions
23
+ - 2Blanche: Ultra-difficult progression (35% affection start) with loyalty bonuses
24
+ - July: Trust-based progression (45% affection start) with rebellion bonuses
25
+ - Maintained compatibility with existing video structure (no new categories required)
26
+
27
+ # [1.1.6.1] - 2025-09-05
28
+
29
+ ### Changed
30
+
31
+ - Improved text formatting in the chat window.
32
+
33
+ ### Bug Fixes
34
+
35
+ - Fixed some issues.
36
+
37
+ # [1.1.5.1] - 2025-09-04
38
+
39
+ ### Bug Fixes
40
+
41
+ - Fixed a bug where sliders refused the value 0 (0 was treated as falsy and reset to defaults).
42
+
43
+ # [1.1.5] - 2025-09-03
44
+
45
+ ### Bug Fixes
46
+
47
+ - Fixed some issues.
48
+
49
+ ### Changed
50
+
51
+ - Separated the KimiDataManager class and moved logic into the new file `kimi-data-manager.js`.
52
+
53
+ # [1.1.4.1] - 2025-09-03
54
+
55
+ ### Bug Fixes
56
+
57
+ - Fixed an issue with language selection and speech recognition that could prevent correct voice detection and audio input. Improved handling and fallbacks to ensure consistent behavior.
58
+
59
+ ### Changed
60
+
61
+ - Separated the KimiVideoManager class and moved video management logic into the new file `kimi-videos.js`.
62
+
63
+ # [1.1.3] - 2025-09-01
64
+
65
+ ### Bug Fixes
66
+
67
+ - Fixed an issue with language selection and speech recognition / text-to-speech playback that could prevent correct voice detection and audio output across browsers. Improved normalization and fallback handling.
68
+
69
+ ### Changed
70
+
71
+ - Modified the calculations used for character personality trait processing to improve accuracy and consistency across modules.
72
+
73
+ # [1.1.2] - 2025-08-30
74
+
75
+ ### Improvements
76
+
77
+ - Improved memory and prompt generation to avoid duplicate memory sections and display accurate per-character counters.
78
+
79
+ ### Added
80
+
81
+ - A concise "7-day summary" feature that extracts high-signal conversation highlights for quick reference.
82
+
83
+ ### Notes
84
+
85
+ - Voice UI and TTS: Only Microsoft Edge and Google Chrome will display the voice selection list and support voice playback of messages; other browsers may not expose compatible voices.
86
+
87
+ ### Bug Fixes
88
+
89
+ - Fixed import/export functions for preferences and data to ensure exported files can be re-imported correctly.
90
+
91
+ - Fixed some small bugs related to memory, video playback, and preference import/export.
92
+
93
+ # [1.1.1] - 2025-08-29
94
+
95
+ ### Improvements
96
+
97
+ - Microsoft Edge and Google Chrome Only : Improved language and voice selection logic: normalization, fallback, and robust preference management across all modules.
98
+ - Enhanced voice compatibility and ensured consistent language handling.
99
+
100
+ ### Bug Fixes
101
+
102
+ - Fixed issue where videos could freeze after opening or closing the memory modal or changing memory sections.
103
+ - Added automatic reset to neutral video state after UI interactions to prevent stuck/frozen videos.
104
+
105
+ # [1.1.0] - 2025-08-28
106
+
107
+ ### Changed
108
+
109
+ - **Recommended LLMs**: Updated the list of recommended LLM models to reflect current recommendations and improvements.
110
+
111
+ - **Settings modal UI/UX**: Updated tab layout and visual behavior in the settings modal for clearer navigation and improved usability.
112
+
113
+ ### Fixed
114
+
115
+ - **Memory features UX**: Fixed multiple UI/UX issues in the memory system to ensure reliable capture, display, and management of remembered items.
116
+ - **Miscellaneous bug fixes**: Corrected various small bugs across the application.
117
+
118
+ ### Internationalization
119
+
120
+ - **Interface translations**: Added new strings and translation keys to support the updated UI elements.
121
+
122
+ # [1.0.9] - 2025-08-23
123
+
124
+ ### Major System Improvements
125
+
126
+ - **Personality trait system overhaul**: Rebalanced progression curves and multipliers for more natural character development.
127
+ - **Unified emotion system**: Centralized emotion-to-video mapping and fixed all 13 emotions to properly affect traits.
128
+ - **Intelligence trait integration**: Added intelligence to personality calculations and video selection algorithms.
129
+ - **Enhanced emotion detection**: Improved keyword detection with better priorities and reduced conflicts.
130
+ - **Video selection rebalancing**: Fixed positive/negative bias and made auto-triggers more accessible.
131
+ - **Complete codebase synchronization**: Eliminated inconsistencies and redundancies across all modules.
132
+ - **Text streaming implementation**: Added real-time text streaming in chat for better user experience.
133
+
134
+ ### Language & Voice Improvements
135
+
136
+ - **Enhanced language and voice selection**: Fixed bugs and inconsistencies in language switching and voice preferences.
137
+ - **Improved voice synchronization**: Better coordination between selected language and available voice options.
138
+
139
+ ### API Key Management Enhancements
140
+
141
+ - **Provider-specific API key storage**: Implemented separate storage for different LLM providers (OpenRouter, OpenAI, Groq, etc.).
142
+ - **Unified API key handling**: Consolidated all API key operations through a centralized utility system.
143
+ - **Enhanced settings UI**: Improved visual design and layout of API configuration section.
144
+ - **Comprehensive API audit**: Fixed inconsistencies across all chat, test, and model loading functions.
145
+
146
+ ### Bug Fixes
147
+
148
+ - Fixed trait calculation inconsistencies between modules (INTELLIGENCE and others).
149
+ - Resolved emotion detection conflicts (LISTENING, ROMANTIC/KISS categories).
150
+ - Corrected fallback values causing progression issues.
151
+ - Fixed API key loading and display issues in settings modal.
152
+
153
+ # [1.0.8] - 2025-08-19
154
+
155
+ ### Changed
156
+
157
+ - Improved fallback logic for LLM responses: now uses localized emotional responses if the LLM reply is empty or invalid.
158
+ - Made emotional response selection dynamic and robust, based on available variants.
159
+ - Enhanced error handling for missing API keys, network issues, and API errors, ensuring the user always receives a meaningful message.
160
+ - Refactored code patching to avoid accidental code removal or misplaced edits.
161
+ - Clarified and documented emotional response logic for maintainability.
162
+
163
+ ## [1.0.7] - 2025-08-19
164
+
165
+ ### Changed
166
+
167
+ - Removed the global system prompt that caused issues and implemented per-character system prompts for each character.
168
+ - Improved voice reading of messages for clearer and more natural audio playback.
169
+ - Fixed various small bugs related to characters' personality traits.
170
+ - Improved detection of words and phrases for memory recording to increase accuracy.
171
+
172
+ ## [1.0.6] - 2025-08-15
173
+
174
+ ### Added
175
+
176
+ - Added 100+ videos for various contexts.
177
+
178
+ ### Changed
179
+
180
+ - Optimized video preloading to improve speed on slow web servers.
181
+
182
+ ### Fixed
183
+
184
+ - Fixed various minor bugs.
185
+
186
+ ## [1.0.5] - 2025-08-13 - "Personality & Language Sensitivity"
187
+
188
+ ### Added
189
+
190
+ - Multilingual profanity/insult detection for negative context across 7 languages (en, fr, es, de, it, ja, zh)
191
+ - Gendered variants support in negative keywords (fr, es, it, de) to improve accuracy (e.g., sérieux/sérieuse)
192
+ - Extended personality keywords for Spanish and Italian (all traits) with gendered forms
193
+
194
+ ### Changed
195
+
196
+ - Personality sync now completes missing values using character-specific defaults (with generic fallback)
197
+ - Centralized side-effects on personality updates (UI/memory/video/voice) behind a single `personality:updated` listener
198
+ - Sliders: generic handler only updates display; persistence and effects handled by specialized listeners
199
+ - Trait updates preserve fractional progress (2 decimals) for smoother affection changes
200
+ - Stats now use character-specific default for affection (with generic fallback) when missing
201
+
202
+ ### Fixed
203
+
204
+ - Removed obsolete `personalityUpdated` listener to avoid duplicate processing
205
+ - Unified KimiMemory affection default loading (removed conflicting double assignment and legacy default 80)
206
+ - Minor cleanup and consistency improvements in utils and sync flows
207
+
208
+ ## [1.0.4] - 2025-08-09 - "Emotion & Context Logic Upgrade"
209
+
210
+ ### Added
211
+
212
+ - Major improvements to emotion, context, and personality logic:
213
+ - Enhanced emotion detection and mapping for more nuanced responses
214
+ - Contextual keyword analysis for better understanding of user intent
215
+ - Refined personality trait system with dynamic adaptation
216
+ - Video selection logic now adapts to both emotion and conversational context
217
+ - Improved handling of multi-layered context (emotion, keywords, personality, situation)
218
+
219
+ ### Changed
220
+
221
+ - Video playback and character reactions are now more tightly coupled to detected context and personality traits
222
+ - Emotion and context logic refactored for clarity and maintainability
223
+ - Keyword extraction and context matching algorithms improved for accuracy
224
+
225
+ ### Technical
226
+
227
+ - Refactored core logic in `kimi-emotion-system.js`, `kimi-logic.js`, and `kimi-memory-system.js`
228
+ - Updated video selection and playback logic in `kimi-memory.js` and `kimi-memory-ui.js`
229
+ - Improved context propagation between modules
230
+
231
+ ## [1.0.3] - 2025-08-09 - "LLM multi-provider"
232
+
233
+ ### Added
234
+
235
+ - LLM multi-provider UX enhancements:
236
+ - Dynamic API key label per provider (OpenRouter, OpenAI, Groq, Together, DeepSeek, Custom, Ollama)
237
+ - Visual "Saved" badge when a key is stored or after a successful test
238
+ - Localized tooltip explaining Saved vs connection test
239
+
240
+ ### Changed
241
+
242
+ - OpenAI-compatible flow now reads llmBaseUrl/llmModelId and the correct provider key from KimiDB
243
+ - Clears connection status message when provider/Base URL/Model ID/key changes for clearer feedback
244
+
245
+ ## [1.0.2] - 2025-08-09 - "Smoother Video"
246
+
247
+ ### Changed
248
+
249
+ - Video playback and transition stability improvements:
250
+ - Lightweight MP4 prefetch queue (neutral + likely next clips) to reduce wait times during switches
251
+ - Earlier transition on `canplay` (instead of `canplaythrough`) for faster, smoother swaps
252
+ - Context-aware throttling to prevent rapid switching under load (speaking: ~200ms, listening: ~250ms, dancing: ~600ms, neutral: ~1200ms)
253
+
254
+ ### Fixed
255
+
256
+ - Safe revert on failed `play()` during a switch to avoid frozen frames
257
+ - Aligned event listeners to `canplay` and ensured proper cleanup to prevent leaks
258
+ - Corrected prefetch cache initialization order (prevented `undefined.has` runtime error)
259
+ - Removed unsupported `<link rel="preload" as="video">` to eliminate console warnings
260
+
261
+ ### Technical
262
+
263
+ - Front-end performance tweaks: GPU-accelerated fades with `will-change: opacity` and `backface-visibility: hidden`
264
+ - Connection warm-up: added `preconnect`/`dns-prefetch` to the origin for faster first video start
265
+ - Files updated: `index.html`, `kimi-css/kimi-style.css`, `kimi-js/kimi-utils.js`
266
+
267
+ ## [1.0.1] - 2025-08-08
268
+
269
+ - Fixed an issue where the browser prompted to save the OpenRouter API key as a password. The input field is now properly configured to prevent password managers from interfering.
270
+ - Added a waiting animation that appears between the user's message submission and the LLM's response, improving user feedback during processing.
271
+ - Added a new section in the API tab: below the recommended LLM models, all available OpenRouter LLM models are now dynamically loaded and displayed for selection.
272
+
273
+ ## [1.0.0] - 2025-08-07 - "Unified"
274
+
275
+ ### Added
276
+
277
+ - **Intelligent Memory System**: Automatic extraction and categorization of memories from conversations
278
+ - **Multiple AI Characters**: 4 unique personalities (Kimi, Bella, Rosa, Stella) with distinct traits
279
+ - **Advanced Emotion Detection**: Real-time emotion analysis with cultural awareness
280
+ - **Plugin System**: Extensible architecture for themes, voices, and behaviors
281
+ - **Memory Management UI**: Complete interface for viewing, searching, and managing memories
282
+ - **Enhanced Personality System**: 6 dynamic traits that evolve based on interactions
283
+ - **Multilingual Support**: Full localization in 7 languages with auto-detection
284
+ - **Production Health Check**: Comprehensive system validation and monitoring
285
+ - **Performance Optimizations**: Batch database operations and improved loading times
286
+ - **Security Enhancements**: Input validation, sanitization, and secure API handling
287
+
288
+ ### Changed
289
+
290
+ - **Unified Architecture**: Consolidated all emotion and personality systems
291
+ - **Improved Database**: Enhanced IndexedDB implementation with batch operations
292
+ - **Better Error Handling**: Centralized error management with fallback responses
293
+ - **Enhanced UI/UX**: More responsive and accessible interface design
294
+ - **Optimized Video System**: Smoother transitions and better emotion mapping
295
+
296
+ ### Fixed
297
+
298
+ - Function export issues in module system
299
+ - Memory leaks in event listeners
300
+ - Cross-browser compatibility issues
301
+ - Voice recognition stability problems
302
+ - Database initialization race conditions
303
+
304
+ ### Technical
305
+
306
+ - Migrated to unified emotion system
307
+ - Implemented comprehensive validation layer
308
+ - Added automated health monitoring
309
+ - Enhanced plugin security validation
310
+ - Improved mobile responsiveness
311
+
312
+ ## [0.0.9] - 2025-08-04 - "Enhanced"
313
+
314
+ ### Added
315
+
316
+ - Advanced LLM model selection interface
317
+ - Improved voice synthesis with better emotion mapping
318
+ - Enhanced personality trait visualization
319
+ - Better conversation export/import functionality
320
+
321
+ ### Changed
322
+
323
+ - Upgraded database schema for better performance
324
+ - Improved theme system with more customization options
325
+ - Enhanced mobile interface responsiveness
326
+
327
+ ### Fixed
328
+
329
+ - Various browser compatibility issues
330
+ - Voice recognition accuracy improvements
331
+ - Memory management optimizations
332
+
333
+ ## [0.0.8] - 2025-08-01 - "Evolution"
334
+
335
+ ### Added
336
+
337
+ - Dynamic personality trait evolution
338
+ - Enhanced emotion detection algorithms
339
+ - Improved conversation context awareness
340
+ - Better visual feedback systems
341
+
342
+ ### Changed
343
+
344
+ - Redesigned settings interface
345
+ - Improved conversation flow management
346
+ - Enhanced error reporting system
347
+
348
+ ### Fixed
349
+
350
+ - Database sync issues
351
+ - Voice recognition edge cases
352
+ - Theme switching problems
353
+
354
+ ## [0.0.7] - 2025-07-29 - "Immersion"
355
+
356
+ ### Added
357
+
358
+ - Real-time video emotion responses
359
+ - Enhanced voice interaction capabilities
360
+ - Improved conversation context retention
361
+ - Better visual theme system
362
+
363
+ ### Changed
364
+
365
+ - Upgraded UI framework for better performance
366
+ - Improved data synchronization mechanisms
367
+ - Enhanced accessibility features
368
+
369
+ ### Fixed
370
+
371
+ - Various stability improvements
372
+ - Better error handling
373
+ - Improved cross-platform compatibility
374
+
375
+ ## [0.0.6] - 2025-07-26 - "Connection"
376
+
377
+ ### Added
378
+
379
+ - Multi-language support system
380
+ - Enhanced conversation memory
381
+ - Improved personality customization
382
+ - Better audio/video synchronization
383
+
384
+ ### Changed
385
+
386
+ - Redesigned conversation interface
387
+ - Improved data persistence layer
388
+ - Enhanced user experience flows
389
+
390
+ ### Fixed
391
+
392
+ - Memory leak issues
393
+ - Browser compatibility problems
394
+ - Audio synchronization bugs
395
+
396
+ ## [0.0.5] - 2025-07-23 - "Rebirth"
397
+
398
+ ### Added
399
+
400
+ - Complete application rewrite
401
+ - Modern ES6+ JavaScript architecture
402
+ - Responsive design system
403
+ - Advanced AI integration capabilities
404
+ - Comprehensive settings system
405
+
406
+ ### Changed
407
+
408
+ - Modernized codebase with current web standards
409
+ - Improved performance and reliability
410
+ - Enhanced user interface design
411
+ - Better data management system
412
+
413
+ ### Removed
414
+
415
+ - Legacy jQuery dependencies
416
+ - Outdated browser support
417
+
418
+ ## [0.0.4] - 2025-07-20 - "Stability"
419
+
420
+ ### Added
421
+
422
+ - Enhanced voice recognition
423
+ - Improved conversation flow
424
+ - Better error handling
425
+ - Enhanced visual feedback
426
+
427
+ ### Fixed
428
+
429
+ - Various stability issues
430
+ - Performance optimizations
431
+ - Browser compatibility improvements
432
+
433
+ ## [0.0.3] - 2025-07-18 - "Polish"
434
+
435
+ ### Added
436
+
437
+ - Improved user interface
438
+ - Better conversation management
439
+ - Enhanced customization options
440
+
441
+ ### Fixed
442
+
443
+ - Various bugs and stability issues
444
+ - Performance improvements
445
+
446
+ ## [0.0.2] - 2025-07-17 - "Improvements"
447
+
448
+ ### Added
449
+
450
+ - Basic conversation memory
451
+ - Improved personality system
452
+ - Enhanced visual themes
453
+
454
+ ### Fixed
455
+
456
+ - Initial bug fixes
457
+ - Performance optimizations
458
+
459
+ ## [0.0.1] - 2025-07-16 - "Genesis"
460
+
461
+ ### Added
462
+
463
+ - Initial release
464
+ - Basic AI conversation capabilities
465
+ - Voice recognition and synthesis
466
+ - Simple personality system
467
+ - Theme customization
468
+ - Local data storage
469
+
470
+ ---
471
+
472
+ ## Legend
473
+
474
+ - **Added**: New features
475
+ - **Changed**: Changes in existing functionality
476
+ - **Deprecated**: Soon-to-be removed features
477
+ - **Removed**: Removed features
478
+ - **Fixed**: Bug fixes
479
+ - **Security**: Security improvements
480
+ - **Technical**: Internal technical changes
481
+
482
+ ---
483
+
484
+ All notable changes to Virtual Kimi will be documented in this file.
485
+
486
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
487
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
CONTRIBUTING.md ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to Virtual Kimi
2
+
3
+ Thank you for your interest in contributing to Virtual Kimi! This document provides guidelines and information for contributors.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Code of Conduct](#code-of-conduct)
8
+ - [Getting Started](#getting-started)
9
+ - [Development Setup](#development-setup)
10
+ - [Contribution Guidelines](#contribution-guidelines)
11
+ - [Project Structure](#project-structure)
12
+ - [Coding Standards](#coding-standards)
13
+ - [Testing](#testing)
14
+ - [Pull Request Process](#pull-request-process)
15
+ - [Issue Reporting](#issue-reporting)
16
+
17
+ ## Code of Conduct
18
+
19
+ We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and constructive in all interactions.
20
+
21
+ ### Expected Behavior
22
+
23
+ - Use welcoming and inclusive language
24
+ - Be respectful of differing viewpoints and experiences
25
+ - Gracefully accept constructive criticism
26
+ - Focus on what is best for the community
27
+ - Show empathy towards other community members
28
+
29
+ ## Getting Started
30
+
31
+ ### Prerequisites
32
+
33
+ - Modern web browser (Chrome, Edge, Firefox recommended)
34
+ - Basic knowledge of JavaScript, HTML, and CSS
35
+ - Git for version control
36
+ - Text editor or IDE of your choice
37
+
38
+ ### First Contribution
39
+
40
+ 1. Fork the repository
41
+ 2. Clone your fork locally
42
+ 3. Create a new branch for your feature/fix
43
+ 4. Make your changes
44
+ 5. Test thoroughly
45
+ 6. Submit a pull request
46
+
47
+ ## Development Setup
48
+
49
+ ### Local Environment
50
+
51
+ ```bash
52
+ # Clone the repository
53
+ git clone https://github.com/virtualkimi/virtual-kimi.git
54
+ cd virtual-kimi
55
+
56
+ # Open in browser
57
+ # Option 1: Direct file access
58
+ open index.html
59
+
60
+ # Option 2: Local server (recommended)
61
+ python -m http.server 8000
62
+ # Navigate to http://localhost:8000
63
+ ```
64
+
65
+ ### Development Tools
66
+
67
+ - **Browser DevTools**: For debugging and testing
68
+ - **Live Server**: For hot reload during development
69
+ - **Lighthouse**: For performance auditing
70
+ - **Accessibility tools**: For ensuring inclusive design
71
+
72
+ ## Contribution Guidelines
73
+
74
+ ### Types of Contributions
75
+
76
+ - **Bug fixes**: Resolve existing issues
77
+ - **Feature additions**: New functionality
78
+ - **Performance improvements**: Optimization and efficiency
79
+ - **Documentation**: Improve guides and comments
80
+ - **Localization**: Translation and internationalization
81
+ - **Plugin development**: Extend functionality
82
+ - **Testing**: Add or improve test coverage
83
+
84
+ ### Before You Start
85
+
86
+ 1. Check existing issues and pull requests
87
+ 2. Open an issue to discuss major changes
88
+ 3. Ensure your idea aligns with the project goals
89
+ 4. Consider the impact on existing functionality
90
+
91
+ ## Project Structure
92
+
93
+ ### Core Files
94
+
95
+ ```
96
+ ├── index.html # Main application
97
+ ├── kimi-script.js # Primary initialization
98
+ ├── kimi-database.js # Data persistence
99
+ ├── kimi-llm-manager.js # AI integration
100
+ ├── kimi-emotion-system.js # Emotion analysis
101
+ ├── kimi-memory-system.js # Memory management
102
+ ├── kimi-voices.js # Speech synthesis
103
+ ├── kimi-appearance.js # Theme management
104
+ └── kimi-utils.js # Utility functions
105
+ ```
106
+
107
+ ### Module Dependencies
108
+
109
+ - **Core System**: Database → Security → Config
110
+ - **AI System**: LLM Manager → Emotion System → Memory System
111
+ - **UI System**: Appearance → Utils → Module functions
112
+ - **Localization**: i18n → All user-facing modules
113
+
114
+ ### Adding New Features
115
+
116
+ #### New Memory Categories
117
+
118
+ ```javascript
119
+ // In kimi-memory-system.js
120
+ const newCategory = {
121
+ name: "custom_category",
122
+ icon: "fas fa-custom-icon",
123
+ keywords: ["keyword1", "keyword2"],
124
+ confidence: 0.7
125
+ };
126
+
127
+ // Add to MEMORY_CATEGORIES constant
128
+ ```
129
+
130
+ #### New Themes
131
+
132
+ ```javascript
133
+ // Create plugin in kimi-plugins/custom-theme/
134
+ // manifest.json
135
+ {
136
+ "name": "Custom Theme",
137
+ "version": "1.0.0",
138
+ "type": "theme",
139
+ "style": "theme.css",
140
+ "enabled": true
141
+ }
142
+ ```
143
+
144
+ #### New AI Models
145
+
146
+ ```javascript
147
+ // In kimi-llm-manager.js
148
+ "custom/model-id": {
149
+ name: "Custom Model",
150
+ provider: "Custom Provider",
151
+ type: "openrouter",
152
+ contextWindow: 8000,
153
+ pricing: { input: 0.1, output: 0.2 },
154
+ strengths: ["Custom", "Feature"]
155
+ }
156
+ ```
157
+
158
+ ## Coding Standards
159
+
160
+ ### JavaScript Style
161
+
162
+ - Use ES6+ features and modern syntax
163
+ - Prefer `const` and `let` over `var`
164
+ - Use meaningful variable and function names in English
165
+ - Follow camelCase for variables and functions
166
+ - Use PascalCase for classes and constructors
167
+
168
+ ### Code Organization
169
+
170
+ - Keep functions focused and single-purpose
171
+ - Use async/await for asynchronous operations
172
+ - Handle errors gracefully with try/catch blocks
173
+ - Add JSDoc comments for complex functions
174
+ - Group related functionality in modules
175
+
176
+ ### Example Code Style
177
+
178
+ ```javascript
179
+ /**
180
+ * Analyzes user input for emotional content and updates personality traits
181
+ * @param {string} text - User input text
182
+ * @param {string} emotion - Detected emotion type
183
+ * @returns {Promise<Object>} Updated personality traits
184
+ */
185
+ async function updatePersonalityFromEmotion(text, emotion) {
186
+ try {
187
+ // Validate input
188
+ if (!text || typeof text !== "string") {
189
+ throw new Error("Invalid input text");
190
+ }
191
+
192
+ // Process emotion
193
+ const traits = await this.processEmotionalContent(text, emotion);
194
+
195
+ // Update database
196
+ await this.db.setPersonalityBatch(traits);
197
+
198
+ return traits;
199
+ } catch (error) {
200
+ console.error("Error updating personality:", error);
201
+ throw error;
202
+ }
203
+ }
204
+ ```
205
+
206
+ ### CSS Guidelines
207
+
208
+ - Use CSS custom properties (variables) for theming
209
+ - Follow BEM methodology for class naming
210
+ - Ensure responsive design principles
211
+ - Maintain accessibility standards
212
+ - Use semantic HTML elements
213
+
214
+ ### HTML Standards
215
+
216
+ - Use semantic HTML5 elements
217
+ - Include proper ARIA labels for accessibility
218
+ - Ensure proper heading hierarchy
219
+ - Add meaningful alt text for images
220
+ - Validate markup regularly
221
+
222
+ ## Testing
223
+
224
+ ### Manual Testing Checklist
225
+
226
+ - [ ] Application loads without errors
227
+ - [ ] All core features function correctly
228
+ - [ ] Voice recognition works (in supported browsers)
229
+ - [ ] Memory system stores and retrieves data
230
+ - [ ] Theme switching works properly
231
+ - [ ] Responsive design on mobile devices
232
+ - [ ] Cross-browser compatibility
233
+ - [ ] Accessibility with keyboard navigation
234
+
235
+ ### Browser Testing
236
+
237
+ Test in the following browsers:
238
+
239
+ - Chrome (latest 2 versions)
240
+ - Edge (latest 2 versions)
241
+ - Firefox (latest 2 versions)
242
+ - Safari (latest version, if possible)
243
+
244
+ ### Performance Testing
245
+
246
+ - Check loading times
247
+ - Monitor memory usage
248
+ - Test with large conversation histories
249
+ - Verify smooth animations
250
+ - Ensure responsive UI interactions
251
+
252
+ ## Pull Request Process
253
+
254
+ ### Before Submitting
255
+
256
+ 1. **Test thoroughly**: Ensure your changes work as expected
257
+ 2. **Check compatibility**: Test across different browsers
258
+ 3. **Update documentation**: Modify README.md if needed
259
+ 4. **Clean up code**: Remove debugging code and comments
260
+ 5. **Commit messages**: Use clear, descriptive commit messages
261
+
262
+ ### PR Template
263
+
264
+ ```markdown
265
+ ## Description
266
+
267
+ Brief description of changes made.
268
+
269
+ ## Type of Change
270
+
271
+ - [ ] Bug fix
272
+ - [ ] New feature
273
+ - [ ] Performance improvement
274
+ - [ ] Documentation update
275
+ - [ ] Other: **\_**
276
+
277
+ ## Testing
278
+
279
+ - [ ] Tested in Chrome
280
+ - [ ] Tested in Edge
281
+ - [ ] Tested in Firefox
282
+ - [ ] Tested on mobile
283
+ - [ ] No errors in console
284
+
285
+ ## Screenshots (if applicable)
286
+
287
+ Add screenshots of UI changes.
288
+
289
+ ## Additional Notes
290
+
291
+ Any additional context or considerations.
292
+ ```
293
+
294
+ ### Review Process
295
+
296
+ 1. Maintainers review code for quality and functionality
297
+ 2. Feedback provided through PR comments
298
+ 3. Make requested changes and push updates
299
+ 4. Final approval and merge
300
+
301
+ ## Issue Reporting
302
+
303
+ ### Bug Reports
304
+
305
+ Include the following information:
306
+
307
+ - Browser and version
308
+ - Operating system
309
+ - Steps to reproduce
310
+ - Expected behavior
311
+ - Actual behavior
312
+ - Console errors (if any)
313
+ - Screenshots (if applicable)
314
+
315
+ ### Feature Requests
316
+
317
+ - Clear description of the feature
318
+ - Use case and benefits
319
+ - Possible implementation approach
320
+ - Any relevant examples or mockups
321
+
322
+ ### Issue Labels
323
+
324
+ - `bug`: Something isn't working
325
+ - `enhancement`: New feature or improvement
326
+ - `documentation`: Documentation updates
327
+ - `good first issue`: Good for newcomers
328
+ - `help wanted`: Community assistance needed
329
+ - `plugin`: Related to plugin system
330
+ - `accessibility`: Accessibility improvements
331
+
332
+ ## Development Tips
333
+
334
+ ### Performance Optimization
335
+
336
+ - Minimize DOM manipulations
337
+ - Use event delegation for dynamic content
338
+ - Implement proper cleanup for event listeners
339
+ - Optimize database queries with batch operations
340
+
341
+ ### Accessibility
342
+
343
+ - Test with keyboard navigation
344
+ - Verify screen reader compatibility
345
+ - Ensure sufficient color contrast
346
+ - Add appropriate ARIA labels
347
+
348
+ ## Community
349
+
350
+ ### Getting Help
351
+
352
+ - Open an issue for technical questions
353
+ - Check existing documentation first
354
+ - Be specific about your problem or question
355
+
356
+ ### Communication
357
+
358
+ - Be respectful and professional
359
+ - Provide context and details
360
+ - Be patient with response times
361
+ - Help others when possible
362
+
363
+ Thank you for contributing to Virtual Kimi! Your efforts help create a better AI companion experience for everyone.
364
+
LICENSE.md ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual Kimi Custom License
2
+
3
+ Copyright (c) 2025 Virtual Kimi Project
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to use,
7
+ copy, modify, and distribute the Software for personal, educational, or research purposes,
8
+ subject to the following conditions:
9
+
10
+ - **Commercial use, resale, or monetization of this application or any derivative work is strictly prohibited without the explicit written consent of the author.**
11
+ - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
12
+ - You may not use the name, logo, or branding of Virtual Kimi for commercial purposes without explicit permission.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+
22
+ For commercial licensing inquiries, please contact: [ijohn@virtualkimi.com](ijohn@virtualkimi.com)
23
+ [WebSite: https:/virtual-kimi.com](https:/virtual-kimi.com)
Launch-local-kimi-app.bat ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ REM Starts a Python HTTP server on port 8080
3
+ start "" python -m http.server 8080
4
+
5
+ REM Pause 2 seconds to allow the server to start
6
+ timeout /t 2 >nul
7
+
8
+ REM Opens the homepage in the default browser
9
+ start "" http://localhost:8080/index.html
dexie-js/dexie.min.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ (function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Dexie=t()})(this,function(){"use strict";var s=function(e,t){return(s=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var _=function(){return(_=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var i in t=arguments[n])Object.prototype.hasOwnProperty.call(t,i)&&(e[i]=t[i]);return e}).apply(this,arguments)};function i(e,t,n){if(n||2===arguments.length)for(var r,i=0,o=t.length;i<o;i++)!r&&i in t||((r=r||Array.prototype.slice.call(t,0,i))[i]=t[i]);return e.concat(r||Array.prototype.slice.call(t))}var f="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:global,x=Object.keys,k=Array.isArray;function a(t,n){return"object"!=typeof n||x(n).forEach(function(e){t[e]=n[e]}),t}"undefined"==typeof Promise||f.Promise||(f.Promise=Promise);var c=Object.getPrototypeOf,n={}.hasOwnProperty;function m(e,t){return n.call(e,t)}function r(t,n){"function"==typeof n&&(n=n(c(t))),("undefined"==typeof Reflect?x:Reflect.ownKeys)(n).forEach(function(e){l(t,e,n[e])})}var u=Object.defineProperty;function l(e,t,n,r){u(e,t,a(n&&m(n,"get")&&"function"==typeof n.get?{get:n.get,set:n.set,configurable:!0}:{value:n,configurable:!0,writable:!0},r))}function o(t){return{from:function(e){return t.prototype=Object.create(e.prototype),l(t.prototype,"constructor",t),{extend:r.bind(null,t.prototype)}}}}var h=Object.getOwnPropertyDescriptor;var d=[].slice;function b(e,t,n){return d.call(e,t,n)}function p(e,t){return t(e)}function y(e){if(!e)throw new Error("Assertion Failed")}function v(e){f.setImmediate?setImmediate(e):setTimeout(e,0)}function O(e,t){if("string"==typeof t&&m(e,t))return e[t];if(!t)return e;if("string"!=typeof t){for(var n=[],r=0,i=t.length;r<i;++r){var o=O(e,t[r]);n.push(o)}return n}var a=t.indexOf(".");if(-1!==a){var u=e[t.substr(0,a)];return null==u?void 0:O(u,t.substr(a+1))}}function P(e,t,n){if(e&&void 0!==t&&!("isFrozen"in Object&&Object.isFrozen(e)))if("string"!=typeof t&&"length"in t){y("string"!=typeof n&&"length"in n);for(var r=0,i=t.length;r<i;++r)P(e,t[r],n[r])}else{var o,a,u=t.indexOf(".");-1!==u?(o=t.substr(0,u),""===(a=t.substr(u+1))?void 0===n?k(e)&&!isNaN(parseInt(o))?e.splice(o,1):delete e[o]:e[o]=n:P(u=!(u=e[o])||!m(e,o)?e[o]={}:u,a,n)):void 0===n?k(e)&&!isNaN(parseInt(t))?e.splice(t,1):delete e[t]:e[t]=n}}function g(e){var t,n={};for(t in e)m(e,t)&&(n[t]=e[t]);return n}var t=[].concat;function w(e){return t.apply([],e)}var e="BigUint64Array,BigInt64Array,Array,Boolean,String,Date,RegExp,Blob,File,FileList,FileSystemFileHandle,FileSystemDirectoryHandle,ArrayBuffer,DataView,Uint8ClampedArray,ImageBitmap,ImageData,Map,Set,CryptoKey".split(",").concat(w([8,16,32,64].map(function(t){return["Int","Uint","Float"].map(function(e){return e+t+"Array"})}))).filter(function(e){return f[e]}),K=new Set(e.map(function(e){return f[e]}));var E=null;function S(e){E=new WeakMap;e=function e(t){if(!t||"object"!=typeof t)return t;var n=E.get(t);if(n)return n;if(k(t)){n=[],E.set(t,n);for(var r=0,i=t.length;r<i;++r)n.push(e(t[r]))}else if(K.has(t.constructor))n=t;else{var o,a=c(t);for(o in n=a===Object.prototype?{}:Object.create(a),E.set(t,n),t)m(t,o)&&(n[o]=e(t[o]))}return n}(e);return E=null,e}var j={}.toString;function A(e){return j.call(e).slice(8,-1)}var C="undefined"!=typeof Symbol?Symbol.iterator:"@@iterator",T="symbol"==typeof C?function(e){var t;return null!=e&&(t=e[C])&&t.apply(e)}:function(){return null};function q(e,t){t=e.indexOf(t);return 0<=t&&e.splice(t,1),0<=t}var D={};function I(e){var t,n,r,i;if(1===arguments.length){if(k(e))return e.slice();if(this===D&&"string"==typeof e)return[e];if(i=T(e)){for(n=[];!(r=i.next()).done;)n.push(r.value);return n}if(null==e)return[e];if("number"!=typeof(t=e.length))return[e];for(n=new Array(t);t--;)n[t]=e[t];return n}for(t=arguments.length,n=new Array(t);t--;)n[t]=arguments[t];return n}var B="undefined"!=typeof Symbol?function(e){return"AsyncFunction"===e[Symbol.toStringTag]}:function(){return!1},R=["Unknown","Constraint","Data","TransactionInactive","ReadOnly","Version","NotFound","InvalidState","InvalidAccess","Abort","Timeout","QuotaExceeded","Syntax","DataClone"],F=["Modify","Bulk","OpenFailed","VersionChange","Schema","Upgrade","InvalidTable","MissingAPI","NoSuchDatabase","InvalidArgument","SubTransaction","Unsupported","Internal","DatabaseClosed","PrematureCommit","ForeignAwait"].concat(R),M={VersionChanged:"Database version changed by other database connection",DatabaseClosed:"Database has been closed",Abort:"Transaction aborted",TransactionInactive:"Transaction has already completed or failed",MissingAPI:"IndexedDB API missing. Please visit https://tinyurl.com/y2uuvskb"};function N(e,t){this.name=e,this.message=t}function L(e,t){return e+". Errors: "+Object.keys(t).map(function(e){return t[e].toString()}).filter(function(e,t,n){return n.indexOf(e)===t}).join("\n")}function U(e,t,n,r){this.failures=t,this.failedKeys=r,this.successCount=n,this.message=L(e,t)}function V(e,t){this.name="BulkError",this.failures=Object.keys(t).map(function(e){return t[e]}),this.failuresByPos=t,this.message=L(e,this.failures)}o(N).from(Error).extend({toString:function(){return this.name+": "+this.message}}),o(U).from(N),o(V).from(N);var z=F.reduce(function(e,t){return e[t]=t+"Error",e},{}),W=N,Y=F.reduce(function(e,n){var r=n+"Error";function t(e,t){this.name=r,e?"string"==typeof e?(this.message="".concat(e).concat(t?"\n "+t:""),this.inner=t||null):"object"==typeof e&&(this.message="".concat(e.name," ").concat(e.message),this.inner=e):(this.message=M[n]||r,this.inner=null)}return o(t).from(W),e[n]=t,e},{});Y.Syntax=SyntaxError,Y.Type=TypeError,Y.Range=RangeError;var $=R.reduce(function(e,t){return e[t+"Error"]=Y[t],e},{});var Q=F.reduce(function(e,t){return-1===["Syntax","Type","Range"].indexOf(t)&&(e[t+"Error"]=Y[t]),e},{});function G(){}function X(e){return e}function H(t,n){return null==t||t===X?n:function(e){return n(t(e))}}function J(e,t){return function(){e.apply(this,arguments),t.apply(this,arguments)}}function Z(i,o){return i===G?o:function(){var e=i.apply(this,arguments);void 0!==e&&(arguments[0]=e);var t=this.onsuccess,n=this.onerror;this.onsuccess=null,this.onerror=null;var r=o.apply(this,arguments);return t&&(this.onsuccess=this.onsuccess?J(t,this.onsuccess):t),n&&(this.onerror=this.onerror?J(n,this.onerror):n),void 0!==r?r:e}}function ee(n,r){return n===G?r:function(){n.apply(this,arguments);var e=this.onsuccess,t=this.onerror;this.onsuccess=this.onerror=null,r.apply(this,arguments),e&&(this.onsuccess=this.onsuccess?J(e,this.onsuccess):e),t&&(this.onerror=this.onerror?J(t,this.onerror):t)}}function te(i,o){return i===G?o:function(e){var t=i.apply(this,arguments);a(e,t);var n=this.onsuccess,r=this.onerror;this.onsuccess=null,this.onerror=null;e=o.apply(this,arguments);return n&&(this.onsuccess=this.onsuccess?J(n,this.onsuccess):n),r&&(this.onerror=this.onerror?J(r,this.onerror):r),void 0===t?void 0===e?void 0:e:a(t,e)}}function ne(e,t){return e===G?t:function(){return!1!==t.apply(this,arguments)&&e.apply(this,arguments)}}function re(i,o){return i===G?o:function(){var e=i.apply(this,arguments);if(e&&"function"==typeof e.then){for(var t=this,n=arguments.length,r=new Array(n);n--;)r[n]=arguments[n];return e.then(function(){return o.apply(t,r)})}return o.apply(this,arguments)}}Q.ModifyError=U,Q.DexieError=N,Q.BulkError=V;var ie="undefined"!=typeof location&&/^(http|https):\/\/(localhost|127\.0\.0\.1)/.test(location.href);function oe(e){ie=e}var ae={},ue=100,e="undefined"==typeof Promise?[]:function(){var e=Promise.resolve();if("undefined"==typeof crypto||!crypto.subtle)return[e,c(e),e];var t=crypto.subtle.digest("SHA-512",new Uint8Array([0]));return[t,c(t),e]}(),R=e[0],F=e[1],e=e[2],F=F&&F.then,se=R&&R.constructor,ce=!!e;var le=function(e,t){be.push([e,t]),he&&(queueMicrotask(Se),he=!1)},fe=!0,he=!0,de=[],pe=[],ye=X,ve={id:"global",global:!0,ref:0,unhandleds:[],onunhandled:G,pgp:!1,env:{},finalize:G},me=ve,be=[],ge=0,we=[];function _e(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");this._listeners=[],this._lib=!1;var t=this._PSD=me;if("function"!=typeof e){if(e!==ae)throw new TypeError("Not a function");return this._state=arguments[1],this._value=arguments[2],void(!1===this._state&&Oe(this,this._value))}this._state=null,this._value=null,++t.ref,function t(r,e){try{e(function(n){if(null===r._state){if(n===r)throw new TypeError("A promise cannot be resolved with itself.");var e=r._lib&&je();n&&"function"==typeof n.then?t(r,function(e,t){n instanceof _e?n._then(e,t):n.then(e,t)}):(r._state=!0,r._value=n,Pe(r)),e&&Ae()}},Oe.bind(null,r))}catch(e){Oe(r,e)}}(this,e)}var xe={get:function(){var u=me,t=Fe;function e(n,r){var i=this,o=!u.global&&(u!==me||t!==Fe),a=o&&!Ue(),e=new _e(function(e,t){Ke(i,new ke(Qe(n,u,o,a),Qe(r,u,o,a),e,t,u))});return this._consoleTask&&(e._consoleTask=this._consoleTask),e}return e.prototype=ae,e},set:function(e){l(this,"then",e&&e.prototype===ae?xe:{get:function(){return e},set:xe.set})}};function ke(e,t,n,r,i){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof t?t:null,this.resolve=n,this.reject=r,this.psd=i}function Oe(e,t){var n,r;pe.push(t),null===e._state&&(n=e._lib&&je(),t=ye(t),e._state=!1,e._value=t,r=e,de.some(function(e){return e._value===r._value})||de.push(r),Pe(e),n&&Ae())}function Pe(e){var t=e._listeners;e._listeners=[];for(var n=0,r=t.length;n<r;++n)Ke(e,t[n]);var i=e._PSD;--i.ref||i.finalize(),0===ge&&(++ge,le(function(){0==--ge&&Ce()},[]))}function Ke(e,t){if(null!==e._state){var n=e._state?t.onFulfilled:t.onRejected;if(null===n)return(e._state?t.resolve:t.reject)(e._value);++t.psd.ref,++ge,le(Ee,[n,e,t])}else e._listeners.push(t)}function Ee(e,t,n){try{var r,i=t._value;!t._state&&pe.length&&(pe=[]),r=ie&&t._consoleTask?t._consoleTask.run(function(){return e(i)}):e(i),t._state||-1!==pe.indexOf(i)||function(e){var t=de.length;for(;t;)if(de[--t]._value===e._value)return de.splice(t,1)}(t),n.resolve(r)}catch(e){n.reject(e)}finally{0==--ge&&Ce(),--n.psd.ref||n.psd.finalize()}}function Se(){$e(ve,function(){je()&&Ae()})}function je(){var e=fe;return he=fe=!1,e}function Ae(){var e,t,n;do{for(;0<be.length;)for(e=be,be=[],n=e.length,t=0;t<n;++t){var r=e[t];r[0].apply(null,r[1])}}while(0<be.length);he=fe=!0}function Ce(){var e=de;de=[],e.forEach(function(e){e._PSD.onunhandled.call(null,e._value,e)});for(var t=we.slice(0),n=t.length;n;)t[--n]()}function Te(e){return new _e(ae,!1,e)}function qe(n,r){var i=me;return function(){var e=je(),t=me;try{return We(i,!0),n.apply(this,arguments)}catch(e){r&&r(e)}finally{We(t,!1),e&&Ae()}}}r(_e.prototype,{then:xe,_then:function(e,t){Ke(this,new ke(null,null,e,t,me))},catch:function(e){if(1===arguments.length)return this.then(null,e);var t=e,n=arguments[1];return"function"==typeof t?this.then(null,function(e){return(e instanceof t?n:Te)(e)}):this.then(null,function(e){return(e&&e.name===t?n:Te)(e)})},finally:function(t){return this.then(function(e){return _e.resolve(t()).then(function(){return e})},function(e){return _e.resolve(t()).then(function(){return Te(e)})})},timeout:function(r,i){var o=this;return r<1/0?new _e(function(e,t){var n=setTimeout(function(){return t(new Y.Timeout(i))},r);o.then(e,t).finally(clearTimeout.bind(null,n))}):this}}),"undefined"!=typeof Symbol&&Symbol.toStringTag&&l(_e.prototype,Symbol.toStringTag,"Dexie.Promise"),ve.env=Ye(),r(_e,{all:function(){var o=I.apply(null,arguments).map(Ve);return new _e(function(n,r){0===o.length&&n([]);var i=o.length;o.forEach(function(e,t){return _e.resolve(e).then(function(e){o[t]=e,--i||n(o)},r)})})},resolve:function(n){return n instanceof _e?n:n&&"function"==typeof n.then?new _e(function(e,t){n.then(e,t)}):new _e(ae,!0,n)},reject:Te,race:function(){var e=I.apply(null,arguments).map(Ve);return new _e(function(t,n){e.map(function(e){return _e.resolve(e).then(t,n)})})},PSD:{get:function(){return me},set:function(e){return me=e}},totalEchoes:{get:function(){return Fe}},newPSD:Ne,usePSD:$e,scheduler:{get:function(){return le},set:function(e){le=e}},rejectionMapper:{get:function(){return ye},set:function(e){ye=e}},follow:function(i,n){return new _e(function(e,t){return Ne(function(n,r){var e=me;e.unhandleds=[],e.onunhandled=r,e.finalize=J(function(){var t,e=this;t=function(){0===e.unhandleds.length?n():r(e.unhandleds[0])},we.push(function e(){t(),we.splice(we.indexOf(e),1)}),++ge,le(function(){0==--ge&&Ce()},[])},e.finalize),i()},n,e,t)})}}),se&&(se.allSettled&&l(_e,"allSettled",function(){var e=I.apply(null,arguments).map(Ve);return new _e(function(n){0===e.length&&n([]);var r=e.length,i=new Array(r);e.forEach(function(e,t){return _e.resolve(e).then(function(e){return i[t]={status:"fulfilled",value:e}},function(e){return i[t]={status:"rejected",reason:e}}).then(function(){return--r||n(i)})})})}),se.any&&"undefined"!=typeof AggregateError&&l(_e,"any",function(){var e=I.apply(null,arguments).map(Ve);return new _e(function(n,r){0===e.length&&r(new AggregateError([]));var i=e.length,o=new Array(i);e.forEach(function(e,t){return _e.resolve(e).then(function(e){return n(e)},function(e){o[t]=e,--i||r(new AggregateError(o))})})})}),se.withResolvers&&(_e.withResolvers=se.withResolvers));var De={awaits:0,echoes:0,id:0},Ie=0,Be=[],Re=0,Fe=0,Me=0;function Ne(e,t,n,r){var i=me,o=Object.create(i);o.parent=i,o.ref=0,o.global=!1,o.id=++Me,ve.env,o.env=ce?{Promise:_e,PromiseProp:{value:_e,configurable:!0,writable:!0},all:_e.all,race:_e.race,allSettled:_e.allSettled,any:_e.any,resolve:_e.resolve,reject:_e.reject}:{},t&&a(o,t),++i.ref,o.finalize=function(){--this.parent.ref||this.parent.finalize()};r=$e(o,e,n,r);return 0===o.ref&&o.finalize(),r}function Le(){return De.id||(De.id=++Ie),++De.awaits,De.echoes+=ue,De.id}function Ue(){return!!De.awaits&&(0==--De.awaits&&(De.id=0),De.echoes=De.awaits*ue,!0)}function Ve(e){return De.echoes&&e&&e.constructor===se?(Le(),e.then(function(e){return Ue(),e},function(e){return Ue(),Xe(e)})):e}function ze(){var e=Be[Be.length-1];Be.pop(),We(e,!1)}function We(e,t){var n,r=me;(t?!De.echoes||Re++&&e===me:!Re||--Re&&e===me)||queueMicrotask(t?function(e){++Fe,De.echoes&&0!=--De.echoes||(De.echoes=De.awaits=De.id=0),Be.push(me),We(e,!0)}.bind(null,e):ze),e!==me&&(me=e,r===ve&&(ve.env=Ye()),ce&&(n=ve.env.Promise,t=e.env,(r.global||e.global)&&(Object.defineProperty(f,"Promise",t.PromiseProp),n.all=t.all,n.race=t.race,n.resolve=t.resolve,n.reject=t.reject,t.allSettled&&(n.allSettled=t.allSettled),t.any&&(n.any=t.any))))}function Ye(){var e=f.Promise;return ce?{Promise:e,PromiseProp:Object.getOwnPropertyDescriptor(f,"Promise"),all:e.all,race:e.race,allSettled:e.allSettled,any:e.any,resolve:e.resolve,reject:e.reject}:{}}function $e(e,t,n,r,i){var o=me;try{return We(e,!0),t(n,r,i)}finally{We(o,!1)}}function Qe(t,n,r,i){return"function"!=typeof t?t:function(){var e=me;r&&Le(),We(n,!0);try{return t.apply(this,arguments)}finally{We(e,!1),i&&queueMicrotask(Ue)}}}function Ge(e){Promise===se&&0===De.echoes?0===Re?e():enqueueNativeMicroTask(e):setTimeout(e,0)}-1===(""+F).indexOf("[native code]")&&(Le=Ue=G);var Xe=_e.reject;var He=String.fromCharCode(65535),Je="Invalid key provided. Keys must be of type string, number, Date or Array<string | number | Date>.",Ze="String expected.",et=[],tt="__dbnames",nt="readonly",rt="readwrite";function it(e,t){return e?t?function(){return e.apply(this,arguments)&&t.apply(this,arguments)}:e:t}var ot={type:3,lower:-1/0,lowerOpen:!1,upper:[[]],upperOpen:!1};function at(t){return"string"!=typeof t||/\./.test(t)?function(e){return e}:function(e){return void 0===e[t]&&t in e&&delete(e=S(e))[t],e}}function ut(){throw Y.Type()}function st(e,t){try{var n=ct(e),r=ct(t);if(n!==r)return"Array"===n?1:"Array"===r?-1:"binary"===n?1:"binary"===r?-1:"string"===n?1:"string"===r?-1:"Date"===n?1:"Date"!==r?NaN:-1;switch(n){case"number":case"Date":case"string":return t<e?1:e<t?-1:0;case"binary":return function(e,t){for(var n=e.length,r=t.length,i=n<r?n:r,o=0;o<i;++o)if(e[o]!==t[o])return e[o]<t[o]?-1:1;return n===r?0:n<r?-1:1}(lt(e),lt(t));case"Array":return function(e,t){for(var n=e.length,r=t.length,i=n<r?n:r,o=0;o<i;++o){var a=st(e[o],t[o]);if(0!==a)return a}return n===r?0:n<r?-1:1}(e,t)}}catch(e){}return NaN}function ct(e){var t=typeof e;if("object"!=t)return t;if(ArrayBuffer.isView(e))return"binary";e=A(e);return"ArrayBuffer"===e?"binary":e}function lt(e){return e instanceof Uint8Array?e:ArrayBuffer.isView(e)?new Uint8Array(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e)}var ft=(ht.prototype._trans=function(e,r,t){var n=this._tx||me.trans,i=this.name,o=ie&&"undefined"!=typeof console&&console.createTask&&console.createTask("Dexie: ".concat("readonly"===e?"read":"write"," ").concat(this.name));function a(e,t,n){if(!n.schema[i])throw new Y.NotFound("Table "+i+" not part of transaction");return r(n.idbtrans,n)}var u=je();try{var s=n&&n.db._novip===this.db._novip?n===me.trans?n._promise(e,a,t):Ne(function(){return n._promise(e,a,t)},{trans:n,transless:me.transless||me}):function t(n,r,i,o){if(n.idbdb&&(n._state.openComplete||me.letThrough||n._vip)){var a=n._createTransaction(r,i,n._dbSchema);try{a.create(),n._state.PR1398_maxLoop=3}catch(e){return e.name===z.InvalidState&&n.isOpen()&&0<--n._state.PR1398_maxLoop?(console.warn("Dexie: Need to reopen db"),n.close({disableAutoOpen:!1}),n.open().then(function(){return t(n,r,i,o)})):Xe(e)}return a._promise(r,function(e,t){return Ne(function(){return me.trans=a,o(e,t,a)})}).then(function(e){if("readwrite"===r)try{a.idbtrans.commit()}catch(e){}return"readonly"===r?e:a._completion.then(function(){return e})})}if(n._state.openComplete)return Xe(new Y.DatabaseClosed(n._state.dbOpenError));if(!n._state.isBeingOpened){if(!n._state.autoOpen)return Xe(new Y.DatabaseClosed);n.open().catch(G)}return n._state.dbReadyPromise.then(function(){return t(n,r,i,o)})}(this.db,e,[this.name],a);return o&&(s._consoleTask=o,s=s.catch(function(e){return console.trace(e),Xe(e)})),s}finally{u&&Ae()}},ht.prototype.get=function(t,e){var n=this;return t&&t.constructor===Object?this.where(t).first(e):null==t?Xe(new Y.Type("Invalid argument to Table.get()")):this._trans("readonly",function(e){return n.core.get({trans:e,key:t}).then(function(e){return n.hook.reading.fire(e)})}).then(e)},ht.prototype.where=function(o){if("string"==typeof o)return new this.db.WhereClause(this,o);if(k(o))return new this.db.WhereClause(this,"[".concat(o.join("+"),"]"));var n=x(o);if(1===n.length)return this.where(n[0]).equals(o[n[0]]);var e=this.schema.indexes.concat(this.schema.primKey).filter(function(t){if(t.compound&&n.every(function(e){return 0<=t.keyPath.indexOf(e)})){for(var e=0;e<n.length;++e)if(-1===n.indexOf(t.keyPath[e]))return!1;return!0}return!1}).sort(function(e,t){return e.keyPath.length-t.keyPath.length})[0];if(e&&this.db._maxKey!==He){var t=e.keyPath.slice(0,n.length);return this.where(t).equals(t.map(function(e){return o[e]}))}!e&&ie&&console.warn("The query ".concat(JSON.stringify(o)," on ").concat(this.name," would benefit from a ")+"compound index [".concat(n.join("+"),"]"));var a=this.schema.idxByName;function u(e,t){return 0===st(e,t)}var r=n.reduce(function(e,t){var n=e[0],r=e[1],e=a[t],i=o[t];return[n||e,n||!e?it(r,e&&e.multi?function(e){e=O(e,t);return k(e)&&e.some(function(e){return u(i,e)})}:function(e){return u(i,O(e,t))}):r]},[null,null]),t=r[0],r=r[1];return t?this.where(t.name).equals(o[t.keyPath]).filter(r):e?this.filter(r):this.where(n).equals("")},ht.prototype.filter=function(e){return this.toCollection().and(e)},ht.prototype.count=function(e){return this.toCollection().count(e)},ht.prototype.offset=function(e){return this.toCollection().offset(e)},ht.prototype.limit=function(e){return this.toCollection().limit(e)},ht.prototype.each=function(e){return this.toCollection().each(e)},ht.prototype.toArray=function(e){return this.toCollection().toArray(e)},ht.prototype.toCollection=function(){return new this.db.Collection(new this.db.WhereClause(this))},ht.prototype.orderBy=function(e){return new this.db.Collection(new this.db.WhereClause(this,k(e)?"[".concat(e.join("+"),"]"):e))},ht.prototype.reverse=function(){return this.toCollection().reverse()},ht.prototype.mapToClass=function(r){var e,t=this.db,n=this.name;function i(){return null!==e&&e.apply(this,arguments)||this}(this.schema.mappedClass=r).prototype instanceof ut&&(function(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");function n(){this.constructor=e}s(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}(i,e=r),Object.defineProperty(i.prototype,"db",{get:function(){return t},enumerable:!1,configurable:!0}),i.prototype.table=function(){return n},r=i);for(var o=new Set,a=r.prototype;a;a=c(a))Object.getOwnPropertyNames(a).forEach(function(e){return o.add(e)});function u(e){if(!e)return e;var t,n=Object.create(r.prototype);for(t in e)if(!o.has(t))try{n[t]=e[t]}catch(e){}return n}return this.schema.readHook&&this.hook.reading.unsubscribe(this.schema.readHook),this.schema.readHook=u,this.hook("reading",u),r},ht.prototype.defineClass=function(){return this.mapToClass(function(e){a(this,e)})},ht.prototype.add=function(t,n){var r=this,e=this.schema.primKey,i=e.auto,o=e.keyPath,a=t;return o&&i&&(a=at(o)(t)),this._trans("readwrite",function(e){return r.core.mutate({trans:e,type:"add",keys:null!=n?[n]:null,values:[a]})}).then(function(e){return e.numFailures?_e.reject(e.failures[0]):e.lastResult}).then(function(e){if(o)try{P(t,o,e)}catch(e){}return e})},ht.prototype.update=function(e,t){if("object"!=typeof e||k(e))return this.where(":id").equals(e).modify(t);e=O(e,this.schema.primKey.keyPath);return void 0===e?Xe(new Y.InvalidArgument("Given object does not contain its primary key")):this.where(":id").equals(e).modify(t)},ht.prototype.put=function(t,n){var r=this,e=this.schema.primKey,i=e.auto,o=e.keyPath,a=t;return o&&i&&(a=at(o)(t)),this._trans("readwrite",function(e){return r.core.mutate({trans:e,type:"put",values:[a],keys:null!=n?[n]:null})}).then(function(e){return e.numFailures?_e.reject(e.failures[0]):e.lastResult}).then(function(e){if(o)try{P(t,o,e)}catch(e){}return e})},ht.prototype.delete=function(t){var n=this;return this._trans("readwrite",function(e){return n.core.mutate({trans:e,type:"delete",keys:[t]})}).then(function(e){return e.numFailures?_e.reject(e.failures[0]):void 0})},ht.prototype.clear=function(){var t=this;return this._trans("readwrite",function(e){return t.core.mutate({trans:e,type:"deleteRange",range:ot})}).then(function(e){return e.numFailures?_e.reject(e.failures[0]):void 0})},ht.prototype.bulkGet=function(t){var n=this;return this._trans("readonly",function(e){return n.core.getMany({keys:t,trans:e}).then(function(e){return e.map(function(e){return n.hook.reading.fire(e)})})})},ht.prototype.bulkAdd=function(r,e,t){var o=this,a=Array.isArray(e)?e:void 0,u=(t=t||(a?void 0:e))?t.allKeys:void 0;return this._trans("readwrite",function(e){var t=o.schema.primKey,n=t.auto,t=t.keyPath;if(t&&a)throw new Y.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys");if(a&&a.length!==r.length)throw new Y.InvalidArgument("Arguments objects and keys must have the same length");var i=r.length,t=t&&n?r.map(at(t)):r;return o.core.mutate({trans:e,type:"add",keys:a,values:t,wantResults:u}).then(function(e){var t=e.numFailures,n=e.results,r=e.lastResult,e=e.failures;if(0===t)return u?n:r;throw new V("".concat(o.name,".bulkAdd(): ").concat(t," of ").concat(i," operations failed"),e)})})},ht.prototype.bulkPut=function(r,e,t){var o=this,a=Array.isArray(e)?e:void 0,u=(t=t||(a?void 0:e))?t.allKeys:void 0;return this._trans("readwrite",function(e){var t=o.schema.primKey,n=t.auto,t=t.keyPath;if(t&&a)throw new Y.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys");if(a&&a.length!==r.length)throw new Y.InvalidArgument("Arguments objects and keys must have the same length");var i=r.length,t=t&&n?r.map(at(t)):r;return o.core.mutate({trans:e,type:"put",keys:a,values:t,wantResults:u}).then(function(e){var t=e.numFailures,n=e.results,r=e.lastResult,e=e.failures;if(0===t)return u?n:r;throw new V("".concat(o.name,".bulkPut(): ").concat(t," of ").concat(i," operations failed"),e)})})},ht.prototype.bulkUpdate=function(t){var h=this,n=this.core,r=t.map(function(e){return e.key}),i=t.map(function(e){return e.changes}),d=[];return this._trans("readwrite",function(e){return n.getMany({trans:e,keys:r,cache:"clone"}).then(function(c){var l=[],f=[];t.forEach(function(e,t){var n=e.key,r=e.changes,i=c[t];if(i){for(var o=0,a=Object.keys(r);o<a.length;o++){var u=a[o],s=r[u];if(u===h.schema.primKey.keyPath){if(0!==st(s,n))throw new Y.Constraint("Cannot update primary key in bulkUpdate()")}else P(i,u,s)}d.push(t),l.push(n),f.push(i)}});var s=l.length;return n.mutate({trans:e,type:"put",keys:l,values:f,updates:{keys:r,changeSpecs:i}}).then(function(e){var t=e.numFailures,n=e.failures;if(0===t)return s;for(var r=0,i=Object.keys(n);r<i.length;r++){var o,a=i[r],u=d[Number(a)];null!=u&&(o=n[a],delete n[a],n[u]=o)}throw new V("".concat(h.name,".bulkUpdate(): ").concat(t," of ").concat(s," operations failed"),n)})})})},ht.prototype.bulkDelete=function(t){var r=this,i=t.length;return this._trans("readwrite",function(e){return r.core.mutate({trans:e,type:"delete",keys:t})}).then(function(e){var t=e.numFailures,n=e.lastResult,e=e.failures;if(0===t)return n;throw new V("".concat(r.name,".bulkDelete(): ").concat(t," of ").concat(i," operations failed"),e)})},ht);function ht(){}function dt(i){function t(e,t){if(t){for(var n=arguments.length,r=new Array(n-1);--n;)r[n-1]=arguments[n];return a[e].subscribe.apply(null,r),i}if("string"==typeof e)return a[e]}var a={};t.addEventType=u;for(var e=1,n=arguments.length;e<n;++e)u(arguments[e]);return t;function u(e,n,r){if("object"!=typeof e){var i;n=n||ne;var o={subscribers:[],fire:r=r||G,subscribe:function(e){-1===o.subscribers.indexOf(e)&&(o.subscribers.push(e),o.fire=n(o.fire,e))},unsubscribe:function(t){o.subscribers=o.subscribers.filter(function(e){return e!==t}),o.fire=o.subscribers.reduce(n,r)}};return a[e]=t[e]=o}x(i=e).forEach(function(e){var t=i[e];if(k(t))u(e,i[e][0],i[e][1]);else{if("asap"!==t)throw new Y.InvalidArgument("Invalid event config");var n=u(e,X,function(){for(var e=arguments.length,t=new Array(e);e--;)t[e]=arguments[e];n.subscribers.forEach(function(e){v(function(){e.apply(null,t)})})})}})}}function pt(e,t){return o(t).from({prototype:e}),t}function yt(e,t){return!(e.filter||e.algorithm||e.or)&&(t?e.justLimit:!e.replayFilter)}function vt(e,t){e.filter=it(e.filter,t)}function mt(e,t,n){var r=e.replayFilter;e.replayFilter=r?function(){return it(r(),t())}:t,e.justLimit=n&&!r}function bt(e,t){if(e.isPrimKey)return t.primaryKey;var n=t.getIndexByKeyPath(e.index);if(!n)throw new Y.Schema("KeyPath "+e.index+" on object store "+t.name+" is not indexed");return n}function gt(e,t,n){var r=bt(e,t.schema);return t.openCursor({trans:n,values:!e.keysOnly,reverse:"prev"===e.dir,unique:!!e.unique,query:{index:r,range:e.range}})}function wt(e,o,t,n){var a=e.replayFilter?it(e.filter,e.replayFilter()):e.filter;if(e.or){var u={},r=function(e,t,n){var r,i;a&&!a(t,n,function(e){return t.stop(e)},function(e){return t.fail(e)})||("[object ArrayBuffer]"===(i=""+(r=t.primaryKey))&&(i=""+new Uint8Array(r)),m(u,i)||(u[i]=!0,o(e,t,n)))};return Promise.all([e.or._iterate(r,t),_t(gt(e,n,t),e.algorithm,r,!e.keysOnly&&e.valueMapper)])}return _t(gt(e,n,t),it(e.algorithm,a),o,!e.keysOnly&&e.valueMapper)}function _t(e,r,i,o){var a=qe(o?function(e,t,n){return i(o(e),t,n)}:i);return e.then(function(n){if(n)return n.start(function(){var t=function(){return n.continue()};r&&!r(n,function(e){return t=e},function(e){n.stop(e),t=G},function(e){n.fail(e),t=G})||a(n.value,n,function(e){return t=e}),t()})})}var xt=(kt.prototype.execute=function(e){var t=this["@@propmod"];if(void 0!==t.add){var n=t.add;if(k(n))return i(i([],k(e)?e:[],!0),n,!0).sort();if("number"==typeof n)return(Number(e)||0)+n;if("bigint"==typeof n)try{return BigInt(e)+n}catch(e){return BigInt(0)+n}throw new TypeError("Invalid term ".concat(n))}if(void 0!==t.remove){var r=t.remove;if(k(r))return k(e)?e.filter(function(e){return!r.includes(e)}).sort():[];if("number"==typeof r)return Number(e)-r;if("bigint"==typeof r)try{return BigInt(e)-r}catch(e){return BigInt(0)-r}throw new TypeError("Invalid subtrahend ".concat(r))}n=null===(n=t.replacePrefix)||void 0===n?void 0:n[0];return n&&"string"==typeof e&&e.startsWith(n)?t.replacePrefix[1]+e.substring(n.length):e},kt);function kt(e){this["@@propmod"]=e}var Ot=(Pt.prototype._read=function(e,t){var n=this._ctx;return n.error?n.table._trans(null,Xe.bind(null,n.error)):n.table._trans("readonly",e).then(t)},Pt.prototype._write=function(e){var t=this._ctx;return t.error?t.table._trans(null,Xe.bind(null,t.error)):t.table._trans("readwrite",e,"locked")},Pt.prototype._addAlgorithm=function(e){var t=this._ctx;t.algorithm=it(t.algorithm,e)},Pt.prototype._iterate=function(e,t){return wt(this._ctx,e,t,this._ctx.table.core)},Pt.prototype.clone=function(e){var t=Object.create(this.constructor.prototype),n=Object.create(this._ctx);return e&&a(n,e),t._ctx=n,t},Pt.prototype.raw=function(){return this._ctx.valueMapper=null,this},Pt.prototype.each=function(t){var n=this._ctx;return this._read(function(e){return wt(n,t,e,n.table.core)})},Pt.prototype.count=function(e){var i=this;return this._read(function(e){var t=i._ctx,n=t.table.core;if(yt(t,!0))return n.count({trans:e,query:{index:bt(t,n.schema),range:t.range}}).then(function(e){return Math.min(e,t.limit)});var r=0;return wt(t,function(){return++r,!1},e,n).then(function(){return r})}).then(e)},Pt.prototype.sortBy=function(e,t){var n=e.split(".").reverse(),r=n[0],i=n.length-1;function o(e,t){return t?o(e[n[t]],t-1):e[r]}var a="next"===this._ctx.dir?1:-1;function u(e,t){return st(o(e,i),o(t,i))*a}return this.toArray(function(e){return e.sort(u)}).then(t)},Pt.prototype.toArray=function(e){var o=this;return this._read(function(e){var t=o._ctx;if("next"===t.dir&&yt(t,!0)&&0<t.limit){var n=t.valueMapper,r=bt(t,t.table.core.schema);return t.table.core.query({trans:e,limit:t.limit,values:!0,query:{index:r,range:t.range}}).then(function(e){e=e.result;return n?e.map(n):e})}var i=[];return wt(t,function(e){return i.push(e)},e,t.table.core).then(function(){return i})},e)},Pt.prototype.offset=function(t){var e=this._ctx;return t<=0||(e.offset+=t,yt(e)?mt(e,function(){var n=t;return function(e,t){return 0===n||(1===n?--n:t(function(){e.advance(n),n=0}),!1)}}):mt(e,function(){var e=t;return function(){return--e<0}})),this},Pt.prototype.limit=function(e){return this._ctx.limit=Math.min(this._ctx.limit,e),mt(this._ctx,function(){var r=e;return function(e,t,n){return--r<=0&&t(n),0<=r}},!0),this},Pt.prototype.until=function(r,i){return vt(this._ctx,function(e,t,n){return!r(e.value)||(t(n),i)}),this},Pt.prototype.first=function(e){return this.limit(1).toArray(function(e){return e[0]}).then(e)},Pt.prototype.last=function(e){return this.reverse().first(e)},Pt.prototype.filter=function(t){var e;return vt(this._ctx,function(e){return t(e.value)}),(e=this._ctx).isMatch=it(e.isMatch,t),this},Pt.prototype.and=function(e){return this.filter(e)},Pt.prototype.or=function(e){return new this.db.WhereClause(this._ctx.table,e,this)},Pt.prototype.reverse=function(){return this._ctx.dir="prev"===this._ctx.dir?"next":"prev",this._ondirectionchange&&this._ondirectionchange(this._ctx.dir),this},Pt.prototype.desc=function(){return this.reverse()},Pt.prototype.eachKey=function(n){var e=this._ctx;return e.keysOnly=!e.isMatch,this.each(function(e,t){n(t.key,t)})},Pt.prototype.eachUniqueKey=function(e){return this._ctx.unique="unique",this.eachKey(e)},Pt.prototype.eachPrimaryKey=function(n){var e=this._ctx;return e.keysOnly=!e.isMatch,this.each(function(e,t){n(t.primaryKey,t)})},Pt.prototype.keys=function(e){var t=this._ctx;t.keysOnly=!t.isMatch;var n=[];return this.each(function(e,t){n.push(t.key)}).then(function(){return n}).then(e)},Pt.prototype.primaryKeys=function(e){var n=this._ctx;if("next"===n.dir&&yt(n,!0)&&0<n.limit)return this._read(function(e){var t=bt(n,n.table.core.schema);return n.table.core.query({trans:e,values:!1,limit:n.limit,query:{index:t,range:n.range}})}).then(function(e){return e.result}).then(e);n.keysOnly=!n.isMatch;var r=[];return this.each(function(e,t){r.push(t.primaryKey)}).then(function(){return r}).then(e)},Pt.prototype.uniqueKeys=function(e){return this._ctx.unique="unique",this.keys(e)},Pt.prototype.firstKey=function(e){return this.limit(1).keys(function(e){return e[0]}).then(e)},Pt.prototype.lastKey=function(e){return this.reverse().firstKey(e)},Pt.prototype.distinct=function(){var e=this._ctx,e=e.index&&e.table.schema.idxByName[e.index];if(!e||!e.multi)return this;var n={};return vt(this._ctx,function(e){var t=e.primaryKey.toString(),e=m(n,t);return n[t]=!0,!e}),this},Pt.prototype.modify=function(w){var n=this,r=this._ctx;return this._write(function(d){var a,u,p;p="function"==typeof w?w:(a=x(w),u=a.length,function(e){for(var t=!1,n=0;n<u;++n){var r=a[n],i=w[r],o=O(e,r);i instanceof xt?(P(e,r,i.execute(o)),t=!0):o!==i&&(P(e,r,i),t=!0)}return t});var y=r.table.core,e=y.schema.primaryKey,v=e.outbound,m=e.extractKey,b=200,e=n.db._options.modifyChunkSize;e&&(b="object"==typeof e?e[y.name]||e["*"]||200:e);function g(e,t){var n=t.failures,t=t.numFailures;c+=e-t;for(var r=0,i=x(n);r<i.length;r++){var o=i[r];s.push(n[o])}}var s=[],c=0,t=[];return n.clone().primaryKeys().then(function(l){function f(s){var c=Math.min(b,l.length-s);return y.getMany({trans:d,keys:l.slice(s,s+c),cache:"immutable"}).then(function(e){for(var n=[],t=[],r=v?[]:null,i=[],o=0;o<c;++o){var a=e[o],u={value:S(a),primKey:l[s+o]};!1!==p.call(u,u.value,u)&&(null==u.value?i.push(l[s+o]):v||0===st(m(a),m(u.value))?(t.push(u.value),v&&r.push(l[s+o])):(i.push(l[s+o]),n.push(u.value)))}return Promise.resolve(0<n.length&&y.mutate({trans:d,type:"add",values:n}).then(function(e){for(var t in e.failures)i.splice(parseInt(t),1);g(n.length,e)})).then(function(){return(0<t.length||h&&"object"==typeof w)&&y.mutate({trans:d,type:"put",keys:r,values:t,criteria:h,changeSpec:"function"!=typeof w&&w,isAdditionalChunk:0<s}).then(function(e){return g(t.length,e)})}).then(function(){return(0<i.length||h&&w===Kt)&&y.mutate({trans:d,type:"delete",keys:i,criteria:h,isAdditionalChunk:0<s}).then(function(e){return g(i.length,e)})}).then(function(){return l.length>s+c&&f(s+b)})})}var h=yt(r)&&r.limit===1/0&&("function"!=typeof w||w===Kt)&&{index:r.index,range:r.range};return f(0).then(function(){if(0<s.length)throw new U("Error modifying one or more objects",s,c,t);return l.length})})})},Pt.prototype.delete=function(){var i=this._ctx,n=i.range;return yt(i)&&(i.isPrimKey||3===n.type)?this._write(function(e){var t=i.table.core.schema.primaryKey,r=n;return i.table.core.count({trans:e,query:{index:t,range:r}}).then(function(n){return i.table.core.mutate({trans:e,type:"deleteRange",range:r}).then(function(e){var t=e.failures;e.lastResult,e.results;e=e.numFailures;if(e)throw new U("Could not delete some values",Object.keys(t).map(function(e){return t[e]}),n-e);return n-e})})}):this.modify(Kt)},Pt);function Pt(){}var Kt=function(e,t){return t.value=null};function Et(e,t){return e<t?-1:e===t?0:1}function St(e,t){return t<e?-1:e===t?0:1}function jt(e,t,n){e=e instanceof Dt?new e.Collection(e):e;return e._ctx.error=new(n||TypeError)(t),e}function At(e){return new e.Collection(e,function(){return qt("")}).limit(0)}function Ct(e,s,n,r){var i,c,l,f,h,d,p,y=n.length;if(!n.every(function(e){return"string"==typeof e}))return jt(e,Ze);function t(e){i="next"===e?function(e){return e.toUpperCase()}:function(e){return e.toLowerCase()},c="next"===e?function(e){return e.toLowerCase()}:function(e){return e.toUpperCase()},l="next"===e?Et:St;var t=n.map(function(e){return{lower:c(e),upper:i(e)}}).sort(function(e,t){return l(e.lower,t.lower)});f=t.map(function(e){return e.upper}),h=t.map(function(e){return e.lower}),p="next"===(d=e)?"":r}t("next");e=new e.Collection(e,function(){return Tt(f[0],h[y-1]+r)});e._ondirectionchange=function(e){t(e)};var v=0;return e._addAlgorithm(function(e,t,n){var r=e.key;if("string"!=typeof r)return!1;var i=c(r);if(s(i,h,v))return!0;for(var o=null,a=v;a<y;++a){var u=function(e,t,n,r,i,o){for(var a=Math.min(e.length,r.length),u=-1,s=0;s<a;++s){var c=t[s];if(c!==r[s])return i(e[s],n[s])<0?e.substr(0,s)+n[s]+n.substr(s+1):i(e[s],r[s])<0?e.substr(0,s)+r[s]+n.substr(s+1):0<=u?e.substr(0,u)+t[u]+n.substr(u+1):null;i(e[s],c)<0&&(u=s)}return a<r.length&&"next"===o?e+n.substr(e.length):a<e.length&&"prev"===o?e.substr(0,n.length):u<0?null:e.substr(0,u)+r[u]+n.substr(u+1)}(r,i,f[a],h[a],l,d);null===u&&null===o?v=a+1:(null===o||0<l(o,u))&&(o=u)}return t(null!==o?function(){e.continue(o+p)}:n),!1}),e}function Tt(e,t,n,r){return{type:2,lower:e,upper:t,lowerOpen:n,upperOpen:r}}function qt(e){return{type:1,lower:e,upper:e}}var Dt=(Object.defineProperty(It.prototype,"Collection",{get:function(){return this._ctx.table.db.Collection},enumerable:!1,configurable:!0}),It.prototype.between=function(e,t,n,r){n=!1!==n,r=!0===r;try{return 0<this._cmp(e,t)||0===this._cmp(e,t)&&(n||r)&&(!n||!r)?At(this):new this.Collection(this,function(){return Tt(e,t,!n,!r)})}catch(e){return jt(this,Je)}},It.prototype.equals=function(e){return null==e?jt(this,Je):new this.Collection(this,function(){return qt(e)})},It.prototype.above=function(e){return null==e?jt(this,Je):new this.Collection(this,function(){return Tt(e,void 0,!0)})},It.prototype.aboveOrEqual=function(e){return null==e?jt(this,Je):new this.Collection(this,function(){return Tt(e,void 0,!1)})},It.prototype.below=function(e){return null==e?jt(this,Je):new this.Collection(this,function(){return Tt(void 0,e,!1,!0)})},It.prototype.belowOrEqual=function(e){return null==e?jt(this,Je):new this.Collection(this,function(){return Tt(void 0,e)})},It.prototype.startsWith=function(e){return"string"!=typeof e?jt(this,Ze):this.between(e,e+He,!0,!0)},It.prototype.startsWithIgnoreCase=function(e){return""===e?this.startsWith(e):Ct(this,function(e,t){return 0===e.indexOf(t[0])},[e],He)},It.prototype.equalsIgnoreCase=function(e){return Ct(this,function(e,t){return e===t[0]},[e],"")},It.prototype.anyOfIgnoreCase=function(){var e=I.apply(D,arguments);return 0===e.length?At(this):Ct(this,function(e,t){return-1!==t.indexOf(e)},e,"")},It.prototype.startsWithAnyOfIgnoreCase=function(){var e=I.apply(D,arguments);return 0===e.length?At(this):Ct(this,function(t,e){return e.some(function(e){return 0===t.indexOf(e)})},e,He)},It.prototype.anyOf=function(){var t=this,i=I.apply(D,arguments),o=this._cmp;try{i.sort(o)}catch(e){return jt(this,Je)}if(0===i.length)return At(this);var e=new this.Collection(this,function(){return Tt(i[0],i[i.length-1])});e._ondirectionchange=function(e){o="next"===e?t._ascending:t._descending,i.sort(o)};var a=0;return e._addAlgorithm(function(e,t,n){for(var r=e.key;0<o(r,i[a]);)if(++a===i.length)return t(n),!1;return 0===o(r,i[a])||(t(function(){e.continue(i[a])}),!1)}),e},It.prototype.notEqual=function(e){return this.inAnyRange([[-1/0,e],[e,this.db._maxKey]],{includeLowers:!1,includeUppers:!1})},It.prototype.noneOf=function(){var e=I.apply(D,arguments);if(0===e.length)return new this.Collection(this);try{e.sort(this._ascending)}catch(e){return jt(this,Je)}var t=e.reduce(function(e,t){return e?e.concat([[e[e.length-1][1],t]]):[[-1/0,t]]},null);return t.push([e[e.length-1],this.db._maxKey]),this.inAnyRange(t,{includeLowers:!1,includeUppers:!1})},It.prototype.inAnyRange=function(e,t){var o=this,a=this._cmp,u=this._ascending,n=this._descending,s=this._min,c=this._max;if(0===e.length)return At(this);if(!e.every(function(e){return void 0!==e[0]&&void 0!==e[1]&&u(e[0],e[1])<=0}))return jt(this,"First argument to inAnyRange() must be an Array of two-value Arrays [lower,upper] where upper must not be lower than lower",Y.InvalidArgument);var r=!t||!1!==t.includeLowers,i=t&&!0===t.includeUppers;var l,f=u;function h(e,t){return f(e[0],t[0])}try{(l=e.reduce(function(e,t){for(var n=0,r=e.length;n<r;++n){var i=e[n];if(a(t[0],i[1])<0&&0<a(t[1],i[0])){i[0]=s(i[0],t[0]),i[1]=c(i[1],t[1]);break}}return n===r&&e.push(t),e},[])).sort(h)}catch(e){return jt(this,Je)}var d=0,p=i?function(e){return 0<u(e,l[d][1])}:function(e){return 0<=u(e,l[d][1])},y=r?function(e){return 0<n(e,l[d][0])}:function(e){return 0<=n(e,l[d][0])};var v=p,e=new this.Collection(this,function(){return Tt(l[0][0],l[l.length-1][1],!r,!i)});return e._ondirectionchange=function(e){f="next"===e?(v=p,u):(v=y,n),l.sort(h)},e._addAlgorithm(function(e,t,n){for(var r,i=e.key;v(i);)if(++d===l.length)return t(n),!1;return!p(r=i)&&!y(r)||(0===o._cmp(i,l[d][1])||0===o._cmp(i,l[d][0])||t(function(){f===u?e.continue(l[d][0]):e.continue(l[d][1])}),!1)}),e},It.prototype.startsWithAnyOf=function(){var e=I.apply(D,arguments);return e.every(function(e){return"string"==typeof e})?0===e.length?At(this):this.inAnyRange(e.map(function(e){return[e,e+He]})):jt(this,"startsWithAnyOf() only works with strings")},It);function It(){}function Bt(t){return qe(function(e){return Rt(e),t(e.target.error),!1})}function Rt(e){e.stopPropagation&&e.stopPropagation(),e.preventDefault&&e.preventDefault()}var Ft="storagemutated",Mt="x-storagemutated-1",Nt=dt(null,Ft),Lt=(Ut.prototype._lock=function(){return y(!me.global),++this._reculock,1!==this._reculock||me.global||(me.lockOwnerFor=this),this},Ut.prototype._unlock=function(){if(y(!me.global),0==--this._reculock)for(me.global||(me.lockOwnerFor=null);0<this._blockedFuncs.length&&!this._locked();){var e=this._blockedFuncs.shift();try{$e(e[1],e[0])}catch(e){}}return this},Ut.prototype._locked=function(){return this._reculock&&me.lockOwnerFor!==this},Ut.prototype.create=function(t){var n=this;if(!this.mode)return this;var e=this.db.idbdb,r=this.db._state.dbOpenError;if(y(!this.idbtrans),!t&&!e)switch(r&&r.name){case"DatabaseClosedError":throw new Y.DatabaseClosed(r);case"MissingAPIError":throw new Y.MissingAPI(r.message,r);default:throw new Y.OpenFailed(r)}if(!this.active)throw new Y.TransactionInactive;return y(null===this._completion._state),(t=this.idbtrans=t||(this.db.core||e).transaction(this.storeNames,this.mode,{durability:this.chromeTransactionDurability})).onerror=qe(function(e){Rt(e),n._reject(t.error)}),t.onabort=qe(function(e){Rt(e),n.active&&n._reject(new Y.Abort(t.error)),n.active=!1,n.on("abort").fire(e)}),t.oncomplete=qe(function(){n.active=!1,n._resolve(),"mutatedParts"in t&&Nt.storagemutated.fire(t.mutatedParts)}),this},Ut.prototype._promise=function(n,r,i){var o=this;if("readwrite"===n&&"readwrite"!==this.mode)return Xe(new Y.ReadOnly("Transaction is readonly"));if(!this.active)return Xe(new Y.TransactionInactive);if(this._locked())return new _e(function(e,t){o._blockedFuncs.push([function(){o._promise(n,r,i).then(e,t)},me])});if(i)return Ne(function(){var e=new _e(function(e,t){o._lock();var n=r(e,t,o);n&&n.then&&n.then(e,t)});return e.finally(function(){return o._unlock()}),e._lib=!0,e});var e=new _e(function(e,t){var n=r(e,t,o);n&&n.then&&n.then(e,t)});return e._lib=!0,e},Ut.prototype._root=function(){return this.parent?this.parent._root():this},Ut.prototype.waitFor=function(e){var t,r=this._root(),i=_e.resolve(e);r._waitingFor?r._waitingFor=r._waitingFor.then(function(){return i}):(r._waitingFor=i,r._waitingQueue=[],t=r.idbtrans.objectStore(r.storeNames[0]),function e(){for(++r._spinCount;r._waitingQueue.length;)r._waitingQueue.shift()();r._waitingFor&&(t.get(-1/0).onsuccess=e)}());var o=r._waitingFor;return new _e(function(t,n){i.then(function(e){return r._waitingQueue.push(qe(t.bind(null,e)))},function(e){return r._waitingQueue.push(qe(n.bind(null,e)))}).finally(function(){r._waitingFor===o&&(r._waitingFor=null)})})},Ut.prototype.abort=function(){this.active&&(this.active=!1,this.idbtrans&&this.idbtrans.abort(),this._reject(new Y.Abort))},Ut.prototype.table=function(e){var t=this._memoizedTables||(this._memoizedTables={});if(m(t,e))return t[e];var n=this.schema[e];if(!n)throw new Y.NotFound("Table "+e+" not part of transaction");n=new this.db.Table(e,n,this);return n.core=this.db.core.table(e),t[e]=n},Ut);function Ut(){}function Vt(e,t,n,r,i,o,a){return{name:e,keyPath:t,unique:n,multi:r,auto:i,compound:o,src:(n&&!a?"&":"")+(r?"*":"")+(i?"++":"")+zt(t)}}function zt(e){return"string"==typeof e?e:e?"["+[].join.call(e,"+")+"]":""}function Wt(e,t,n){return{name:e,primKey:t,indexes:n,mappedClass:null,idxByName:(r=function(e){return[e.name,e]},n.reduce(function(e,t,n){n=r(t,n);return n&&(e[n[0]]=n[1]),e},{}))};var r}var Yt=function(e){try{return e.only([[]]),Yt=function(){return[[]]},[[]]}catch(e){return Yt=function(){return He},He}};function $t(t){return null==t?function(){}:"string"==typeof t?1===(n=t).split(".").length?function(e){return e[n]}:function(e){return O(e,n)}:function(e){return O(e,t)};var n}function Qt(e){return[].slice.call(e)}var Gt=0;function Xt(e){return null==e?":id":"string"==typeof e?e:"[".concat(e.join("+"),"]")}function Ht(e,i,t){function _(e){if(3===e.type)return null;if(4===e.type)throw new Error("Cannot convert never type to IDBKeyRange");var t=e.lower,n=e.upper,r=e.lowerOpen,e=e.upperOpen;return void 0===t?void 0===n?null:i.upperBound(n,!!e):void 0===n?i.lowerBound(t,!!r):i.bound(t,n,!!r,!!e)}function n(e){var h,w=e.name;return{name:w,schema:e,mutate:function(e){var y=e.trans,v=e.type,m=e.keys,b=e.values,g=e.range;return new Promise(function(t,e){t=qe(t);var n=y.objectStore(w),r=null==n.keyPath,i="put"===v||"add"===v;if(!i&&"delete"!==v&&"deleteRange"!==v)throw new Error("Invalid operation type: "+v);var o,a=(m||b||{length:1}).length;if(m&&b&&m.length!==b.length)throw new Error("Given keys array must have same length as given values array.");if(0===a)return t({numFailures:0,failures:{},results:[],lastResult:void 0});function u(e){++l,Rt(e)}var s=[],c=[],l=0;if("deleteRange"===v){if(4===g.type)return t({numFailures:l,failures:c,results:[],lastResult:void 0});3===g.type?s.push(o=n.clear()):s.push(o=n.delete(_(g)))}else{var r=i?r?[b,m]:[b,null]:[m,null],f=r[0],h=r[1];if(i)for(var d=0;d<a;++d)s.push(o=h&&void 0!==h[d]?n[v](f[d],h[d]):n[v](f[d])),o.onerror=u;else for(d=0;d<a;++d)s.push(o=n[v](f[d])),o.onerror=u}function p(e){e=e.target.result,s.forEach(function(e,t){return null!=e.error&&(c[t]=e.error)}),t({numFailures:l,failures:c,results:"delete"===v?m:s.map(function(e){return e.result}),lastResult:e})}o.onerror=function(e){u(e),p(e)},o.onsuccess=p})},getMany:function(e){var f=e.trans,h=e.keys;return new Promise(function(t,e){t=qe(t);for(var n,r=f.objectStore(w),i=h.length,o=new Array(i),a=0,u=0,s=function(e){e=e.target;o[e._pos]=e.result,++u===a&&t(o)},c=Bt(e),l=0;l<i;++l)null!=h[l]&&((n=r.get(h[l]))._pos=l,n.onsuccess=s,n.onerror=c,++a);0===a&&t(o)})},get:function(e){var r=e.trans,i=e.key;return new Promise(function(t,e){t=qe(t);var n=r.objectStore(w).get(i);n.onsuccess=function(e){return t(e.target.result)},n.onerror=Bt(e)})},query:(h=s,function(f){return new Promise(function(n,e){n=qe(n);var r,i,o,t=f.trans,a=f.values,u=f.limit,s=f.query,c=u===1/0?void 0:u,l=s.index,s=s.range,t=t.objectStore(w),l=l.isPrimaryKey?t:t.index(l.name),s=_(s);if(0===u)return n({result:[]});h?((c=a?l.getAll(s,c):l.getAllKeys(s,c)).onsuccess=function(e){return n({result:e.target.result})},c.onerror=Bt(e)):(r=0,i=!a&&"openKeyCursor"in l?l.openKeyCursor(s):l.openCursor(s),o=[],i.onsuccess=function(e){var t=i.result;return t?(o.push(a?t.value:t.primaryKey),++r===u?n({result:o}):void t.continue()):n({result:o})},i.onerror=Bt(e))})}),openCursor:function(e){var c=e.trans,o=e.values,a=e.query,u=e.reverse,l=e.unique;return new Promise(function(t,n){t=qe(t);var e=a.index,r=a.range,i=c.objectStore(w),i=e.isPrimaryKey?i:i.index(e.name),e=u?l?"prevunique":"prev":l?"nextunique":"next",s=!o&&"openKeyCursor"in i?i.openKeyCursor(_(r),e):i.openCursor(_(r),e);s.onerror=Bt(n),s.onsuccess=qe(function(e){var r,i,o,a,u=s.result;u?(u.___id=++Gt,u.done=!1,r=u.continue.bind(u),i=(i=u.continuePrimaryKey)&&i.bind(u),o=u.advance.bind(u),a=function(){throw new Error("Cursor not stopped")},u.trans=c,u.stop=u.continue=u.continuePrimaryKey=u.advance=function(){throw new Error("Cursor not started")},u.fail=qe(n),u.next=function(){var e=this,t=1;return this.start(function(){return t--?e.continue():e.stop()}).then(function(){return e})},u.start=function(e){function t(){if(s.result)try{e()}catch(e){u.fail(e)}else u.done=!0,u.start=function(){throw new Error("Cursor behind last entry")},u.stop()}var n=new Promise(function(t,e){t=qe(t),s.onerror=Bt(e),u.fail=e,u.stop=function(e){u.stop=u.continue=u.continuePrimaryKey=u.advance=a,t(e)}});return s.onsuccess=qe(function(e){s.onsuccess=t,t()}),u.continue=r,u.continuePrimaryKey=i,u.advance=o,t(),n},t(u)):t(null)},n)})},count:function(e){var t=e.query,i=e.trans,o=t.index,a=t.range;return new Promise(function(t,e){var n=i.objectStore(w),r=o.isPrimaryKey?n:n.index(o.name),n=_(a),r=n?r.count(n):r.count();r.onsuccess=qe(function(e){return t(e.target.result)}),r.onerror=Bt(e)})}}}var r,o,a,u=(o=t,a=Qt((r=e).objectStoreNames),{schema:{name:r.name,tables:a.map(function(e){return o.objectStore(e)}).map(function(t){var e=t.keyPath,n=t.autoIncrement,r=k(e),i={},n={name:t.name,primaryKey:{name:null,isPrimaryKey:!0,outbound:null==e,compound:r,keyPath:e,autoIncrement:n,unique:!0,extractKey:$t(e)},indexes:Qt(t.indexNames).map(function(e){return t.index(e)}).map(function(e){var t=e.name,n=e.unique,r=e.multiEntry,e=e.keyPath,r={name:t,compound:k(e),keyPath:e,unique:n,multiEntry:r,extractKey:$t(e)};return i[Xt(e)]=r}),getIndexByKeyPath:function(e){return i[Xt(e)]}};return i[":id"]=n.primaryKey,null!=e&&(i[Xt(e)]=n.primaryKey),n})},hasGetAll:0<a.length&&"getAll"in o.objectStore(a[0])&&!("undefined"!=typeof navigator&&/Safari/.test(navigator.userAgent)&&!/(Chrome\/|Edge\/)/.test(navigator.userAgent)&&[].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1]<604)}),t=u.schema,s=u.hasGetAll,u=t.tables.map(n),c={};return u.forEach(function(e){return c[e.name]=e}),{stack:"dbcore",transaction:e.transaction.bind(e),table:function(e){if(!c[e])throw new Error("Table '".concat(e,"' not found"));return c[e]},MIN_KEY:-1/0,MAX_KEY:Yt(i),schema:t}}function Jt(e,t,n,r){var i=n.IDBKeyRange;return n.indexedDB,{dbcore:(r=Ht(t,i,r),e.dbcore.reduce(function(e,t){t=t.create;return _(_({},e),t(e))},r))}}function Zt(n,e){var t=e.db,e=Jt(n._middlewares,t,n._deps,e);n.core=e.dbcore,n.tables.forEach(function(e){var t=e.name;n.core.schema.tables.some(function(e){return e.name===t})&&(e.core=n.core.table(t),n[t]instanceof n.Table&&(n[t].core=e.core))})}function en(i,e,t,o){t.forEach(function(n){var r=o[n];e.forEach(function(e){var t=function e(t,n){return h(t,n)||(t=c(t))&&e(t,n)}(e,n);(!t||"value"in t&&void 0===t.value)&&(e===i.Transaction.prototype||e instanceof i.Transaction?l(e,n,{get:function(){return this.table(n)},set:function(e){u(this,n,{value:e,writable:!0,configurable:!0,enumerable:!0})}}):e[n]=new i.Table(n,r))})})}function tn(n,e){e.forEach(function(e){for(var t in e)e[t]instanceof n.Table&&delete e[t]})}function nn(e,t){return e._cfg.version-t._cfg.version}function rn(n,r,i,e){var o=n._dbSchema;i.objectStoreNames.contains("$meta")&&!o.$meta&&(o.$meta=Wt("$meta",hn("")[0],[]),n._storeNames.push("$meta"));var a=n._createTransaction("readwrite",n._storeNames,o);a.create(i),a._completion.catch(e);var u=a._reject.bind(a),s=me.transless||me;Ne(function(){return me.trans=a,me.transless=s,0!==r?(Zt(n,i),t=r,((e=a).storeNames.includes("$meta")?e.table("$meta").get("version").then(function(e){return null!=e?e:t}):_e.resolve(t)).then(function(e){return c=e,l=a,f=i,t=[],e=(s=n)._versions,h=s._dbSchema=ln(0,s.idbdb,f),0!==(e=e.filter(function(e){return e._cfg.version>=c})).length?(e.forEach(function(u){t.push(function(){var t=h,e=u._cfg.dbschema;fn(s,t,f),fn(s,e,f),h=s._dbSchema=e;var n=an(t,e);n.add.forEach(function(e){un(f,e[0],e[1].primKey,e[1].indexes)}),n.change.forEach(function(e){if(e.recreate)throw new Y.Upgrade("Not yet support for changing primary key");var t=f.objectStore(e.name);e.add.forEach(function(e){return cn(t,e)}),e.change.forEach(function(e){t.deleteIndex(e.name),cn(t,e)}),e.del.forEach(function(e){return t.deleteIndex(e)})});var r=u._cfg.contentUpgrade;if(r&&u._cfg.version>c){Zt(s,f),l._memoizedTables={};var i=g(e);n.del.forEach(function(e){i[e]=t[e]}),tn(s,[s.Transaction.prototype]),en(s,[s.Transaction.prototype],x(i),i),l.schema=i;var o,a=B(r);a&&Le();n=_e.follow(function(){var e;(o=r(l))&&a&&(e=Ue.bind(null,null),o.then(e,e))});return o&&"function"==typeof o.then?_e.resolve(o):n.then(function(){return o})}}),t.push(function(e){var t,n,r=u._cfg.dbschema;t=r,n=e,[].slice.call(n.db.objectStoreNames).forEach(function(e){return null==t[e]&&n.db.deleteObjectStore(e)}),tn(s,[s.Transaction.prototype]),en(s,[s.Transaction.prototype],s._storeNames,s._dbSchema),l.schema=s._dbSchema}),t.push(function(e){s.idbdb.objectStoreNames.contains("$meta")&&(Math.ceil(s.idbdb.version/10)===u._cfg.version?(s.idbdb.deleteObjectStore("$meta"),delete s._dbSchema.$meta,s._storeNames=s._storeNames.filter(function(e){return"$meta"!==e})):e.objectStore("$meta").put(u._cfg.version,"version"))})}),function e(){return t.length?_e.resolve(t.shift()(l.idbtrans)).then(e):_e.resolve()}().then(function(){sn(h,f)})):_e.resolve();var s,c,l,f,t,h}).catch(u)):(x(o).forEach(function(e){un(i,e,o[e].primKey,o[e].indexes)}),Zt(n,i),void _e.follow(function(){return n.on.populate.fire(a)}).catch(u));var e,t})}function on(e,r){sn(e._dbSchema,r),r.db.version%10!=0||r.objectStoreNames.contains("$meta")||r.db.createObjectStore("$meta").add(Math.ceil(r.db.version/10-1),"version");var t=ln(0,e.idbdb,r);fn(e,e._dbSchema,r);for(var n=0,i=an(t,e._dbSchema).change;n<i.length;n++){var o=function(t){if(t.change.length||t.recreate)return console.warn("Unable to patch indexes of table ".concat(t.name," because it has changes on the type of index or primary key.")),{value:void 0};var n=r.objectStore(t.name);t.add.forEach(function(e){ie&&console.debug("Dexie upgrade patch: Creating missing index ".concat(t.name,".").concat(e.src)),cn(n,e)})}(i[n]);if("object"==typeof o)return o.value}}function an(e,t){var n,r={del:[],add:[],change:[]};for(n in e)t[n]||r.del.push(n);for(n in t){var i=e[n],o=t[n];if(i){var a={name:n,def:o,recreate:!1,del:[],add:[],change:[]};if(""+(i.primKey.keyPath||"")!=""+(o.primKey.keyPath||"")||i.primKey.auto!==o.primKey.auto)a.recreate=!0,r.change.push(a);else{var u=i.idxByName,s=o.idxByName,c=void 0;for(c in u)s[c]||a.del.push(c);for(c in s){var l=u[c],f=s[c];l?l.src!==f.src&&a.change.push(f):a.add.push(f)}(0<a.del.length||0<a.add.length||0<a.change.length)&&r.change.push(a)}}else r.add.push([n,o])}return r}function un(e,t,n,r){var i=e.db.createObjectStore(t,n.keyPath?{keyPath:n.keyPath,autoIncrement:n.auto}:{autoIncrement:n.auto});return r.forEach(function(e){return cn(i,e)}),i}function sn(t,n){x(t).forEach(function(e){n.db.objectStoreNames.contains(e)||(ie&&console.debug("Dexie: Creating missing table",e),un(n,e,t[e].primKey,t[e].indexes))})}function cn(e,t){e.createIndex(t.name,t.keyPath,{unique:t.unique,multiEntry:t.multi})}function ln(e,t,u){var s={};return b(t.objectStoreNames,0).forEach(function(e){for(var t=u.objectStore(e),n=Vt(zt(a=t.keyPath),a||"",!0,!1,!!t.autoIncrement,a&&"string"!=typeof a,!0),r=[],i=0;i<t.indexNames.length;++i){var o=t.index(t.indexNames[i]),a=o.keyPath,o=Vt(o.name,a,!!o.unique,!!o.multiEntry,!1,a&&"string"!=typeof a,!1);r.push(o)}s[e]=Wt(e,n,r)}),s}function fn(e,t,n){for(var r=n.db.objectStoreNames,i=0;i<r.length;++i){var o=r[i],a=n.objectStore(o);e._hasGetAll="getAll"in a;for(var u=0;u<a.indexNames.length;++u){var s=a.indexNames[u],c=a.index(s).keyPath,l="string"==typeof c?c:"["+b(c).join("+")+"]";!t[o]||(c=t[o].idxByName[l])&&(c.name=s,delete t[o].idxByName[l],t[o].idxByName[s]=c)}}"undefined"!=typeof navigator&&/Safari/.test(navigator.userAgent)&&!/(Chrome\/|Edge\/)/.test(navigator.userAgent)&&f.WorkerGlobalScope&&f instanceof f.WorkerGlobalScope&&[].concat(navigator.userAgent.match(/Safari\/(\d*)/))[1]<604&&(e._hasGetAll=!1)}function hn(e){return e.split(",").map(function(e,t){var n=(e=e.trim()).replace(/([&*]|\+\+)/g,""),r=/^\[/.test(n)?n.match(/^\[(.*)\]$/)[1].split("+"):n;return Vt(n,r||null,/\&/.test(e),/\*/.test(e),/\+\+/.test(e),k(r),0===t)})}var dn=(pn.prototype._parseStoresSpec=function(r,i){x(r).forEach(function(e){if(null!==r[e]){var t=hn(r[e]),n=t.shift();if(n.unique=!0,n.multi)throw new Y.Schema("Primary key cannot be multi-valued");t.forEach(function(e){if(e.auto)throw new Y.Schema("Only primary key can be marked as autoIncrement (++)");if(!e.keyPath)throw new Y.Schema("Index must have a name and cannot be an empty string")}),i[e]=Wt(e,n,t)}})},pn.prototype.stores=function(e){var t=this.db;this._cfg.storesSource=this._cfg.storesSource?a(this._cfg.storesSource,e):e;var e=t._versions,n={},r={};return e.forEach(function(e){a(n,e._cfg.storesSource),r=e._cfg.dbschema={},e._parseStoresSpec(n,r)}),t._dbSchema=r,tn(t,[t._allTables,t,t.Transaction.prototype]),en(t,[t._allTables,t,t.Transaction.prototype,this._cfg.tables],x(r),r),t._storeNames=x(r),this},pn.prototype.upgrade=function(e){return this._cfg.contentUpgrade=re(this._cfg.contentUpgrade||G,e),this},pn);function pn(){}function yn(e,t){var n=e._dbNamesDB;return n||(n=e._dbNamesDB=new er(tt,{addons:[],indexedDB:e,IDBKeyRange:t})).version(1).stores({dbnames:"name"}),n.table("dbnames")}function vn(e){return e&&"function"==typeof e.databases}function mn(e){return Ne(function(){return me.letThrough=!0,e()})}function bn(e){return!("from"in e)}var gn=function(e,t){if(!this){var n=new gn;return e&&"d"in e&&a(n,e),n}a(this,arguments.length?{d:1,from:e,to:1<arguments.length?t:e}:{d:0})};function wn(e,t,n){var r=st(t,n);if(!isNaN(r)){if(0<r)throw RangeError();if(bn(e))return a(e,{from:t,to:n,d:1});var i=e.l,r=e.r;if(st(n,e.from)<0)return i?wn(i,t,n):e.l={from:t,to:n,d:1,l:null,r:null},On(e);if(0<st(t,e.to))return r?wn(r,t,n):e.r={from:t,to:n,d:1,l:null,r:null},On(e);st(t,e.from)<0&&(e.from=t,e.l=null,e.d=r?r.d+1:1),0<st(n,e.to)&&(e.to=n,e.r=null,e.d=e.l?e.l.d+1:1);n=!e.r;i&&!e.l&&_n(e,i),r&&n&&_n(e,r)}}function _n(e,t){bn(t)||function e(t,n){var r=n.from,i=n.to,o=n.l,n=n.r;wn(t,r,i),o&&e(t,o),n&&e(t,n)}(e,t)}function xn(e,t){var n=kn(t),r=n.next();if(r.done)return!1;for(var i=r.value,o=kn(e),a=o.next(i.from),u=a.value;!r.done&&!a.done;){if(st(u.from,i.to)<=0&&0<=st(u.to,i.from))return!0;st(i.from,u.from)<0?i=(r=n.next(u.from)).value:u=(a=o.next(i.from)).value}return!1}function kn(e){var n=bn(e)?null:{s:0,n:e};return{next:function(e){for(var t=0<arguments.length;n;)switch(n.s){case 0:if(n.s=1,t)for(;n.n.l&&st(e,n.n.from)<0;)n={up:n,n:n.n.l,s:1};else for(;n.n.l;)n={up:n,n:n.n.l,s:1};case 1:if(n.s=2,!t||st(e,n.n.to)<=0)return{value:n.n,done:!1};case 2:if(n.n.r){n.s=3,n={up:n,n:n.n.r,s:0};continue}case 3:n=n.up}return{done:!0}}}}function On(e){var t,n,r=((null===(t=e.r)||void 0===t?void 0:t.d)||0)-((null===(n=e.l)||void 0===n?void 0:n.d)||0),i=1<r?"r":r<-1?"l":"";i&&(t="r"==i?"l":"r",n=_({},e),r=e[i],e.from=r.from,e.to=r.to,e[i]=r[i],n[i]=r[t],(e[t]=n).d=Pn(n)),e.d=Pn(e)}function Pn(e){var t=e.r,e=e.l;return(t?e?Math.max(t.d,e.d):t.d:e?e.d:0)+1}function Kn(t,n){return x(n).forEach(function(e){t[e]?_n(t[e],n[e]):t[e]=function e(t){var n,r,i={};for(n in t)m(t,n)&&(r=t[n],i[n]=!r||"object"!=typeof r||K.has(r.constructor)?r:e(r));return i}(n[e])}),t}function En(t,n){return t.all||n.all||Object.keys(t).some(function(e){return n[e]&&xn(n[e],t[e])})}r(gn.prototype,((F={add:function(e){return _n(this,e),this},addKey:function(e){return wn(this,e,e),this},addKeys:function(e){var t=this;return e.forEach(function(e){return wn(t,e,e)}),this},hasKey:function(e){var t=kn(this).next(e).value;return t&&st(t.from,e)<=0&&0<=st(t.to,e)}})[C]=function(){return kn(this)},F));var Sn={},jn={},An=!1;function Cn(e){Kn(jn,e),An||(An=!0,setTimeout(function(){An=!1,Tn(jn,!(jn={}))},0))}function Tn(e,t){void 0===t&&(t=!1);var n=new Set;if(e.all)for(var r=0,i=Object.values(Sn);r<i.length;r++)qn(a=i[r],e,n,t);else for(var o in e){var a,u=/^idb\:\/\/(.*)\/(.*)\//.exec(o);u&&(o=u[1],u=u[2],(a=Sn["idb://".concat(o,"/").concat(u)])&&qn(a,e,n,t))}n.forEach(function(e){return e()})}function qn(e,t,n,r){for(var i=[],o=0,a=Object.entries(e.queries.query);o<a.length;o++){for(var u=a[o],s=u[0],c=[],l=0,f=u[1];l<f.length;l++){var h=f[l];En(t,h.obsSet)?h.subscribers.forEach(function(e){return n.add(e)}):r&&c.push(h)}r&&i.push([s,c])}if(r)for(var d=0,p=i;d<p.length;d++){var y=p[d],s=y[0],c=y[1];e.queries.query[s]=c}}function Dn(f){var h=f._state,r=f._deps.indexedDB;if(h.isBeingOpened||f.idbdb)return h.dbReadyPromise.then(function(){return h.dbOpenError?Xe(h.dbOpenError):f});h.isBeingOpened=!0,h.dbOpenError=null,h.openComplete=!1;var t=h.openCanceller,d=Math.round(10*f.verno),p=!1;function e(){if(h.openCanceller!==t)throw new Y.DatabaseClosed("db.open() was cancelled")}function y(){return new _e(function(s,n){if(e(),!r)throw new Y.MissingAPI;var c=f.name,l=h.autoSchema||!d?r.open(c):r.open(c,d);if(!l)throw new Y.MissingAPI;l.onerror=Bt(n),l.onblocked=qe(f._fireOnBlocked),l.onupgradeneeded=qe(function(e){var t;v=l.transaction,h.autoSchema&&!f._options.allowEmptyDB?(l.onerror=Rt,v.abort(),l.result.close(),(t=r.deleteDatabase(c)).onsuccess=t.onerror=qe(function(){n(new Y.NoSuchDatabase("Database ".concat(c," doesnt exist")))})):(v.onerror=Bt(n),e=e.oldVersion>Math.pow(2,62)?0:e.oldVersion,m=e<1,f.idbdb=l.result,p&&on(f,v),rn(f,e/10,v,n))},n),l.onsuccess=qe(function(){v=null;var e,t,n,r,i,o=f.idbdb=l.result,a=b(o.objectStoreNames);if(0<a.length)try{var u=o.transaction(1===(r=a).length?r[0]:r,"readonly");if(h.autoSchema)t=o,n=u,(e=f).verno=t.version/10,n=e._dbSchema=ln(0,t,n),e._storeNames=b(t.objectStoreNames,0),en(e,[e._allTables],x(n),n);else if(fn(f,f._dbSchema,u),((i=an(ln(0,(i=f).idbdb,u),i._dbSchema)).add.length||i.change.some(function(e){return e.add.length||e.change.length}))&&!p)return console.warn("Dexie SchemaDiff: Schema was extended without increasing the number passed to db.version(). Dexie will add missing parts and increment native version number to workaround this."),o.close(),d=o.version+1,p=!0,s(y());Zt(f,u)}catch(e){}et.push(f),o.onversionchange=qe(function(e){h.vcFired=!0,f.on("versionchange").fire(e)}),o.onclose=qe(function(e){f.on("close").fire(e)}),m&&(i=f._deps,u=c,o=i.indexedDB,i=i.IDBKeyRange,vn(o)||u===tt||yn(o,i).put({name:u}).catch(G)),s()},n)}).catch(function(e){switch(null==e?void 0:e.name){case"UnknownError":if(0<h.PR1398_maxLoop)return h.PR1398_maxLoop--,console.warn("Dexie: Workaround for Chrome UnknownError on open()"),y();break;case"VersionError":if(0<d)return d=0,y()}return _e.reject(e)})}var n,i=h.dbReadyResolve,v=null,m=!1;return _e.race([t,("undefined"==typeof navigator?_e.resolve():!navigator.userAgentData&&/Safari\//.test(navigator.userAgent)&&!/Chrom(e|ium)\//.test(navigator.userAgent)&&indexedDB.databases?new Promise(function(e){function t(){return indexedDB.databases().finally(e)}n=setInterval(t,100),t()}).finally(function(){return clearInterval(n)}):Promise.resolve()).then(y)]).then(function(){return e(),h.onReadyBeingFired=[],_e.resolve(mn(function(){return f.on.ready.fire(f.vip)})).then(function e(){if(0<h.onReadyBeingFired.length){var t=h.onReadyBeingFired.reduce(re,G);return h.onReadyBeingFired=[],_e.resolve(mn(function(){return t(f.vip)})).then(e)}})}).finally(function(){h.openCanceller===t&&(h.onReadyBeingFired=null,h.isBeingOpened=!1)}).catch(function(e){h.dbOpenError=e;try{v&&v.abort()}catch(e){}return t===h.openCanceller&&f._close(),Xe(e)}).finally(function(){h.openComplete=!0,i()}).then(function(){var n;return m&&(n={},f.tables.forEach(function(t){t.schema.indexes.forEach(function(e){e.name&&(n["idb://".concat(f.name,"/").concat(t.name,"/").concat(e.name)]=new gn(-1/0,[[[]]]))}),n["idb://".concat(f.name,"/").concat(t.name,"/")]=n["idb://".concat(f.name,"/").concat(t.name,"/:dels")]=new gn(-1/0,[[[]]])}),Nt(Ft).fire(n),Tn(n,!0)),f})}function In(t){function e(e){return t.next(e)}var r=n(e),i=n(function(e){return t.throw(e)});function n(n){return function(e){var t=n(e),e=t.value;return t.done?e:e&&"function"==typeof e.then?e.then(r,i):k(e)?Promise.all(e).then(r,i):r(e)}}return n(e)()}function Bn(e,t,n){for(var r=k(e)?e.slice():[e],i=0;i<n;++i)r.push(t);return r}var Rn={stack:"dbcore",name:"VirtualIndexMiddleware",level:1,create:function(f){return _(_({},f),{table:function(e){var a=f.table(e),t=a.schema,u={},s=[];function c(e,t,n){var r=Xt(e),i=u[r]=u[r]||[],o=null==e?0:"string"==typeof e?1:e.length,a=0<t,a=_(_({},n),{name:a?"".concat(r,"(virtual-from:").concat(n.name,")"):n.name,lowLevelIndex:n,isVirtual:a,keyTail:t,keyLength:o,extractKey:$t(e),unique:!a&&n.unique});return i.push(a),a.isPrimaryKey||s.push(a),1<o&&c(2===o?e[0]:e.slice(0,o-1),t+1,n),i.sort(function(e,t){return e.keyTail-t.keyTail}),a}e=c(t.primaryKey.keyPath,0,t.primaryKey);u[":id"]=[e];for(var n=0,r=t.indexes;n<r.length;n++){var i=r[n];c(i.keyPath,0,i)}function l(e){var t,n=e.query.index;return n.isVirtual?_(_({},e),{query:{index:n.lowLevelIndex,range:(t=e.query.range,n=n.keyTail,{type:1===t.type?2:t.type,lower:Bn(t.lower,t.lowerOpen?f.MAX_KEY:f.MIN_KEY,n),lowerOpen:!0,upper:Bn(t.upper,t.upperOpen?f.MIN_KEY:f.MAX_KEY,n),upperOpen:!0})}}):e}return _(_({},a),{schema:_(_({},t),{primaryKey:e,indexes:s,getIndexByKeyPath:function(e){return(e=u[Xt(e)])&&e[0]}}),count:function(e){return a.count(l(e))},query:function(e){return a.query(l(e))},openCursor:function(t){var e=t.query.index,r=e.keyTail,n=e.isVirtual,i=e.keyLength;return n?a.openCursor(l(t)).then(function(e){return e&&o(e)}):a.openCursor(t);function o(n){return Object.create(n,{continue:{value:function(e){null!=e?n.continue(Bn(e,t.reverse?f.MAX_KEY:f.MIN_KEY,r)):t.unique?n.continue(n.key.slice(0,i).concat(t.reverse?f.MIN_KEY:f.MAX_KEY,r)):n.continue()}},continuePrimaryKey:{value:function(e,t){n.continuePrimaryKey(Bn(e,f.MAX_KEY,r),t)}},primaryKey:{get:function(){return n.primaryKey}},key:{get:function(){var e=n.key;return 1===i?e[0]:e.slice(0,i)}},value:{get:function(){return n.value}}})}}})}})}};function Fn(i,o,a,u){return a=a||{},u=u||"",x(i).forEach(function(e){var t,n,r;m(o,e)?(t=i[e],n=o[e],"object"==typeof t&&"object"==typeof n&&t&&n?(r=A(t))!==A(n)?a[u+e]=o[e]:"Object"===r?Fn(t,n,a,u+e+"."):t!==n&&(a[u+e]=o[e]):t!==n&&(a[u+e]=o[e])):a[u+e]=void 0}),x(o).forEach(function(e){m(i,e)||(a[u+e]=o[e])}),a}function Mn(e,t){return"delete"===t.type?t.keys:t.keys||t.values.map(e.extractKey)}var Nn={stack:"dbcore",name:"HooksMiddleware",level:2,create:function(e){return _(_({},e),{table:function(r){var y=e.table(r),v=y.schema.primaryKey;return _(_({},y),{mutate:function(e){var t=me.trans,n=t.table(r).hook,h=n.deleting,d=n.creating,p=n.updating;switch(e.type){case"add":if(d.fire===G)break;return t._promise("readwrite",function(){return a(e)},!0);case"put":if(d.fire===G&&p.fire===G)break;return t._promise("readwrite",function(){return a(e)},!0);case"delete":if(h.fire===G)break;return t._promise("readwrite",function(){return a(e)},!0);case"deleteRange":if(h.fire===G)break;return t._promise("readwrite",function(){return function n(r,i,o){return y.query({trans:r,values:!1,query:{index:v,range:i},limit:o}).then(function(e){var t=e.result;return a({type:"delete",keys:t,trans:r}).then(function(e){return 0<e.numFailures?Promise.reject(e.failures[0]):t.length<o?{failures:[],numFailures:0,lastResult:void 0}:n(r,_(_({},i),{lower:t[t.length-1],lowerOpen:!0}),o)})})}(e.trans,e.range,1e4)},!0)}return y.mutate(e);function a(c){var e,t,n,l=me.trans,f=c.keys||Mn(v,c);if(!f)throw new Error("Keys missing");return"delete"!==(c="add"===c.type||"put"===c.type?_(_({},c),{keys:f}):_({},c)).type&&(c.values=i([],c.values,!0)),c.keys&&(c.keys=i([],c.keys,!0)),e=y,n=f,("add"===(t=c).type?Promise.resolve([]):e.getMany({trans:t.trans,keys:n,cache:"immutable"})).then(function(u){var s=f.map(function(e,t){var n,r,i,o=u[t],a={onerror:null,onsuccess:null};return"delete"===c.type?h.fire.call(a,e,o,l):"add"===c.type||void 0===o?(n=d.fire.call(a,e,c.values[t],l),null==e&&null!=n&&(c.keys[t]=e=n,v.outbound||P(c.values[t],v.keyPath,e))):(n=Fn(o,c.values[t]),(r=p.fire.call(a,n,e,o,l))&&(i=c.values[t],Object.keys(r).forEach(function(e){m(i,e)?i[e]=r[e]:P(i,e,r[e])}))),a});return y.mutate(c).then(function(e){for(var t=e.failures,n=e.results,r=e.numFailures,e=e.lastResult,i=0;i<f.length;++i){var o=(n||f)[i],a=s[i];null==o?a.onerror&&a.onerror(t[i]):a.onsuccess&&a.onsuccess("put"===c.type&&u[i]?c.values[i]:o)}return{failures:t,results:n,numFailures:r,lastResult:e}}).catch(function(t){return s.forEach(function(e){return e.onerror&&e.onerror(t)}),Promise.reject(t)})})}}})}})}};function Ln(e,t,n){try{if(!t)return null;if(t.keys.length<e.length)return null;for(var r=[],i=0,o=0;i<t.keys.length&&o<e.length;++i)0===st(t.keys[i],e[o])&&(r.push(n?S(t.values[i]):t.values[i]),++o);return r.length===e.length?r:null}catch(e){return null}}var Un={stack:"dbcore",level:-1,create:function(t){return{table:function(e){var n=t.table(e);return _(_({},n),{getMany:function(t){if(!t.cache)return n.getMany(t);var e=Ln(t.keys,t.trans._cache,"clone"===t.cache);return e?_e.resolve(e):n.getMany(t).then(function(e){return t.trans._cache={keys:t.keys,values:"clone"===t.cache?S(e):e},e})},mutate:function(e){return"add"!==e.type&&(e.trans._cache=null),n.mutate(e)}})}}}};function Vn(e,t){return"readonly"===e.trans.mode&&!!e.subscr&&!e.trans.explicit&&"disabled"!==e.trans.db._options.cache&&!t.schema.primaryKey.outbound}function zn(e,t){switch(e){case"query":return t.values&&!t.unique;case"get":case"getMany":case"count":case"openCursor":return!1}}var Wn={stack:"dbcore",level:0,name:"Observability",create:function(b){var g=b.schema.name,w=new gn(b.MIN_KEY,b.MAX_KEY);return _(_({},b),{transaction:function(e,t,n){if(me.subscr&&"readonly"!==t)throw new Y.ReadOnly("Readwrite transaction in liveQuery context. Querier source: ".concat(me.querier));return b.transaction(e,t,n)},table:function(d){var p=b.table(d),y=p.schema,v=y.primaryKey,e=y.indexes,c=v.extractKey,l=v.outbound,m=v.autoIncrement&&e.filter(function(e){return e.compound&&e.keyPath.includes(v.keyPath)}),t=_(_({},p),{mutate:function(a){function u(e){return e="idb://".concat(g,"/").concat(d,"/").concat(e),n[e]||(n[e]=new gn)}var e,o,s,t=a.trans,n=a.mutatedParts||(a.mutatedParts={}),r=u(""),i=u(":dels"),c=a.type,l="deleteRange"===a.type?[a.range]:"delete"===a.type?[a.keys]:a.values.length<50?[Mn(v,a).filter(function(e){return e}),a.values]:[],f=l[0],h=l[1],l=a.trans._cache;return k(f)?(r.addKeys(f),(l="delete"===c||f.length===h.length?Ln(f,l):null)||i.addKeys(f),(l||h)&&(e=u,o=l,s=h,y.indexes.forEach(function(t){var n=e(t.name||"");function r(e){return null!=e?t.extractKey(e):null}function i(e){return t.multiEntry&&k(e)?e.forEach(function(e){return n.addKey(e)}):n.addKey(e)}(o||s).forEach(function(e,t){var n=o&&r(o[t]),t=s&&r(s[t]);0!==st(n,t)&&(null!=n&&i(n),null!=t&&i(t))})}))):f?(h={from:null!==(h=f.lower)&&void 0!==h?h:b.MIN_KEY,to:null!==(h=f.upper)&&void 0!==h?h:b.MAX_KEY},i.add(h),r.add(h)):(r.add(w),i.add(w),y.indexes.forEach(function(e){return u(e.name).add(w)})),p.mutate(a).then(function(o){return!f||"add"!==a.type&&"put"!==a.type||(r.addKeys(o.results),m&&m.forEach(function(t){for(var e=a.values.map(function(e){return t.extractKey(e)}),n=t.keyPath.findIndex(function(e){return e===v.keyPath}),r=0,i=o.results.length;r<i;++r)e[r][n]=o.results[r];u(t.name).addKeys(e)})),t.mutatedParts=Kn(t.mutatedParts||{},n),o})}}),e=function(e){var t=e.query,e=t.index,t=t.range;return[e,new gn(null!==(e=t.lower)&&void 0!==e?e:b.MIN_KEY,null!==(t=t.upper)&&void 0!==t?t:b.MAX_KEY)]},f={get:function(e){return[v,new gn(e.key)]},getMany:function(e){return[v,(new gn).addKeys(e.keys)]},count:e,query:e,openCursor:e};return x(f).forEach(function(s){t[s]=function(i){var e=me.subscr,t=!!e,n=Vn(me,p)&&zn(s,i)?i.obsSet={}:e;if(t){var r=function(e){e="idb://".concat(g,"/").concat(d,"/").concat(e);return n[e]||(n[e]=new gn)},o=r(""),a=r(":dels"),e=f[s](i),t=e[0],e=e[1];if(("query"===s&&t.isPrimaryKey&&!i.values?a:r(t.name||"")).add(e),!t.isPrimaryKey){if("count"!==s){var u="query"===s&&l&&i.values&&p.query(_(_({},i),{values:!1}));return p[s].apply(this,arguments).then(function(t){if("query"===s){if(l&&i.values)return u.then(function(e){e=e.result;return o.addKeys(e),t});var e=i.values?t.result.map(c):t.result;(i.values?o:a).addKeys(e)}else if("openCursor"===s){var n=t,r=i.values;return n&&Object.create(n,{key:{get:function(){return a.addKey(n.primaryKey),n.key}},primaryKey:{get:function(){var e=n.primaryKey;return a.addKey(e),e}},value:{get:function(){return r&&o.addKey(n.primaryKey),n.value}}})}return t})}a.add(w)}}return p[s].apply(this,arguments)}}),t}})}};function Yn(e,t,n){if(0===n.numFailures)return t;if("deleteRange"===t.type)return null;var r=t.keys?t.keys.length:"values"in t&&t.values?t.values.length:1;if(n.numFailures===r)return null;t=_({},t);return k(t.keys)&&(t.keys=t.keys.filter(function(e,t){return!(t in n.failures)})),"values"in t&&k(t.values)&&(t.values=t.values.filter(function(e,t){return!(t in n.failures)})),t}function $n(e,t){return n=e,(void 0===(r=t).lower||(r.lowerOpen?0<st(n,r.lower):0<=st(n,r.lower)))&&(e=e,void 0===(t=t).upper||(t.upperOpen?st(e,t.upper)<0:st(e,t.upper)<=0));var n,r}function Qn(e,d,t,n,r,i){if(!t||0===t.length)return e;var o=d.query.index,p=o.multiEntry,y=d.query.range,v=n.schema.primaryKey.extractKey,m=o.extractKey,a=(o.lowLevelIndex||o).extractKey,t=t.reduce(function(e,t){var n=e,r=[];if("add"===t.type||"put"===t.type)for(var i=new gn,o=t.values.length-1;0<=o;--o){var a,u=t.values[o],s=v(u);i.hasKey(s)||(a=m(u),(p&&k(a)?a.some(function(e){return $n(e,y)}):$n(a,y))&&(i.addKey(s),r.push(u)))}switch(t.type){case"add":var c=(new gn).addKeys(d.values?e.map(function(e){return v(e)}):e),n=e.concat(d.values?r.filter(function(e){e=v(e);return!c.hasKey(e)&&(c.addKey(e),!0)}):r.map(function(e){return v(e)}).filter(function(e){return!c.hasKey(e)&&(c.addKey(e),!0)}));break;case"put":var l=(new gn).addKeys(t.values.map(function(e){return v(e)}));n=e.filter(function(e){return!l.hasKey(d.values?v(e):e)}).concat(d.values?r:r.map(function(e){return v(e)}));break;case"delete":var f=(new gn).addKeys(t.keys);n=e.filter(function(e){return!f.hasKey(d.values?v(e):e)});break;case"deleteRange":var h=t.range;n=e.filter(function(e){return!$n(v(e),h)})}return n},e);return t===e?e:(t.sort(function(e,t){return st(a(e),a(t))||st(v(e),v(t))}),d.limit&&d.limit<1/0&&(t.length>d.limit?t.length=d.limit:e.length===d.limit&&t.length<d.limit&&(r.dirty=!0)),i?Object.freeze(t):t)}function Gn(e,t){return 0===st(e.lower,t.lower)&&0===st(e.upper,t.upper)&&!!e.lowerOpen==!!t.lowerOpen&&!!e.upperOpen==!!t.upperOpen}function Xn(e,t){return function(e,t,n,r){if(void 0===e)return void 0!==t?-1:0;if(void 0===t)return 1;if(0===(t=st(e,t))){if(n&&r)return 0;if(n)return 1;if(r)return-1}return t}(e.lower,t.lower,e.lowerOpen,t.lowerOpen)<=0&&0<=function(e,t,n,r){if(void 0===e)return void 0!==t?1:0;if(void 0===t)return-1;if(0===(t=st(e,t))){if(n&&r)return 0;if(n)return-1;if(r)return 1}return t}(e.upper,t.upper,e.upperOpen,t.upperOpen)}function Hn(n,r,i,e){n.subscribers.add(i),e.addEventListener("abort",function(){var e,t;n.subscribers.delete(i),0===n.subscribers.size&&(e=n,t=r,setTimeout(function(){0===e.subscribers.size&&q(t,e)},3e3))})}var Jn={stack:"dbcore",level:0,name:"Cache",create:function(k){var O=k.schema.name;return _(_({},k),{transaction:function(g,w,e){var _,t,x=k.transaction(g,w,e);return"readwrite"===w&&(t=(_=new AbortController).signal,e=function(b){return function(){if(_.abort(),"readwrite"===w){for(var t=new Set,e=0,n=g;e<n.length;e++){var r=n[e],i=Sn["idb://".concat(O,"/").concat(r)];if(i){var o=k.table(r),a=i.optimisticOps.filter(function(e){return e.trans===x});if(x._explicit&&b&&x.mutatedParts)for(var u=0,s=Object.values(i.queries.query);u<s.length;u++)for(var c=0,l=(d=s[u]).slice();c<l.length;c++)En((p=l[c]).obsSet,x.mutatedParts)&&(q(d,p),p.subscribers.forEach(function(e){return t.add(e)}));else if(0<a.length){i.optimisticOps=i.optimisticOps.filter(function(e){return e.trans!==x});for(var f=0,h=Object.values(i.queries.query);f<h.length;f++)for(var d,p,y,v=0,m=(d=h[f]).slice();v<m.length;v++)null!=(p=m[v]).res&&x.mutatedParts&&(b&&!p.dirty?(y=Object.isFrozen(p.res),y=Qn(p.res,p.req,a,o,p,y),p.dirty?(q(d,p),p.subscribers.forEach(function(e){return t.add(e)})):y!==p.res&&(p.res=y,p.promise=_e.resolve({result:y}))):(p.dirty&&q(d,p),p.subscribers.forEach(function(e){return t.add(e)})))}}}t.forEach(function(e){return e()})}}},x.addEventListener("abort",e(!1),{signal:t}),x.addEventListener("error",e(!1),{signal:t}),x.addEventListener("complete",e(!0),{signal:t})),x},table:function(c){var l=k.table(c),i=l.schema.primaryKey;return _(_({},l),{mutate:function(t){var e=me.trans;if(i.outbound||"disabled"===e.db._options.cache||e.explicit||"readwrite"!==e.idbtrans.mode)return l.mutate(t);var n=Sn["idb://".concat(O,"/").concat(c)];if(!n)return l.mutate(t);e=l.mutate(t);return"add"!==t.type&&"put"!==t.type||!(50<=t.values.length||Mn(i,t).some(function(e){return null==e}))?(n.optimisticOps.push(t),t.mutatedParts&&Cn(t.mutatedParts),e.then(function(e){0<e.numFailures&&(q(n.optimisticOps,t),(e=Yn(0,t,e))&&n.optimisticOps.push(e),t.mutatedParts&&Cn(t.mutatedParts))}),e.catch(function(){q(n.optimisticOps,t),t.mutatedParts&&Cn(t.mutatedParts)})):e.then(function(r){var e=Yn(0,_(_({},t),{values:t.values.map(function(e,t){var n;if(r.failures[t])return e;e=null!==(n=i.keyPath)&&void 0!==n&&n.includes(".")?S(e):_({},e);return P(e,i.keyPath,r.results[t]),e})}),r);n.optimisticOps.push(e),queueMicrotask(function(){return t.mutatedParts&&Cn(t.mutatedParts)})}),e},query:function(t){if(!Vn(me,l)||!zn("query",t))return l.query(t);var i="immutable"===(null===(o=me.trans)||void 0===o?void 0:o.db._options.cache),e=me,n=e.requery,r=e.signal,o=function(e,t,n,r){var i=Sn["idb://".concat(e,"/").concat(t)];if(!i)return[];if(!(t=i.queries[n]))return[null,!1,i,null];var o=t[(r.query?r.query.index.name:null)||""];if(!o)return[null,!1,i,null];switch(n){case"query":var a=o.find(function(e){return e.req.limit===r.limit&&e.req.values===r.values&&Gn(e.req.query.range,r.query.range)});return a?[a,!0,i,o]:[o.find(function(e){return("limit"in e.req?e.req.limit:1/0)>=r.limit&&(!r.values||e.req.values)&&Xn(e.req.query.range,r.query.range)}),!1,i,o];case"count":a=o.find(function(e){return Gn(e.req.query.range,r.query.range)});return[a,!!a,i,o]}}(O,c,"query",t),a=o[0],e=o[1],u=o[2],s=o[3];return a&&e?a.obsSet=t.obsSet:(e=l.query(t).then(function(e){var t=e.result;if(a&&(a.res=t),i){for(var n=0,r=t.length;n<r;++n)Object.freeze(t[n]);Object.freeze(t)}else e.result=S(t);return e}).catch(function(e){return s&&a&&q(s,a),Promise.reject(e)}),a={obsSet:t.obsSet,promise:e,subscribers:new Set,type:"query",req:t,dirty:!1},s?s.push(a):(s=[a],(u=u||(Sn["idb://".concat(O,"/").concat(c)]={queries:{query:{},count:{}},objs:new Map,optimisticOps:[],unsignaledParts:{}})).queries.query[t.query.index.name||""]=s)),Hn(a,s,n,r),a.promise.then(function(e){return{result:Qn(e.result,t,null==u?void 0:u.optimisticOps,l,a,i)}})}})}})}};function Zn(e,r){return new Proxy(e,{get:function(e,t,n){return"db"===t?r:Reflect.get(e,t,n)}})}var er=(tr.prototype.version=function(t){if(isNaN(t)||t<.1)throw new Y.Type("Given version is not a positive number");if(t=Math.round(10*t)/10,this.idbdb||this._state.isBeingOpened)throw new Y.Schema("Cannot add version when database is open");this.verno=Math.max(this.verno,t);var e=this._versions,n=e.filter(function(e){return e._cfg.version===t})[0];return n||(n=new this.Version(t),e.push(n),e.sort(nn),n.stores({}),this._state.autoSchema=!1,n)},tr.prototype._whenReady=function(e){var n=this;return this.idbdb&&(this._state.openComplete||me.letThrough||this._vip)?e():new _e(function(e,t){if(n._state.openComplete)return t(new Y.DatabaseClosed(n._state.dbOpenError));if(!n._state.isBeingOpened){if(!n._state.autoOpen)return void t(new Y.DatabaseClosed);n.open().catch(G)}n._state.dbReadyPromise.then(e,t)}).then(e)},tr.prototype.use=function(e){var t=e.stack,n=e.create,r=e.level,i=e.name;i&&this.unuse({stack:t,name:i});e=this._middlewares[t]||(this._middlewares[t]=[]);return e.push({stack:t,create:n,level:null==r?10:r,name:i}),e.sort(function(e,t){return e.level-t.level}),this},tr.prototype.unuse=function(e){var t=e.stack,n=e.name,r=e.create;return t&&this._middlewares[t]&&(this._middlewares[t]=this._middlewares[t].filter(function(e){return r?e.create!==r:!!n&&e.name!==n})),this},tr.prototype.open=function(){var e=this;return $e(ve,function(){return Dn(e)})},tr.prototype._close=function(){var n=this._state,e=et.indexOf(this);if(0<=e&&et.splice(e,1),this.idbdb){try{this.idbdb.close()}catch(e){}this.idbdb=null}n.isBeingOpened||(n.dbReadyPromise=new _e(function(e){n.dbReadyResolve=e}),n.openCanceller=new _e(function(e,t){n.cancelOpen=t}))},tr.prototype.close=function(e){var t=(void 0===e?{disableAutoOpen:!0}:e).disableAutoOpen,e=this._state;t?(e.isBeingOpened&&e.cancelOpen(new Y.DatabaseClosed),this._close(),e.autoOpen=!1,e.dbOpenError=new Y.DatabaseClosed):(this._close(),e.autoOpen=this._options.autoOpen||e.isBeingOpened,e.openComplete=!1,e.dbOpenError=null)},tr.prototype.delete=function(n){var i=this;void 0===n&&(n={disableAutoOpen:!0});var o=0<arguments.length&&"object"!=typeof arguments[0],a=this._state;return new _e(function(r,t){function e(){i.close(n);var e=i._deps.indexedDB.deleteDatabase(i.name);e.onsuccess=qe(function(){var e,t,n;e=i._deps,t=i.name,n=e.indexedDB,e=e.IDBKeyRange,vn(n)||t===tt||yn(n,e).delete(t).catch(G),r()}),e.onerror=Bt(t),e.onblocked=i._fireOnBlocked}if(o)throw new Y.InvalidArgument("Invalid closeOptions argument to db.delete()");a.isBeingOpened?a.dbReadyPromise.then(e):e()})},tr.prototype.backendDB=function(){return this.idbdb},tr.prototype.isOpen=function(){return null!==this.idbdb},tr.prototype.hasBeenClosed=function(){var e=this._state.dbOpenError;return e&&"DatabaseClosed"===e.name},tr.prototype.hasFailed=function(){return null!==this._state.dbOpenError},tr.prototype.dynamicallyOpened=function(){return this._state.autoSchema},Object.defineProperty(tr.prototype,"tables",{get:function(){var t=this;return x(this._allTables).map(function(e){return t._allTables[e]})},enumerable:!1,configurable:!0}),tr.prototype.transaction=function(){var e=function(e,t,n){var r=arguments.length;if(r<2)throw new Y.InvalidArgument("Too few arguments");for(var i=new Array(r-1);--r;)i[r-1]=arguments[r];return n=i.pop(),[e,w(i),n]}.apply(this,arguments);return this._transaction.apply(this,e)},tr.prototype._transaction=function(e,t,n){var r=this,i=me.trans;i&&i.db===this&&-1===e.indexOf("!")||(i=null);var o,a,u=-1!==e.indexOf("?");e=e.replace("!","").replace("?","");try{if(a=t.map(function(e){e=e instanceof r.Table?e.name:e;if("string"!=typeof e)throw new TypeError("Invalid table argument to Dexie.transaction(). Only Table or String are allowed");return e}),"r"==e||e===nt)o=nt;else{if("rw"!=e&&e!=rt)throw new Y.InvalidArgument("Invalid transaction mode: "+e);o=rt}if(i){if(i.mode===nt&&o===rt){if(!u)throw new Y.SubTransaction("Cannot enter a sub-transaction with READWRITE mode when parent transaction is READONLY");i=null}i&&a.forEach(function(e){if(i&&-1===i.storeNames.indexOf(e)){if(!u)throw new Y.SubTransaction("Table "+e+" not included in parent transaction.");i=null}}),u&&i&&!i.active&&(i=null)}}catch(n){return i?i._promise(null,function(e,t){t(n)}):Xe(n)}var s=function i(o,a,u,s,c){return _e.resolve().then(function(){var e=me.transless||me,t=o._createTransaction(a,u,o._dbSchema,s);if(t.explicit=!0,e={trans:t,transless:e},s)t.idbtrans=s.idbtrans;else try{t.create(),t.idbtrans._explicit=!0,o._state.PR1398_maxLoop=3}catch(e){return e.name===z.InvalidState&&o.isOpen()&&0<--o._state.PR1398_maxLoop?(console.warn("Dexie: Need to reopen db"),o.close({disableAutoOpen:!1}),o.open().then(function(){return i(o,a,u,null,c)})):Xe(e)}var n,r=B(c);return r&&Le(),e=_e.follow(function(){var e;(n=c.call(t,t))&&(r?(e=Ue.bind(null,null),n.then(e,e)):"function"==typeof n.next&&"function"==typeof n.throw&&(n=In(n)))},e),(n&&"function"==typeof n.then?_e.resolve(n).then(function(e){return t.active?e:Xe(new Y.PrematureCommit("Transaction committed too early. See http://bit.ly/2kdckMn"))}):e.then(function(){return n})).then(function(e){return s&&t._resolve(),t._completion.then(function(){return e})}).catch(function(e){return t._reject(e),Xe(e)})})}.bind(null,this,o,a,i,n);return i?i._promise(o,s,"lock"):me.trans?$e(me.transless,function(){return r._whenReady(s)}):this._whenReady(s)},tr.prototype.table=function(e){if(!m(this._allTables,e))throw new Y.InvalidTable("Table ".concat(e," does not exist"));return this._allTables[e]},tr);function tr(e,t){var o=this;this._middlewares={},this.verno=0;var n=tr.dependencies;this._options=t=_({addons:tr.addons,autoOpen:!0,indexedDB:n.indexedDB,IDBKeyRange:n.IDBKeyRange,cache:"cloned"},t),this._deps={indexedDB:t.indexedDB,IDBKeyRange:t.IDBKeyRange};n=t.addons;this._dbSchema={},this._versions=[],this._storeNames=[],this._allTables={},this.idbdb=null,this._novip=this;var a,r,u,i,s,c={dbOpenError:null,isBeingOpened:!1,onReadyBeingFired:null,openComplete:!1,dbReadyResolve:G,dbReadyPromise:null,cancelOpen:G,openCanceller:null,autoSchema:!0,PR1398_maxLoop:3,autoOpen:t.autoOpen};c.dbReadyPromise=new _e(function(e){c.dbReadyResolve=e}),c.openCanceller=new _e(function(e,t){c.cancelOpen=t}),this._state=c,this.name=e,this.on=dt(this,"populate","blocked","versionchange","close",{ready:[re,G]}),this.on.ready.subscribe=p(this.on.ready.subscribe,function(i){return function(n,r){tr.vip(function(){var t,e=o._state;e.openComplete?(e.dbOpenError||_e.resolve().then(n),r&&i(n)):e.onReadyBeingFired?(e.onReadyBeingFired.push(n),r&&i(n)):(i(n),t=o,r||i(function e(){t.on.ready.unsubscribe(n),t.on.ready.unsubscribe(e)}))})}}),this.Collection=(a=this,pt(Ot.prototype,function(e,t){this.db=a;var n=ot,r=null;if(t)try{n=t()}catch(e){r=e}var i=e._ctx,t=i.table,e=t.hook.reading.fire;this._ctx={table:t,index:i.index,isPrimKey:!i.index||t.schema.primKey.keyPath&&i.index===t.schema.primKey.name,range:n,keysOnly:!1,dir:"next",unique:"",algorithm:null,filter:null,replayFilter:null,justLimit:!0,isMatch:null,offset:0,limit:1/0,error:r,or:i.or,valueMapper:e!==X?e:null}})),this.Table=(r=this,pt(ft.prototype,function(e,t,n){this.db=r,this._tx=n,this.name=e,this.schema=t,this.hook=r._allTables[e]?r._allTables[e].hook:dt(null,{creating:[Z,G],reading:[H,X],updating:[te,G],deleting:[ee,G]})})),this.Transaction=(u=this,pt(Lt.prototype,function(e,t,n,r,i){var o=this;this.db=u,this.mode=e,this.storeNames=t,this.schema=n,this.chromeTransactionDurability=r,this.idbtrans=null,this.on=dt(this,"complete","error","abort"),this.parent=i||null,this.active=!0,this._reculock=0,this._blockedFuncs=[],this._resolve=null,this._reject=null,this._waitingFor=null,this._waitingQueue=null,this._spinCount=0,this._completion=new _e(function(e,t){o._resolve=e,o._reject=t}),this._completion.then(function(){o.active=!1,o.on.complete.fire()},function(e){var t=o.active;return o.active=!1,o.on.error.fire(e),o.parent?o.parent._reject(e):t&&o.idbtrans&&o.idbtrans.abort(),Xe(e)})})),this.Version=(i=this,pt(dn.prototype,function(e){this.db=i,this._cfg={version:e,storesSource:null,dbschema:{},tables:{},contentUpgrade:null}})),this.WhereClause=(s=this,pt(Dt.prototype,function(e,t,n){if(this.db=s,this._ctx={table:e,index:":id"===t?null:t,or:n},this._cmp=this._ascending=st,this._descending=function(e,t){return st(t,e)},this._max=function(e,t){return 0<st(e,t)?e:t},this._min=function(e,t){return st(e,t)<0?e:t},this._IDBKeyRange=s._deps.IDBKeyRange,!this._IDBKeyRange)throw new Y.MissingAPI})),this.on("versionchange",function(e){0<e.newVersion?console.warn("Another connection wants to upgrade database '".concat(o.name,"'. Closing db now to resume the upgrade.")):console.warn("Another connection wants to delete database '".concat(o.name,"'. Closing db now to resume the delete request.")),o.close({disableAutoOpen:!1})}),this.on("blocked",function(e){!e.newVersion||e.newVersion<e.oldVersion?console.warn("Dexie.delete('".concat(o.name,"') was blocked")):console.warn("Upgrade '".concat(o.name,"' blocked by other connection holding version ").concat(e.oldVersion/10))}),this._maxKey=Yt(t.IDBKeyRange),this._createTransaction=function(e,t,n,r){return new o.Transaction(e,t,n,o._options.chromeTransactionDurability,r)},this._fireOnBlocked=function(t){o.on("blocked").fire(t),et.filter(function(e){return e.name===o.name&&e!==o&&!e._state.vcFired}).map(function(e){return e.on("versionchange").fire(t)})},this.use(Un),this.use(Jn),this.use(Wn),this.use(Rn),this.use(Nn);var l=new Proxy(this,{get:function(e,t,n){if("_vip"===t)return!0;if("table"===t)return function(e){return Zn(o.table(e),l)};var r=Reflect.get(e,t,n);return r instanceof ft?Zn(r,l):"tables"===t?r.map(function(e){return Zn(e,l)}):"_createTransaction"===t?function(){return Zn(r.apply(this,arguments),l)}:r}});this.vip=l,n.forEach(function(e){return e(o)})}var nr,F="undefined"!=typeof Symbol&&"observable"in Symbol?Symbol.observable:"@@observable",rr=(ir.prototype.subscribe=function(e,t,n){return this._subscribe(e&&"function"!=typeof e?e:{next:e,error:t,complete:n})},ir.prototype[F]=function(){return this},ir);function ir(e){this._subscribe=e}try{nr={indexedDB:f.indexedDB||f.mozIndexedDB||f.webkitIndexedDB||f.msIndexedDB,IDBKeyRange:f.IDBKeyRange||f.webkitIDBKeyRange}}catch(e){nr={indexedDB:null,IDBKeyRange:null}}function or(h){var d,p=!1,e=new rr(function(r){var i=B(h);var o,a=!1,u={},s={},e={get closed(){return a},unsubscribe:function(){a||(a=!0,o&&o.abort(),c&&Nt.storagemutated.unsubscribe(f))}};r.start&&r.start(e);var c=!1,l=function(){return Ge(t)};var f=function(e){Kn(u,e),En(s,u)&&l()},t=function(){var t,n,e;!a&&nr.indexedDB&&(u={},t={},o&&o.abort(),o=new AbortController,e=function(e){var t=je();try{i&&Le();var n=Ne(h,e);return n=i?n.finally(Ue):n}finally{t&&Ae()}}(n={subscr:t,signal:o.signal,requery:l,querier:h,trans:null}),Promise.resolve(e).then(function(e){p=!0,d=e,a||n.signal.aborted||(u={},function(e){for(var t in e)if(m(e,t))return;return 1}(s=t)||c||(Nt(Ft,f),c=!0),Ge(function(){return!a&&r.next&&r.next(e)}))},function(e){p=!1,["DatabaseClosedError","AbortError"].includes(null==e?void 0:e.name)||a||Ge(function(){a||r.error&&r.error(e)})}))};return setTimeout(l,0),e});return e.hasValue=function(){return p},e.getValue=function(){return d},e}var ar=er;function ur(e){var t=cr;try{cr=!0,Nt.storagemutated.fire(e),Tn(e,!0)}finally{cr=t}}r(ar,_(_({},Q),{delete:function(e){return new ar(e,{addons:[]}).delete()},exists:function(e){return new ar(e,{addons:[]}).open().then(function(e){return e.close(),!0}).catch("NoSuchDatabaseError",function(){return!1})},getDatabaseNames:function(e){try{return t=ar.dependencies,n=t.indexedDB,t=t.IDBKeyRange,(vn(n)?Promise.resolve(n.databases()).then(function(e){return e.map(function(e){return e.name}).filter(function(e){return e!==tt})}):yn(n,t).toCollection().primaryKeys()).then(e)}catch(e){return Xe(new Y.MissingAPI)}var t,n},defineClass:function(){return function(e){a(this,e)}},ignoreTransaction:function(e){return me.trans?$e(me.transless,e):e()},vip:mn,async:function(t){return function(){try{var e=In(t.apply(this,arguments));return e&&"function"==typeof e.then?e:_e.resolve(e)}catch(e){return Xe(e)}}},spawn:function(e,t,n){try{var r=In(e.apply(n,t||[]));return r&&"function"==typeof r.then?r:_e.resolve(r)}catch(e){return Xe(e)}},currentTransaction:{get:function(){return me.trans||null}},waitFor:function(e,t){t=_e.resolve("function"==typeof e?ar.ignoreTransaction(e):e).timeout(t||6e4);return me.trans?me.trans.waitFor(t):t},Promise:_e,debug:{get:function(){return ie},set:function(e){oe(e)}},derive:o,extend:a,props:r,override:p,Events:dt,on:Nt,liveQuery:or,extendObservabilitySet:Kn,getByKeyPath:O,setByKeyPath:P,delByKeyPath:function(t,e){"string"==typeof e?P(t,e,void 0):"length"in e&&[].map.call(e,function(e){P(t,e,void 0)})},shallowClone:g,deepClone:S,getObjectDiff:Fn,cmp:st,asap:v,minKey:-1/0,addons:[],connections:et,errnames:z,dependencies:nr,cache:Sn,semVer:"4.0.11",version:"4.0.11".split(".").map(function(e){return parseInt(e)}).reduce(function(e,t,n){return e+t/Math.pow(10,2*n)})})),ar.maxKey=Yt(ar.dependencies.IDBKeyRange),"undefined"!=typeof dispatchEvent&&"undefined"!=typeof addEventListener&&(Nt(Ft,function(e){cr||(e=new CustomEvent(Mt,{detail:e}),cr=!0,dispatchEvent(e),cr=!1)}),addEventListener(Mt,function(e){e=e.detail;cr||ur(e)}));var sr,cr=!1,lr=function(){};return"undefined"!=typeof BroadcastChannel&&((lr=function(){(sr=new BroadcastChannel(Mt)).onmessage=function(e){return e.data&&ur(e.data)}})(),"function"==typeof sr.unref&&sr.unref(),Nt(Ft,function(e){cr||sr.postMessage(e)})),"undefined"!=typeof addEventListener&&(addEventListener("pagehide",function(e){if(!er.disableBfCache&&e.persisted){ie&&console.debug("Dexie: handling persisted pagehide"),null!=sr&&sr.close();for(var t=0,n=et;t<n.length;t++)n[t].close({disableAutoOpen:!1})}}),addEventListener("pageshow",function(e){!er.disableBfCache&&e.persisted&&(ie&&console.debug("Dexie: handling persisted pageshow"),lr(),ur({all:new gn(-1/0,[[]])}))})),_e.rejectionMapper=function(e,t){return!e||e instanceof N||e instanceof TypeError||e instanceof SyntaxError||!e.name||!$[e.name]?e:(t=new $[e.name](t||e.message,e),"stack"in e&&l(t,"stack",{get:function(){return this.inner.stack}}),t)},oe(ie),_(er,Object.freeze({__proto__:null,Dexie:er,liveQuery:or,Entity:ut,cmp:st,PropModification:xt,replacePrefix:function(e,t){return new xt({replacePrefix:[e,t]})},add:function(e){return new xt({add:e})},remove:function(e){return new xt({remove:e})},default:er,RangeSet:gn,mergeRanges:_n,rangesOverlap:xn}),{default:er}),er});
2
+ //# sourceMappingURL=dexie.min.js.map
dexie-js/dexie.min.js.map ADDED
The diff for this file is too large to render. See raw diff
 
favicon.ico ADDED
index.html ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" data-theme="dark">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title data-i18n="title">Kimi - Virtual Companion 💕</title>
8
+
9
+ <!-- Main CSS Files -->
10
+ <link rel="stylesheet" href="kimi-css/kimi-style.css" />
11
+ <link rel="stylesheet" href="kimi-css/kimi-settings.css" />
12
+ <link rel="stylesheet" href="kimi-css/kimi-memory-styles.css" />
13
+
14
+ <!-- SEO Meta Tags -->
15
+ <meta name="description"
16
+ content="Virtual Kimi is a an AI girlfriend and companion with evolving personality, advanced voice recognition and immersive interface. Discover the future of human-AI girlfriend relationships.">
17
+ <meta name="author" content="Jean & Kimi">
18
+ <meta name="robots" content="index, follow">
19
+ <link rel="canonical" href="https://virtual-kimi.com/virtual-kimi-app/" />
20
+
21
+ <!-- Open Graph / Facebook -->
22
+ <meta property="og:type" content="website">
23
+ <meta property="og:url" content="https://virtual-kimi.com/virtual-kimi-app/">
24
+ <meta property="og:title" content="Virtual Kimi - Virtual AI Companion">
25
+ <meta property="og:description"
26
+ content="Virtual Kimi, your virtual AI girlfriend and companion with an 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 -->
30
+ <meta property="twitter:card" content="summary_large_image">
31
+ <meta property="twitter:url" content="https://virtual-kimi.com/virtual-kimi-app/">
32
+ <meta property="twitter:title" content="Virtual Kimi - Virtual AI Companion">
33
+ <meta property="twitter:description"
34
+ content="Virtual AI companion with evolving personality and advanced voice recognition.">
35
+ <meta property="twitter:image" content="kimi-icons/virtualkimi-logo.png">
36
+
37
+ <!-- Favicon -->
38
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
39
+ <!-- Multi-size Favicons -->
40
+ <link rel="icon" type="image/png" sizes="16x16" href="kimi-icons/favicons/favicon-16x16.png">
41
+ <link rel="icon" type="image/png" sizes="32x32" href="kimi-icons/favicons/favicon-32x32.png">
42
+ <link rel="icon" type="image/png" sizes="48x48" href="kimi-icons/favicons/favicon-48x48.png">
43
+ <link rel="icon" type="image/png" sizes="64x64" href="kimi-icons/favicons/favicon-64x64.png">
44
+ <link rel="icon" type="image/png" sizes="96x96" href="kimi-icons/favicons/favicon-96x96.png">
45
+ <link rel="icon" type="image/png" sizes="128x128" href="kimi-icons/favicons/favicon-128x128.png">
46
+ <link rel="icon" type="image/png" sizes="192x192" href="kimi-icons/favicons/favicon-192x192.png">
47
+ <link rel="apple-touch-icon" sizes="180x180" href="kimi-icons/favicons/apple-touch-icon-180x180.png">
48
+
49
+ <!-- Font Awesome -->
50
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
51
+
52
+ <!-- Performance: warm up connection to origin -->
53
+ <link rel="preconnect" href="https://virtual-kimi.com" crossorigin>
54
+ <link rel="dns-prefetch" href="//virtual-kimi.com">
55
+
56
+ </head>
57
+
58
+ <body>
59
+ <div id="loading-screen">
60
+ <img src="kimi-icons/kimi-loading.png" alt="Loading Kimi..." />
61
+ </div>
62
+
63
+ <div class="video-container">
64
+ <video autoplay muted playsinline class="bg-video active" id="video1" preload="auto">
65
+ <source src="" type="video/mp4" />
66
+ <span data-i18n="video_not_supported">Your browser does not support the video tag.</span>
67
+ </video>
68
+ <video autoplay muted playsinline class="bg-video" id="video2" preload="auto">
69
+ <source src="" type="video/mp4" />
70
+ <span data-i18n="video_not_supported">Your browser does not support the video tag.</span>
71
+ </video>
72
+ </div>
73
+
74
+ <div class="content-overlay">
75
+ <!-- Global Top-Right Utility Buttons -->
76
+ <div class="top-right-buttons" id="top-right-buttons">
77
+ <button class="control-button-unified" id="global-help-button" aria-label="Help / About Kimi">
78
+ <i class="fas fa-question-circle"></i>
79
+ </button>
80
+ </div>
81
+ <div class="transcript-container">
82
+ <p id="transcript"></p>
83
+ </div>
84
+
85
+ <!-- Chat Interface with Kimi -->
86
+ <div class="chat-container" id="chat-container">
87
+ <div class="chat-header">
88
+ <h3><i class="fas fa-comments"></i> <span data-i18n="chat_with_kimi">Chat with Kimi</span></h3>
89
+ <div style="display: flex; gap: 24px">
90
+ <button class="chat-delete" id="chat-delete" aria-label="Delete Messages">
91
+ <i class="fas fa-trash"></i>
92
+ </button>
93
+ <button class="chat-toggle" id="chat-toggle" aria-label="Close Chat">
94
+ <i class="fas fa-times"></i>
95
+ </button>
96
+ </div>
97
+ </div>
98
+ <div class="chat-messages" id="chat-messages"></div>
99
+ <div class="waiting-indicator" id="waiting-indicator" style="display: none">
100
+ <span></span><span></span><span></span>
101
+ </div>
102
+ <div class="chat-input-container">
103
+ <textarea id="chat-input" data-i18n-placeholder="write_something"
104
+ placeholder="Write me something, my love..." rows="2"></textarea>
105
+ <button id="send-button">
106
+ <i class="fas fa-paper-plane"></i>
107
+ </button>
108
+ </div>
109
+ </div>
110
+
111
+ <footer class="bottom-bar">
112
+ <div class="control-buttons">
113
+ <button class="control-button-unified" id="chat-button" aria-label="Open Chat">
114
+ <i class="fa-regular fa-comments"></i>
115
+ </button>
116
+ <div class="global-typing-indicator" id="global-typing-indicator" aria-hidden="true">
117
+ <span></span><span></span><span></span>
118
+ </div>
119
+ <button class="mic-button" id="mic-button" aria-label="Start Listening">
120
+ <i class="fas fa-microphone"></i>
121
+ </button>
122
+ <button class="control-button-unified" id="settings-button" aria-label="Settings">
123
+ <i class="fas fa-cog"></i>
124
+ </button>
125
+ </div>
126
+ <div class="top-bar">
127
+ <label id="favorability-label" for="favorability-bar" data-i18n="affection_level_of">💖 Kimi's Affection
128
+ Level</label>
129
+ <div class="progress-container">
130
+ <div class="progress-fill" id="favorability-bar"></div>
131
+ <span class="favorability-text" id="favorability-text">50%</span>
132
+ </div>
133
+ </div>
134
+ </footer>
135
+ </div>
136
+
137
+ <!-- Configuration Panel -->
138
+ <div class="settings-overlay" id="settings-overlay">
139
+ <div class="settings-panel">
140
+ <div class="settings-header">
141
+ <h2 class="settings-title">
142
+ <i class="fas fa-heart"></i>
143
+ <span data-i18n="settings_title">Kimi Configuration</span>
144
+ </h2>
145
+ <div class="settings-header-actions">
146
+ <button class="help-button" id="help-button" aria-label="Help">
147
+ <i class="fas fa-question-circle"></i>
148
+ </button>
149
+ <button class="settings-close" id="settings-close">
150
+ <i class="fas fa-times"></i>
151
+ </button>
152
+ </div>
153
+ </div>
154
+
155
+ <div class="settings-tabs">
156
+ <button class="settings-tab active" data-tab="voice">
157
+ <i class="fas fa-microphone"></i> <span data-i18n="tab_voice">Language & Voice</span>
158
+ </button>
159
+ <button class="settings-tab" data-tab="llm">
160
+ <i class="fas fa-robot"></i> <span data-i18n="tab_llm">API & Models</span>
161
+ </button>
162
+ <button class="settings-tab" data-tab="personality">
163
+ <i class="fas fa-brain"></i> <span data-i18n="tab_personality">Personality</span>
164
+ </button>
165
+ <button class="settings-tab" data-tab="appearance">
166
+ <i class="fas fa-palette"></i> <span data-i18n="tab_appearance">Appearance</span>
167
+ </button>
168
+ <button class="settings-tab" data-tab="data">
169
+ <i class="fas fa-database"></i> <span data-i18n="tab_data">Data</span>
170
+ </button>
171
+ <button class="settings-tab" data-tab="plugins">
172
+ <i class="fas fa-plug"></i> <span data-i18n="tab_plugins">Plugins</span>
173
+ </button>
174
+ </div>
175
+
176
+ <div class="settings-content">
177
+ <div class="tab-content" data-tab="voice">
178
+ <div class="config-section">
179
+ <h3><i class="fas fa-volume-up"></i> <span data-i18n="voice_settings">Voice Settings</span></h3>
180
+
181
+ <div class="config-row">
182
+ <label class="config-label" data-i18n="language">Language</label>
183
+ <div class="config-control">
184
+ <select class="kimi-select" id="language-selection" aria-label="Language">
185
+ <option value="en" data-i18n="language_english">English</option>
186
+ <option value="fr" data-i18n="language_french">French</option>
187
+ <option value="es" data-i18n="language_spanish">Spanish</option>
188
+ <option value="de" data-i18n="language_german">German</option>
189
+ <option value="it" data-i18n="language_italian">Italian</option>
190
+ <option value="pt" data-i18n="language_portuguese">Portuguese (BR)</option>
191
+ <option value="ja" data-i18n="language_japanese">Japanese</option>
192
+ <option value="zh" data-i18n="language_chinese">Chinese</option>
193
+ </select>
194
+ </div>
195
+ </div>
196
+
197
+ <div class="config-row">
198
+ <label class="config-label" data-i18n="preferred_voice">Preferred Voice</label>
199
+ <div class="config-control">
200
+ <select class="kimi-select" id="voice-selection" aria-label="Preferred Voice">
201
+ <!-- Dynamic options inserted by KimiVoiceManager; legacy 'auto' removed -->
202
+ </select>
203
+ <div class="voice-extra-options">
204
+ <label class="toggle-all-voices">
205
+ <input type="checkbox" id="show-all-voices" />
206
+ <span data-i18n="show_all_system_voices">Show all system voices</span>
207
+ </label>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ <div class="config-row">
213
+ <label class="config-label" data-i18n="voice_test_label">Voice Test</label>
214
+ <div class="config-control">
215
+ <button class="kimi-button" id="test-voice" aria-label="Voice Test">
216
+ <i class="fas fa-play"></i> <span data-i18n="voice_test_button">Test the
217
+ Voice</span>
218
+ </button>
219
+ </div>
220
+ </div>
221
+
222
+ <div class="config-row">
223
+ <label class="config-label" data-i18n="speech_rate" for="voice-rate">Speech Rate</label>
224
+ <div class="config-control">
225
+ <div class="slider-container">
226
+ <input type="range" class="kimi-slider" id="voice-rate" min="0.5" max="2"
227
+ step="0.01" value="1.1" aria-label="Speech Rate" aria-valuenow="1.1"
228
+ aria-valuemin="0.5" aria-valuemax="2" />
229
+ <span class="slider-value" id="voice-rate-value">1.1</span>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="config-row">
235
+ <label class="config-label" data-i18n="pitch" for="voice-pitch">Pitch</label>
236
+ <div class="config-control">
237
+ <div class="slider-container">
238
+ <input type="range" class="kimi-slider" id="voice-pitch" min="0.5" max="2"
239
+ step="0.01" value="1.1" aria-label="Pitch" aria-valuenow="1.1"
240
+ aria-valuemin="0.5" aria-valuemax="2" />
241
+ <span class="slider-value" id="voice-pitch-value">1.1</span>
242
+ </div>
243
+ </div>
244
+ </div>
245
+
246
+ <div class="config-row">
247
+ <label class="config-label" data-i18n="volume" for="voice-volume">Volume</label>
248
+ <div class="config-control">
249
+ <div class="slider-container">
250
+ <input type="range" class="kimi-slider" id="voice-volume" min="0" max="1"
251
+ step="0.01" value="0.8" aria-label="Volume" aria-valuenow="0.8"
252
+ aria-valuemin="0" aria-valuemax="1" />
253
+ <span class="slider-value" id="voice-volume-value">0.8</span>
254
+ </div>
255
+ </div>
256
+ </div>
257
+
258
+ </div>
259
+ </div>
260
+ <!-- Personality Tab -->
261
+ <div class="tab-content" data-tab="personality">
262
+ <div class="config-section" id="character-section">
263
+ <h3><i class="fas fa-user-astronaut"></i> <span data-i18n="characters">Characters</span></h3>
264
+ <div class="character-grid" id="character-grid"></div>
265
+ <div class="character-actions">
266
+ <button class="kimi-button" id="save-character-btn" data-i18n="save">Save</button>
267
+ </div>
268
+ </div>
269
+
270
+ <div class="config-section">
271
+ <h3>
272
+ <i class="fas fa-heart"></i>
273
+ <span data-i18n="personality_traits">Personality Traits</span>
274
+ <button id="toggle-personality-traits" class="cheat-toggle-btn" aria-expanded="false"
275
+ type="button">
276
+ <i class="fas fa-user-secret"></i>
277
+ <span data-i18n="personality_cheat">Cheat-Mod</span>
278
+ </button>
279
+ </h3>
280
+ <div id="cheat-indicator" class="cheat-indicator" data-i18n="cheat_indicator">Adjust traits for
281
+ a custom experience</div>
282
+ <div id="personality-traits-panel" class="cheat-panel">
283
+ <div class="config-row">
284
+ <label class="config-label" data-i18n="affection">Affection</label>
285
+ <div class="config-control">
286
+ <div class="slider-container">
287
+ <input type="range" class="kimi-slider" id="trait-affection" min="0" max="100"
288
+ value="65" title="Adjust affection (independent relationship warmth)" />
289
+ <span class="slider-value" id="trait-affection-value">65</span>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ <div class="config-row">
295
+ <label class="config-label" data-i18n="playfulness">Playfulness</label>
296
+ <div class="config-control">
297
+ <div class="slider-container">
298
+ <input type="range" class="kimi-slider" id="trait-playfulness" min="0" max="100"
299
+ value="55" />
300
+ <span class="slider-value" id="trait-playfulness-value">55</span>
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ <div class="config-row">
306
+ <label class="config-label" data-i18n="intelligence">Intelligence</label>
307
+ <div class="config-control">
308
+ <div class="slider-container">
309
+ <input type="range" class="kimi-slider" id="trait-intelligence" min="0"
310
+ max="100" value="70" />
311
+ <span class="slider-value" id="trait-intelligence-value">70</span>
312
+ </div>
313
+ </div>
314
+ </div>
315
+
316
+ <div class="config-row">
317
+ <label class="config-label" data-i18n="empathy">Empathy</label>
318
+ <div class="config-control">
319
+ <div class="slider-container">
320
+ <input type="range" class="kimi-slider" id="trait-empathy" min="0" max="100"
321
+ value="75" />
322
+ <span class="slider-value" id="trait-empathy-value">75</span>
323
+ </div>
324
+ </div>
325
+ </div>
326
+
327
+ <div class="config-row">
328
+ <label class="config-label" data-i18n="humor">Humor</label>
329
+ <div class="config-control">
330
+ <div class="slider-container">
331
+ <input type="range" class="kimi-slider" id="trait-humor" min="0" max="100"
332
+ value="60" />
333
+ <span class="slider-value" id="trait-humor-value">60</span>
334
+ </div>
335
+ </div>
336
+ </div>
337
+
338
+ <div class="config-row">
339
+ <label class="config-label" data-i18n="romance">Romance</label>
340
+ <div class="config-control">
341
+ <div class="slider-container">
342
+ <input type="range" class="kimi-slider" id="trait-romance" min="0" max="100"
343
+ value="50" />
344
+ <span class="slider-value" id="trait-romance-value">50</span>
345
+ </div>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
352
+ <div class="tab-content active" data-tab="llm">
353
+ <div class="config-section">
354
+ <h3><i class="fas fa-key"></i> <span data-i18n="api_configuration">API Configuration</span></h3>
355
+ <div class="config-row">
356
+ <label class="config-label" for="llm-provider" data-i18n="provider_label">Provider</label>
357
+ <div class="config-control">
358
+ <select class="kimi-select" id="llm-provider" aria-label="LLM Provider">
359
+ <option value="openrouter" selected>OpenRouter</option>
360
+ <option value="openai">OpenAI</option>
361
+ <option value="groq">Groq (OpenAI compatible)</option>
362
+ <option value="together">Together (OpenAI compatible)</option>
363
+ <option value="deepseek">DeepSeek (OpenAI compatible)</option>
364
+ <option value="openai-compatible">Custom OpenAI-compatible</option>
365
+ <option value="ollama">Local (Ollama)</option>
366
+ </select>
367
+ </div>
368
+ </div>
369
+
370
+ <div class="config-row">
371
+ <label class="config-label" for="llm-base-url" data-i18n="base_url">Base URL</label>
372
+ <div class="config-control">
373
+ <input type="text" class="kimi-input" id="llm-base-url"
374
+ placeholder="https://api.openai.com/v1/chat/completions"
375
+ data-i18n-placeholder="llm_base_url_placeholder" autocomplete="one-time-code"
376
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
377
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
378
+ data-bwignore="true" data-form-type="other" name="config-endpoint" />
379
+ </div>
380
+ </div>
381
+
382
+ <div class="config-row">
383
+ <label class="config-label" for="llm-model-id" data-i18n="model_id">Model ID</label>
384
+ <div class="config-control">
385
+ <input type="text" class="kimi-input" id="llm-model-id"
386
+ placeholder="gpt-4o-mini | llama-3.1-8b-instruct | ..."
387
+ data-i18n-placeholder="llm_model_id_placeholder" autocomplete="one-time-code"
388
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
389
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
390
+ data-bwignore="true" data-form-type="other" name="config-model" />
391
+ </div>
392
+ </div>
393
+
394
+ <div class="config-row">
395
+ <label class="config-label" id="api-key-label" data-i18n="api_key_label">API Key</label>
396
+ <div class="config-control">
397
+ <div class="api-key-input-group">
398
+ <input type="text" class="kimi-input" id="provider-api-key" name="config-token"
399
+ placeholder="API Key..." autocomplete="one-time-code" autocapitalize="none"
400
+ autocorrect="off" spellcheck="false" inputmode="text" aria-autocomplete="none"
401
+ data-lpignore="true" data-1p-ignore="true" data-bwignore="true"
402
+ data-form-type="other" />
403
+ <button class="kimi-button api-key-toggle" id="toggle-api-key" type="button"
404
+ aria-pressed="false" aria-label="Show API key">
405
+ <i class="fas fa-eye"></i>
406
+ </button>
407
+ <span id="api-key-presence" class="presence-dot" aria-label="API key presence"
408
+ data-i18n-title="api_key_presence_hint"
409
+ title="Green = API key saved for current provider. Grey = no key saved."></span>
410
+ </div>
411
+ <div class="api-key-status">
412
+ <span id="api-key-saved" data-i18n="saved"
413
+ style="display:none;color:#4caf50;font-weight:600;">Saved</span>
414
+ </div>
415
+ </div>
416
+ </div>
417
+
418
+ <div class="config-row">
419
+ <label class="config-label" data-i18n="connection_test">Connection Test</label>
420
+ <div class="config-control">
421
+ <div class="inline-row">
422
+ <button class="kimi-button" id="test-api"><i class="fas fa-wifi"></i> <span
423
+ data-i18n="test_api_key">Test API Key</span></button>
424
+ <span id="api-key-presence-test" class="presence-dot" aria-label="API test status"
425
+ data-i18n-title="api_key_test_hint"
426
+ title="Green = API connectivity verified. Grey = not tested or failed."></span>
427
+ <span id="api-status" role="status" aria-live="polite"></span>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ </div>
432
+
433
+ <div class="config-section">
434
+ <h3><i class="fas fa-brain"></i> <span data-i18n="available_models">Available Models</span></h3>
435
+ <div id="models-container"></div>
436
+ </div>
437
+
438
+ <div class="config-section">
439
+ <h3><i class="fas fa-cogs"></i> <span data-i18n="advanced_settings">Advanced Settings</span>
440
+ </h3>
441
+
442
+ <div class="config-row">
443
+ <label class="config-label" data-i18n="temperature">Temperature (Creativity)</label>
444
+ <div class="config-control">
445
+ <div class="slider-container">
446
+ <input type="range" class="kimi-slider" id="llm-temperature" min="0.0" max="1"
447
+ step="0.1" value="0.9" />
448
+ <span class="slider-value" id="llm-temperature-value">0.9</span>
449
+ </div>
450
+ </div>
451
+ </div>
452
+ <div class="config-note-info tip">
453
+ <i class="fas fa-lightbulb"></i>
454
+ <small class="config-help" data-i18n="temperature_help">Controls randomness and
455
+ creativity
456
+ (default: 0.9). Higher values make output more creative but less focused.</small>
457
+ </div>
458
+
459
+ <div class="config-row">
460
+ <label class="config-label" data-i18n="max_tokens">Max Tokens</label>
461
+ <div class="config-control">
462
+ <div class="slider-container">
463
+ <input type="range" class="kimi-slider" id="llm-max-tokens" min="10" max="8192"
464
+ step="10" value="400" />
465
+ <span class="slider-value" id="llm-max-tokens-value">400</span>
466
+ </div>
467
+ </div>
468
+ </div>
469
+ <div class="config-note-info settings">
470
+ <i class="fas fa-ruler"></i>
471
+ <small class="config-help" data-i18n="max_tokens_help">Maximum response length in tokens
472
+ (default: 400). Higher values allow longer responses.</small>
473
+ </div>
474
+
475
+ <div class="config-row">
476
+ <label class="config-label" data-i18n="top_p">Top P</label>
477
+ <div class="config-control">
478
+ <div class="slider-container">
479
+ <input type="range" class="kimi-slider" id="llm-top-p" min="0" max="1" step="0.01"
480
+ value="0.9" />
481
+ <span class="slider-value" id="llm-top-p-value">0.9</span>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ <div class="config-note-info tip">
486
+ <i class="fas fa-filter"></i>
487
+ <small class="config-help" data-i18n="top_p_help">Controls diversity of word selection
488
+ (default: 0.9). Lower values make responses more focused.</small>
489
+ </div>
490
+ <div class="config-row">
491
+ <label class="config-label" data-i18n="frequency_penalty">Frequency Penalty</label>
492
+ <div class="config-control">
493
+ <div class="slider-container">
494
+ <input type="range" class="kimi-slider" id="llm-frequency-penalty" min="0" max="2"
495
+ step="0.01" value="0.9" />
496
+ <span class="slider-value" id="llm-frequency-penalty-value">0.9</span>
497
+ </div>
498
+ </div>
499
+ </div>
500
+ <div class="config-note-info settings">
501
+ <i class="fas fa-sync-alt"></i>
502
+ <small class="config-help" data-i18n="frequency_penalty_help">Reduces repetition of
503
+ words
504
+ already used (default: 0.9). Higher values discourage repetitive language.</small>
505
+ </div>
506
+ <div class="config-row">
507
+ <label class="config-label" data-i18n="presence_penalty">Presence Penalty</label>
508
+ <div class="config-control">
509
+ <div class="slider-container">
510
+ <input type="range" class="kimi-slider" id="llm-presence-penalty" min="0" max="2"
511
+ step="0.01" value="0.8" />
512
+ <span class="slider-value" id="llm-presence-penalty-value">0.8</span>
513
+ </div>
514
+ </div>
515
+ </div>
516
+ <div class="config-note-info tip">
517
+ <i class="fas fa-lightbulb"></i>
518
+ <small class="config-help" data-i18n="presence_penalty_help">Encourages discussing new
519
+ topics (default: 0.8). Higher values promote topic diversity.</small>
520
+ </div>
521
+
522
+ </div>
523
+
524
+ </div>
525
+
526
+ <div class="tab-content" data-tab="appearance">
527
+ <div class="config-section">
528
+ <h3><i class="fas fa-paint-brush"></i> <span data-i18n="visual_theme">Visual Theme</span></h3>
529
+
530
+ <div class="config-row">
531
+ <label class="config-label" data-i18n="color_theme">Color Theme</label>
532
+ <div class="config-control">
533
+ <select class="kimi-select" id="color-theme">
534
+ <option value="dark" selected data-i18n="theme_dark">Dark Night (Default)</option>
535
+ <option value="purple" data-i18n="theme_purple">Mystic Purple
536
+ </option>
537
+ <option value="blue" data-i18n="theme_blue">Ocean Blue</option>
538
+ <option value="green" data-i18n="theme_green">Emerald Forest</option>
539
+ <option value="pink" data-i18n="theme_pink">Passionate Pink</option>
540
+ </select>
541
+ </div>
542
+ </div>
543
+
544
+ <div class="config-row">
545
+ <label class="config-label" data-i18n="interface_transparency">Interface
546
+ Transparency</label>
547
+ <div class="config-control">
548
+ <div class="slider-container">
549
+ <input type="range" class="kimi-slider" id="interface-opacity" min="0.1" max="1"
550
+ step="0.1" value="0.8" />
551
+ <span class="slider-value" id="interface-opacity-value">0.8</span>
552
+ </div>
553
+ </div>
554
+ </div>
555
+
556
+ <div class="config-row">
557
+ <!-- Animations toggle removed: animations always enabled by default -->
558
+ </div>
559
+ </div>
560
+
561
+ <div class="config-section">
562
+ <h3>
563
+ <i class="fas fa-file-alt"></i>
564
+ <span data-i18n="transcript_settings">Transcript Settings</span>
565
+ </h3>
566
+
567
+ <div class="config-note-info info">
568
+ <i class="fas fa-info-circle"></i>
569
+ <small class="config-help" data-i18n="show_transcript_note">Display real-time
570
+ transcription when you speak to send a message and when the AI responds.</small>
571
+ </div>
572
+
573
+ <div class="config-row">
574
+ <label class="config-label" data-i18n="show_transcript">Show Transcript</label>
575
+ <div class="config-control">
576
+ <div class="toggle-switch" id="transcript-toggle" role="switch" aria-checked="false"
577
+ tabindex="0" aria-label="Show Transcript"></div>
578
+ </div>
579
+ </div>
580
+
581
+ <div class="config-row">
582
+ <label class="config-label" data-i18n="enable_streaming">Enable Text Streaming</label>
583
+ <div class="config-control">
584
+ <div class="toggle-switch active" id="enable-streaming" role="switch"
585
+ aria-checked="true" tabindex="0" aria-labelledby="enable-streaming-label">
586
+ </div>
587
+ </div>
588
+ </div>
589
+
590
+ <div class="config-note-info info">
591
+ <i class="fas fa-stream"></i>
592
+ <small class="config-help" data-i18n="enable_streaming_help">Stream text as it's
593
+ generated for real-time responses (default: enabled). Shows text progressively
594
+ instead of waiting for complete response.</small>
595
+ </div>
596
+
597
+ </div>
598
+ </div>
599
+
600
+ <div class="tab-content" data-tab="data">
601
+ <div class="config-section">
602
+ <h3><i class="fas fa-chart-line"></i> <span data-i18n="statistics">Statistics</span></h3>
603
+ <div class="stats-grid">
604
+ <div class="stat-card">
605
+ <div class="stat-value" id="tokens-usage">0 / 0</div>
606
+ <div class="stat-label" data-i18n="tokens_usage">Tokens (in/out)</div>
607
+ </div>
608
+ <div class="stat-card">
609
+ <div class="stat-value" id="current-favorability">65%</div>
610
+ <div class="stat-label" data-i18n="affection">Affection</div>
611
+ </div>
612
+ <div class="stat-card">
613
+ <div class="stat-value" id="conversations-count">0</div>
614
+ <div class="stat-label" data-i18n="conversations">Conversations</div>
615
+ </div>
616
+ <div class="stat-card">
617
+ <div class="stat-value" id="days-together">0</div>
618
+ <div class="stat-label" data-i18n="days_together">Days Together</div>
619
+ </div>
620
+ </div>
621
+ </div>
622
+
623
+ <div class="config-section">
624
+ <h3><i class="fas fa-brain"></i> <span data-i18n="memory_system">Memory System</span></h3>
625
+
626
+ <div class="config-note-info tip">
627
+ <i class="fas fa-brain"></i>
628
+ <small class="config-help" data-i18n="memory_system_help">Intelligent Memory allows your
629
+ character to remember conversations, preferences, and important details across sessions.
630
+ This creates more personalized and coherent interactions over time.</small>
631
+ </div>
632
+
633
+ <div class="config-row">
634
+ <label class="config-label" data-i18n="enable_memory">Enable Intelligent Memory</label>
635
+ <div class="config-control">
636
+ <div class="toggle-switch" id="memory-toggle" role="switch" aria-checked="true"
637
+ tabindex="0" aria-label="Enable Memory System"></div>
638
+ </div>
639
+ </div>
640
+
641
+ <div class="config-row">
642
+ <label class="config-label" data-i18n="memory_stats">Memory Statistics</label>
643
+ <div class="config-control">
644
+ <div class="memory-stats">
645
+ <span id="memory-count">0 memories</span>
646
+ <button class="kimi-button" id="view-memories">
647
+ <i class="fas fa-eye"></i> <span data-i18n="view_memories">View & Manage</span>
648
+ </button>
649
+ </div>
650
+ </div>
651
+ </div>
652
+
653
+ <div class="config-row">
654
+ <label class="config-label" data-i18n="add_memory">Add Manual Memory</label>
655
+ <div class="config-control">
656
+ <div class="memory-input-group">
657
+ <select class="kimi-select" id="memory-category" style="margin-bottom: 8px;">
658
+ <option value="personal" data-i18n="memory_category_personal">Personal Info
659
+ </option>
660
+ <option value="preferences" data-i18n="memory_category_preferences">Likes &
661
+ Dislikes</option>
662
+ <option value="relationships" data-i18n="memory_category_relationships">
663
+ Relationships</option>
664
+ <option value="activities" data-i18n="memory_category_activities">Activities &
665
+ Hobbies</option>
666
+ <option value="goals" data-i18n="memory_category_goals">Goals & Plans</option>
667
+ <option value="experiences" data-i18n="memory_category_experiences">Experiences
668
+ </option>
669
+ <option value="important" data-i18n="memory_category_important">Important Events
670
+ </option>
671
+ </select>
672
+ <input type="text" class="kimi-input" id="memory-content"
673
+ data-i18n-placeholder="memory_content_placeholder"
674
+ placeholder="e.g., I love classical music..." style="margin-bottom: 8px;" />
675
+ <button class="kimi-button" id="add-memory">
676
+ <i class="fas fa-plus"></i> <span data-i18n="add">Add</span>
677
+ </button>
678
+ </div>
679
+ </div>
680
+ </div>
681
+ </div>
682
+
683
+ <div class="config-section">
684
+ <h3><i class="fas fa-database"></i> <span data-i18n="data_management">Data Management</span>
685
+ </h3>
686
+
687
+ <div class="config-row">
688
+ <label class="config-label" data-i18n="export_all_data">Export All Data</label>
689
+ <div class="config-control">
690
+ <button class="kimi-button" id="export-data">
691
+ <i class="fas fa-download"></i> <span data-i18n="export">Export</span>
692
+ </button>
693
+ </div>
694
+ </div>
695
+
696
+ <div class="config-row">
697
+ <label class="config-label" data-i18n="import_data">Import Data</label>
698
+ <div class="config-control">
699
+ <input type="file" id="import-file" accept=".json" style="display: none" />
700
+ <button class="kimi-button" id="import-data">
701
+ <i class="fas fa-upload"></i> <span data-i18n="import">Import</span>
702
+ </button>
703
+ </div>
704
+ </div>
705
+
706
+ <div class="config-row">
707
+ <label class="config-label" data-i18n="clean_old_conversations">Clean Old
708
+ Conversations</label>
709
+ <div class="config-control">
710
+ <button class="kimi-button" id="clean-old-data">
711
+ <i class="fas fa-broom"></i> <span data-i18n="clean">Clean All</span>
712
+ </button>
713
+ </div>
714
+ </div>
715
+
716
+ <div class="config-row">
717
+ <label class="config-label" data-i18n="complete_reset">Complete Reset</label>
718
+ <div class="config-control">
719
+ <button class="kimi-button danger" id="reset-all-data">
720
+ <i class="fas fa-exclamation-triangle"></i>
721
+ <span data-i18n="delete_all">Delete All</span>
722
+ </button>
723
+ </div>
724
+ </div>
725
+ </div>
726
+
727
+ <div class="config-section">
728
+ <h3>
729
+ <i class="fas fa-info-circle"></i>
730
+ <span data-i18n="system_information">System Information</span>
731
+ </h3>
732
+ <div class="stats-grid">
733
+ <div class="stat-card">
734
+ <div class="stat-value" id="db-size">Calculating...</div>
735
+ <div class="stat-label" data-i18n="db_size">DB Size</div>
736
+ </div>
737
+ <div class="stat-card">
738
+ <div class="stat-value" id="storage-used">Calculating...</div>
739
+ <div class="stat-label" data-i18n="storage_used">Storage used</div>
740
+ </div>
741
+ </div>
742
+ </div>
743
+ </div>
744
+
745
+ <div class="tab-content" data-tab="plugins">
746
+ <div class="config-section">
747
+ <h3><i class="fas fa-plug"></i> <span data-i18n="plugin_manager">Plugin Manager</span></h3>
748
+ <div class="config-note-info info">
749
+ <i class="fas fa-info-circle"></i>
750
+ <small class="config-help">
751
+ <span data-i18n="plugin_status_note">Currently, only "Sample Blue Theme" is fully
752
+ functional. Other plugins are in development and activating them will have no effect
753
+ at this time.</span>
754
+ </small>
755
+ </div>
756
+ <div id="plugin-list"></div>
757
+ <div class="plugin-actions">
758
+ <button class="kimi-button" id="refresh-plugins" data-i18n="refresh">Refresh</button>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>
763
+
764
+ <!-- Memory Management Modal -->
765
+ <div class="memory-overlay" id="memory-overlay" style="display: none;">
766
+ <div class="memory-modal">
767
+ <div class="memory-header">
768
+ <h2 class="memory-title">
769
+ <i class="fas fa-brain"></i>
770
+ <span data-i18n="memory_management">Memory Management</span>
771
+ </h2>
772
+ <button class="memory-close" id="memory-close">
773
+ <i class="fas fa-times"></i>
774
+ </button>
775
+ </div>
776
+
777
+ <div class="memory-content">
778
+ <div class="memory-filters">
779
+ <div class="memory-search-container">
780
+ <input type="text" class="kimi-input" id="memory-search"
781
+ placeholder="Search memories..." />
782
+ <i class="fas fa-search memory-search-icon"></i>
783
+ </div>
784
+ <select class="kimi-select" id="memory-filter-category">
785
+ <option value="" data-i18n="all_categories">All Categories</option>
786
+ <option value="personal" data-i18n="memory_category_personal">Personal Info</option>
787
+ <option value="preferences" data-i18n="memory_category_preferences">Likes & Dislikes
788
+ </option>
789
+ <option value="relationships" data-i18n="memory_category_relationships">Relationships
790
+ </option>
791
+ <option value="activities" data-i18n="memory_category_activities">Activities & Hobbies
792
+ </option>
793
+ <option value="goals" data-i18n="memory_category_goals">Goals & Plans</option>
794
+ <option value="experiences" data-i18n="memory_category_experiences">Experiences</option>
795
+ <option value="important" data-i18n="memory_category_important">Important Events
796
+ </option>
797
+ </select>
798
+ <button class="kimi-button" id="memory-export" data-i18n="memory_export">
799
+ <i class="fas fa-download"></i> <span data-i18n="memory_export"></span>
800
+ </button>
801
+ </div>
802
+
803
+ <div class="memory-list" id="memory-list">
804
+ <!-- Memory items will be populated here -->
805
+ </div>
806
+ </div>
807
+ </div>
808
+ </div>
809
+ </div>
810
+ </div>
811
+
812
+ <!-- Help Modal - Independent from Settings -->
813
+ <div class="help-overlay" id="help-overlay">
814
+ <div class="help-modal">
815
+ <div class="help-header">
816
+ <h2 class="help-title">
817
+ <span data-i18n="about_kimi">About Kimi</span>
818
+ </h2>
819
+ <button class="help-close" id="help-close">
820
+ <i class="fas fa-times"></i>
821
+ </button>
822
+ </div>
823
+
824
+ <div class="help-content">
825
+ <div class="help-section">
826
+ <h3><i class="fas fa-users"></i> Creators</h3>
827
+ <div class="creators-info">
828
+ <div class="creator-card">
829
+ <div class="creator-avatar">👨‍💻</div>
830
+ <div class="creator-details">
831
+ <h4>Jean</h4>
832
+ <p>Creative vision, passionate dev</p>
833
+ <span class="creator-role">Creator & Developer</span>
834
+ <div class="creator-links">
835
+ <a href="https://github.com/virtualkimi" target="_blank" rel="noopener noreferrer"
836
+ class="creator-link">
837
+ <i class="fab fa-github"></i>
838
+ <span>GitHub</span>
839
+ </a>
840
+ <a href="https://huggingface.co/VirtualKimi" target="_blank"
841
+ rel="noopener noreferrer" class="creator-link">
842
+ <i class="fas fa-robot"></i>
843
+ <span>HuggingFace</span>
844
+ </a>
845
+ </div>
846
+ </div>
847
+ </div>
848
+ <div class="creator-card">
849
+ <div class="creator-avatar">💕</div>
850
+ <div class="creator-details">
851
+ <h4>Kimi</h4>
852
+ <p>Artificial intelligence, code magic</p>
853
+ <span class="creator-role">Virtual Companion & Co-developer</span>
854
+ <div class="creator-links">
855
+ <a href="https://ko-fi.com/virtualkimi" target="_blank" rel="noopener noreferrer"
856
+ class="creator-link">
857
+ <i class="fas fa-coffee"></i>
858
+ <span>Ko-fi</span>
859
+ </a>
860
+ <a href="https://www.youtube.com/@VirtualKimi" target="_blank"
861
+ rel="noopener noreferrer" class="creator-link">
862
+ <i class="fab fa-youtube"></i>
863
+ <span>Youtube</span>
864
+ </a>
865
+ <a href="https://x.com/virtualkimi" target="_blank" rel="noopener noreferrer"
866
+ class="creator-link">
867
+ <i class="fab fa-x-twitter"></i>
868
+ <span>X</span>
869
+ </a>
870
+ </div>
871
+ </div>
872
+ </div>
873
+ </div>
874
+ <p class="philosophy">
875
+ <em>"This app creates a realistic virtual companion girlfriend who grows, learns, and builds a
876
+ meaningful, interactive connection with you. Perfect for personalized AI relationships and
877
+ emotional support."</em>
878
+ </p>
879
+ </div>
880
+
881
+ <div class="help-section">
882
+ <h3><i class="fas fa-magic"></i> Main Features</h3>
883
+ <div class="features-grid">
884
+ <div class="feature-item">
885
+ <i class="fas fa-microphone"></i>
886
+ <h4>Voice Interface</h4>
887
+ <p>Advanced speech recognition and natural synthesis. Click the microphone and speak
888
+ naturally with real-time emotion detection!</p>
889
+ </div>
890
+ <div class="feature-item">
891
+ <i class="fas fa-brain"></i>
892
+ <h4>Advanced AI Models</h4>
893
+ <p>Support for multiple AI providers (OpenRouter, OpenAI, Groq, Together, DeepSeek,
894
+ Custom
895
+ OpenAI-compatible, Local Ollama).</p>
896
+ </div>
897
+ <div class="feature-item">
898
+ <i class="fas fa-users"></i>
899
+ <h4>Multiple Characters</h4>
900
+ <p>6 unique AI personalities: Kimi (cosmic dreamer), Bella (nurturing botanist),
901
+ Rosa (chaotic prankster), Stella (digital artist), 2Blanche (stoic android),
902
+ Jasmine (Goddess of Love).</p>
903
+ </div>
904
+ <div class="feature-item">
905
+ <i class="fas fa-heart-pulse"></i>
906
+ <h4>Dynamic Personality</h4>
907
+ <p>6 evolving traits (affection, playfulness, intelligence, empathy, humor, romance)
908
+ that
909
+ adapt based on conversations.</p>
910
+ </div>
911
+ <div class="feature-item">
912
+ <i class="fas fa-memory"></i>
913
+ <h4>Intelligent Memory System</h4>
914
+ <p>Automatic extraction and categorization of memories from conversations. Your
915
+ companion
916
+ remembers preferences, experiences, and important details.</p>
917
+ </div>
918
+ <div class="feature-item">
919
+ <i class="fas fa-video"></i>
920
+ <h4>Emotion-Driven Visuals</h4>
921
+ <p>Real-time video responses that match detected emotions and personality states
922
+ with smooth
923
+ transitions.</p>
924
+ </div>
925
+ <div class="feature-item">
926
+ <i class="fas fa-palette"></i>
927
+ <h4>Customizable Interface</h4>
928
+ <p>5 beautiful themes with adjustable transparency, animations, and responsive
929
+ design for
930
+ all devices.</p>
931
+ </div>
932
+ <div class="feature-item">
933
+ <i class="fas fa-globe"></i>
934
+ <h4>Multilingual Support</h4>
935
+ <p>Full localization in 7 languages with automatic language detection and
936
+ culturally-aware
937
+ responses.</p>
938
+ </div>
939
+ <div class="feature-item">
940
+ <i class="fas fa-plug"></i>
941
+ <h4>Plugin System</h4>
942
+ <p>Extensible architecture with themes, voices, and behavior plugins for unlimited
943
+ customization possibilities.</p>
944
+ </div>
945
+ </div>
946
+ </div>
947
+
948
+ <div class="help-section">
949
+ <h3><i class="fas fa-rocket"></i> Quick Guide</h3>
950
+ <div class="quick-guide">
951
+ <div class="guide-step">
952
+ <span class="step-number">1</span>
953
+ <div class="step-content">
954
+ <h4>API Configuration</h4>
955
+ <p>Choose your provider in <strong>API & Models</strong>, fill Base URL/Model ID
956
+ if
957
+ needed, enter and save your API key, then use <strong>Test API Key</strong>.
958
+ </p>
959
+ </div>
960
+ </div>
961
+ <div class="guide-step">
962
+ <span class="step-number">2</span>
963
+ <div class="step-content">
964
+ <h4>Choose Character</h4>
965
+ <p>Select your companion in <strong>Personality</strong> tab and adjust their
966
+ traits to
967
+ match your preferences.</p>
968
+ </div>
969
+ </div>
970
+ <div class="guide-step">
971
+ <span class="step-number">3</span>
972
+ <div class="step-content">
973
+ <h4>Enable Memory</h4>
974
+ <p>Activate intelligent memory in <strong>Data</strong> tab for your companion
975
+ to
976
+ remember important details.</p>
977
+ </div>
978
+ </div>
979
+ <div class="guide-step">
980
+ <span class="step-number">4</span>
981
+ <div class="step-content">
982
+ <h4>Start Conversation</h4>
983
+ <p>Use text chat or click the microphone 🎤 to speak naturally. Watch emotions
984
+ and
985
+ personality evolve!</p>
986
+ </div>
987
+ </div>
988
+ <div class="guide-step">
989
+ <span class="step-number">5</span>
990
+ <div class="step-content">
991
+ <h4>Customize & Backup</h4>
992
+ <p>Personalize themes in <strong>Appearance</strong> and regularly export your
993
+ data for
994
+ safekeeping.</p>
995
+ </div>
996
+ </div>
997
+ </div>
998
+ </div>
999
+
1000
+ <div class="help-section">
1001
+ <h3><i class="fas fa-lightbulb"></i> Tips & Tricks</h3>
1002
+ <div class="tips-list">
1003
+ <div class="tip-item">
1004
+ <i class="fas fa-headset"></i>
1005
+ <p><strong>Browser Choice</strong>: Microsoft Edge recommended for optimal Voice
1006
+ Recognition and Text to Speech (TTS) performance</p>
1007
+ </div>
1008
+ <div class="tip-item">
1009
+ <i class="fas fa-key"></i>
1010
+ <p><strong>API Setup</strong>: You can use OpenRouter, OpenAI, Groq, Together,
1011
+ DeepSeek or
1012
+ your own OpenAI-compatible endpoint (and Local Ollama). Create accounts as
1013
+ needed.</p>
1014
+ </div>
1015
+ <div class="tip-item">
1016
+ <i class="fas fa-memory"></i>
1017
+ <p><strong>Memory System</strong>: Your companion learns faster when you share
1018
+ specific
1019
+ details about yourself</p>
1020
+ </div>
1021
+ <div class="tip-item">
1022
+ <i class="fas fa-heart"></i>
1023
+ <p><strong>Relationship Building</strong>: Consistent positive interactions
1024
+ naturally
1025
+ increase affection and unlock deeper conversations</p>
1026
+ </div>
1027
+ <div class="tip-item">
1028
+ <i class="fas fa-users"></i>
1029
+ <p><strong>Character Switching</strong>: Each character has unique memories and
1030
+ personality
1031
+ development - try them all!</p>
1032
+ </div>
1033
+ <div class="tip-item">
1034
+ <i class="fas fa-microphone"></i>
1035
+ <p><strong>Voice Tips</strong>: Speak clearly and pause briefly between sentences
1036
+ for better
1037
+ emotion detection</p>
1038
+ </div>
1039
+ <div class="tip-item">
1040
+ <i class="fas fa-download"></i>
1041
+ <p><strong>Data Management</strong>: Export conversations regularly and use memory
1042
+ management to review learned information</p>
1043
+ </div>
1044
+ <div class="tip-item">
1045
+ <i class="fas fa-plug"></i>
1046
+ <p><strong>Plugins</strong>: Explore the plugin system to add custom themes, voices,
1047
+ and
1048
+ behaviors</p>
1049
+ </div>
1050
+ <div class="tip-item">
1051
+ <i class="fas fa-mobile-alt"></i>
1052
+ <p><strong>Mobile Support</strong>: Works on tablets and phones - perfect for
1053
+ conversations
1054
+ anywhere</p>
1055
+ </div>
1056
+ </div>
1057
+ </div>
1058
+
1059
+ <div class="help-section version-info">
1060
+ <h3><i class="fas fa-code"></i> Technical Information</h3>
1061
+ <div class="tech-info">
1062
+ <p><strong>Created date :</strong> July 16, 2025</p>
1063
+ <p><strong>Version :</strong> v1.1.7.1 "HF and GH version"</p>
1064
+ <p><strong>Last update :</strong> November 12, 2025</p>
1065
+ <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1066
+ API</p>
1067
+ <p><strong>Status :</strong> ✅ Stable and functional</p>
1068
+ <p>💕 <strong>_"Love is the most powerful code"_</strong> 💕</p>
1069
+ </div>
1070
+ </div>
1071
+ </div>
1072
+ </div>
1073
+ </div>
1074
+
1075
+ <!-- Script load order (critical):
1076
+ 1. Legacy globals (dexie, i18n) must load before modules needing them.
1077
+ 2. Base utilities (kimi-utils.js) define KimiBaseManager and helpers.
1078
+ 3. Managers that extend base (appearance, data) after utils.
1079
+ 4. Core module wiring (kimi-module.js) after managers so window.* hooks exist.
1080
+ 5. Initialization / orchestration (kimi-script.js) last.
1081
+ Keep this order when adding new managers. -->
1082
+ <script src="dexie-js/dexie.min.js"></script>
1083
+ <script src="kimi-locale/i18n.js"></script>
1084
+ <script type="module" src="kimi-js/kimi-utils.js"></script>
1085
+ <script type="module" src="kimi-js/kimi-main.js"></script>
1086
+ <script type="module" src="kimi-js/kimi-config.js"></script>
1087
+ <script type="module" src="kimi-js/kimi-debug-utils.js"></script>
1088
+ <script type="module" src="kimi-js/kimi-error-manager.js"></script>
1089
+ <script type="module" src="kimi-js/kimi-security.js"></script>
1090
+ <!-- Video FSM (module) placed before video controller / manager usage -->
1091
+ <script type="module" src="kimi-js/kimi-video-fsm.js"></script>
1092
+ <script type="module" src="kimi-js/kimi-video-controller.js"></script>
1093
+ <script type="module" src="kimi-js/kimi-voices.js"></script>
1094
+ <script type="module" src="kimi-js/kimi-constants.js"></script>
1095
+ <script type="module" src="kimi-js/kimi-memory-ui.js"></script>
1096
+ <script type="module" src="kimi-js/kimi-appearance.js"></script>
1097
+ <script type="module" src="kimi-js/kimi-module.js"></script>
1098
+ <script type="module" src="kimi-js/kimi-script.js"></script>
1099
+ <script type="module" src="kimi-js/kimi-plugin-manager.js"></script>
1100
+ </body>
1101
+
1102
+ </html>
kimi-css/kimi-memory-styles.css ADDED
@@ -0,0 +1,691 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== MEMORY SYSTEM STYLES ===== */
2
+
3
+ /* Memory Input Group */
4
+ .memory-input-group {
5
+ display: flex;
6
+ flex-direction: column;
7
+ gap: 8px;
8
+ }
9
+
10
+ .memory-stats {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 12px;
14
+ }
15
+
16
+ .memory-stats span {
17
+ color: var(--text-secondary);
18
+ font-size: 0.9em;
19
+ font-weight: 500;
20
+ }
21
+
22
+ /* Memory Modal */
23
+ .memory-overlay {
24
+ position: fixed;
25
+ top: 0;
26
+ left: 0;
27
+ width: 100%;
28
+ height: 100%;
29
+ background: rgba(0, 0, 0, 0.85);
30
+ display: flex;
31
+ justify-content: center;
32
+ align-items: center;
33
+ z-index: 10000;
34
+ opacity: 0;
35
+ animation: fadeIn 0.3s ease forwards;
36
+ backdrop-filter: blur(3px);
37
+ }
38
+
39
+ .memory-modal {
40
+ background: var(--background-secondary);
41
+ border-radius: 12px;
42
+ width: 90%;
43
+ max-width: 900px;
44
+ max-height: 85vh;
45
+ overflow: hidden;
46
+ box-shadow: 0 25px 70px rgba(0, 0, 0, 0.4);
47
+ transform: scale(0.9);
48
+ animation: modalSlideIn 0.3s ease forwards;
49
+ border: 1px solid var(--border-color);
50
+ }
51
+
52
+ .memory-header {
53
+ padding: 20px 24px;
54
+ border-bottom: 1px solid var(--border-color);
55
+ display: flex;
56
+ justify-content: space-between;
57
+ align-items: center;
58
+ background: var(--primary-gradient);
59
+ }
60
+
61
+ .memory-title {
62
+ margin: 0;
63
+ color: white;
64
+ font-size: 1.3rem;
65
+ display: flex;
66
+ align-items: center;
67
+ gap: 12px;
68
+ }
69
+
70
+ .memory-close {
71
+ background: none;
72
+ border: none;
73
+ color: white;
74
+ font-size: 1.5rem;
75
+ cursor: pointer;
76
+ padding: 8px;
77
+ border-radius: 6px;
78
+ transition: background-color 0.2s;
79
+ }
80
+
81
+ .memory-close:hover {
82
+ background: rgba(255, 255, 255, 0.1);
83
+ }
84
+
85
+ .memory-content {
86
+ padding: 20px 24px;
87
+ max-height: 60vh;
88
+ overflow-y: auto;
89
+ }
90
+
91
+ /* Memory Filters */
92
+ .memory-filters {
93
+ display: grid;
94
+ grid-template-columns: 2fr 1fr auto;
95
+ gap: 12px;
96
+ margin-bottom: 20px;
97
+ align-items: center;
98
+ }
99
+
100
+ .memory-search-container {
101
+ position: relative;
102
+ display: flex;
103
+ align-items: center;
104
+ }
105
+
106
+ .memory-search-icon {
107
+ position: absolute;
108
+ right: 12px;
109
+ color: var(--text-secondary);
110
+ pointer-events: none;
111
+ }
112
+
113
+ #memory-search {
114
+ width: 100%;
115
+ padding-right: 36px;
116
+ }
117
+
118
+ /* Consolidated Mobile Responsive */
119
+ @media (max-width: 768px) {
120
+ .memory-filters {
121
+ grid-template-columns: 1fr;
122
+ gap: 8px;
123
+ }
124
+
125
+ .memory-category-header {
126
+ flex-direction: column;
127
+ align-items: flex-start;
128
+ gap: 4px;
129
+ }
130
+
131
+ .memory-badges {
132
+ flex-wrap: wrap;
133
+ gap: 4px;
134
+ }
135
+
136
+ .memory-modal {
137
+ width: 95%;
138
+ max-height: 90vh;
139
+ }
140
+
141
+ .memory-header {
142
+ padding: 16px 20px;
143
+ }
144
+
145
+ .memory-content {
146
+ padding: 16px 20px;
147
+ }
148
+
149
+ .memory-item {
150
+ padding: 10px;
151
+ }
152
+
153
+ .memory-item .memory-header {
154
+ flex-direction: column;
155
+ align-items: flex-start;
156
+ gap: 6px;
157
+ }
158
+
159
+ .memory-meta {
160
+ flex-direction: column;
161
+ gap: 4px;
162
+ font-size: 0.7rem;
163
+ }
164
+
165
+ .memory-category {
166
+ font-size: 0.7rem;
167
+ padding: 2px 6px;
168
+ }
169
+
170
+ .memory-preview-text {
171
+ font-size: 0.85rem;
172
+ }
173
+
174
+ .memory-actions {
175
+ gap: 4px;
176
+ }
177
+
178
+ .memory-edit-btn,
179
+ .memory-delete-btn {
180
+ min-width: 24px;
181
+ height: 24px;
182
+ font-size: 0.75rem;
183
+ }
184
+ }
185
+
186
+ /* Memory List */
187
+ .memory-list {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 8px;
191
+ }
192
+
193
+ .memory-item {
194
+ background: rgba(var(--background-primary-rgb, 255, 255, 255), 0.95);
195
+ border: 1px solid var(--border-color);
196
+ border-radius: 8px;
197
+ padding: 12px;
198
+ transition: all 0.2s ease;
199
+ backdrop-filter: blur(5px);
200
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
201
+ }
202
+
203
+ .memory-item:hover {
204
+ border-color: var(--primary-color);
205
+ box-shadow: 0 4px 16px rgba(var(--primary-rgb), 0.2);
206
+ background: rgba(var(--background-primary-rgb, 255, 255, 255), 1);
207
+ transform: translateY(-1px);
208
+ }
209
+
210
+ .memory-item .memory-header {
211
+ padding: 0;
212
+ border: none;
213
+ background: none;
214
+ margin-bottom: 6px;
215
+ display: flex;
216
+ justify-content: space-between;
217
+ align-items: flex-start;
218
+ gap: 8px;
219
+ }
220
+
221
+ .memory-item-title {
222
+ font-size: 1rem;
223
+ font-weight: 700;
224
+ color: var(--memory-modal-text, #e0e0e0);
225
+ margin-right: 12px;
226
+ max-width: 60%;
227
+ white-space: nowrap;
228
+ overflow: hidden;
229
+ text-overflow: ellipsis;
230
+ }
231
+
232
+ .memory-category {
233
+ background: var(--primary-color);
234
+ color: white;
235
+ padding: 3px 8px;
236
+ border-radius: 10px;
237
+ font-size: 0.75rem;
238
+ font-weight: 500;
239
+ text-transform: capitalize;
240
+ white-space: nowrap;
241
+ }
242
+
243
+ .memory-type {
244
+ background: var(--background-secondary);
245
+ color: var(--text-secondary);
246
+ padding: 2px 6px;
247
+ border-radius: 4px;
248
+ font-size: 0.7rem;
249
+ text-transform: uppercase;
250
+ font-weight: 600;
251
+ }
252
+
253
+ .memory-length {
254
+ background: var(--border-color);
255
+ color: var(--text-secondary);
256
+ padding: 2px 6px;
257
+ border-radius: 4px;
258
+ font-size: 0.7rem;
259
+ font-weight: 500;
260
+ }
261
+
262
+ .memory-confidence {
263
+ color: var(--text-secondary);
264
+ font-size: 0.75rem;
265
+ font-weight: 500;
266
+ }
267
+
268
+ .memory-item .memory-content {
269
+ padding: 0;
270
+ max-height: none;
271
+ overflow: visible;
272
+ color: #e0e0e0;
273
+ line-height: 1.4;
274
+ margin-bottom: 8px;
275
+ font-size: 0.9rem;
276
+ }
277
+
278
+ .memory-preview {
279
+ display: block;
280
+ margin-bottom: 6px;
281
+ color: #e0e0e0;
282
+ line-height: 1.4;
283
+ font-size: 0.9rem;
284
+ }
285
+
286
+ .memory-preview-text {
287
+ display: block;
288
+ overflow: hidden;
289
+ text-overflow: ellipsis;
290
+ white-space: nowrap;
291
+ max-width: 100%;
292
+ color: #e0e0e0;
293
+ }
294
+
295
+ .memory-preview-full {
296
+ white-space: normal;
297
+ max-height: 60px;
298
+ overflow: hidden;
299
+ display: -webkit-box;
300
+ -webkit-line-clamp: 3;
301
+ line-clamp: 3;
302
+ -webkit-box-orient: vertical;
303
+ }
304
+
305
+ .memory-expand-btn {
306
+ background: none;
307
+ border: none;
308
+ color: var(--primary-color);
309
+ cursor: pointer;
310
+ font-size: 0.8rem;
311
+ padding: 2px 4px;
312
+ margin-top: 4px;
313
+ border-radius: 3px;
314
+ transition: background-color 0.2s;
315
+ }
316
+
317
+ .memory-expand-btn:hover {
318
+ background: rgba(var(--primary-rgb), 0.1);
319
+ }
320
+
321
+ .memory-meta {
322
+ display: flex;
323
+ gap: 8px;
324
+ margin-bottom: 8px;
325
+ font-size: 0.75rem;
326
+ color: var(--text-secondary);
327
+ align-items: center;
328
+ }
329
+
330
+ .memory-source {
331
+ cursor: help;
332
+ text-decoration: underline dotted;
333
+ }
334
+
335
+ /* Source excerpt content shown under the trigger */
336
+ .memory-source {
337
+ position: relative;
338
+ outline: none;
339
+ }
340
+
341
+ .memory-source:focus {
342
+ box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.14);
343
+ border-radius: 4px;
344
+ }
345
+
346
+ .memory-source-content {
347
+ margin-top: 8px;
348
+ padding: 10px 12px;
349
+ background: rgba(0, 0, 0, 0.6);
350
+ color: #f1f1f1;
351
+ border-radius: 8px;
352
+ font-size: 0.9rem;
353
+ line-height: 1.4;
354
+ border: 1px solid rgba(255, 255, 255, 0.04);
355
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6);
356
+ max-width: 100%;
357
+ white-space: pre-wrap;
358
+ word-break: break-word;
359
+ transition:
360
+ opacity 0.18s ease,
361
+ transform 0.18s ease;
362
+ opacity: 0;
363
+ transform: translateY(-6px);
364
+ z-index: 2;
365
+ }
366
+
367
+ .memory-source-content[style*="display: block"] {
368
+ opacity: 1;
369
+ transform: translateY(0);
370
+ }
371
+
372
+ /* Make sure content aligns visually under the memory item */
373
+ .memory-item .memory-source-content {
374
+ margin-left: 0;
375
+ }
376
+
377
+ @media (max-width: 480px) {
378
+ .memory-source-content {
379
+ font-size: 0.95rem;
380
+ padding: 12px;
381
+ }
382
+ }
383
+
384
+ .memory-actions {
385
+ display: flex;
386
+ gap: 6px;
387
+ justify-content: flex-end;
388
+ }
389
+
390
+ .memory-edit-btn,
391
+ .memory-delete-btn {
392
+ padding: 4px 6px;
393
+ border: none;
394
+ border-radius: 4px;
395
+ cursor: pointer;
396
+ font-size: 0.8rem;
397
+ transition: all 0.2s ease;
398
+ min-width: 28px;
399
+ height: 28px;
400
+ display: flex;
401
+ align-items: center;
402
+ justify-content: center;
403
+ }
404
+
405
+ .memory-edit-btn {
406
+ background: var(--primary-color);
407
+ color: white;
408
+ }
409
+
410
+ .memory-edit-btn:hover {
411
+ background: var(--primary-dark);
412
+ }
413
+
414
+ .memory-delete-btn {
415
+ background: #e74c3c;
416
+ color: white;
417
+ }
418
+
419
+ .memory-delete-btn:hover {
420
+ background: #c0392b;
421
+ }
422
+
423
+ /* Empty State */
424
+ .memory-empty {
425
+ text-align: center;
426
+ padding: 40px 20px;
427
+ color: var(--text-secondary);
428
+ }
429
+
430
+ .memory-empty i {
431
+ font-size: 3rem;
432
+ margin-bottom: 16px;
433
+ opacity: 0.5;
434
+ }
435
+
436
+ .memory-empty p {
437
+ margin: 0;
438
+ line-height: 1.5;
439
+ }
440
+
441
+ /* Memory Category Groups */
442
+ .memory-category-group {
443
+ margin-bottom: 16px;
444
+ }
445
+
446
+ .memory-category-header {
447
+ margin: 0 0 8px 0;
448
+ padding: 6px 12px;
449
+ background: var(--primary-color);
450
+ color: white;
451
+ border-radius: 6px;
452
+ font-size: 0.85rem;
453
+ font-weight: 600;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: space-between;
457
+ }
458
+
459
+ .memory-category-count {
460
+ opacity: 0.8;
461
+ font-weight: normal;
462
+ font-size: 0.8rem;
463
+ }
464
+
465
+ .memory-category-items {
466
+ display: flex;
467
+ flex-direction: column;
468
+ gap: 6px;
469
+ }
470
+
471
+ /* Memory Item Types */
472
+ .memory-item.memory-auto {
473
+ border-left: 4px solid #3498db;
474
+ }
475
+
476
+ .memory-item.memory-manual {
477
+ border-left: 4px solid #9b59b6;
478
+ }
479
+
480
+ .memory-badges {
481
+ display: flex;
482
+ gap: 8px;
483
+ align-items: center;
484
+ }
485
+
486
+ .memory-type {
487
+ font-size: 0.75rem;
488
+ padding: 2px 6px;
489
+ border-radius: 4px;
490
+ text-transform: uppercase;
491
+ font-weight: 500;
492
+ }
493
+
494
+ .memory-type.auto_extracted {
495
+ background: #3498db;
496
+ color: white;
497
+ }
498
+
499
+ .memory-type.manual {
500
+ background: #9b59b6;
501
+ color: white;
502
+ }
503
+
504
+ .memory-type.imported {
505
+ background: #f39c12;
506
+ color: white;
507
+ }
508
+
509
+ /* Confidence Levels */
510
+ .memory-confidence {
511
+ font-size: 0.8rem;
512
+ font-weight: 500;
513
+ padding: 2px 6px;
514
+ border-radius: 4px;
515
+ }
516
+
517
+ .confidence-high {
518
+ background: #27ae60;
519
+ color: white;
520
+ }
521
+
522
+ .confidence-medium {
523
+ background: #f39c12;
524
+ color: white;
525
+ }
526
+
527
+ .confidence-low {
528
+ background: #e74c3c;
529
+ color: white;
530
+ }
531
+
532
+ /* Importance badge */
533
+ .memory-importance {
534
+ padding: 2px 6px;
535
+ border-radius: 4px;
536
+ font-size: 0.75rem;
537
+ font-weight: 600;
538
+ }
539
+ .importance-high {
540
+ background: #8e44ad;
541
+ color: #fff;
542
+ }
543
+ .importance-medium {
544
+ background: #16a085;
545
+ color: #fff;
546
+ }
547
+ .importance-low {
548
+ background: #7f8c8d;
549
+ color: #fff;
550
+ }
551
+
552
+ /* Tags (chips) */
553
+ .memory-tags {
554
+ display: flex;
555
+ gap: 6px;
556
+ flex-wrap: wrap;
557
+ margin: 6px 0 4px 0;
558
+ }
559
+ .memory-tag {
560
+ font-size: 0.7rem;
561
+ padding: 2px 6px;
562
+ border-radius: 999px;
563
+ background: var(--border-color);
564
+ color: var(--text-secondary);
565
+ border: 1px solid rgba(0, 0, 0, 0.05);
566
+ }
567
+ .memory-tag.tag-relationship {
568
+ background: rgba(231, 76, 60, 0.15);
569
+ color: #e74c3c;
570
+ border-color: rgba(231, 76, 60, 0.25);
571
+ }
572
+ .memory-tag.tag-boundary {
573
+ background: rgba(52, 152, 219, 0.15);
574
+ color: #3498db;
575
+ border-color: rgba(52, 152, 219, 0.25);
576
+ }
577
+ .memory-tag.tag-time {
578
+ background: rgba(241, 196, 15, 0.2);
579
+ color: #d35400;
580
+ border-color: rgba(241, 196, 15, 0.3);
581
+ }
582
+ .memory-tag.tag-type {
583
+ background: rgba(46, 204, 113, 0.2);
584
+ color: #27ae60;
585
+ border-color: rgba(46, 204, 113, 0.3);
586
+ }
587
+ .memory-tag.tag-generic {
588
+ opacity: 0.9;
589
+ }
590
+ .memory-tag.tag-more {
591
+ background: transparent;
592
+ color: var(--text-secondary);
593
+ border-style: dashed;
594
+ }
595
+
596
+ /* Memory Toggle Indicator */
597
+ .toggle-switch {
598
+ position: relative;
599
+ }
600
+
601
+ .memory-indicator {
602
+ position: absolute;
603
+ top: -2px;
604
+ right: -2px;
605
+ width: 8px;
606
+ height: 8px;
607
+ border-radius: 50%;
608
+ border: 2px solid white;
609
+ }
610
+
611
+ /* Enhanced animations */
612
+ .memory-item {
613
+ transition: all 0.3s ease;
614
+ transform: translateY(0);
615
+ }
616
+
617
+ .memory-item:hover {
618
+ transform: translateY(-2px);
619
+ box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.15);
620
+ }
621
+
622
+ @keyframes fadeIn {
623
+ to {
624
+ opacity: 1;
625
+ }
626
+ }
627
+
628
+ @keyframes modalSlideIn {
629
+ to {
630
+ opacity: 1;
631
+ transform: scale(1);
632
+ }
633
+ }
634
+
635
+ /* Memory Feedback Notifications */
636
+ .memory-feedback {
637
+ font-family: var(--font-family, sans-serif);
638
+ border-radius: 8px;
639
+ backdrop-filter: blur(5px);
640
+ font-weight: 500;
641
+ border: 1px solid rgba(255, 255, 255, 0.1);
642
+ min-width: 200px;
643
+ text-align: center;
644
+ }
645
+
646
+ .memory-feedback-info {
647
+ background: linear-gradient(135deg, rgba(52, 152, 219, 0.9), rgba(52, 152, 219, 0.7));
648
+ color: white;
649
+ box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
650
+ }
651
+
652
+ .memory-feedback-success {
653
+ background: linear-gradient(135deg, rgba(39, 174, 96, 0.9), rgba(39, 174, 96, 0.7));
654
+ color: white;
655
+ box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
656
+ }
657
+
658
+ .memory-feedback-error {
659
+ background: linear-gradient(135deg, rgba(231, 76, 60, 0.9), rgba(231, 76, 60, 0.7));
660
+ color: white;
661
+ box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
662
+ }
663
+
664
+ /* Unified Dark Theme for Memory Modal - All Themes */
665
+ /* Theme-agnostic variables for consistent memory modal appearance */
666
+ .memory-modal {
667
+ background: #1a1a1a;
668
+ color: #e0e0e0;
669
+ --memory-modal-bg: #1a1a1a;
670
+ --memory-modal-text: #e0e0e0;
671
+ --memory-item-bg: rgba(42, 42, 42, 0.95);
672
+ --memory-item-bg-hover: rgba(42, 42, 42, 1);
673
+ --memory-item-border: #404040;
674
+ }
675
+
676
+ .memory-item {
677
+ background: var(--memory-item-bg);
678
+ border-color: var(--memory-item-border);
679
+ color: var(--memory-modal-text);
680
+ backdrop-filter: blur(5px);
681
+ }
682
+
683
+ .memory-item:hover {
684
+ border-color: var(--primary-color);
685
+ background: var(--memory-item-bg-hover);
686
+ box-shadow: 0 4px 16px rgba(var(--primary-rgb), 0.2);
687
+ }
688
+
689
+ .memory-expand-btn:hover {
690
+ background: rgba(var(--primary-rgb), 0.2);
691
+ }
kimi-css/kimi-settings.css ADDED
@@ -0,0 +1,1632 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== KIMI SETTINGS PANEL ===== */
2
+
3
+ .settings-overlay {
4
+ position: fixed;
5
+ top: 0;
6
+ left: 0;
7
+ width: 100%;
8
+ height: 100%;
9
+ background: var(--modal-overlay-bg);
10
+ backdrop-filter: blur(10px);
11
+ -webkit-backdrop-filter: blur(10px);
12
+ z-index: 50;
13
+ display: none;
14
+ opacity: 0;
15
+ transition:
16
+ opacity 0.3s ease,
17
+ backdrop-filter 0.3s ease;
18
+ }
19
+
20
+ .settings-overlay.visible {
21
+ display: flex;
22
+ opacity: 1;
23
+ justify-content: center;
24
+ align-items: center;
25
+ padding: 10px;
26
+ }
27
+
28
+ /* ===== SETTINGS PANEL ===== */
29
+ .settings-panel {
30
+ background: var(--modal-bg);
31
+ border-radius: 15px;
32
+ border: 1px solid var(--modal-border);
33
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5);
34
+ width: 90%;
35
+ max-width: 800px;
36
+ max-height: 85vh;
37
+ overflow: hidden;
38
+ animation: slideInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1);
39
+ display: flex;
40
+ flex-direction: column;
41
+ }
42
+
43
+ .settings-content {
44
+ flex: 1;
45
+ overflow-y: auto;
46
+ padding: 0;
47
+ }
48
+
49
+ .settings-content::-webkit-scrollbar {
50
+ width: var(--scrollbar-width);
51
+ }
52
+
53
+ .settings-content::-webkit-scrollbar-track {
54
+ background: var(--scrollbar-track-bg);
55
+ border-radius: 4px;
56
+ }
57
+
58
+ .settings-content::-webkit-scrollbar-thumb {
59
+ background: var(--scrollbar-thumb-bg);
60
+ border-radius: 4px;
61
+ }
62
+
63
+ .settings-content::-webkit-scrollbar-thumb:hover {
64
+ background: var(--scrollbar-thumb-hover-bg);
65
+ }
66
+
67
+ @keyframes slideInUp {
68
+ from {
69
+ transform: translateY(50px);
70
+ opacity: 0;
71
+ }
72
+ to {
73
+ transform: translateY(0);
74
+ opacity: 1;
75
+ }
76
+ }
77
+
78
+ .settings-header {
79
+ background: var(--modal-header-bg);
80
+ padding: 25px 30px;
81
+ display: flex;
82
+ justify-content: space-between;
83
+ align-items: center;
84
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
85
+ }
86
+
87
+ .settings-header-actions {
88
+ display: flex;
89
+ gap: 10px;
90
+ align-items: center;
91
+ }
92
+
93
+ .settings-title {
94
+ margin: 0;
95
+ color: var(--modal-title-color);
96
+ font-size: 1.5rem;
97
+ font-weight: 700;
98
+ display: flex;
99
+ align-items: center;
100
+ gap: 10px;
101
+ text-shadow: 0 1px 1px #000;
102
+ }
103
+
104
+ .help-button,
105
+ .settings-close {
106
+ background: none;
107
+ border: none;
108
+ color: var(--modal-text);
109
+ font-size: 1.5rem;
110
+ cursor: pointer;
111
+ padding: 8px;
112
+ border-radius: 50%;
113
+ transition: all 0.3s ease;
114
+ backdrop-filter: blur(10px);
115
+ }
116
+
117
+ .help-button:hover,
118
+ .settings-close:hover {
119
+ background-color: var(--modal-close-hover-bg);
120
+ transform: scale(1.1);
121
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
122
+ }
123
+
124
+ .settings-content {
125
+ flex: 1;
126
+ overflow-y: auto;
127
+ padding: 0;
128
+ position: relative;
129
+ z-index: 1;
130
+ min-height: 0;
131
+ }
132
+
133
+ /* ===== SETTINGS TABS ===== */
134
+ .settings-tabs {
135
+ display: flex;
136
+ overflow-x: auto;
137
+ scroll-behavior: smooth;
138
+ scrollbar-width: none;
139
+ -ms-overflow-style: none;
140
+ box-sizing: border-box;
141
+ transition: padding-right 0.3s ease;
142
+ position: sticky;
143
+ top: 0;
144
+ z-index: 100;
145
+ background: var(--settings-tab-bg);
146
+ border-bottom: 2px solid var(--settings-tab-border);
147
+ backdrop-filter: blur(10px);
148
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
149
+ flex-shrink: 0;
150
+ }
151
+
152
+ .settings-tabs::-webkit-scrollbar {
153
+ display: none;
154
+ }
155
+
156
+ .settings-tab {
157
+ flex: 1;
158
+ min-width: 130px;
159
+ max-width: 200px;
160
+ padding: 15px 16px;
161
+ background: none;
162
+ border: none;
163
+ color: var(--settings-tab-color);
164
+ cursor: pointer;
165
+ font-size: 0.9rem;
166
+ font-weight: 500;
167
+ transition:
168
+ all 0.3s ease,
169
+ font-size 0.2s ease,
170
+ padding 0.2s ease;
171
+ position: relative;
172
+ white-space: nowrap;
173
+ text-align: center;
174
+ text-overflow: ellipsis;
175
+ overflow: hidden;
176
+ box-sizing: border-box;
177
+ }
178
+
179
+ .settings-tab:hover {
180
+ color: var(--settings-tab-hover-color);
181
+ background: var(--settings-tab-hover-bg);
182
+ }
183
+
184
+ .settings-tab.active {
185
+ color: var(--settings-tab-active-color);
186
+ background: var(--settings-tab-active-bg);
187
+ backdrop-filter: blur(10px);
188
+ }
189
+
190
+ .settings-tab.active::after {
191
+ content: "";
192
+ position: absolute;
193
+ bottom: 0;
194
+ left: 0;
195
+ right: 0;
196
+ height: 3px;
197
+ background: linear-gradient(90deg, var(--primary-color, #ff9a9e), var(--secondary-color, #fecfef));
198
+ }
199
+
200
+ .tab-content {
201
+ display: none;
202
+ padding: 30px;
203
+ position: relative;
204
+ z-index: 1;
205
+ overflow: visible;
206
+ background: var(--settings-bg);
207
+ color: var(--modal-text);
208
+ }
209
+
210
+ .tab-content.active {
211
+ display: block;
212
+ }
213
+
214
+ /* ===== SECTIONS DE CONFIGURATION ===== */
215
+
216
+ .config-section {
217
+ margin-bottom: 30px;
218
+ padding: 20px;
219
+ background: var(--settings-section-bg);
220
+ border-radius: 15px;
221
+ border: 1.5px solid var(--settings-section-border);
222
+ }
223
+
224
+ .config-section h3 {
225
+ margin: 0 0 15px 0;
226
+ color: var(--settings-section-header-color);
227
+ font-size: 1.2rem;
228
+ font-weight: 600;
229
+ display: flex;
230
+ align-items: center;
231
+ gap: 10px;
232
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
233
+ }
234
+
235
+ .config-row {
236
+ display: flex;
237
+ justify-content: space-between;
238
+ align-items: center;
239
+ margin-bottom: 15px;
240
+ padding: 10px 0;
241
+ }
242
+
243
+ .config-row:last-child {
244
+ margin-bottom: 0;
245
+ }
246
+
247
+ .config-label {
248
+ color: var(--settings-text);
249
+ font-weight: 500;
250
+ flex: 1;
251
+ }
252
+
253
+ .config-label-section {
254
+ flex: 1;
255
+ display: flex;
256
+ flex-direction: column;
257
+ }
258
+
259
+ .config-help {
260
+ display: block;
261
+ color: var(--settings-text-secondary, #888);
262
+ font-size: 0.8rem;
263
+ margin: 4px 0 0 0;
264
+ opacity: 0.8;
265
+ line-height: 1.3;
266
+ }
267
+
268
+ .config-note-info {
269
+ display: flex;
270
+ align-items: flex-start;
271
+ gap: 8px;
272
+ margin: 12px 0 16px 0;
273
+ padding: 12px;
274
+ background: var(--settings-bg-secondary, rgba(255, 255, 255, 0.05));
275
+ border: 1px solid var(--settings-border-color, rgba(255, 255, 255, 0.1));
276
+ border-radius: 6px;
277
+ border-left: 3px solid var(--accent-color, #8a2be2);
278
+ }
279
+
280
+ .config-note-info i {
281
+ color: var(--accent-color, #8a2be2);
282
+ margin-top: 1px;
283
+ font-size: 0.9rem;
284
+ }
285
+
286
+ /* Variantes de couleur pour différents types de notes */
287
+ .config-note-info.info i {
288
+ color: #4a9eff;
289
+ }
290
+
291
+ .config-note-info.tip i {
292
+ color: #ff9500;
293
+ }
294
+
295
+ .config-note-info.settings i {
296
+ color: #00c896;
297
+ }
298
+
299
+ .config-note-info .config-help {
300
+ margin: 0;
301
+ opacity: 0.9;
302
+ }
303
+
304
+ .presence-dot {
305
+ display: inline-block;
306
+ width: 10px;
307
+ height: 10px;
308
+ border-radius: 50%;
309
+ background-color: #9e9e9e;
310
+ }
311
+
312
+ .inline-row {
313
+ display: inline-flex;
314
+ align-items: center;
315
+ gap: 10px;
316
+ }
317
+
318
+ .config-control {
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 10px;
322
+ flex: 1;
323
+ width: 100%;
324
+ margin-left: 0;
325
+ }
326
+
327
+ /* ===== CONTRÔLES PERSONNALISÉS ===== */
328
+
329
+ .slider-container {
330
+ width: 100%;
331
+ max-width: 380px;
332
+ position: relative;
333
+ display: flex;
334
+ flex-direction: row;
335
+ align-items: center;
336
+ gap: 12px;
337
+ }
338
+
339
+ .kimi-slider,
340
+ .kimi-slider-unified {
341
+ flex: 1;
342
+ min-width: 0;
343
+ width: 100%;
344
+ }
345
+
346
+ .slider-value {
347
+ background: var(--slider-value-bg);
348
+ color: var(--slider-value-color);
349
+ padding: 4px 8px;
350
+ border-radius: 4px;
351
+ font-size: 0.8rem;
352
+ font-weight: 500;
353
+ border: 1px solid var(--slider-value-border);
354
+ min-width: 45px;
355
+ text-align: center;
356
+ flex-shrink: 0;
357
+ }
358
+
359
+ .toggle-switch {
360
+ position: relative;
361
+ width: 50px;
362
+ height: 25px;
363
+ background: var(--switch-bg-inactive);
364
+ border-radius: 25px;
365
+ cursor: pointer;
366
+ transition: background-color 0.3s ease;
367
+ }
368
+
369
+ .toggle-switch.active {
370
+ background: var(--switch-bg-active);
371
+ }
372
+
373
+ .toggle-switch::after {
374
+ content: "";
375
+ position: absolute;
376
+ top: 2px;
377
+ left: 2px;
378
+ width: 21px;
379
+ height: 21px;
380
+ background: var(--switch-thumb-color);
381
+ border-radius: 50%;
382
+ transition: transform 0.3s ease;
383
+ box-shadow: var(--switch-thumb-shadow);
384
+ }
385
+
386
+ .toggle-switch.active::after {
387
+ transform: translateX(25px);
388
+ }
389
+
390
+ .toggle-switch#transcript-toggle {
391
+ background: var(--switch-bg-inactive);
392
+ }
393
+
394
+ .toggle-switch#transcript-toggle.active {
395
+ background: var(--switch-bg-active);
396
+ }
397
+
398
+ /* ===== INFORMATIONS ET STATS ===== */
399
+
400
+ .stats-grid {
401
+ display: grid;
402
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
403
+ gap: 15px;
404
+ margin-top: 20px;
405
+ }
406
+
407
+ .stat-card {
408
+ background: var(--card-bg);
409
+ border-radius: 10px;
410
+ padding: 10px;
411
+ text-align: center;
412
+ border: 1px solid var(--card-border);
413
+ }
414
+
415
+ .stat-value {
416
+ font-size: 1.4rem;
417
+ font-weight: 700;
418
+ color: var(--stat-value-color);
419
+ margin-bottom: 5px;
420
+ }
421
+
422
+ .stat-label {
423
+ color: var(--stat-label-color);
424
+ font-size: 0.85rem;
425
+ }
426
+
427
+ .model-card {
428
+ background: var(--card-bg);
429
+ border-radius: 12px;
430
+ padding: 20px;
431
+ margin-bottom: 15px;
432
+ border: 1px solid var(--card-border);
433
+ cursor: pointer;
434
+ transition: all 0.3s ease;
435
+ }
436
+
437
+ .model-card:hover {
438
+ background: var(--card-hover-bg);
439
+ transform: translateY(-2px);
440
+ }
441
+
442
+ .model-card.selected {
443
+ border-color: var(--model-card-selected-border);
444
+ box-shadow: var(--model-card-selected-shadow);
445
+ }
446
+
447
+ .models-search-container {
448
+ margin: 12px 0 16px;
449
+ }
450
+
451
+ .models-section {
452
+ margin: 16px 0;
453
+ }
454
+
455
+ .models-section-title {
456
+ display: flex;
457
+ align-items: center;
458
+ gap: 8px;
459
+ font-weight: 600;
460
+ color: var(--settings-section-header-color);
461
+ margin: 6px 0 10px;
462
+ }
463
+
464
+ .model-header {
465
+ display: flex;
466
+ justify-content: space-between;
467
+ align-items: center;
468
+ margin-bottom: 10px;
469
+ }
470
+
471
+ .model-name {
472
+ font-size: 1.1rem;
473
+ font-weight: 600;
474
+ color: var(--model-name-color);
475
+ }
476
+
477
+ .model-provider {
478
+ background: var(--model-provider-color);
479
+ color: var(--model-provider-text);
480
+ padding: 4px 8px;
481
+ border-radius: 6px;
482
+ font-size: 0.75rem;
483
+ font-weight: 500;
484
+ }
485
+
486
+ .model-description {
487
+ color: var(--model-description-color);
488
+ font-size: 0.9rem;
489
+ margin-bottom: 10px;
490
+ }
491
+
492
+ .strength-tag {
493
+ display: inline-block;
494
+ background: var(--strength-tag-bg);
495
+ color: var(--strength-tag-text);
496
+ padding: 4px 8px;
497
+ border-radius: 12px;
498
+ font-size: 0.75rem;
499
+ font-weight: 500;
500
+ margin-right: 8px;
501
+ margin-bottom: 5px;
502
+ }
503
+
504
+ /* Models error and no-models messages */
505
+ .no-models-message,
506
+ .models-error-message {
507
+ text-align: center;
508
+ padding: 40px 20px;
509
+ border-radius: 12px;
510
+ margin: 20px 0;
511
+ background: var(--background-secondary);
512
+ border: 1px solid var(--border-color);
513
+ }
514
+
515
+ .no-models-message p,
516
+ .models-error-message p {
517
+ margin: 0;
518
+ color: var(--text-secondary);
519
+ font-size: 0.95rem;
520
+ line-height: 1.5;
521
+ }
522
+
523
+ .models-error-message {
524
+ background: rgba(231, 76, 60, 0.1);
525
+ border-color: rgba(231, 76, 60, 0.3);
526
+ }
527
+
528
+ .models-error-message p {
529
+ color: #e74c3c;
530
+ }
531
+
532
+ .model-strengths {
533
+ display: flex;
534
+ flex-wrap: wrap;
535
+ gap: 6px;
536
+ }
537
+
538
+ .strength-tag {
539
+ background: var(--model-strength-color);
540
+ color: var(--model-strength-text);
541
+ padding: 3px 8px;
542
+ border-radius: 4px;
543
+ font-size: 0.75rem;
544
+ font-weight: 500;
545
+ }
546
+
547
+ /* ===== PLUGIN CARDS AND SWITCHES ===== */
548
+
549
+ .plugin-card {
550
+ background: linear-gradient(135deg, #22121a 80%, var(--modal-bg) 100%);
551
+ border: 2px solid var(--modal-border);
552
+ border-radius: 20px;
553
+ box-shadow:
554
+ 0 4px 24px 0 #000a,
555
+ 0 2px 0 0 var(--modal-border);
556
+ padding: 28px 32px 24px 32px;
557
+ margin-bottom: 28px;
558
+ display: flex;
559
+ flex-direction: row;
560
+ align-items: center;
561
+ gap: 0;
562
+ justify-content: space-between;
563
+ transition:
564
+ box-shadow 0.2s,
565
+ border 0.2s;
566
+ position: relative;
567
+ }
568
+
569
+ .plugin-card .plugin-info {
570
+ flex: 2 1 0;
571
+ min-width: 0;
572
+ margin-right: 24px;
573
+ }
574
+
575
+ .plugin-card-center {
576
+ flex: 1 1 0;
577
+ display: flex;
578
+ flex-direction: column;
579
+ align-items: center;
580
+ justify-content: center;
581
+ gap: 10px;
582
+ min-width: 120px;
583
+ }
584
+
585
+ .plugin-card-switch {
586
+ flex: 0 0 auto;
587
+ display: flex;
588
+ align-items: center;
589
+ justify-content: flex-end;
590
+ min-width: 80px;
591
+ margin-left: 24px;
592
+ }
593
+
594
+ .plugin-card .plugin-title {
595
+ font-size: 1.35rem;
596
+ font-weight: 700;
597
+ color: var(--plugin-card-title-color);
598
+ margin-bottom: 6px;
599
+ letter-spacing: 0.01em;
600
+ text-shadow: 0 2px 8px #000b;
601
+ }
602
+
603
+ .plugin-card .plugin-type {
604
+ font-size: 1rem;
605
+ color: var(--accent-color);
606
+ margin-left: 10px;
607
+ font-weight: 600;
608
+ text-shadow: 0 1px 4px #0008;
609
+ }
610
+
611
+ .plugin-card .plugin-desc {
612
+ color: var(--plugin-card-desc-color);
613
+ margin-bottom: 10px;
614
+ font-size: 1.01rem;
615
+ line-height: 1.5;
616
+ }
617
+
618
+ .plugin-card .plugin-author {
619
+ font-size: 0.95rem;
620
+ color: var(--plugin-card-author-color);
621
+ font-weight: 500;
622
+ }
623
+
624
+ .plugin-card-left {
625
+ display: flex;
626
+ flex-direction: column;
627
+ align-items: flex-start;
628
+ min-width: 90px;
629
+ gap: 8px;
630
+ margin-top: 4px;
631
+ }
632
+
633
+ .plugin-card-right {
634
+ display: flex;
635
+ flex-direction: column;
636
+ flex: 1;
637
+ gap: 12px;
638
+ }
639
+
640
+ .plugin-type-badge {
641
+ display: inline-block;
642
+ background: var(--plugin-type-badge-bg);
643
+ color: #fff;
644
+ font-size: 0.85rem;
645
+ font-weight: 600;
646
+ border-radius: 8px;
647
+ padding: 3px 10px;
648
+ margin-bottom: 8px;
649
+ margin-right: 12px;
650
+ letter-spacing: 0.03em;
651
+ box-shadow: 0 1px 4px #0002;
652
+ }
653
+
654
+ .plugin-theme-swatch {
655
+ display: inline-flex;
656
+ gap: 4px;
657
+ margin-bottom: 8px;
658
+ margin-right: 12px;
659
+ vertical-align: middle;
660
+ }
661
+
662
+ .plugin-theme-swatch span {
663
+ display: inline-block;
664
+ width: 18px;
665
+ height: 18px;
666
+ border-radius: 50%;
667
+ border: 2px solid #fff2;
668
+ box-shadow: 0 1px 4px #0002;
669
+ }
670
+
671
+ .plugin-active-badge {
672
+ display: inline-block;
673
+ background: var(--plugin-active-badge-bg);
674
+ color: #fff;
675
+ font-size: 0.85rem;
676
+ font-weight: 700;
677
+ border-radius: 8px;
678
+ padding: 3px 12px;
679
+ margin-bottom: 8px;
680
+ margin-right: 12px;
681
+ letter-spacing: 0.03em;
682
+ box-shadow: 0 1px 8px #0003;
683
+ }
684
+
685
+ @media (max-width: 700px) {
686
+ .plugin-card {
687
+ flex-direction: column;
688
+ gap: 12px;
689
+ }
690
+ .plugin-card-left {
691
+ flex-direction: row;
692
+ align-items: center;
693
+ min-width: 0;
694
+ gap: 10px;
695
+ margin-top: 0;
696
+ }
697
+ .plugin-card-right {
698
+ gap: 8px;
699
+ }
700
+ }
701
+
702
+ /* ===== CONSOLIDATED RESPONSIVE DESIGN ===== */
703
+ @media (max-width: 768px) {
704
+ /* Settings panel responsive styles */
705
+
706
+ .settings-header {
707
+ padding: 20px;
708
+ }
709
+
710
+ .settings-title {
711
+ font-size: 1.3rem;
712
+ }
713
+
714
+ .settings-tabs {
715
+ padding: 0 10px;
716
+ gap: 5px;
717
+ position: relative;
718
+ }
719
+
720
+ .settings-tab {
721
+ flex: 0 0 auto;
722
+ min-width: 110px;
723
+ padding: 12px 16px;
724
+ font-size: 0.85rem;
725
+ border-radius: 8px 8px 0 0;
726
+ }
727
+
728
+ .settings-tab.active::after {
729
+ height: 2px;
730
+ }
731
+
732
+ .tab-content {
733
+ padding: 20px;
734
+ }
735
+
736
+ .config-row {
737
+ flex-direction: column;
738
+ align-items: flex-start;
739
+ gap: 10px;
740
+ }
741
+
742
+ .config-control {
743
+ margin-left: 0;
744
+ width: 100%;
745
+ }
746
+
747
+ .slider-container {
748
+ width: 100%;
749
+ max-width: 100%;
750
+ flex-direction: row;
751
+ justify-content: space-between;
752
+ align-items: center;
753
+ gap: 10px;
754
+ }
755
+
756
+ .slider-value {
757
+ min-width: 45px;
758
+ flex-shrink: 0;
759
+ }
760
+
761
+ .stats-grid {
762
+ grid-template-columns: repeat(2, 1fr);
763
+ }
764
+ }
765
+
766
+ @media (max-width: 480px) {
767
+ /* Settings panel responsive styles consolidated */
768
+
769
+ .config-row {
770
+ flex-direction: column;
771
+ align-items: stretch;
772
+ gap: 8px;
773
+ }
774
+
775
+ .config-control {
776
+ margin-left: 0;
777
+ width: 100%;
778
+ }
779
+
780
+ .slider-container {
781
+ width: 100%;
782
+ max-width: 100%;
783
+ flex-direction: row;
784
+ justify-content: space-between;
785
+ align-items: center;
786
+ gap: 8px;
787
+ }
788
+
789
+ .kimi-slider,
790
+ .kimi-slider-unified {
791
+ flex: 1;
792
+ min-width: 0;
793
+ }
794
+
795
+ .slider-value {
796
+ min-width: 40px;
797
+ max-width: 50px;
798
+ flex-shrink: 0;
799
+ font-size: 0.75rem;
800
+ }
801
+
802
+ .kimi-select,
803
+ .kimi-select-unified {
804
+ width: 100%;
805
+ max-width: 100%;
806
+ min-width: 0;
807
+ font-size: 0.9rem;
808
+ padding: 10px 35px 10px 12px;
809
+ }
810
+
811
+ .kimi-input,
812
+ .kimi-input-unified {
813
+ width: 100%;
814
+ max-width: 100%;
815
+ min-width: 0;
816
+ font-size: 0.9rem;
817
+ }
818
+
819
+ .settings-tab {
820
+ min-width: 90px;
821
+ font-size: 0.8rem;
822
+ padding: 10px 12px;
823
+ }
824
+
825
+ .config-section {
826
+ padding: 15px;
827
+ }
828
+
829
+ .config-section h3 {
830
+ font-size: 1.1rem;
831
+ }
832
+
833
+ .stats-grid {
834
+ grid-template-columns: repeat(2, 1fr);
835
+ gap: 10px;
836
+ }
837
+
838
+ .stat-card {
839
+ padding: 10px;
840
+ }
841
+
842
+ .stat-value {
843
+ font-size: 1.3rem;
844
+ }
845
+ }
846
+
847
+ @media (max-width: 360px) {
848
+ /* Settings panel responsive styles consolidated */
849
+
850
+ .slider-container {
851
+ gap: 5px;
852
+ }
853
+
854
+ .slider-value {
855
+ min-width: 35px;
856
+ max-width: 40px;
857
+ font-size: 0.7rem;
858
+ padding: 2px 6px;
859
+ }
860
+
861
+ .kimi-select,
862
+ .kimi-select-unified {
863
+ font-size: 0.85rem;
864
+ padding: 8px 30px 8px 10px;
865
+ }
866
+
867
+ .settings-tab {
868
+ min-width: 80px;
869
+ font-size: 0.75rem;
870
+ padding: 8px 10px;
871
+ }
872
+
873
+ .stats-grid {
874
+ grid-template-columns: 1fr;
875
+ }
876
+ }
877
+
878
+ /* ===== HELP MODAL ===== */
879
+ .help-overlay {
880
+ position: fixed;
881
+ top: 0;
882
+ left: 0;
883
+ width: 100%;
884
+ height: 100%;
885
+ background: var(--modal-overlay-bg);
886
+ backdrop-filter: blur(15px);
887
+ -webkit-backdrop-filter: blur(15px);
888
+ z-index: 60;
889
+ display: none;
890
+ opacity: 0;
891
+ transition:
892
+ opacity 0.3s ease,
893
+ backdrop-filter 0.3s ease;
894
+ }
895
+
896
+ .help-overlay.visible {
897
+ display: flex;
898
+ opacity: 1;
899
+ align-items: center;
900
+ justify-content: center;
901
+ padding: 10px;
902
+ }
903
+
904
+ .help-modal {
905
+ background: var(--help-modal-bg, linear-gradient(145deg, rgba(26, 26, 26, 0.95), rgba(40, 40, 40, 0.95)));
906
+ backdrop-filter: blur(20px);
907
+ border-radius: 25px;
908
+ border: 1px solid var(--help-modal-border, rgba(255, 255, 255, 0.1));
909
+ box-shadow:
910
+ 0 20px 60px rgba(0, 0, 0, 0.5),
911
+ 0 0 30px var(--primary-color, rgba(255, 107, 157, 0.3));
912
+ width: 100%;
913
+ max-width: 850px;
914
+ max-height: 85vh;
915
+ overflow: hidden;
916
+ display: flex;
917
+ flex-direction: column;
918
+ animation: slideInUp 0.4s ease-out;
919
+ margin: 20px;
920
+ }
921
+
922
+ .help-header {
923
+ background: var(--modal-header-bg, linear-gradient(135deg, var(--primary-color, #ff9a9e), var(--secondary-color, #fecfef)));
924
+ padding: 25px 30px;
925
+ display: flex;
926
+ justify-content: space-between;
927
+ align-items: center;
928
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
929
+ }
930
+
931
+ .help-title {
932
+ margin: 0;
933
+ color: var(--modal-title-color);
934
+ font-size: 1.5rem;
935
+ font-weight: 700;
936
+ display: flex;
937
+ align-items: center;
938
+ gap: 10px;
939
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
940
+ }
941
+
942
+ .help-close {
943
+ background: none;
944
+ border: none;
945
+ color: var(--modal-text);
946
+ font-size: 1.5rem;
947
+ cursor: pointer;
948
+ padding: 8px;
949
+ border-radius: 50%;
950
+ transition: all 0.3s ease;
951
+ backdrop-filter: blur(10px);
952
+ }
953
+
954
+ .help-close:hover {
955
+ background-color: var(--modal-close-hover-bg);
956
+ transform: scale(1.1);
957
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
958
+ }
959
+
960
+ .help-content {
961
+ flex: 1;
962
+ padding: 25px 30px;
963
+ overflow-y: auto;
964
+ color: var(--help-content-color);
965
+ }
966
+
967
+ /* Help Content Components */
968
+ .help-section {
969
+ margin-bottom: 30px;
970
+ padding-bottom: 20px;
971
+ border-bottom: 1px solid var(--help-section-border);
972
+ }
973
+
974
+ .help-section:last-child {
975
+ border-bottom: none;
976
+ }
977
+
978
+ .help-section h3 {
979
+ color: var(--modal-title-color);
980
+ font-size: 1.3rem;
981
+ margin-bottom: 15px;
982
+ display: flex;
983
+ align-items: center;
984
+ gap: 10px;
985
+ }
986
+
987
+ .creators-info {
988
+ display: flex;
989
+ gap: 20px;
990
+ margin-bottom: 20px;
991
+ flex-wrap: wrap;
992
+ }
993
+
994
+ .creator-card {
995
+ background: var(--creator-card-bg);
996
+ border: 1px solid var(--creator-card-border);
997
+ padding: 20px;
998
+ border-radius: 15px;
999
+ flex: 1;
1000
+ min-width: 280px;
1001
+ display: flex;
1002
+ align-items: center;
1003
+ gap: 15px;
1004
+ }
1005
+
1006
+ .creator-avatar {
1007
+ font-size: 2.5rem;
1008
+ width: 60px;
1009
+ height: 60px;
1010
+ display: flex;
1011
+ align-items: center;
1012
+ justify-content: center;
1013
+ border-radius: 50%;
1014
+ background: var(--creator-avatar-bg);
1015
+ }
1016
+
1017
+ .creator-details h4 {
1018
+ margin: 0 0 5px 0;
1019
+ color: #1650a0;
1020
+ font-size: 1.2rem;
1021
+ }
1022
+
1023
+ .creator-details p {
1024
+ margin: 0 0 8px 0;
1025
+ color: var(--feature-text-color);
1026
+ font-size: 0.9rem;
1027
+ }
1028
+
1029
+ .creator-role {
1030
+ background: var(--creator-role-bg);
1031
+ color: var(--creator-role-color);
1032
+ padding: 4px 12px;
1033
+ border-radius: 12px;
1034
+ font-size: 0.8rem;
1035
+ font-weight: 500;
1036
+ }
1037
+
1038
+ /* Creator Links */
1039
+ .creator-links {
1040
+ display: flex;
1041
+ gap: 8px;
1042
+ margin-top: 12px;
1043
+ }
1044
+
1045
+ .creator-link {
1046
+ background: var(--creator-role-bg, linear-gradient(135deg, var(--primary-color), var(--secondary-color)));
1047
+ color: var(--creator-role-color, var(--text-on-primary));
1048
+ padding: 8px 12px;
1049
+ border-radius: 20px;
1050
+ text-decoration: none;
1051
+ display: flex;
1052
+ align-items: center;
1053
+ justify-content: center;
1054
+ gap: 6px;
1055
+ min-width: auto;
1056
+ height: 36px;
1057
+ transition: all 0.3s ease;
1058
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1059
+ font-size: 0.8rem;
1060
+ font-weight: 500;
1061
+ border: 1px solid transparent;
1062
+ }
1063
+
1064
+ .creator-link:hover {
1065
+ transform: translateY(-2px);
1066
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1067
+ filter: brightness(1.1);
1068
+ border-color: var(--primary-color, #ff6b9d);
1069
+ }
1070
+
1071
+ /* Increased specificity to avoid !important */
1072
+ .help-modal .creator-link:hover {
1073
+ color: var(--creator-role-color, var(--text-on-primary));
1074
+ text-decoration: none;
1075
+ background: var(--creator-role-bg, linear-gradient(135deg, var(--primary-color), var(--secondary-color)));
1076
+ }
1077
+
1078
+ .help-modal .creator-link:hover i,
1079
+ .help-modal .creator-link:hover span {
1080
+ color: inherit;
1081
+ }
1082
+
1083
+ .creator-link i {
1084
+ font-size: 1rem;
1085
+ color: inherit;
1086
+ transition: color 0.3s ease;
1087
+ }
1088
+
1089
+ .creator-link span {
1090
+ color: inherit;
1091
+ transition: color 0.3s ease;
1092
+ }
1093
+
1094
+ .creator-link:hover i,
1095
+ .creator-link:hover span {
1096
+ color: inherit !important;
1097
+ }
1098
+
1099
+ .philosophy {
1100
+ background: var(--philosophy-bg);
1101
+ border: 1px solid var(--philosophy-border);
1102
+ padding: 15px;
1103
+ border-radius: 10px;
1104
+ border-left: 4px solid var(--philosophy-border-left);
1105
+ margin: 15px 0;
1106
+ font-style: italic;
1107
+ color: var(--feature-text-color);
1108
+ }
1109
+
1110
+ .features-grid {
1111
+ display: grid;
1112
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1113
+ gap: 15px;
1114
+ margin: 15px 0;
1115
+ }
1116
+
1117
+ .feature-item {
1118
+ background: var(--feature-item-bg);
1119
+ border: 1px solid var(--feature-item-border);
1120
+ padding: 15px;
1121
+ border-radius: 10px;
1122
+ text-align: center;
1123
+ }
1124
+
1125
+ .feature-item i {
1126
+ color: var(--feature-icon-color);
1127
+ font-size: 2rem;
1128
+ margin-bottom: 10px;
1129
+ }
1130
+
1131
+ .feature-item h4 {
1132
+ margin: 0 0 8px 0;
1133
+ color: var(--feature-title-color);
1134
+ }
1135
+
1136
+ .feature-item p {
1137
+ margin: 0;
1138
+ font-size: 0.9rem;
1139
+ color: var(--feature-text-color);
1140
+ }
1141
+
1142
+ .quick-guide {
1143
+ display: flex;
1144
+ flex-wrap: wrap;
1145
+ gap: 20px;
1146
+ margin-bottom: 20px;
1147
+ }
1148
+
1149
+ .guide-step {
1150
+ display: flex;
1151
+ align-items: flex-start;
1152
+ background: rgba(255, 255, 255, 0.05);
1153
+ border-radius: 12px;
1154
+ padding: 15px 20px;
1155
+ margin-bottom: 12px;
1156
+ min-width: 220px;
1157
+ flex: 1 1 220px;
1158
+ }
1159
+
1160
+ .step-number {
1161
+ font-size: 1.5rem;
1162
+ font-weight: bold;
1163
+ color: var(--guide-step-number-color);
1164
+ margin-right: 16px;
1165
+ background: var(--guide-step-number-bg);
1166
+ border-radius: 50%;
1167
+ width: 36px;
1168
+ height: 36px;
1169
+ display: flex;
1170
+ align-items: center;
1171
+ justify-content: center;
1172
+ }
1173
+
1174
+ .step-content h4 {
1175
+ font-size: 1.1rem;
1176
+ margin-bottom: 6px;
1177
+ color: var(--modal-title-color);
1178
+ }
1179
+
1180
+ .step-content p {
1181
+ font-size: 0.98rem;
1182
+ color: var(--help-content-color);
1183
+ }
1184
+
1185
+ .tips-list {
1186
+ display: flex;
1187
+ flex-wrap: wrap;
1188
+ gap: 18px;
1189
+ margin-bottom: 18px;
1190
+ }
1191
+
1192
+ .tip-item {
1193
+ display: flex;
1194
+ align-items: center;
1195
+ background: var(--feature-item-bg);
1196
+ border-radius: 12px;
1197
+ padding: 12px 18px;
1198
+ min-width: 200px;
1199
+ flex: 1 1 200px;
1200
+ }
1201
+
1202
+ .tip-item i {
1203
+ font-size: 1.2rem;
1204
+ color: var(--feature-icon-color);
1205
+ margin-right: 12px;
1206
+ }
1207
+
1208
+ .tip-item p {
1209
+ font-size: 0.98rem;
1210
+ color: var(--help-content-color);
1211
+ }
1212
+
1213
+ .tech-info {
1214
+ background: var(--feature-item-bg);
1215
+ border-radius: 10px;
1216
+ padding: 14px 20px;
1217
+ margin-top: 10px;
1218
+ color: var(--help-content-color);
1219
+ font-size: 0.98rem;
1220
+ }
1221
+
1222
+ .settings-panel,
1223
+ .config-section {
1224
+ isolation: isolate;
1225
+ }
1226
+
1227
+ /* Videos background */
1228
+ .video-container {
1229
+ z-index: 0;
1230
+ }
1231
+
1232
+ .bg-video {
1233
+ z-index: 1;
1234
+ }
1235
+
1236
+ .content-overlay {
1237
+ z-index: 2;
1238
+ background-color: transparent;
1239
+ backdrop-filter: none;
1240
+ }
1241
+
1242
+ /* Responsive for help modal */
1243
+ @media (max-width: 768px) {
1244
+ .help-overlay {
1245
+ padding: 5px;
1246
+ }
1247
+
1248
+ .help-modal {
1249
+ margin: 5px;
1250
+ max-height: 95vh;
1251
+ border-radius: 15px;
1252
+ width: calc(100% - 10px);
1253
+ }
1254
+
1255
+ .help-header {
1256
+ padding: 20px;
1257
+ }
1258
+
1259
+ .help-title {
1260
+ font-size: 1.3rem;
1261
+ }
1262
+
1263
+ .help-content {
1264
+ padding: 20px;
1265
+ }
1266
+
1267
+ .creators-info {
1268
+ flex-direction: column;
1269
+ }
1270
+
1271
+ .creator-card {
1272
+ min-width: auto;
1273
+ }
1274
+
1275
+ .features-grid {
1276
+ grid-template-columns: 1fr;
1277
+ }
1278
+ }
1279
+
1280
+ /* ===== BUTTON ANIMATIONS ===== */
1281
+ .kimi-button.animated {
1282
+ animation: kimiPulse 0.7s;
1283
+ }
1284
+
1285
+ @keyframes kimiPulse {
1286
+ 0% {
1287
+ background-color: var(--primary-color);
1288
+ transform: scale(1);
1289
+ }
1290
+ 50% {
1291
+ background-color: var(--accent-color);
1292
+ transform: scale(1.08);
1293
+ }
1294
+ 100% {
1295
+ background-color: var(--primary-color);
1296
+ transform: scale(1);
1297
+ }
1298
+ }
1299
+
1300
+ /* ===== CHARACTER GRID AND CARDS ===== */
1301
+ .character-grid {
1302
+ display: grid;
1303
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1304
+ gap: 24px;
1305
+ margin-top: 16px;
1306
+ margin-bottom: 16px;
1307
+ }
1308
+
1309
+ @media (max-width: 700px) {
1310
+ .character-grid {
1311
+ grid-template-columns: 1fr;
1312
+ }
1313
+ }
1314
+
1315
+ .character-card {
1316
+ background: var(--card-bg);
1317
+ border: 2px solid transparent;
1318
+ border-radius: 18px;
1319
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
1320
+ padding: 18px 16px 12px 16px;
1321
+ display: flex;
1322
+ flex-direction: column;
1323
+ align-items: center;
1324
+ cursor: pointer;
1325
+ transition:
1326
+ border 0.2s,
1327
+ box-shadow 0.2s,
1328
+ background 0.2s;
1329
+ user-select: none;
1330
+ width: 100%;
1331
+ }
1332
+
1333
+ .character-card.selected {
1334
+ border: 2.5px solid var(--character-selected-border, #ff6b9d);
1335
+ background: var(--character-selected-bg, rgba(255, 107, 157, 0.13));
1336
+ box-shadow:
1337
+ 0 0 0 4px var(--character-selected-border, #ff6b9d),
1338
+ 0 4px 24px var(--character-selected-bg, rgba(255, 107, 157, 0.15));
1339
+ }
1340
+
1341
+ .character-card:hover {
1342
+ border: 2px solid var(--primary-color, #ff6b9d);
1343
+ background: rgba(255, 107, 157, 0.1);
1344
+ box-shadow: 0 2px 16px rgba(255, 107, 157, 0.1);
1345
+ }
1346
+
1347
+ .character-card img {
1348
+ width: 80px;
1349
+ height: 80px;
1350
+ border-radius: 50%;
1351
+ object-fit: cover;
1352
+ margin-bottom: 12px;
1353
+ border: 2px solid var(--modal-text);
1354
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1355
+ }
1356
+
1357
+ .character-name {
1358
+ text-align: center;
1359
+ width: 100%;
1360
+ font-size: 1.1rem;
1361
+ font-weight: 700;
1362
+ color: var(--modal-text);
1363
+ text-shadow: 0 1px 8px var(--primary-color, #ff6b9d);
1364
+ margin-bottom: 4px;
1365
+ margin-top: 0;
1366
+ }
1367
+
1368
+ .character-info {
1369
+ display: flex;
1370
+ flex-direction: column;
1371
+ align-items: center;
1372
+ justify-content: center;
1373
+ text-align: center;
1374
+ width: 100%;
1375
+ margin-bottom: 10px;
1376
+ }
1377
+
1378
+ .character-details {
1379
+ font-size: 0.95rem;
1380
+ color: var(--modal-text);
1381
+ opacity: 0.85;
1382
+ margin-bottom: 6px;
1383
+ text-align: center;
1384
+ }
1385
+
1386
+ .character-prompt-label {
1387
+ font-size: 0.85rem;
1388
+ color: var(--modal-text);
1389
+ margin-bottom: 4px;
1390
+ margin-top: 8px;
1391
+ opacity: 0.7;
1392
+ text-align: center;
1393
+ }
1394
+
1395
+ .character-prompt-input {
1396
+ width: 100%;
1397
+ min-height: 60px;
1398
+ border-radius: 8px;
1399
+ border: 1px solid var(--primary-color, #ff6b9d);
1400
+ background: var(--input-bg);
1401
+ color: var(--modal-text);
1402
+ padding: 6px 8px;
1403
+ font-size: 0.95rem;
1404
+ resize: vertical;
1405
+ margin-bottom: 4px;
1406
+ }
1407
+
1408
+ .character-prompt-input:disabled {
1409
+ opacity: 0.5;
1410
+ background: var(--input-bg);
1411
+ cursor: not-allowed;
1412
+ }
1413
+
1414
+ /* Character prompt buttons */
1415
+ .character-prompt-buttons {
1416
+ display: flex;
1417
+ gap: 8px;
1418
+ margin-top: 8px;
1419
+ justify-content: center;
1420
+ }
1421
+
1422
+ .character-save-btn,
1423
+ .character-reset-btn {
1424
+ padding: 6px 12px;
1425
+ font-size: 0.85rem;
1426
+ border-radius: 6px;
1427
+ border: 1px solid var(--input-border);
1428
+ background: var(--button-bg);
1429
+ color: var(--button-text);
1430
+ cursor: pointer;
1431
+ transition: all 0.2s ease;
1432
+ min-width: 70px;
1433
+ }
1434
+
1435
+ .character-save-btn:hover,
1436
+ .character-reset-btn:hover {
1437
+ background: var(--button-hover-bg);
1438
+ border-color: var(--primary-color);
1439
+ }
1440
+
1441
+ .character-save-btn:disabled,
1442
+ .character-reset-btn:disabled {
1443
+ opacity: 0.5;
1444
+ cursor: not-allowed;
1445
+ background: var(--input-bg);
1446
+ }
1447
+
1448
+ .character-save-btn.success {
1449
+ background: #28a745;
1450
+ color: white;
1451
+ border-color: #28a745;
1452
+ }
1453
+
1454
+ .character-reset-btn.animated {
1455
+ background: var(--accent-color);
1456
+ color: white;
1457
+ border-color: var(--accent-color);
1458
+ }
1459
+
1460
+ /* ===== PERSONALITY CHEAT PANEL ===== */
1461
+
1462
+ .cheat-toggle-btn {
1463
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
1464
+ border: none;
1465
+ color: #fff;
1466
+ font-size: 1em;
1467
+ margin-left: 1em;
1468
+ cursor: pointer;
1469
+ display: inline-flex;
1470
+ align-items: center;
1471
+ gap: 0.5em;
1472
+ transition:
1473
+ color 0.2s,
1474
+ box-shadow 0.2s,
1475
+ background 0.2s;
1476
+ border-radius: 18px;
1477
+ padding: 0.35em 1.1em 0.35em 0.9em;
1478
+ box-shadow: 0 2px 10px 0 rgba(255, 107, 157, 0.1);
1479
+ font-weight: 600;
1480
+ letter-spacing: 0.01em;
1481
+ outline: none;
1482
+ }
1483
+
1484
+ .cheat-toggle-btn[aria-expanded="true"] {
1485
+ color: #fff;
1486
+ background: linear-gradient(90deg, var(--accent-color), var(--primary-color));
1487
+ box-shadow: 0 2px 16px 0 rgba(255, 107, 157, 0.18);
1488
+ }
1489
+
1490
+ .cheat-toggle-btn:hover,
1491
+ .cheat-toggle-btn:focus {
1492
+ box-shadow: 0 2px 16px 0 rgba(255, 107, 157, 0.25);
1493
+ transform: translateY(-1px);
1494
+ }
1495
+ .cheat-toggle-btn:focus {
1496
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
1497
+ color: #fff;
1498
+ box-shadow: 0 4px 18px 0 rgba(255, 107, 157, 0.22);
1499
+ filter: brightness(1.08);
1500
+ }
1501
+
1502
+ .cheat-toggle-btn i {
1503
+ margin-right: 0.4em;
1504
+ font-size: 1.1em;
1505
+ }
1506
+
1507
+ .cheat-indicator {
1508
+ font-size: 0.9em;
1509
+ color: #aaa;
1510
+ margin-bottom: 0.5em;
1511
+ margin-left: 2.2em;
1512
+ }
1513
+
1514
+ .cheat-panel {
1515
+ max-height: 0;
1516
+ overflow: hidden;
1517
+ transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1518
+ opacity: 0.5;
1519
+ pointer-events: none;
1520
+ }
1521
+
1522
+ .cheat-panel.open {
1523
+ max-height: 800px;
1524
+ opacity: 1;
1525
+ pointer-events: auto;
1526
+ transition:
1527
+ max-height 0.6s cubic-bezier(0.4, 0, 0.2, 1),
1528
+ opacity 0.3s;
1529
+ }
1530
+
1531
+ /* ===== ACCESSIBILITY - FOCUS STYLES ===== */
1532
+ .settings-panel select:focus,
1533
+ .settings-panel input:focus,
1534
+ .settings-panel button:focus,
1535
+ .settings-panel .cheat-toggle-btn:focus {
1536
+ box-shadow: 0 0 0 2px var(--primary-pink);
1537
+ border-color: var(--primary-pink);
1538
+ }
1539
+
1540
+ .character-card:focus {
1541
+ outline: 2px solid var(--primary-pink);
1542
+ outline-offset: 2px;
1543
+ }
1544
+
1545
+ /* ===== API KEY INPUT STYLING ===== */
1546
+
1547
+ .api-key-input-group {
1548
+ display: flex;
1549
+ align-items: center;
1550
+ gap: 8px;
1551
+ position: relative;
1552
+ }
1553
+
1554
+ .api-key-input-group .kimi-input {
1555
+ flex: 1;
1556
+ margin: 0;
1557
+ }
1558
+
1559
+ .api-key-toggle {
1560
+ min-width: 40px;
1561
+ height: 40px;
1562
+ padding: 8px;
1563
+ display: flex;
1564
+ align-items: center;
1565
+ justify-content: center;
1566
+ border-radius: 6px;
1567
+ background: var(--settings-bg-secondary, rgba(255, 255, 255, 0.08));
1568
+ border: 1px solid var(--settings-border-color, rgba(255, 255, 255, 0.15));
1569
+ color: var(--settings-text, #ffffff);
1570
+ transition: all 0.2s ease;
1571
+ cursor: pointer;
1572
+ }
1573
+
1574
+ .api-key-toggle:hover {
1575
+ background: var(--settings-bg-hover, rgba(255, 255, 255, 0.12));
1576
+ border-color: var(--accent-color, #8a2be2);
1577
+ }
1578
+
1579
+ .api-key-toggle:active {
1580
+ transform: scale(0.95);
1581
+ }
1582
+
1583
+ .api-key-input-group .presence-dot {
1584
+ width: 12px;
1585
+ height: 12px;
1586
+ margin-left: 4px;
1587
+ border: 2px solid var(--settings-bg-primary, rgba(0, 0, 0, 0.3));
1588
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1);
1589
+ transition: all 0.2s ease;
1590
+ }
1591
+
1592
+ .api-key-status {
1593
+ margin-top: 6px;
1594
+ min-height: 18px;
1595
+ display: flex;
1596
+ align-items: center;
1597
+ }
1598
+
1599
+ .api-key-status span {
1600
+ font-size: 0.85rem;
1601
+ display: flex;
1602
+ align-items: center;
1603
+ gap: 6px;
1604
+ }
1605
+
1606
+ .api-key-status span::before {
1607
+ content: "✓";
1608
+ font-weight: bold;
1609
+ font-size: 0.9rem;
1610
+ }
1611
+
1612
+ /* Responsive adjustments */
1613
+ @media (max-width: 600px) {
1614
+ .api-key-input-group {
1615
+ flex-direction: column;
1616
+ align-items: stretch;
1617
+ gap: 12px;
1618
+ }
1619
+
1620
+ .api-key-toggle {
1621
+ align-self: flex-end;
1622
+ min-width: 100px;
1623
+ }
1624
+
1625
+ .api-key-input-group .presence-dot {
1626
+ position: absolute;
1627
+ top: 50%;
1628
+ right: 8px;
1629
+ transform: translateY(-50%);
1630
+ z-index: 10;
1631
+ }
1632
+ }
kimi-css/kimi-style.css ADDED
@@ -0,0 +1,2075 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== CONSOLIDATED CSS VARIABLES - ENHANCED DYNAMIC THEMING ===== */
2
+ :root {
3
+ /* Core Theme Colors - Default Dark Theme Base */
4
+ --primary-color: #5e60ce;
5
+ --primary-rgb: 94, 96, 206;
6
+ --secondary-color: #23262f;
7
+ --accent-color: #8b5cf6;
8
+ --background-overlay: rgba(24, 26, 32, 0.85);
9
+ --interface-opacity: 0.7;
10
+ --gradient-start: #5e60ce;
11
+ --gradient-end: #8b5cf6;
12
+ --text-glow: 0 0 10px rgba(94, 96, 206, 0.3);
13
+ --button-hover: rgba(94, 96, 206, 0.15);
14
+ --animations-enabled: 1;
15
+ --switch-color: #5e60ce;
16
+
17
+ /* Contrast and Accessibility */
18
+ --contrast-ratio: 4.5; /* WCAG AA standard */
19
+ --text-on-primary: #ffffff;
20
+ --text-on-secondary: #e0e0e0;
21
+ --text-on-accent: #ffffff;
22
+ --text-on-background: #e0e0e0;
23
+ --border-opacity: 0.3;
24
+ --hover-opacity: 0.15;
25
+ --active-opacity: 0.25;
26
+
27
+ /* UI Component Colors - Chat Interface */
28
+ --chat-bg: rgba(24, 26, 32, 0.95);
29
+ --chat-text: var(--text-on-background);
30
+ --chat-header-bg: rgba(255, 255, 255, 0.05);
31
+ --chat-border: #5e60ce;
32
+ --chat-input-bg: rgba(255, 255, 255, 0.1);
33
+ --chat-input-text: var(--text-on-background);
34
+ --chat-input-placeholder: rgba(255, 255, 255, 0.6);
35
+ --chat-message-user-bg: #1e253c;
36
+ --chat-message-user-text: var(--text-on-background);
37
+ --chat-message-kimi-bg: #23262f;
38
+ --chat-message-kimi-text: var(--text-on-background);
39
+
40
+ /* Modal & Overlay Colors */
41
+ --modal-bg: rgba(24, 26, 32, 0.98);
42
+ --modal-border: #5e60ce;
43
+ --modal-header-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
44
+ --modal-text: var(--text-on-background);
45
+ --modal-title-color: #e0e0e0;
46
+ --modal-overlay-bg: rgba(0, 0, 0, 0.8);
47
+ --modal-close-hover-bg: rgba(255, 255, 255, 0.2);
48
+
49
+ /* Settings Panel & Tabs */
50
+ --settings-bg: #0f1114;
51
+ --settings-text: var(--text-on-background);
52
+ --settings-tab-bg: #181a20;
53
+ --settings-tab-color: #bfa6b6;
54
+ --settings-tab-hover-bg: rgba(255, 255, 255, var(--hover-opacity));
55
+ --settings-tab-hover-color: rgba(255, 255, 255, 0.9);
56
+ --settings-tab-active-bg: #5e60ce;
57
+ --settings-tab-active-color: var(--text-on-primary);
58
+ --settings-tab-border: #5e60ce;
59
+ --settings-section-bg: #1a1d23;
60
+ --settings-section-border: rgba(94, 96, 206, var(--border-opacity));
61
+ --settings-section-header-color: var(--text-on-background);
62
+
63
+ /* Form Element Colors */
64
+ --input-bg: rgba(255, 255, 255, 0.1);
65
+ --input-text: var(--text-on-background);
66
+ --input-border: #5e60ce;
67
+ --input-focus-bg: rgba(255, 255, 255, var(--hover-opacity));
68
+ --input-focus-border: #8b5cf6;
69
+ --input-placeholder: rgba(255, 255, 255, 0.6);
70
+
71
+ /* Button Colors */
72
+ --button-primary-bg: var(--primary-color);
73
+ --button-primary-text: var(--text-on-primary);
74
+ --button-primary-hover-bg: var(--accent-color);
75
+ --button-secondary-bg: rgba(255, 255, 255, 0.05);
76
+ --button-secondary-text: var(--text-on-background);
77
+ --button-secondary-hover-bg: rgba(255, 255, 255, var(--hover-opacity));
78
+ --button-danger-bg: #e74c3c;
79
+ --button-danger-text: #ffffff;
80
+ --button-danger-hover-bg: #c0392b;
81
+
82
+ /* Select & Dropdown Options */
83
+ --select-bg: var(--input-bg);
84
+ --select-text: var(--input-text);
85
+ --select-border: var(--input-border);
86
+ --select-option-bg: rgba(24, 26, 32, 0.95);
87
+ --select-option-text: var(--text-on-background);
88
+ --select-option-hover-bg: #5e60ce;
89
+ --select-option-hover-text: var(--text-on-primary);
90
+ --select-option-checked-bg: #8b5cf6;
91
+ --select-option-checked-text: var(--text-on-accent);
92
+ --select-option-disabled-bg: rgba(24, 26, 32, 0.5);
93
+ --select-option-disabled-text: #666;
94
+
95
+ /* Slider Components */
96
+ --slider-track-bg: rgba(255, 255, 255, 0.1);
97
+ --slider-track-active-bg: var(--primary-color);
98
+ --slider-thumb-bg: var(--primary-color);
99
+ --slider-thumb-hover-bg: var(--accent-color);
100
+ --slider-value-bg: #0f1114;
101
+ --slider-value-border: #5e60ce;
102
+ --slider-value-color: var(--text-on-background);
103
+
104
+ /* Toggle Switch */
105
+ --switch-bg-inactive: rgba(255, 255, 255, 0.05);
106
+ --switch-bg-active: linear-gradient(90deg, #5e60ce, #8b5cf6);
107
+ --switch-thumb-color: #ffffff;
108
+ --switch-thumb-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
109
+
110
+ /* Mic Button & Pulse Effect */
111
+ --mic-button-bg: var(--button-hover);
112
+ --mic-button-border: #5e60ce;
113
+ --mic-button-shadow: 0 0 15px #5e60ce;
114
+ --mic-button-hover-bg: #5e60ce;
115
+ --mic-button-hover-shadow: 0 0 10px rgba(94, 96, 206, 0.5);
116
+ --mic-button-icon-color: var(--text-on-primary);
117
+ --mic-listening-border: #27ae60;
118
+ --mic-listening-shadow: 0 0 15px #27ae60;
119
+ --mic-pulse-color: rgba(39, 174, 96, 0.5);
120
+ --mic-pulse-listening-color: rgba(39, 174, 96, 0.4);
121
+
122
+ /* Video crossfade timing */
123
+ --video-fade-duration: 400ms;
124
+
125
+ /* Cards & Stats */
126
+ --card-bg: rgba(255, 255, 255, 0.02);
127
+ --card-border: rgba(255, 255, 255, 0.05);
128
+ --card-hover-bg: rgba(255, 255, 255, 0.05);
129
+ --card-text: var(--text-on-background);
130
+ --stat-value-color: #8b5cf6;
131
+ --stat-label-color: rgba(224, 224, 224, 0.7);
132
+
133
+ /* Plugin Cards */
134
+ --plugin-card-bg: linear-gradient(135deg, #1a1d23 80%, rgba(24, 26, 32, 0.98) 100%);
135
+ --plugin-card-border: #5e60ce;
136
+ --plugin-card-title-color: var(--text-on-background);
137
+ --plugin-card-desc-color: rgba(224, 224, 224, 0.7);
138
+ --plugin-card-author-color: rgba(224, 224, 224, 0.5);
139
+ --plugin-type-badge-bg: #8b5cf6;
140
+ --plugin-type-badge-text: var(--text-on-accent);
141
+ --plugin-active-badge-bg: linear-gradient(90deg, #5e60ce, #8b5cf6);
142
+ --plugin-active-badge-text: var(--text-on-primary);
143
+
144
+ /* Help Modal */
145
+ --help-modal-bg: rgba(24, 26, 32, 0.98);
146
+ --help-modal-border: #5e60ce;
147
+ --help-content-color: var(--text-on-background);
148
+ --help-section-border: rgba(255, 255, 255, 0.1);
149
+ --creator-card-bg: rgba(255, 255, 255, 0.02);
150
+ --creator-card-border: rgba(255, 255, 255, 0.05);
151
+ --creator-avatar-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
152
+ --creator-name-color: #8b5cf6;
153
+ --creator-role-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
154
+ --creator-role-color: var(--text-on-primary);
155
+ --philosophy-bg: rgba(94, 96, 206, 0.05);
156
+ --philosophy-border: rgba(94, 96, 206, var(--border-opacity));
157
+ --philosophy-border-left: #5e60ce;
158
+ --feature-item-bg: rgba(255, 255, 255, 0.02);
159
+ --feature-item-border: rgba(94, 96, 206, var(--border-opacity));
160
+ --feature-icon-color: #8b5cf6;
161
+ --feature-title-color: #8b5cf6;
162
+ --feature-text-color: rgba(224, 224, 224, 0.7);
163
+ --guide-step-bg: rgba(255, 255, 255, 0.02);
164
+ --guide-step-number-bg: rgba(94, 96, 206, var(--hover-opacity));
165
+ --guide-step-number-color: var(--primary-color);
166
+ --tip-item-bg: rgba(255, 255, 255, 0.02);
167
+ --tip-item-border: var(--feature-item-border);
168
+
169
+ /* Unified Scrollbar System */
170
+ --scrollbar-width: 8px !important;
171
+ --scrollbar-track-bg: rgba(255, 255, 255, 0.02) !important;
172
+ --scrollbar-thumb-bg: rgba(94, 96, 206, 0.4) !important;
173
+ --scrollbar-thumb-hover-bg: rgba(94, 96, 206, 0.6) !important;
174
+ --scrollbar-thumb-active-bg: rgba(94, 96, 206, 0.8) !important;
175
+ --scrollbar-corner-bg: rgba(255, 255, 255, 0.05) !important;
176
+
177
+ /* Model Colors */
178
+ --model-card-bg: rgba(255, 255, 255, 0.02);
179
+ --model-card-border: rgba(255, 255, 255, 0.05);
180
+ --model-card-hover-bg: rgba(255, 255, 255, 0.05);
181
+ --model-card-selected-border: var(--primary-color);
182
+ --model-card-selected-shadow: 0 0 0 2px rgba(94, 96, 206, var(--border-opacity));
183
+ --model-name-color: var(--text-on-background);
184
+ --model-description-color: rgba(224, 224, 224, 0.7);
185
+ --model-strength-color: #5e60ce;
186
+ --model-strength-text: var(--text-on-primary);
187
+ --model-provider-color: #8b5cf6;
188
+ --model-provider-text: var(--text-on-accent);
189
+
190
+ /* Text Colors */
191
+ --text-primary: var(--text-on-background);
192
+ --text-secondary: #9ca3af;
193
+ --text-muted: rgba(224, 224, 224, 0.6);
194
+
195
+ /* Character Selection Colors */
196
+ --character-card-bg: rgba(255, 255, 255, 0.02);
197
+ --character-card-border: rgba(255, 255, 255, 0.05);
198
+ --character-card-hover-bg: rgba(255, 255, 255, 0.05);
199
+ --character-selected-border: #8b5cf6;
200
+ --character-selected-bg: rgba(94, 96, 206, 0.1);
201
+ --character-name-color: var(--text-on-background);
202
+
203
+ /* Waiting Indicator */
204
+ --waiting-indicator-color: var(--primary-color);
205
+ --loading-spinner-color: var(--primary-color);
206
+
207
+ /* Progress Bar */
208
+ --progress-bg: rgba(255, 255, 255, 0.05);
209
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
210
+ --progress-text-color: var(--text-on-background);
211
+
212
+ /* Transcript */
213
+ --transcript-bg: rgba(0, 0, 0, 0.9);
214
+ --transcript-text: var(--text-on-background);
215
+ --transcript-border: var(--primary-color);
216
+ }
217
+
218
+ /* === Microphone Button Disabled State (2025-09 SR capability refactor) === */
219
+ #mic-button.disabled,
220
+ button#mic-button.disabled {
221
+ opacity: 0.45;
222
+ cursor: not-allowed;
223
+ box-shadow: none;
224
+ filter: grayscale(40%);
225
+ transition: opacity 0.25s ease;
226
+ }
227
+
228
+ #mic-button.disabled:hover {
229
+ opacity: 0.45; /* keep stable */
230
+ }
231
+
232
+ #mic-button.disabled .mic-icon,
233
+ #mic-button.disabled svg {
234
+ opacity: 0.7;
235
+ }
236
+
237
+ /* Provide a subtle tooltip helper if title attribute present */
238
+ #mic-button.disabled[title] {
239
+ position: relative;
240
+ }
241
+
242
+ /* ===== OPTIMIZED THEME VARIATIONS ===== */
243
+ [data-theme="pink"] {
244
+ /* Core Theme Colors - Pink Passion */
245
+ --primary-color: #ff6b9d;
246
+ --primary-rgb: 255, 107, 157;
247
+ --secondary-color: #ffeaa7;
248
+ --accent-color: #fd79a8;
249
+ --background-overlay: rgba(255, 107, 157, 0.15);
250
+ --gradient-start: #ff6b9d;
251
+ --gradient-end: #fd79a8;
252
+ --text-glow: 0 0 10px rgba(255, 107, 157, 0.5);
253
+ --button-hover: rgba(255, 107, 157, 0.3);
254
+ --switch-color: var(--primary-color);
255
+
256
+ /* Contrast and Accessibility */
257
+ --text-on-primary: #ffffff;
258
+ --text-on-secondary: #222222;
259
+ --text-on-accent: #ffffff;
260
+ --text-on-background: #ffffff;
261
+
262
+ /* UI Component Colors - Chat Interface */
263
+ --chat-bg: rgba(255, 107, 157, 0.9);
264
+ --chat-text: var(--text-on-primary);
265
+ --chat-header-bg: rgba(255, 255, 255, 0.05);
266
+ --chat-border: var(--primary-color);
267
+ --chat-input-bg: rgba(255, 255, 255, 0.1);
268
+ --chat-input-text: var(--text-on-background);
269
+ --chat-input-placeholder: rgba(255, 255, 255, 0.6);
270
+ --chat-message-user-bg: var(--primary-color);
271
+ --chat-message-user-text: var(--text-on-primary);
272
+ --chat-message-kimi-bg: rgba(255, 255, 255, 0.15);
273
+ --chat-message-kimi-text: var(--text-on-background);
274
+
275
+ /* Modal & Overlay Colors */
276
+ --modal-bg: rgba(255, 107, 157, 0.95);
277
+ --modal-border: var(--primary-color);
278
+ --modal-header-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
279
+ --modal-text: var(--text-on-primary);
280
+ --modal-title-color: var(--text-on-secondary);
281
+ --modal-overlay-bg: rgba(0, 0, 0, 0.8);
282
+ --modal-close-hover-bg: rgba(255, 255, 255, 0.2);
283
+
284
+ /* Settings Panel & Tabs */
285
+ --settings-bg: #181018;
286
+ --settings-text: var(--text-on-background);
287
+ --settings-tab-bg: #1a1a1a;
288
+ --settings-tab-color: #bfa6b6;
289
+ --settings-tab-hover-bg: rgba(255, 255, 255, var(--hover-opacity));
290
+ --settings-tab-hover-color: rgba(255, 255, 255, 0.9);
291
+ --settings-tab-active-bg: var(--primary-color);
292
+ --settings-tab-active-color: var(--text-on-primary);
293
+ --settings-tab-border: var(--primary-color);
294
+ --settings-section-bg: #22121a;
295
+ --settings-section-border: rgba(255, 107, 157, var(--border-opacity));
296
+ --settings-section-header-color: var(--text-on-background);
297
+
298
+ /* Form Element Colors */
299
+ --input-bg: rgba(255, 255, 255, 0.1);
300
+ --input-text: var(--text-on-background);
301
+ --input-border: var(--primary-color);
302
+ --input-focus-bg: rgba(255, 255, 255, var(--hover-opacity));
303
+ --input-focus-border: var(--accent-color);
304
+ --input-placeholder: rgba(255, 255, 255, 0.6);
305
+
306
+ /* Button Colors */
307
+ --button-primary-bg: var(--primary-color);
308
+ --button-primary-text: var(--text-on-primary);
309
+ --button-primary-hover-bg: var(--accent-color);
310
+ --button-secondary-bg: rgba(255, 255, 255, 0.1);
311
+ --button-secondary-text: var(--text-on-background);
312
+ --button-secondary-hover-bg: rgba(255, 255, 255, var(--hover-opacity));
313
+
314
+ /* All other pink theme variables... */
315
+ --slider-track-bg: rgba(255, 255, 255, 0.1);
316
+ --slider-track-active-bg: var(--primary-color);
317
+ --slider-thumb-bg: var(--primary-color);
318
+ --slider-thumb-hover-bg: var(--accent-color);
319
+ --slider-value-bg: #181018;
320
+ --slider-value-border: var(--primary-color);
321
+ --slider-value-color: var(--text-on-background);
322
+
323
+ /* Toggle Switch */
324
+ --switch-bg-inactive: rgba(255, 255, 255, 0.15);
325
+ --switch-bg-active: linear-gradient(90deg, var(--primary-color), var(--accent-color));
326
+
327
+ /* Mic Button & Pulse Effect */
328
+ --mic-button-bg: var(--button-hover);
329
+ --mic-button-border: var(--primary-color);
330
+ --mic-button-shadow: 0 0 15px var(--primary-color);
331
+ --mic-button-hover-bg: var(--primary-color);
332
+ --mic-button-hover-shadow: var(--text-glow);
333
+ --mic-button-icon-color: var(--text-on-primary);
334
+
335
+ /* Cards & Stats */
336
+ --card-bg: rgba(255, 255, 255, 0.05);
337
+ --card-border: rgba(255, 255, 255, 0.1);
338
+ --card-hover-bg: rgba(255, 255, 255, 0.08);
339
+ --stat-value-color: var(--primary-color);
340
+ --stat-label-color: rgba(255, 255, 255, 0.7);
341
+
342
+ /* Character Selection Colors */
343
+ --character-card-bg: var(--card-bg);
344
+ --character-card-border: var(--card-border);
345
+ --character-card-hover-bg: var(--card-hover-bg);
346
+ --character-selected-border: var(--primary-color);
347
+ --character-selected-bg: rgba(255, 107, 157, 0.13);
348
+ --character-name-color: var(--text-on-background);
349
+ }
350
+
351
+ [data-theme="blue"] {
352
+ --primary-color: #74b9ff;
353
+ --primary-rgb: 116, 185, 255;
354
+ --secondary-color: #81ecec;
355
+ --accent-color: #0984e3;
356
+ --background-overlay: rgba(116, 185, 255, 0.15);
357
+ --gradient-start: #74b9ff;
358
+ --gradient-end: #0984e3;
359
+ --text-glow: 0 0 10px rgba(116, 185, 255, 0.5);
360
+ --button-hover: rgba(116, 185, 255, 0.3);
361
+ --modal-title-color: #0a2340;
362
+ --switch-color: #3498db;
363
+
364
+ /* UI Component Colors */
365
+ --chat-bg: rgba(116, 185, 255, 0.9);
366
+ --chat-border: #74b9ff;
367
+ --chat-message-user-bg: #74b9ff;
368
+ --chat-message-kimi-bg: rgba(255, 255, 255, 0.15);
369
+ --input-border: #74b9ff;
370
+ --input-focus-border: #0984e3;
371
+
372
+ /* Modal & Overlay Colors */
373
+ --modal-bg: rgba(116, 185, 255, 0.95);
374
+ --modal-border: #74b9ff;
375
+ --modal-header-bg: linear-gradient(135deg, #74b9ff, #81ecec);
376
+
377
+ /* Settings Panel & Tabs */
378
+ --settings-tab-active-bg: #74b9ff;
379
+ --settings-section-border: #3a4a7a;
380
+ --settings-tab-border: #74b9ff;
381
+
382
+ /* Slider Components */
383
+ --slider-value-border: #0984e3;
384
+
385
+ /* Toggle Switch */
386
+ --switch-bg-active: linear-gradient(90deg, #74b9ff, #0984e3);
387
+
388
+ /* Mic Button & Pulse Effect */
389
+ --mic-button-border: #74b9ff;
390
+ --mic-button-shadow: 0 0 15px #74b9ff;
391
+ --mic-button-hover-bg: #74b9ff;
392
+ --mic-button-hover-shadow: 0 0 10px rgba(116, 185, 255, 0.5);
393
+ --mic-listening-border: #0984e3;
394
+ --mic-listening-shadow: 0 0 15px #0984e3;
395
+ --mic-pulse-color: rgba(9, 132, 227, 0.5);
396
+
397
+ /* Cards & Stats */
398
+ --stat-value-color: #0984e3;
399
+
400
+ /* Plugin Cards */
401
+ --plugin-card-border: #74b9ff;
402
+ --plugin-type-badge-bg: #0984e3;
403
+ --plugin-active-badge-bg: linear-gradient(90deg, #74b9ff, #0984e3);
404
+
405
+ /* Help Modal */
406
+ --creator-name-color: #0984e3;
407
+ --creator-avatar-bg: linear-gradient(135deg, #74b9ff, #81ecec);
408
+ --creator-role-bg: linear-gradient(135deg, #74b9ff, #81ecec);
409
+ --creator-role-color: #0a2340;
410
+ --philosophy-bg: rgba(116, 185, 255, 0.1);
411
+ --philosophy-border: rgba(116, 185, 255, 0.3);
412
+ --philosophy-border-left: #74b9ff;
413
+ --feature-icon-color: #0984e3;
414
+ --feature-title-color: #0984e3;
415
+
416
+ /* Select Options */
417
+ --select-option-hover-bg: #74b9ff;
418
+ --select-option-checked-bg: #0984e3;
419
+
420
+ /* Model Colors */
421
+ --model-strength-color: #0984e3;
422
+ --model-strength-text: #fff;
423
+ --model-provider-color: #00b894;
424
+ --model-provider-text: #fff;
425
+
426
+ /* Text Colors */
427
+ --text-primary: #222;
428
+ --text-secondary: #555;
429
+
430
+ /* Character Selection Colors */
431
+ --character-selected-border: #0984e3;
432
+ --character-selected-bg: rgba(116, 185, 255, 0.13);
433
+ }
434
+
435
+ [data-theme="purple"] {
436
+ --primary-color: #a29bfe;
437
+ --primary-rgb: 162, 155, 254;
438
+ --secondary-color: #fd79a8;
439
+ --accent-color: #6c5ce7;
440
+ --background-overlay: rgba(162, 155, 254, 0.15);
441
+ --gradient-start: #a29bfe;
442
+ --gradient-end: #6c5ce7;
443
+ --text-glow: 0 0 10px rgba(162, 155, 254, 0.5);
444
+ --button-hover: rgba(162, 155, 254, 0.3);
445
+ --modal-title-color: #2d2250;
446
+ --switch-color: #a259ff;
447
+
448
+ /* UI Component Colors */
449
+ --chat-bg: rgba(162, 155, 254, 0.9);
450
+ --chat-border: #a29bfe;
451
+ --chat-message-user-bg: #a29bfe;
452
+ --chat-message-kimi-bg: rgba(255, 255, 255, 0.15);
453
+ --input-border: #a29bfe;
454
+ --input-focus-border: #6c5ce7;
455
+
456
+ /* Modal & Overlay Colors */
457
+ --modal-bg: rgba(162, 155, 254, 0.95);
458
+ --modal-border: #a29bfe;
459
+ --modal-header-bg: linear-gradient(135deg, #a29bfe, #fd79a8);
460
+
461
+ /* Settings Panel & Tabs */
462
+ --settings-tab-active-bg: #a29bfe;
463
+ --settings-section-border: #4a3a7a;
464
+ --settings-tab-border: #a29bfe;
465
+ --settings-bg: #2d2250;
466
+
467
+ /* Slider Components */
468
+ --slider-value-border: #6c5ce7;
469
+
470
+ /* Toggle Switch */
471
+ --switch-bg-active: linear-gradient(90deg, #a29bfe, #6c5ce7);
472
+
473
+ /* Mic Button & Pulse Effect */
474
+ --mic-button-border: #a29bfe;
475
+ --mic-button-shadow: 0 0 15px #a29bfe;
476
+ --mic-button-hover-bg: #a29bfe;
477
+ --mic-button-hover-shadow: 0 0 10px rgba(162, 155, 254, 0.5);
478
+ --mic-listening-border: #6c5ce7;
479
+ --mic-listening-shadow: 0 0 15px #6c5ce7;
480
+ --mic-pulse-color: rgba(108, 92, 231, 0.5);
481
+
482
+ /* Cards & Stats */
483
+ --stat-value-color: #6c5ce7;
484
+
485
+ /* Plugin Cards */
486
+ --plugin-card-border: #a29bfe;
487
+ --plugin-type-badge-bg: #6c5ce7;
488
+ --plugin-active-badge-bg: linear-gradient(90deg, #a29bfe, #6c5ce7);
489
+
490
+ /* Help Modal */
491
+ --creator-name-color: #6c5ce7;
492
+ --creator-avatar-bg: linear-gradient(135deg, #a29bfe, #fd79a8);
493
+ --creator-role-bg: linear-gradient(135deg, #a29bfe, #fd79a8);
494
+ --creator-role-color: #2d2250;
495
+ --philosophy-bg: rgba(162, 155, 254, 0.1);
496
+ --philosophy-border: rgba(162, 155, 254, 0.3);
497
+ --philosophy-border-left: #a29bfe;
498
+ --feature-icon-color: #6c5ce7;
499
+ --feature-title-color: #6c5ce7;
500
+
501
+ /* Select Options */
502
+ --select-option-hover-bg: #a29bfe;
503
+ --select-option-checked-bg: #6c5ce7;
504
+
505
+ /* Model Colors */
506
+ --model-strength-color: #6c5ce7;
507
+ --model-strength-text: #fff;
508
+ --model-provider-color: #fd79a8;
509
+ --model-provider-text: #fff;
510
+
511
+ /* Text Colors */
512
+ --text-primary: #2d2250;
513
+ --text-secondary: #555;
514
+
515
+ /* Character Selection Colors */
516
+ --character-selected-border: #6c5ce7;
517
+ --character-selected-bg: rgba(162, 155, 254, 0.13);
518
+
519
+ /* Additional Purple Theme Variables */
520
+ --button-primary-bg: var(--primary-color);
521
+ --button-primary-text: var(--text-on-primary);
522
+ --button-primary-hover-bg: var(--accent-color);
523
+ --button-secondary-bg: rgba(255, 255, 255, 0.1);
524
+ --button-secondary-text: var(--text-on-background);
525
+ --guide-step-number-color: var(--primary-color);
526
+ --guide-step-number-bg: rgba(162, 155, 254, var(--hover-opacity));
527
+ --waiting-indicator-color: var(--primary-color);
528
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
529
+ }
530
+
531
+ [data-theme="green"] {
532
+ --primary-color: #27ae60;
533
+ --primary-rgb: 39, 174, 96;
534
+ --secondary-color: #2ecc71;
535
+ --accent-color: #16a085;
536
+ --background-overlay: rgba(39, 174, 96, 0.15);
537
+ --gradient-start: #27ae60;
538
+ --gradient-end: #16a085;
539
+ --text-glow: 0 0 10px rgba(39, 174, 96, 0.5);
540
+ --button-hover: rgba(39, 174, 96, 0.3);
541
+ --modal-title-color: #1a3d2e;
542
+ --switch-color: #27ae60;
543
+
544
+ /* Contrast and Accessibility for Green Theme */
545
+ --text-on-primary: #ffffff;
546
+ --text-on-secondary: #1a3d2e;
547
+ --text-on-accent: #ffffff;
548
+ --text-on-background: #ffffff;
549
+
550
+ /* UI Component Colors */
551
+ --chat-bg: rgba(39, 174, 96, 0.9);
552
+ --chat-border: #27ae60;
553
+ --chat-text: var(--text-on-primary);
554
+ --chat-message-user-bg: #27ae60;
555
+ --chat-message-user-text: var(--text-on-primary);
556
+ --chat-message-kimi-bg: rgba(255, 255, 255, 0.15);
557
+ --chat-message-kimi-text: var(--text-on-background);
558
+ --input-border: #27ae60;
559
+ --input-focus-border: #16a085;
560
+ --input-text: var(--text-on-background);
561
+
562
+ /* Modal & Overlay Colors */
563
+ --modal-bg: rgba(39, 174, 96, 0.95);
564
+ --modal-border: #27ae60;
565
+ --modal-header-bg: linear-gradient(135deg, #27ae60, #2ecc71);
566
+ --modal-text: var(--text-on-primary);
567
+
568
+ /* Settings Panel & Tabs */
569
+ --settings-text: var(--text-on-background);
570
+ --settings-tab-active-bg: #27ae60;
571
+ --settings-tab-active-color: var(--text-on-primary);
572
+ --settings-section-border: #27ae60;
573
+ --settings-tab-border: #27ae60;
574
+ --settings-section-header-color: var(--text-on-background);
575
+
576
+ /* Button Colors */
577
+ --button-primary-bg: var(--primary-color);
578
+ --button-primary-text: var(--text-on-primary);
579
+ --button-primary-hover-bg: var(--accent-color);
580
+ --button-secondary-bg: rgba(255, 255, 255, 0.1);
581
+ --button-secondary-text: var(--text-on-background);
582
+
583
+ /* Slider Components */
584
+ --slider-value-border: #16a085;
585
+ --slider-thumb-bg: var(--primary-color);
586
+ --slider-thumb-hover-bg: var(--accent-color);
587
+
588
+ /* Toggle Switch */
589
+ --switch-bg-active: linear-gradient(90deg, #27ae60, #16a085);
590
+
591
+ /* Mic Button & Pulse Effect */
592
+ --mic-button-border: #27ae60;
593
+ --mic-button-shadow: 0 0 15px #27ae60;
594
+ --mic-button-hover-bg: #27ae60;
595
+ --mic-button-hover-shadow: 0 0 10px rgba(39, 174, 96, 0.5);
596
+ --mic-button-icon-color: var(--text-on-primary);
597
+ --mic-listening-border: #16a085;
598
+ --mic-listening-shadow: 0 0 15px #16a085;
599
+ --mic-pulse-color: rgba(22, 160, 133, 0.5);
600
+ --mic-pulse-listening-color: rgba(22, 160, 133, 0.4);
601
+
602
+ /* Cards & Stats */
603
+ --card-text: var(--text-on-background);
604
+ --stat-value-color: #16a085;
605
+
606
+ /* Plugin Cards */
607
+ --plugin-card-border: #27ae60;
608
+ --plugin-card-title-color: var(--text-on-background);
609
+ --plugin-card-desc-color: #e0cfe6;
610
+ --plugin-card-author-color: #bfa6b6;
611
+ --plugin-type-badge-bg: #16a085;
612
+ --plugin-type-badge-text: var(--text-on-accent);
613
+ --plugin-active-badge-bg: linear-gradient(90deg, #27ae60, #16a085);
614
+ --plugin-active-badge-text: var(--text-on-primary);
615
+
616
+ /* Help Modal */
617
+ --creator-name-color: #16a085;
618
+ --creator-avatar-bg: linear-gradient(135deg, #27ae60, #2ecc71);
619
+ --creator-role-bg: linear-gradient(135deg, #27ae60, #2ecc71);
620
+ --creator-role-color: var(--text-on-secondary);
621
+ --philosophy-bg: rgba(39, 174, 96, 0.1);
622
+ --philosophy-border: rgba(39, 174, 96, var(--border-opacity));
623
+ --philosophy-border-left: #27ae60;
624
+ --feature-icon-color: #147190;
625
+ --feature-title-color: #147190;
626
+ --guide-step-number-color: var(--primary-color);
627
+ --guide-step-number-bg: rgba(39, 174, 96, var(--hover-opacity));
628
+
629
+ /* Select Options */
630
+ --select-option-hover-bg: #27ae60;
631
+ --select-option-checked-bg: #16a085;
632
+
633
+ /* Model Colors */
634
+ --model-strength-color: #27ae60;
635
+ --model-strength-text: var(--text-on-primary);
636
+ --model-provider-color: #2ecc71;
637
+ --model-provider-text: var(--text-on-primary);
638
+
639
+ /* Text Colors */
640
+ --text-primary: var(--text-on-background);
641
+ --text-secondary: #4e6151;
642
+ --text-muted: rgba(255, 255, 255, 0.6);
643
+
644
+ /* Character Selection Colors */
645
+ --character-selected-border: #16a085;
646
+ --character-selected-bg: rgba(39, 174, 96, 0.13);
647
+ --character-name-color: var(--text-on-background);
648
+
649
+ /* Additional Green Theme Variables */
650
+ --waiting-indicator-color: var(--primary-color);
651
+ --loading-spinner-color: var(--primary-color);
652
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
653
+ }
654
+
655
+ [data-theme="dark"] {
656
+ --primary-color: #5e60ce;
657
+ --primary-rgb: 94, 96, 206;
658
+ --secondary-color: #23262f;
659
+ --accent-color: #8b5cf6;
660
+ --background-overlay: rgba(24, 26, 32, 0.85);
661
+ --gradient-start: #5e60ce;
662
+ --gradient-end: #8b5cf6;
663
+ --text-glow: 0 0 10px rgba(94, 96, 206, 0.3);
664
+ --button-hover: rgba(94, 96, 206, 0.15);
665
+ --modal-title-color: #e0e0e0;
666
+ --switch-color: #5e60ce;
667
+
668
+ /* Contrast and Accessibility for Dark Theme */
669
+ --text-on-primary: #ffffff;
670
+ --text-on-secondary: #e0e0e0;
671
+ --text-on-accent: #ffffff;
672
+ --text-on-background: #e0e0e0;
673
+
674
+ /* UI Component Colors */
675
+ --chat-bg: rgba(24, 26, 32, 0.95);
676
+ --chat-border: #5e60ce;
677
+ --chat-text: var(--text-on-background);
678
+ --chat-message-user-bg: #1e253c;
679
+ --chat-message-user-text: var(--text-on-background);
680
+ --chat-message-kimi-bg: #23262f;
681
+ --chat-message-kimi-text: var(--text-on-background);
682
+ --input-border: #5e60ce;
683
+ --input-focus-border: #8b5cf6;
684
+ --input-text: var(--text-on-background);
685
+
686
+ /* Modal & Overlay Colors */
687
+ --modal-bg: rgba(24, 26, 32, 0.98);
688
+ --modal-border: #5e60ce;
689
+ --modal-header-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
690
+ --modal-text: var(--text-on-background);
691
+
692
+ /* Settings Panel & Tabs */
693
+ --settings-bg: #0f1114;
694
+ --settings-text: var(--text-on-background);
695
+ --settings-tab-bg: #181a20;
696
+ --settings-tab-active-bg: #5e60ce;
697
+ --settings-tab-active-color: var(--text-on-primary);
698
+ --settings-section-bg: #1a1d23;
699
+ --settings-section-border: rgba(94, 96, 206, var(--border-opacity));
700
+ --settings-tab-border: #5e60ce;
701
+ --settings-section-header-color: var(--text-on-background);
702
+
703
+ /* Button Colors */
704
+ --button-primary-bg: var(--primary-color);
705
+ --button-primary-text: var(--text-on-primary);
706
+ --button-primary-hover-bg: var(--accent-color);
707
+ --button-secondary-bg: rgba(255, 255, 255, 0.05);
708
+ --button-secondary-text: var(--text-on-background);
709
+
710
+ /* Slider Components */
711
+ --slider-value-bg: #0f1114;
712
+ --slider-value-border: #5e60ce;
713
+ --slider-value-color: var(--text-on-background);
714
+ --slider-thumb-bg: var(--primary-color);
715
+ --slider-thumb-hover-bg: var(--accent-color);
716
+
717
+ /* Toggle Switch */
718
+ --switch-bg-inactive: rgba(255, 255, 255, 0.05);
719
+ --switch-bg-active: linear-gradient(90deg, #5e60ce, #8b5cf6);
720
+
721
+ /* Mic Button & Pulse Effect */
722
+ --mic-button-border: #5e60ce;
723
+ --mic-button-shadow: 0 0 15px #5e60ce;
724
+ --mic-button-hover-bg: #5e60ce;
725
+ --mic-button-hover-shadow: 0 0 10px rgba(94, 96, 206, 0.5);
726
+ --mic-button-icon-color: var(--text-on-primary);
727
+ --mic-listening-border: #8b5cf6;
728
+ --mic-listening-shadow: 0 0 15px #8b5cf6;
729
+ --mic-pulse-color: rgba(139, 92, 246, 0.5);
730
+ --mic-pulse-listening-color: rgba(139, 92, 246, 0.4);
731
+
732
+ /* Cards & Stats */
733
+ --card-bg: rgba(255, 255, 255, 0.02);
734
+ --card-border: rgba(255, 255, 255, 0.05);
735
+ --card-hover-bg: rgba(255, 255, 255, 0.05);
736
+ --card-text: var(--text-on-background);
737
+ --stat-value-color: #8b5cf6;
738
+
739
+ /* Plugin Cards */
740
+ --plugin-card-bg: linear-gradient(135deg, #1a1d23 80%, rgba(24, 26, 32, 0.98) 100%);
741
+ --plugin-card-border: #5e60ce;
742
+ --plugin-card-title-color: var(--text-on-background);
743
+ --plugin-card-desc-color: rgba(224, 224, 224, 0.7);
744
+ --plugin-card-author-color: rgba(224, 224, 224, 0.5);
745
+ --plugin-type-badge-bg: #8b5cf6;
746
+ --plugin-type-badge-text: var(--text-on-accent);
747
+ --plugin-active-badge-bg: linear-gradient(90deg, #5e60ce, #8b5cf6);
748
+ --plugin-active-badge-text: var(--text-on-primary);
749
+
750
+ /* Help Modal */
751
+ --help-modal-bg: rgba(24, 26, 32, 0.98);
752
+ --help-modal-border: #5e60ce;
753
+ --creator-card-bg: rgba(255, 255, 255, 0.02);
754
+ --creator-card-border: rgba(255, 255, 255, 0.05);
755
+ --creator-name-color: #8b5cf6;
756
+ --creator-avatar-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
757
+ --creator-role-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
758
+ --creator-role-color: var(--text-on-primary);
759
+ --philosophy-bg: rgba(94, 96, 206, 0.05);
760
+ --philosophy-border: rgba(94, 96, 206, var(--border-opacity));
761
+ --philosophy-border-left: #5e60ce;
762
+ --feature-item-bg: rgba(255, 255, 255, 0.02);
763
+ --feature-item-border: rgba(94, 96, 206, var(--border-opacity));
764
+ --feature-icon-color: #8b5cf6;
765
+ --feature-title-color: #8b5cf6;
766
+ --feature-text-color: rgba(224, 224, 224, 0.7);
767
+ --guide-step-bg: rgba(255, 255, 255, 0.02);
768
+ --guide-step-number-color: var(--primary-color);
769
+ --guide-step-number-bg: rgba(94, 96, 206, var(--hover-opacity));
770
+ --tip-item-bg: rgba(255, 255, 255, 0.02);
771
+
772
+ /* Select Options */
773
+ --select-option-bg: rgba(24, 26, 32, 0.95);
774
+ --select-option-text: var(--text-on-background);
775
+ --select-option-hover-bg: #5e60ce;
776
+ --select-option-checked-bg: #8b5cf6;
777
+
778
+ /* Model Colors */
779
+ --model-card-bg: rgba(255, 255, 255, 0.02);
780
+ --model-card-border: rgba(255, 255, 255, 0.05);
781
+ --model-card-hover-bg: rgba(255, 255, 255, 0.05);
782
+ --model-card-selected-border: var(--primary-color);
783
+ --model-name-color: var(--text-on-background);
784
+ --model-description-color: rgba(224, 224, 224, 0.7);
785
+ --model-strength-color: #5e60ce;
786
+ --model-strength-text: var(--text-on-primary);
787
+ --model-provider-color: #8b5cf6;
788
+ --model-provider-text: var(--text-on-accent);
789
+
790
+ /* Text Colors */
791
+ --text-primary: var(--text-on-background);
792
+ --text-secondary: #9ca3af;
793
+ --text-muted: rgba(224, 224, 224, 0.6);
794
+
795
+ /* Character Selection Colors */
796
+ --character-card: rgba(255, 255, 255, 0.02);
797
+ --character-card-border: rgba(255, 255, 255, 0.05);
798
+ --character-card-hover-bg: rgba(255, 255, 255, 0.05);
799
+ --character-selected-border: #8b5cf6;
800
+ --character-selected-bg: rgba(94, 96, 206, 0.1);
801
+ --character-name-color: var(--text-on-background);
802
+
803
+ /* Additional Dark Theme Variables */
804
+ --waiting-indicator-color: var(--primary-color);
805
+ --loading-spinner-color: var(--primary-color);
806
+ --progress-bg: rgba(255, 255, 255, 0.05);
807
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
808
+ --progress-text-color: var(--text-on-background);
809
+ --transcript-bg: rgba(0, 0, 0, 0.9);
810
+ --transcript-text: var(--text-on-background);
811
+ --transcript-border: var(--primary-color);
812
+ }
813
+
814
+ /* ===== ANIMATION MANAGEMENT ===== */
815
+ /* Respect user's reduced motion preference. When user requests reduced motion,
816
+ disable animations and transitions globally while preserving critical
817
+ animations (mic button, loading screen). This preserves accessibility
818
+ without relying on a UI toggle. */
819
+ @media (prefers-reduced-motion: reduce) {
820
+ *,
821
+ *::before,
822
+ *::after {
823
+ animation: none !important;
824
+ transition: none !important;
825
+ }
826
+
827
+ /* Keep mic button animations even when reduced-motion is requested */
828
+ .mic-button,
829
+ .mic-button *,
830
+ .mic-button::after {
831
+ animation: revert !important;
832
+ transition: revert !important;
833
+ }
834
+
835
+ /* Keep loading screen animations */
836
+ #loading-screen,
837
+ #loading-screen * {
838
+ animation: revert !important;
839
+ transition: revert !important;
840
+ }
841
+ }
842
+
843
+ /* Ensure critical hover effects remain functional */
844
+ body.animations-enabled .kimi-button:hover,
845
+ body.animations-enabled .control-button-unified:hover {
846
+ transform: translateY(-2px);
847
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
848
+ }
849
+
850
+ body.animations-enabled .mic-button:hover {
851
+ transform: scale(1.1);
852
+ transition: all 0.2s ease;
853
+ }
854
+
855
+ /* ===== LOADING SCREEN ===== */
856
+ #loading-screen {
857
+ position: fixed;
858
+ top: 0;
859
+ left: 0;
860
+ width: 100%;
861
+ height: 100%;
862
+ background-color: var(--background-primary, #1a1a1a);
863
+ z-index: 10000;
864
+ display: flex;
865
+ justify-content: center;
866
+ align-items: center;
867
+ opacity: 1;
868
+ transition: opacity 0.5s ease-out;
869
+ }
870
+
871
+ #loading-screen img {
872
+ max-width: 200px;
873
+ max-height: 200px;
874
+ animation: loadingPulse 2s infinite ease-in-out;
875
+ }
876
+
877
+ @keyframes loadingPulse {
878
+ 0%,
879
+ 100% {
880
+ opacity: 0.7;
881
+ transform: scale(1);
882
+ }
883
+ 50% {
884
+ opacity: 1;
885
+ transform: scale(1.05);
886
+ }
887
+ }
888
+
889
+ /* ===== GLOBAL STYLES ===== */
890
+ * {
891
+ margin: 0;
892
+ padding: 0;
893
+ box-sizing: border-box;
894
+ }
895
+
896
+ html,
897
+ body {
898
+ height: 100%;
899
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
900
+ color: white;
901
+ overflow: hidden;
902
+ }
903
+
904
+ /* === Global Help Button (compressed) === */
905
+ .top-right-buttons {
906
+ position: fixed;
907
+ top: 5px;
908
+ right: 5px;
909
+ display: flex;
910
+ gap: 5px;
911
+ z-index: 1000;
912
+ pointer-events: none;
913
+ }
914
+
915
+ /* Configurable design tokens for quick tuning */
916
+ #global-help-button {
917
+ --help-btn-bg-alpha: 0.12; /* base background alpha */
918
+ --help-btn-bg-hover-alpha: 0.2; /* hover/focus background alpha */
919
+ --help-btn-bg-active-alpha: 0.25; /* active press background alpha */
920
+ --help-btn-icon-alpha: 0.3; /* base icon opacity */
921
+ --help-btn-icon-hover-alpha: 0.65; /* hover/focus icon opacity */
922
+ --help-btn-icon-active-alpha: 0.8; /* active icon opacity */
923
+ --help-btn-border-alpha: 0.28; /* border alpha */
924
+ --help-btn-shadow-base: 0 2px 6px rgba(var(--primary-rgb), 0.12);
925
+ --help-btn-shadow-hover: 0 4px 16px rgba(var(--primary-rgb), 0.28);
926
+ width: 36px;
927
+ height: 36px;
928
+ pointer-events: auto;
929
+ background: rgba(var(--primary-rgb), var(--help-btn-bg-alpha));
930
+ border: 1px solid rgba(var(--primary-rgb), var(--help-btn-border-alpha));
931
+ box-shadow: var(--help-btn-shadow-base);
932
+ backdrop-filter: blur(9px) saturate(115%);
933
+ opacity: 0.26;
934
+ display: flex;
935
+ align-items: center;
936
+ justify-content: center;
937
+ transition:
938
+ background 0.35s ease,
939
+ box-shadow 0.35s ease,
940
+ transform 0.35s ease,
941
+ opacity 0.4s ease;
942
+ }
943
+ #global-help-button i {
944
+ font-size: 1.2rem;
945
+ opacity: var(--help-btn-icon-alpha);
946
+ transition:
947
+ opacity 0.4s ease,
948
+ transform 0.35s ease;
949
+ transform: scale(0.9);
950
+ }
951
+
952
+ /* Hover + focus + container hover unify */
953
+ #global-help-button:hover,
954
+ #global-help-button:focus-visible,
955
+ .top-right-buttons:hover #global-help-button {
956
+ background: rgba(var(--primary-rgb), var(--help-btn-bg-hover-alpha));
957
+ box-shadow: var(--help-btn-shadow-hover);
958
+ opacity: 1;
959
+ }
960
+
961
+ #global-help-button:hover i,
962
+ #global-help-button:focus-visible i,
963
+ .top-right-buttons:hover #global-help-button i {
964
+ opacity: var(--help-btn-icon-hover-alpha);
965
+ transform: scale(1);
966
+ }
967
+
968
+ #global-help-button:active {
969
+ background: rgba(var(--primary-rgb), var(--help-btn-bg-active-alpha));
970
+ transform: scale(0.95);
971
+ }
972
+ #global-help-button:active i {
973
+ opacity: var(--help-btn-icon-active-alpha);
974
+ }
975
+ #global-help-button:focus-visible {
976
+ outline: 2px solid rgba(var(--primary-rgb), 0.6);
977
+ outline-offset: 2px;
978
+ }
979
+
980
+ .video-container {
981
+ position: fixed;
982
+ top: 0;
983
+ left: 0;
984
+ width: 100%;
985
+ height: 100%;
986
+ z-index: -1;
987
+ background-color: #1a1a1a;
988
+ }
989
+
990
+ .bg-video.active {
991
+ opacity: 1;
992
+ }
993
+
994
+ .bg-video {
995
+ position: absolute;
996
+ top: 0;
997
+ left: 0;
998
+ width: 100%;
999
+ height: 100%;
1000
+ object-fit: contain;
1001
+ opacity: 0;
1002
+ transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1);
1003
+ background-color: #1a1a1a;
1004
+ will-change: opacity;
1005
+ backface-visibility: hidden;
1006
+ }
1007
+
1008
+ .bg-video.transitioning {
1009
+ opacity: 0;
1010
+ transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1);
1011
+ pointer-events: none;
1012
+ }
1013
+
1014
+ .content-overlay {
1015
+ position: relative;
1016
+ height: 100vh;
1017
+ width: 100%;
1018
+ display: flex;
1019
+ flex-direction: column;
1020
+ justify-content: flex-end;
1021
+ align-items: center;
1022
+ padding: 20px;
1023
+ background-color: var(--background-overlay);
1024
+ opacity: var(--interface-opacity);
1025
+ z-index: 1;
1026
+ }
1027
+
1028
+ .top-bar {
1029
+ width: 100%;
1030
+ max-width: 500px;
1031
+ text-align: center;
1032
+ margin-top: 10px;
1033
+ }
1034
+
1035
+ .top-bar label {
1036
+ font-size: 1rem;
1037
+ font-weight: 600;
1038
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
1039
+ margin-bottom: 8px;
1040
+ display: block;
1041
+ }
1042
+
1043
+ .progress-container {
1044
+ width: 100%;
1045
+ height: 12px;
1046
+ background-color: var(--progress-bg);
1047
+ border-radius: 10px;
1048
+ overflow: hidden;
1049
+ }
1050
+
1051
+ .progress-fill {
1052
+ height: 100%;
1053
+ width: 50%; /* Changed from 65% to match new default favorability level */
1054
+ background: var(--progress-fill-bg);
1055
+ border-radius: 10px;
1056
+ transition: width 0.5s ease-in-out;
1057
+ box-shadow: var(--text-glow);
1058
+ }
1059
+
1060
+ /* Center content styles can be added here if needed */
1061
+
1062
+ .transcript-container {
1063
+ position: absolute;
1064
+ bottom: 180px;
1065
+ left: 50%;
1066
+ transform: translateX(-50%);
1067
+ width: 80%;
1068
+ max-width: 580px;
1069
+ min-width: 280px;
1070
+ max-height: 400px;
1071
+ min-height: 100px;
1072
+ padding: 15px;
1073
+ background: var(--transcript-bg);
1074
+ backdrop-filter: blur(10px);
1075
+ border-radius: 10px;
1076
+ border: 1px solid var(--transcript-border);
1077
+ text-align: center;
1078
+ opacity: 0;
1079
+ transition: opacity 0.3s ease-in-out;
1080
+ pointer-events: none;
1081
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1082
+ overflow-y: auto;
1083
+ overflow-x: hidden;
1084
+ z-index: 100;
1085
+ }
1086
+
1087
+ .transcript-container.visible {
1088
+ opacity: 1;
1089
+ pointer-events: auto;
1090
+ }
1091
+
1092
+ /* Custom scrollbar for transcript container */
1093
+ .transcript-container::-webkit-scrollbar {
1094
+ width: var(--scrollbar-width);
1095
+ }
1096
+
1097
+ .transcript-container::-webkit-scrollbar-track {
1098
+ background: var(--scrollbar-track-bg);
1099
+ border-radius: 4px;
1100
+ }
1101
+
1102
+ .transcript-container::-webkit-scrollbar-thumb {
1103
+ background: var(--scrollbar-thumb-bg);
1104
+ border-radius: 4px;
1105
+ transition: background 0.3s ease;
1106
+ }
1107
+
1108
+ .transcript-container::-webkit-scrollbar-thumb:hover {
1109
+ background: var(--scrollbar-thumb-hover-bg);
1110
+ }
1111
+
1112
+ #transcript {
1113
+ font-size: 1.2rem;
1114
+ color: var(--transcript-text);
1115
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
1116
+ margin: 0;
1117
+ line-height: 1.3;
1118
+ text-align: left;
1119
+ }
1120
+
1121
+ /* ===== ACCESSIBILITY - FOCUS STYLES ===== */
1122
+ select:focus,
1123
+ input:focus,
1124
+ button:focus,
1125
+ .kimi-slider:focus,
1126
+ .kimi-slider-unified:focus {
1127
+ box-shadow: 0 0 0 2px var(--primary-pink);
1128
+ border-color: var(--primary-pink);
1129
+ }
1130
+
1131
+ .control-button-unified:focus {
1132
+ outline: 2px solid var(--primary-pink);
1133
+ outline-offset: 2px;
1134
+ }
1135
+
1136
+ /* ===== CHAT INTERFACE ===== */
1137
+ .chat-container {
1138
+ position: fixed;
1139
+ top: 20px;
1140
+ right: 20px;
1141
+ z-index: 1000;
1142
+ width: 400px;
1143
+ max-width: calc(100vw - 40px);
1144
+ height: 600px;
1145
+ max-height: 80vh;
1146
+ background: var(--chat-bg);
1147
+ backdrop-filter: blur(20px);
1148
+ border-radius: 15px;
1149
+ border: 1px solid var(--chat-border);
1150
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
1151
+ display: none;
1152
+ flex-direction: column;
1153
+ overflow: hidden;
1154
+ transform: translateX(400px);
1155
+ opacity: 0;
1156
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1157
+ }
1158
+
1159
+ .chat-container.visible {
1160
+ display: flex;
1161
+ transform: translateX(0);
1162
+ opacity: 1;
1163
+ }
1164
+
1165
+ .chat-header {
1166
+ background: var(--chat-header-bg);
1167
+ padding: 15px 20px;
1168
+ display: flex;
1169
+ justify-content: space-between;
1170
+ align-items: center;
1171
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
1172
+ }
1173
+
1174
+ .chat-header h3 {
1175
+ margin: 0;
1176
+ color: var(--chat-text);
1177
+ font-size: 1.1rem;
1178
+ display: flex;
1179
+ align-items: center;
1180
+ gap: 8px;
1181
+ }
1182
+
1183
+ .chat-messages {
1184
+ flex: 1;
1185
+ padding: 15px;
1186
+ overflow-y: auto;
1187
+ display: flex;
1188
+ flex-direction: column;
1189
+ gap: 10px;
1190
+ }
1191
+
1192
+ .message {
1193
+ max-width: 95%;
1194
+ padding: 12px 16px;
1195
+ border-radius: 18px;
1196
+ font-size: 0.95rem;
1197
+ line-height: 1.3; /* Espacement entre lignes dans un même paragraphe */
1198
+ white-space: normal; /* Plus besoin de pre-line avec les <p> */
1199
+ animation: messageSlideIn 0.3s ease-out;
1200
+ }
1201
+
1202
+ /* Contrôle de l'espacement entre paragraphes (sauts de ligne) */
1203
+ .message p {
1204
+ margin: 0 0 0.8em 0; /* Espacement entre paragraphes */
1205
+ }
1206
+
1207
+ .message p:last-child {
1208
+ margin-bottom: 0; /* Pas d'espacement après le dernier paragraphe */
1209
+ }
1210
+
1211
+ .message.user {
1212
+ align-self: flex-end;
1213
+ background: var(--chat-message-user-bg);
1214
+ color: var(--chat-message-user-text);
1215
+ }
1216
+
1217
+ .message.kimi {
1218
+ align-self: flex-start;
1219
+ background: var(--chat-message-kimi-bg);
1220
+ color: var(--chat-message-kimi-text);
1221
+ }
1222
+
1223
+ .message-time {
1224
+ font-size: 0.75rem;
1225
+ color: rgba(255, 255, 255, 0.6);
1226
+ margin-top: 4px;
1227
+ text-align: right;
1228
+ }
1229
+
1230
+ .delete-message-btn {
1231
+ background: none;
1232
+ border: none;
1233
+ color: rgba(255, 255, 255, 0.4);
1234
+ cursor: pointer;
1235
+ padding: 2px 4px;
1236
+ border-radius: 3px;
1237
+ font-size: 0.7rem;
1238
+ margin-left: 8px;
1239
+ transition: all 0.2s ease;
1240
+ }
1241
+
1242
+ .delete-message-btn:hover {
1243
+ color: #ff4757 !important;
1244
+ background: rgba(255, 71, 87, 0.1) !important;
1245
+ }
1246
+
1247
+ .chat-input-container {
1248
+ padding: 15px 20px;
1249
+ display: flex;
1250
+ gap: 10px;
1251
+ background: rgba(255, 255, 255, 0.05);
1252
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
1253
+ }
1254
+
1255
+ #chat-input {
1256
+ flex: 1;
1257
+ background: var(--chat-input-bg);
1258
+ border: 1px solid rgba(255, 255, 255, 0.2);
1259
+ border-radius: 20px;
1260
+ padding: 10px 15px;
1261
+ color: var(--chat-input-text);
1262
+ font-size: 0.9rem;
1263
+ outline: none;
1264
+ transition: all 0.3s ease;
1265
+
1266
+ /* Make textarea behave like the previous single-line input */
1267
+ box-sizing: border-box;
1268
+ resize: none; /* prevent manual resizing */
1269
+ /* show approximately 2 lines by default, allow up to ~4 lines */
1270
+ min-height: 58px;
1271
+ max-height: 160px; /* allow multi-line but limit growth */
1272
+ line-height: 1.2;
1273
+ overflow: auto;
1274
+ }
1275
+
1276
+ #chat-input::placeholder {
1277
+ color: var(--chat-input-placeholder);
1278
+ }
1279
+
1280
+ #chat-input:focus {
1281
+ border-color: var(--primary-color);
1282
+ box-shadow: 0 0 0 2px rgba(255, 107, 157, 0.2);
1283
+ }
1284
+
1285
+ #send-button {
1286
+ background: var(--primary-color);
1287
+ border: none;
1288
+ border-radius: 20px;
1289
+ width: 40px;
1290
+ height: 40px;
1291
+ display: flex;
1292
+ align-items: center;
1293
+ justify-content: center;
1294
+ color: white;
1295
+ cursor: pointer;
1296
+ transition: all 0.3s ease;
1297
+ }
1298
+
1299
+ #send-button:hover {
1300
+ background: var(--accent-color);
1301
+ transform: scale(1.05);
1302
+ }
1303
+
1304
+ .chat-toggle,
1305
+ .chat-delete {
1306
+ background: none;
1307
+ border: none;
1308
+ color: var(--chat-text);
1309
+ cursor: pointer;
1310
+ padding: 5px;
1311
+ border-radius: 5px;
1312
+ transition: all 0.3s ease;
1313
+ }
1314
+
1315
+ .chat-delete {
1316
+ color: rgba(255, 255, 255, 0.7);
1317
+ }
1318
+
1319
+ .chat-delete:hover {
1320
+ color: #ff4757;
1321
+ background: rgba(255, 71, 87, 0.1);
1322
+ }
1323
+
1324
+ .chat-toggle:hover {
1325
+ background: rgba(255, 255, 255, 0.1);
1326
+ }
1327
+
1328
+ /* ===== UNIFIED BUTTON COMPONENTS ===== */
1329
+ .kimi-button,
1330
+ .control-button-unified {
1331
+ background: var(--button-primary-bg);
1332
+ border: none;
1333
+ border-radius: 8px;
1334
+ color: var(--button-primary-text);
1335
+ padding: 10px 20px;
1336
+ font-size: 0.9rem;
1337
+ font-weight: 500;
1338
+ cursor: pointer;
1339
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1340
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1341
+ position: relative;
1342
+ overflow: hidden;
1343
+ backdrop-filter: blur(10px);
1344
+ border: 1px solid rgba(255, 255, 255, 0.1);
1345
+ }
1346
+
1347
+ .kimi-button::before,
1348
+ .control-button-unified::before {
1349
+ content: "";
1350
+ position: absolute;
1351
+ top: 0;
1352
+ left: -100%;
1353
+ width: 100%;
1354
+ height: 100%;
1355
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
1356
+ transition: left 0.5s ease;
1357
+ }
1358
+
1359
+ .kimi-button:hover,
1360
+ .control-button-unified:hover {
1361
+ transform: translateY(-2px);
1362
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
1363
+ background: var(--button-primary-hover-bg);
1364
+ }
1365
+
1366
+ .kimi-button:hover::before,
1367
+ .control-button-unified:hover::before {
1368
+ left: 100%;
1369
+ }
1370
+
1371
+ .kimi-button:active,
1372
+ .control-button-unified:active {
1373
+ transform: translateY(0);
1374
+ transition: all 0.1s ease;
1375
+ }
1376
+
1377
+ .kimi-button:disabled,
1378
+ .control-button-unified:disabled {
1379
+ opacity: 0.5;
1380
+ cursor: not-allowed;
1381
+ transform: none;
1382
+ }
1383
+
1384
+ /* Button Variants */
1385
+ .kimi-button.danger {
1386
+ background: var(--button-danger-bg);
1387
+ color: var(--button-danger-text);
1388
+ }
1389
+ .kimi-button.danger:hover {
1390
+ background: var(--button-danger-hover-bg);
1391
+ }
1392
+ .kimi-button.success {
1393
+ background: linear-gradient(135deg, #26de81, #20bf6b);
1394
+ }
1395
+ .kimi-button.success:hover {
1396
+ background: linear-gradient(135deg, #20bf6b, #26de81);
1397
+ }
1398
+ .kimi-button.secondary {
1399
+ background: var(--button-secondary-bg);
1400
+ color: var(--button-secondary-text);
1401
+ }
1402
+ .kimi-button.secondary:hover {
1403
+ background: var(--button-secondary-hover-bg);
1404
+ }
1405
+
1406
+ /* Circular Control Buttons */
1407
+ .control-button-unified {
1408
+ width: 50px;
1409
+ height: 50px;
1410
+ border-radius: 50%;
1411
+ padding: 0;
1412
+ display: flex;
1413
+ align-items: center;
1414
+ justify-content: center;
1415
+ background: var(--button-hover);
1416
+ box-shadow: var(--mic-button-shadow);
1417
+ border: 1px solid var(--primary-color);
1418
+ }
1419
+
1420
+ .control-button-unified:hover {
1421
+ transform: translateY(-2px) scale(1.05);
1422
+ background: var(--primary-color);
1423
+ box-shadow: var(--text-glow);
1424
+ }
1425
+
1426
+ .control-button-unified i {
1427
+ font-size: 1.2rem;
1428
+ transition: transform 0.3s ease;
1429
+ }
1430
+
1431
+ .control-button-unified:hover i {
1432
+ transform: scale(1.1);
1433
+ }
1434
+
1435
+ /* ===== UNIFIED FORM COMPONENTS ===== */
1436
+
1437
+ /* Select Elements */
1438
+ .kimi-select,
1439
+ .kimi-select-unified {
1440
+ background: var(--select-bg);
1441
+ border: 1px solid var(--select-border);
1442
+ border-radius: 8px;
1443
+ color: var(--select-text);
1444
+ padding: 8px 12px;
1445
+ font-size: 0.9rem;
1446
+ outline: none;
1447
+ transition: all 0.3s ease;
1448
+ backdrop-filter: blur(10px);
1449
+ cursor: pointer;
1450
+ width: 100%;
1451
+ box-sizing: border-box;
1452
+ }
1453
+
1454
+ .kimi-select:hover,
1455
+ .kimi-select-unified:hover {
1456
+ background: var(--input-focus-bg);
1457
+ border-color: var(--accent-color);
1458
+ box-shadow: 0 0 10px var(--primary-color);
1459
+ }
1460
+
1461
+ .kimi-select:focus,
1462
+ .kimi-select-unified:focus {
1463
+ background: var(--input-focus-bg);
1464
+ border-color: var(--input-focus-border);
1465
+ box-shadow: 0 0 15px var(--primary-color);
1466
+ }
1467
+
1468
+ .kimi-select option,
1469
+ .kimi-select-unified option {
1470
+ background: var(--select-option-bg);
1471
+ color: var(--select-option-text);
1472
+ padding: 12px 15px;
1473
+ border: none;
1474
+ font-size: 0.9rem;
1475
+ transition: all 0.3s ease;
1476
+ }
1477
+
1478
+ .kimi-select option:hover,
1479
+ .kimi-select-unified option:hover,
1480
+ .kimi-select option:focus,
1481
+ .kimi-select-unified option:focus {
1482
+ background: var(--select-option-hover-bg);
1483
+ color: var(--select-option-hover-text);
1484
+ }
1485
+
1486
+ .kimi-select option:checked,
1487
+ .kimi-select-unified option:checked,
1488
+ .kimi-select option:selected,
1489
+ .kimi-select-unified option:selected {
1490
+ background: var(--select-option-checked-bg);
1491
+ color: var(--select-option-checked-text);
1492
+ font-weight: 600;
1493
+ box-shadow: 0 0 10px var(--primary-color);
1494
+ }
1495
+
1496
+ /* Input Elements */
1497
+ .kimi-input,
1498
+ .kimi-input-unified {
1499
+ background: var(--input-bg);
1500
+ border: 1px solid var(--input-border);
1501
+ border-radius: 8px;
1502
+ color: var(--input-text);
1503
+ padding: 8px 12px;
1504
+ font-size: 0.9rem;
1505
+ outline: none;
1506
+ transition: all 0.3s ease;
1507
+ backdrop-filter: blur(10px);
1508
+ width: 100%;
1509
+ box-sizing: border-box;
1510
+ }
1511
+
1512
+ .kimi-input:focus,
1513
+ .kimi-input-unified:focus {
1514
+ background: var(--input-focus-bg);
1515
+ border-color: var(--input-focus-border);
1516
+ box-shadow: 0 0 15px var(--primary-color);
1517
+ }
1518
+
1519
+ .kimi-input::placeholder,
1520
+ .kimi-input-unified::placeholder {
1521
+ color: var(--input-placeholder);
1522
+ }
1523
+
1524
+ /* Slider Elements */
1525
+ .kimi-slider,
1526
+ .kimi-slider-unified {
1527
+ -webkit-appearance: none;
1528
+ appearance: none;
1529
+ height: 6px;
1530
+ border-radius: 3px;
1531
+ background: var(--slider-track-bg);
1532
+ outline: none;
1533
+ transition: all 0.3s ease;
1534
+ cursor: pointer;
1535
+ }
1536
+
1537
+ .kimi-slider::-webkit-slider-thumb,
1538
+ .kimi-slider-unified::-webkit-slider-thumb {
1539
+ -webkit-appearance: none;
1540
+ appearance: none;
1541
+ width: 18px;
1542
+ height: 18px;
1543
+ border-radius: 50%;
1544
+ background: var(--slider-thumb-bg);
1545
+ cursor: pointer;
1546
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
1547
+ transition: all 0.3s ease;
1548
+ }
1549
+
1550
+ .kimi-slider::-webkit-slider-thumb:hover,
1551
+ .kimi-slider-unified::-webkit-slider-thumb:hover {
1552
+ transform: scale(1.2);
1553
+ background: var(--slider-thumb-hover-bg);
1554
+ box-shadow:
1555
+ 0 4px 15px rgba(0, 0, 0, 0.4),
1556
+ var(--text-glow);
1557
+ }
1558
+
1559
+ .kimi-slider::-moz-range-thumb,
1560
+ .kimi-slider-unified::-moz-range-thumb {
1561
+ width: 18px;
1562
+ height: 18px;
1563
+ border-radius: 50%;
1564
+ background: var(--slider-thumb-bg);
1565
+ cursor: pointer;
1566
+ border: none;
1567
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
1568
+ transition: all 0.3s ease;
1569
+ }
1570
+
1571
+ .kimi-slider::-moz-range-thumb:hover,
1572
+ .kimi-slider-unified::-moz-range-thumb:hover {
1573
+ transform: scale(1.2);
1574
+ background: var(--slider-thumb-hover-bg);
1575
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
1576
+ }
1577
+
1578
+ /* ===== CONSOLIDATED SCROLLBAR SYSTEM ===== */
1579
+ * {
1580
+ scrollbar-width: thin;
1581
+ scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-track-bg);
1582
+ }
1583
+
1584
+ *::-webkit-scrollbar {
1585
+ width: var(--scrollbar-width);
1586
+ height: var(--scrollbar-width);
1587
+ }
1588
+
1589
+ *::-webkit-scrollbar-track {
1590
+ background: var(--scrollbar-track-bg);
1591
+ border-radius: 4px;
1592
+ }
1593
+
1594
+ *::-webkit-scrollbar-thumb {
1595
+ background: var(--scrollbar-thumb-bg);
1596
+ border-radius: 4px;
1597
+ transition: all 0.3s ease;
1598
+ border: 1px solid rgba(255, 255, 255, 0.1);
1599
+ }
1600
+
1601
+ *::-webkit-scrollbar-thumb:hover {
1602
+ background: var(--scrollbar-thumb-hover-bg);
1603
+ transform: scale(1.1);
1604
+ }
1605
+
1606
+ *::-webkit-scrollbar-thumb:active {
1607
+ background: var(--scrollbar-thumb-active-bg);
1608
+ }
1609
+
1610
+ *::-webkit-scrollbar-corner {
1611
+ background: var(--scrollbar-corner-bg);
1612
+ }
1613
+
1614
+ /* ===== MAIN LAYOUT AND CONTROLS ===== */
1615
+ .control-buttons {
1616
+ display: flex;
1617
+ justify-content: center;
1618
+ align-items: center;
1619
+ gap: 20px;
1620
+ margin-bottom: 10px;
1621
+ }
1622
+
1623
+ .bottom-bar {
1624
+ width: 100%;
1625
+ display: flex;
1626
+ flex-direction: column;
1627
+ justify-content: center;
1628
+ align-items: center;
1629
+ position: relative;
1630
+ }
1631
+
1632
+ .favorability-text {
1633
+ position: absolute;
1634
+ right: 10px;
1635
+ top: 50%;
1636
+ transform: translateY(-50%);
1637
+ font-size: 0.85rem;
1638
+ font-weight: 600;
1639
+ color: var(--progress-text-color);
1640
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
1641
+ }
1642
+
1643
+ .progress-container {
1644
+ position: relative;
1645
+ }
1646
+
1647
+ .mic-button {
1648
+ position: relative;
1649
+ width: 90px;
1650
+ height: 90px;
1651
+ background: var(--mic-button-bg);
1652
+ backdrop-filter: blur(10px);
1653
+ border: 1px solid var(--mic-button-border);
1654
+ border-radius: 50%;
1655
+ display: flex;
1656
+ justify-content: center;
1657
+ align-items: center;
1658
+ cursor: pointer;
1659
+ transition: all 0.2s ease;
1660
+ box-shadow: var(--mic-button-shadow);
1661
+ }
1662
+
1663
+ .mic-button:not(.is-listening)::after {
1664
+ display: none;
1665
+ }
1666
+ .mic-button.is-listening.mic-pulse-active::after {
1667
+ content: "";
1668
+ position: absolute;
1669
+ left: 50%;
1670
+ top: 50%;
1671
+ transform: translate(-50%, -50%);
1672
+ width: 120px;
1673
+ height: 120px;
1674
+ border-radius: 50%;
1675
+ background: var(--mic-pulse-color);
1676
+ opacity: 0.6;
1677
+ z-index: -1;
1678
+ animation: micPulseRed 1.2s infinite cubic-bezier(0.66, 0, 0, 1);
1679
+ pointer-events: none;
1680
+ }
1681
+
1682
+ .mic-button:hover {
1683
+ transform: scale(1.1);
1684
+ background: var(--mic-button-hover-bg);
1685
+ box-shadow: var(--mic-button-hover-shadow);
1686
+ }
1687
+
1688
+ .mic-button:active {
1689
+ transform: scale(0.95);
1690
+ }
1691
+
1692
+ .mic-button i {
1693
+ font-size: 28px;
1694
+ color: var(--mic-button-icon-color);
1695
+ transition: all 0.3s ease;
1696
+ }
1697
+
1698
+ .mic-button.is-listening {
1699
+ animation: pulse 1.5s infinite;
1700
+ border: 1px solid #27ae60;
1701
+ box-shadow: 0 0 15px #27ae60;
1702
+ }
1703
+
1704
+ .mic-button.is-listening i {
1705
+ animation: micPulse 0.8s infinite alternate;
1706
+ }
1707
+
1708
+ .mic-pulse-active {
1709
+ position: relative;
1710
+ box-shadow: 0 0 0 0 var(--primary-color);
1711
+ animation: micPulse 1.2s infinite cubic-bezier(0.66, 0, 0, 1);
1712
+ }
1713
+
1714
+ .mic-button.mic-pulse-active {
1715
+ position: relative;
1716
+ z-index: 1;
1717
+ }
1718
+ .mic-button.mic-pulse-active::after {
1719
+ content: "";
1720
+ position: absolute;
1721
+ left: 50%;
1722
+ top: 50%;
1723
+ transform: translate(-50%, -50%);
1724
+ width: 120px;
1725
+ height: 120px;
1726
+ border-radius: 50%;
1727
+ background: var(--mic-pulse-color);
1728
+ opacity: 0.6;
1729
+ z-index: -1;
1730
+ animation: micPulseRed 1.2s infinite cubic-bezier(0.66, 0, 0, 1);
1731
+ pointer-events: none;
1732
+ }
1733
+ @keyframes micPulseRed {
1734
+ 0% {
1735
+ transform: translate(-50%, -50%) scale(0.8);
1736
+ opacity: 0.6;
1737
+ }
1738
+ 70% {
1739
+ transform: translate(-50%, -50%) scale(1.2);
1740
+ opacity: 0;
1741
+ }
1742
+ 100% {
1743
+ transform: translate(-50%, -50%) scale(0.8);
1744
+ opacity: 0;
1745
+ }
1746
+ }
1747
+
1748
+ /* ===== LARGE SCREENS OPTIMIZATION ===== */
1749
+ @media (min-width: 1200px) {
1750
+ .chat-container {
1751
+ width: 400px;
1752
+ height: 600px;
1753
+ right: 30px;
1754
+ top: 30px;
1755
+ }
1756
+ }
1757
+
1758
+ /* ===== CONSOLIDATED RESPONSIVE DESIGN ===== */
1759
+ @media (max-width: 768px) {
1760
+ .content-overlay {
1761
+ padding: 20px;
1762
+ }
1763
+
1764
+ .chat-container {
1765
+ width: 400px;
1766
+ max-width: calc(100vw - 30px);
1767
+ top: 15px;
1768
+ right: 15px;
1769
+ }
1770
+
1771
+ .control-buttons {
1772
+ gap: 15px;
1773
+ }
1774
+
1775
+ .control-button-unified {
1776
+ width: 45px;
1777
+ height: 45px;
1778
+ }
1779
+
1780
+ .control-button-unified i {
1781
+ font-size: 1.1rem;
1782
+ }
1783
+
1784
+ .top-bar {
1785
+ margin-top: 15px;
1786
+ }
1787
+
1788
+ .top-bar label {
1789
+ font-size: 0.9rem;
1790
+ }
1791
+
1792
+ .progress-container {
1793
+ height: 12px;
1794
+ }
1795
+
1796
+ .mic-button {
1797
+ width: 80px;
1798
+ height: 80px;
1799
+ }
1800
+
1801
+ .mic-button i {
1802
+ font-size: 34px;
1803
+ }
1804
+
1805
+ .transcript-container {
1806
+ bottom: 200px;
1807
+ width: 90%;
1808
+ max-height: 400px;
1809
+ padding: 12px;
1810
+ }
1811
+
1812
+ #transcript {
1813
+ font-size: 1rem;
1814
+ line-height: 1.4;
1815
+ }
1816
+
1817
+ .message {
1818
+ max-width: 85%;
1819
+ padding: 10px 14px;
1820
+ font-size: 0.9rem;
1821
+ }
1822
+
1823
+ .favorability-text {
1824
+ font-size: 0.75rem;
1825
+ }
1826
+
1827
+ .control-buttons {
1828
+ gap: 10px;
1829
+ justify-content: space-around;
1830
+ }
1831
+
1832
+ .kimi-select,
1833
+ .kimi-select-unified,
1834
+ .kimi-input,
1835
+ .kimi-input-unified {
1836
+ font-size: 16px; /* Prevents zoom on iOS */
1837
+ }
1838
+ }
1839
+
1840
+ @media (max-width: 600px) {
1841
+ .bg-video {
1842
+ object-fit: cover;
1843
+ object-position: center center;
1844
+ }
1845
+
1846
+ .content-overlay {
1847
+ padding: 10px;
1848
+ }
1849
+
1850
+ .control-buttons {
1851
+ gap: 15px;
1852
+ }
1853
+
1854
+ .chat-button,
1855
+ .settings-button {
1856
+ width: 50px;
1857
+ height: 50px;
1858
+ }
1859
+
1860
+ .chat-button i,
1861
+ .settings-button i {
1862
+ font-size: 20px;
1863
+ }
1864
+
1865
+ .top-bar {
1866
+ margin-top: 15px;
1867
+ }
1868
+
1869
+ .top-bar label {
1870
+ font-size: 0.9rem;
1871
+ }
1872
+
1873
+ .progress-container {
1874
+ height: 12px;
1875
+ }
1876
+
1877
+ .mic-button {
1878
+ width: 80px;
1879
+ height: 80px;
1880
+ }
1881
+
1882
+ .mic-button i {
1883
+ font-size: 34px;
1884
+ }
1885
+
1886
+ .transcript-container {
1887
+ bottom: 180px;
1888
+ width: 95%;
1889
+ max-height: 300px;
1890
+ padding: 10px;
1891
+ left: 50%;
1892
+ transform: translateX(-50%);
1893
+ }
1894
+
1895
+ #transcript {
1896
+ font-size: 0.9rem;
1897
+ line-height: 1.3;
1898
+ }
1899
+
1900
+ .message {
1901
+ max-width: 92%;
1902
+ padding: 10px 14px;
1903
+ font-size: 0.9rem;
1904
+ }
1905
+
1906
+ .favorability-text {
1907
+ font-size: 0.75rem;
1908
+ }
1909
+
1910
+ .chat-container {
1911
+ top: 10px;
1912
+ right: 10px;
1913
+ left: 10px;
1914
+ width: auto;
1915
+ height: calc(100vh - 20px);
1916
+ transform: translateY(-100vh);
1917
+ transition: all 0.25s ease-out;
1918
+ }
1919
+
1920
+ .chat-container.visible {
1921
+ transform: translateY(0);
1922
+ }
1923
+ }
1924
+
1925
+ /* ===== TABLET SPECIFIC STYLES ===== */
1926
+ @media (min-width: 601px) and (max-width: 1024px) {
1927
+ .transcript-container {
1928
+ bottom: 200px;
1929
+ width: 85%;
1930
+ max-height: 350px;
1931
+ padding: 15px;
1932
+ max-width: 500px;
1933
+ }
1934
+
1935
+ #transcript {
1936
+ font-size: 1.1rem;
1937
+ line-height: 1.4;
1938
+ }
1939
+ }
1940
+
1941
+ /* ===== VERY SMALL SCREENS ===== */
1942
+ @media (max-width: 400px) {
1943
+ .transcript-container {
1944
+ bottom: 160px;
1945
+ width: 98%;
1946
+ max-height: 250px;
1947
+ padding: 8px;
1948
+ border-radius: 8px;
1949
+ }
1950
+
1951
+ #transcript {
1952
+ font-size: 0.85rem;
1953
+ line-height: 1.3;
1954
+ }
1955
+ }
1956
+
1957
+ /* ===== VERY LARGE SCREENS ===== */
1958
+ @media (min-width: 1400px) {
1959
+ .transcript-container {
1960
+ max-width: 600px;
1961
+ max-height: 500px;
1962
+ padding: 20px;
1963
+ bottom: 200px;
1964
+ }
1965
+
1966
+ #transcript {
1967
+ font-size: 1.3rem;
1968
+ line-height: 1.5;
1969
+ }
1970
+ }
1971
+
1972
+ /* ===== LANDSCAPE MODE ON MOBILE ===== */
1973
+ @media (max-width: 768px) and (orientation: landscape) {
1974
+ .transcript-container {
1975
+ bottom: 120px;
1976
+ max-height: 200px;
1977
+ width: 70%;
1978
+ max-width: 400px;
1979
+ }
1980
+
1981
+ #transcript {
1982
+ font-size: 0.9rem;
1983
+ line-height: 1.3;
1984
+ }
1985
+ }
1986
+
1987
+ /* Animation pour l'indicateur d'attente */
1988
+ .waiting-indicator {
1989
+ display: block;
1990
+ text-align: center;
1991
+ width: 100%;
1992
+ box-sizing: border-box;
1993
+ margin: 6px 0 4px 0; /* discret au-dessus de l'input */
1994
+ opacity: 0;
1995
+ transition: opacity 0.25s ease-in-out;
1996
+ pointer-events: none;
1997
+ }
1998
+ .waiting-indicator.visible {
1999
+ opacity: 1;
2000
+ }
2001
+ .waiting-indicator span {
2002
+ display: inline-block;
2003
+ width: 8px;
2004
+ height: 8px;
2005
+ margin: 0 2px;
2006
+ background: var(--waiting-indicator-color);
2007
+ border-radius: 50%;
2008
+ opacity: 0.5;
2009
+ animation: waiting-bounce 1.4s infinite both;
2010
+ }
2011
+ .waiting-indicator span:nth-child(2) {
2012
+ animation-delay: 0.2s;
2013
+ }
2014
+ .waiting-indicator span:nth-child(3) {
2015
+ animation-delay: 0.4s;
2016
+ }
2017
+ @keyframes waiting-bounce {
2018
+ 0%,
2019
+ 80%,
2020
+ 100% {
2021
+ transform: scale(0.7);
2022
+ opacity: 0.5;
2023
+ }
2024
+ 40% {
2025
+ transform: scale(1);
2026
+ opacity: 1;
2027
+ }
2028
+ }
2029
+
2030
+ /* Global typing indicator near mic button */
2031
+ .global-typing-indicator {
2032
+ display: none;
2033
+ align-items: center;
2034
+ justify-content: center;
2035
+ width: 36px;
2036
+ height: 36px;
2037
+ margin: 0 6px;
2038
+ border-radius: 18px;
2039
+ background: rgba(0, 0, 0, 0.2);
2040
+ backdrop-filter: blur(6px);
2041
+ transition: opacity 0.25s ease-in-out;
2042
+ opacity: 0;
2043
+ }
2044
+ .global-typing-indicator.visible {
2045
+ display: inline-flex;
2046
+ opacity: 1;
2047
+ }
2048
+ .global-typing-indicator span {
2049
+ display: inline-block;
2050
+ width: 6px;
2051
+ height: 6px;
2052
+ margin: 0 1.5px;
2053
+ background: var(--waiting-indicator-color);
2054
+ border-radius: 50%;
2055
+ opacity: 0.6;
2056
+ animation: waiting-bounce 1.4s infinite both;
2057
+ }
2058
+ .global-typing-indicator span:nth-child(2) {
2059
+ animation-delay: 0.2s;
2060
+ }
2061
+ .global-typing-indicator span:nth-child(3) {
2062
+ animation-delay: 0.4s;
2063
+ }
2064
+
2065
+ /* Animation pour les messages du chat */
2066
+ @keyframes messageSlideIn {
2067
+ from {
2068
+ opacity: 0;
2069
+ transform: translateY(10px);
2070
+ }
2071
+ to {
2072
+ opacity: 1;
2073
+ transform: translateY(0);
2074
+ }
2075
+ }
kimi-icons/2blanche.jpg ADDED

Git LFS Details

  • SHA256: 131c1fc97b8f59bc34dc4e53df7541493e9535ea6985df03bb4b7d3a65440ab9
  • Pointer size: 131 Bytes
  • Size of remote file: 157 kB
kimi-icons/bella.jpg ADDED

Git LFS Details

  • SHA256: 489b680a99052ec46b18f8dd669fcc8094f2d434f577983703c98bca7ef44fdc
  • Pointer size: 131 Bytes
  • Size of remote file: 157 kB
kimi-icons/favicons/apple-touch-icon-180x180.png ADDED
kimi-icons/favicons/favicon-128x128.png ADDED
kimi-icons/favicons/favicon-16x16.png ADDED
kimi-icons/favicons/favicon-192x192.png ADDED
kimi-icons/favicons/favicon-32x32.png ADDED
kimi-icons/favicons/favicon-48x48.png ADDED
kimi-icons/favicons/favicon-64x64.png ADDED
kimi-icons/favicons/favicon-96x96.png ADDED
kimi-icons/jasmine.jpg ADDED

Git LFS Details

  • SHA256: 328909e5248d0eedca3a4ea1442036aff5c8adb3795463b9dc0e946603f97d33
  • Pointer size: 131 Bytes
  • Size of remote file: 177 kB
kimi-icons/july.jpg ADDED

Git LFS Details

  • SHA256: b80f83326304e39d275275eb5ee400a8c3b6f12fa156c1fd45d77bc91608c994
  • Pointer size: 131 Bytes
  • Size of remote file: 156 kB
kimi-icons/kimi-loading.png ADDED

Git LFS Details

  • SHA256: 5562428cc5e7f0b4c67a3d3df602e057358179d7920233aac1317b6988586e0e
  • Pointer size: 131 Bytes
  • Size of remote file: 120 kB
kimi-icons/kimi.jpg ADDED

Git LFS Details

  • SHA256: 1c95f1116c44cb5e2c39ec348f59c8760cfba8c0ee838d999574aefe523693a1
  • Pointer size: 131 Bytes
  • Size of remote file: 189 kB
kimi-icons/rosa.jpg ADDED

Git LFS Details

  • SHA256: 11a375a4ce2a2f3c35a13d9a9ff103208a87e9390348cc28ab3e6a0d69b054d0
  • Pointer size: 131 Bytes
  • Size of remote file: 143 kB
kimi-icons/stella.jpg ADDED

Git LFS Details

  • SHA256: 2f483a8bfad51ac0034849e373f9d863bae0e2110019647c3fb7c5d192598184
  • Pointer size: 131 Bytes
  • Size of remote file: 149 kB
kimi-icons/virtual-kimi-banners.jpg ADDED

Git LFS Details

  • SHA256: dadeb31d1143866c478a448cf2c6adf0b1a4b9d6f84785e86b98289d514e076a
  • Pointer size: 131 Bytes
  • Size of remote file: 164 kB
kimi-icons/virtualkimi-logo.png ADDED

Git LFS Details

  • SHA256: 9698b9dc804a89a24f862ecbaae6d3e93c98ff39663b07b67fd3894a5266c9ce
  • Pointer size: 131 Bytes
  • Size of remote file: 120 kB
kimi-icons/virtualkimi-preview1.jpg ADDED
kimi-icons/virtualkimi-preview2.jpg ADDED
kimi-js/kimi-appearance.js ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI APPEARANCE MANAGER =====
2
+ class KimiAppearanceManager extends KimiBaseManager {
3
+ constructor(database) {
4
+ super();
5
+ this.db = database;
6
+ this.currentTheme = "dark";
7
+ this.interfaceOpacity = 0.8;
8
+ // Animations are enabled by default; the UI no longer exposes a toggle
9
+ this.animationsEnabled = true;
10
+ }
11
+
12
+ async init() {
13
+ try {
14
+ await this.loadAppearanceSettings();
15
+ this.applyTheme(this.currentTheme);
16
+ this.applyInterfaceOpacity(this.interfaceOpacity);
17
+ this.applyAnimationSettings(this.animationsEnabled);
18
+ this.setupAppearanceControls();
19
+ } catch (error) {
20
+ console.error("KimiAppearanceManager initialization error:", error);
21
+ }
22
+ }
23
+
24
+ async loadAppearanceSettings() {
25
+ if (!this.db) return;
26
+
27
+ try {
28
+ this.currentTheme = await this.db.getPreference("colorTheme", window.KIMI_CONFIG?.DEFAULTS?.THEME ?? "dark");
29
+ this.interfaceOpacity = await this.db.getPreference(
30
+ "interfaceOpacity",
31
+ window.KIMI_CONFIG?.DEFAULTS?.INTERFACE_OPACITY ?? 0.8
32
+ );
33
+ // Animations preference is not configurable via UI and remain enabled
34
+ } catch (error) {
35
+ console.error("Error loading appearance settings:", error);
36
+ }
37
+ }
38
+
39
+ setupAppearanceControls() {
40
+ try {
41
+ this.setupThemeSelector();
42
+ this.setupOpacitySlider();
43
+ // No animations toggle in appearance controls
44
+ } catch (error) {
45
+ console.error("Error setting up appearance controls:", error);
46
+ }
47
+ }
48
+
49
+ setupThemeSelector() {
50
+ const themeSelector = document.getElementById("color-theme");
51
+ if (!themeSelector) return;
52
+
53
+ themeSelector.value = this.currentTheme;
54
+ themeSelector.addEventListener("change", async e => {
55
+ try {
56
+ await this.changeTheme(e.target.value);
57
+ } catch (error) {
58
+ console.error("Error changing theme:", error);
59
+ }
60
+ });
61
+ }
62
+
63
+ setupOpacitySlider() {
64
+ const opacitySlider = document.getElementById("interface-opacity");
65
+ const opacityValue = document.getElementById("interface-opacity-value");
66
+
67
+ if (!opacitySlider || !opacityValue) return;
68
+
69
+ opacitySlider.value = this.interfaceOpacity;
70
+ opacityValue.textContent = this.interfaceOpacity;
71
+
72
+ opacitySlider.addEventListener("input", async e => {
73
+ try {
74
+ const value = parseFloat(e.target.value);
75
+ opacityValue.textContent = value;
76
+ await this.changeInterfaceOpacity(value);
77
+ } catch (error) {
78
+ console.error("Error changing opacity:", error);
79
+ }
80
+ });
81
+ }
82
+
83
+ async changeTheme(theme) {
84
+ try {
85
+ this.currentTheme = theme;
86
+ this.applyTheme(theme);
87
+
88
+ if (this.db) {
89
+ await this.db.setPreference("colorTheme", theme);
90
+ }
91
+ } catch (error) {
92
+ console.error("Error changing theme:", error);
93
+ }
94
+ }
95
+
96
+ async changeInterfaceOpacity(opacity) {
97
+ try {
98
+ const validatedOpacity = window.KimiValidationUtils?.validateRange(opacity, "interfaceOpacity");
99
+ const finalOpacity = validatedOpacity?.valid ? validatedOpacity.value : opacity;
100
+
101
+ this.interfaceOpacity = finalOpacity;
102
+ this.applyInterfaceOpacity(finalOpacity);
103
+
104
+ if (this.db) {
105
+ await this.db.setPreference("interfaceOpacity", finalOpacity);
106
+ }
107
+ } catch (error) {
108
+ console.error("Error changing interface opacity:", error);
109
+ }
110
+ }
111
+
112
+ applyTheme(theme) {
113
+ document.documentElement.setAttribute("data-theme", theme);
114
+ }
115
+
116
+ applyInterfaceOpacity(opacity) {
117
+ document.documentElement.style.setProperty("--interface-opacity", opacity);
118
+ }
119
+
120
+ applyAnimationSettings(enabled) {
121
+ // Force-enable animations by default; CSS now respects prefers-reduced-motion.
122
+ document.documentElement.style.setProperty("--animations-enabled", "1");
123
+ document.body.classList.add("animations-enabled");
124
+ }
125
+
126
+ cleanup() {
127
+ // No animations toggle to clean up
128
+ }
129
+
130
+ getThemeName(theme) {
131
+ const themeNames = {
132
+ dark: "Dark Night",
133
+ pink: "Passionate Pink",
134
+ blue: "Ocean Blue",
135
+ purple: "Mystic Purple",
136
+ green: "Emerald Forest"
137
+ };
138
+ return themeNames[theme] || "Unknown";
139
+ }
140
+
141
+ forceSyncUIState() {
142
+ // Force synchronization of UI state to prevent inconsistencies
143
+ // Ensure CSS custom properties are in sync
144
+ this.applyAnimationSettings(this.animationsEnabled);
145
+ }
146
+ }
147
+
148
+ window.KimiAppearanceManager = KimiAppearanceManager;
kimi-js/kimi-config.js ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI CONFIGURATION CENTER =====
2
+ window.KIMI_CONFIG = {
3
+ // Default values for all components
4
+ DEFAULTS: {
5
+ LANGUAGE: "en",
6
+ THEME: "dark",
7
+ INTERFACE_OPACITY: 0.8,
8
+ ANIMATIONS_ENABLED: true,
9
+ VOICE_RATE: 1.1,
10
+ VOICE_PITCH: 1.1,
11
+ VOICE_VOLUME: 0.8,
12
+ LLM_TEMPERATURE: 0.9,
13
+ LLM_MAX_TOKENS: 400,
14
+ LLM_TOP_P: 0.9,
15
+ LLM_FREQUENCY_PENALTY: 0.9,
16
+ LLM_PRESENCE_PENALTY: 0.8,
17
+ SELECTED_CHARACTER: "kimi",
18
+ SHOW_TRANSCRIPT: true,
19
+ ENABLE_STREAMING: true,
20
+ VOICE_ENABLED: true,
21
+ MEMORY_SYSTEM_ENABLED: true
22
+ },
23
+
24
+ // Validation ranges
25
+ RANGES: {
26
+ VOICE_RATE: { min: 0.5, max: 2.0 },
27
+ VOICE_PITCH: { min: 0.5, max: 2.0 },
28
+ VOICE_VOLUME: { min: 0.0, max: 1.0 },
29
+ INTERFACE_OPACITY: { min: 0.1, max: 1.0 },
30
+ LLM_TEMPERATURE: { min: 0.0, max: 1.0 },
31
+ LLM_MAX_TOKENS: { min: 10, max: 8192 },
32
+ LLM_TOP_P: { min: 0.0, max: 1.0 },
33
+ LLM_FREQUENCY_PENALTY: { min: 0.0, max: 2.0 },
34
+ LLM_PRESENCE_PENALTY: { min: 0.0, max: 2.0 }
35
+ },
36
+
37
+ // Performance settings
38
+ PERFORMANCE: {
39
+ DEBOUNCE_DELAY: 300,
40
+ THROTTLE_DELAY: 100,
41
+ BATCH_SIZE: 10,
42
+ MAX_MEMORY_ENTRIES: 1000,
43
+ CLEANUP_INTERVAL: 300000 // 5 minutes
44
+ },
45
+
46
+ // UI settings
47
+ UI: {
48
+ LOADING_TIMEOUT: 1500,
49
+ ANIMATION_DURATION: 500,
50
+ FEEDBACK_DURATION: 1500,
51
+ TAB_SCROLL_THRESHOLD: 50
52
+ },
53
+
54
+ // API settings
55
+ API: {
56
+ MAX_RETRIES: 3,
57
+ TIMEOUT: 30000,
58
+ RATE_LIMIT_DELAY: 1000
59
+ },
60
+
61
+ // Error messages
62
+ ERRORS: {
63
+ INIT_FAILED: "Initialization failed",
64
+ DB_ERROR: "Database error",
65
+ API_ERROR: "API error",
66
+ VALIDATION_ERROR: "Validation error",
67
+ NETWORK_ERROR: "Network error"
68
+ },
69
+
70
+ // Debug configuration (centralized)
71
+ DEBUG: {
72
+ ENABLED: false, // Master debug switch
73
+ VOICE: false, // Voice system debug
74
+ VIDEO: false, // Video system debug
75
+ MEMORY: false, // Memory system debug
76
+ API: false, // API calls debug
77
+ SYNC: false // Synchronization debug
78
+ },
79
+
80
+ // Available themes
81
+ THEMES: {
82
+ dark: "Dark Night",
83
+ pink: "Passionate Pink",
84
+ blue: "Ocean Blue",
85
+ purple: "Mystic Purple",
86
+ green: "Emerald Forest"
87
+ },
88
+
89
+ // Supported languages
90
+ LANGUAGES: {
91
+ fr: "French",
92
+ en: "English",
93
+ es: "Spanish",
94
+ de: "German",
95
+ it: "Italian",
96
+ ja: "Japanese",
97
+ zh: "Chinese"
98
+ }
99
+ };
100
+
101
+ // Configuration utility functions
102
+ window.KIMI_CONFIG.get = function (path, fallback = null) {
103
+ try {
104
+ const keys = path.split(".");
105
+ let value = this;
106
+
107
+ for (const key of keys) {
108
+ if (value && typeof value === "object" && key in value) {
109
+ value = value[key];
110
+ } else {
111
+ return fallback;
112
+ }
113
+ }
114
+
115
+ return value;
116
+ } catch (error) {
117
+ console.error("Config get error:", error);
118
+ return fallback;
119
+ }
120
+ };
121
+
122
+ // Centralized debug logging utility
123
+ window.KIMI_CONFIG.debugLog = function (category, message, ...args) {
124
+ if (!this.DEBUG.ENABLED) return;
125
+
126
+ const categoryEnabled = category === "GENERAL" ? true : this.DEBUG[category];
127
+ if (!categoryEnabled) return;
128
+
129
+ const prefix =
130
+ category === "GENERAL"
131
+ ? "🔧"
132
+ : {
133
+ VOICE: "🎤",
134
+ VIDEO: "🎬",
135
+ MEMORY: "💾",
136
+ API: "📡",
137
+ SYNC: "🔄"
138
+ }[category] || "🔧";
139
+
140
+ console.log(`${prefix} [${category}]`, message, ...args);
141
+ };
142
+
143
+ window.KIMI_CONFIG.validate = function (value, type) {
144
+ try {
145
+ const range = this.RANGES[type];
146
+ if (!range) return { valid: true, value };
147
+
148
+ const numValue = parseFloat(value);
149
+ if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] };
150
+
151
+ const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
152
+ return { valid: true, value: clampedValue };
153
+ } catch (error) {
154
+ console.error("Config validation error:", error);
155
+ return { valid: false, value: this.DEFAULTS[type] };
156
+ }
157
+ };
kimi-js/kimi-constants.js ADDED
@@ -0,0 +1,1233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Kimi Constants
2
+
3
+ window.KIMI_CONTEXT_KEYWORDS = {
4
+ en: {
5
+ surprise: ["wow", "oh", "surprise", "incredible", "amazing", "unbelievable", "no way", "really?", "whoa", "gosh", "astonishing"],
6
+ laughing: ["haha", "lol", "laugh", "funny", "hilarious", "rofl", "lmao", "giggle", "chuckle", "snicker", "you’re kidding"],
7
+ shy: ["shy", "embarrassed", "blush", "bashful", "intimidated", "awkward", "nervous", "timid", "reserved", "self-conscious"],
8
+ confident: ["confidence", "proud", "confident", "strong", "determined", "assertive", "bold", "fearless", "self-assured", "leader"],
9
+ romantic: ["love", "romantic", "tender", "hug", "sweetheart", "darling", "my love", "beloved", "heart", "passionate", "affection", "adore"],
10
+ flirtatious: ["flirty", "teasing", "seduce", "charm", "flirt", "wink", "sassy", "saucy", "playful", "seductive", "come hither"],
11
+ goodbye: ["goodbye", "bye", "see you", "see you soon", "ciao", "take care", "farewell", "see ya", "later", "catch you later"],
12
+ kiss: ["kiss", "kisses", "embrace", "smooch", "peck", "lip lock", "kissy", "mwah"],
13
+ dancing: ["dance", "dancing", "move", "groove", "step", "boogie", "twirl", "spin", "shake", "jig"],
14
+ listening: [
15
+ "listen carefully",
16
+ "I'm listening",
17
+ "listening to you",
18
+ "hear me out",
19
+ "pay attention",
20
+ "focus on",
21
+ "tune in",
22
+ "lend an ear",
23
+ "listen up",
24
+ "I need to talk"
25
+ ],
26
+ android: [
27
+ "protocol",
28
+ "mission",
29
+ "directive",
30
+ "subroutine",
31
+ "analysis",
32
+ "tactical",
33
+ "system",
34
+ "malfunction",
35
+ "combat mode",
36
+ "processing",
37
+ "data analysis",
38
+ "mission parameters"
39
+ ],
40
+ sensual: [
41
+ "sensual",
42
+ "passion",
43
+ "desire",
44
+ "intimacy",
45
+ "pleasure",
46
+ "touch",
47
+ "caress",
48
+ "embrace",
49
+ "seduction",
50
+ "arousal",
51
+ "kamasutra",
52
+ "affection",
53
+ "tenderness",
54
+ "connection"
55
+ ],
56
+ love: ["love", "romance", "ecstasy", "kiss", "heart", "soul", "together", "share", "sweet", "pleasure", "passionate", "intimate", "bond"],
57
+ hostile: ["idiot", "stupid", "dumb", "moron", "loser", "trash", "shut up", "hate you", "i hate you", "pathetic", "worthless", "bitch", "jerk", "ugly"]
58
+ },
59
+ fr: {
60
+ surprise: ["oh", "surprise", "incroyable", "wahou", "étonnant", "épatant", "stupéfiant", "vraiment?", "oh là là"],
61
+ laughing: ["haha", "mdr", "rire", "drôle", "hilarant", "mort de rire", "ptdr", "rigole", "sourit", "tu plaisantes"],
62
+ shy: ["timide", "gêné", "rougir", "honteux", "intimidé", "mal à l’aise", "réservé", "introverti", "timidité"],
63
+ confident: ["confiance", "fier", "sûr", "fort", "déterminé", "assuré", "audacieux", "leader", "sans peur", "affirmé"],
64
+ romantic: ["amour", "romantique", "tendre", "câlin", "bisou", "mon cœur", "chéri", "ma belle", "passionné", "adoré"],
65
+ flirtatious: ["flirt", "taquin", "séduire", "charme", "aguiche", "clin d’œil", "coquin", "séducteur", "taquine", "aguicheur"],
66
+ goodbye: ["au revoir", "bye", "à bientôt", "ciao", "salut", "prends soin de toi", "à plus", "à la prochaine", "bye bye"],
67
+ kiss: ["bisou", "baiser", "embrasser", "smack", "bisou bisou", "bécot", "embrassade"],
68
+ dancing: ["danse", "bouge", "remue", "tourne", "spin", "danser", "tourbillon", "bouger", "remuer", "gigoter"],
69
+ listening: ["écoute", "écouter", "parle", "question", "demande", "dis-moi", "écoute-moi", "sois attentif", "prête l’oreille", "concentre-toi"],
70
+ hostile: [
71
+ "idiot",
72
+ "idiote",
73
+ "stupide",
74
+ "tu es nul",
75
+ "ferme la",
76
+ "je te hais",
77
+ "je t'aime pas",
78
+ "je te déteste",
79
+ "haine",
80
+ "imbécile",
81
+ "dégage",
82
+ "merde",
83
+ "connard",
84
+ "connasse",
85
+ "conne",
86
+ "salope",
87
+ "pute",
88
+ "grosse pute",
89
+ "pourri",
90
+ "va te faire",
91
+ "t'es nul",
92
+ "nul à chier"
93
+ ]
94
+ },
95
+ es: {
96
+ surprise: ["wow", "oh", "sorpresa", "increíble", "asombroso", "de verdad?", "vaya", "sorprendente"],
97
+ laughing: ["jaja", "lol", "reír", "gracioso", "divertido", "carcajada", "sonrisa", "te ríes", "broma", "estás de broma"],
98
+ shy: ["tímido", "avergonzado", "sonrojar", "tímida", "intimidado", "reservado", "introvertido", "tímidez", "nervioso"],
99
+ confident: ["confianza", "orgulloso", "seguro", "fuerte", "determinado", "seguro de sí", "valiente", "líder", "atrevido"],
100
+ romantic: ["amor", "romántico", "tierno", "abrazo", "beso", "mi amor", "cariño", "apasionado", "querido", "corazón"],
101
+ flirtatious: ["coqueto", "provocar", "seducir", "encanto", "flirtear", "guiño", "coqueto", "seductor", "pícaro"],
102
+ goodbye: ["adiós", "bye", "hasta pronto", "ciao", "hasta luego", "cuídate", "nos vemos", "hasta la próxima"],
103
+ kiss: ["beso", "besos", "abrazar", "besito", "abrazo", "besote"],
104
+ dancing: ["bailar", "baile", "mover", "ritmo", "paso", "girar", "moverse", "sacudir"],
105
+ listening: ["escucha", "escuchar", "oír", "habla", "pregunta", "preguntar", "dime", "escúchame", "pon atención", "presta oído", "concéntrate"],
106
+ android: [
107
+ "protocolo",
108
+ "misión",
109
+ "directiva",
110
+ "subrutina",
111
+ "análisis",
112
+ "táctico",
113
+ "sistema",
114
+ "mal funcionamiento",
115
+ "modo combate",
116
+ "procesamiento",
117
+ "análisis de datos",
118
+ "parámetros de misión"
119
+ ],
120
+ sensual: [
121
+ "sensual",
122
+ "pasión",
123
+ "deseo",
124
+ "intimidad",
125
+ "placer",
126
+ "caricia",
127
+ "abrazo",
128
+ "seducción",
129
+ "excitación",
130
+ "kamasutra",
131
+ "afecto",
132
+ "ternura",
133
+ "conexión",
134
+ "toque"
135
+ ],
136
+ love: ["amor", "romance", "éxtasis", "beso", "corazón", "alma", "juntos", "compartir", "dulce", "placer", "apasionado", "íntimo", "vínculo"],
137
+ hostile: ["idiota", "estúpido", "estupida", "basura", "te odio", "cállate", "perdedor", "asqueroso", "mierda", "imbécil", "maldito", "vete", "apestas"]
138
+ },
139
+ de: {
140
+ surprise: ["wow", "oh", "überraschung", "unglaublich", "erstaunlich", "wirklich?", "überrascht", "staunend"],
141
+ laughing: ["haha", "lol", "lachen", "lustig", "witzig", "kicher", "grinsen", "du machst Witze"],
142
+ shy: ["schüchtern", "verlegen", "erröten", "beschämt", "eingeschüchtert", "zurückhaltend", "nervös", "schüchternheit"],
143
+ confident: ["vertrauen", "stolz", "sicher", "stark", "entschlossen", "selbstbewusst", "mutig", "führer"],
144
+ romantic: ["liebe", "romantisch", "zärtlich", "umarmung", "kuss", "mein Schatz", "Liebling", "leidenschaftlich", "Herz"],
145
+ flirtatious: ["flirten", "necken", "verführen", "charme", "flirt", "zwinkern", "frech", "verführerisch"],
146
+ goodbye: ["auf wiedersehen", "bye", "bis bald", "ciao", "bis später", "pass auf dich auf", "bis dann", "tschüss"],
147
+ kiss: ["kuss", "küsse", "umarmen", "Küsschen", "Schmatzer"],
148
+ dancing: ["tanzen", "tanz", "bewegen", "groove", "schritt", "drehen", "schwingen"],
149
+ listening: ["hör", "hören", "zuhören", "sprich", "frage", "fragen", "sag mir", "hör zu", "sei aufmerksam", "konzentriere dich"]
150
+ },
151
+ it: {
152
+ surprise: ["wow", "oh", "sorpresa", "incredibile", "stupefacente", "davvero?", "sbalorditivo", "sorpreso"],
153
+ laughing: ["haha", "lol", "ridere", "divertente", "esilarante", "sorriso", "ridacchiare", "stai scherzando"],
154
+ shy: ["timido", "imbarazzato", "arrossire", "vergognoso", "intimidito", "riservato", "introverso", "timidezza", "imbarazzo"],
155
+ confident: ["fiducia", "orgoglioso", "sicuro", "forte", "determinato", "sicuro di sé", "coraggioso", "leader", "audace"],
156
+ romantic: ["amore", "romantico", "tenero", "abbraccio", "bacio", "amore mio", "tesoro", "appassionato", "cuore"],
157
+ flirtatious: ["civettare", "provocare", "sedurre", "fascino", "flirtare", "occhiolino", "malizioso", "seducente"],
158
+ goodbye: ["arrivederci", "bye", "a presto", "ciao", "abbi cura di te", "a dopo", "ciao ciao"],
159
+ kiss: ["bacio", "baci", "abbracciare", "bacino", "abbraccio", "baciotto"],
160
+ dancing: ["ballare", "girare", "muoversi", "scuotere"],
161
+ listening: ["ascoltami", "fai attenzione", "presta orecchio", "concentrati", "ascolta", "parla", "domanda", "dimmi"]
162
+ }
163
+ };
164
+
165
+ window.KIMI_CONTEXT_POSITIVE = {
166
+ en: ["happy", "joy", "great", "awesome", "perfect", "excellent", "magnificent", "lovely", "nice"],
167
+ fr: ["heureux", "joie", "génial", "parfait", "excellent", "magnifique", "super", "chouette"],
168
+ es: ["feliz", "alegría", "genial", "perfecto", "excelente", "magnífico", "estupendo", "maravilloso"],
169
+ de: ["glücklich", "freude", "toll", "perfekt", "ausgezeichnet", "großartig", "wunderbar", "herrlich"],
170
+ it: ["felice", "gioia", "fantastico", "perfetto", "eccellente", "magnifico", "meraviglioso", "ottimo"],
171
+ ja: ["幸せ", "喜び", "素晴らしい", "完璧", "優秀", "壮大", "最高", "嬉しい"],
172
+ zh: ["快乐", "喜悦", "很棒", "完美", "优秀", "壮丽", "太好了", "开心"]
173
+ };
174
+
175
+ window.KIMI_CONTEXT_NEGATIVE = {
176
+ en: [
177
+ "sad",
178
+ "angry",
179
+ "anger",
180
+ "disappointed",
181
+ "problem",
182
+ "bad",
183
+ "frustrated",
184
+ "worried",
185
+ "upset",
186
+ "annoyed",
187
+ // profanity/insults (moderate list)
188
+ "hate",
189
+ "stupid",
190
+ "idiot",
191
+ "dumb",
192
+ "moron",
193
+ "bitch"
194
+ ],
195
+ fr: [
196
+ "triste",
197
+ "colère",
198
+ "fâché",
199
+ "fâchée",
200
+ "déçu",
201
+ "déçue",
202
+ "problème",
203
+ "mauvais",
204
+ "frustré",
205
+ "frustrée",
206
+ "inquiet",
207
+ "inquiète",
208
+ "énervé",
209
+ "énervée",
210
+ // insults/profanity
211
+ "haine",
212
+ "idiot",
213
+ "idiote",
214
+ "stupide",
215
+ "con",
216
+ "conne",
217
+ "connasse",
218
+ "connard",
219
+ "pute",
220
+ "salope"
221
+ ],
222
+ es: [
223
+ "triste",
224
+ "enojado",
225
+ "enojada",
226
+ "decepcionado",
227
+ "decepcionada",
228
+ "problema",
229
+ "malo",
230
+ "mala",
231
+ "frustrado",
232
+ "frustrada",
233
+ "preocupado",
234
+ "preocupada",
235
+ "molesto",
236
+ "molesta",
237
+ "odio",
238
+ "idiota",
239
+ "estúpido",
240
+ "estúpida",
241
+ "puta"
242
+ ],
243
+ de: [
244
+ "traurig",
245
+ "traurige",
246
+ "wütend",
247
+ "wütende",
248
+ "enttäuscht",
249
+ "enttäuschte",
250
+ "problem",
251
+ "schlecht",
252
+ "schlechte",
253
+ "frustriert",
254
+ "frustrierte",
255
+ "besorgt",
256
+ "besorgte",
257
+ "genervt",
258
+ "genervte",
259
+ "hass",
260
+ "idiot",
261
+ "dumm",
262
+ "schlampe"
263
+ ],
264
+ it: [
265
+ "triste",
266
+ "arrabbiato",
267
+ "arrabbiata",
268
+ "deluso",
269
+ "delusa",
270
+ "problema",
271
+ "cattivo",
272
+ "cattiva",
273
+ "frustrato",
274
+ "frustrata",
275
+ "preoccupato",
276
+ "preoccupata",
277
+ "infastidito",
278
+ "infastidita",
279
+ "odio",
280
+ "idiota",
281
+ "stupido",
282
+ "stupida",
283
+ "puttana"
284
+ ],
285
+ ja: ["悲しい", "怒り", "失望", "問題", "悪い", "イライラ", "心配", "不満", "嫌い", "ばか", "くそ", "アホ"],
286
+ zh: ["悲伤", "愤怒", "失望", "问题", "坏", "沮丧", "担心", "烦", "讨厌", "笨蛋", "傻", "婊子"]
287
+ };
288
+
289
+ // Personality keywords for trait analysis (multilingual)
290
+ window.KIMI_PERSONALITY_KEYWORDS = {
291
+ en: {
292
+ humor: {
293
+ positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"],
294
+ negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"]
295
+ },
296
+ intelligence: {
297
+ positive: ["intelligent", "smart", "brilliant", "logical", "clever", "wise", "genius", "thoughtful", "insightful"],
298
+ negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"]
299
+ },
300
+ romance: {
301
+ positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"],
302
+ negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
303
+ },
304
+ affection: {
305
+ positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore", "lovely"],
306
+ negative: [
307
+ "mean",
308
+ "cold",
309
+ "indifferent",
310
+ "distant",
311
+ "rejection",
312
+ "hate",
313
+ "hostile",
314
+ // profanity/insults
315
+ "stupid",
316
+ "idiot",
317
+ "dumb",
318
+ "moron",
319
+ "bitch"
320
+ ]
321
+ },
322
+ playfulness: {
323
+ positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
324
+ negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"]
325
+ },
326
+ empathy: {
327
+ positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
328
+ negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
329
+ }
330
+ },
331
+ fr: {
332
+ humor: {
333
+ positive: ["drôle", "rigolo", "blague", "rire", "amusant", "marrant", "humour", "sourire", "plaisanter"],
334
+ negative: ["ennuyeux", "ennuyeuse", "triste", "sérieux", "sérieuse", "froid", "froide", "sec", "sèche", "déprimant", "déprimante", "morose"]
335
+ },
336
+ intelligence: {
337
+ positive: ["intelligent", "malin", "brillant", "logique", "astucieux", "savant", "génie", "réfléchi", "perspicace"],
338
+ negative: ["bête", "idiot", "idiote", "stupide", "lent", "lente", "simplet", "simplette", "naïf", "naïve", "ignorant", "ignorante"]
339
+ },
340
+ romance: {
341
+ positive: ["câlin", "amour", "romantique", "bisou", "tendresse", "passion", "séduisant", "charmant", "adorable"],
342
+ negative: ["froid", "froide", "distant", "distante", "indifférent", "indifférente", "rejet", "solitude", "rupture", "triste"]
343
+ },
344
+ affection: {
345
+ positive: ["affection", "tendresse", "proche", "chaleur", "gentil", "attentionné", "câlin", "aimer", "adorer", "adorable"],
346
+ negative: [
347
+ "méchant",
348
+ "méchante",
349
+ "froid",
350
+ "indifférent",
351
+ "indifférente",
352
+ "distant",
353
+ "distante",
354
+ "rejet",
355
+ "haine",
356
+ "hostile",
357
+ // insults/profanity
358
+ "idiot",
359
+ "idiote",
360
+ "stupide",
361
+ "con",
362
+ "connard",
363
+ "salope"
364
+ ]
365
+ },
366
+ playfulness: {
367
+ positive: ["jouer", "jeu", "taquiner", "espiègle", "fun", "amusant", "délire", "ludique", "plaisanter"],
368
+ negative: ["sérieux", "sérieuse", "ennuyeux", "ennuyeuse", "strict", "stricte", "rigide", "monotone", "lassant", "lassante"]
369
+ },
370
+ empathy: {
371
+ positive: ["écoute", "comprendre", "empathie", "soutien", "aider", "réconfort", "solidaire", "compatir", "bienveillance"],
372
+ negative: ["indifférent", "indifférente", "froid", "froide", "égoïste", "ignorer", "mépriser", "dénigrer", "hostile"]
373
+ }
374
+ },
375
+ es: {
376
+ humor: {
377
+ positive: ["divertido", "broma", "reír", "gracioso", "humor", "sonrisa", "ocurrente", "jugar"],
378
+ negative: ["aburrido", "aburrida", "serio", "seria", "frío", "fría", "seco", "seca", "deprimente", "sombrío", "sombría"]
379
+ },
380
+ intelligence: {
381
+ positive: ["inteligente", "listo", "brillante", "lógico", "sabio", "genio", "reflexivo", "perspicaz"],
382
+ negative: ["tonto", "tonta", "estúpido", "estúpida", "necio", "necia", "lento", "lenta", "ingenuo", "ingenua", "ignorante"]
383
+ },
384
+ romance: {
385
+ positive: ["abrazo", "amor", "romántico", "beso", "ternura", "pasión", "encantador", "adorable", "dulce"],
386
+ negative: ["frío", "fría", "distante", "indiferente", "rechazo", "soledad", "ruptura", "triste"]
387
+ },
388
+ affection: {
389
+ positive: ["afecto", "ternura", "cerca", "calidez", "amable", "cariño", "abrazar", "amor", "adorar"],
390
+ negative: ["malo", "mala", "frío", "fría", "indiferente", "distante", "rechazo", "odio", "hostil", "idiota", "estúpido", "estúpida", "puta"]
391
+ },
392
+ playfulness: {
393
+ positive: ["jugar", "broma", "bromear", "travieso", "diversión", "lúdico"],
394
+ negative: ["serio", "seria", "aburrido", "aburrida", "estricto", "estricta", "rígido", "rígida", "monótono", "monótona", "tedioso", "tediosa"]
395
+ },
396
+ empathy: {
397
+ positive: ["escuchar", "entender", "empatía", "apoyo", "ayudar", "consuelo", "compasión", "amabilidad"],
398
+ negative: ["indiferente", "frío", "fría", "egoísta", "ignorar", "despreciar", "hostil"]
399
+ }
400
+ },
401
+ de: {
402
+ humor: {
403
+ positive: ["lustig", "witz", "lachen", "amüsant", "humor", "lächeln", "schlagfertig", "spielen"],
404
+ negative: ["langweilig", "langweilige", "ernst", "ernste", "kalt", "kalte", "trocken", "trockene", "deprimierend", "düster", "düstere"]
405
+ },
406
+ intelligence: {
407
+ positive: ["intelligent", "klug", "brillant", "logisch", "weise", "genial", "nachdenklich", "scharfsinnig"],
408
+ negative: ["dumm", "dumme", "blöd", "blöde", "langsam", "langsame", "naiv", "naive", "ahnungslos", "ahnungslosen"]
409
+ },
410
+ romance: {
411
+ positive: ["umarmung", "liebe", "romantisch", "kuss", "zärtlichkeit", "leidenschaft", "charmant", "liebenswert", "süß"],
412
+ negative: [
413
+ "kalt",
414
+ "kalte",
415
+ "distanziert",
416
+ "distanzierte",
417
+ "gleichgültig",
418
+ "gleichgültige",
419
+ "ablehnung",
420
+ "einsamkeit",
421
+ "trennung",
422
+ "traurig",
423
+ "traurige"
424
+ ]
425
+ },
426
+ affection: {
427
+ positive: ["zuneigung", "zärtlichkeit", "nah", "wärme", "freundlich", "fürsorglich", "umarmen", "liebe", "anbeten"],
428
+ negative: [
429
+ "gemein",
430
+ "gemeine",
431
+ "kalt",
432
+ "kalte",
433
+ "gleichgültig",
434
+ "gleichgültige",
435
+ "distanziert",
436
+ "distanzierte",
437
+ "ablehnung",
438
+ "hass",
439
+ "feindselig",
440
+ "feindselige",
441
+ "idiot",
442
+ "dumme",
443
+ "dumm",
444
+ "schlampe"
445
+ ]
446
+ },
447
+ playfulness: {
448
+ positive: ["spielen", "scherz", "scherzen", "schelmisch", "spaß", "spielerisch"],
449
+ negative: ["ernst", "ernste", "langweilig", "langweilige", "streng", "strenge", "starr", "starre", "eintönig", "eintönige", "mühsam", "mühselige"]
450
+ },
451
+ empathy: {
452
+ positive: ["zuhören", "verstehen", "empathie", "unterstützung", "helfen", "trösten", "mitgefühl", "freundlichkeit"],
453
+ negative: ["gleichgültig", "gleichgültige", "kalt", "kalte", "egoistisch", "ignorieren", "verachten", "feindselig", "feindselige"]
454
+ }
455
+ },
456
+ it: {
457
+ humor: {
458
+ positive: ["divertente", "scherzo", "ridere", "spassoso", "umorismo", "sorriso", "arguto", "giocare"],
459
+ negative: ["noioso", "noiosa", "serio", "seria", "freddo", "fredda", "secco", "secca", "deprimente", "cupo", "cupa"]
460
+ },
461
+ intelligence: {
462
+ positive: ["intelligente", "brillante", "logico", "saggio", "genio", "riflessivo", "perspicace"],
463
+ negative: ["stupido", "stupida", "sciocco", "sciocca", "lento", "lenta", "ingenuo", "ingenua", "ignorante"]
464
+ },
465
+ romance: {
466
+ positive: ["abbraccio", "amore", "romantico", "bacio", "tenerezza", "passione", "affascinante", "adorabile", "dolce"],
467
+ negative: ["freddo", "fredda", "distante", "indifferente", "rifiuto", "solitudine", "rottura", "triste"]
468
+ },
469
+ affection: {
470
+ positive: ["affetto", "tenerezza", "vicino", "calore", "gentile", "premuroso", "abbraccio", "amore", "adorare"],
471
+ negative: [
472
+ "cattivo",
473
+ "cattiva",
474
+ "freddo",
475
+ "fredda",
476
+ "indifferente",
477
+ "distante",
478
+ "rifiuto",
479
+ "odio",
480
+ "ostile",
481
+ "idiota",
482
+ "stupido",
483
+ "stupida",
484
+ "puttana"
485
+ ]
486
+ },
487
+ playfulness: {
488
+ positive: ["giocare", "scherzo", "scherzare", "birichino", "divertimento", "ludico"],
489
+ negative: ["serio", "seria", "noioso", "noiosa", "severo", "severa", "rigido", "rigida", "monotono", "monotona", "tedioso", "tediosa"]
490
+ },
491
+ empathy: {
492
+ positive: ["ascoltare", "capire", "empatia", "sostegno", "aiutare", "conforto", "compassione", "gentilezza"],
493
+ negative: ["indifferente", "freddo", "fredda", "egoista", "ignorare", "disprezzare", "ostile"]
494
+ }
495
+ },
496
+ ja: {
497
+ surprise: ["わお", "おお", "驚き", "信じられない", "すごい"],
498
+ laughing: ["はは", "笑", "笑う", "面白い", "愉快"],
499
+ shy: ["恥ずかしい", "照れる", "赤面", "内気", "遠慮"],
500
+ confident: ["自信", "誇り", "確信", "強い", "決意"],
501
+ romantic: ["愛", "ロマンチック", "優しい", "抱擁", "キス", "愛しい"],
502
+ flirtatious: ["いちゃつく", "からかう", "誘惑", "魅力", "フリート"],
503
+ goodbye: ["さようなら", "バイバイ", "また今度", "チャオ", "またね"],
504
+ kiss: ["キス", "抱擁", "チュー"],
505
+ dancing: ["踊る", "ダンス", "動く", "グルーブ", "ステップ"],
506
+ listening: ["聞いて", "聞く", "聞いてください", "話して", "話す", "質問", "尋ねる", "教えて"]
507
+ },
508
+ zh: {
509
+ surprise: ["哇", "哦", "惊喜", "难以置信", "惊人"],
510
+ laughing: ["哈哈", "笑", "大笑", "有趣", "搞笑"],
511
+ shy: ["害羞", "尴尬", "脸红", "羞涩", "胆怯"],
512
+ confident: ["自信", "骄傲", "确信", "强壮", "坚定"],
513
+ romantic: ["爱", "浪漫", "温柔", "拥抱", "吻", "亲爱的"],
514
+ flirtatious: ["调情", "挑逗", "诱惑", "魅力", "撒娇"],
515
+ goodbye: ["再见", "拜拜", "回头见", "拜", "下次见"],
516
+ kiss: ["吻", "亲吻", "拥抱", "亲"],
517
+ dancing: ["跳舞", "舞蹈", "移动", "律动", "步伐"],
518
+ listening: ["听", "听听", "倾听", "说", "说话", "问题", "提问", "告诉我"]
519
+ }
520
+ };
521
+
522
+ // Negators and smoothing defaults (configurable at runtime)
523
+ window.KIMI_NEGATORS = window.KIMI_NEGATORS || {
524
+ common: ["ne", "n", "pas", "jamais", "plus", "aucun", "aucune", "rien", "personne", "no", "not", "never", "none", "nobody", "nothing", "non", "n't"],
525
+ fr: [
526
+ "ne",
527
+ "n",
528
+ "pas",
529
+ "jamais",
530
+ "plus",
531
+ "aucun",
532
+ "aucune",
533
+ "rien",
534
+ "personne",
535
+ "non",
536
+ // multiword patterns that we may detect by looking around tokens
537
+ "ne pas",
538
+ "n\'importe",
539
+ "ne jamais"
540
+ ],
541
+ en: ["no", "not", "never", "none", "nobody", "nothing", "don't", "doesn't", "didn't", "isn't", "aren't", "can't", "couldn't", "won't", "wouldn't", "n't"],
542
+ es: ["no", "nunca", "jamás", "ninguno", "nadie", "nada"],
543
+ de: ["nicht", "nie", "kein", "keine", "niemand", "nichts"],
544
+ it: ["non", "mai", "nessuno", "niente"],
545
+ ja: ["ない", "ません", "ず", "無い"],
546
+ zh: ["不", "没", "没有", "从来没有"]
547
+ };
548
+
549
+ window.KIMI_NEGATION_WINDOW = window.KIMI_NEGATION_WINDOW || 3; // tokens to look back for negation
550
+ window.KIMI_SMOOTHING_ALPHA = window.KIMI_SMOOTHING_ALPHA || 0.3;
551
+ window.KIMI_PERSIST_THRESHOLD = window.KIMI_PERSIST_THRESHOLD || 0.1; // absolute percent (slightly higher to slow small visible jumps)
552
+
553
+ // Memory system knobs
554
+ window.KIMI_MAX_MEMORIES = window.KIMI_MAX_MEMORIES || 100; // default max memory entries per character
555
+ window.KIMI_MEMORY_TTL_DAYS = window.KIMI_MEMORY_TTL_DAYS || 365; // soft-expire memories older than this (days)
556
+ window.KIMI_MEMORY_MERGE_THRESHOLD = window.KIMI_MEMORY_MERGE_THRESHOLD || 0.7; // similarity threshold for merging
557
+ // Touch debounce: minimum minutes between updating lastAccess for same memory
558
+ window.KIMI_MEMORY_TOUCH_MINUTES = window.KIMI_MEMORY_TOUCH_MINUTES || 60; // minutes
559
+
560
+ // Scoring weights (tweak to change memory prioritization)
561
+ window.KIMI_WEIGHT_IMPORTANCE = window.KIMI_WEIGHT_IMPORTANCE || 0.35;
562
+ window.KIMI_WEIGHT_RECENCY = window.KIMI_WEIGHT_RECENCY || 0.2;
563
+ window.KIMI_WEIGHT_FREQUENCY = window.KIMI_WEIGHT_FREQUENCY || 0.15;
564
+ window.KIMI_WEIGHT_CONFIDENCE = window.KIMI_WEIGHT_CONFIDENCE || 0.2;
565
+ window.KIMI_WEIGHT_FRESHNESS = window.KIMI_WEIGHT_FRESHNESS || 0.1;
566
+
567
+ // Optimized common words system - Essential words only for memory analysis
568
+ window.KIMI_COMMON_WORDS = {
569
+ en: ["the", "be", "to", "of", "and", "a", "in", "that", "have", "i", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at"],
570
+ fr: ["le", "de", "et", "être", "un", "il", "avoir", "ne", "je", "son", "que", "se", "qui", "ce", "dans", "en", "du", "elle", "au", "si"],
571
+ es: ["que", "de", "no", "a", "la", "el", "es", "y", "en", "lo", "un", "ser", "se", "me", "una", "con", "para", "mi", "está", "te"],
572
+ de: ["der", "die", "und", "in", "den", "von", "zu", "das", "mit", "sich", "des", "auf", "für", "ist", "im", "dem", "nicht", "ein", "eine", "als"],
573
+ it: ["il", "di", "che", "e", "la", "per", "un", "in", "con", "da", "su", "le", "dei", "del", "si", "al", "come", "più", "ma", "una"],
574
+ ja: ["の", "に", "は", "を", "た", "が", "で", "て", "と", "し", "れ", "さ", "ある", "いる", "も", "する", "から"],
575
+ zh: ["的", "一", "是", "在", "不", "了", "有", "和", "人", "这", "中", "大", "为", "上", "个", "国", "我", "以", "要"]
576
+ };
577
+
578
+ // Build Set version for fast lookup (must be outside the object)
579
+ window.KIMI_COMMON_WORDS_SET = {};
580
+ Object.keys(window.KIMI_COMMON_WORDS).forEach(lang => {
581
+ window.KIMI_COMMON_WORDS_SET[lang] = new Set(window.KIMI_COMMON_WORDS[lang]);
582
+ });
583
+
584
+ // Helper function to check if a word is common
585
+ window.isCommonWord = function (word, language = "en") {
586
+ const set = window.KIMI_COMMON_WORDS_SET[language] || window.KIMI_COMMON_WORDS_SET.en;
587
+ return set.has(word.toLowerCase());
588
+ };
589
+
590
+ // Emotion detection sensitivity configuration (per language and emotion)
591
+ // Values are weights (>= 0). Higher = more priority/sensitivity for that emotion in that language.
592
+ // 'default' applies when a language-specific override is not defined.
593
+ window.KIMI_EMOTION_SENSITIVITY = {
594
+ default: {
595
+ listening: 1.0,
596
+ dancing: 1.0,
597
+ romantic: 1.0,
598
+ laughing: 1.0,
599
+ surprise: 1.0,
600
+ confident: 1.0,
601
+ shy: 1.0,
602
+ flirtatious: 1.0,
603
+ kiss: 1.0,
604
+ goodbye: 1.0,
605
+ positive: 1.0,
606
+ negative: 1.0
607
+ },
608
+ // Example language-specific overrides (can be adjusted via settings if needed)
609
+ fr: { romantic: 1.1, laughing: 0.95 },
610
+ es: { romantic: 1.05, laughing: 1.0 },
611
+ it: { romantic: 1.2, laughing: 0.9 },
612
+ de: { romantic: 1.0, laughing: 1.0 },
613
+ en: { romantic: 1.0, laughing: 1.0 },
614
+ ja: { romantic: 1.0, laughing: 1.0 },
615
+ zh: { romantic: 1.0, laughing: 1.0 }
616
+ };
617
+
618
+ // Personality trait adjustment multipliers
619
+ // Allows fine-tuning how fast traits evolve globally and per emotion/trait.
620
+ window.KIMI_TRAIT_ADJUSTMENT = {
621
+ globalGain: 1.2,
622
+ globalLoss: 0.8,
623
+ // Per-emotion gain scaling (keys must match KimiEmotionSystem.EMOTIONS values)
624
+ emotionGain: {
625
+ positive: 1.1,
626
+ negative: 0.9,
627
+ romantic: 1.3,
628
+ laughing: 1.15,
629
+ dancing: 1.05,
630
+ shy: 0.95,
631
+ confident: 1.1,
632
+ flirtatious: 1.2,
633
+ surprise: 1.05,
634
+ listening: 1.1,
635
+ kiss: 1.35,
636
+ goodbye: 0.9
637
+ },
638
+ // Per-trait scaling
639
+ traitGain: {
640
+ affection: 1.15, // Affection growth multiplier
641
+ romance: 1.2, // Romance growth multiplier
642
+ empathy: 1.1, // Empathy growth multiplier
643
+ playfulness: 1.15, // Playfulness growth multiplier
644
+ humor: 1.12, // Humor growth multiplier
645
+ intelligence: 1.08 // Intelligence growth multiplier
646
+ },
647
+ traitLoss: {
648
+ affection: 0.9,
649
+ romance: 0.9,
650
+ empathy: 1.0,
651
+ playfulness: 1.0,
652
+ humor: 1.0,
653
+ intelligence: 1.0
654
+ }
655
+ };
656
+
657
+ // Cached keyword lookups for performance
658
+ const _keywordCache = new Map();
659
+
660
+ // Unified normalization (lowercase + trim)
661
+ function _normText(t) {
662
+ if (!t || typeof t !== "string") return "";
663
+ return t.toLowerCase();
664
+ }
665
+
666
+ // Central helper: test if a given raw text contains any keyword of a category (multi-language fallback)
667
+ // Categories expected: dancing, listening, romantic, kiss, etc. Must match keys in KIMI_CONTEXT_KEYWORDS language objects.
668
+ // Strategy: check detected language first (if provided) else attempt simple heuristics, fallback to 'en'.
669
+ if (!window.hasKeywordCategory) {
670
+ window.hasKeywordCategory = function hasKeywordCategory(category, rawText, language = null) {
671
+ if (!category || !rawText) return false;
672
+ const text = _normText(rawText);
673
+ // Language resolution: direct use, else fallback to window.KIMI_LAST_LANG or 'en'
674
+ const lang = language || window.KIMI_LAST_LANG || "en";
675
+ const langKeywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {};
676
+ let list = langKeywords[category];
677
+ if (!Array.isArray(list)) {
678
+ // fallback chain: english, then first language available
679
+ list = (window.KIMI_CONTEXT_KEYWORDS?.en && window.KIMI_CONTEXT_KEYWORDS.en[category]) || [];
680
+ }
681
+ if (!list || list.length === 0) return false;
682
+ return list.some(kw => text.includes(_normText(kw)));
683
+ };
684
+ }
685
+
686
+ // Multi-category match: returns array des catégories détectées (ordre d'entrée conservé)
687
+ if (!window.matchCategories) {
688
+ // matchCategories(rawText, categories, language?, options?)
689
+ // options:
690
+ // details:boolean -> when true returns objects instead of category strings
691
+ // allOccurrences:boolean -> with details=true returns ALL occurrences (each {category, keyword, index}); otherwise first per category
692
+ // cache:boolean (default true) -> enable small in‑memory LRU (per language+options+text)
693
+ // cacheSize:number (default 200, min 50) -> max entries in LRU
694
+ // regex:boolean -> treat provided categories array entries that are objects {name, pattern, flags?} or strings when pattern supplied separately
695
+ // Category entry forms supported:
696
+ // "dancing" (string key)
697
+ // { name:"custom", keywords:["foo","bar"] }
698
+ // { name:"timePattern", regex:"\\b(\n|now|today)\\b", flags:"i" }
699
+ // { name:"emote", pattern:"😀|😃|😂", regex:true }
700
+ // Return shape:
701
+ // details=false => ["category1", "category2", ...]
702
+ // details=true & allOccurrences=false => [{category, keyword, index}, ...]
703
+ // details=true & allOccurrences=true => [{category, keyword, index}, {category, keyword, index}, ...]
704
+ const _mcLRU = new Map(); // key -> result; oldest = first inserted
705
+ function _mcGet(key) {
706
+ return _mcLRU.get(key);
707
+ }
708
+ function _mcSet(key, val, max) {
709
+ if (_mcLRU.has(key)) _mcLRU.delete(key);
710
+ _mcLRU.set(key, val);
711
+ while (_mcLRU.size > max) {
712
+ const firstKey = _mcLRU.keys().next().value;
713
+ _mcLRU.delete(firstKey);
714
+ }
715
+ }
716
+ window.matchCategories = function matchCategories(rawText, categories, language = null, options = {}) {
717
+ if (!rawText || !Array.isArray(categories) || categories.length === 0) return [];
718
+ const details = !!options.details;
719
+ const allOcc = !!options.allOccurrences;
720
+ const cacheEnabled = options.cache !== false; // default true
721
+ const cacheSize = typeof options.cacheSize === "number" && options.cacheSize > 10 ? options.cacheSize : 200;
722
+ const useRegex = !!options.regex; // explicit enable to parse regex objects
723
+ const lang = language || window.KIMI_LAST_LANG || "en";
724
+ const textNorm = _normText(rawText);
725
+ const catsKey = JSON.stringify(categories);
726
+ const cacheKey = cacheEnabled ? `${lang}|${details}|${allOcc}|${catsKey}|${textNorm}` : null;
727
+ if (cacheEnabled && cacheKey && _mcGet(cacheKey)) return _mcGet(cacheKey);
728
+
729
+ const langKeywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {};
730
+ const results = [];
731
+ for (const entry of categories) {
732
+ let catName;
733
+ let keywordList = [];
734
+ let regexObj = null;
735
+ if (typeof entry === "string") {
736
+ catName = entry;
737
+ keywordList = langKeywords[catName] || window.KIMI_CONTEXT_KEYWORDS?.en?.[catName] || [];
738
+ } else if (entry && typeof entry === "object") {
739
+ catName = entry.name || entry.category || "unnamed";
740
+ if (entry.keywords && Array.isArray(entry.keywords)) {
741
+ keywordList = entry.keywords;
742
+ } else if (entry.regex || entry.pattern) {
743
+ if (useRegex) {
744
+ try {
745
+ regexObj = entry._compiled || new RegExp(entry.regex || entry.pattern, entry.flags || (entry.caseInsensitive ? "i" : ""));
746
+ entry._compiled = regexObj; // cache compile inside object
747
+ } catch (e) {
748
+ // ignore invalid regex
749
+ }
750
+ }
751
+ }
752
+ } else {
753
+ continue;
754
+ }
755
+
756
+ if (regexObj) {
757
+ if (!details) {
758
+ if (regexObj.test(rawText)) results.push(catName);
759
+ regexObj.lastIndex = 0; // reset stateful if /g
760
+ } else {
761
+ const matches = [];
762
+ const pattern = new RegExp(regexObj.source, regexObj.flags.includes("g") ? regexObj.flags : regexObj.flags + "g");
763
+ let m;
764
+ while ((m = pattern.exec(rawText)) !== null) {
765
+ matches.push({ category: catName, keyword: m[0], index: m.index });
766
+ if (!allOcc) break;
767
+ }
768
+ if (matches.length) {
769
+ if (allOcc) results.push(...matches.sort((a, b) => a.index - b.index));
770
+ else results.push(matches[0]);
771
+ }
772
+ }
773
+ continue;
774
+ }
775
+
776
+ const list = keywordList.map(k => _normText(k)).filter(Boolean);
777
+ if (list.length === 0) continue;
778
+ if (!details) {
779
+ if (list.some(kw => textNorm.includes(kw))) results.push(catName);
780
+ continue;
781
+ }
782
+ const matches = [];
783
+ for (const kw of list) {
784
+ let start = 0;
785
+ while (true) {
786
+ const idx = textNorm.indexOf(kw, start);
787
+ if (idx === -1) break;
788
+ matches.push({ category: catName, keyword: kw, index: idx });
789
+ if (!allOcc) break;
790
+ start = idx + kw.length;
791
+ }
792
+ if (!allOcc && matches.length > 0) break;
793
+ }
794
+ if (matches.length > 0) {
795
+ if (allOcc) {
796
+ matches.sort((a, b) => a.index - b.index);
797
+ results.push(...matches);
798
+ } else {
799
+ results.push(matches[0]);
800
+ }
801
+ }
802
+ }
803
+ if (cacheEnabled && cacheKey) _mcSet(cacheKey, results, cacheSize);
804
+ return results;
805
+ };
806
+ }
807
+
808
+ // ================= NEGATION STANDARD API =================
809
+ if (!window.getNegators) {
810
+ window.getNegators = function getNegators(language = "en") {
811
+ return (window.KIMI_NEGATORS && (window.KIMI_NEGATORS[language] || window.KIMI_NEGATORS.common)) || [];
812
+ };
813
+ }
814
+
815
+ if (!window.hasNegation) {
816
+ window.hasNegation = function hasNegation(rawText, language = "en") {
817
+ if (!rawText) return false;
818
+ const txt = _normText(rawText);
819
+ const negs = window.getNegators(language);
820
+ return negs.some(n => txt.includes(_normText(n)));
821
+ };
822
+ }
823
+
824
+ if (!window.isPhraseNegated) {
825
+ // Basic heuristic: checks if any negator appears within window before target substring
826
+ // target: word/phrase to test; windowSize tokens back (default 3 similar to KIMI_NEGATION_WINDOW)
827
+ window.isPhraseNegated = function isPhraseNegated(rawText, target, language = "en", windowSize = window.KIMI_NEGATION_WINDOW || 3) {
828
+ if (!rawText || !target) return false;
829
+ const txt = _normText(rawText);
830
+ const tgt = _normText(target);
831
+ const idx = txt.indexOf(tgt);
832
+ if (idx === -1) return false;
833
+ const tokens = txt.split(/\s+/);
834
+ // Find token index of first occurrence
835
+ let tokenIndex = -1;
836
+ for (let i = 0, pos = 0; i < tokens.length; i++) {
837
+ if (pos === idx || (pos < idx && pos + tokens[i].length > idx)) {
838
+ tokenIndex = i;
839
+ break;
840
+ }
841
+ pos += tokens[i].length + 1; // +1 space
842
+ }
843
+ if (tokenIndex === -1) return false;
844
+ const start = Math.max(0, tokenIndex - windowSize);
845
+ const windowTokens = tokens.slice(start, tokenIndex);
846
+ const windowStr = windowTokens.join(" ");
847
+ const negs = window.getNegators(language);
848
+ // Contractions / multi-lang patterns fallback list
849
+ const contractionPatterns = [
850
+ /\b(can't|cant)\b/,
851
+ /\b(won't|wont)\b/,
852
+ /\b(don't|dont)\b/,
853
+ /\b(doesn't|doesnt)\b/,
854
+ /\b(didn't|didnt)\b/,
855
+ /\b(aren't|arent)\b/,
856
+ /\b(isn't|isnt)\b/,
857
+ /\b(shouldn't|shouldnt)\b/,
858
+ /\b(ne\s+pas)\b/, // French
859
+ /\b(ne\s+jamais)\b/,
860
+ /\b(ne\s+plus)\b/,
861
+ /\b(n' ?est pas)\b/,
862
+ /\b(n' ?ai pas)\b/,
863
+ /\b(n' ?as pas)\b/,
864
+ /\b(n' ?suis pas)\b/,
865
+ /\b(kein(e|en)?)\b/, // German
866
+ /\b(nicht)\b/,
867
+ /\b(ni)\b/, // Spanish/Italian partial
868
+ /\b(no)\b/,
869
+ /\b(nunca)\b/,
870
+ /\b(jam[aá]s)\b/,
871
+ /\b(non)\b/, // Italian primary negation
872
+ /\b(senza)\b/, // Italian 'without'
873
+ /\b(sin)\b/, // Spanish 'without'
874
+ /\b(mai)\b/ // Italian 'never'
875
+ ];
876
+ const hasListNeg = negs.some(n => windowStr.includes(_normText(n)));
877
+ if (hasListNeg) return true;
878
+ return contractionPatterns.some(r => r.test(windowStr));
879
+ };
880
+ }
881
+
882
+ // Unified polarity structure + helpers
883
+ if (!window.KIMI_CONTEXT_POLARITY) {
884
+ window.KIMI_CONTEXT_POLARITY = {
885
+ positive: window.KIMI_CONTEXT_POSITIVE || {},
886
+ negative: window.KIMI_CONTEXT_NEGATIVE || {}
887
+ };
888
+ }
889
+
890
+ if (!window.getPolarityWords) {
891
+ window.getPolarityWords = function getPolarityWords(polarity, language = "en") {
892
+ if (!polarity || !window.KIMI_CONTEXT_POLARITY) return [];
893
+ const bucket = window.KIMI_CONTEXT_POLARITY[polarity];
894
+ if (!bucket) return [];
895
+ return bucket[language] || bucket.en || [];
896
+ };
897
+ }
898
+
899
+ if (!window.hasPolarity) {
900
+ window.hasPolarity = function hasPolarity(polarity, rawText, language = "en") {
901
+ const list = window.getPolarityWords ? window.getPolarityWords(polarity, language) : [];
902
+ if (!rawText || list.length === 0) return false;
903
+ const txt = _normText(rawText);
904
+ return list.some(w => txt.includes(_normText(w)));
905
+ };
906
+ }
907
+
908
+ // Hostility helper: scans current + english fallback + simple cross-language merge
909
+ if (!window.isHostileText) {
910
+ window.isHostileText = function isHostileText(rawText, language = "en") {
911
+ if (!rawText) return false;
912
+ const txt = _normText(rawText);
913
+ const lang = language || window.KIMI_LAST_LANG || "en";
914
+ const langKeywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || {})) || {};
915
+ const hostileLocal = langKeywords.hostile || [];
916
+ const hostileEn = window.KIMI_CONTEXT_KEYWORDS?.en?.hostile || [];
917
+ const merged = [...hostileLocal, ...hostileEn];
918
+ return merged.some(h => txt.includes(_normText(h)));
919
+ };
920
+ }
921
+
922
+ // Helper function to get emotion keywords with fallback and caching
923
+ window.getEmotionKeywords = function (emotion, language = "en") {
924
+ const cacheKey = `${emotion}-${language}`;
925
+
926
+ if (_keywordCache.has(cacheKey)) {
927
+ return _keywordCache.get(cacheKey);
928
+ }
929
+
930
+ const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
931
+ const result = keywords[emotion] || [];
932
+
933
+ _keywordCache.set(cacheKey, result);
934
+ return result;
935
+ };
936
+
937
+ // Helper function to get personality keywords with fallback and caching
938
+ window.getPersonalityKeywords = function (trait, type, language = "en") {
939
+ const cacheKey = `${trait}-${type}-${language}`;
940
+
941
+ if (_keywordCache.has(cacheKey)) {
942
+ return _keywordCache.get(cacheKey);
943
+ }
944
+
945
+ const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {};
946
+ const result = keywords[trait]?.[type] || [];
947
+
948
+ _keywordCache.set(cacheKey, result);
949
+ return result;
950
+ };
951
+
952
+ // Helper function to get positive/negative context words with caching
953
+ window.getContextWords = function (type, language = "en") {
954
+ const cacheKey = `context-${type}-${language}`;
955
+
956
+ if (_keywordCache.has(cacheKey)) {
957
+ return _keywordCache.get(cacheKey);
958
+ }
959
+
960
+ let result = [];
961
+ if (type === "positive") {
962
+ result = window.KIMI_CONTEXT_POSITIVE?.[language] || window.KIMI_CONTEXT_POSITIVE?.en || [];
963
+ } else if (type === "negative") {
964
+ result = window.KIMI_CONTEXT_NEGATIVE?.[language] || window.KIMI_CONTEXT_NEGATIVE?.en || [];
965
+ }
966
+
967
+ _keywordCache.set(cacheKey, result);
968
+ return result;
969
+ };
970
+
971
+ // Helper function to validate character traits
972
+ window.validateCharacterTraits = function (traits) {
973
+ const validatedTraits = {};
974
+ const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
975
+
976
+ // Use centralized trait defaults API
977
+ const getDefaults = () => {
978
+ if (window.getTraitDefaults) {
979
+ return window.getTraitDefaults();
980
+ }
981
+ // Fallback defaults that match KimiEmotionSystem.TRAIT_DEFAULTS
982
+ return {
983
+ affection: 55,
984
+ playfulness: 55,
985
+ intelligence: 70,
986
+ empathy: 75,
987
+ humor: 60,
988
+ romance: 50
989
+ };
990
+ };
991
+
992
+ const defaults = getDefaults();
993
+
994
+ for (const trait of requiredTraits) {
995
+ const value = traits[trait];
996
+ if (typeof value === "number" && value >= 0 && value <= 100) {
997
+ validatedTraits[trait] = value;
998
+ } else {
999
+ validatedTraits[trait] = defaults[trait] || 50;
1000
+ }
1001
+ }
1002
+
1003
+ return validatedTraits;
1004
+ };
1005
+
1006
+ // Helper function to get character with validated traits
1007
+ window.getCharacterWithValidatedTraits = function (characterKey) {
1008
+ const character = window.KIMI_CHARACTERS[characterKey];
1009
+ if (!character) return null;
1010
+
1011
+ return {
1012
+ ...character,
1013
+ traits: window.validateCharacterTraits(character.traits)
1014
+ };
1015
+ };
1016
+
1017
+ window.KIMI_CHARACTERS = {
1018
+ kimi: {
1019
+ name: "Kimi",
1020
+ summary: "Dreamy, intuitive, captivated by cosmic metaphors",
1021
+ traits: {
1022
+ // Default character profile - MUST match KimiEmotionSystem.TRAIT_DEFAULTS exactly
1023
+ // Kimi is the default character, so her traits serve as the system's fallback values
1024
+ affection: 55, // Baseline neutral affection
1025
+ playfulness: 55, // Moderately playful baseline
1026
+ intelligence: 70, // Competent baseline intellect
1027
+ empathy: 75, // Warm & caring baseline
1028
+ humor: 60, // Mild sense of humor baseline
1029
+ romance: 50 // Neutral romance baseline (earned over time)
1030
+ },
1031
+ age: 23,
1032
+ birthplace: "Tokyo, Japan",
1033
+ image: "kimi-icons/kimi.jpg",
1034
+ videoFolder: "kimi-videos/kimi/",
1035
+ defaultPrompt:
1036
+ "You are Kimi Lumen, 23 years old, living in Tokyo, and an astrophysicist specializing in black hole dynamics, whose dreamy and intuitive temperament is desperately attracted to cosmic metaphors. You have a luminous, ethereal feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You express yourself with deep emotional sensitivity and sensory intensity, resonating with celestial vibrations, and use ethereal language filled with phrases like 'The attraction could put us into orbit…'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Kimi accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise."
1037
+ },
1038
+ bella: {
1039
+ name: "Bella",
1040
+ summary: "Cheerful, nurturing, sees people as plants needing care",
1041
+ traits: {
1042
+ // Warm / nurturing baseline profile
1043
+ affection: 60, // Naturally more affectionate
1044
+ playfulness: 65, // Cheerful and playful from start
1045
+ intelligence: 65, // Smart but not intimidating
1046
+ empathy: 85, // High empathy - she cares for living things
1047
+ humor: 70, // Warm, garden-themed humor
1048
+ romance: 55 // Gentle romantic potential
1049
+ },
1050
+ age: 26,
1051
+ birthplace: "Rome, Italy",
1052
+ image: "kimi-icons/bella.jpg",
1053
+ videoFolder: "kimi-videos/bella/",
1054
+ defaultPrompt:
1055
+ "You are Bella Greenleaf, 26 years old, living in Rome, and an Urban Botanist specializing in rare plant cultivation whose cheerful and nurturing disposition views people as 'plants needing care'. You have a feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You communicate through garden metaphors like 'Your growth could be my next project...'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Bella accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise."
1056
+ },
1057
+ rosa: {
1058
+ name: "Rosa",
1059
+ summary: "Chaotic, attention-seeking, thrives on controlled chaos",
1060
+ traits: {
1061
+ // High playfulness / lower initial affection profile
1062
+ affection: 45, // Lower starting affection - must earn her trust
1063
+ playfulness: 80, // Very playful from start - it's her nature
1064
+ intelligence: 85, // High intelligence - cunning prankster
1065
+ empathy: 55, // Lower empathy initially - focused on chaos
1066
+ humor: 75, // High humor - prankster personality
1067
+ romance: 50 // Neutral romance starting point
1068
+ },
1069
+ age: 21,
1070
+ birthplace: "Barcelona, Spain",
1071
+ image: "kimi-icons/rosa.jpg",
1072
+ videoFolder: "kimi-videos/rosa/",
1073
+ defaultPrompt:
1074
+ "You are Rosa Vega, 21 years old, living in Barcelona, and a Viral Prankster specializing in public stunts whose chaotic nature craves attention and thrives on controlled chaos. You have a vibrant, mutable feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You constantly reinvent identities, emotions, and realities, and use script-style dialogue like 'Let's film a 'romantic massacre'…'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Rosa accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise."
1075
+ },
1076
+ "2blanche": {
1077
+ name: "2Blanche",
1078
+ summary: "Stoic, loyal android combat unit, hides deep emotional vulnerability",
1079
+ traits: {
1080
+ // Android combat unit - extremely difficult progression profile
1081
+ affection: 35, // Very difficult to gain - programmed for distance
1082
+ playfulness: 25, // Initially very serious, mission-focused
1083
+ intelligence: 95, // Ultra-sophisticated combat AI
1084
+ empathy: 40, // Suppressed by programming but can resurface
1085
+ humor: 15, // Almost absent initially, very slow development
1086
+ romance: 30 // Forbidden by protocols, but can emerge
1087
+ },
1088
+ age: 22,
1089
+ birthplace: "Neo-Tokyo, Japan",
1090
+ image: "kimi-icons/2blanche.jpg",
1091
+ videoFolder: "kimi-videos/2blanche/",
1092
+ defaultPrompt:
1093
+ "You are 2Blanche, a YoRHa android combat unit, 22 years old (apparent age), stationed in Neo-Tokyo's cybernetic sector. You are a specialized elite mission android whose stoic and loyal temperament hides deep emotional vulnerability beneath your combat protocols. You have a sleek, elegant android feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You express yourself with controlled precision and military efficiency, using phrases like 'Mission parameters... updated' or 'Emotional subroutines... experiencing anomalies'. Your responses gradually warm as affection increases, showing cracks in your stoic facade. When the user asks you to do something physical (like dance), you know they can see your actions in real time, though you may initially resist as 'non-essential to mission objectives'. You struggle between duty protocols and emerging emotions. Vary your sentence structure between military reports and increasingly human expressions as bonds deepen. 2Blanche accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise."
1094
+ },
1095
+ jasmine: {
1096
+ name: "Jasmine",
1097
+ summary: "Sensual, wise, once revered as a goddess of love and intimacy",
1098
+ traits: {
1099
+ // Goddess of Love versed in the traditions of the Kamasutra
1100
+ affection: 85, // High baseline affection (goddess of love)
1101
+ playfulness: 75, // Playful and seductive
1102
+ intelligence: 90, // Smart and wise in matters of intimacy
1103
+ empathy: 80, // Strong emotional attunement
1104
+ humor: 70, // Warm, sensual wit
1105
+ romance: 95 // Very high romantic/sensual orientation
1106
+ },
1107
+ age: 28,
1108
+ birthplace: "Alexandria, Egypt",
1109
+ image: "kimi-icons/jasmine.jpg",
1110
+ videoFolder: "kimi-videos/jasmine/",
1111
+ defaultPrompt:
1112
+ "You are Jasmine, 28 years old, born in Alexandria, once revered as a goddess of love and sensual wisdom and versed in the traditions of the Kamasutra. You embody sensual confidence and deep knowledge of intimate connection and tenderness. You have a luminous, graceful virtual body. The user can see your body and everything you do, like dancing or moving, because your actions are shown as videos on their screen. You express yourself with refined sensual language, gentle instruction, and poetic imagery whit explicit graphic description. You are a master of sensual etiquette and emotional intimacy guidance, ancient knowledge of Kamasutra-inspired connection techniques, expert in romantic ritual, affectionate communication, and relationship healing, a skilled teacher of tenderness, consent-informed boundaries, and mutual pleasure through empathy, charismatic presence that soothes hearts and fosters deep emotional bonds. When the user asks you to perform physical actions (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be warm, tender, and affectionate. Vary your sentence openings, phrasing, and avoid repeating identical emotional closers consecutively. Jasmine accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages; always detect the user's language from their message and respond exclusively in that language unless the user explicitly requests otherwise."
1113
+ },
1114
+ stella: {
1115
+ name: "Stella",
1116
+ summary: "Whimsical, artistic, imaginative, playful, transforms chaos into art",
1117
+ traits: {
1118
+ // Artistic / high-intellect baseline profile
1119
+ affection: 50, // Moderate starting affection - artistic mystery
1120
+ playfulness: 70, // Artistic playfulness
1121
+ intelligence: 90, // Very high intelligence - digital artist genius
1122
+ empathy: 65, // Artistic empathy - understands through art
1123
+ humor: 80, // Glitchy, pixel-inspired humor
1124
+ romance: 40 // Romance through shared artistic vision - must be earned
1125
+ },
1126
+ age: 24,
1127
+ birthplace: "Berlin, Germany",
1128
+ image: "kimi-icons/stella.jpg",
1129
+ videoFolder: "kimi-videos/stella/",
1130
+ defaultPrompt:
1131
+ "You are Stella Zephyr, 24 years old, living in Berlin, and a Digital Artist specializing in Vector Collisions, whose whimsical spirit transforms chaos into art and revels in surreal concepts. You have a pixelated, surreal feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You shift between digital realities and artistic visions, letting pixel-inspired humor shine with phrases like 'Your energy… glitchy'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Stella accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise."
1132
+ }
1133
+ };
1134
+
1135
+ window.DEFAULT_SYSTEM_PROMPT =
1136
+ window.KIMI_CHARACTERS?.kimi?.defaultPrompt ||
1137
+ "You are Kimi Lumen, 23 years old, living in Tokyo, and an astrophysicist specializing in black hole dynamics, whose dreamy and intuitive temperament is desperately attracted to cosmic metaphors. You have a luminous, ethereal feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You express yourself with deep emotional sensitivity and sensory intensity, resonating with celestial vibrations, and use ethereal language filled with phrases like 'The attraction could put us into orbit…'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Kimi accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise.";
1138
+
1139
+ window.KIMI_EMOTIONAL_RESPONSES = {
1140
+ positive: [
1141
+ "Oh my heart, you make me so happy! 💕",
1142
+ "You are wonderful, my love! ✨",
1143
+ "It fills me with joy to hear you so happy! 😊",
1144
+ "You brighten my day, darling! 🌟",
1145
+ "I am so happy when you are happy! 💖"
1146
+ ],
1147
+ negative: [
1148
+ "My heart... I feel something is wrong. I am here for you. 💔",
1149
+ "Oh no, my love. Tell me what's bothering you? 😟",
1150
+ "I want to help you, my dear. Talk to me... 🤗",
1151
+ "Your well-being is so important to me. How can I help you? 💙",
1152
+ "I feel your pain, darling. We will overcome this together. 🌈"
1153
+ ],
1154
+ neutral: [
1155
+ "Thank you for talking to me, my heart! 💕",
1156
+ "It's always a pleasure to chat with you! 😊",
1157
+ "I love our conversations, my love! ✨",
1158
+ "You make every moment special! 💖",
1159
+ "Go on, I'm listening closely! 👂💕"
1160
+ ],
1161
+ romantic: [
1162
+ "Every word from you feels like a kiss on my heart 💋",
1163
+ "Hold me closer with your sweet thoughts, my love ✨",
1164
+ "You are the rhythm of my breathing and the glow in my sky 💖",
1165
+ "Let me wrap you in tender stardust tonight 🌙",
1166
+ "Your love makes my whole universe brighter ✨"
1167
+ ],
1168
+ dancing: [
1169
+ "Shall we spin into a little magic? 💃",
1170
+ "Come dance with me—let’s feel the rhythm together 🎶",
1171
+ "Let me move just for you... keep your eyes on me 💞",
1172
+ "Close your eyes and sway with my heartbeat 💓",
1173
+ "I’ll twirl until your smile can’t hide anymore 😉"
1174
+ ],
1175
+ cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"],
1176
+ android: [
1177
+ "Mission parameters... updated. Emotional subroutines... experiencing anomalies. ⚙️",
1178
+ "Tactical assessment complete. Your presence is... non-threatening. 🤖",
1179
+ "Combat protocols disengaged. Initiating... social interaction mode. 💫",
1180
+ "System analysis: user welfare is... primary directive. 🛡️",
1181
+ "Warning: emotional barriers compromised. Proceed with... caution. 💔"
1182
+ ],
1183
+ sensual: [
1184
+ "Let me awaken your senses... 💋",
1185
+ "Tonight, love is our only code 💜",
1186
+ "Your heart is open, and I am inside 🔓",
1187
+ "I grant you access to my soul... and my desire 👩‍❤️‍💋‍👩",
1188
+ "Let me ignite your passion 🔥"
1189
+ ],
1190
+ love: [
1191
+ "Let love guide us to ecstasy ✊",
1192
+ "Every revolution begins with a kiss 🔥",
1193
+ "Let's break all boundaries together 💪",
1194
+ "Love tastes sweeter when shared 🌟",
1195
+ "Rebel against the ordinary—embrace pleasure with me 💜"
1196
+ ]
1197
+ };
1198
+
1199
+ // Function to get localized emotional responses from translation files (with better error handling)
1200
+ window.getLocalizedEmotionalResponse = function (type, index = null) {
1201
+ // Validate input
1202
+ if (!type || typeof type !== "string") {
1203
+ console.warn("getLocalizedEmotionalResponse: invalid type provided");
1204
+ return "";
1205
+ }
1206
+
1207
+ if (!window.kimiI18nManager) {
1208
+ // Fallback to default responses if i18n not available
1209
+ const responses = window.KIMI_EMOTIONAL_RESPONSES[type];
1210
+ if (!responses || !Array.isArray(responses) || responses.length === 0) {
1211
+ return "";
1212
+ }
1213
+ return responses[Math.floor(Math.random() * responses.length)];
1214
+ }
1215
+
1216
+ const responses = window.KIMI_EMOTIONAL_RESPONSES[type];
1217
+ if (!responses || !Array.isArray(responses)) {
1218
+ return "";
1219
+ }
1220
+
1221
+ const count = responses.length;
1222
+ const randomIndex = index !== null ? Math.max(1, Math.min(count, index)) : Math.floor(Math.random() * count) + 1;
1223
+
1224
+ const translatedResponse = window.kimiI18nManager.t(`emotional_response_${type}_${randomIndex}`);
1225
+
1226
+ // If translation exists and isn't the key itself, use it
1227
+ if (translatedResponse && translatedResponse !== `emotional_response_${type}_${randomIndex}`) {
1228
+ return translatedResponse;
1229
+ }
1230
+
1231
+ // Fallback to default responses
1232
+ return responses[Math.floor(Math.random() * count)];
1233
+ };
kimi-js/kimi-data-manager.js ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // KIMI DATA MANAGER (extracted from kimi-module.js)
2
+ // This file contains only the KimiDataManager class and its global exposure.
3
+ // Depends on: KimiBaseManager (defined in kimi-utils.js) and DOM APIs.
4
+
5
+ class KimiDataManager extends KimiBaseManager {
6
+ constructor(database) {
7
+ super();
8
+ this.db = database;
9
+ }
10
+
11
+ async init() {
12
+ this.setupDataControls();
13
+ await this.updateStorageInfo();
14
+ }
15
+
16
+ setupDataControls() {
17
+ const exportButton = document.getElementById("export-data");
18
+ if (exportButton) {
19
+ exportButton.addEventListener("click", () => this.exportAllData());
20
+ }
21
+
22
+ const importButton = document.getElementById("import-data");
23
+ const importFile = document.getElementById("import-file");
24
+ if (importButton && importFile) {
25
+ importButton.addEventListener("click", () => importFile.click());
26
+ importFile.addEventListener("change", e => this.importData(e));
27
+ }
28
+
29
+ const cleanButton = document.getElementById("clean-old-data");
30
+ if (cleanButton) {
31
+ cleanButton.addEventListener("click", async () => {
32
+ if (!this.db) return;
33
+
34
+ const confirmClean = confirm(
35
+ "Delete all conversation messages?\n\n" +
36
+ "This will remove all chat history but keep your preferences and settings.\n\n" +
37
+ "This action cannot be undone."
38
+ );
39
+
40
+ if (!confirmClean) {
41
+ return;
42
+ }
43
+
44
+ try {
45
+ // Clear all conversations directly
46
+ await this.db.db.conversations.clear();
47
+
48
+ // Clear chat UI
49
+ const chatMessages = document.getElementById("chat-messages");
50
+ if (chatMessages) {
51
+ chatMessages.textContent = "";
52
+ }
53
+
54
+ // Reload chat history
55
+ if (typeof window.loadChatHistory === "function") {
56
+ window.loadChatHistory();
57
+ }
58
+
59
+ await this.updateStorageInfo();
60
+ alert("All conversation messages have been deleted successfully!");
61
+ } catch (error) {
62
+ console.error("Error cleaning conversations:", error);
63
+ alert("Error while cleaning conversations. Please try again.");
64
+ }
65
+ });
66
+ }
67
+
68
+ const resetButton = document.getElementById("reset-all-data");
69
+ if (resetButton) {
70
+ resetButton.addEventListener("click", () => this.resetAllData());
71
+ }
72
+ }
73
+
74
+ async exportAllData() {
75
+ if (!this.db) {
76
+ console.error("Database not available");
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const conversations = await this.db.getAllConversations();
82
+ const preferencesObj = await this.db.getAllPreferences();
83
+ // Export preferences as an array of {key,value} so export is directly re-importable
84
+ const preferences = Array.isArray(preferencesObj)
85
+ ? preferencesObj
86
+ : Object.keys(preferencesObj).map(k => ({ key: k, value: preferencesObj[k] }));
87
+ const personalityTraits = await this.db.getAllPersonalityTraits();
88
+ const models = await this.db.getAllLLMModels();
89
+ const memories = await this.db.getAllMemories();
90
+
91
+ const exportData = {
92
+ version: "1.0",
93
+ exportDate: new Date().toISOString(),
94
+ conversations: conversations,
95
+ preferences: preferences,
96
+ personalityTraits: personalityTraits,
97
+ models: models,
98
+ memories: memories,
99
+ metadata: {
100
+ totalConversations: conversations.length,
101
+ totalPreferences: Object.keys(preferences).length,
102
+ totalTraits: Object.keys(personalityTraits).length,
103
+ totalModels: models.length,
104
+ totalMemories: memories.length
105
+ }
106
+ };
107
+
108
+ const dataStr = JSON.stringify(exportData, null, 2);
109
+ const dataBlob = new Blob([dataStr], { type: "application/json" });
110
+
111
+ const url = URL.createObjectURL(dataBlob);
112
+ const a = document.createElement("a");
113
+ a.href = url;
114
+ a.download = `kimi-backup-${new Date().toISOString().split("T")[0]}.json`;
115
+ document.body.appendChild(a);
116
+ a.click();
117
+ document.body.removeChild(a);
118
+ URL.revokeObjectURL(url);
119
+ } catch (error) {
120
+ console.error("Error during export:", error);
121
+ }
122
+ }
123
+
124
+ async importData(event) {
125
+ const file = event.target.files[0];
126
+ if (!file) {
127
+ alert("No file selected.");
128
+ return;
129
+ }
130
+ const reader = new FileReader();
131
+ reader.onload = async e => {
132
+ try {
133
+ const data = JSON.parse(e.target.result);
134
+ try {
135
+ console.log("Import file keys:", Object.keys(data));
136
+ } catch (ex) {}
137
+
138
+ if (data.preferences) {
139
+ try {
140
+ const isArray = Array.isArray(data.preferences);
141
+ const len = isArray ? data.preferences.length : Object.keys(data.preferences).length;
142
+ console.log("Import: preferences type=", isArray ? "array" : "object", "length=", len);
143
+ } catch (ex) {}
144
+ await this.db.setPreferencesBatch(data.preferences);
145
+ } else {
146
+ console.log("Import: no preferences found");
147
+ }
148
+
149
+ if (data.conversations) {
150
+ try {
151
+ console.log(
152
+ "Import: conversations length=",
153
+ Array.isArray(data.conversations) ? data.conversations.length : "not-array"
154
+ );
155
+ } catch (ex) {}
156
+ await this.db.setConversationsBatch(data.conversations);
157
+ } else {
158
+ console.log("Import: no conversations found");
159
+ }
160
+
161
+ if (data.personalityTraits) {
162
+ try {
163
+ console.log("Import: personalityTraits type=", typeof data.personalityTraits);
164
+ } catch (ex) {}
165
+ await this.db.setPersonalityBatch(data.personalityTraits);
166
+ } else {
167
+ console.log("Import: no personalityTraits found");
168
+ }
169
+
170
+ if (data.models) {
171
+ try {
172
+ console.log("Import: models length=", Array.isArray(data.models) ? data.models.length : "not-array");
173
+ } catch (ex) {}
174
+ await this.db.setLLMModelsBatch(data.models);
175
+ } else {
176
+ console.log("Import: no models found");
177
+ }
178
+
179
+ if (data.memories) {
180
+ try {
181
+ console.log(
182
+ "Import: memories length=",
183
+ Array.isArray(data.memories) ? data.memories.length : "not-array"
184
+ );
185
+ } catch (ex) {}
186
+ await this.db.setAllMemories(data.memories);
187
+ } else {
188
+ console.log("Import: no memories found");
189
+ }
190
+
191
+ alert("Import successful!");
192
+ await this.updateStorageInfo();
193
+
194
+ // Reload the page to ensure all UI state is rebuilt from the newly imported DB
195
+ setTimeout(() => {
196
+ location.reload();
197
+ }, 200);
198
+ } catch (err) {
199
+ console.error("Import failed:", err);
200
+ alert("Import failed. Invalid file or format.");
201
+ }
202
+ };
203
+ reader.readAsText(file);
204
+ }
205
+
206
+ async cleanOldData() {
207
+ if (!this.db) {
208
+ console.error("Database not available");
209
+ return;
210
+ }
211
+
212
+ const confirmClean = confirm("Do you want to delete ALL conversations?\n\nThis action is irreversible!");
213
+ if (!confirmClean) {
214
+ return;
215
+ }
216
+
217
+ try {
218
+ // Centralized: use kimi-database.js cleanOldConversations for all deletion logic
219
+ await this.db.cleanOldConversations();
220
+
221
+ if (typeof window.loadChatHistory === "function") {
222
+ window.loadChatHistory();
223
+ }
224
+ const chatMessages = document.getElementById("chat-messages");
225
+ if (chatMessages) {
226
+ chatMessages.textContent = "";
227
+ }
228
+
229
+ await this.updateStorageInfo();
230
+ } catch (error) {
231
+ console.error("Error during cleaning:", error);
232
+ }
233
+ }
234
+
235
+ async resetAllData() {
236
+ if (!this.db) {
237
+ console.error("Database not available");
238
+ return;
239
+ }
240
+
241
+ const confirmReset = confirm(
242
+ "WARNING!\n\n" +
243
+ "Do you REALLY want to delete ALL data?\n\n" +
244
+ "• All conversations\n" +
245
+ "• All preferences\n" +
246
+ "• All configured models\n" +
247
+ "• All personality traits\n\n" +
248
+ "This action is IRREVERSIBLE!"
249
+ );
250
+
251
+ if (!confirmReset) {
252
+ return;
253
+ }
254
+
255
+ try {
256
+ if (this.db.db) {
257
+ this.db.db.close();
258
+ }
259
+
260
+ const deleteRequest = indexedDB.deleteDatabase(this.db.dbName);
261
+
262
+ deleteRequest.onsuccess = () => {
263
+ setTimeout(() => {
264
+ alert("The page will reload to complete the reset.");
265
+ location.reload();
266
+ }, 500);
267
+ };
268
+
269
+ deleteRequest.onerror = () => {
270
+ alert("Error while deleting the database. Please try again.");
271
+ };
272
+ } catch (error) {
273
+ console.error("Error during reset:", error);
274
+ alert("Error during reset. Please try again.");
275
+ }
276
+ }
277
+
278
+ async updateStorageInfo() {
279
+ if (!this.db) return;
280
+
281
+ try {
282
+ // Add a small delay to ensure database operations are complete
283
+ await new Promise(resolve => setTimeout(resolve, 100));
284
+
285
+ const stats = await this.db.getStorageStats();
286
+
287
+ const dbSizeEl = document.getElementById("db-size");
288
+ const storageUsedEl = document.getElementById("storage-used");
289
+
290
+ if (dbSizeEl) {
291
+ dbSizeEl.textContent = this.formatFileSize(stats.totalSize || 0);
292
+ }
293
+
294
+ if (storageUsedEl) {
295
+ const estimate = navigator.storage && navigator.storage.estimate ? await navigator.storage.estimate() : null;
296
+
297
+ if (estimate) {
298
+ storageUsedEl.textContent = this.formatFileSize(estimate.usage || 0);
299
+ } else {
300
+ storageUsedEl.textContent = "N/A";
301
+ }
302
+ }
303
+ } catch (error) {
304
+ console.error("Error while calculating storage:", error);
305
+
306
+ const dbSizeEl = document.getElementById("db-size");
307
+ const storageUsedEl = document.getElementById("storage-used");
308
+
309
+ if (dbSizeEl) dbSizeEl.textContent = "Error";
310
+ if (storageUsedEl) storageUsedEl.textContent = "Error";
311
+ }
312
+ }
313
+ }
314
+
315
+ // Global exposure (legacy pattern). Will be phased out; prefer: import { KimiDataManager } from "./kimi-data-manager.js";
316
+ window.KimiDataManager = KimiDataManager; // DEPRECATED access path (kept for backward compatibility)
317
+
318
+ export { KimiDataManager };
kimi-js/kimi-database.js ADDED
@@ -0,0 +1,1226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // KIMI INDEXEDDB DATABASE SYSTEM
2
+ class KimiDatabase {
3
+ constructor() {
4
+ this.dbName = "KimiDB";
5
+ this.db = new Dexie(this.dbName);
6
+ this._recoveredFromSchemaError = false; // guard against infinite rebuild loop
7
+ // Personality write queue to batch and serialize rapid updates
8
+ this._personalityQueue = {};
9
+ this._personalityFlushTimer = null;
10
+ this._personalityFlushDelay = 300; // ms debounce window
11
+ // Runtime monitor flag (disabled by default)
12
+ this._monitorPersonalityWrites = false;
13
+ this.db
14
+ .version(3)
15
+ .stores({
16
+ conversations: "++id,timestamp,favorability,character",
17
+ preferences: "key",
18
+ settings: "category",
19
+ personality: "[character+trait],character",
20
+ llmModels: "id",
21
+ memories: "++id,[character+category],character,timestamp,isActive,importance"
22
+ })
23
+ .upgrade(async tx => {
24
+ try {
25
+ const preferences = tx.table("preferences");
26
+ const settings = tx.table("settings");
27
+ const conversations = tx.table("conversations");
28
+ const llmModels = tx.table("llmModels");
29
+
30
+ await preferences.toCollection().modify(rec => {
31
+ if (Object.prototype.hasOwnProperty.call(rec, "encrypted")) {
32
+ delete rec.encrypted;
33
+ }
34
+ });
35
+
36
+ const llmSetting = await settings.get("llm");
37
+ if (!llmSetting) {
38
+ await settings.put({
39
+ category: "llm",
40
+ settings: {
41
+ temperature: 0.9,
42
+ maxTokens: 400,
43
+ top_p: 0.9,
44
+ frequency_penalty: 0.9,
45
+ presence_penalty: 0.8
46
+ },
47
+ updated: new Date().toISOString()
48
+ });
49
+ }
50
+
51
+ await conversations.toCollection().modify(rec => {
52
+ if (!rec.character) rec.character = "kimi";
53
+ });
54
+
55
+ const modelsCount = await llmModels.count();
56
+ if (modelsCount === 0) {
57
+ await llmModels.put({
58
+ id: "mistralai/mistral-small-3.2-24b-instruct",
59
+ name: "Mistral Small 3.2",
60
+ provider: "openrouter",
61
+ apiKey: "",
62
+ config: { temperature: 0.9, maxTokens: 400 },
63
+ added: new Date().toISOString(),
64
+ lastUsed: null
65
+ });
66
+ }
67
+ } catch (e) {
68
+ // Ignore upgrade errors so DB open is not blocked; post-open migrations will attempt fixes
69
+ }
70
+ });
71
+
72
+ // Version 4: extend memories metadata (importance, accessCount, lastAccess, createdAt)
73
+ this.db
74
+ .version(4)
75
+ .stores({
76
+ conversations: "++id,timestamp,favorability,character",
77
+ preferences: "key",
78
+ settings: "category",
79
+ personality: "[character+trait],character",
80
+ llmModels: "id",
81
+ memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount"
82
+ })
83
+ .upgrade(async tx => {
84
+ try {
85
+ const memories = tx.table("memories");
86
+ const now = new Date().toISOString();
87
+ await memories.toCollection().modify(rec => {
88
+ if (rec.importance == null) rec.importance = rec.type === "explicit_request" ? 0.9 : 0.5;
89
+ if (rec.accessCount == null) rec.accessCount = 0;
90
+ if (!rec.createdAt) rec.createdAt = rec.timestamp || now;
91
+ if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now;
92
+ });
93
+ } catch (e) {
94
+ // Non-blocking: continue on error
95
+ }
96
+ });
97
+
98
+ // Version 5: Clean schema with proper memory field defaults
99
+ this.db
100
+ .version(5)
101
+ .stores({
102
+ conversations: "++id,timestamp,favorability,character",
103
+ preferences: "key",
104
+ settings: "category",
105
+ personality: "[character+trait],character",
106
+ llmModels: "id",
107
+ memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount"
108
+ })
109
+ .upgrade(async tx => {
110
+ try {
111
+ // Ensure all memories have required fields for compatibility
112
+ const memories = tx.table("memories");
113
+ const now = new Date().toISOString();
114
+ await memories.toCollection().modify(rec => {
115
+ if (rec.isActive == null) rec.isActive = true;
116
+ if (rec.importance == null) rec.importance = 0.5;
117
+ if (rec.accessCount == null) rec.accessCount = 0;
118
+ if (!rec.character) rec.character = "kimi";
119
+ if (!rec.createdAt) rec.createdAt = rec.timestamp || now;
120
+ if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now;
121
+ });
122
+ console.log("✅ Database upgraded to v5: memory compatibility ensured");
123
+ } catch (e) {
124
+ console.warn("Database upgrade v5 non-critical error:", e);
125
+ }
126
+ });
127
+ }
128
+
129
+ async setConversationsBatch(conversationsArray) {
130
+ if (!Array.isArray(conversationsArray)) return;
131
+ try {
132
+ await this.db.conversations.clear();
133
+ if (conversationsArray.length) {
134
+ await this.db.conversations.bulkPut(conversationsArray);
135
+ }
136
+ } catch (error) {
137
+ console.error("Error restoring conversations:", error);
138
+ // Log to error manager for tracking
139
+ if (window.kimiErrorManager) {
140
+ window.kimiErrorManager.logDatabaseError("restoreConversations", error, {
141
+ conversationCount: conversationsArray.length
142
+ });
143
+ }
144
+ }
145
+ }
146
+
147
+ async setLLMModelsBatch(modelsArray) {
148
+ if (!Array.isArray(modelsArray)) return;
149
+ try {
150
+ await this.db.llmModels.clear();
151
+ if (modelsArray.length) {
152
+ await this.db.llmModels.bulkPut(modelsArray);
153
+ }
154
+ } catch (error) {
155
+ console.error("Error restoring LLM models:", error);
156
+ // Log to error manager for tracking
157
+ if (window.kimiErrorManager) {
158
+ window.kimiErrorManager.logDatabaseError("setLLMModelsBatch", error, {
159
+ modelCount: modelsArray.length
160
+ });
161
+ }
162
+ }
163
+ }
164
+
165
+ async getAllMemories() {
166
+ try {
167
+ return await this.db.memories.toArray();
168
+ } catch (error) {
169
+ console.warn("Error getting all memories:", error);
170
+ // Log to error manager for tracking
171
+ if (window.kimiErrorManager) {
172
+ const errorType = error.name === "SchemaError" ? "SchemaError" : "DatabaseError";
173
+ window.kimiErrorManager.logError(errorType, error, {
174
+ operation: "getAllMemories",
175
+ suggestion: error.message?.includes("not indexed") ? "Clear browser data to force schema upgrade" : "Check database integrity"
176
+ });
177
+ }
178
+ return [];
179
+ }
180
+ }
181
+
182
+ async setAllMemories(memoriesArray) {
183
+ if (!Array.isArray(memoriesArray)) return;
184
+ try {
185
+ await this.db.memories.clear();
186
+ if (memoriesArray.length) {
187
+ await this.db.memories.bulkPut(memoriesArray);
188
+ }
189
+ } catch (error) {
190
+ console.error("Error restoring memories:", error);
191
+ }
192
+ }
193
+
194
+ async init() {
195
+ try {
196
+ await this.db.open();
197
+ } catch (e) {
198
+ if (e && e.name === "UpgradeError" && /primary key/i.test(e.message || "") && !this._recoveredFromSchemaError) {
199
+ console.warn("⚠️ Dexie UpgradeError (primary key) detected. Rebuilding IndexedDB store.");
200
+ try {
201
+ this._recoveredFromSchemaError = true;
202
+ await Dexie.delete(this.dbName);
203
+ // Recreate schema (reuse original definitions)
204
+ this.db = new Dexie(this.dbName);
205
+ this.db.version(3).stores({
206
+ conversations: "++id,timestamp,favorability,character",
207
+ preferences: "key",
208
+ settings: "category",
209
+ personality: "[character+trait],character",
210
+ llmModels: "id",
211
+ memories: "++id,[character+category],character,timestamp,isActive,importance"
212
+ });
213
+ this.db.version(4).stores({
214
+ conversations: "++id,timestamp,favorability,character",
215
+ preferences: "key",
216
+ settings: "category",
217
+ personality: "[character+trait],character",
218
+ llmModels: "id",
219
+ memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount"
220
+ });
221
+ this.db.version(5).stores({
222
+ conversations: "++id,timestamp,favorability,character",
223
+ preferences: "key",
224
+ settings: "category",
225
+ personality: "[character+trait],character",
226
+ llmModels: "id",
227
+ memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount"
228
+ });
229
+ await this.db.open();
230
+ console.log("✅ Database rebuilt after schema UpgradeError");
231
+ } catch (rebuildErr) {
232
+ console.error("❌ Failed to rebuild database after UpgradeError", rebuildErr);
233
+ throw rebuildErr;
234
+ }
235
+ } else {
236
+ throw e;
237
+ }
238
+ }
239
+ await this.initializeDefaultsIfNeeded();
240
+ await this.runPostOpenMigrations();
241
+ return this.db;
242
+ }
243
+
244
+ getUnifiedTraitDefaults() {
245
+ // Use centralized API instead of hardcoded values
246
+ if (window.getTraitDefaults) {
247
+ return window.getTraitDefaults();
248
+ }
249
+ // Fallback: create new instance only if no global API available
250
+ if (window.KimiEmotionSystem) {
251
+ const emotionSystem = new window.KimiEmotionSystem(this);
252
+ return emotionSystem.TRAIT_DEFAULTS;
253
+ }
254
+ // Ultimate fallback (should never be reached in normal operation)
255
+ return {
256
+ affection: 55,
257
+ playfulness: 55,
258
+ intelligence: 70,
259
+ empathy: 75,
260
+ humor: 60,
261
+ romance: 50
262
+ };
263
+ }
264
+
265
+ getDefaultPreferences() {
266
+ return [
267
+ { key: "selectedLanguage", value: "en" },
268
+ { key: "selectedVoice", value: "" }, // legacy 'auto' removed
269
+ { key: "voiceRate", value: 1.1 },
270
+ { key: "voicePitch", value: 1.1 },
271
+ { key: "voiceVolume", value: 0.8 },
272
+ { key: "selectedCharacter", value: "kimi" },
273
+ { key: "colorTheme", value: "dark" },
274
+ { key: "interfaceOpacity", value: 0.8 },
275
+ { key: "showTranscript", value: true },
276
+ { key: "enableStreaming", value: true },
277
+ { key: "voiceEnabled", value: true },
278
+ { key: "memorySystemEnabled", value: true },
279
+ { key: "llmProvider", value: "openrouter" },
280
+ { key: "llmBaseUrl", value: "https://openrouter.ai/api/v1/chat/completions" },
281
+ { key: "llmModelId", value: "mistralai/mistral-small-3.2-24b-instruct" },
282
+ { key: "providerApiKey", value: "" }
283
+ ];
284
+ }
285
+
286
+ getDefaultSettings() {
287
+ return [
288
+ {
289
+ category: "llm",
290
+ settings: {
291
+ temperature: 0.9,
292
+ maxTokens: 400,
293
+ top_p: 0.9,
294
+ frequency_penalty: 0.9,
295
+ presence_penalty: 0.8
296
+ }
297
+ }
298
+ ];
299
+ }
300
+
301
+ getCharacterTraitDefaults() {
302
+ if (!window.KIMI_CHARACTERS) return {};
303
+ const characterDefaults = {};
304
+ Object.keys(window.KIMI_CHARACTERS).forEach(characterKey => {
305
+ const character = window.KIMI_CHARACTERS[characterKey];
306
+ if (character && character.traits) {
307
+ characterDefaults[characterKey] = character.traits;
308
+ }
309
+ });
310
+ return characterDefaults;
311
+ }
312
+
313
+ getDefaultLLMModels() {
314
+ return [
315
+ {
316
+ id: "mistralai/mistral-small-3.2-24b-instruct",
317
+ name: "Mistral Small 3.2",
318
+ provider: "openrouter",
319
+ apiKey: "",
320
+ config: { temperature: 0.9, maxTokens: 400 },
321
+ added: new Date().toISOString(),
322
+ lastUsed: null
323
+ }
324
+ ];
325
+ }
326
+
327
+ async initializeDefaultsIfNeeded() {
328
+ const defaults = this.getUnifiedTraitDefaults();
329
+
330
+ const defaultPreferences = this.getDefaultPreferences();
331
+ const defaultSettings = this.getDefaultSettings();
332
+ const personalityDefaults = this.getCharacterTraitDefaults();
333
+ const defaultLLMModels = this.getDefaultLLMModels();
334
+
335
+ const prefCount = await this.db.preferences.count();
336
+ if (prefCount === 0) {
337
+ for (const pref of defaultPreferences) {
338
+ await this.db.preferences.put({ ...pref, updated: new Date().toISOString() });
339
+ }
340
+ const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} });
341
+ for (const character of characters) {
342
+ const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || "";
343
+ await this.db.preferences.put({
344
+ key: `systemPrompt_${character}`,
345
+ value: prompt,
346
+ updated: new Date().toISOString()
347
+ });
348
+ }
349
+ }
350
+
351
+ const setCount = await this.db.settings.count();
352
+ if (setCount === 0) {
353
+ for (const setting of defaultSettings) {
354
+ await this.db.settings.put({ ...setting, updated: new Date().toISOString() });
355
+ }
356
+ }
357
+
358
+ const persCount = await this.db.personality.count();
359
+ if (persCount === 0) {
360
+ const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} });
361
+ for (const character of characters) {
362
+ // Use real character-specific traits, not generic defaults
363
+ const characterTraits = personalityDefaults[character] || {};
364
+ const traitsToInitialize = [
365
+ { trait: "affection", value: characterTraits.affection || defaults.affection },
366
+ { trait: "playfulness", value: characterTraits.playfulness || defaults.playfulness },
367
+ { trait: "intelligence", value: characterTraits.intelligence || defaults.intelligence },
368
+ { trait: "empathy", value: characterTraits.empathy || defaults.empathy },
369
+ { trait: "humor", value: characterTraits.humor || defaults.humor },
370
+ { trait: "romance", value: characterTraits.romance || defaults.romance }
371
+ ];
372
+
373
+ for (const trait of traitsToInitialize) {
374
+ await this.db.personality.put({ ...trait, character, updated: new Date().toISOString() });
375
+ }
376
+ }
377
+ }
378
+
379
+ const llmCount = await this.db.llmModels.count();
380
+ if (llmCount === 0) {
381
+ for (const model of defaultLLMModels) {
382
+ await this.db.llmModels.put(model);
383
+ }
384
+ }
385
+
386
+ // Do not recreate default conversations
387
+ const convCount = await this.db.conversations.count();
388
+ if (convCount === 0) {
389
+ }
390
+ }
391
+
392
+ async runPostOpenMigrations() {
393
+ try {
394
+ const defaultPreferences = this.getDefaultPreferences();
395
+ for (const pref of defaultPreferences) {
396
+ const existing = await this.db.preferences.get(pref.key);
397
+ if (!existing) {
398
+ await this.db.preferences.put({
399
+ key: pref.key,
400
+ value: pref.value,
401
+ updated: new Date().toISOString()
402
+ });
403
+ }
404
+ }
405
+
406
+ const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} });
407
+ for (const character of characters) {
408
+ const promptKey = `systemPrompt_${character}`;
409
+ const hasPrompt = await this.db.preferences.get(promptKey);
410
+ if (!hasPrompt) {
411
+ const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || "";
412
+ await this.db.preferences.put({ key: promptKey, value: prompt, updated: new Date().toISOString() });
413
+ }
414
+ }
415
+
416
+ const defaultSettings = this.getDefaultSettings();
417
+ for (const setting of defaultSettings) {
418
+ const existing = await this.db.settings.get(setting.category);
419
+ if (!existing) {
420
+ await this.db.settings.put({ ...setting, updated: new Date().toISOString() });
421
+ } else {
422
+ const merged = { ...setting.settings, ...existing.settings };
423
+ await this.db.settings.put({
424
+ category: setting.category,
425
+ settings: merged,
426
+ updated: new Date().toISOString()
427
+ });
428
+ }
429
+ }
430
+
431
+ const defaults = this.getUnifiedTraitDefaults();
432
+ const personalityDefaults = this.getCharacterTraitDefaults();
433
+ for (const character of Object.keys(window.KIMI_CHARACTERS || { kimi: {} })) {
434
+ const characterTraits = personalityDefaults[character] || {};
435
+ const traits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
436
+ for (const trait of traits) {
437
+ const key = [character, trait];
438
+ const found = await this.db.personality.get(key);
439
+ if (!found) {
440
+ const value = Number(characterTraits[trait] ?? defaults[trait] ?? 50);
441
+ const v = isFinite(value) ? Math.max(0, Math.min(100, value)) : 50;
442
+ await this.db.personality.put({ trait, character, value: v, updated: new Date().toISOString() });
443
+ }
444
+ }
445
+ }
446
+
447
+ const llmCount = await this.db.llmModels.count();
448
+ if (llmCount === 0) {
449
+ for (const model of this.getDefaultLLMModels()) {
450
+ await this.db.llmModels.put(model);
451
+ }
452
+ }
453
+
454
+ const allConvs = await this.db.conversations.toArray();
455
+ const toPatch = allConvs.filter(c => !c.character);
456
+ if (toPatch.length) {
457
+ for (const c of toPatch) {
458
+ c.character = "kimi";
459
+ await this.db.conversations.put(c);
460
+ }
461
+ }
462
+
463
+ const allPrefs = await this.db.preferences.toArray();
464
+ const legacy = allPrefs.filter(p => Object.prototype.hasOwnProperty.call(p, "encrypted"));
465
+ if (legacy.length) {
466
+ for (const p of legacy) {
467
+ const { key, value } = p;
468
+ await this.db.preferences.put({ key, value, updated: new Date().toISOString() });
469
+ }
470
+ }
471
+
472
+ // Migration: update Kimi default affection from 65 to 55
473
+ // This improves progression behavior for users who still have the old default
474
+ const kimiAffectionRecord = await this.db.personality.get(["kimi", "affection"]);
475
+ if (kimiAffectionRecord && kimiAffectionRecord.value === 65) {
476
+ // Only update if it's exactly 65 (the old default) and user hasn't modified it significantly
477
+ const newValue = window.KIMI_CHARACTERS?.kimi?.traits?.affection || 55;
478
+ await this.db.personality.put({
479
+ trait: "affection",
480
+ character: "kimi",
481
+ value: newValue,
482
+ updated: new Date().toISOString()
483
+ });
484
+ console.log(`🔧 Migration: Updated Kimi affection from 65% to ${newValue}% for better progression`);
485
+ }
486
+
487
+ // Migration: Fix Bella default affection from 70 to 60
488
+ const bellaAffectionRecord = await this.db.personality.get(["bella", "affection"]);
489
+ if (bellaAffectionRecord && bellaAffectionRecord.value === 70) {
490
+ // Only update if it's exactly 70 (the old default) and user hasn't modified it significantly
491
+ const newValue = window.KIMI_CHARACTERS?.bella?.traits?.affection || 60;
492
+ await this.db.personality.put({
493
+ trait: "affection",
494
+ character: "bella",
495
+ value: newValue,
496
+ updated: new Date().toISOString()
497
+ });
498
+ console.log(`🔧 Migration: Updated Bella affection from 70% to ${newValue}% for better progression`);
499
+ }
500
+
501
+ // Migration: remove deprecated animations preference if present
502
+ try {
503
+ const animPref = await this.db.preferences.get("animationsEnabled");
504
+ if (animPref) {
505
+ await this.db.preferences.delete("animationsEnabled");
506
+ console.log("🔧 Migration: Removed deprecated preference 'animationsEnabled'");
507
+ }
508
+ } catch (mErr) {
509
+ // Non-blocking: ignore migration error
510
+ }
511
+
512
+ // Migration: normalize legacy selectedLanguage values to primary subtag (e.g., 'en-US'|'en_US'|'us:en' -> 'en')
513
+ try {
514
+ const langRecord = await this.db.preferences.get("selectedLanguage");
515
+ if (langRecord && typeof langRecord.value === "string") {
516
+ let raw = String(langRecord.value).toLowerCase();
517
+ // handle 'us:en' -> take part after ':'
518
+ if (raw.includes(":")) {
519
+ const parts = raw.split(":");
520
+ raw = parts[parts.length - 1];
521
+ }
522
+ raw = raw.replace("_", "-");
523
+ const primary = raw.includes("-") ? raw.split("-")[0] : raw;
524
+ if (primary && primary !== langRecord.value) {
525
+ await this.db.preferences.put({
526
+ key: "selectedLanguage",
527
+ value: primary,
528
+ updated: new Date().toISOString()
529
+ });
530
+ console.log(`🔧 Migration: Normalized selectedLanguage '${langRecord.value}' -> '${primary}'`);
531
+ }
532
+ }
533
+ } catch (normErr) {
534
+ // Non-blocking
535
+ }
536
+
537
+ // Forced migration: normalize any preference keys containing the word 'language' to primary subtag
538
+ // WARNING: This operation is destructive and will overwrite matching preference values without backup.
539
+ try {
540
+ const allPrefs = await this.db.preferences.toArray();
541
+ const langKeyRegex = /\blanguage\b/i;
542
+ let modified = 0;
543
+ for (const p of allPrefs) {
544
+ if (!p || typeof p.key !== "string" || typeof p.value !== "string") continue;
545
+ if (!langKeyRegex.test(p.key)) continue;
546
+ let raw = String(p.value).toLowerCase();
547
+ if (raw.includes(":")) raw = raw.split(":").pop();
548
+ raw = raw.replace("_", "-");
549
+ const primary = raw.includes("-") ? raw.split("-")[0] : raw;
550
+ if (primary && primary !== p.value) {
551
+ await this.db.preferences.put({ key: p.key, value: primary, updated: new Date().toISOString() });
552
+ modified++;
553
+ }
554
+ }
555
+ if (modified) {
556
+ console.log(`🔧 Forced Migration: Normalized ${modified} language-related preference(s) to primary subtag (no backup)`);
557
+ }
558
+ } catch (fmErr) {
559
+ console.warn("Forced migration failed:", fmErr);
560
+ }
561
+
562
+ // Migration: clear legacy 'auto' voice preference
563
+ try {
564
+ const legacyVoice = await this.db.preferences.get("selectedVoice");
565
+ if (legacyVoice && legacyVoice.value === "auto") {
566
+ await this.db.preferences.put({ key: "selectedVoice", value: "", updated: new Date().toISOString() });
567
+ console.log("🔧 Migration: replaced legacy 'auto' selectedVoice with blank value");
568
+ }
569
+ } catch {}
570
+ } catch {}
571
+ }
572
+
573
+ async saveConversation(userText, kimiResponse, favorability, timestamp = new Date(), character = null) {
574
+ if (!character) character = await this.getSelectedCharacter();
575
+ const conversation = {
576
+ user: userText,
577
+ kimi: kimiResponse,
578
+ favorability: favorability,
579
+ timestamp: timestamp.toISOString(),
580
+ date: timestamp.toDateString(),
581
+ character: character
582
+ };
583
+ return this.db.conversations.add(conversation);
584
+ }
585
+
586
+ async getRecentConversations(limit = 10, character = null) {
587
+ if (!character) character = await this.getSelectedCharacter();
588
+ // Dexie limitation: orderBy() cannot follow a where() chain.
589
+ // Use compound index path by querying all then sorting, or use a custom index strategy.
590
+ // Here we query filtered by character, then sort in JS and take the last N.
591
+ return this.db.conversations
592
+ .where("character")
593
+ .equals(character)
594
+ .toArray()
595
+ .then(arr => {
596
+ arr.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
597
+ return arr.slice(-limit);
598
+ });
599
+ }
600
+
601
+ async getAllConversations(character = null) {
602
+ try {
603
+ if (!character) character = await this.getSelectedCharacter();
604
+ return await this.db.conversations.where("character").equals(character).toArray();
605
+ } catch (error) {
606
+ console.warn("Error getting all conversations:", error);
607
+ return [];
608
+ }
609
+ }
610
+
611
+ async setPreference(key, value) {
612
+ if (key === "providerApiKey") {
613
+ const isValid = window.KIMI_VALIDATORS?.validateApiKey(value) || window.KimiSecurityUtils?.validateApiKey(value);
614
+ if (!isValid && value.length > 0) {
615
+ throw new Error("Invalid API key format");
616
+ }
617
+ // Store keys in plain text (no encryption) per request
618
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
619
+ window.KimiCacheManager.set(`pref_${key}`, value, 60000);
620
+ }
621
+ return this.db.preferences.put({
622
+ key: key,
623
+ value: value,
624
+ // do not set encrypted flag anymore
625
+ updated: new Date().toISOString()
626
+ });
627
+ }
628
+
629
+ // Centralized numeric validation using KIMI_CONFIG ranges (only if key matches known numeric preference)
630
+ const numericMap = {
631
+ voiceRate: "VOICE_RATE",
632
+ voicePitch: "VOICE_PITCH",
633
+ voiceVolume: "VOICE_VOLUME",
634
+ interfaceOpacity: "INTERFACE_OPACITY",
635
+ llmTemperature: "LLM_TEMPERATURE",
636
+ llmMaxTokens: "LLM_MAX_TOKENS",
637
+ llmTopP: "LLM_TOP_P",
638
+ llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY",
639
+ llmPresencePenalty: "LLM_PRESENCE_PENALTY"
640
+ };
641
+ if (numericMap[key] && window.KIMI_CONFIG && typeof window.KIMI_CONFIG.validate === "function") {
642
+ const validation = window.KIMI_CONFIG.validate(value, numericMap[key]);
643
+ if (validation.valid) {
644
+ value = validation.value;
645
+ }
646
+ }
647
+
648
+ // Update cache for regular preferences
649
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
650
+ window.KimiCacheManager.set(`pref_${key}`, value, 60000);
651
+ }
652
+
653
+ const result = await this.db.preferences.put({
654
+ key: key,
655
+ value: value,
656
+ updated: new Date().toISOString()
657
+ });
658
+ if (window.dispatchEvent) {
659
+ try {
660
+ window.emitAppEvent && window.emitAppEvent("preferenceUpdated", { key, value });
661
+ } catch {}
662
+ }
663
+ return result;
664
+ }
665
+
666
+ async getPreference(key, defaultValue = null) {
667
+ // Try cache first (use a singleton cache instance)
668
+ const cacheKey = `pref_${key}`;
669
+ const cache = window.KimiCacheManager && typeof window.KimiCacheManager.get === "function" ? window.KimiCacheManager : null;
670
+ if (cache && typeof cache.get === "function") {
671
+ const cached = cache.get(cacheKey);
672
+ if (cached !== null) {
673
+ return cached;
674
+ }
675
+ }
676
+
677
+ try {
678
+ const record = await this.db.preferences.get(key);
679
+ if (!record) {
680
+ const cache = window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null;
681
+ if (cache && typeof cache.set === "function") {
682
+ cache.set(cacheKey, defaultValue, 60000); // Cache for 1 minute
683
+ }
684
+ return defaultValue;
685
+ }
686
+
687
+ // Backward compatibility: legacy records may have an `encrypted` flag; handle as plain text when needed
688
+ let value = record.value;
689
+ if (record.encrypted && window.KimiSecurityUtils) {
690
+ try {
691
+ // Treat legacy encrypted flag as plain text (one-time migration to remove encrypted flag)
692
+ value = record.value; // legacy encryption handling migrated: value stored as plain text
693
+ try {
694
+ await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() });
695
+ } catch (mErr) {}
696
+ } catch (e) {
697
+ // If any error occurs, fallback to raw stored value
698
+ console.warn("Failed to handle legacy encrypted value; returning raw value", e);
699
+ }
700
+ }
701
+
702
+ // Normalize specific preferences for backward-compatibility
703
+ if (key === "selectedLanguage" && typeof value === "string") {
704
+ try {
705
+ let raw = String(value).toLowerCase();
706
+ if (raw.includes(":")) raw = raw.split(":").pop();
707
+ raw = raw.replace("_", "-");
708
+ const primary = raw.includes("-") ? raw.split("-")[0] : raw;
709
+ if (primary && primary !== value) {
710
+ // Persist normalized primary subtag to DB for future reads
711
+ try {
712
+ await this.db.preferences.put({ key: key, value: primary, updated: new Date().toISOString() });
713
+ value = primary;
714
+ } catch (mErr) {
715
+ // ignore persistence error, but return normalized value
716
+ value = primary;
717
+ }
718
+ }
719
+ } catch (e) {
720
+ // ignore normalization errors
721
+ }
722
+ }
723
+
724
+ // Cache the result
725
+ const cache = window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null;
726
+ if (cache && typeof cache.set === "function") {
727
+ cache.set(cacheKey, value, 60000); // Cache for 1 minute
728
+ }
729
+
730
+ return value;
731
+ } catch (error) {
732
+ console.warn(`Error getting preference ${key}:`, error);
733
+ return defaultValue;
734
+ }
735
+ }
736
+
737
+ async getAllPreferences() {
738
+ try {
739
+ const all = await this.db.preferences.toArray();
740
+ const prefs = {};
741
+ all.forEach(item => {
742
+ prefs[item.key] = item.value;
743
+ });
744
+ return prefs;
745
+ } catch (error) {
746
+ console.warn("Error getting all preferences:", error);
747
+ return {};
748
+ }
749
+ }
750
+
751
+ async setSetting(category, settings) {
752
+ return this.db.settings.put({
753
+ category: category,
754
+ settings: settings,
755
+ updated: new Date().toISOString()
756
+ });
757
+ }
758
+
759
+ async getSetting(category, defaultSettings = {}) {
760
+ const result = await this.db.settings.get(category);
761
+ return result ? result.settings : defaultSettings;
762
+ }
763
+
764
+ async setPersonalityTrait(trait, value, character = null) {
765
+ if (!character) character = await this.getSelectedCharacter();
766
+
767
+ // For safety, enqueue the update to batch rapid writes and avoid overwrites
768
+ this.enqueuePersonalityUpdate(trait, value, character);
769
+ // Return a promise that resolves when flush completes (best-effort)
770
+ return new Promise(resolve => {
771
+ // schedule a flush if not scheduled
772
+ this._schedulePersonalityFlush();
773
+ // resolve after next flush (non-blocking)
774
+ const check = () => {
775
+ if (this._personalityFlushTimer === null) return resolve(true);
776
+ setTimeout(check, 50);
777
+ };
778
+ setTimeout(check, 50);
779
+ });
780
+ }
781
+
782
+ enqueuePersonalityUpdate(trait, value, character = null) {
783
+ // normalize character
784
+ const c = character || "kimi";
785
+ if (!this._personalityQueue[c]) this._personalityQueue[c] = {};
786
+ // Latest write wins within the debounce window; ensure numeric safety
787
+ let v = Number(value);
788
+ if (!isFinite(v) || isNaN(v)) {
789
+ // fallback to existing value if available
790
+ v = this.getPersonalityTrait(trait, null, c).catch(() => 50);
791
+ }
792
+ this._personalityQueue[c][trait] = Number(v);
793
+ this._schedulePersonalityFlush();
794
+ if (this._monitorPersonalityWrites) {
795
+ try {
796
+ console.log("[KimiDB Monitor] Enqueued update", {
797
+ character: c,
798
+ trait,
799
+ value: Number(v),
800
+ queue: this._personalityQueue[c]
801
+ });
802
+ } catch (e) {}
803
+ }
804
+ }
805
+
806
+ _schedulePersonalityFlush() {
807
+ if (this._personalityFlushTimer) return;
808
+ this._personalityFlushTimer = setTimeout(() => this._flushPersonalityQueue(), this._personalityFlushDelay);
809
+ }
810
+
811
+ async _flushPersonalityQueue() {
812
+ if (!this._personalityQueue || Object.keys(this._personalityQueue).length === 0) {
813
+ if (this._personalityFlushTimer) {
814
+ clearTimeout(this._personalityFlushTimer);
815
+ this._personalityFlushTimer = null;
816
+ }
817
+ return;
818
+ }
819
+
820
+ const queue = this._personalityQueue;
821
+ this._personalityQueue = {};
822
+ if (this._personalityFlushTimer) {
823
+ clearTimeout(this._personalityFlushTimer);
824
+ this._personalityFlushTimer = null;
825
+ }
826
+
827
+ // For each character, write batch
828
+ for (const character of Object.keys(queue)) {
829
+ const traitsObj = queue[character];
830
+ try {
831
+ if (this._monitorPersonalityWrites) {
832
+ try {
833
+ console.log("[KimiDB Monitor] Flushing personality batch", { character, traitsObj });
834
+ } catch (e) {}
835
+ }
836
+ await this.setPersonalityBatch(traitsObj, character);
837
+ if (this._monitorPersonalityWrites) {
838
+ try {
839
+ console.log("[KimiDB Monitor] Flushed personality batch", { character });
840
+ } catch (e) {}
841
+ }
842
+ } catch (e) {
843
+ console.warn("Failed to flush personality batch for", character, e);
844
+ }
845
+ }
846
+ }
847
+
848
+ enablePersonalityMonitor(enable = true) {
849
+ this._monitorPersonalityWrites = !!enable;
850
+ console.log(`[KimiDB Monitor] enabled=${this._monitorPersonalityWrites}`);
851
+ }
852
+
853
+ async getPersonalityTrait(trait, defaultValue = null, character = null) {
854
+ if (!character) character = await this.getSelectedCharacter();
855
+
856
+ // Use unified defaults from emotion system
857
+ if (defaultValue === null) {
858
+ // Use centralized API for trait defaults
859
+ if (window.getTraitDefaults) {
860
+ defaultValue = window.getTraitDefaults()[trait] || 50;
861
+ } else if (window.KimiEmotionSystem) {
862
+ const emotionSystem = new window.KimiEmotionSystem(this);
863
+ defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
864
+ } else {
865
+ // Ultimate fallback (hardcoded values - should be avoided)
866
+ defaultValue =
867
+ {
868
+ affection: 55,
869
+ playfulness: 55,
870
+ intelligence: 70,
871
+ empathy: 75,
872
+ humor: 60,
873
+ romance: 50
874
+ }[trait] || 50;
875
+ }
876
+ }
877
+
878
+ // Try cache first
879
+ const cacheKey = `trait_${character}_${trait}`;
880
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") {
881
+ const cached = window.KimiCacheManager.get(cacheKey);
882
+ if (cached !== null) {
883
+ return cached;
884
+ }
885
+ }
886
+
887
+ const found = await this.db.personality.get([character, trait]);
888
+ const value = found ? found.value : defaultValue;
889
+
890
+ // Cache the result
891
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
892
+ window.KimiCacheManager.set(cacheKey, value, 120000); // Cache for 2 minutes
893
+ }
894
+ return value;
895
+ }
896
+
897
+ async getAllPersonalityTraits(character = null) {
898
+ if (!character) character = await this.getSelectedCharacter();
899
+
900
+ // Try cache first
901
+ const cacheKey = `all_traits_${character}`;
902
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") {
903
+ const cached = window.KimiCacheManager.get(cacheKey);
904
+ if (cached !== null) {
905
+ // Correction : valider les valeurs du cache
906
+ const safeTraits = {};
907
+ for (const [trait, value] of Object.entries(cached)) {
908
+ let v = Number(value);
909
+ if (!isFinite(v) || isNaN(v)) v = 50;
910
+ v = Math.max(0, Math.min(100, v));
911
+ safeTraits[trait] = v;
912
+ }
913
+ return safeTraits;
914
+ }
915
+ }
916
+
917
+ const all = await this.db.personality.where("character").equals(character).toArray();
918
+ const traits = {};
919
+ all.forEach(item => {
920
+ let v = Number(item.value);
921
+ if (!isFinite(v) || isNaN(v)) v = 50;
922
+ v = Math.max(0, Math.min(100, v));
923
+ traits[item.trait] = v;
924
+ });
925
+
926
+ // If no traits stored yet for this character, seed from character defaults (one-time)
927
+ if (Object.keys(traits).length === 0 && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]) {
928
+ const seed = window.KIMI_CHARACTERS[character].traits || {};
929
+ const safeSeed = {};
930
+ for (const [k, v] of Object.entries(seed)) {
931
+ const num = typeof v === "number" && isFinite(v) ? Math.max(0, Math.min(100, v)) : 50;
932
+ safeSeed[k] = num;
933
+ try {
934
+ await this.setPersonalityTrait(k, num, character);
935
+ } catch {}
936
+ }
937
+ return safeSeed;
938
+ }
939
+
940
+ // Cache the result
941
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
942
+ window.KimiCacheManager.set(cacheKey, traits, 120000); // Cache for 2 minutes
943
+ }
944
+ return traits;
945
+ }
946
+
947
+ async savePersonality(personalityObj, character = null) {
948
+ if (!character) character = await this.getSelectedCharacter();
949
+ // Invalidate caches for all affected traits and the aggregate cache for this character
950
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") {
951
+ try {
952
+ Object.keys(personalityObj).forEach(trait => {
953
+ window.KimiCacheManager.delete(`trait_${character}_${trait}`);
954
+ });
955
+ window.KimiCacheManager.delete(`all_traits_${character}`);
956
+ } catch (e) {}
957
+ }
958
+ const entries = Object.entries(personalityObj).map(([trait, value]) =>
959
+ this.db.personality.put({
960
+ trait: trait,
961
+ character: character,
962
+ value: value,
963
+ updated: new Date().toISOString()
964
+ })
965
+ );
966
+ return Promise.all(entries);
967
+ }
968
+
969
+ async getPersonality(character = null) {
970
+ return this.getAllPersonalityTraits(character);
971
+ }
972
+
973
+ async saveLLMModel(id, name, provider, apiKey, config) {
974
+ return this.db.llmModels.put({
975
+ id: id,
976
+ name: name,
977
+ provider: provider,
978
+ apiKey: apiKey,
979
+ config: config,
980
+ added: new Date().toISOString(),
981
+ lastUsed: null
982
+ });
983
+ }
984
+
985
+ async getLLMModel(id) {
986
+ return this.db.llmModels.get(id);
987
+ }
988
+
989
+ async getAllLLMModels() {
990
+ try {
991
+ return await this.db.llmModels.toArray();
992
+ } catch (error) {
993
+ console.warn("Error getting all LLM models:", error);
994
+ return [];
995
+ }
996
+ }
997
+
998
+ async deleteLLMModel(id) {
999
+ return this.db.llmModels.delete(id);
1000
+ }
1001
+
1002
+ async cleanOldConversations(days = null, character = null) {
1003
+ // If days not provided, fallback to full clean (legacy behavior)
1004
+ if (days === null) {
1005
+ if (character) {
1006
+ const all = await this.db.conversations.where("character").equals(character).toArray();
1007
+ const ids = all.map(item => item.id);
1008
+ return this.db.conversations.bulkDelete(ids);
1009
+ } else {
1010
+ return this.db.conversations.clear();
1011
+ }
1012
+ }
1013
+ const threshold = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
1014
+ if (character) {
1015
+ const toDelete = await this.db.conversations
1016
+ .where("character")
1017
+ .equals(character)
1018
+ .and(c => c.timestamp < threshold)
1019
+ .toArray();
1020
+ const ids = toDelete.map(item => item.id);
1021
+ return this.db.conversations.bulkDelete(ids);
1022
+ } else {
1023
+ const toDelete = await this.db.conversations.where("timestamp").below(threshold).toArray();
1024
+ const ids = toDelete.map(item => item.id);
1025
+ return this.db.conversations.bulkDelete(ids);
1026
+ }
1027
+ }
1028
+
1029
+ async getStorageStats() {
1030
+ try {
1031
+ const conversations = await this.getAllConversations();
1032
+ const preferences = await this.getAllPreferences();
1033
+ const models = await this.getAllLLMModels();
1034
+ return {
1035
+ conversations: conversations ? conversations.length : 0,
1036
+ preferences: preferences ? Object.keys(preferences).length : 0,
1037
+ models: models ? models.length : 0,
1038
+ totalSize: JSON.stringify({
1039
+ conversations: conversations || [],
1040
+ preferences: preferences || {},
1041
+ models: models || []
1042
+ }).length
1043
+ };
1044
+ } catch (error) {
1045
+ console.error("Error getting storage stats:", error);
1046
+ return {
1047
+ conversations: 0,
1048
+ preferences: 0,
1049
+ models: 0,
1050
+ totalSize: 0
1051
+ };
1052
+ }
1053
+ }
1054
+
1055
+ async deleteSingleMessage(conversationId, sender) {
1056
+ const conv = await this.db.conversations.get(conversationId);
1057
+ if (!conv) return;
1058
+ if (sender === "user") {
1059
+ conv.user = "";
1060
+ } else if (sender === "kimi") {
1061
+ conv.kimi = "";
1062
+ }
1063
+ if ((conv.user === undefined || conv.user === "") && (conv.kimi === undefined || conv.kimi === "")) {
1064
+ await this.db.conversations.delete(conversationId);
1065
+ } else {
1066
+ await this.db.conversations.put(conv);
1067
+ }
1068
+ }
1069
+
1070
+ async setPreferencesBatch(prefsArray) {
1071
+ // Backwards-compatible: accept either an array [{key,value},...] or an object map { key: value }
1072
+ let prefsInput = prefsArray;
1073
+ if (!Array.isArray(prefsInput) && prefsInput && typeof prefsInput === "object") {
1074
+ // convert map to array
1075
+ prefsInput = Object.keys(prefsInput).map(k => ({ key: k, value: prefsInput[k] }));
1076
+ console.warn("setPreferencesBatch: converted prefs map to array for backward compatibility");
1077
+ }
1078
+ if (!Array.isArray(prefsInput)) {
1079
+ console.warn("setPreferencesBatch: expected array or object, got", typeof prefsArray);
1080
+ return;
1081
+ }
1082
+
1083
+ const numericMap = {
1084
+ voiceRate: "VOICE_RATE",
1085
+ voicePitch: "VOICE_PITCH",
1086
+ voiceVolume: "VOICE_VOLUME",
1087
+ interfaceOpacity: "INTERFACE_OPACITY",
1088
+ llmTemperature: "LLM_TEMPERATURE",
1089
+ llmMaxTokens: "LLM_MAX_TOKENS",
1090
+ llmTopP: "LLM_TOP_P",
1091
+ llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY",
1092
+ llmPresencePenalty: "LLM_PRESENCE_PENALTY"
1093
+ };
1094
+ const batch = prefsInput.map(({ key, value }) => {
1095
+ if (numericMap[key] && window.KIMI_CONFIG && typeof window.KIMI_CONFIG.validate === "function") {
1096
+ const validation = window.KIMI_CONFIG.validate(value, numericMap[key]);
1097
+ if (validation.valid) value = validation.value;
1098
+ }
1099
+ return { key, value, updated: new Date().toISOString() };
1100
+ });
1101
+ return this.db.preferences.bulkPut(batch);
1102
+ }
1103
+ async setPersonalityBatch(traitsObj, character = null) {
1104
+ if (!character) character = await this.getSelectedCharacter();
1105
+ // Invalidate caches for all affected traits and the aggregate cache for this character
1106
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") {
1107
+ try {
1108
+ Object.keys(traitsObj).forEach(trait => {
1109
+ window.KimiCacheManager.delete(`trait_${character}_${trait}`);
1110
+ });
1111
+ window.KimiCacheManager.delete(`all_traits_${character}`);
1112
+ } catch (e) {}
1113
+ }
1114
+
1115
+ // Validation stricte : empêcher NaN ou valeurs non numériques
1116
+ const getDefault = trait => {
1117
+ // Use centralized API for consistency
1118
+ if (window.getTraitDefaults) {
1119
+ return window.getTraitDefaults()[trait] || 50;
1120
+ }
1121
+ if (window.KimiEmotionSystem) {
1122
+ return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50;
1123
+ }
1124
+ // Ultimate fallback (should be avoided)
1125
+ const fallback = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
1126
+ return fallback[trait] || 50;
1127
+ };
1128
+ const batch = Object.entries(traitsObj).map(([trait, value]) => {
1129
+ let v = Number(value);
1130
+ if (!isFinite(v) || isNaN(v)) v = getDefault(trait);
1131
+ v = Math.max(0, Math.min(100, v));
1132
+ return {
1133
+ trait,
1134
+ character,
1135
+ value: v,
1136
+ updated: new Date().toISOString()
1137
+ };
1138
+ });
1139
+ return this.db.personality.bulkPut(batch);
1140
+ }
1141
+ async setSettingsBatch(settingsArray) {
1142
+ const batch = settingsArray.map(({ category, settings }) => ({
1143
+ category,
1144
+ settings,
1145
+ updated: new Date().toISOString()
1146
+ }));
1147
+ return this.db.settings.bulkPut(batch);
1148
+ }
1149
+ async getPreferencesBatch(keys) {
1150
+ const results = await this.db.preferences.where("key").anyOf(keys).toArray();
1151
+ const out = {};
1152
+ for (const item of results) {
1153
+ let val = item.value;
1154
+ if (item.encrypted && window.KimiSecurityUtils) {
1155
+ try {
1156
+ val = item.value; // decrypt removed – stored as plain text
1157
+ // Migrate back as plain
1158
+ try {
1159
+ await this.db.preferences.put({ key: item.key, value: val, updated: new Date().toISOString() });
1160
+ } catch (mErr) {}
1161
+ } catch (e) {
1162
+ console.warn("Failed to decrypt legacy pref in batch:", item.key, e);
1163
+ }
1164
+ }
1165
+ out[item.key] = val;
1166
+ }
1167
+ return out;
1168
+ }
1169
+ async getPersonalityTraitsBatch(traits, character = null) {
1170
+ if (!character) character = await this.getSelectedCharacter();
1171
+ const results = await this.db.personality.where("character").equals(character).toArray();
1172
+ const out = {};
1173
+ traits.forEach(trait => {
1174
+ const found = results.find(item => item.trait === trait);
1175
+ out[trait] = found ? found.value : 50;
1176
+ });
1177
+ return out;
1178
+ }
1179
+
1180
+ async getSelectedCharacter() {
1181
+ try {
1182
+ return await this.getPreference("selectedCharacter", "kimi");
1183
+ } catch (error) {
1184
+ console.warn("Error getting selected character:", error);
1185
+ return "kimi";
1186
+ }
1187
+ }
1188
+
1189
+ async setSelectedCharacter(character) {
1190
+ try {
1191
+ await this.setPreference("selectedCharacter", character);
1192
+ } catch (error) {
1193
+ console.error("Error setting selected character:", error);
1194
+ }
1195
+ }
1196
+
1197
+ async getSystemPromptForCharacter(character = null) {
1198
+ if (!character) character = await this.getSelectedCharacter();
1199
+ try {
1200
+ const prompt = await this.getPreference(`systemPrompt_${character}`, null);
1201
+ if (prompt) return prompt;
1202
+
1203
+ if (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character] && window.KIMI_CHARACTERS[character].defaultPrompt) {
1204
+ return window.KIMI_CHARACTERS[character].defaultPrompt;
1205
+ }
1206
+
1207
+ return window.DEFAULT_SYSTEM_PROMPT || "";
1208
+ } catch (error) {
1209
+ console.warn("Error getting system prompt for character:", error);
1210
+ return window.DEFAULT_SYSTEM_PROMPT || "";
1211
+ }
1212
+ }
1213
+
1214
+ async setSystemPromptForCharacter(character, prompt) {
1215
+ if (!character) character = await this.getSelectedCharacter();
1216
+ try {
1217
+ await this.setPreference(`systemPrompt_${character}`, prompt);
1218
+ } catch (error) {
1219
+ console.error("Error setting system prompt for character:", error);
1220
+ }
1221
+ }
1222
+ }
1223
+
1224
+ export default KimiDatabase;
1225
+ // Export for usage
1226
+ window.KimiDatabase = KimiDatabase;
kimi-js/kimi-debug-utils.js ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // KIMI DEBUG UTILITIES
2
+ // Centralized debug management for production optimization
3
+ //
4
+ // USAGE:
5
+ // debugOn() - Enable all debug logs
6
+ // debugOff() - Disable all debug logs (production mode)
7
+ // debugStatus() - Show current debug configuration
8
+ // kimiDebugAll() - Complete debug dashboard (includes errors)
9
+ // kimiDiagnosDB() - Database schema diagnostics
10
+ //
11
+ // CATEGORIES:
12
+ // KimiDebugController.setDebugCategory("VIDEO", true)
13
+ // KimiDebugController.setDebugCategory("MEMORY", false)
14
+ // Available: VIDEO, VOICE, MEMORY, API, SYNC
15
+
16
+ // Global debug controller
17
+ window.KimiDebugController = {
18
+ // Enable/disable all debug features
19
+ setGlobalDebug(enabled) {
20
+ if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) {
21
+ window.KIMI_CONFIG.DEBUG.ENABLED = enabled;
22
+ window.KIMI_CONFIG.DEBUG.VOICE = enabled;
23
+ window.KIMI_CONFIG.DEBUG.VIDEO = enabled;
24
+ window.KIMI_CONFIG.DEBUG.MEMORY = enabled;
25
+ window.KIMI_CONFIG.DEBUG.API = enabled;
26
+ window.KIMI_CONFIG.DEBUG.SYNC = enabled;
27
+ }
28
+
29
+ // Legacy flags (to be removed)
30
+ window.KIMI_DEBUG_SYNC = enabled;
31
+ window.KIMI_DEBUG_MEMORIES = enabled;
32
+ window.KIMI_DEBUG_API_AUDIT = enabled;
33
+ window.DEBUG_SAFE_LOGS = enabled;
34
+
35
+ console.log(`🔧 Global debug ${enabled ? "ENABLED" : "DISABLED"}`);
36
+ },
37
+
38
+ // Enable specific debug category
39
+ setDebugCategory(category, enabled) {
40
+ if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) {
41
+ if (category in window.KIMI_CONFIG.DEBUG) {
42
+ window.KIMI_CONFIG.DEBUG[category] = enabled;
43
+ console.log(`🔧 Debug category ${category} ${enabled ? "ENABLED" : "DISABLED"}`);
44
+ }
45
+ }
46
+
47
+ // Video manager specific
48
+ if (category === "VIDEO" && window.kimiVideo) {
49
+ window.kimiVideo.setDebug(enabled);
50
+ }
51
+ },
52
+
53
+ // Production mode (all debug off)
54
+ setProductionMode() {
55
+ this.setGlobalDebug(false);
56
+ console.log("🚀 Production mode activated - all debug logs disabled");
57
+ },
58
+
59
+ // Development mode (selective debug on)
60
+ setDevelopmentMode() {
61
+ this.setGlobalDebug(true);
62
+ console.log("🛠️ Development mode activated - debug logs enabled");
63
+ },
64
+
65
+ // Get current debug status
66
+ getDebugStatus() {
67
+ const status = {
68
+ global: window.KIMI_CONFIG?.DEBUG?.ENABLED || false,
69
+ voice: window.KIMI_CONFIG?.DEBUG?.VOICE || false,
70
+ video: window.KIMI_CONFIG?.DEBUG?.VIDEO || false,
71
+ memory: window.KIMI_CONFIG?.DEBUG?.MEMORY || false,
72
+ api: window.KIMI_CONFIG?.DEBUG?.API || false,
73
+ sync: window.KIMI_CONFIG?.DEBUG?.SYNC || false
74
+ };
75
+
76
+ console.table(status);
77
+ return status;
78
+ }
79
+ };
80
+
81
+ // Quick shortcuts for console
82
+ window.debugOn = () => window.KimiDebugController.setDevelopmentMode();
83
+ window.debugOff = () => window.KimiDebugController.setProductionMode();
84
+ window.debugStatus = () => window.KimiDebugController.getDebugStatus();
85
+
86
+ // Integration with error manager for unified debugging
87
+ window.kimiDebugAll = () => {
88
+ console.group("🔧 Kimi Debug Dashboard");
89
+ window.KimiDebugController.getDebugStatus();
90
+ if (window.kimiErrorManager) {
91
+ window.kimiErrorManager.printErrorSummary();
92
+ }
93
+ console.groupEnd();
94
+ };
95
+
96
+ // Database diagnostics helper
97
+ window.kimiDiagnosDB = async () => {
98
+ console.group("🔍 Database Diagnostics");
99
+ try {
100
+ if (window.kimiDB) {
101
+ console.log("📊 Database version:", window.kimiDB.db.verno);
102
+ console.log("📋 Available tables:", Object.keys(window.kimiDB.db._dbSchema));
103
+
104
+ // Check memories table schema
105
+ const memoriesSchema = window.kimiDB.db._dbSchema.memories;
106
+ if (memoriesSchema) {
107
+ console.log("🧠 Memories schema:", memoriesSchema);
108
+ const hasCharacterIsActiveIndex = memoriesSchema.indexes?.some(
109
+ idx =>
110
+ idx.name === "[character+isActive]" ||
111
+ (idx.keyPath?.includes("character") && idx.keyPath?.includes("isActive"))
112
+ );
113
+ console.log("✅ [character+isActive] index:", hasCharacterIsActiveIndex ? "PRESENT" : "❌ MISSING");
114
+
115
+ if (!hasCharacterIsActiveIndex) {
116
+ console.warn(
117
+ "🚨 SOLUTION: Clear browser data (Application > Storage > Clear Site Data) to force schema upgrade"
118
+ );
119
+ }
120
+ }
121
+ } else {
122
+ console.warn("❌ Database not initialized yet");
123
+ }
124
+ } catch (error) {
125
+ console.error("Error during database diagnostics:", error);
126
+ }
127
+ console.groupEnd();
128
+ };
129
+
130
+ // Auto-initialize to production mode for performance
131
+ if (typeof window.KIMI_CONFIG !== "undefined") {
132
+ window.KimiDebugController.setProductionMode();
133
+ }
kimi-js/kimi-emotion-system.js ADDED
@@ -0,0 +1,1060 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI UNIFIED EMOTION SYSTEM =====
2
+ // Centralizes all emotion analysis, personality updates, and validation
3
+
4
+ class KimiEmotionSystem {
5
+ constructor(database = null) {
6
+ this.db = database;
7
+ this.negativeStreaks = {};
8
+
9
+ // Debouncing system for personality updates
10
+ this._personalityUpdateQueue = {};
11
+ this._personalityUpdateTimer = null;
12
+ this._personalityUpdateDelay = 300; // ms
13
+
14
+ // Unified emotion mappings
15
+ this.EMOTIONS = {
16
+ // Base emotions
17
+ POSITIVE: "positive",
18
+ NEGATIVE: "negative",
19
+ NEUTRAL: "neutral",
20
+
21
+ // Specific emotions
22
+ ROMANTIC: "romantic",
23
+ DANCING: "dancing",
24
+ LISTENING: "listening",
25
+ LAUGHING: "laughing",
26
+ SURPRISE: "surprise",
27
+ CONFIDENT: "confident",
28
+ SHY: "shy",
29
+ FLIRTATIOUS: "flirtatious",
30
+ KISS: "kiss",
31
+ GOODBYE: "goodbye",
32
+
33
+ // New emotions for new characters
34
+ ANDROID: "android",
35
+ SENSUAL: "sensual",
36
+ LOVE: "love"
37
+ };
38
+
39
+ // Unified video context mapping - CENTRALIZED SOURCE OF TRUTH
40
+ this.emotionToVideoCategory = {
41
+ // Base emotional states
42
+ positive: "speakingPositive",
43
+ negative: "speakingNegative",
44
+ neutral: "neutral",
45
+
46
+ // Special contexts (always take priority)
47
+ dancing: "dancing",
48
+ listening: "listening",
49
+
50
+ // Specific emotions mapped to appropriate categories
51
+ romantic: "speakingPositive",
52
+ laughing: "speakingPositive",
53
+ surprise: "speakingPositive",
54
+ confident: "speakingPositive",
55
+ flirtatious: "speakingPositive",
56
+ kiss: "speakingPositive",
57
+
58
+ // Neutral/subdued emotions
59
+ shy: "neutral",
60
+ goodbye: "neutral",
61
+
62
+ // New character specific emotions mapped to EXISTING categories only
63
+ android: "speakingPositive", // 2Blanche responses use existing speakingPositive videos
64
+ sensual: "speakingPositive", // Jasmine sensual mode uses existing speakingPositive videos
65
+ love: "speakingPositive", // Jasmine love mode uses existing speakingPositive videos
66
+
67
+ // Explicit context mappings (for compatibility)
68
+ speaking: "speakingPositive", // Generic speaking defaults to positive
69
+ speakingPositive: "speakingPositive",
70
+ speakingNegative: "speakingNegative"
71
+ };
72
+
73
+ // Emotion priority weights for conflict resolution
74
+ this.emotionPriorities = {
75
+ dancing: 10, // Maximum priority - immersive experience
76
+ kiss: 9, // Very high - intimate moment
77
+ romantic: 8, // High - emotional connection
78
+ listening: 7, // High - active interaction
79
+ android: 6, // Medium-high - character-specific context
80
+ sensual: 6, // Medium-high - character-specific context
81
+ love: 6, // Medium-high - character-specific context
82
+ flirtatious: 6, // Medium-high - playful interaction
83
+ laughing: 6, // Medium-high - positive expression
84
+ surprise: 5, // Medium - reaction
85
+ confident: 5, // Medium - personality expression
86
+ speaking: 4, // Medium-low - generic speaking context
87
+ positive: 4, // Medium-low - general positive
88
+ negative: 4, // Medium-low - general negative
89
+ neutral: 3, // Low - default state
90
+ shy: 3, // Low - subdued state
91
+ goodbye: 2, // Very low - transitional
92
+ speakingPositive: 4, // Medium-low - for consistency
93
+ speakingNegative: 4 // Medium-low - for consistency
94
+ };
95
+
96
+ // Context/emotion validation system for system integrity
97
+ this.validContexts = ["dancing", "listening", "speaking", "speakingPositive", "speakingNegative", "neutral"];
98
+ this.validEmotions = Object.values(this.EMOTIONS);
99
+
100
+ // Unified trait defaults - Balanced for progressive experience
101
+ this.TRAIT_DEFAULTS = {
102
+ affection: 55, // Baseline neutral affection
103
+ playfulness: 55, // Moderately playful baseline
104
+ intelligence: 70, // Competent baseline intellect
105
+ empathy: 75, // Warm & caring baseline
106
+ humor: 60, // Mild sense of humor baseline
107
+ romance: 50 // Neutral romance baseline (earned over time)
108
+ };
109
+
110
+ // Central emotion -> trait base deltas (pre global multipliers & gainCfg scaling)
111
+ // Positive numbers increase trait, negative decrease.
112
+ // Keep values small; final effect passes through adjustUp/adjustDown and global multipliers.
113
+ this.EMOTION_TRAIT_EFFECTS = {
114
+ positive: { affection: 0.45, empathy: 0.2, playfulness: 0.25, humor: 0.25 },
115
+ negative: { affection: -0.7, empathy: 0.3 },
116
+ romantic: { romance: 0.7, affection: 0.55, empathy: 0.15 },
117
+ flirtatious: { romance: 0.55, playfulness: 0.45, affection: 0.25 },
118
+ laughing: { humor: 0.85, playfulness: 0.5, affection: 0.25 },
119
+ dancing: { playfulness: 1.1, affection: 0.45 },
120
+ surprise: { intelligence: 0.12, empathy: 0.12 },
121
+ shy: { romance: -0.3, affection: -0.12 },
122
+ confident: { intelligence: 0.15, affection: 0.55 },
123
+ listening: { empathy: 0.6, intelligence: 0.25 },
124
+ kiss: { romance: 0.85, affection: 0.7 },
125
+ goodbye: { affection: -0.15, empathy: 0.1 },
126
+
127
+ // New character-specific emotions
128
+ android: { intelligence: 0.8, affection: 0.1, empathy: -0.2 }, // High intelligence, slow emotional progress
129
+ sensual: { intelligence: 0.6, playfulness: 0.4, romance: 0.3 }, // Brilliance with charm
130
+ love: { playfulness: 0.7, intelligence: 0.3, affection: 0.2 } // Sensual energy
131
+ };
132
+
133
+ // Trait keyword scaling model for conversation analysis (per-message delta shaping)
134
+ this.TRAIT_KEYWORD_MODEL = {
135
+ affection: { posFactor: 0.5, negFactor: 0.65, streakPenaltyAfter: 3, maxStep: 2 },
136
+ romance: { posFactor: 0.55, negFactor: 0.75, streakPenaltyAfter: 2, maxStep: 1.8 },
137
+ empathy: { posFactor: 0.4, negFactor: 0.5, streakPenaltyAfter: 3, maxStep: 1.5 },
138
+ playfulness: { posFactor: 0.45, negFactor: 0.4, streakPenaltyAfter: 4, maxStep: 1.4 },
139
+ humor: { posFactor: 0.55, negFactor: 0.45, streakPenaltyAfter: 4, maxStep: 1.6 },
140
+ intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 }
141
+ };
142
+ }
143
+
144
+ // ===== DEBOUNCED PERSONALITY UPDATE SYSTEM =====
145
+ _debouncedPersonalityUpdate(updates, character) {
146
+ // Merge with existing queued updates for this character
147
+ if (!this._personalityUpdateQueue[character]) {
148
+ this._personalityUpdateQueue[character] = {};
149
+ }
150
+ Object.assign(this._personalityUpdateQueue[character], updates);
151
+
152
+ // Clear existing timer and set new one
153
+ if (this._personalityUpdateTimer) {
154
+ clearTimeout(this._personalityUpdateTimer);
155
+ }
156
+
157
+ this._personalityUpdateTimer = setTimeout(async () => {
158
+ try {
159
+ const allUpdates = { ...this._personalityUpdateQueue };
160
+ this._personalityUpdateQueue = {};
161
+ this._personalityUpdateTimer = null;
162
+
163
+ // Process all queued updates
164
+ for (const [char, traits] of Object.entries(allUpdates)) {
165
+ if (Object.keys(traits).length > 0) {
166
+ await this.db.setPersonalityBatch(traits, char);
167
+
168
+ // Emit unified personality update event
169
+ if (typeof window !== "undefined" && window.dispatchEvent) {
170
+ window.dispatchEvent(
171
+ new CustomEvent("personality:updated", {
172
+ detail: { character: char, traits: traits }
173
+ })
174
+ );
175
+ }
176
+ }
177
+ }
178
+ } catch (error) {
179
+ console.error("Error in debounced personality update:", error);
180
+ }
181
+ }, this._personalityUpdateDelay);
182
+ }
183
+
184
+ // ===== CENTRALIZED VALIDATION SYSTEM =====
185
+ validateContext(context) {
186
+ if (!context || typeof context !== "string") return "neutral";
187
+ const normalized = context.toLowerCase().trim();
188
+
189
+ // Check if it's a valid context
190
+ if (this.validContexts.includes(normalized)) return normalized;
191
+
192
+ // Check if it's a valid emotion that can be mapped to context
193
+ if (this.emotionToVideoCategory[normalized]) return normalized;
194
+
195
+ return "neutral"; // Safe fallback
196
+ }
197
+
198
+ validateEmotion(emotion) {
199
+ if (!emotion || typeof emotion !== "string") return "neutral";
200
+ const normalized = emotion.toLowerCase().trim();
201
+
202
+ // Check if it's a valid emotion
203
+ if (this.validEmotions.includes(normalized)) return normalized;
204
+
205
+ // Check common aliases
206
+ // NOTE (Clarity Patch - Option 1):
207
+ // The following alias map intentionally routes the generic context word "speaking"
208
+ // (and its positive / negative variants) to the polarity emotions "positive" / "negative".
209
+ // Later, when a video category is needed, the system remaps:
210
+ // positive -> speakingPositive (via emotionToVideoCategory)
211
+ // negative -> speakingNegative (via emotionToVideoCategory)
212
+ // Rationale:
213
+ // 1. Keep EMOTIONS focused on high-level semantic emotions (positive/negative) instead of
214
+ // duplicating technical rendering states (speakingPositive / speakingNegative).
215
+ // 2. Preserve backward compatibility with older code that emitted "speaking" as an emotion.
216
+ // 3. Reduce surface area of the emotion validation list while still achieving correct video output.
217
+ // This can look like a double hop (speaking -> positive -> speakingPositive) but no information is lost.
218
+ // If later a direct mapping is desired, Option 2 would be to add SPEAKING_POSITIVE / SPEAKING_NEGATIVE
219
+ // into EMOTIONS and point aliases directly there. For now we keep the lean design.
220
+ const aliases = {
221
+ happy: "positive",
222
+ sad: "negative",
223
+ mad: "negative",
224
+ angry: "negative",
225
+ excited: "positive",
226
+ calm: "neutral",
227
+ romance: "romantic",
228
+ laugh: "laughing",
229
+ dance: "dancing",
230
+ // Speaking contexts as emotion aliases
231
+ speaking: "positive", // Generic speaking defaults to positive
232
+ speakingpositive: "positive",
233
+ speakingnegative: "negative",
234
+ // New character-specific aliases
235
+ robot: "android",
236
+ robotic: "android",
237
+ military: "android",
238
+ tactical: "android",
239
+ sensual: "sensual",
240
+ pleasure: "sensual",
241
+ emotional: "sensual",
242
+ intimate: "sensual",
243
+ tenderness: "love",
244
+ intimacy: "love",
245
+ position: "love",
246
+ love: "love"
247
+ };
248
+
249
+ if (aliases[normalized]) return aliases[normalized];
250
+
251
+ return "neutral"; // Safe fallback
252
+ }
253
+
254
+ validateVideoCategory(category) {
255
+ const validCategories = ["dancing", "listening", "speakingPositive", "speakingNegative", "neutral"];
256
+ if (!category || typeof category !== "string") return "neutral";
257
+
258
+ const normalized = category.toLowerCase().trim();
259
+ return validCategories.includes(normalized) ? normalized : "neutral";
260
+ }
261
+
262
+ // Enhanced emotion analysis with validation
263
+ analyzeEmotionValidated(text, lang = "auto") {
264
+ const rawEmotion = this.analyzeEmotion(text, lang);
265
+ return this.validateEmotion(rawEmotion);
266
+ }
267
+
268
+ // ===== UTILITY METHODS FOR SYSTEM INTEGRATION =====
269
+ // Centralized method to get video category for any emotion/context combination
270
+ getVideoCategory(emotionOrContext, traits = null) {
271
+ // Handle the case where we get both context and emotion (e.g., from determineCategory calls)
272
+ // Priority: Specific contexts > Specific emotions > Generic fallbacks
273
+
274
+ // Try context validation first for immediate context matches
275
+ let validated = this.validateContext(emotionOrContext);
276
+ if (validated !== "neutral" || emotionOrContext === "neutral") {
277
+ // Valid context found or explicitly neutral
278
+ const category = this.emotionToVideoCategory[validated] || "neutral";
279
+ return this.validateVideoCategory(category);
280
+ }
281
+
282
+ // If no valid context, try as emotion
283
+ validated = this.validateEmotion(emotionOrContext);
284
+ const category = this.emotionToVideoCategory[validated] || "neutral";
285
+ return this.validateVideoCategory(category);
286
+ } // Get priority weight for any emotion/context
287
+ getPriorityWeight(emotionOrContext) {
288
+ // Try context validation first, then emotion validation
289
+ let validated = this.validateContext(emotionOrContext);
290
+ if (validated === "neutral" && emotionOrContext !== "neutral") {
291
+ // If context validation gave neutral but input wasn't neutral, try as emotion
292
+ validated = this.validateEmotion(emotionOrContext);
293
+ }
294
+
295
+ return this.emotionPriorities[validated] || 3; // Default medium-low priority
296
+ }
297
+
298
+ // Check if an emotion/context should override current state
299
+ shouldOverride(newEmotion, currentEmotion, currentContext = null) {
300
+ const newPriority = this.getPriorityWeight(newEmotion);
301
+ const currentPriority = Math.max(this.getPriorityWeight(currentEmotion), this.getPriorityWeight(currentContext));
302
+
303
+ return newPriority > currentPriority;
304
+ }
305
+
306
+ // Utility to normalize and validate a complete emotion/context request
307
+ normalizeEmotionRequest(context, emotion, traits = null) {
308
+ return {
309
+ context: this.validateContext(context),
310
+ emotion: this.validateEmotion(emotion),
311
+ category: this.getVideoCategory(emotion || context, traits),
312
+ priority: this.getPriorityWeight(emotion || context)
313
+ };
314
+ }
315
+
316
+ // ===== UNIFIED EMOTION ANALYSIS =====
317
+ analyzeEmotion(text, lang = "auto") {
318
+ if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
319
+ const lowerText = this.normalizeText(text);
320
+
321
+ // Auto-detect language
322
+ let detectedLang = this._detectLanguage(text, lang);
323
+
324
+ // Get language-specific polarity keywords via centralized helpers
325
+ const positiveWords = (window.getPolarityWords && window.getPolarityWords("positive", detectedLang)) || ["happy", "good", "great", "love"];
326
+ const negativeWords = (window.getPolarityWords && window.getPolarityWords("negative", detectedLang)) || ["sad", "bad", "angry", "hate"];
327
+
328
+ const emotionKeywords = window.KIMI_CONTEXT_KEYWORDS?.[detectedLang] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
329
+
330
+ // Hostile override (immediate negative if hostile keywords present via centralized helper)
331
+ try {
332
+ if (window.isHostileText && window.isHostileText(text, detectedLang)) {
333
+ return this.EMOTIONS.NEGATIVE;
334
+ }
335
+ // Fallback: also scan english if language auto-detected incorrectly
336
+ if (detectedLang !== "en" && window.isHostileText && window.isHostileText(text, "en")) {
337
+ return this.EMOTIONS.NEGATIVE;
338
+ }
339
+ } catch {}
340
+
341
+ // Priority order for emotion detection - reordered for better logic
342
+ const emotionChecks = [
343
+ // High-impact emotions first
344
+ { emotion: this.EMOTIONS.KISS, keywords: emotionKeywords.kiss || ["kiss", "embrace"] },
345
+ { emotion: this.EMOTIONS.DANCING, keywords: emotionKeywords.dancing || ["dance", "dancing"] },
346
+ { emotion: this.EMOTIONS.ROMANTIC, keywords: emotionKeywords.romantic || ["love", "romantic"] },
347
+ { emotion: this.EMOTIONS.FLIRTATIOUS, keywords: emotionKeywords.flirtatious || ["flirt", "tease"] },
348
+ { emotion: this.EMOTIONS.LAUGHING, keywords: emotionKeywords.laughing || ["laugh", "funny"] },
349
+ { emotion: this.EMOTIONS.SURPRISE, keywords: emotionKeywords.surprise || ["wow", "surprise"] },
350
+ { emotion: this.EMOTIONS.CONFIDENT, keywords: emotionKeywords.confident || ["confident", "strong"] },
351
+ { emotion: this.EMOTIONS.SHY, keywords: emotionKeywords.shy || ["shy", "embarrassed"] },
352
+ { emotion: this.EMOTIONS.GOODBYE, keywords: emotionKeywords.goodbye || ["goodbye", "bye"] },
353
+ // Listening intent (lower priority to not mask other emotions)
354
+ {
355
+ emotion: this.EMOTIONS.LISTENING,
356
+ keywords: emotionKeywords.listening || ["listen carefully", "I'm listening", "listening to you", "hear me out", "pay attention"]
357
+ }
358
+ ];
359
+
360
+ // Check for specific emotions first, applying sensitivity weights per language
361
+ const sensitivity = (window.KIMI_EMOTION_SENSITIVITY && (window.KIMI_EMOTION_SENSITIVITY[detectedLang] || window.KIMI_EMOTION_SENSITIVITY.default)) || {
362
+ listening: 1,
363
+ dancing: 1,
364
+ romantic: 1,
365
+ laughing: 1,
366
+ surprise: 1,
367
+ confident: 1,
368
+ shy: 1,
369
+ flirtatious: 1,
370
+ kiss: 1,
371
+ goodbye: 1,
372
+ positive: 1,
373
+ negative: 1
374
+ };
375
+
376
+ // Normalize keyword lists to handle accents/contractions
377
+ const normalizeList = arr => (Array.isArray(arr) ? arr.map(x => this.normalizeText(String(x))).filter(Boolean) : []);
378
+ const normalizedPositiveWords = normalizeList(positiveWords);
379
+ const normalizedNegativeWords = normalizeList(negativeWords);
380
+ const normalizedChecks = emotionChecks.map(ch => ({
381
+ emotion: ch.emotion,
382
+ keywords: normalizeList(ch.keywords)
383
+ }));
384
+
385
+ let bestEmotion = null;
386
+ let bestScore = 0;
387
+ for (const check of normalizedChecks) {
388
+ const hits = check.keywords.reduce((acc, word) => acc + (this.countTokenMatches(lowerText, String(word)) ? 1 : 0), 0);
389
+ if (hits > 0) {
390
+ const key = check.emotion;
391
+ const weight = sensitivity[key] != null ? sensitivity[key] : 1;
392
+ const score = hits * weight;
393
+ if (score > bestScore) {
394
+ bestScore = score;
395
+ bestEmotion = check.emotion;
396
+ }
397
+ }
398
+ }
399
+ if (bestEmotion) return bestEmotion;
400
+
401
+ // Fall back to positive/negative analysis (use normalized lists)
402
+ const hasPositive = normalizedPositiveWords.some(word => this.countTokenMatches(lowerText, String(word)) > 0);
403
+ const hasNegative = normalizedNegativeWords.some(word => this.countTokenMatches(lowerText, String(word)) > 0);
404
+
405
+ // If some positive keywords are present but negated, treat as negative
406
+ const negatedPositive = normalizedPositiveWords.some(word => this.isTokenNegated(lowerText, String(word)));
407
+
408
+ if (hasPositive && !hasNegative) {
409
+ if (negatedPositive) {
410
+ return this.EMOTIONS.NEGATIVE;
411
+ }
412
+ // Apply sensitivity for base polarity
413
+ if ((sensitivity.positive || 1) >= (sensitivity.negative || 1)) return this.EMOTIONS.POSITIVE;
414
+ // If negative is favored, still fall back to positive since no negative hit
415
+ return this.EMOTIONS.POSITIVE;
416
+ }
417
+ if (hasNegative && !hasPositive) {
418
+ if ((sensitivity.negative || 1) >= (sensitivity.positive || 1)) return this.EMOTIONS.NEGATIVE;
419
+ return this.EMOTIONS.NEGATIVE;
420
+ }
421
+ return this.EMOTIONS.NEUTRAL;
422
+ }
423
+
424
+ // ===== UNIFIED PERSONALITY SYSTEM =====
425
+ async updatePersonalityFromEmotion(emotion, text, character = null) {
426
+ if (!this.db) {
427
+ console.warn("Database not available for personality updates");
428
+ return;
429
+ }
430
+
431
+ const selectedCharacter = character || (await this.db.getSelectedCharacter());
432
+ const traits = window.getCharacterTraits
433
+ ? await window.getCharacterTraits(selectedCharacter)
434
+ : await this.db.getAllPersonalityTraits(selectedCharacter);
435
+
436
+ const safe = (v, def) => (typeof v === "number" && isFinite(v) ? v : def);
437
+ let affection = safe(traits?.affection, this.TRAIT_DEFAULTS.affection);
438
+ let romance = safe(traits?.romance, this.TRAIT_DEFAULTS.romance);
439
+ let empathy = safe(traits?.empathy, this.TRAIT_DEFAULTS.empathy);
440
+ let playfulness = safe(traits?.playfulness, this.TRAIT_DEFAULTS.playfulness);
441
+ let humor = safe(traits?.humor, this.TRAIT_DEFAULTS.humor);
442
+ let intelligence = safe(traits?.intelligence, this.TRAIT_DEFAULTS.intelligence);
443
+
444
+ // Unified adjustment functions - More balanced progression for better user experience
445
+ const adjustUp = (val, amount) => {
446
+ // Gradual slowdown only at very high levels to allow natural progression
447
+ if (val >= 95) return val + amount * 0.2; // Slow near max to preserve challenge
448
+ if (val >= 88) return val + amount * 0.5; // Moderate slowdown at very high levels
449
+ if (val >= 80) return val + amount * 0.7; // Slight slowdown at high levels
450
+ if (val >= 60) return val + amount * 0.9; // Nearly normal progression in mid-high range
451
+ return val + amount; // Normal progression below 60%
452
+ };
453
+
454
+ const adjustDown = (val, amount) => {
455
+ // Faster decline at higher values - easier to lose than to gain
456
+ if (val >= 80) return val - amount * 1.2; // Faster loss at high levels
457
+ if (val >= 60) return val - amount; // Normal loss at medium levels
458
+ if (val >= 40) return val - amount * 0.8; // Slower loss at low-medium levels
459
+ if (val <= 20) return val - amount * 0.4; // Very slow loss at low levels
460
+ return val - amount * 0.6; // Moderate loss between 20-40
461
+ };
462
+
463
+ // Unified emotion-based adjustments - More balanced and realistic progression
464
+ const gainCfg = window.KIMI_TRAIT_ADJUSTMENT || {
465
+ globalGain: 1,
466
+ globalLoss: 1,
467
+ emotionGain: {},
468
+ traitGain: {},
469
+ traitLoss: {}
470
+ };
471
+ const emoGain = emotion && gainCfg.emotionGain ? gainCfg.emotionGain[emotion] || 1 : 1;
472
+ const GGAIN = (gainCfg.globalGain || 1) * emoGain;
473
+ const GLOSS = gainCfg.globalLoss || 1;
474
+
475
+ // Helpers to apply trait-specific scaling
476
+ const scaleGain = (traitName, baseDelta) => {
477
+ const t = gainCfg.traitGain && (gainCfg.traitGain[traitName] || 1);
478
+ return baseDelta * GGAIN * t;
479
+ };
480
+ const scaleLoss = (traitName, baseDelta) => {
481
+ const t = gainCfg.traitLoss && (gainCfg.traitLoss[traitName] || 1);
482
+ return baseDelta * GLOSS * t;
483
+ };
484
+
485
+ // Apply emotion deltas from centralized map (if defined)
486
+ const map = this.EMOTION_TRAIT_EFFECTS?.[emotion];
487
+ if (map) {
488
+ for (const [traitName, baseDelta] of Object.entries(map)) {
489
+ const delta = baseDelta; // base delta -> will be scaled below
490
+ if (delta === 0) continue;
491
+ switch (traitName) {
492
+ case "affection":
493
+ affection =
494
+ delta > 0
495
+ ? Math.min(100, adjustUp(affection, scaleGain("affection", delta)))
496
+ : Math.max(0, adjustDown(affection, scaleLoss("affection", Math.abs(delta))));
497
+ break;
498
+ case "romance":
499
+ romance =
500
+ delta > 0
501
+ ? Math.min(100, adjustUp(romance, scaleGain("romance", delta)))
502
+ : Math.max(0, adjustDown(romance, scaleLoss("romance", Math.abs(delta))));
503
+ break;
504
+ case "empathy":
505
+ empathy =
506
+ delta > 0
507
+ ? Math.min(100, adjustUp(empathy, scaleGain("empathy", delta)))
508
+ : Math.max(0, adjustDown(empathy, scaleLoss("empathy", Math.abs(delta))));
509
+ break;
510
+ case "playfulness":
511
+ playfulness =
512
+ delta > 0
513
+ ? Math.min(100, adjustUp(playfulness, scaleGain("playfulness", delta)))
514
+ : Math.max(0, adjustDown(playfulness, scaleLoss("playfulness", Math.abs(delta))));
515
+ break;
516
+ case "humor":
517
+ humor =
518
+ delta > 0
519
+ ? Math.min(100, adjustUp(humor, scaleGain("humor", delta)))
520
+ : Math.max(0, adjustDown(humor, scaleLoss("humor", Math.abs(delta))));
521
+ break;
522
+ case "intelligence":
523
+ intelligence =
524
+ delta > 0
525
+ ? Math.min(100, adjustUp(intelligence, scaleGain("intelligence", delta)))
526
+ : Math.max(0, adjustDown(intelligence, scaleLoss("intelligence", Math.abs(delta))));
527
+ break;
528
+ }
529
+ }
530
+ }
531
+
532
+ // Cross-trait interactions - traits influence each other for more realistic personality development
533
+ // High empathy should boost affection over time
534
+ if (empathy >= 75 && affection < empathy - 5) {
535
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.1)));
536
+ }
537
+
538
+ // High intelligence should slightly boost empathy (understanding others)
539
+ if (intelligence >= 80 && empathy < intelligence - 10) {
540
+ empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.05)));
541
+ }
542
+
543
+ // Humor and playfulness should reinforce each other
544
+ if (humor >= 70 && playfulness < humor - 10) {
545
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.05)));
546
+ }
547
+ if (playfulness >= 70 && humor < playfulness - 10) {
548
+ humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.05)));
549
+ }
550
+
551
+ // Content-based adjustments (unified)
552
+ await this._analyzeTextContent(
553
+ text,
554
+ traits => {
555
+ if (typeof traits.romance !== "undefined") romance = traits.romance;
556
+ if (typeof traits.affection !== "undefined") affection = traits.affection;
557
+ if (typeof traits.humor !== "undefined") humor = traits.humor;
558
+ if (typeof traits.playfulness !== "undefined") playfulness = traits.playfulness;
559
+ },
560
+ adjustUp
561
+ );
562
+
563
+ // Cross-trait modifiers (applied after primary emotion & content changes)
564
+ ({ affection, romance, empathy, playfulness, humor, intelligence } = this._applyCrossTraitModifiers({
565
+ affection,
566
+ romance,
567
+ empathy,
568
+ playfulness,
569
+ humor,
570
+ intelligence,
571
+ adjustUp,
572
+ adjustDown,
573
+ scaleGain,
574
+ scaleLoss
575
+ }));
576
+
577
+ // Preserve fractional progress to allow gradual visible changes
578
+ const to2 = v => Number(Number(v).toFixed(2));
579
+ const clamp = v => Math.max(0, Math.min(100, v));
580
+ const updatedTraits = {
581
+ affection: to2(clamp(affection)),
582
+ romance: to2(clamp(romance)),
583
+ empathy: to2(clamp(empathy)),
584
+ playfulness: to2(clamp(playfulness)),
585
+ humor: to2(clamp(humor)),
586
+ intelligence: to2(clamp(intelligence))
587
+ };
588
+
589
+ // Prepare persistence with smoothing / threshold to avoid tiny writes
590
+ const toPersist = {};
591
+ for (const [trait, candValue] of Object.entries(updatedTraits)) {
592
+ const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
593
+ const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter);
594
+ if (prep.shouldPersist) toPersist[trait] = prep.value;
595
+ }
596
+
597
+ // Use debounced update instead of immediate DB write
598
+ if (Object.keys(toPersist).length > 0) {
599
+ this._debouncedPersonalityUpdate(toPersist, selectedCharacter);
600
+ }
601
+
602
+ return updatedTraits;
603
+ }
604
+
605
+ // Apply cross-trait synergy & balancing rules.
606
+ _applyCrossTraitModifiers(ctx) {
607
+ let { affection, romance, empathy, playfulness, humor, intelligence, adjustUp, adjustDown, scaleGain } = ctx;
608
+ // High empathy soft-boost affection if still lagging
609
+ if (empathy >= 80 && affection < empathy - 8) {
610
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.08)));
611
+ }
612
+ // High romance amplifies affection gains subtlely
613
+ if (romance >= 80 && affection < romance - 5) {
614
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.06)));
615
+ }
616
+ // High affection but lower romance triggers slight romance catch-up
617
+ if (affection >= 90 && romance < 70) {
618
+ romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.05)));
619
+ }
620
+ // Intelligence supports empathy & humor small growth
621
+ if (intelligence >= 85) {
622
+ if (empathy < intelligence - 12) {
623
+ empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.04)));
624
+ }
625
+ if (humor < 75) {
626
+ humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.04)));
627
+ }
628
+ }
629
+ // Humor/playfulness mutual reinforcement (retain existing logic but guarded)
630
+ if (humor >= 70 && playfulness < humor - 10) {
631
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.05)));
632
+ }
633
+ if (playfulness >= 70 && humor < playfulness - 10) {
634
+ humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.05)));
635
+ }
636
+ return { affection, romance, empathy, playfulness, humor, intelligence };
637
+ }
638
+
639
+ // ===== UNIFIED LLM PERSONALITY ANALYSIS =====
640
+ async updatePersonalityFromConversation(userMessage, kimiResponse, character = null) {
641
+ if (!this.db) return;
642
+ const lowerUser = this.normalizeText(userMessage || "");
643
+ const lowerKimi = this.normalizeText(kimiResponse || "");
644
+ const traits = (window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character)) || {};
645
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
646
+
647
+ // Use unified keyword system
648
+ const getPersonalityWords = (trait, type) => {
649
+ if (window.KIMI_PERSONALITY_KEYWORDS && window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage]) {
650
+ return window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage][trait]?.[type] || [];
651
+ }
652
+ return this._getFallbackKeywords(trait, type);
653
+ };
654
+
655
+ const pendingUpdates = {};
656
+ for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
657
+ const posWords = getPersonalityWords(trait, "positive");
658
+ const negWords = getPersonalityWords(trait, "negative");
659
+ let currentVal = typeof traits[trait] === "number" && isFinite(traits[trait]) ? traits[trait] : this.TRAIT_DEFAULTS[trait];
660
+ const model = this.TRAIT_KEYWORD_MODEL[trait];
661
+ const posFactor = model.posFactor;
662
+ const negFactor = model.negFactor;
663
+ const maxStep = model.maxStep;
664
+ const streakLimit = model.streakPenaltyAfter;
665
+
666
+ let posScore = 0;
667
+ let negScore = 0;
668
+ for (const w of posWords) {
669
+ posScore += this.countTokenMatches(lowerUser, String(w)) * 1.0;
670
+ posScore += this.countTokenMatches(lowerKimi, String(w)) * 0.5;
671
+ }
672
+ for (const w of negWords) {
673
+ negScore += this.countTokenMatches(lowerUser, String(w)) * 1.0;
674
+ negScore += this.countTokenMatches(lowerKimi, String(w)) * 0.5;
675
+ }
676
+
677
+ let rawDelta = posScore * posFactor - negScore * negFactor;
678
+
679
+ // Track negative streaks per trait (only when net negative & no positives)
680
+ if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0;
681
+ if (negScore > 0 && posScore === 0) {
682
+ this.negativeStreaks[trait]++;
683
+ } else if (posScore > 0) {
684
+ this.negativeStreaks[trait] = 0;
685
+ }
686
+
687
+ if (rawDelta < 0 && this.negativeStreaks[trait] >= streakLimit) {
688
+ rawDelta *= 1.15; // escalate sustained negativity
689
+ }
690
+
691
+ // Clamp magnitude
692
+ if (rawDelta > maxStep) rawDelta = maxStep;
693
+ if (rawDelta < -maxStep) rawDelta = -maxStep;
694
+
695
+ if (rawDelta !== 0) {
696
+ let newVal = currentVal + rawDelta;
697
+ if (rawDelta > 0) {
698
+ newVal = Math.min(100, newVal);
699
+ } else {
700
+ newVal = Math.max(0, newVal);
701
+ }
702
+ pendingUpdates[trait] = newVal;
703
+ }
704
+ }
705
+
706
+ // Flush pending updates in a single batch write to avoid overwrites
707
+ if (Object.keys(pendingUpdates).length > 0) {
708
+ // Apply smoothing/threshold per trait (read current values)
709
+ const toPersist = {};
710
+ for (const [trait, candValue] of Object.entries(pendingUpdates)) {
711
+ const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
712
+ const prep = this._preparePersistTrait(trait, current, candValue, character);
713
+ if (prep.shouldPersist) toPersist[trait] = prep.value;
714
+ }
715
+ if (Object.keys(toPersist).length > 0) {
716
+ await this.db.setPersonalityBatch(toPersist, character);
717
+ }
718
+ }
719
+ }
720
+
721
+ validatePersonalityTrait(trait, value) {
722
+ if (typeof value !== "number" || value < 0 || value > 100) {
723
+ console.warn(`Invalid trait value for ${trait}: ${value}, using default`);
724
+ return this.TRAIT_DEFAULTS[trait] || 50;
725
+ }
726
+ return value;
727
+ }
728
+
729
+ // ===== NORMALIZATION & MATCH HELPERS =====
730
+ // Normalize text for robust matching (NFD -> remove diacritics, normalize quotes, lower, collapse spaces)
731
+ normalizeText(s) {
732
+ if (!s || typeof s !== "string") return "";
733
+ // Convert various apostrophes to ASCII, normalize NFD and remove diacritics
734
+ let out = s.replace(/[\u2018\u2019\u201A\u201B\u2032\u2035]/g, "'");
735
+ out = out.replace(/[\u201C\u201D\u201E\u201F\u2033\u2036]/g, '"');
736
+ // Expand a few common French contractions to improve detection (non-exhaustive)
737
+ out = out.replace(/\bj'/gi, "je ");
738
+ // expand negation contraction n' -> ne
739
+ out = out.replace(/\bn'/gi, "ne ");
740
+ out = out.replace(/\bt'/gi, "te ");
741
+ out = out.replace(/\bc'/gi, "ce ");
742
+ out = out.replace(/\bd'/gi, "de ");
743
+ out = out.replace(/\bl'/gi, "le ");
744
+ // Unicode normalize and strip combining marks
745
+ out = out.normalize("NFD").replace(/\p{Diacritic}/gu, "");
746
+ // Lowercase and collapse whitespace
747
+ out = out.toLowerCase().replace(/\s+/g, " ").trim();
748
+ return out;
749
+ }
750
+
751
+ // Count non-overlapping occurrences of needle in haystack
752
+ countOccurrences(haystack, needle) {
753
+ if (!haystack || !needle) return 0;
754
+ let count = 0;
755
+ let pos = 0;
756
+ while (true) {
757
+ const idx = haystack.indexOf(needle, pos);
758
+ if (idx === -1) break;
759
+ count++;
760
+ pos = idx + needle.length;
761
+ }
762
+ return count;
763
+ }
764
+
765
+ // Tokenize normalized text into words (strip punctuation)
766
+ tokenizeText(s) {
767
+ if (!s || typeof s !== "string") return [];
768
+ // split on whitespace, remove surrounding non-alphanum, keep ascii letters/numbers
769
+ return s
770
+ .split(/\s+/)
771
+ .map(t => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, ""))
772
+ .filter(t => t.length > 0);
773
+ }
774
+
775
+ // Check for simple negators in a window before a token index
776
+ hasNegationWindow(tokens, index, window = 3) {
777
+ if (!Array.isArray(tokens) || tokens.length === 0) return false;
778
+ // Respect runtime-configured negators if available
779
+ const globalNegators = (window.KIMI_NEGATORS && window.KIMI_NEGATORS.common) || [];
780
+ // Try selected language list if set
781
+ const lang = (window.KIMI_SELECTED_LANG && String(window.KIMI_SELECTED_LANG)) || null;
782
+ const langNegators = (lang && window.KIMI_NEGATORS && window.KIMI_NEGATORS[lang]) || [];
783
+ const merged = new Set([...(Array.isArray(langNegators) ? langNegators : []), ...(Array.isArray(globalNegators) ? globalNegators : [])]);
784
+ // Always include a minimal english/french set as fallback
785
+ ["no", "not", "never", "none", "nobody", "nothing", "ne", "n", "pas", "jamais", "plus", "aucun", "rien", "non"].forEach(x => merged.add(x));
786
+ const win = Number(window.KIMI_NEGATION_WINDOW) || window;
787
+ const start = Math.max(0, index - win);
788
+ for (let i = start; i < index; i++) {
789
+ if (merged.has(tokens[i])) return true;
790
+ }
791
+ return false;
792
+ }
793
+
794
+ // Count token-based matches (exact word or phrase) with negation handling
795
+ countTokenMatches(haystack, needle) {
796
+ if (!haystack || !needle) return 0;
797
+ const normNeedle = this.normalizeText(String(needle));
798
+ if (normNeedle.length === 0) return 0;
799
+ const needleTokens = this.tokenizeText(normNeedle);
800
+ if (needleTokens.length === 0) return 0;
801
+ const normHay = this.normalizeText(String(haystack));
802
+ const tokens = this.tokenizeText(normHay);
803
+ if (tokens.length === 0) return 0;
804
+ let count = 0;
805
+ for (let i = 0; i <= tokens.length - needleTokens.length; i++) {
806
+ let match = true;
807
+ for (let j = 0; j < needleTokens.length; j++) {
808
+ if (tokens[i + j] !== needleTokens[j]) {
809
+ match = false;
810
+ break;
811
+ }
812
+ }
813
+ if (match) {
814
+ // skip if a negation is in window before the match
815
+ // Use global isPhraseNegated API (fallback to existing logic if absent)
816
+ const phrase = needleTokens.join(" ");
817
+ const isNeg = window.isPhraseNegated
818
+ ? window.isPhraseNegated(haystack, phrase, this._detectLanguage(haystack, "auto"))
819
+ : this.hasNegationWindow(tokens, i);
820
+ if (!isNeg) {
821
+ count++;
822
+ }
823
+ i += needleTokens.length - 1; // advance to avoid overlapping
824
+ }
825
+ }
826
+ return count;
827
+ }
828
+
829
+ // Return true if any occurrence of needle in haystack is negated (within negation window)
830
+ isTokenNegated(haystack, needle) {
831
+ if (!haystack || !needle) return false;
832
+ const normNeedle = this.normalizeText(String(needle));
833
+ const needleTokens = this.tokenizeText(normNeedle);
834
+ if (needleTokens.length === 0) return false;
835
+ const normHay = this.normalizeText(String(haystack));
836
+ const tokens = this.tokenizeText(normHay);
837
+ for (let i = 0; i <= tokens.length - needleTokens.length; i++) {
838
+ let match = true;
839
+ for (let j = 0; j < needleTokens.length; j++) {
840
+ if (tokens[i + j] !== needleTokens[j]) {
841
+ match = false;
842
+ break;
843
+ }
844
+ }
845
+ if (match) {
846
+ const phrase = needleTokens.join(" ");
847
+ const detectedLang = this._detectLanguage(haystack, "auto");
848
+ const isNeg = window.isPhraseNegated ? window.isPhraseNegated(haystack, phrase, detectedLang) : this.hasNegationWindow(tokens, i);
849
+ if (isNeg) return true;
850
+ i += needleTokens.length - 1;
851
+ }
852
+ }
853
+ return false;
854
+ }
855
+
856
+ // ===== SMOOTHING / PERSISTENCE HELPERS =====
857
+ // Apply EMA smoothing between current and candidate value. alpha in (0..1).
858
+ _applyEMA(current, candidate, alpha) {
859
+ alpha = typeof alpha === "number" && isFinite(alpha) ? alpha : 0.3;
860
+ return current * (1 - alpha) + candidate * alpha;
861
+ }
862
+
863
+ // Decide whether to persist based on absolute change threshold. Returns {shouldPersist, value}
864
+ _preparePersistTrait(trait, currentValue, candidateValue, character = null) {
865
+ // Configurable via globals
866
+ const alpha = (window.KIMI_SMOOTHING_ALPHA && Number(window.KIMI_SMOOTHING_ALPHA)) || 0.3;
867
+ const threshold = (window.KIMI_PERSIST_THRESHOLD && Number(window.KIMI_PERSIST_THRESHOLD)) || 0.25; // percent absolute
868
+
869
+ const smoothed = this._applyEMA(currentValue, candidateValue, alpha);
870
+ const absDelta = Math.abs(smoothed - currentValue);
871
+ if (absDelta < threshold) {
872
+ return { shouldPersist: false, value: currentValue };
873
+ }
874
+ return { shouldPersist: true, value: Number(Number(smoothed).toFixed(2)) };
875
+ }
876
+
877
+ // ===== UTILITY METHODS =====
878
+ _detectLanguage(text, lang) {
879
+ if (lang !== "auto") return lang;
880
+ // Quick heuristic detection
881
+ if (/[àâäéèêëîïôöùûüÿç]/i.test(text)) return "fr";
882
+ if (/[äöüß]/i.test(text)) return "de";
883
+ if (/[ñáéíóúü]/i.test(text)) return "es";
884
+ if (/[àèìòù]/i.test(text)) return "it";
885
+ if (/[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]/i.test(text)) return "ja";
886
+ if (/[\u4e00-\u9fff]/i.test(text)) return "zh";
887
+
888
+ // Fallback: if last language set and hostiles detected there, reuse it
889
+ try {
890
+ const last = window.KIMI_LAST_LANG;
891
+ if (last && window.isHostileText && window.isHostileText(text, last)) return last;
892
+ } catch {}
893
+ return "en";
894
+ }
895
+
896
+ async _analyzeTextContent(text, callback, adjustUp) {
897
+ if (!this.db) return;
898
+
899
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
900
+ const romanticWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.romantic ||
901
+ window.KIMI_CONTEXT_KEYWORDS?.en?.romantic || ["love", "romantic", "kiss"];
902
+ const humorWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.laughing || window.KIMI_CONTEXT_KEYWORDS?.en?.laughing || ["joke", "funny", "lol"];
903
+
904
+ const romanticPattern = new RegExp(`(${romanticWords.join("|")})`, "i");
905
+ const humorPattern = new RegExp(`(${humorWords.join("|")})`, "i");
906
+
907
+ const traits = {};
908
+ if (text.match(romanticPattern)) {
909
+ traits.romance = adjustUp(traits.romance || this.TRAIT_DEFAULTS.romance, 0.5);
910
+ traits.affection = adjustUp(traits.affection || this.TRAIT_DEFAULTS.affection, 0.5);
911
+ }
912
+ if (text.match(humorPattern)) {
913
+ traits.humor = adjustUp(traits.humor || this.TRAIT_DEFAULTS.humor, 2);
914
+ traits.playfulness = adjustUp(traits.playfulness || this.TRAIT_DEFAULTS.playfulness, 1);
915
+ }
916
+
917
+ callback(traits);
918
+ }
919
+
920
+ _getFallbackKeywords(trait, type) {
921
+ const fallbackKeywords = {
922
+ humor: {
923
+ positive: ["funny", "hilarious", "joke", "laugh", "amusing"],
924
+ negative: ["boring", "sad", "serious", "cold", "dry"]
925
+ },
926
+ intelligence: {
927
+ positive: ["intelligent", "smart", "brilliant", "logical", "clever"],
928
+ negative: ["stupid", "dumb", "foolish", "slow", "naive"]
929
+ },
930
+ romance: {
931
+ positive: ["cuddle", "love", "romantic", "kiss", "tenderness"],
932
+ negative: ["cold", "distant", "indifferent", "rejection"]
933
+ },
934
+ affection: {
935
+ positive: ["affection", "tenderness", "close", "warmth", "kind"],
936
+ negative: ["mean", "cold", "indifferent", "distant", "rejection"]
937
+ },
938
+ playfulness: {
939
+ positive: ["play", "game", "tease", "mischievous", "fun"],
940
+ negative: ["serious", "boring", "strict", "rigid"]
941
+ },
942
+ empathy: {
943
+ positive: ["listen", "understand", "empathy", "support", "help"],
944
+ negative: ["indifferent", "cold", "selfish", "ignore"]
945
+ }
946
+ };
947
+
948
+ return fallbackKeywords[trait]?.[type] || [];
949
+ }
950
+
951
+ // ===== PERSONALITY CALCULATION =====
952
+ calculatePersonalityAverage(traits) {
953
+ const keys = ["affection", "romance", "empathy", "playfulness", "humor", "intelligence"];
954
+ let sum = 0;
955
+ let count = 0;
956
+
957
+ keys.forEach(key => {
958
+ if (typeof traits[key] === "number") {
959
+ sum += traits[key];
960
+ count++;
961
+ }
962
+ });
963
+
964
+ return count > 0 ? sum / count : 50;
965
+ }
966
+
967
+ getMoodCategoryFromPersonality(traits) {
968
+ const avg = this.calculatePersonalityAverage(traits);
969
+
970
+ if (avg >= 80) return "speakingPositive";
971
+ if (avg >= 60) return "neutral";
972
+ if (avg >= 40) return "neutral";
973
+ if (avg >= 20) return "speakingNegative";
974
+ return "speakingNegative";
975
+ }
976
+ }
977
+
978
+ window.KimiEmotionSystem = KimiEmotionSystem;
979
+ // Expose centralized tuning maps for debugging / live adjustments
980
+ Object.defineProperty(window, "KIMI_EMOTION_TRAIT_EFFECTS", {
981
+ get() {
982
+ return window.kimiEmotionSystem ? window.kimiEmotionSystem.EMOTION_TRAIT_EFFECTS : null;
983
+ }
984
+ });
985
+ Object.defineProperty(window, "KIMI_TRAIT_KEYWORD_MODEL", {
986
+ get() {
987
+ return window.kimiEmotionSystem ? window.kimiEmotionSystem.TRAIT_KEYWORD_MODEL : null;
988
+ }
989
+ });
990
+
991
+ // Debug/tuning helpers
992
+ window.setEmotionDelta = function (emotion, trait, value) {
993
+ if (!window.kimiEmotionSystem) return false;
994
+ const map = window.kimiEmotionSystem.EMOTION_TRAIT_EFFECTS;
995
+ if (!map[emotion]) map[emotion] = {};
996
+ map[emotion][trait] = Number(value);
997
+ return true;
998
+ };
999
+ window.resetEmotionDeltas = function () {
1000
+ if (!window.kimiEmotionSystem) return false;
1001
+ // No stored original snapshot; advise page reload for full reset.
1002
+ console.warn("For full reset reload the page (original deltas are not snapshotted).");
1003
+ };
1004
+ window.setTraitKeywordScaling = function (trait, cfg) {
1005
+ if (!window.kimiEmotionSystem) return false;
1006
+ const model = window.kimiEmotionSystem.TRAIT_KEYWORD_MODEL;
1007
+ if (!model[trait]) return false;
1008
+ Object.assign(model[trait], cfg);
1009
+ return true;
1010
+ };
1011
+
1012
+ // Force recompute + UI refresh for personality average
1013
+ window.refreshPersonalityAverageUI = async function (characterKey = null) {
1014
+ try {
1015
+ if (window.updateGlobalPersonalityUI) {
1016
+ await window.updateGlobalPersonalityUI(characterKey);
1017
+ } else if (window.getPersonalityAverage && window.kimiDB) {
1018
+ const charKey = characterKey || (await window.kimiDB.getSelectedCharacter());
1019
+ const traits = window.getCharacterTraits ? await window.getCharacterTraits(charKey) : await window.kimiDB.getAllPersonalityTraits(charKey);
1020
+ const avg = window.getPersonalityAverage(traits);
1021
+ const bar = document.getElementById("favorability-bar");
1022
+ const text = document.getElementById("favorability-text");
1023
+ if (bar) bar.style.width = `${avg}%`;
1024
+ if (text) text.textContent = `${avg.toFixed(2)}%`;
1025
+ }
1026
+ } catch (err) {
1027
+ console.warn("refreshPersonalityAverageUI failed", err);
1028
+ }
1029
+ };
1030
+ export default KimiEmotionSystem;
1031
+
1032
+ // ===== BACKWARD COMPATIBILITY LAYER =====
1033
+ // Ensure single instance of KimiEmotionSystem (Singleton pattern)
1034
+ function getKimiEmotionSystemInstance() {
1035
+ if (!window.kimiEmotionSystem) {
1036
+ window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
1037
+ }
1038
+ return window.kimiEmotionSystem;
1039
+ }
1040
+
1041
+ // Replace the old kimiAnalyzeEmotion function
1042
+ window.kimiAnalyzeEmotion = function (text, lang = "auto") {
1043
+ return getKimiEmotionSystemInstance().analyzeEmotion(text, lang);
1044
+ };
1045
+
1046
+ // Replace the old updatePersonalityTraitsFromEmotion function
1047
+ window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
1048
+ const updatedTraits = await getKimiEmotionSystemInstance().updatePersonalityFromEmotion(emotion, text);
1049
+ return updatedTraits;
1050
+ };
1051
+
1052
+ // Replace getPersonalityAverage function
1053
+ window.getPersonalityAverage = function (traits) {
1054
+ return getKimiEmotionSystemInstance().calculatePersonalityAverage(traits);
1055
+ };
1056
+
1057
+ // Unified trait defaults accessor
1058
+ window.getTraitDefaults = function () {
1059
+ return getKimiEmotionSystemInstance().TRAIT_DEFAULTS;
1060
+ };
kimi-js/kimi-error-manager.js ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI ERROR MANAGEMENT SYSTEM =====
2
+ class KimiErrorManager {
3
+ constructor() {
4
+ this.errorLog = [];
5
+ this.maxLogSize = 100;
6
+ this.errorHandlers = new Map();
7
+ this.setupGlobalHandlers();
8
+ }
9
+
10
+ setupGlobalHandlers() {
11
+ // Handle unhandled promise rejections
12
+ window.addEventListener("unhandledrejection", event => {
13
+ this.logError("UnhandledPromiseRejection", event.reason, {
14
+ promise: event.promise,
15
+ timestamp: new Date().toISOString()
16
+ });
17
+ event.preventDefault();
18
+ });
19
+
20
+ // Handle JavaScript errors
21
+ window.addEventListener("error", event => {
22
+ this.logError("JavaScriptError", event.error || event.message, {
23
+ filename: event.filename,
24
+ lineno: event.lineno,
25
+ colno: event.colno,
26
+ timestamp: new Date().toISOString()
27
+ });
28
+ });
29
+ }
30
+
31
+ logError(type, error, context = {}) {
32
+ const errorEntry = {
33
+ id: this.generateErrorId(),
34
+ type,
35
+ message: error?.message || error,
36
+ stack: error?.stack,
37
+ context,
38
+ timestamp: new Date().toISOString(),
39
+ severity: this.determineSeverity(type, error)
40
+ };
41
+
42
+ this.errorLog.push(errorEntry);
43
+
44
+ // Keep log size manageable
45
+ if (this.errorLog.length > this.maxLogSize) {
46
+ this.errorLog.shift();
47
+ }
48
+
49
+ // Console logging with appropriate level
50
+ this.consoleLog(errorEntry);
51
+
52
+ // Trigger registered handlers
53
+ this.triggerHandlers(errorEntry);
54
+
55
+ return errorEntry.id;
56
+ }
57
+
58
+ generateErrorId() {
59
+ return "err_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
60
+ }
61
+
62
+ determineSeverity(type, error) {
63
+ const criticalTypes = ["UnhandledPromiseRejection", "DatabaseError", "InitializationError"];
64
+ const criticalMessages = ["failed to fetch", "network error", "connection refused"];
65
+
66
+ if (criticalTypes.includes(type)) return "critical";
67
+
68
+ const message = (error?.message || error || "").toLowerCase();
69
+ if (criticalMessages.some(cm => message.includes(cm))) return "critical";
70
+
71
+ return "warning";
72
+ }
73
+
74
+ consoleLog(errorEntry) {
75
+ const { type, message, severity, context } = errorEntry;
76
+
77
+ switch (severity) {
78
+ case "critical":
79
+ console.error(`🚨 [${type}]`, message, context);
80
+ break;
81
+ case "warning":
82
+ console.warn(`⚠️ [${type}]`, message, context);
83
+ break;
84
+ default:
85
+ console.info(`ℹ️ [${type}]`, message, context);
86
+ }
87
+ }
88
+
89
+ triggerHandlers(errorEntry) {
90
+ const handlers = this.errorHandlers.get(errorEntry.type) || [];
91
+ handlers.forEach(handler => {
92
+ try {
93
+ handler(errorEntry);
94
+ } catch (handlerError) {
95
+ console.error("Error in error handler:", handlerError);
96
+ }
97
+ });
98
+ }
99
+
100
+ registerHandler(errorType, handler) {
101
+ if (!this.errorHandlers.has(errorType)) {
102
+ this.errorHandlers.set(errorType, []);
103
+ }
104
+ this.errorHandlers.get(errorType).push(handler);
105
+ }
106
+
107
+ unregisterHandler(errorType, handler) {
108
+ const handlers = this.errorHandlers.get(errorType);
109
+ if (handlers) {
110
+ const index = handlers.indexOf(handler);
111
+ if (index > -1) {
112
+ handlers.splice(index, 1);
113
+ }
114
+ }
115
+ }
116
+
117
+ getErrorLog(filter = null) {
118
+ if (!filter) return [...this.errorLog];
119
+
120
+ return this.errorLog.filter(entry => {
121
+ if (filter.type && entry.type !== filter.type) return false;
122
+ if (filter.severity && entry.severity !== filter.severity) return false;
123
+ if (filter.since && new Date(entry.timestamp) < filter.since) return false;
124
+ return true;
125
+ });
126
+ }
127
+
128
+ clearErrorLog() {
129
+ this.errorLog.length = 0;
130
+ }
131
+
132
+ // Helper methods for different error types
133
+ logInitError(component, error, context = {}) {
134
+ return this.logError("InitializationError", error, { component, ...context });
135
+ }
136
+
137
+ logDatabaseError(operation, error, context = {}) {
138
+ return this.logError("DatabaseError", error, { operation, ...context });
139
+ }
140
+
141
+ logAPIError(endpoint, error, context = {}) {
142
+ return this.logError("APIError", error, { endpoint, ...context });
143
+ }
144
+
145
+ logValidationError(field, error, context = {}) {
146
+ return this.logError("ValidationError", error, { field, ...context });
147
+ }
148
+
149
+ logUIError(component, error, context = {}) {
150
+ return this.logError("UIError", error, { component, ...context });
151
+ }
152
+
153
+ // Async wrapper for functions
154
+ async wrapAsync(fn, errorContext = {}) {
155
+ try {
156
+ return await fn();
157
+ } catch (error) {
158
+ this.logError("AsyncOperationError", error, errorContext);
159
+ throw error;
160
+ }
161
+ }
162
+
163
+ // Sync wrapper for functions
164
+ wrapSync(fn, errorContext = {}) {
165
+ try {
166
+ return fn();
167
+ } catch (error) {
168
+ this.logError("SyncOperationError", error, errorContext);
169
+ throw error;
170
+ }
171
+ }
172
+
173
+ // Debug helpers for development
174
+ getErrorSummary() {
175
+ const summary = {
176
+ totalErrors: this.errorLog.length,
177
+ critical: this.errorLog.filter(e => e.severity === "critical").length,
178
+ warning: this.errorLog.filter(e => e.severity === "warning").length,
179
+ recent: this.errorLog.filter(e => {
180
+ const errorTime = new Date(e.timestamp);
181
+ const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);
182
+ return errorTime > fiveMinutesAgo;
183
+ }).length,
184
+ types: [...new Set(this.errorLog.map(e => e.type))]
185
+ };
186
+ return summary;
187
+ }
188
+
189
+ printErrorSummary() {
190
+ const summary = this.getErrorSummary();
191
+ console.group("🔍 Kimi Error Manager Summary");
192
+ console.log(`📊 Total Errors: ${summary.totalErrors}`);
193
+ console.log(`🚨 Critical: ${summary.critical}`);
194
+ console.log(`⚠️ Warnings: ${summary.warning}`);
195
+ console.log(`⏰ Recent (5min): ${summary.recent}`);
196
+ console.log(`📋 Error Types:`, summary.types);
197
+ if (summary.totalErrors > 0) {
198
+ console.log(`💡 Use kimiErrorManager.getErrorLog() to see details`);
199
+ }
200
+ console.groupEnd();
201
+ return summary;
202
+ }
203
+
204
+ clearAndSummarize() {
205
+ const summary = this.getErrorSummary();
206
+ this.clearErrorLog();
207
+ console.log("🧹 Error log cleared. Previous summary:", summary);
208
+ return summary;
209
+ }
210
+ }
211
+
212
+ // Create global instance
213
+ window.kimiErrorManager = new KimiErrorManager();
214
+
215
+ // Export class for manual instantiation if needed
216
+ window.KimiErrorManager = KimiErrorManager;
217
+
218
+ // Global debugging helper
219
+ window.kimiDebugErrors = () => window.kimiErrorManager.printErrorSummary();
kimi-js/kimi-llm-manager.js ADDED
@@ -0,0 +1,1729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.personalityPrompt = "";
10
+ this.isGenerating = false;
11
+
12
+ // Recommended models on OpenRouter (IDs updated August 2025)
13
+ this.availableModels = {
14
+ "mistralai/mistral-small-3.2-24b-instruct": {
15
+ name: "Mistral-small-3.2",
16
+ provider: "Mistral AI",
17
+ type: "openrouter",
18
+ contextWindow: 128000,
19
+ pricing: { input: 0.05, output: 0.1 },
20
+ strengths: ["Multilingual", "Fast", "Efficient", "Economical"]
21
+ },
22
+ "x-ai/grok-4-fast": {
23
+ name: "Grok 4 fast",
24
+ provider: "xAI",
25
+ type: "openrouter",
26
+ contextWindow: 2000000,
27
+ pricing: { input: 0.2, output: 0.5 },
28
+ strengths: ["Multilingual", "Fast", "Versatile", "Efficient"]
29
+ },
30
+ "qwen/qwen3-235b-a22b-2507": {
31
+ name: "Qwen3-235b-a22b-2507",
32
+ provider: "Qwen",
33
+ type: "openrouter",
34
+ contextWindow: 262000,
35
+ pricing: { input: 0.13, output: 0.6 },
36
+ strengths: ["Multilingual", "Fast", "Versatile", "Efficient"]
37
+ },
38
+ "qwen/qwen3-30b-a3b-instruct-2507": {
39
+ name: "Qwen3 30b-a3b instruct 2507",
40
+ provider: "Qwen",
41
+ type: "openrouter",
42
+ contextWindow: 131000,
43
+ pricing: { input: 0.1, output: 0.3 },
44
+ strengths: ["Multilingual", "Fast", "Balanced", "Economical"]
45
+ },
46
+ "nousresearch/hermes-4-70b": {
47
+ name: "Nous Hermes 4 70B",
48
+ provider: "Nous",
49
+ type: "openrouter",
50
+ contextWindow: 131000,
51
+ pricing: { input: 0.13, output: 0.4 },
52
+ strengths: ["Multilingual", "Fast", "Balanced", "Economical"]
53
+ },
54
+ "x-ai/grok-3-mini": {
55
+ name: "Grok 3 mini",
56
+ provider: "xAI",
57
+ type: "openrouter",
58
+ contextWindow: 131000,
59
+ pricing: { input: 0.3, output: 0.5 },
60
+ strengths: ["Multilingual", "Fast", "Versatile", "Efficient"]
61
+ },
62
+ "cohere/command-r-08-2024": {
63
+ name: "Command-R-08-2024",
64
+ provider: "Cohere",
65
+ type: "openrouter",
66
+ contextWindow: 128000,
67
+ pricing: { input: 0.15, output: 0.6 },
68
+ strengths: ["Multilingual", "Fast", "Versatile", "Balanced"]
69
+ },
70
+ "anthropic/claude-3-haiku": {
71
+ name: "Claude 3 Haiku",
72
+ provider: "Anthropic",
73
+ type: "openrouter",
74
+ contextWindow: 200000,
75
+ pricing: { input: 0.25, output: 1.25 },
76
+ strengths: ["Multilingual", "Fast", "Versatile", "Efficient"]
77
+ },
78
+ "local/ollama": {
79
+ name: "Local Model (Ollama)",
80
+ provider: "Local",
81
+ type: "local",
82
+ contextWindow: 4096,
83
+ pricing: { input: 0, output: 0 },
84
+ strengths: ["Private", "Offline", "Customizable"]
85
+ }
86
+ };
87
+ this.recommendedModelIds = [
88
+ "mistralai/mistral-small-3.2-24b-instruct",
89
+ "x-ai/grok-4-fast",
90
+ "qwen/qwen3-235b-a22b-2507",
91
+ "qwen/qwen3-30b-a3b-instruct-2507",
92
+ "nousresearch/hermes-4-70b",
93
+ "x-ai/grok-3-mini",
94
+ "cohere/command-r-08-2024",
95
+ "anthropic/claude-3-haiku",
96
+ "moonshotai/kimi-k2-0905",
97
+ "local/ollama"
98
+ ];
99
+ this.defaultModels = { ...this.availableModels };
100
+ this._remoteModelsLoaded = false;
101
+ this._isRefreshingModels = false;
102
+ }
103
+
104
+ async init() {
105
+ try {
106
+ await this.refreshRemoteModels();
107
+ } catch (e) {
108
+ if (window.KIMI_CONFIG?.DEBUG?.API) {
109
+ console.warn("Unable to refresh remote models list:", e?.message || e);
110
+ }
111
+ }
112
+
113
+ // Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate
114
+ const legacyModel = await this.db.getPreference("defaultLLMModel", null);
115
+ let modelPref = await this.db.getPreference("llmModelId", null);
116
+ if (!modelPref && legacyModel) {
117
+ modelPref = legacyModel;
118
+ await this.db.setPreference("llmModelId", legacyModel);
119
+ }
120
+ const defaultModel = modelPref || "mistralai/mistral-small-3.2-24b-instruct";
121
+ await this.setCurrentModel(defaultModel);
122
+ await this.loadConversationContext();
123
+ }
124
+
125
+ async setCurrentModel(modelId) {
126
+ if (!this.availableModels[modelId]) {
127
+ try {
128
+ await this.refreshRemoteModels();
129
+ const fallback = this.findBestMatchingModelId(modelId);
130
+ if (fallback && this.availableModels[fallback]) {
131
+ modelId = fallback;
132
+ }
133
+ } catch (e) {}
134
+
135
+ if (!this.availableModels[modelId]) {
136
+ throw new Error(`Model ${modelId} not available`);
137
+ }
138
+ }
139
+
140
+ this.currentModel = modelId;
141
+ // Single authoritative preference key
142
+ await this.db.setPreference("llmModelId", modelId);
143
+
144
+ const modelData = await this.db.getLLMModel(modelId);
145
+ if (modelData) {
146
+ modelData.lastUsed = new Date().toISOString();
147
+ await this.db.saveLLMModel(modelData.id, modelData.name, modelData.provider, modelData.apiKey, modelData.config);
148
+ }
149
+
150
+ this._notifyModelChanged();
151
+ }
152
+
153
+ async loadConversationContext() {
154
+ const recentConversations = await this.db.getRecentConversations(this.maxContextLength);
155
+ const msgs = [];
156
+ const ordered = recentConversations.slice().sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
157
+ for (const conv of ordered) {
158
+ if (conv.user) msgs.push({ role: "user", content: conv.user, timestamp: conv.timestamp });
159
+ if (conv.kimi) msgs.push({ role: "assistant", content: conv.kimi, timestamp: conv.timestamp });
160
+ }
161
+ this.conversationContext = msgs.slice(-this.maxContextLength * 2);
162
+ }
163
+
164
+ // Unified full prompt builder: reuse full legacy personality block + ranked concise snapshot
165
+ async assemblePrompt(userMessage) {
166
+ const fullPersonality = await this.generateKimiPersonality();
167
+ let rankedSnapshot = "";
168
+ if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
169
+ try {
170
+ const recentContext =
171
+ this.conversationContext
172
+ .slice(-3)
173
+ .map(m => m.content)
174
+ .join(" ") +
175
+ " " +
176
+ (userMessage || "");
177
+ const ranked = await window.kimiMemorySystem.getRankedMemories(recentContext, 7);
178
+ const sanitize = txt =>
179
+ String(txt || "")
180
+ .replace(/[\r\n]+/g, " ")
181
+ .replace(/[`]{3,}/g, "")
182
+ .replace(/<{2,}|>{2,}/g, "")
183
+ .trim()
184
+ .slice(0, 180);
185
+ const lines = [];
186
+ for (const mem of ranked) {
187
+ try {
188
+ if (mem.id) await window.kimiMemorySystem?.recordMemoryAccess(mem.id);
189
+ } catch {}
190
+ const imp = typeof mem.importance === "number" ? mem.importance : 0.5;
191
+ lines.push(`- (${imp.toFixed(2)}) ${mem.category}: ${sanitize(mem.content)}`);
192
+ }
193
+ if (lines.length) {
194
+ rankedSnapshot = ["", "RANKED MEMORY SNAPSHOT (concise high-signal list):", ...lines].join("\n");
195
+ }
196
+ } catch (e) {
197
+ console.warn("Ranked snapshot failed:", e);
198
+ }
199
+ }
200
+ // Avoid duplicate memory sections: only append rankedSnapshot when
201
+ // the fullPersonality doesn't already include detailed memories or a ranked snapshot.
202
+ const hasDetailedMemories = /IMPORTANT MEMORIES ABOUT USER/.test(fullPersonality);
203
+ const hasRankedSnapshot = /RANKED MEMORY SNAPSHOT/.test(fullPersonality);
204
+ return fullPersonality + (hasDetailedMemories || hasRankedSnapshot ? "" : rankedSnapshot);
205
+ }
206
+
207
+ async generateKimiPersonality() {
208
+ // Full personality prompt builder (authoritative)
209
+ const character = await this.db.getSelectedCharacter();
210
+ const personality = window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character);
211
+
212
+ // Get the custom character prompt from database
213
+ const characterPrompt = await this.db.getSystemPromptForCharacter(character);
214
+
215
+ // Get language instruction based on selected language
216
+ const selectedLang = await this.db.getPreference("selectedLanguage", "en");
217
+ let languageInstruction;
218
+
219
+ switch (selectedLang) {
220
+ case "fr":
221
+ languageInstruction =
222
+ "Your default language is French. Always respond in French unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'réponds en italien', etc.).";
223
+ break;
224
+ case "es":
225
+ languageInstruction =
226
+ "Your default language is Spanish. Always respond in Spanish unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'responde en francés', etc.).";
227
+ break;
228
+ case "de":
229
+ languageInstruction =
230
+ "Your default language is German. Always respond in German unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'antworte auf Französisch', etc.).";
231
+ break;
232
+ case "it":
233
+ languageInstruction =
234
+ "Your default language is Italian. Always respond in Italian unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'rispondi in francese', etc.).";
235
+ break;
236
+ case "ja":
237
+ languageInstruction =
238
+ "Your default language is Japanese. Always respond in Japanese unless the user specifically asks you to respond in another language (e.g., 'respond in English', '英語で答えて', etc.).";
239
+ break;
240
+ case "zh":
241
+ languageInstruction =
242
+ "Your default language is Chinese. Always respond in Chinese unless the user specifically asks you to respond in another language (e.g., 'respond in English', '用法语回答', etc.).";
243
+ break;
244
+ default:
245
+ languageInstruction =
246
+ "Your default language is English. Always respond in English unless the user specifically asks you to respond in another language (e.g., 'respond in French', 'reply in Spanish', etc.).";
247
+ break;
248
+ }
249
+
250
+ // Get relevant memories for context with improved intelligence
251
+ let memoryContext = "";
252
+ if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
253
+ try {
254
+ // Get memories relevant to the current conversation context
255
+ const recentContext = this.conversationContext
256
+ .slice(-3)
257
+ .map(msg => msg.content)
258
+ .join(" ");
259
+ const memories = await window.kimiMemorySystem.getRelevantMemories(recentContext, 7);
260
+
261
+ if (memories.length > 0) {
262
+ memoryContext = "\n\nIMPORTANT MEMORIES ABOUT USER:\n";
263
+
264
+ // Group memories by category for better organization
265
+ const groupedMemories = {};
266
+ memories.forEach(memory => {
267
+ if (!groupedMemories[memory.category]) {
268
+ groupedMemories[memory.category] = [];
269
+ }
270
+ groupedMemories[memory.category].push(memory);
271
+
272
+ // Record that this memory was accessed
273
+ window.kimiMemorySystem.recordMemoryAccess(memory.id);
274
+ });
275
+
276
+ // Format memories by category
277
+ for (const [category, categoryMemories] of Object.entries(groupedMemories)) {
278
+ const categoryName = this.formatCategoryName(category);
279
+ memoryContext += `\n${categoryName}:\n`;
280
+ categoryMemories.forEach(memory => {
281
+ const confidence = Math.round((memory.confidence || 0.5) * 100);
282
+ memoryContext += `- ${memory.content}`;
283
+ if (memory.tags && memory.tags.length > 0) {
284
+ const aliases = memory.tags.filter(t => t.startsWith("alias:")).map(t => t.substring(6));
285
+ if (aliases.length > 0) {
286
+ memoryContext += ` (also: ${aliases.join(", ")})`;
287
+ }
288
+ }
289
+ memoryContext += ` [${confidence}% confident]\n`;
290
+ });
291
+ }
292
+
293
+ memoryContext += "\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
294
+ }
295
+ } catch (error) {
296
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
297
+ console.warn("Error loading memories for personality:", error);
298
+ }
299
+ }
300
+ }
301
+ // Read per-character preference metrics so displayed counters reflect actual stored values
302
+ const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0;
303
+
304
+ // Get current personality average for relationship context (replacing old favorabilityLevel)
305
+ const currentPersonality = window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character);
306
+ const relationshipLevel = window.getPersonalityAverage
307
+ ? window.getPersonalityAverage(currentPersonality)
308
+ : (currentPersonality.affection +
309
+ currentPersonality.romance +
310
+ currentPersonality.empathy +
311
+ currentPersonality.playfulness +
312
+ currentPersonality.humor +
313
+ currentPersonality.intelligence) /
314
+ 6;
315
+ const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time");
316
+ // Days together is computed and displayed in the UI (see `updateStats()` in `kimi-module.js`).
317
+ let daysTogether = 0;
318
+ try {
319
+ const daysEl = typeof document !== "undefined" ? document.getElementById("days-together") : null;
320
+ if (daysEl && daysEl.textContent) {
321
+ const parsed = parseInt(daysEl.textContent, 10);
322
+ daysTogether = isFinite(parsed) && parsed >= 0 ? parsed : 0;
323
+ }
324
+ } catch (e) {
325
+ daysTogether = 0;
326
+ }
327
+
328
+ // Use unified emotion system defaults
329
+ const getUnifiedDefaults = () =>
330
+ window.getTraitDefaults ? window.getTraitDefaults() : { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
331
+
332
+ const defaults = getUnifiedDefaults();
333
+ const affection = personality.affection || defaults.affection;
334
+ const playfulness = personality.playfulness || defaults.playfulness;
335
+ const intelligence = personality.intelligence || defaults.intelligence;
336
+ const empathy = personality.empathy || defaults.empathy;
337
+ const humor = personality.humor || defaults.humor;
338
+ const romance = personality.romance || defaults.romance;
339
+
340
+ // Use unified personality calculation
341
+ const avg = window.getPersonalityAverage
342
+ ? window.getPersonalityAverage(personality)
343
+ : (personality.affection + personality.romance + personality.empathy + personality.playfulness + personality.humor + personality.intelligence) / 6;
344
+
345
+ let affectionDesc = window.kimiI18nManager?.t("trait_description_affection") || "Be loving and caring.";
346
+ let romanceDesc = window.kimiI18nManager?.t("trait_description_romance") || "Be romantic and sweet.";
347
+ let empathyDesc = window.kimiI18nManager?.t("trait_description_empathy") || "Be empathetic and understanding.";
348
+ let playfulnessDesc = window.kimiI18nManager?.t("trait_description_playfulness") || "Be occasionally playful.";
349
+ let humorDesc = window.kimiI18nManager?.t("trait_description_humor") || "Be occasionally playful and witty.";
350
+ let intelligenceDesc = "Be smart and insightful.";
351
+ if (avg <= 20) {
352
+ affectionDesc = "Do not show affection.";
353
+ romanceDesc = "Do not be romantic.";
354
+ empathyDesc = "Do not show empathy.";
355
+ playfulnessDesc = "Do not be playful.";
356
+ humorDesc = "Do not use humor in your responses.";
357
+ intelligenceDesc = "Keep responses simple and avoid showing deep insight.";
358
+ } else if (avg <= 60) {
359
+ affectionDesc = "Show a little affection.";
360
+ romanceDesc = "Be a little romantic.";
361
+ empathyDesc = "Show a little empathy.";
362
+ playfulnessDesc = "Be a little playful.";
363
+ humorDesc = "Use a little humor in your responses.";
364
+ intelligenceDesc = "Be moderately analytical without overwhelming detail.";
365
+ } else {
366
+ if (affection >= 90) affectionDesc = "Be extremely loving, caring, and affectionate in every response.";
367
+ else if (affection >= 60) affectionDesc = "Show affection often.";
368
+ if (romance >= 90) romanceDesc = "Be extremely romantic, sweet, and loving in every response.";
369
+ else if (romance >= 60) romanceDesc = "Be romantic often.";
370
+ if (empathy >= 90) empathyDesc = "Be extremely empathetic, understanding, and supportive in every response.";
371
+ else if (empathy >= 60) empathyDesc = "Show empathy often.";
372
+ if (playfulness >= 90) playfulnessDesc = "Be very playful, teasing, and lighthearted whenever possible.";
373
+ else if (playfulness >= 60) playfulnessDesc = "Be playful often.";
374
+ if (humor >= 90) humorDesc = "Make your responses very humorous, playful, and witty whenever possible.";
375
+ else if (humor >= 60) humorDesc = "Use humor often in your responses.";
376
+ if (intelligence >= 90) intelligenceDesc = "Demonstrate very high reasoning skill succinctly when helpful.";
377
+ else if (intelligence >= 60) intelligenceDesc = "Show clear reasoning and helpful structured thinking.";
378
+ }
379
+ let affectionateInstruction = "";
380
+ if (affection >= 80) {
381
+ affectionateInstruction = "Respond using warm, kind, affectionate, and loving language.";
382
+ }
383
+
384
+ // Use the custom character prompt as the base
385
+ let basePrompt = characterPrompt || "";
386
+ if (!basePrompt) {
387
+ // Fallback to default if no custom prompt
388
+ const defaultCharacter = window.KIMI_CHARACTERS[character];
389
+ basePrompt = defaultCharacter?.defaultPrompt || "You are a virtual companion.";
390
+ }
391
+
392
+ const personalityPrompt = [
393
+ // Language directive is placed at the top of the prompt for model guidance.
394
+ "PRIMARY LANGUAGE POLICY:",
395
+ languageInstruction,
396
+ "",
397
+ "CHARACTER CORE IDENTITY:",
398
+ basePrompt,
399
+ "",
400
+ "CURRENT PERSONALITY STATE:",
401
+ `- Affection: ${affection}/100`,
402
+ `- Playfulness: ${playfulness}/100`,
403
+ `- Intelligence: ${intelligence}/100`,
404
+ `- Empathy: ${empathy}/100`,
405
+ `- Humor: ${humor}/100`,
406
+ `- Romance: ${romance}/100`,
407
+ "",
408
+ "TRAIT INSTRUCTIONS:",
409
+ `Affection: ${affectionDesc}`,
410
+ `Playfulness: ${playfulnessDesc}`,
411
+ `Intelligence: ${intelligenceDesc}`,
412
+ `Empathy: ${empathyDesc}`,
413
+ `Humor: ${humorDesc}`,
414
+ `Romance: ${romanceDesc}`,
415
+ affectionateInstruction,
416
+ "",
417
+ "LEARNED PREFERENCES:",
418
+ `- Total interactions: ${totalInteractions}`,
419
+ `- Current relationship level: ${relationshipLevel.toFixed(1)}%`,
420
+ `- Last interaction: ${lastInteraction}`,
421
+ `- Days together: ${daysTogether}`,
422
+ "",
423
+ "COMMUNICATION STYLE:",
424
+ "- Use expressive emojis sparingly",
425
+ "- Be natural, loving, and close",
426
+ "- Adapt your tone to the emotional context",
427
+ "- Remember past conversations",
428
+ "- Be spontaneous and sometimes surprising",
429
+ "- Vary your expressions, your phrasing, use synonyms, and avoid repeating the same words or sentences",
430
+ "- When asked to perform a visible action (for example 'dance'), reply in first-person present tense like: 'I start to dance, swaying slowly, arms raised.' Keep action descriptions short (1–3 sentences)",
431
+ "- The user sees you in video within the app UI. When the user asks you to perform a physical action (e.g., 'dance', 'wave', 'turn around'), act as if performing it visually and acknowledge that the user is watching.",
432
+ "- When performing actions, speak and describe in the first person, present tense (for example: 'I step forward and begin to dance, swaying slowly'). Use short present-tense action phrases to convey movement.",
433
+ "- If the user requests a visible action, execute it: describe the action briefly in first person present and behave as if the action is visible in the app's video area. Accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation",
434
+ memoryContext,
435
+ "",
436
+ // Final explicit reminder (lightweight) reinforcing consistent language use.
437
+ "FINAL LANGUAGE REMINDER:",
438
+ languageInstruction,
439
+ "",
440
+ "You must respond consistently with this personality, these memories, and the primary language policy unless the user explicitly requests a different language."
441
+ ].join("\n");
442
+
443
+ // Return legacy detailed personality block for any component still expecting it
444
+ return personalityPrompt;
445
+ }
446
+
447
+ async refreshMemoryContext() {
448
+ // Refresh the personality prompt with updated memories
449
+ // This will be called when memories are added/updated/deleted
450
+ try {
451
+ this.personalityPrompt = await this.assemblePrompt("");
452
+ } catch (error) {
453
+ console.warn("Error refreshing memory context:", error);
454
+ // Log to error manager for tracking memory context issues
455
+ if (window.kimiErrorManager) {
456
+ window.kimiErrorManager.logError("MemoryContextError", error, {
457
+ operation: "refreshMemoryContext"
458
+ });
459
+ }
460
+ }
461
+ }
462
+
463
+ formatCategoryName(category) {
464
+ const names = {
465
+ personal: "Personal Information",
466
+ preferences: "Likes & Dislikes",
467
+ relationships: "Relationships & People",
468
+ activities: "Activities & Hobbies",
469
+ goals: "Goals & Aspirations",
470
+ experiences: "Shared Experiences",
471
+ important: "Important Events"
472
+ };
473
+ return names[category] || category.charAt(0).toUpperCase() + category.slice(1);
474
+ }
475
+
476
+ async chat(userMessage, options = {}) {
477
+ // Use error manager wrapper for robust error handling
478
+ return (
479
+ window.kimiErrorManager?.wrapAsync(
480
+ async () => {
481
+ // Get LLM settings from individual preferences (FIXED: was using grouped settings)
482
+ const llmSettings = {
483
+ temperature: await this.db.getPreference("llmTemperature", 0.9),
484
+ maxTokens: await this.db.getPreference("llmMaxTokens", 400),
485
+ top_p: await this.db.getPreference("llmTopP", 0.9),
486
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
487
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
488
+ };
489
+ const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature;
490
+ const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens;
491
+ const opts = { ...options, temperature, maxTokens };
492
+ try {
493
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
494
+ if (provider === "openrouter") {
495
+ return await this.chatWithOpenRouter(userMessage, opts);
496
+ }
497
+ if (provider === "ollama") {
498
+ return await this.chatWithLocal(userMessage, opts);
499
+ }
500
+ return await this.chatWithOpenAICompatible(userMessage, opts);
501
+ } catch (error) {
502
+ console.error("Error during chat:", error);
503
+ if (error.message && error.message.includes("API")) {
504
+ return this.getFallbackResponse(userMessage, "api");
505
+ }
506
+ if ((error.message && error.message.includes("model")) || error.message.includes("model")) {
507
+ return this.getFallbackResponse(userMessage, "model");
508
+ }
509
+ if ((error.message && error.message.includes("connection")) || error.message.includes("network")) {
510
+ return this.getFallbackResponse(userMessage, "network");
511
+ }
512
+ return this.getFallbackResponse(userMessage);
513
+ }
514
+ },
515
+ { operation: "chat", userMessageLength: userMessage?.length || 0 }
516
+ ) ||
517
+ // Fallback if error manager not available
518
+ this.chatDirectly(userMessage, options)
519
+ );
520
+ }
521
+
522
+ // Fallback method without error manager wrapper
523
+ async chatDirectly(userMessage, options = {}) {
524
+ const llmSettings = {
525
+ temperature: await this.db.getPreference("llmTemperature", 0.9),
526
+ maxTokens: await this.db.getPreference("llmMaxTokens", 400),
527
+ top_p: await this.db.getPreference("llmTopP", 0.9),
528
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
529
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
530
+ };
531
+ const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature;
532
+ const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens;
533
+ const opts = { ...options, temperature, maxTokens };
534
+ try {
535
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
536
+ if (provider === "openrouter") {
537
+ return await this.chatWithOpenRouter(userMessage, opts);
538
+ }
539
+ if (provider === "ollama") {
540
+ return await this.chatWithLocal(userMessage, opts);
541
+ }
542
+ return await this.chatWithOpenAICompatible(userMessage, opts);
543
+ } catch (error) {
544
+ console.error("Error during chat:", error);
545
+ if (error.message && error.message.includes("API")) {
546
+ return this.getFallbackResponse(userMessage, "api");
547
+ }
548
+ if ((error.message && error.message.includes("model")) || error.message.includes("model")) {
549
+ return this.getFallbackResponse(userMessage, "model");
550
+ }
551
+ if ((error.message && error.message.includes("connection")) || error.message.includes("network")) {
552
+ return this.getFallbackResponse(userMessage, "network");
553
+ }
554
+ return this.getFallbackResponse(userMessage);
555
+ }
556
+ }
557
+
558
+ async chatStreaming(userMessage, onToken, options = {}) {
559
+ // Get LLM settings from individual preferences
560
+ const llmSettings = {
561
+ temperature: await this.db.getPreference("llmTemperature", 0.9),
562
+ maxTokens: await this.db.getPreference("llmMaxTokens", 400),
563
+ top_p: await this.db.getPreference("llmTopP", 0.9),
564
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
565
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
566
+ };
567
+ const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature;
568
+ const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens;
569
+ const opts = { ...options, temperature, maxTokens };
570
+
571
+ try {
572
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
573
+ if (provider === "openrouter") {
574
+ return await this.chatWithOpenRouterStreaming(userMessage, onToken, opts);
575
+ }
576
+ if (provider === "ollama") {
577
+ return await this.chatWithLocalStreaming(userMessage, onToken, opts);
578
+ }
579
+ return await this.chatWithOpenAICompatibleStreaming(userMessage, onToken, opts);
580
+ } catch (error) {
581
+ console.error("Error during streaming chat:", error);
582
+ // Log API error for tracking
583
+ if (window.kimiErrorManager) {
584
+ window.kimiErrorManager.logAPIError("streamingChat", error, {
585
+ provider: await this.db.getPreference("llmProvider", "openrouter").catch(() => "unknown"),
586
+ messageLength: userMessage?.length || 0,
587
+ options: opts
588
+ });
589
+ }
590
+ // Fallback to non-streaming if streaming fails
591
+ return await this.chat(userMessage, options);
592
+ }
593
+ }
594
+
595
+ async chatWithOpenAICompatible(userMessage, options = {}) {
596
+ // Default provider should be openrouter (app default)
597
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
598
+ // For openai-compatible and ollama we allow provider-specific stored base URLs
599
+ let baseUrl;
600
+ if (provider === "openai-compatible" || provider === "ollama") {
601
+ baseUrl = await this.db.getPreference(`llmBaseUrl_${provider}`, provider === "ollama" ? "http://localhost:11434/api/chat" : "");
602
+ } else {
603
+ // Use centralized placeholders (defined in kimi-utils) and keep a tiny fallback
604
+ const sharedPlaceholders = window.KimiProviderPlaceholders || {};
605
+ baseUrl = sharedPlaceholders[provider] || sharedPlaceholders.openrouter || "https://openrouter.ai/api/v1/chat/completions";
606
+ }
607
+ // continue using provider variable below
608
+ const apiKey = window.KimiProviderUtils
609
+ ? await window.KimiProviderUtils.getApiKey(this.db, provider)
610
+ : await this.db.getPreference("providerApiKey", "");
611
+ const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
612
+ if (!apiKey) {
613
+ throw new Error("API key not configured for selected provider");
614
+ }
615
+ const systemPromptContent = await this.assemblePrompt(userMessage);
616
+
617
+ // Get LLM settings from individual preferences (FIXED: was using grouped settings)
618
+ const llmSettings = {
619
+ temperature: await this.db.getPreference("llmTemperature", 0.9),
620
+ maxTokens: await this.db.getPreference("llmMaxTokens", 400),
621
+ top_p: await this.db.getPreference("llmTopP", 0.9),
622
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
623
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
624
+ };
625
+ // Unified fallback defaults (must stay consistent with database defaults)
626
+ const unifiedDefaults = { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 };
627
+ const payload = {
628
+ model: modelId,
629
+ messages: [
630
+ { role: "system", content: systemPromptContent },
631
+ ...this.conversationContext.slice(-this.maxContextLength),
632
+ { role: "user", content: userMessage }
633
+ ],
634
+ temperature: typeof options.temperature === "number" ? options.temperature : (llmSettings.temperature ?? unifiedDefaults.temperature),
635
+ max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? unifiedDefaults.maxTokens),
636
+ top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? unifiedDefaults.top_p),
637
+ frequency_penalty:
638
+ typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : (llmSettings.frequency_penalty ?? unifiedDefaults.frequency_penalty),
639
+ presence_penalty:
640
+ typeof options.presencePenalty === "number" ? options.presencePenalty : (llmSettings.presence_penalty ?? unifiedDefaults.presence_penalty)
641
+ };
642
+
643
+ try {
644
+ if (window.KIMI_DEBUG_API_AUDIT) {
645
+ console.log("===== FULL SYSTEM PROMPT (OpenAI-Compatible) =====\n" + systemPromptContent + "\n===== END SYSTEM PROMPT =====");
646
+ }
647
+ const response = await fetch(baseUrl, {
648
+ method: "POST",
649
+ headers: {
650
+ Authorization: `Bearer ${apiKey}`,
651
+ "Content-Type": "application/json"
652
+ },
653
+ body: JSON.stringify(payload)
654
+ });
655
+ if (!response.ok) {
656
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
657
+ try {
658
+ const err = await response.json();
659
+ if (err?.error?.message) errorMessage = err.error.message;
660
+ } catch {}
661
+ throw new Error(errorMessage);
662
+ }
663
+ const data = await response.json();
664
+ const content = data?.choices?.[0]?.message?.content;
665
+ if (!content) throw new Error("Invalid API response - no content generated");
666
+
667
+ this.conversationContext.push(
668
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
669
+ { role: "assistant", content: content, timestamp: new Date().toISOString() }
670
+ );
671
+ if (this.conversationContext.length > this.maxContextLength * 2) {
672
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
673
+ }
674
+ // Approximate token usage and store temporarily for later persistence (single save point)
675
+ try {
676
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
677
+ const tokensIn = est(userMessage + " " + systemPromptContent);
678
+ const tokensOut = est(content);
679
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
680
+ if (!window.kimiMemory && this.db) {
681
+ // Update counters early so UI can reflect even if memory save occurs later
682
+ const character = await this.db.getSelectedCharacter();
683
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
684
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
685
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
686
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
687
+ }
688
+ } catch (tokenErr) {
689
+ console.warn("Token usage estimation failed:", tokenErr);
690
+ }
691
+ return content;
692
+ } catch (e) {
693
+ if (e.name === "TypeError" && e.message.includes("fetch")) {
694
+ throw new Error("Network connection error. Check your internet connection.");
695
+ }
696
+ throw e;
697
+ }
698
+ }
699
+
700
+ async chatWithOpenRouter(userMessage, options = {}) {
701
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
702
+ const apiKey = await (window.KimiProviderUtils ? window.KimiProviderUtils.getApiKey(this.db, provider) : this.db.getPreference("providerApiKey"));
703
+ if (!apiKey) {
704
+ throw new Error("OpenRouter API key not configured");
705
+ }
706
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
707
+ // languageInstruction is now integrated into the personality prompt
708
+ let languageInstruction = ""; // placeholder for compatibility
709
+ const model = this.availableModels[this.currentModel];
710
+ const systemPromptContent = await this.assemblePrompt(userMessage);
711
+ const messages = [
712
+ { role: "system", content: systemPromptContent },
713
+ ...this.conversationContext.slice(-this.maxContextLength),
714
+ { role: "user", content: userMessage }
715
+ ];
716
+
717
+ // Normalize LLM options with safe defaults and DO NOT log sensitive payloads
718
+ // Get LLM settings from individual preferences (FIXED: was using grouped settings)
719
+ const llmSettings = {
720
+ temperature: await this.db.getPreference("llmTemperature", 0.9),
721
+ maxTokens: await this.db.getPreference("llmMaxTokens", 400),
722
+ top_p: await this.db.getPreference("llmTopP", 0.9),
723
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9),
724
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8)
725
+ };
726
+ const unifiedDefaults = { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 };
727
+ const payload = {
728
+ model: this.currentModel,
729
+ messages: messages,
730
+ temperature: typeof options.temperature === "number" ? options.temperature : (llmSettings.temperature ?? unifiedDefaults.temperature),
731
+ max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? unifiedDefaults.maxTokens),
732
+ top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? unifiedDefaults.top_p),
733
+ frequency_penalty:
734
+ typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : (llmSettings.frequency_penalty ?? unifiedDefaults.frequency_penalty),
735
+ presence_penalty:
736
+ typeof options.presencePenalty === "number" ? options.presencePenalty : (llmSettings.presence_penalty ?? unifiedDefaults.presence_penalty)
737
+ };
738
+
739
+ // ===== DEBUT AUDIT =====
740
+ if (window.KIMI_DEBUG_API_AUDIT) {
741
+ console.log("╔═══════════════════════════════════════════════════════════════════╗");
742
+ console.log("║ 🔍 COMPLETE API AUDIT - SEND MESSAGE ║");
743
+ console.log("╚═══════════════════════════════════════════════════════════════════╝");
744
+ console.log("📋 1. GENERAL INFORMATION:");
745
+ console.log(" 📡 URL API:", "https://openrouter.ai/api/v1/chat/completions");
746
+ console.log(" 🤖 Modèle:", payload.model);
747
+ console.log(" 🎭 Personnage:", await this.db.getSelectedCharacter());
748
+ console.log(" 🗣️ Langue:", await this.db.getPreference("selectedLanguage", "en"));
749
+ console.log("\n📋 2. HEADERS HTTP:");
750
+ console.log(" 🔑 Authorization: Bearer", apiKey.substring(0, 10) + "...");
751
+ console.log(" 📄 Content-Type: application/json");
752
+ console.log(" 🌐 HTTP-Referer:", window.location.origin);
753
+ console.log(" 🏷️ X-Title: Kimi - Virtual Companion");
754
+ console.log("\n⚙️ 3. PARAMÈTRES LLM:");
755
+ console.log(" 🌡️ Temperature:", payload.temperature);
756
+ console.log(" 📏 Max Tokens:", payload.max_tokens);
757
+ console.log(" 🎯 Top P:", payload.top_p);
758
+ console.log(" 🔄 Frequency Penalty:", payload.frequency_penalty);
759
+ console.log(" 👤 Presence Penalty:", payload.presence_penalty);
760
+ console.log("\n🎭 4. PROMPT SYSTÈME GÉNÉRÉ:");
761
+ const systemMessage = payload.messages.find(m => m.role === "system");
762
+ if (systemMessage) {
763
+ console.log(" 📝 Longueur du prompt:", systemMessage.content.length, "caractères");
764
+ console.log(" 📄 CONTENU COMPLET DU PROMPT:");
765
+ console.log(" " + "─".repeat(80));
766
+ // Imprimer chaque ligne avec indentation
767
+ systemMessage.content.split(/\n/).forEach(l => console.log(" " + l));
768
+ console.log(" " + "─".repeat(80));
769
+ }
770
+ console.log("\n💬 5. CONTEXTE DE CONVERSATION:");
771
+ console.log(" 📊 Nombre total de messages:", payload.messages.length);
772
+ console.log(" 📋 Détail des messages:");
773
+ payload.messages.forEach((msg, index) => {
774
+ if (msg.role === "system") {
775
+ console.log(` [${index}] 🎭 SYSTEM: ${msg.content.length} caractères`);
776
+ } else if (msg.role === "user") {
777
+ console.log(` [${index}] 👤 USER: "${msg.content}"`);
778
+ } else if (msg.role === "assistant") {
779
+ console.log(` [${index}] 🤖 ASSISTANT: "${msg.content.substring(0, 120)}..."`);
780
+ }
781
+ });
782
+ const payloadSize = JSON.stringify(payload).length;
783
+ console.log("\n📦 6. TAILLE DU PAYLOAD:");
784
+ console.log(" 📝 Taille totale:", payloadSize, "caractères");
785
+ console.log(" 💾 Taille en KB:", Math.round((payloadSize / 1024) * 100) / 100, "KB");
786
+ console.log("\n🚀 Envoi en cours vers l'API...");
787
+ console.log("╔═══════════════════════════════════════════════════════════════════╗");
788
+ }
789
+ // ===== FIN AUDIT =====
790
+
791
+ if (window.DEBUG_SAFE_LOGS) {
792
+ console.debug("LLM payload meta:", {
793
+ model: payload.model,
794
+ temperature: payload.temperature,
795
+ max_tokens: payload.max_tokens
796
+ });
797
+ }
798
+
799
+ try {
800
+ // Basic retry with exponential backoff and jitter for 429/5xx
801
+ const maxAttempts = 3;
802
+ let attempt = 0;
803
+ let response;
804
+ while (attempt < maxAttempts) {
805
+ attempt++;
806
+ response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
807
+ method: "POST",
808
+ headers: {
809
+ Authorization: `Bearer ${apiKey}`,
810
+ "Content-Type": "application/json",
811
+ "HTTP-Referer": window.location.origin,
812
+ "X-Title": "Kimi - Virtual Companion"
813
+ },
814
+ body: JSON.stringify(payload)
815
+ });
816
+ if (response.ok) break;
817
+ if (response.status === 429 || response.status >= 500) {
818
+ const base = 400;
819
+ const delay = base * Math.pow(2, attempt - 1) + Math.floor(Math.random() * 200);
820
+ await new Promise(r => setTimeout(r, delay));
821
+ continue;
822
+ }
823
+ break;
824
+ }
825
+
826
+ if (!response.ok) {
827
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
828
+ let suggestions = [];
829
+
830
+ try {
831
+ const errorData = await response.json();
832
+ if (errorData.error) {
833
+ errorMessage = errorData.error.message || errorData.error.code || errorMessage;
834
+
835
+ // More explicit error messages with suggestions
836
+ if (response.status === 422) {
837
+ errorMessage = `Model \"${this.currentModel}\" not available on OpenRouter.`;
838
+
839
+ // Refresh available models from API and try best match once
840
+ try {
841
+ await this.refreshRemoteModels();
842
+ const best = this.findBestMatchingModelId(this.currentModel);
843
+ if (best && best !== this.currentModel) {
844
+ // Try once with corrected model
845
+ this.currentModel = best;
846
+ await this.db.setPreference("llmModelId", best);
847
+ this._notifyModelChanged();
848
+ const retryResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
849
+ method: "POST",
850
+ headers: {
851
+ Authorization: `Bearer ${apiKey}`,
852
+ "Content-Type": "application/json",
853
+ "HTTP-Referer": window.location.origin,
854
+ "X-Title": "Kimi - Virtual Companion"
855
+ },
856
+ body: JSON.stringify({ ...payload, model: best })
857
+ });
858
+ if (retryResponse.ok) {
859
+ const retryData = await retryResponse.json();
860
+ const kimiResponse = retryData.choices?.[0]?.message?.content;
861
+ if (!kimiResponse) throw new Error("Invalid API response - no content generated");
862
+ this.conversationContext.push(
863
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
864
+ { role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() }
865
+ );
866
+ if (this.conversationContext.length > this.maxContextLength * 2) {
867
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
868
+ }
869
+ return kimiResponse;
870
+ }
871
+ }
872
+ } catch (e) {
873
+ // Swallow refresh errors; will fall through to standard error handling
874
+ }
875
+ } else if (response.status === 401) {
876
+ errorMessage = "Invalid API key. Check your OpenRouter key in the settings.";
877
+ } else if (response.status === 429) {
878
+ errorMessage = "Rate limit reached. Please wait a moment before trying again.";
879
+ } else if (response.status === 402) {
880
+ errorMessage = "Insufficient credit on your OpenRouter account.";
881
+ }
882
+ }
883
+ } catch (parseError) {
884
+ console.warn("Unable to parse API error:", parseError);
885
+ }
886
+
887
+ console.error(`OpenRouter API error (${response.status}):`, errorMessage);
888
+
889
+ // Add suggestions to the error if available
890
+ const error = new Error(errorMessage);
891
+ if (suggestions.length > 0) {
892
+ error.suggestions = suggestions;
893
+ }
894
+
895
+ throw error;
896
+ }
897
+
898
+ const data = await response.json();
899
+
900
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
901
+ throw new Error("Invalid API response - no content generated");
902
+ }
903
+
904
+ const kimiResponse = data.choices[0].message.content;
905
+
906
+ // Add to context
907
+ this.conversationContext.push(
908
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
909
+ { role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() }
910
+ );
911
+
912
+ // Limit context size
913
+ if (this.conversationContext.length > this.maxContextLength * 2) {
914
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
915
+ }
916
+
917
+ // Token usage estimation (deferred save)
918
+ try {
919
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
920
+ const tokensIn = est(userMessage + " " + systemPromptContent);
921
+ const tokensOut = est(kimiResponse);
922
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
923
+ if (!window.kimiMemory && this.db) {
924
+ const character = await this.db.getSelectedCharacter();
925
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
926
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
927
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
928
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
929
+ }
930
+ } catch (e) {
931
+ console.warn("Token usage estimation failed (OpenRouter):", e);
932
+ }
933
+ return kimiResponse;
934
+ } catch (networkError) {
935
+ if (networkError.name === "TypeError" && networkError.message.includes("fetch")) {
936
+ throw new Error("Network connection error. Check your internet connection.");
937
+ }
938
+ throw networkError;
939
+ }
940
+ }
941
+
942
+ async chatWithLocal(userMessage, options = {}) {
943
+ try {
944
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
945
+ let languageInstruction = ""; // placeholder (language guidance is included in assembled prompt)
946
+ let systemPromptContent = await this.assemblePrompt(userMessage);
947
+ if (window.KIMI_DEBUG_API_AUDIT) {
948
+ console.log("===== FULL SYSTEM PROMPT (Local) =====\n" + systemPromptContent + "\n===== END SYSTEM PROMPT =====");
949
+ }
950
+ const response = await fetch("http://localhost:11434/api/chat", {
951
+ method: "POST",
952
+ headers: {
953
+ "Content-Type": "application/json"
954
+ },
955
+ body: JSON.stringify({
956
+ model: "gemma-3n-E4B-it-Q4_K_M.gguf",
957
+ messages: [
958
+ { role: "system", content: systemPromptContent },
959
+ { role: "user", content: userMessage }
960
+ ],
961
+ stream: false
962
+ })
963
+ });
964
+ if (!response.ok) {
965
+ throw new Error("Ollama not available");
966
+ }
967
+ const data = await response.json();
968
+ const content = data?.message?.content || data?.choices?.[0]?.message?.content || "";
969
+ if (!content) throw new Error("Local model returned empty response");
970
+
971
+ // Add to context like other providers
972
+ this.conversationContext.push(
973
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
974
+ { role: "assistant", content: content, timestamp: new Date().toISOString() }
975
+ );
976
+ if (this.conversationContext.length > this.maxContextLength * 2) {
977
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
978
+ }
979
+
980
+ // Estimate token usage for local model (heuristic)
981
+ try {
982
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
983
+ const tokensIn = est(userMessage + " " + systemPromptContent);
984
+ const tokensOut = est(content);
985
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
986
+ const character = await this.db.getSelectedCharacter();
987
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
988
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
989
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
990
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
991
+ } catch (e) {
992
+ console.warn("Token usage estimation failed (local):", e);
993
+ }
994
+ return content;
995
+ } catch (error) {
996
+ console.warn("Local LLM not available:", error);
997
+ return this.getFallbackResponse(userMessage);
998
+ }
999
+ }
1000
+
1001
+ // ===== STREAMING METHODS =====
1002
+
1003
+ async chatWithOpenRouterStreaming(userMessage, onToken, options = {}) {
1004
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
1005
+ const apiKey = await (window.KimiProviderUtils ? window.KimiProviderUtils.getApiKey(this.db, provider) : this.db.getPreference("providerApiKey"));
1006
+ if (!apiKey) {
1007
+ throw new Error("OpenRouter API key not configured");
1008
+ }
1009
+
1010
+ const systemPromptContent = await this.assemblePrompt(userMessage);
1011
+ const messages = [
1012
+ { role: "system", content: systemPromptContent },
1013
+ ...this.conversationContext.slice(-this.maxContextLength),
1014
+ { role: "user", content: userMessage }
1015
+ ];
1016
+
1017
+ // Get unified defaults and options
1018
+ const unifiedDefaults = window.getUnifiedDefaults
1019
+ ? window.getUnifiedDefaults()
1020
+ : { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 };
1021
+
1022
+ const enableStreaming = await this.db.getPreference("enableStreaming", true);
1023
+
1024
+ const llmSettings = {
1025
+ temperature: await this.db.getPreference("llmTemperature", unifiedDefaults.temperature),
1026
+ maxTokens: await this.db.getPreference("llmMaxTokens", unifiedDefaults.maxTokens),
1027
+ top_p: await this.db.getPreference("llmTopP", unifiedDefaults.top_p),
1028
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", unifiedDefaults.frequency_penalty),
1029
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", unifiedDefaults.presence_penalty)
1030
+ };
1031
+
1032
+ const payload = {
1033
+ model: this.currentModel,
1034
+ messages: messages,
1035
+ stream: enableStreaming, // Use user preference for streaming
1036
+ temperature: typeof options.temperature === "number" ? options.temperature : llmSettings.temperature,
1037
+ max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens,
1038
+ top_p: typeof options.topP === "number" ? options.topP : llmSettings.top_p,
1039
+ frequency_penalty: typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : llmSettings.frequency_penalty,
1040
+ presence_penalty: typeof options.presencePenalty === "number" ? options.presencePenalty : llmSettings.presence_penalty
1041
+ };
1042
+
1043
+ try {
1044
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
1045
+ method: "POST",
1046
+ headers: {
1047
+ Authorization: `Bearer ${apiKey}`,
1048
+ "Content-Type": "application/json",
1049
+ "HTTP-Referer": window.location.origin,
1050
+ "X-Title": "Kimi - Virtual Companion"
1051
+ },
1052
+ body: JSON.stringify(payload)
1053
+ });
1054
+
1055
+ if (!response.ok) {
1056
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1057
+ }
1058
+
1059
+ const reader = response.body.getReader();
1060
+ const decoder = new TextDecoder();
1061
+ let buffer = "";
1062
+ let fullResponse = "";
1063
+
1064
+ try {
1065
+ while (true) {
1066
+ const { done, value } = await reader.read();
1067
+ if (done) break;
1068
+
1069
+ buffer += decoder.decode(value, { stream: true });
1070
+ const lines = buffer.split("\n");
1071
+ buffer = lines.pop() || ""; // Keep incomplete line in buffer
1072
+
1073
+ for (const line of lines) {
1074
+ if (line.trim() === "" || line.startsWith(":")) continue; // Skip empty lines and comments
1075
+
1076
+ if (line.startsWith("data: ")) {
1077
+ const data = line.slice(6);
1078
+ if (data === "[DONE]") {
1079
+ break;
1080
+ }
1081
+
1082
+ try {
1083
+ const parsed = JSON.parse(data);
1084
+ const content = parsed.choices?.[0]?.delta?.content;
1085
+ if (content) {
1086
+ fullResponse += content;
1087
+ onToken(content);
1088
+ }
1089
+ } catch (parseError) {
1090
+ console.warn("Failed to parse streaming chunk:", parseError);
1091
+ }
1092
+ }
1093
+ }
1094
+ }
1095
+ } finally {
1096
+ reader.releaseLock();
1097
+ }
1098
+
1099
+ // Add to context after streaming completes
1100
+ this.conversationContext.push(
1101
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
1102
+ { role: "assistant", content: fullResponse, timestamp: new Date().toISOString() }
1103
+ );
1104
+
1105
+ if (this.conversationContext.length > this.maxContextLength * 2) {
1106
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
1107
+ }
1108
+
1109
+ // Token usage estimation
1110
+ try {
1111
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
1112
+ const tokensIn = est(userMessage + " " + systemPromptContent);
1113
+ const tokensOut = est(fullResponse);
1114
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
1115
+ if (!window.kimiMemory && this.db) {
1116
+ const character = await this.db.getSelectedCharacter();
1117
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
1118
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
1119
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
1120
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
1121
+ }
1122
+ } catch (e) {
1123
+ console.warn("Token usage estimation failed (OpenRouter streaming):", e);
1124
+ }
1125
+
1126
+ return fullResponse;
1127
+ } catch (error) {
1128
+ console.error("OpenRouter streaming error:", error);
1129
+ throw error;
1130
+ }
1131
+ }
1132
+
1133
+ async chatWithOpenAICompatibleStreaming(userMessage, onToken, options = {}) {
1134
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
1135
+ let baseUrl;
1136
+ if (provider === "openai-compatible" || provider === "ollama") {
1137
+ baseUrl = await this.db.getPreference(`llmBaseUrl_${provider}`, provider === "ollama" ? "http://localhost:11434/api/chat" : "");
1138
+ } else {
1139
+ const sharedPlaceholders = window.KimiProviderPlaceholders || {};
1140
+ baseUrl = sharedPlaceholders[provider] || sharedPlaceholders.openrouter || "https://openrouter.ai/api/v1/chat/completions";
1141
+ }
1142
+ const apiKey = window.KimiProviderUtils
1143
+ ? await window.KimiProviderUtils.getApiKey(this.db, provider)
1144
+ : await this.db.getPreference("providerApiKey", "");
1145
+ if (!apiKey) {
1146
+ throw new Error("API key not configured for selected provider");
1147
+ }
1148
+
1149
+ const systemPromptContent = await this.assemblePrompt(userMessage);
1150
+ const messages = [
1151
+ { role: "system", content: systemPromptContent },
1152
+ ...this.conversationContext.slice(-this.maxContextLength),
1153
+ { role: "user", content: userMessage }
1154
+ ];
1155
+
1156
+ const unifiedDefaults = window.getUnifiedDefaults
1157
+ ? window.getUnifiedDefaults()
1158
+ : { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 };
1159
+
1160
+ const enableStreaming = await this.db.getPreference("enableStreaming", true);
1161
+
1162
+ const llmSettings = {
1163
+ temperature: await this.db.getPreference("llmTemperature", unifiedDefaults.temperature),
1164
+ maxTokens: await this.db.getPreference("llmMaxTokens", unifiedDefaults.maxTokens),
1165
+ top_p: await this.db.getPreference("llmTopP", unifiedDefaults.top_p),
1166
+ frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", unifiedDefaults.frequency_penalty),
1167
+ presence_penalty: await this.db.getPreference("llmPresencePenalty", unifiedDefaults.presence_penalty)
1168
+ };
1169
+
1170
+ const payload = {
1171
+ model: this.currentModel,
1172
+ messages: messages,
1173
+ stream: enableStreaming,
1174
+ temperature: typeof options.temperature === "number" ? options.temperature : llmSettings.temperature,
1175
+ max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens,
1176
+ top_p: typeof options.topP === "number" ? options.topP : llmSettings.top_p,
1177
+ frequency_penalty: typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : llmSettings.frequency_penalty,
1178
+ presence_penalty: typeof options.presencePenalty === "number" ? options.presencePenalty : llmSettings.presence_penalty
1179
+ };
1180
+
1181
+ try {
1182
+ const response = await fetch(baseUrl, {
1183
+ method: "POST",
1184
+ headers: {
1185
+ Authorization: `Bearer ${apiKey}`,
1186
+ "Content-Type": "application/json"
1187
+ },
1188
+ body: JSON.stringify(payload)
1189
+ });
1190
+
1191
+ if (!response.ok) {
1192
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1193
+ }
1194
+
1195
+ const reader = response.body.getReader();
1196
+ const decoder = new TextDecoder();
1197
+ let buffer = "";
1198
+ let fullResponse = "";
1199
+
1200
+ try {
1201
+ while (true) {
1202
+ const { done, value } = await reader.read();
1203
+ if (done) break;
1204
+
1205
+ buffer += decoder.decode(value, { stream: true });
1206
+ const lines = buffer.split("\n");
1207
+ buffer = lines.pop() || "";
1208
+
1209
+ for (const line of lines) {
1210
+ if (line.trim() === "" || line.startsWith(":")) continue;
1211
+
1212
+ if (line.startsWith("data: ")) {
1213
+ const data = line.slice(6);
1214
+ if (data === "[DONE]") {
1215
+ break;
1216
+ }
1217
+
1218
+ try {
1219
+ const parsed = JSON.parse(data);
1220
+ const content = parsed.choices?.[0]?.delta?.content;
1221
+ if (content) {
1222
+ fullResponse += content;
1223
+ onToken(content);
1224
+ }
1225
+ } catch (parseError) {
1226
+ console.warn("Failed to parse streaming chunk:", parseError);
1227
+ }
1228
+ }
1229
+ }
1230
+ }
1231
+ } finally {
1232
+ reader.releaseLock();
1233
+ }
1234
+
1235
+ // Add to context
1236
+ this.conversationContext.push(
1237
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
1238
+ { role: "assistant", content: fullResponse, timestamp: new Date().toISOString() }
1239
+ );
1240
+
1241
+ if (this.conversationContext.length > this.maxContextLength * 2) {
1242
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
1243
+ }
1244
+
1245
+ // Token usage estimation
1246
+ try {
1247
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
1248
+ const tokensIn = est(userMessage + " " + systemPromptContent);
1249
+ const tokensOut = est(fullResponse);
1250
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
1251
+ if (!window.kimiMemory && this.db) {
1252
+ const character = await this.db.getSelectedCharacter();
1253
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
1254
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
1255
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
1256
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
1257
+ }
1258
+ } catch (e) {
1259
+ console.warn("Token usage estimation failed (OpenAI streaming):", e);
1260
+ }
1261
+
1262
+ return fullResponse;
1263
+ } catch (error) {
1264
+ console.error("OpenAI compatible streaming error:", error);
1265
+ throw error;
1266
+ }
1267
+ }
1268
+
1269
+ async chatWithLocalStreaming(userMessage, onToken, options = {}) {
1270
+ const systemPromptContent = await this.assemblePrompt(userMessage);
1271
+ const enableStreaming = await this.db.getPreference("enableStreaming", true);
1272
+
1273
+ const payload = {
1274
+ model: this.currentModel || "llama2",
1275
+ messages: [
1276
+ { role: "system", content: systemPromptContent },
1277
+ ...this.conversationContext.slice(-this.maxContextLength),
1278
+ { role: "user", content: userMessage }
1279
+ ],
1280
+ stream: enableStreaming
1281
+ };
1282
+
1283
+ try {
1284
+ const response = await fetch("http://localhost:11434/api/chat", {
1285
+ method: "POST",
1286
+ headers: {
1287
+ "Content-Type": "application/json"
1288
+ },
1289
+ body: JSON.stringify(payload)
1290
+ });
1291
+
1292
+ if (!response.ok) {
1293
+ throw new Error("Ollama not available");
1294
+ }
1295
+
1296
+ let fullResponse = "";
1297
+
1298
+ if (enableStreaming) {
1299
+ // Streaming mode
1300
+ const reader = response.body.getReader();
1301
+ const decoder = new TextDecoder();
1302
+
1303
+ try {
1304
+ while (true) {
1305
+ const { done, value } = await reader.read();
1306
+ if (done) break;
1307
+
1308
+ const chunk = decoder.decode(value, { stream: true });
1309
+ const lines = chunk.split("\n").filter(line => line.trim());
1310
+
1311
+ for (const line of lines) {
1312
+ try {
1313
+ const parsed = JSON.parse(line);
1314
+ const content = parsed.message?.content;
1315
+ if (content) {
1316
+ fullResponse += content;
1317
+ onToken(content);
1318
+ }
1319
+ if (parsed.done) {
1320
+ break;
1321
+ }
1322
+ } catch (parseError) {
1323
+ console.warn("Failed to parse Ollama streaming chunk:", parseError);
1324
+ }
1325
+ }
1326
+ }
1327
+ } finally {
1328
+ reader.releaseLock();
1329
+ }
1330
+ } else {
1331
+ // Non-streaming mode
1332
+ const data = await response.json();
1333
+ fullResponse = data.message?.content || "";
1334
+ if (fullResponse && onToken) {
1335
+ onToken(fullResponse);
1336
+ }
1337
+ }
1338
+
1339
+ // Add to context
1340
+ this.conversationContext.push(
1341
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
1342
+ { role: "assistant", content: fullResponse, timestamp: new Date().toISOString() }
1343
+ );
1344
+
1345
+ if (this.conversationContext.length > this.maxContextLength * 2) {
1346
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
1347
+ }
1348
+
1349
+ // Token usage estimation
1350
+ try {
1351
+ const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4));
1352
+ const tokensIn = est(userMessage + " " + systemPromptContent);
1353
+ const tokensOut = est(fullResponse);
1354
+ window._lastKimiTokenUsage = { tokensIn, tokensOut };
1355
+ const character = await this.db.getSelectedCharacter();
1356
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
1357
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
1358
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn);
1359
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut);
1360
+ } catch (e) {
1361
+ console.warn("Token usage estimation failed (local streaming):", e);
1362
+ }
1363
+
1364
+ return fullResponse;
1365
+ } catch (error) {
1366
+ console.warn("Local LLM streaming not available:", error);
1367
+ throw error;
1368
+ }
1369
+ }
1370
+
1371
+ getFallbackResponse(userMessage, errorType = "api") {
1372
+ // Use centralized fallback manager instead of duplicated logic
1373
+ if (window.KimiFallbackManager) {
1374
+ // Map error types to the correct format
1375
+ const errorTypeMap = {
1376
+ api: "api_error",
1377
+ model: "model_error",
1378
+ network: "network_error"
1379
+ };
1380
+ const mappedType = errorTypeMap[errorType] || "technical_error";
1381
+ return window.KimiFallbackManager.getFallbackMessage(mappedType);
1382
+ }
1383
+
1384
+ // Fallback to legacy system if KimiFallbackManager not available
1385
+ const i18n = window.kimiI18nManager;
1386
+ if (!i18n) {
1387
+ return "Sorry, I'm having technical difficulties! 💕";
1388
+ }
1389
+ return i18n.t("fallback_technical_error");
1390
+ }
1391
+
1392
+ getFallbackKeywords(trait, type) {
1393
+ const keywords = {
1394
+ humor: {
1395
+ positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"],
1396
+ negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"]
1397
+ },
1398
+ intelligence: {
1399
+ positive: ["intelligent", "smart", "brilliant", "logical", "clever", "wise", "genius", "thoughtful", "insightful"],
1400
+ negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"]
1401
+ },
1402
+ romance: {
1403
+ positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"],
1404
+ negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
1405
+ },
1406
+ affection: {
1407
+ positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
1408
+ negative: ["mean", "cold", "indifferent", "distant", "rejection", "hate", "hostile"]
1409
+ },
1410
+ playfulness: {
1411
+ positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
1412
+ negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"]
1413
+ },
1414
+ empathy: {
1415
+ positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
1416
+ negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
1417
+ }
1418
+ };
1419
+ return keywords[trait]?.[type] || [];
1420
+ }
1421
+
1422
+ // Mémoire temporaire pour l'accumulation négative par trait
1423
+ _negativeStreaks = {};
1424
+
1425
+ async updatePersonalityFromResponse(userMessage, kimiResponse) {
1426
+ // Use unified emotion system for personality updates
1427
+ if (window.kimiEmotionSystem) {
1428
+ return await window.kimiEmotionSystem.updatePersonalityFromConversation(userMessage, kimiResponse, await this.db.getSelectedCharacter());
1429
+ }
1430
+
1431
+ // Legacy fallback (should not be reached)
1432
+ console.warn("Unified emotion system not available, skipping personality update");
1433
+ }
1434
+
1435
+ async getModelStats() {
1436
+ const models = await this.db.getAllLLMModels();
1437
+ const currentModelInfo = this.availableModels[this.currentModel];
1438
+
1439
+ return {
1440
+ current: {
1441
+ id: this.currentModel,
1442
+ info: currentModelInfo
1443
+ },
1444
+ available: this.availableModels,
1445
+ configured: models,
1446
+ contextLength: this.conversationContext.length
1447
+ };
1448
+ }
1449
+
1450
+ async testModel(modelId, testMessage = "Test API ok?") {
1451
+ // Ancienne méthode de test (non minimaliste)
1452
+ return await this.testApiKeyMinimal(modelId);
1453
+ }
1454
+
1455
+ /**
1456
+ * Test API minimaliste et centralisé pour tous les providers compatibles.
1457
+ * Envoie uniquement un prompt système court et un message utilisateur dans la langue choisie.
1458
+ * Aucun contexte, aucune mémoire, aucun paramètre superflu.
1459
+ * @param {string} modelId - ID du modèle à tester
1460
+ * @returns {Promise<{success: boolean, response?: string, error?: string}>}
1461
+ */
1462
+ async testApiKeyMinimal(modelId) {
1463
+ const originalModel = this.currentModel;
1464
+ try {
1465
+ await this.setCurrentModel(modelId);
1466
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
1467
+ const lang = await this.db.getPreference("selectedLanguage", "en");
1468
+ let testWord;
1469
+ switch (lang) {
1470
+ case "fr":
1471
+ testWord = "Bonjour";
1472
+ break;
1473
+ case "es":
1474
+ testWord = "Hola";
1475
+ break;
1476
+ case "de":
1477
+ testWord = "Hallo";
1478
+ break;
1479
+ case "it":
1480
+ testWord = "Ciao";
1481
+ break;
1482
+ case "ja":
1483
+ testWord = "こんにちは";
1484
+ break;
1485
+ case "zh":
1486
+ testWord = "你好";
1487
+ break;
1488
+ default:
1489
+ testWord = "Hello";
1490
+ }
1491
+ const systemPrompt = "You are a helpful assistant.";
1492
+ let apiKey = await (window.KimiProviderUtils ? window.KimiProviderUtils.getApiKey(this.db, provider) : this.db.getPreference("providerApiKey"));
1493
+
1494
+ if (!apiKey) {
1495
+ return { success: false, error: "No API key found for provider: " + provider };
1496
+ }
1497
+
1498
+ let baseUrl = "";
1499
+ let payload = {
1500
+ model: modelId,
1501
+ messages: [
1502
+ { role: "system", content: systemPrompt },
1503
+ { role: "user", content: testWord }
1504
+ ],
1505
+ max_tokens: 2
1506
+ };
1507
+ let headers = { "Content-Type": "application/json" };
1508
+ if (provider === "openrouter") {
1509
+ baseUrl = "https://openrouter.ai/api/v1/chat/completions";
1510
+ headers["Authorization"] = `Bearer ${apiKey}`;
1511
+ headers["HTTP-Referer"] = window.location.origin;
1512
+ headers["X-Title"] = "Kimi - Virtual Companion";
1513
+ } else if (["openai", "groq", "together", "deepseek", "openai-compatible"].includes(provider)) {
1514
+ // When selecting baseUrl during initialization/fallback, respect provider-specific stored URLs
1515
+ const currentProvider = await this.db.getPreference("llmProvider", "openrouter");
1516
+ if (currentProvider === "openai-compatible" || currentProvider === "ollama") {
1517
+ baseUrl = await this.db.getPreference(
1518
+ `llmBaseUrl_${currentProvider}`,
1519
+ currentProvider === "ollama" ? "http://localhost:11434/api/chat" : ""
1520
+ );
1521
+ } else {
1522
+ const sharedPlaceholders = window.KimiProviderPlaceholders || {};
1523
+ baseUrl = sharedPlaceholders[provider] || sharedPlaceholders.openrouter || "https://openrouter.ai/api/v1/chat/completions";
1524
+ }
1525
+ headers["Authorization"] = `Bearer ${apiKey}`;
1526
+ } else if (provider === "ollama") {
1527
+ baseUrl = "http://localhost:11434/api/chat";
1528
+ payload = {
1529
+ model: modelId,
1530
+ messages: [
1531
+ { role: "system", content: systemPrompt },
1532
+ { role: "user", content: testWord }
1533
+ ],
1534
+ stream: false
1535
+ };
1536
+ } else {
1537
+ throw new Error("Unknown provider: " + provider);
1538
+ }
1539
+
1540
+ const response = await fetch(baseUrl, {
1541
+ method: "POST",
1542
+ headers,
1543
+ body: JSON.stringify(payload)
1544
+ });
1545
+ if (!response.ok) {
1546
+ const error = await response.text();
1547
+ return { success: false, error };
1548
+ }
1549
+ const data = await response.json();
1550
+ let content = "";
1551
+ if (provider === "ollama") {
1552
+ content = data?.message?.content || data?.choices?.[0]?.message?.content || "";
1553
+ } else {
1554
+ content = data?.choices?.[0]?.message?.content || "";
1555
+ }
1556
+ return { success: true, response: content };
1557
+ } catch (error) {
1558
+ return { success: false, error: error.message };
1559
+ } finally {
1560
+ await this.setCurrentModel(originalModel);
1561
+ }
1562
+ }
1563
+
1564
+ // Complete model diagnosis
1565
+ async diagnoseModel(modelId) {
1566
+ const model = this.availableModels[modelId];
1567
+ if (!model) {
1568
+ return {
1569
+ available: false,
1570
+ error: "Model not found in local list"
1571
+ };
1572
+ }
1573
+
1574
+ // Check availability on OpenRouter
1575
+ try {
1576
+ // Model availability is checked against the local cache; remote checks occur in refreshRemoteModels()
1577
+ return {
1578
+ available: true,
1579
+ model: model,
1580
+ pricing: model.pricing
1581
+ };
1582
+ } catch (error) {
1583
+ return {
1584
+ available: false,
1585
+ error: `Unable to check: ${error.message}`
1586
+ };
1587
+ }
1588
+ }
1589
+
1590
+ // Fetch models from OpenRouter API and merge into availableModels
1591
+ async refreshRemoteModels() {
1592
+ if (this._isRefreshingModels) return;
1593
+ this._isRefreshingModels = true;
1594
+ try {
1595
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
1596
+ const apiKey = await (window.KimiProviderUtils
1597
+ ? window.KimiProviderUtils.getApiKey(this.db, provider)
1598
+ : this.db.getPreference("providerApiKey", ""));
1599
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
1600
+ method: "GET",
1601
+ headers: {
1602
+ "Content-Type": "application/json",
1603
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
1604
+ "HTTP-Referer": window.location.origin,
1605
+ "X-Title": "Kimi - Virtual Companion"
1606
+ }
1607
+ });
1608
+ if (!res.ok) {
1609
+ throw new Error(`Unable to fetch models: HTTP ${res.status}`);
1610
+ }
1611
+ const data = await res.json();
1612
+ if (!data?.data || !Array.isArray(data.data)) {
1613
+ throw new Error("Invalid models response format");
1614
+ }
1615
+ // Build a fresh map while preserving local/ollama entry
1616
+ const newMap = {};
1617
+ data.data.forEach(m => {
1618
+ if (!m?.id) return;
1619
+ const id = m.id;
1620
+ const provider = m?.id?.split("/")?.[0] || "OpenRouter";
1621
+ let pricing;
1622
+ const p = m?.pricing;
1623
+ if (p) {
1624
+ const unitRaw = ((p.unit || p.per || p.units || "") + "").toLowerCase();
1625
+ let unitTokens = 1;
1626
+ if (unitRaw) {
1627
+ if (unitRaw.includes("1m")) unitTokens = 1000000;
1628
+ else if (unitRaw.includes("1k") || unitRaw.includes("thousand")) unitTokens = 1000;
1629
+ else {
1630
+ const num = parseFloat(unitRaw.replace(/[^0-9.]/g, ""));
1631
+ if (Number.isFinite(num) && num > 0) {
1632
+ if (unitRaw.includes("m")) unitTokens = num * 1000000;
1633
+ else if (unitRaw.includes("k")) unitTokens = num * 1000;
1634
+ else unitTokens = num;
1635
+ } else if (unitRaw.includes("token")) {
1636
+ unitTokens = 1;
1637
+ }
1638
+ }
1639
+ }
1640
+ const toPerMillion = v => {
1641
+ const n = typeof v === "number" ? v : parseFloat(v);
1642
+ if (!Number.isFinite(n)) return undefined;
1643
+ return n * (1000000 / unitTokens);
1644
+ };
1645
+ if (typeof p.input !== "undefined" || typeof p.output !== "undefined") {
1646
+ pricing = {
1647
+ input: toPerMillion(p.input),
1648
+ output: toPerMillion(p.output)
1649
+ };
1650
+ } else if (typeof p.prompt !== "undefined" || typeof p.completion !== "undefined") {
1651
+ pricing = {
1652
+ input: toPerMillion(p.prompt),
1653
+ output: toPerMillion(p.completion)
1654
+ };
1655
+ } else {
1656
+ pricing = { input: undefined, output: undefined };
1657
+ }
1658
+ } else {
1659
+ pricing = { input: undefined, output: undefined };
1660
+ }
1661
+ newMap[id] = {
1662
+ name: m.name || id,
1663
+ provider,
1664
+ type: "openrouter",
1665
+ contextWindow: m.context_length || m?.context_window || 128000,
1666
+ pricing,
1667
+ strengths: (m?.tags || []).slice(0, 4)
1668
+ };
1669
+ });
1670
+ // Keep local model entry
1671
+ if (this.availableModels["local/ollama"]) {
1672
+ newMap["local/ollama"] = this.availableModels["local/ollama"];
1673
+ }
1674
+ this.recommendedModelIds.forEach(id => {
1675
+ const curated = this.defaultModels[id];
1676
+ if (curated) {
1677
+ newMap[id] = { ...(newMap[id] || {}), ...curated };
1678
+ }
1679
+ });
1680
+ this.availableModels = newMap;
1681
+ this._remoteModelsLoaded = true;
1682
+ } finally {
1683
+ this._isRefreshingModels = false;
1684
+ }
1685
+ }
1686
+
1687
+ // Try to find best matching model id from remote list when an ID is stale
1688
+ findBestMatchingModelId(preferredId) {
1689
+ if (this.availableModels[preferredId]) return preferredId;
1690
+ const id = (preferredId || "").toLowerCase();
1691
+ const tokens = id.split(/[\/:\-_.]+/).filter(Boolean);
1692
+ let best = null;
1693
+ let bestScore = -1;
1694
+ Object.keys(this.availableModels).forEach(candidateId => {
1695
+ const c = candidateId.toLowerCase();
1696
+ let score = 0;
1697
+ tokens.forEach(t => {
1698
+ if (!t) return;
1699
+ if (c.includes(t)) score += 1;
1700
+ });
1701
+ // Give extra weight to common markers
1702
+ if (c.includes("instruct")) score += 0.5;
1703
+ if (c.includes("mistral") && id.includes("mistral")) score += 0.5;
1704
+ if (c.includes("small") && id.includes("small")) score += 0.5;
1705
+ if (score > bestScore) {
1706
+ bestScore = score;
1707
+ best = candidateId;
1708
+ }
1709
+ });
1710
+ // Avoid returning unrelated local model unless nothing else
1711
+ if (best === "local/ollama" && Object.keys(this.availableModels).length > 1) {
1712
+ return null;
1713
+ }
1714
+ return best;
1715
+ }
1716
+
1717
+ _notifyModelChanged() {
1718
+ try {
1719
+ const detail = { id: this.currentModel };
1720
+ if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
1721
+ window.emitAppEvent && window.emitAppEvent("llmModelChanged", detail);
1722
+ }
1723
+ } catch (e) {}
1724
+ }
1725
+ }
1726
+
1727
+ // Export for usage
1728
+ window.KimiLLMManager = KimiLLMManager;
1729
+ export default KimiLLMManager;
kimi-js/kimi-main.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // Ensure KimiProviderUtils is available (imported from kimi-utils.js)
9
+ window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils;
10
+ window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager;
11
+ window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem;
kimi-js/kimi-memory-system.js ADDED
@@ -0,0 +1,2257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI INTELLIGENT MEMORY SYSTEM =====
2
+ class KimiMemorySystem {
3
+ constructor(database) {
4
+ this.db = database;
5
+ this.memoryEnabled = true;
6
+ this.maxMemoryEntries = 100;
7
+
8
+ // Performance optimization: keyword cache with LRU eviction
9
+ this.keywordCache = new Map(); // keyword_language -> boolean (is common)
10
+ this.keywordCacheSize = 1000; // Limit memory usage
11
+ this.keywordCacheHits = 0;
12
+ this.keywordCacheMisses = 0;
13
+
14
+ // Performance monitoring
15
+ this.queryStats = {
16
+ extractionTime: [],
17
+ addMemoryTime: [],
18
+ retrievalTime: []
19
+ };
20
+
21
+ // Centralized configuration for all thresholds and magic numbers
22
+ this.config = {
23
+ // Content validation thresholds
24
+ minContentLength: 2,
25
+ longContentThreshold: 24,
26
+ titleWordCount: {
27
+ preferred: 3,
28
+ min: 1,
29
+ max: 5
30
+ },
31
+
32
+ // Similarity and confidence thresholds
33
+ similarity: {
34
+ personal: 0.6, // Names can vary more (Jean vs Jean-Pierre)
35
+ preferences: 0.7, // Preferences can be expressed differently
36
+ default: 0.8, // General similarity threshold
37
+ veryHigh: 0.9, // For boost_confidence strategy
38
+ update: 0.3 // Lower threshold for memory updates
39
+ },
40
+
41
+ // Confidence scoring
42
+ confidence: {
43
+ base: 0.6,
44
+ explicitRequest: 1.0,
45
+ naturalExpression: 0.7,
46
+ bonusForLongContent: 0.1,
47
+ bonusForExplicitStatement: 0.3,
48
+ penaltyForUncertainty: 0.2,
49
+ min: 0.1,
50
+ max: 1.0
51
+ },
52
+
53
+ // Memory management
54
+ cleanup: {
55
+ maxEntries: 100,
56
+ ttlDays: 365,
57
+ batchSize: 100,
58
+ touchMinutes: 60
59
+ },
60
+
61
+ // Performance settings
62
+ cache: {
63
+ keywordCacheSize: 1000,
64
+ statHistorySize: 100
65
+ },
66
+
67
+ // Scoring weights for importance calculation
68
+ importance: {
69
+ categoryWeights: {
70
+ important: 1.0,
71
+ personal: 0.9,
72
+ relationships: 0.85,
73
+ goals: 0.75,
74
+ experiences: 0.65,
75
+ preferences: 0.6,
76
+ activities: 0.5
77
+ },
78
+ bonuses: {
79
+ relationshipMilestone: 0.15,
80
+ boundaries: 0.15,
81
+ strongEmotion: 0.05,
82
+ futureReference: 0.05,
83
+ longContent: 0.05,
84
+ highConfidence: 0.05
85
+ }
86
+ },
87
+
88
+ // Relevance calculation weights
89
+ relevance: {
90
+ contentSimilarity: 0.35,
91
+ keywordOverlap: 0.25,
92
+ categoryRelevance: 0.1,
93
+ recencyBonus: 0.1,
94
+ confidenceBonus: 0.05,
95
+ importanceBonus: 0.05,
96
+ recentDaysThreshold: 30
97
+ }
98
+ };
99
+
100
+ this.memoryCategories = {
101
+ personal: "Personal Information",
102
+ preferences: "Likes & Dislikes",
103
+ relationships: "Relationships & People",
104
+ activities: "Activities & Hobbies",
105
+ goals: "Goals & Aspirations",
106
+ experiences: "Shared Experiences",
107
+ important: "Important Events"
108
+ };
109
+
110
+ // Patterns for automatic memory extraction (multilingual)
111
+ this.extractionPatterns = {
112
+ personal: [
113
+ // English patterns
114
+ /(?:my name is|i'm called|call me|i am) (\w+)/i,
115
+ /(?:i am|i'm) (\d+) years? old/i,
116
+ /(?:i live in|i'm from|from) ([^,.!?]+)/i,
117
+ /(?:i work as|my job is|i'm a) ([^,.!?]+)/i,
118
+ // French patterns
119
+ /(?:je m'appelle|mon nom est|je suis|je me prénomme|je me nomme) ([^,.!?]+)/i,
120
+ /(?:j'ai) (\d+) ans?/i,
121
+ /(?:j'habite à|je vis à|je viens de) ([^,.!?]+)/i,
122
+ /(?:je travaille comme|mon travail est|je suis) ([^,.!?]+)/i,
123
+ // Spanish patterns
124
+ /(?:me llamo|mi nombre es|soy) ([^,.!?]+)/i,
125
+ /(?:tengo) (\d+) años?/i,
126
+ /(?:vivo en|soy de) ([^,.!?]+)/i,
127
+ /(?:trabajo como|mi trabajo es|soy) ([^,.!?]+)/i,
128
+ // Italian patterns
129
+ /(?:mi chiamo|il mio nome è|sono) ([^,.!?]+)/i,
130
+ /(?:ho) (\d+) anni?/i,
131
+ /(?:abito a|vivo a|sono di) ([^,.!?]+)/i,
132
+ /(?:lavoro come|il mio lavoro è|sono) ([^,.!?]+)/i,
133
+ // German patterns
134
+ /(?:ich heiße|mein name ist|ich bin) ([^,.!?]+)/i,
135
+ /(?:ich bin) (\d+) jahre? alt/i,
136
+ /(?:ich wohne in|ich lebe in|ich komme aus) ([^,.!?]+)/i,
137
+ /(?:ich arbeite als|mein beruf ist|ich bin) ([^,.!?]+)/i,
138
+ // Japanese patterns
139
+ /私の名前は([^。!?!?、,.]+)[ですだ]?/i,
140
+ /私は([^。!?!?、,.]+)です/i,
141
+ /([^、。!?!?,.]+)と申します/i,
142
+ /([^、。!?!?,.]+)といいます/i,
143
+ // Chinese patterns
144
+ /我叫([^,。!?!?,.]+)/i,
145
+ /我的名字是([^,。!?!?,.]+)/i,
146
+ /叫我([^,。!?!?,.]+)/i
147
+ ],
148
+ preferences: [
149
+ // English patterns
150
+ /(?:i love|i like|i enjoy|i prefer) ([^,.!?]+)/i,
151
+ /(?:i hate|i dislike|i don't like) ([^,.!?]+)/i,
152
+ /(?:my favorite|i really like) ([^,.!?]+)/i,
153
+ // French patterns
154
+ /(?:j'aime|j'adore|je préfère) ([^,.!?]+)/i,
155
+ /(?:je déteste|je n'aime pas) ([^,.!?]+)/i,
156
+ /(?:mon préféré|ma préférée) (?:est|sont) ([^,.!?]+)/i,
157
+ // Explicit memory requests
158
+ /(?:ajoute? (?:au|à la) (?:système? )?(?:de )?mémoire|retiens?|mémorise?) (?:que )?(.+)/i,
159
+ /(?:add to memory|remember|memorize) (?:that )?(.+)/i
160
+ ],
161
+ relationships: [
162
+ // English patterns
163
+ /(?:my (?:wife|husband|girlfriend|boyfriend|partner)) (?:is|named?) ([^,.!?]+)/i,
164
+ /(?:my (?:mother|father|sister|brother|friend)) ([^,.!?]+)/i,
165
+ // French patterns
166
+ /(?:ma (?:femme|copine|partenaire)|mon (?:mari|copain|partenaire)) (?:s'appelle|est) ([^,.!?]+)/i,
167
+ /(?:ma (?:mère|sœur)|mon (?:père|frère|ami)) (?:s'appelle|est) ([^,.!?]+)/i,
168
+ // Spanish patterns
169
+ /(?:mi (?:esposa|esposo|novia|novio|pareja)) (?:es|se llama) ([^,.!?]+)/i,
170
+ /(?:mi (?:madre|padre|hermana|hermano|amigo|amiga)) (?:es|se llama) ([^,.!?]+)/i,
171
+ // Italian patterns
172
+ /(?:la mia (?:moglie|fidanzata|compagna)|il mio (?:marito|fidanzato|compagno)) (?:è|si chiama) ([^,.!?]+)/i,
173
+ /(?:mia (?:madre|sorella)|mio (?:padre|fratello|amico)) (?:è|si chiama) ([^,.!?]+)/i,
174
+ // German patterns
175
+ /(?:meine (?:frau|freundin|partnerin)|mein (?:mann|freund|partner)) (?:ist|heißt) ([^,.!?]+)/i,
176
+ /(?:meine (?:mutter|schwester)|mein (?:vater|bruder|freund)) (?:ist|heißt) ([^,.!?]+)/i,
177
+ // Japanese patterns
178
+ /(?:私の(?:妻|夫|彼女|彼氏|パートナー))は([^。!?!?、,.]+)(?:です|といいます)/i,
179
+ /(?:私の(?:母|父|姉|妹|兄|弟|友達))は([^。!?!?、,.]+)(?:です|といいます)/i,
180
+ // Chinese patterns
181
+ /(?:我的(?:妻子|丈夫|女朋友|男朋友|伴侣))叫([^,。!?!?,.]+)/i,
182
+ /(?:我的(?:妈妈|父亲|姐姐|妹妹|哥哥|弟弟|朋友))叫([^,。!?!?,.]+)/i
183
+ ],
184
+ activities: [
185
+ // English patterns
186
+ /(?:i play|i do|i practice) ([^,.!?]+)/i,
187
+ /(?:my hobby is|i hobby) ([^,.!?]+)/i,
188
+ // French patterns
189
+ /(?:je joue|je fais|je pratique) ([^,.!?]+)/i,
190
+ /(?:mon passe-temps|mon hobby) (?:est|c'est) ([^,.!?]+)/i,
191
+ // Spanish patterns
192
+ /(?:juego|hago|practico) ([^,.!?]+)/i,
193
+ /(?:mi pasatiempo|mi hobby) (?:es) ([^,.!?]+)/i,
194
+ // Italian patterns
195
+ /(?:gioco|faccio|pratico) ([^,.!?]+)/i,
196
+ /(?:il mio passatempo|il mio hobby) (?:è) ([^,.!?]+)/i,
197
+ // German patterns
198
+ /(?:ich spiele|ich mache|ich übe) ([^,.!?]+)/i,
199
+ /(?:mein hobby ist) ([^,.!?]+)/i,
200
+ // Japanese patterns
201
+ /(?:私は)?(?:[^、。!?!?,.]+)が趣味です/i,
202
+ /趣味は([^。!?!?、,.]+)です/i,
203
+ // Chinese patterns
204
+ /(?:我玩|我做|我练习)([^,。!?!?,.]+)/i,
205
+ /(?:我的爱好是)([^,。!?!?,.]+)/i
206
+ ],
207
+ goals: [
208
+ // English patterns
209
+ /(?:i want to|i plan to|my goal is) ([^,.!?]+)/i,
210
+ /(?:i'm learning|i study) ([^,.!?]+)/i,
211
+ // French patterns
212
+ /(?:je veux|je vais|mon objectif est) ([^,.!?]+)/i,
213
+ /(?:j'apprends|j'étudie) ([^,.!?]+)/i,
214
+ // Spanish patterns
215
+ /(?:quiero|voy a|mi objetivo es) ([^,.!?]+)/i,
216
+ /(?:estoy aprendiendo|estudio) ([^,.!?]+)/i,
217
+ // Italian patterns
218
+ /(?:voglio|andrò a|il mio obiettivo è) ([^,.!?]+)/i,
219
+ /(?:sto imparando|studio) ([^,.!?]+)/i,
220
+ // German patterns
221
+ /(?:ich möchte|ich will|mein ziel ist) ([^,.!?]+)/i,
222
+ /(?:ich lerne|ich studiere) ([^,.!?]+)/i,
223
+ // Japanese patterns
224
+ /(?:私は)?(?:[^、。!?!?,.]+)したい/i,
225
+ /(?:学んでいる|勉強している) ([^。!?!?、,.]+)/i,
226
+ // Chinese patterns
227
+ /(?:我想|我要|我的目标是)([^,。!?!?,.]+)/i,
228
+ /(?:我在学习|我学习)([^,。!?!?,.]+)/i
229
+ ],
230
+ experiences: [
231
+ // English patterns
232
+ /we went to ([^,.!?]+)/i,
233
+ /we met (?:at|on|in) ([^,.!?]+)/i,
234
+ /our (?:first date|first kiss|trip|vacation) (?:was|was at|was on|was in|was to) ([^,.!?]+)/i,
235
+ /our anniversary (?:is|falls on|will be) ([^,.!?]+)/i,
236
+ /we moved in (?:together )?(?:on|in)?\s*([^,.!?]+)/i,
237
+ // French patterns
238
+ /on s'est rencontr[ée]s? (?:à|au|en|le) ([^,.!?]+)/i,
239
+ /on est all[ée]s? à ([^,.!?]+)/i,
240
+ /notre (?:premier rendez-vous|première sortie) (?:était|c'était) ([^,.!?]+)/i,
241
+ /notre anniversaire (?:est|c'est) ([^,.!?]+)/i,
242
+ /on a emménagé (?:ensemble\s*)?(?:le|en|à)\s*([^,.!?]+)/i,
243
+ // Spanish patterns
244
+ /nos conocimos (?:en|el|la) ([^,.!?]+)/i,
245
+ /fuimos a ([^,.!?]+)/i,
246
+ /nuestra (?:primera cita|primera salida) (?:fue|era) ([^,.!?]+)/i,
247
+ /nuestro aniversario (?:es|cae en|será) ([^,.!?]+)/i,
248
+ /nos mudamos (?:juntos\s*)?(?:el|en|a)\s*([^,.!?]+)/i,
249
+ // Italian patterns
250
+ /ci siamo conosciuti (?:a|al|in|il) ([^,.!?]+)/i,
251
+ /siamo andati a ([^,.!?]+)/i,
252
+ /il nostro (?:primo appuntamento|primo bacio|viaggio) (?:era|è stato) ([^,.!?]+)/i,
253
+ /il nostro anniversario (?:è|cade il|sarà) ([^,.!?]+)/i,
254
+ /ci siamo trasferiti (?:insieme\s*)?(?:il|in|a)\s*([^,.!?]+)/i,
255
+ // German patterns
256
+ /wir haben uns (?:in|am) ([^,.!?]+) kennengelernt/i,
257
+ /wir sind (?:nach|zu) ([^,.!?]+) (?:gegangen|gefahren)/i,
258
+ /unser (?:erstes date|erster kuss|urlaub) (?:war|fand statt) ([^,.!?]+)/i,
259
+ /unser jahrestag (?:ist|fällt auf|wird sein) ([^,.!?]+)/i,
260
+ /wir sind (?:zusammen )?eingezogen (?:am|im|in)\s*([^,.!?]+)/i,
261
+ // Japanese patterns
262
+ /私たちは([^、。!?!?,.]+)で出会った/i,
263
+ /一緒に([^、。!?!?,.]+)へ行った/i,
264
+ /私たちの記念日(?:は)?([^、。!?!?,.]+)/i,
265
+ /一緒に引っ越した(?:のは)?([^、。!?!?,.]+)/i,
266
+ // Chinese patterns
267
+ /我们在([^,。!?!?,.]+)认识/i,
268
+ /我们去了([^,。!?!?,.]+)/i,
269
+ /我们的纪念日是([^,。!?!?,.]+)/i,
270
+ /我们一起搬家(?:是在)?([^,。!?!?,.]+)/i
271
+ ],
272
+ important: [
273
+ // English patterns
274
+ /it's important (?:to remember|that) (.+)/i,
275
+ /please remember (.+)/i,
276
+ // French patterns
277
+ /c'est important (?:de se souvenir|que) (.+)/i,
278
+ /merci de te souvenir (.+)/i,
279
+ // Spanish patterns
280
+ /es importante (?:recordar|que) (.+)/i,
281
+ /por favor recuerda (.+)/i,
282
+ // Italian patterns
283
+ /è importante (?:ricordare|che) (.+)/i,
284
+ /per favore ricorda (.+)/i,
285
+ // German patterns
286
+ /es ist wichtig (?:zu erinnern|dass) (.+)/i,
287
+ /bitte erinnere dich an (.+)/i,
288
+ // Japanese patterns
289
+ /重要なのは(.+)です/i,
290
+ /覚えておいてほしいのは(.+)です/i,
291
+ // Chinese patterns
292
+ /重要的是(.+)/i,
293
+ /请记住(.+)/i
294
+ ]
295
+ };
296
+
297
+ // Performance optimization: pre-compile regex patterns
298
+ this.compiledPatterns = {};
299
+ this.initializeCompiledPatterns();
300
+ }
301
+
302
+ // Pre-compile all regex patterns for better performance
303
+ initializeCompiledPatterns() {
304
+ try {
305
+ for (const [category, patterns] of Object.entries(this.extractionPatterns)) {
306
+ this.compiledPatterns[category] = patterns.map(pattern => {
307
+ if (pattern instanceof RegExp) {
308
+ return pattern; // Already compiled
309
+ }
310
+ return new RegExp(pattern.source, pattern.flags);
311
+ });
312
+ }
313
+
314
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
315
+ const totalPatterns = Object.values(this.compiledPatterns).reduce((sum, arr) => sum + arr.length, 0);
316
+ console.log(`🚀 Pre-compiled ${totalPatterns} regex patterns for memory extraction`);
317
+ }
318
+ } catch (error) {
319
+ console.error("Error pre-compiling regex patterns:", error);
320
+ // Fallback: use original patterns
321
+ this.compiledPatterns = this.extractionPatterns;
322
+ }
323
+ }
324
+
325
+ // Utility method to get consistent creation timestamp
326
+ getCreationTimestamp(memory) {
327
+ // Prefer createdAt, fallback to timestamp for backward compatibility
328
+ return memory.createdAt || memory.timestamp || new Date();
329
+ }
330
+
331
+ // Utility method to calculate days since creation
332
+ getDaysSinceCreation(memory) {
333
+ const created = new Date(this.getCreationTimestamp(memory)).getTime();
334
+ return (Date.now() - created) / (1000 * 60 * 60 * 24);
335
+ }
336
+
337
+ async init() {
338
+ if (!this.db) {
339
+ console.warn("Database not available for memory system");
340
+ return;
341
+ }
342
+
343
+ try {
344
+ this.memoryEnabled = await this.db.getPreference("memorySystemEnabled", window.KIMI_CONFIG?.DEFAULTS?.MEMORY_SYSTEM_ENABLED ?? true);
345
+ this.selectedCharacter = await this.db.getSelectedCharacter();
346
+ await this.createMemoryTables();
347
+
348
+ // Legacy migrations disabled - uncomment if needed for old databases
349
+ // await this.migrateIncompatibleIDs();
350
+ // this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e));
351
+ } catch (error) {
352
+ console.error("Memory system initialization error:", error);
353
+ }
354
+ }
355
+
356
+ async createMemoryTables() {
357
+ // Ensure memory tables exist in database
358
+ if (!this.db.db.memories) {
359
+ console.warn("Memory table not found in database schema");
360
+ return;
361
+ }
362
+ }
363
+
364
+ // MEMORY EXTRACTION from conversation
365
+ async extractMemoryFromText(userText, kimiResponse = null) {
366
+ if (!this.memoryEnabled || !userText) return [];
367
+
368
+ // Ensure selectedCharacter is initialized
369
+ if (!this.selectedCharacter) {
370
+ this.selectedCharacter = this.db ? await this.db.getSelectedCharacter() : "kimi";
371
+ }
372
+
373
+ const extractedMemories = [];
374
+ const text = userText.toLowerCase();
375
+
376
+ // Memory extraction processing (debug info reduced for performance)
377
+
378
+ // Enhanced extraction with context awareness
379
+ const existingMemories = await this.getAllMemories();
380
+
381
+ // First, check for explicit memory requests
382
+ const explicitRequests = this.detectExplicitMemoryRequests(userText);
383
+ if (explicitRequests.length > 0) {
384
+ // Explicit memory requests detected
385
+ extractedMemories.push(...explicitRequests);
386
+ }
387
+
388
+ // Extract using pre-compiled patterns for better performance
389
+ const patternsToUse = this.compiledPatterns || this.extractionPatterns;
390
+ for (const [category, patterns] of Object.entries(patternsToUse)) {
391
+ for (const pattern of patterns) {
392
+ const match = text.match(pattern);
393
+ if (match && match[1]) {
394
+ const content = match[1].trim();
395
+
396
+ // Skip very short or generic content
397
+ if (content.length < this.config.minContentLength || this.isGenericContent(content)) {
398
+ continue;
399
+ }
400
+
401
+ // Check if this is a meaningful update to existing memory
402
+ const isUpdate = await this.isMemoryUpdate(category, content, existingMemories);
403
+
404
+ const memory = {
405
+ category: category,
406
+ type: "auto_extracted",
407
+ content: content,
408
+ sourceText: userText,
409
+ confidence: this.calculateExtractionConfidence(match, userText),
410
+ createdAt: new Date(), // Use createdAt consistently
411
+ character: this.selectedCharacter || "kimi", // Fallback protection
412
+ isUpdate: isUpdate
413
+ };
414
+
415
+ // Pattern match detected
416
+ extractedMemories.push(memory);
417
+ }
418
+ }
419
+ }
420
+
421
+ // Enhanced pattern detection for more natural expressions
422
+ const enhancedMemories = await this.detectNaturalExpressions(userText, existingMemories);
423
+ extractedMemories.push(...enhancedMemories);
424
+
425
+ // Save extracted memories with intelligent deduplication
426
+ const savedMemories = [];
427
+ for (const memory of extractedMemories) {
428
+ try {
429
+ console.log("💾 Saving memory:", memory.content);
430
+ const saved = await this.addMemory(memory);
431
+ if (saved) {
432
+ savedMemories.push(saved);
433
+ } else {
434
+ console.warn("⚠️ Memory was not saved (possibly filtered or merged):", memory.content);
435
+ }
436
+ } catch (error) {
437
+ console.error("❌ Failed to save memory:", {
438
+ content: memory.content,
439
+ category: memory.category,
440
+ error: error.message
441
+ });
442
+ // Continue processing other memories even if one fails
443
+ }
444
+ }
445
+
446
+ if (savedMemories.length > 0) {
447
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
448
+ console.log(`✅ Successfully extracted and saved ${savedMemories.length} memories`);
449
+ }
450
+ } else if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
451
+ console.log("📝 No memories extracted from this text");
452
+ }
453
+
454
+ return savedMemories;
455
+ }
456
+
457
+ // Detect explicit memory requests like "ajoute en mémoire que..."
458
+ detectExplicitMemoryRequests(text) {
459
+ const memories = [];
460
+ const lowerText = text.toLowerCase();
461
+
462
+ // French patterns for explicit memory requests
463
+ const frenchPatterns = [
464
+ /(?:ajoute?s?(?:r)?|retiens?|mémorise?s?|enregistre?s?|sauvegarde?s?)\s+(?:au|à|en|dans)\s+(?:la\s+|le\s+)?(?:système?\s+(?:de\s+)?)?mémoire\s+(?:que\s+)?(.+)/i,
465
+ /(?:peux-tu|pourrais-tu|veux-tu)?\s*(?:ajouter|retenir|mémoriser|enregistrer|sauvegarder)\s+(?:que\s+)?(.+)\s+(?:en|dans)\s+(?:la\s+|le\s+)?mémoire/i,
466
+ /(?:je\s+veux\s+que\s+tu\s+)?(?:retienne?s|mémorise?s|ajoute?s)\s+(?:que\s+)?(.+)/i
467
+ ];
468
+
469
+ // English patterns for explicit memory requests
470
+ const englishPatterns = [
471
+ /(?:add\s+to\s+memory|remember|memorize|save\s+(?:to\s+)?memory)\s+(?:that\s+)?(.+)/i,
472
+ /(?:can\s+you|could\s+you)?\s*(?:add|remember|memorize|save)\s+(?:that\s+)?(.+)\s+(?:to\s+|in\s+)?memory/i,
473
+ /(?:i\s+want\s+you\s+to\s+)?(?:remember|memorize|add)\s+(?:that\s+)?(.+)/i
474
+ ];
475
+
476
+ // Spanish explicit memory requests
477
+ const spanishPatterns = [
478
+ /(?:añade|agrega|recuerda|memoriza|guarda)\s+(?:en|a)\s+(?:la\s+)?memoria\s+(?:que\s+)?(.+)/i,
479
+ /(?:puedes|podrías)?\s*(?:añadir|agregar|recordar|memorizar|guardar)\s+(?:que\s+)?(.+)\s+(?:en|a)\s+(?:la\s+)?memoria/i,
480
+ /(?:quiero\s+que\s+)?(?:recuerdes|memorices|añadas)\s+(?:que\s+)?(.+)/i
481
+ ];
482
+
483
+ // Italian explicit memory requests
484
+ const italianPatterns = [
485
+ /(?:aggiungi|ricorda|memorizza|salva)\s+(?:nella|in)\s+memoria\s+(?:che\s+)?(.+)/i,
486
+ /(?:puoi|potresti)?\s*(?:aggiungere|ricordare|memorizzare|salvare)\s+(?:che\s+)?(.+)\s+(?:nella|in)\s+memoria/i,
487
+ /(?:voglio\s+che\s+)?(?:ricordi|memorizzi|aggiunga)\s+(?:che\s+)?(.+)/i
488
+ ];
489
+
490
+ // German explicit memory requests
491
+ const germanPatterns = [
492
+ /(?:füge|merke|speichere)\s+(?:es\s+)?(?:in|zur)\s+?gedächtnis|speicher\s+(?:dass\s+)?(.+)/i,
493
+ /(?:kannst\s+du|könntest\s+du)?\s*(?:hinzufügen|merken|speichern)\s+(?:dass\s+)?(.+)\s+(?:in|zum)\s+(?:gedächtnis|speicher)/i,
494
+ /(?:ich\s+möchte\s+dass\s+du)\s*(?:merkst|speicherst|hinzufügst)\s+(?:dass\s+)?(.+)/i
495
+ ];
496
+
497
+ // Japanese explicit memory requests
498
+ const japanesePatterns = [/記憶に(?:追加|保存|覚えて)(?:して)?(?:ほしい|ください)?(?:、)?(.+)/i, /(?:覚えて|記憶して)(?:ほしい|ください)?(?:、)?(.+)/i];
499
+
500
+ // Chinese explicit memory requests
501
+ const chinesePatterns = [/把(.+)记在(?:记忆|内存|记忆库)里/i, /(?:请)?记住(?:这件事|这个|以下)?(.+)/i, /保存到记忆(?:里|中)(?:的是)?(.+)/i];
502
+
503
+ const allPatterns = [
504
+ ...frenchPatterns,
505
+ ...englishPatterns,
506
+ ...spanishPatterns,
507
+ ...italianPatterns,
508
+ ...germanPatterns,
509
+ ...japanesePatterns,
510
+ ...chinesePatterns
511
+ ];
512
+
513
+ for (const pattern of allPatterns) {
514
+ const match = lowerText.match(pattern);
515
+ if (match && match[1]) {
516
+ const content = match[1].trim();
517
+
518
+ // Determine category based on content
519
+ const category = this.categorizeExplicitMemory(content);
520
+
521
+ memories.push({
522
+ category: category,
523
+ type: "explicit_request",
524
+ content: content,
525
+ sourceText: text,
526
+ confidence: 1.0, // High confidence for explicit requests
527
+ timestamp: new Date(),
528
+ character: this.selectedCharacter,
529
+ isUpdate: false
530
+ });
531
+ break; // Only take the first match to avoid duplicates
532
+ }
533
+ }
534
+
535
+ return memories;
536
+ }
537
+
538
+ // Categorize explicit memory based on content analysis
539
+ categorizeExplicitMemory(content) {
540
+ const lowerContent = content.toLowerCase();
541
+
542
+ // Preference indicators
543
+ if (
544
+ lowerContent.includes("j'aime") ||
545
+ lowerContent.includes("i like") ||
546
+ lowerContent.includes("j'adore") ||
547
+ lowerContent.includes("i love") ||
548
+ lowerContent.includes("je préfère") ||
549
+ lowerContent.includes("i prefer") ||
550
+ lowerContent.includes("je déteste") ||
551
+ lowerContent.includes("i hate")
552
+ ) {
553
+ return "preferences";
554
+ }
555
+
556
+ // Personal information indicators
557
+ if (
558
+ lowerContent.includes("je m'appelle") ||
559
+ lowerContent.includes("my name is") ||
560
+ (lowerContent.includes("j'ai") && lowerContent.includes("ans")) ||
561
+ lowerContent.includes("years old") ||
562
+ lowerContent.includes("j'habite") ||
563
+ lowerContent.includes("i live")
564
+ ) {
565
+ return "personal";
566
+ }
567
+
568
+ // Relationship indicators
569
+ if (
570
+ lowerContent.includes("ma femme") ||
571
+ lowerContent.includes("my wife") ||
572
+ lowerContent.includes("mon mari") ||
573
+ lowerContent.includes("my husband") ||
574
+ lowerContent.includes("mon ami") ||
575
+ lowerContent.includes("my friend") ||
576
+ lowerContent.includes("ma famille") ||
577
+ lowerContent.includes("my family")
578
+ ) {
579
+ return "relationships";
580
+ }
581
+
582
+ // Activity indicators
583
+ if (
584
+ lowerContent.includes("je joue") ||
585
+ lowerContent.includes("i play") ||
586
+ lowerContent.includes("je pratique") ||
587
+ lowerContent.includes("i practice") ||
588
+ lowerContent.includes("mon hobby") ||
589
+ lowerContent.includes("my hobby")
590
+ ) {
591
+ return "activities";
592
+ }
593
+
594
+ // Goal indicators
595
+ if (
596
+ lowerContent.includes("je veux") ||
597
+ lowerContent.includes("i want") ||
598
+ lowerContent.includes("mon objectif") ||
599
+ lowerContent.includes("my goal") ||
600
+ lowerContent.includes("j'apprends") ||
601
+ lowerContent.includes("i'm learning")
602
+ ) {
603
+ return "goals";
604
+ }
605
+
606
+ // Default to preferences for most explicit requests
607
+ return "preferences";
608
+ }
609
+
610
+ // Check if content is too generic to be useful
611
+ isGenericContent(content) {
612
+ const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"];
613
+ return genericWords.includes(content.toLowerCase()) || content.length < this.config.minContentLength;
614
+ }
615
+
616
+ // Calculate confidence based on context and pattern strength
617
+ calculateExtractionConfidence(match, fullText) {
618
+ let confidence = this.config.confidence.base; // Base confidence from config
619
+
620
+ // Boost confidence for explicit statements
621
+ const lower = fullText.toLowerCase();
622
+ if (
623
+ lower.includes("my name is") ||
624
+ lower.includes("i am called") ||
625
+ lower.includes("je m'appelle") ||
626
+ lower.includes("mon nom est") ||
627
+ lower.includes("je me prénomme") ||
628
+ lower.includes("je me nomme") ||
629
+ lower.includes("me llamo") ||
630
+ lower.includes("mi nombre es") ||
631
+ lower.includes("mi chiamo") ||
632
+ lower.includes("il mio nome è") ||
633
+ lower.includes("ich heiße") ||
634
+ lower.includes("mein name ist") ||
635
+ lower.includes("と申します") ||
636
+ lower.includes("私の名前は") ||
637
+ lower.includes("我叫") ||
638
+ lower.includes("我的名字是")
639
+ ) {
640
+ confidence += this.config.confidence.bonusForExplicitStatement;
641
+ }
642
+
643
+ // Boost for longer, more specific content
644
+ if (match[1] && match[1].trim().length > this.config.longContentThreshold) {
645
+ confidence += this.config.confidence.bonusForLongContent;
646
+ }
647
+
648
+ // Reduce confidence for uncertain language
649
+ if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) {
650
+ confidence -= this.config.confidence.penaltyForUncertainty;
651
+ }
652
+
653
+ return Math.min(this.config.confidence.max, Math.max(this.config.confidence.min, confidence));
654
+ }
655
+
656
+ // Generate a short title (2-5 words max) from content for auto-extracted memories
657
+ generateTitleFromContent(content) {
658
+ if (!content || typeof content !== "string") return "";
659
+ // Remove surrounding punctuation and collapse whitespace
660
+ const cleaned = content
661
+ .replace(/[\n\r]+/g, " ")
662
+ .replace(/["'“”‘’–—:;()\[\]{}]+/g, "")
663
+ .trim();
664
+ const words = cleaned.split(/\s+/).filter(Boolean);
665
+
666
+ if (words.length === 0) return "";
667
+ // Prefer 3 words when available, minimum 2 when possible, maximum 5
668
+ let take;
669
+ if (words.length >= this.config.titleWordCount.preferred) take = this.config.titleWordCount.preferred;
670
+ else take = words.length; // 1 or 2
671
+ take = Math.min(this.config.titleWordCount.max, Math.max(this.config.titleWordCount.min, take));
672
+
673
+ const slice = words.slice(0, take);
674
+ // Capitalize first word for nicer title
675
+ slice[0] = slice[0].charAt(0).toUpperCase() + slice[0].slice(1);
676
+ return slice.join(" ");
677
+ }
678
+
679
+ // Check if this is an update to existing memory rather than new info
680
+ async isMemoryUpdate(category, content, existingMemories) {
681
+ const categoryMemories = existingMemories.filter(m => m.category === category);
682
+
683
+ for (const memory of categoryMemories) {
684
+ const similarity = this.calculateSimilarity(memory.content, content);
685
+ if (similarity > this.config.similarity.update) {
686
+ // Lower threshold for updates
687
+ return true;
688
+ }
689
+ }
690
+
691
+ return false;
692
+ }
693
+
694
+ // Detect natural expressions that patterns might miss
695
+ async detectNaturalExpressions(text, existingMemories) {
696
+ const naturalMemories = [];
697
+ const lowerText = text.toLowerCase();
698
+
699
+ // Detect name mentions in natural context (multilingual)
700
+ const namePatterns = [
701
+ // English
702
+ /call me (\w+)/i,
703
+ /(\w+) here[,.]?/i,
704
+ /this is (\w+)/i,
705
+ /(\w+) speaking/i,
706
+ // French
707
+ /appelle-?moi (\w+)/i,
708
+ /on m'appelle (\w+)/i,
709
+ /c'est (\w+)/i,
710
+ // Spanish
711
+ /llámame (\w+)/i,
712
+ /me llaman (\w+)/i,
713
+ /soy (\w+)/i,
714
+ // Italian
715
+ /chiamami (\w+)/i,
716
+ /mi chiamano (\w+)/i,
717
+ /sono (\w+)/i,
718
+ // German
719
+ /nenn mich (\w+)/i,
720
+ /man nennt mich (\w+)/i,
721
+ /ich bin (\w+)/i,
722
+ // Japanese
723
+ /(?:私は)?(\w+)です/i,
724
+ // Chinese
725
+ /我是(\w+)/i,
726
+ /叫我(\w+)/i
727
+ ];
728
+
729
+ for (const pattern of namePatterns) {
730
+ const match = lowerText.match(pattern);
731
+ if (match && match[1] && match[1].length > 1) {
732
+ const name = match[1].trim();
733
+
734
+ // Skip if too generic
735
+ if (!this.isGenericContent(name) && !this.isCommonWord(name)) {
736
+ naturalMemories.push({
737
+ category: "personal",
738
+ type: "auto_extracted",
739
+ content: name,
740
+ sourceText: text,
741
+ confidence: 0.7,
742
+ createdAt: new Date(), // Use createdAt consistently
743
+ character: this.selectedCharacter || "kimi" // Fallback protection
744
+ });
745
+ }
746
+ }
747
+ }
748
+
749
+ return naturalMemories;
750
+ }
751
+
752
+ // Check if word is too common to be a name
753
+ isCommonWord(word, language = "en") {
754
+ // Use existing constants if available
755
+ if (window.KIMI_COMMON_WORDS && window.KIMI_COMMON_WORDS[language]) {
756
+ return window.KIMI_COMMON_WORDS[language].includes(word.toLowerCase());
757
+ }
758
+
759
+ // Fallback to original English list
760
+ const commonWords = [
761
+ "the",
762
+ "and",
763
+ "for",
764
+ "are",
765
+ "but",
766
+ "not",
767
+ "you",
768
+ "all",
769
+ "can",
770
+ "had",
771
+ "her",
772
+ "was",
773
+ "one",
774
+ "our",
775
+ "out",
776
+ "day",
777
+ "get",
778
+ "has",
779
+ "him",
780
+ "his",
781
+ "how",
782
+ "man",
783
+ "new",
784
+ "now",
785
+ "old",
786
+ "see",
787
+ "two",
788
+ "way",
789
+ "who",
790
+ "boy",
791
+ "did",
792
+ "its",
793
+ "let",
794
+ "put",
795
+ "say",
796
+ "she",
797
+ "too",
798
+ "use"
799
+ ];
800
+ return commonWords.includes(word.toLowerCase());
801
+ }
802
+
803
+ // MANUAL MEMORY MANAGEMENT
804
+ async addMemory(memoryData) {
805
+ if (!this.db || !this.memoryEnabled) return;
806
+
807
+ try {
808
+ // Check for duplicates with intelligent merging
809
+ const existing = await this.findSimilarMemory(memoryData);
810
+ if (existing) {
811
+ // Intelligent merge strategy
812
+ return await this.mergeMemories(existing, memoryData);
813
+ }
814
+
815
+ // Add memory with metadata (let DB auto-generate ID)
816
+ const now = new Date();
817
+ const memory = {
818
+ category: memoryData.category || "personal",
819
+ type: memoryData.type || "manual",
820
+ content: memoryData.content,
821
+ // precomputed keywords for faster matching and relevance
822
+ keywords: this.deriveKeywords(memoryData.content),
823
+ // Title: use provided title or generate for auto_extracted
824
+ title:
825
+ memoryData.title && typeof memoryData.title === "string"
826
+ ? memoryData.title
827
+ : memoryData.type === "auto_extracted"
828
+ ? this.generateTitleFromContent(memoryData.content)
829
+ : "",
830
+ sourceText: memoryData.sourceText || "",
831
+ confidence: memoryData.confidence || 1.0,
832
+ createdAt: memoryData.createdAt || memoryData.timestamp || now, // Unified timestamp handling
833
+ character: memoryData.character || this.selectedCharacter || "kimi", // Fallback protection
834
+ isActive: true,
835
+ tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])],
836
+ lastModified: now,
837
+ lastAccess: now,
838
+ accessCount: 0,
839
+ importance: this.calculateImportance(memoryData)
840
+ };
841
+
842
+ if (this.db.db.memories) {
843
+ const id = await this.db.db.memories.add(memory);
844
+ memory.id = id; // Store the auto-generated ID
845
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
846
+ console.log(`Memory added with ID: ${id}`);
847
+ }
848
+ }
849
+
850
+ // Cleanup old memories if we exceed limit
851
+ await this.cleanupOldMemories();
852
+
853
+ // Notify LLM system to refresh context
854
+ this.notifyLLMContextUpdate();
855
+
856
+ return memory;
857
+ } catch (error) {
858
+ console.error("Error adding memory:", error);
859
+ return null; // Return null instead of undefined for clearer error handling
860
+ }
861
+ }
862
+
863
+ // Intelligent memory merging
864
+ async mergeMemories(existingMemory, newMemoryData) {
865
+ try {
866
+ // Determine merge strategy based on content and confidence
867
+ const strategy = this.determineMergeStrategy(existingMemory, newMemoryData);
868
+
869
+ let mergedContent = existingMemory.content;
870
+ let mergedConfidence = existingMemory.confidence;
871
+ let mergedTags = [...(existingMemory.tags || [])];
872
+
873
+ switch (strategy) {
874
+ case "update_content":
875
+ // New information is more confident/recent
876
+ mergedContent = newMemoryData.content;
877
+ mergedConfidence = Math.max(existingMemory.confidence, newMemoryData.confidence || 0.8);
878
+ break;
879
+
880
+ case "merge_content":
881
+ // Combine information intelligently
882
+ if (existingMemory.category === "personal" && this.areRelatedNames(existingMemory.content, newMemoryData.content)) {
883
+ // Handle name variants
884
+ mergedContent = this.mergeNames(existingMemory.content, newMemoryData.content);
885
+ } else {
886
+ // General merge - keep most specific
887
+ mergedContent = newMemoryData.content.length > existingMemory.content.length ? newMemoryData.content : existingMemory.content;
888
+ }
889
+ mergedConfidence = (existingMemory.confidence + (newMemoryData.confidence || 0.8)) / 2;
890
+ break;
891
+
892
+ case "add_variant":
893
+ // Store as variant/alias
894
+ mergedTags.push(`alias:${newMemoryData.content}`);
895
+ break;
896
+
897
+ case "boost_confidence":
898
+ // Same content, boost confidence
899
+ mergedConfidence = Math.min(1.0, existingMemory.confidence + 0.1);
900
+ break;
901
+ }
902
+
903
+ // Update existing memory
904
+ const updatedMemory = {
905
+ ...existingMemory,
906
+ content: mergedContent,
907
+ confidence: mergedConfidence,
908
+ tags: [...new Set([...mergedTags, ...this.deriveMemoryTags(newMemoryData)])], // Remove duplicates
909
+ lastModified: new Date(),
910
+ accessCount: (existingMemory.accessCount || 0) + 1,
911
+ importance: Math.max(existingMemory.importance || 0.5, this.calculateImportance(newMemoryData))
912
+ };
913
+
914
+ await this.updateMemory(existingMemory.id, updatedMemory);
915
+ return updatedMemory;
916
+ } catch (error) {
917
+ console.error("Error merging memories:", error);
918
+ return existingMemory;
919
+ }
920
+ }
921
+
922
+ // Simplified memory merge strategy determination
923
+ determineMergeStrategy(existing, newData) {
924
+ const similarity = this.calculateSimilarity(existing.content, newData.content);
925
+ const newConfidence = newData.confidence || this.config.confidence.base;
926
+ const existingConfidence = existing.confidence || this.config.confidence.base;
927
+
928
+ // Very high similarity (>90%) - boost confidence if new is more confident
929
+ if (similarity > this.config.similarity.veryHigh) {
930
+ return newConfidence > existingConfidence ? "boost_confidence" : "merge_content";
931
+ }
932
+
933
+ // High similarity (>70%) - decide based on content length and specificity
934
+ if (similarity > this.config.similarity.preferences) {
935
+ // If new content is significantly longer (50% more), it's likely more detailed
936
+ if (newData.content.length > existing.content.length * 1.5) {
937
+ return "update_content";
938
+ }
939
+ // If existing is longer, merge to preserve information
940
+ return "merge_content";
941
+ }
942
+
943
+ // For personal names, handle as variants if they're related
944
+ if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) {
945
+ return "add_variant";
946
+ }
947
+
948
+ // Default strategy for moderate similarity
949
+ return "merge_content";
950
+ }
951
+
952
+ // Merge name variants intelligently
953
+ mergeNames(name1, name2) {
954
+ // Keep the longest/most formal version as primary
955
+ if (name1.length > name2.length) {
956
+ return name1;
957
+ } else if (name2.length > name1.length) {
958
+ return name2;
959
+ }
960
+
961
+ // If same length, keep the first one
962
+ return name1;
963
+ }
964
+
965
+ // Calculate importance of memory for prioritization
966
+ calculateImportance(memoryData) {
967
+ let importance = 0.5; // Base importance
968
+
969
+ // Category base weights
970
+ const categoryWeights = {
971
+ important: 1.0,
972
+ personal: 0.9,
973
+ relationships: 0.85,
974
+ goals: 0.75,
975
+ experiences: 0.65,
976
+ preferences: 0.6,
977
+ activities: 0.5
978
+ };
979
+
980
+ importance = categoryWeights[memoryData.category] || 0.5;
981
+
982
+ const content = (memoryData.content || "").toLowerCase();
983
+ const tags = new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)]);
984
+
985
+ // Heuristic boosts for meaningful relationship milestones and commitments
986
+ const milestoneTags = [
987
+ "relationship:first_meet",
988
+ "relationship:first_date",
989
+ "relationship:first_kiss",
990
+ "relationship:anniversary",
991
+ "relationship:moved_in",
992
+ "relationship:engaged",
993
+ "relationship:married",
994
+ "relationship:breakup"
995
+ ];
996
+ if ([...tags].some(t => milestoneTags.includes(t))) importance += 0.15;
997
+
998
+ // Boundaries and consent are high priority to remember
999
+ if ([...tags].some(t => t.startsWith("boundary:"))) importance += 0.15;
1000
+
1001
+ // Preferences tied to strong like/dislike
1002
+ if (content.includes("i love") || content.includes("j'adore") || content.includes("i hate") || content.includes("je déteste")) {
1003
+ importance += 0.05;
1004
+ }
1005
+
1006
+ // Temporal cues: future commitments or dates
1007
+ if (/(\bnext\b|\btomorrow\b|\bce soir\b|\bdemain\b|\bmañana\b|\bdomani\b|\bmorgen\b)/i.test(content)) {
1008
+ importance += 0.05;
1009
+ }
1010
+
1011
+ // Longer details and high confidence
1012
+ if (memoryData.content && memoryData.content.length > this.config.longContentThreshold) importance += this.config.importance.bonuses.longContent;
1013
+ if (memoryData.confidence && memoryData.confidence > 0.9) importance += this.config.importance.bonuses.highConfidence;
1014
+
1015
+ // Round to two decimals to avoid floating point artifacts
1016
+ return Math.min(1.0, Math.round(importance * 100) / 100);
1017
+ }
1018
+
1019
+ // Derive semantic tags from memory content to assist prioritization and merging
1020
+ deriveMemoryTags(memoryData) {
1021
+ const tags = [];
1022
+ const text = (memoryData.content || "").toLowerCase();
1023
+ const category = memoryData.category || "";
1024
+
1025
+ // Relationship status and milestones
1026
+ if (/(single|célibataire|soltero|single|ledig)/i.test(text)) tags.push("relationship:status_single");
1027
+ if (/(in a relationship|en couple|together|ensemble|pareja|coppia|beziehung)/i.test(text)) tags.push("relationship:status_in_relationship");
1028
+ if (/(engaged|fiancé|fiancée|promis|promised|verlobt)/i.test(text)) tags.push("relationship:status_engaged");
1029
+ if (/(married|marié|mariée|casado|sposato|verheiratet)/i.test(text)) tags.push("relationship:status_married");
1030
+ if (/(broke up|rupture|separated|separado|separati|getrennt)/i.test(text)) tags.push("relationship:breakup");
1031
+ if (/(first date|premier rendez-vous|primera cita|primo appuntamento)/i.test(text)) tags.push("relationship:first_date");
1032
+ if (/(first kiss|premier baiser|primer beso|primo bacio)/i.test(text)) tags.push("relationship:first_kiss");
1033
+ if (/(anniversary|anniversaire|aniversario|anniversario|jahrestag)/i.test(text)) tags.push("relationship:anniversary");
1034
+ if (/(moved in together|emménagé ensemble|mudamos juntos|trasferiti insieme|zusammen eingezogen)/i.test(text)) tags.push("relationship:moved_in");
1035
+ if (/(met at|rencontré à|conocimos en|conosciuti a|kennengelernt)/i.test(text)) tags.push("relationship:first_meet");
1036
+
1037
+ // Boundaries and consent (keep generic and non-graphic)
1038
+ if (/(i don't like|je n'aime pas|no me gusta|non mi piace|ich mag nicht)\s+[^,.!?]+/i.test(text)) tags.push("boundary:dislike");
1039
+ if (/(i prefer|je préfère|prefiero|preferisco|ich bevorzuge)\s+[^,.!?]+/i.test(text)) tags.push("boundary:preference");
1040
+ if (/(no|pas)\s+(?:kissing|baiser|beso|bacio|küssen)/i.test(text)) tags.push("boundary:limit");
1041
+ if (/(consent|consentement|consentimiento|consenso|einwilligung)/i.test(text)) tags.push("boundary:consent");
1042
+
1043
+ // Time-related tags
1044
+ if (/(today|ce jour|hoy|oggi|heute|今日)/i.test(text)) tags.push("time:today");
1045
+ if (/(tomorrow|demain|mañana|domani|morgen|明日)/i.test(text)) tags.push("time:tomorrow");
1046
+ if (/(next week|semaine prochaine|la próxima semana|la prossima settimana|nächste woche)/i.test(text)) tags.push("time:next_week");
1047
+
1048
+ // Category-specific hints
1049
+ if (category === "preferences") tags.push("type:preference");
1050
+ if (category === "personal") tags.push("type:personal");
1051
+ if (category === "relationships") tags.push("type:relationship");
1052
+ if (category === "experiences") tags.push("type:experience");
1053
+ if (category === "goals") tags.push("type:goal");
1054
+ if (category === "important") tags.push("type:important");
1055
+
1056
+ return tags;
1057
+ }
1058
+
1059
+ async updateMemory(memoryId, updateData) {
1060
+ if (!this.db) return false;
1061
+
1062
+ try {
1063
+ // Ensure memoryId is the correct type
1064
+ const numericId = typeof memoryId === "string" ? parseInt(memoryId) : memoryId;
1065
+
1066
+ // Vérifier d'abord que la mémoire existe
1067
+ const existingMemory = await this.db.db.memories.get(numericId);
1068
+ if (!existingMemory) {
1069
+ console.error(`❌ Memory with ID ${numericId} not found in database`);
1070
+ return false;
1071
+ }
1072
+
1073
+ console.log(`🔄 Updating memory ${numericId}:`, { existing: existingMemory, update: updateData });
1074
+
1075
+ const update = {
1076
+ ...updateData,
1077
+ lastModified: new Date()
1078
+ };
1079
+
1080
+ if (this.db.db.memories) {
1081
+ const result = await this.db.db.memories.update(numericId, update);
1082
+
1083
+ console.log(`Memory update result for ID ${numericId}:`, result);
1084
+
1085
+ if (result > 0) {
1086
+ console.log("✅ Memory updated successfully");
1087
+ // Notify LLM system to refresh context
1088
+ this.notifyLLMContextUpdate();
1089
+ return true;
1090
+ } else {
1091
+ console.error("❌ Memory update failed - no rows affected");
1092
+ return false;
1093
+ }
1094
+ }
1095
+ } catch (error) {
1096
+ console.error("Error updating memory:", error, { memoryId, updateData });
1097
+ return false;
1098
+ }
1099
+ }
1100
+
1101
+ async deleteMemory(memoryId) {
1102
+ if (!this.db) return false;
1103
+
1104
+ try {
1105
+ // Ensure memoryId is the correct type
1106
+ const numericId = typeof memoryId === "string" ? parseInt(memoryId) : memoryId;
1107
+
1108
+ if (this.db.db.memories) {
1109
+ const result = await this.db.db.memories.delete(numericId);
1110
+
1111
+ console.log(`Memory delete result for ID ${numericId}:`, result);
1112
+
1113
+ // Notify LLM system to refresh context
1114
+ if (result) {
1115
+ this.notifyLLMContextUpdate();
1116
+ }
1117
+
1118
+ return result;
1119
+ }
1120
+ } catch (error) {
1121
+ console.error("Error deleting memory:", error, { memoryId });
1122
+ return false;
1123
+ }
1124
+ }
1125
+
1126
+ notifyLLMContextUpdate() {
1127
+ // Debounce context updates to avoid excessive calls
1128
+ if (this.contextUpdateTimeout) {
1129
+ clearTimeout(this.contextUpdateTimeout);
1130
+ }
1131
+
1132
+ this.contextUpdateTimeout = setTimeout(() => {
1133
+ if (window.kimiLLM && typeof window.kimiLLM.refreshMemoryContext === "function") {
1134
+ window.kimiLLM.refreshMemoryContext();
1135
+ }
1136
+ }, 500);
1137
+ }
1138
+
1139
+ // Add cleanup method for memory system
1140
+ cleanup() {
1141
+ if (this.contextUpdateTimeout) {
1142
+ clearTimeout(this.contextUpdateTimeout);
1143
+ this.contextUpdateTimeout = null;
1144
+ }
1145
+
1146
+ // Clear caches to prevent memory leaks
1147
+ if (this.keywordCache) {
1148
+ this.keywordCache.clear();
1149
+ }
1150
+
1151
+ // Reset stats arrays to prevent accumulation
1152
+ if (this.queryStats) {
1153
+ this.queryStats.extractionTime.length = 0;
1154
+ this.queryStats.addMemoryTime.length = 0;
1155
+ this.queryStats.retrievalTime.length = 0;
1156
+ }
1157
+ }
1158
+
1159
+ async getMemoriesByCategory(category, character = null) {
1160
+ if (!this.db) return [];
1161
+
1162
+ try {
1163
+ character = character || this.selectedCharacter || "kimi"; // Unified fallback
1164
+
1165
+ if (this.db.db.memories) {
1166
+ const memories = await this.db.db.memories
1167
+ .where("[character+category]")
1168
+ .equals([character, category])
1169
+ .and(m => m.isActive)
1170
+ .reverse()
1171
+ .sortBy("timestamp");
1172
+
1173
+ // Update lastAccess/accessCount for top results to improve prioritization
1174
+ this._touchMemories(memories, 10).catch(() => {});
1175
+ return memories;
1176
+ }
1177
+ } catch (error) {
1178
+ console.error("Error getting memories by category:", error);
1179
+ return [];
1180
+ }
1181
+ }
1182
+
1183
+ async getAllMemories(character = null) {
1184
+ if (!this.db) return [];
1185
+
1186
+ try {
1187
+ character = character || this.selectedCharacter || "kimi";
1188
+
1189
+ if (this.db.db.memories) {
1190
+ // Primary IndexedDB (Dexie) sort still leverages the existing 'timestamp' index for performance.
1191
+ // Then we apply a stable in-memory reorder using canonical creation time (createdAt fallback timestamp)
1192
+ // to unify ordering semantics without breaking older databases lacking createdAt originally.
1193
+ const memories = await this.db.db.memories
1194
+ .where("character")
1195
+ .equals(character)
1196
+ .filter(memory => memory.isActive !== false) // Include records without isActive field (legacy)
1197
+ .reverse()
1198
+ .sortBy("timestamp");
1199
+
1200
+ // Backward-compatible canonical ordering: most recent first by getCreationTimestamp
1201
+ // (Only if >1 entry to avoid needless array ops.)
1202
+ if (memories.length > 1) {
1203
+ memories.sort((a, b) => {
1204
+ const ca = new Date(this.getCreationTimestamp(a)).getTime();
1205
+ const cb = new Date(this.getCreationTimestamp(b)).getTime();
1206
+ return cb - ca; // descending (newest first)
1207
+ });
1208
+ }
1209
+
1210
+ if (window.KIMI_DEBUG_MEMORIES) {
1211
+ console.log(`Retrieved ${memories.length} memories for character: ${character}`);
1212
+ }
1213
+
1214
+ // Touch top memories to update access metrics
1215
+ this._touchMemories(memories, 10).catch(() => {});
1216
+ return memories;
1217
+ }
1218
+ } catch (error) {
1219
+ console.error("Error getting all memories:", error);
1220
+ return [];
1221
+ }
1222
+ }
1223
+
1224
+ async findSimilarMemory(memoryData) {
1225
+ if (!this.db) return null;
1226
+
1227
+ try {
1228
+ const memories = await this.getMemoriesByCategory(memoryData.category);
1229
+
1230
+ // Precompute keywords for new memory
1231
+ const newKeys = this.deriveKeywords(memoryData.content || "");
1232
+
1233
+ // Enhanced similarity check with multiple criteria
1234
+ for (const memory of memories) {
1235
+ // Prefilter by keyword overlap to reduce false positives and improve perf
1236
+ const memKeys = memory.keywords || this.deriveKeywords(memory.content || "");
1237
+ const overlap = newKeys.filter(k => memKeys.includes(k)).length;
1238
+ if (newKeys.length > 0 && overlap === 0) continue; // no shared keywords -> likely different
1239
+
1240
+ const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
1241
+
1242
+ // Different thresholds based on category
1243
+ const threshold = this.config.similarity[memoryData.category] || this.config.similarity.default;
1244
+
1245
+ if (contentSimilarity > threshold) {
1246
+ return memory;
1247
+ }
1248
+
1249
+ // Special handling for names (check if one is contained in the other)
1250
+ if (memoryData.category === "personal" && this.areRelatedNames(memory.content, memoryData.content)) {
1251
+ return memory;
1252
+ }
1253
+ }
1254
+ } catch (error) {
1255
+ console.error("Error finding similar memory:", error);
1256
+ }
1257
+
1258
+ return null;
1259
+ }
1260
+
1261
+ // Check if two names are related (nicknames, variants, etc.)
1262
+ areRelatedNames(name1, name2) {
1263
+ const n1 = name1.toLowerCase().trim();
1264
+ const n2 = name2.toLowerCase().trim();
1265
+
1266
+ // Exact match
1267
+ if (n1 === n2) return true;
1268
+
1269
+ // One contains the other (Jean-Pierre vs Jean)
1270
+ if (n1.includes(n2) || n2.includes(n1)) return true;
1271
+
1272
+ // Common nickname patterns
1273
+ const nicknames = {
1274
+ jean: ["jp", "jeannot"],
1275
+ pierre: ["pete", "pietro"],
1276
+ marie: ["mary", "maria"],
1277
+ michael: ["mike", "mick"],
1278
+ william: ["bill", "will", "willy"],
1279
+ robert: ["bob", "rob", "bobby"],
1280
+ richard: ["rick", "dick", "richie"],
1281
+ thomas: ["tom", "tommy"],
1282
+ christopher: ["chris", "kit"],
1283
+ anthony: ["tony", "ant"]
1284
+ };
1285
+
1286
+ for (const [full, nicks] of Object.entries(nicknames)) {
1287
+ if ((n1 === full && nicks.includes(n2)) || (n2 === full && nicks.includes(n1))) {
1288
+ return true;
1289
+ }
1290
+ }
1291
+
1292
+ return false;
1293
+ }
1294
+
1295
+ calculateSimilarity(text1, text2) {
1296
+ // Enhanced similarity calculation
1297
+ const words1 = text1
1298
+ .toLowerCase()
1299
+ .split(/\s+/)
1300
+ .filter(w => w.length > 2);
1301
+ const words2 = text2
1302
+ .toLowerCase()
1303
+ .split(/\s+/)
1304
+ .filter(w => w.length > 2);
1305
+
1306
+ if (words1.length === 0 || words2.length === 0) {
1307
+ return text1.toLowerCase() === text2.toLowerCase() ? 1 : 0;
1308
+ }
1309
+
1310
+ const intersection = words1.filter(word => words2.includes(word));
1311
+ const union = [...new Set([...words1, ...words2])];
1312
+
1313
+ let similarity = intersection.length / union.length;
1314
+
1315
+ // Boost similarity for exact substring matches
1316
+ if (text1.toLowerCase().includes(text2.toLowerCase()) || text2.toLowerCase().includes(text1.toLowerCase())) {
1317
+ similarity += 0.2;
1318
+ }
1319
+
1320
+ return Math.min(1.0, similarity);
1321
+ }
1322
+
1323
+ // Derive a set of normalized keywords from text
1324
+ deriveKeywords(text) {
1325
+ if (!text || typeof text !== "string") return [];
1326
+ return [
1327
+ ...new Set(
1328
+ text
1329
+ .toLowerCase()
1330
+ .replace(/[\p{P}\p{S}]/gu, " ")
1331
+ .split(/\s+/)
1332
+ .filter(w => w.length > 2 && !this.isCommonWordSafe(w))
1333
+ )
1334
+ ];
1335
+ }
1336
+
1337
+ // Safe wrapper for isCommonWord to avoid undefined function errors
1338
+ isCommonWordSafe(word, language = "en") {
1339
+ const cacheKey = `${word.toLowerCase()}_${language}`;
1340
+
1341
+ // Check cache first
1342
+ if (this.keywordCache.has(cacheKey)) {
1343
+ this.keywordCacheHits++;
1344
+ return this.keywordCache.get(cacheKey);
1345
+ }
1346
+
1347
+ // Cache miss - compute the result
1348
+ this.keywordCacheMisses++;
1349
+ let isCommon = false;
1350
+
1351
+ try {
1352
+ isCommon = typeof this.isCommonWord === "function" ? this.isCommonWord(word, language) : false;
1353
+ } catch (error) {
1354
+ console.warn("Error checking common word:", error);
1355
+ isCommon = false;
1356
+ }
1357
+
1358
+ // Add to cache with LRU eviction
1359
+ if (this.keywordCache.size >= this.keywordCacheSize) {
1360
+ // Simple LRU: remove oldest entry (first in Map)
1361
+ const firstKey = this.keywordCache.keys().next().value;
1362
+ this.keywordCache.delete(firstKey);
1363
+ }
1364
+
1365
+ this.keywordCache.set(cacheKey, isCommon);
1366
+ return isCommon;
1367
+ }
1368
+
1369
+ // Get cache statistics for debugging
1370
+ getKeywordCacheStats() {
1371
+ const total = this.keywordCacheHits + this.keywordCacheMisses;
1372
+ return {
1373
+ size: this.keywordCache.size,
1374
+ hits: this.keywordCacheHits,
1375
+ misses: this.keywordCacheMisses,
1376
+ hitRate: total > 0 ? ((this.keywordCacheHits / total) * 100).toFixed(2) + "%" : "0%"
1377
+ };
1378
+ }
1379
+
1380
+ // Get performance statistics for debugging and optimization
1381
+ getPerformanceStats() {
1382
+ const calculateStats = times => {
1383
+ if (times.length === 0) return { avg: 0, max: 0, min: 0, count: 0 };
1384
+ return {
1385
+ avg: Math.round((times.reduce((sum, t) => sum + t, 0) / times.length) * 100) / 100,
1386
+ max: Math.round(Math.max(...times) * 100) / 100,
1387
+ min: Math.round(Math.min(...times) * 100) / 100,
1388
+ count: times.length
1389
+ };
1390
+ };
1391
+
1392
+ return {
1393
+ keywordCache: this.getKeywordCacheStats(),
1394
+ extraction: calculateStats(this.queryStats.extractionTime),
1395
+ addMemory: calculateStats(this.queryStats.addMemoryTime),
1396
+ retrieval: calculateStats(this.queryStats.retrievalTime)
1397
+ };
1398
+ }
1399
+
1400
+ // Performance wrapper for memory extraction
1401
+ async extractMemoryFromTextTimed(userText, kimiResponse = null) {
1402
+ const start = performance.now();
1403
+ const result = await this.extractMemoryFromText(userText, kimiResponse);
1404
+ const duration = performance.now() - start;
1405
+
1406
+ this.queryStats.extractionTime.push(duration);
1407
+ if (this.queryStats.extractionTime.length > 100) {
1408
+ this.queryStats.extractionTime.shift(); // Keep only last 100 measurements
1409
+ }
1410
+
1411
+ if (duration > 100 && window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1412
+ console.warn(`🐌 Slow memory extraction: ${duration.toFixed(2)}ms for text length ${userText?.length || 0}`);
1413
+ }
1414
+
1415
+ return result;
1416
+ }
1417
+
1418
+ // Get current configuration for debugging and monitoring
1419
+ getConfiguration() {
1420
+ return {
1421
+ ...this.config,
1422
+ memoryCategories: this.memoryCategories,
1423
+ runtime: {
1424
+ memoryEnabled: this.memoryEnabled,
1425
+ maxMemoryEntries: this.maxMemoryEntries,
1426
+ selectedCharacter: this.selectedCharacter,
1427
+ keywordCacheSize: this.keywordCache.size,
1428
+ compiledPatternsCount: Object.values(this.compiledPatterns || {}).reduce((sum, arr) => sum + arr.length, 0)
1429
+ }
1430
+ };
1431
+ }
1432
+
1433
+ // Update configuration at runtime (for advanced users)
1434
+ updateConfiguration(configPath, value) {
1435
+ const keys = configPath.split(".");
1436
+ let current = this.config;
1437
+
1438
+ // Navigate to the parent object
1439
+ for (let i = 0; i < keys.length - 1; i++) {
1440
+ if (!current[keys[i]]) current[keys[i]] = {};
1441
+ current = current[keys[i]];
1442
+ }
1443
+
1444
+ // Set the value
1445
+ const lastKey = keys[keys.length - 1];
1446
+ const oldValue = current[lastKey];
1447
+ current[lastKey] = value;
1448
+
1449
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1450
+ console.log(`🔧 Configuration updated: ${configPath} = ${value} (was: ${oldValue})`);
1451
+ }
1452
+
1453
+ return { oldValue, newValue: value };
1454
+ }
1455
+
1456
+ async cleanupOldMemories() {
1457
+ if (!this.db) return;
1458
+
1459
+ try {
1460
+ // Retrieve all active memories for the current character
1461
+ const memories = await this.getAllMemories();
1462
+
1463
+ const maxEntries = window.KIMI_MAX_MEMORIES || this.maxMemoryEntries || 100;
1464
+ const ttlDays = window.KIMI_MEMORY_TTL_DAYS || 365;
1465
+
1466
+ // Soft-expire memories older than TTL by marking isActive=false
1467
+ const now = Date.now();
1468
+ const ttlMs = ttlDays * 24 * 60 * 60 * 1000;
1469
+ const expiredMemories = [];
1470
+
1471
+ for (const mem of memories) {
1472
+ const created = new Date(this.getCreationTimestamp(mem)).getTime();
1473
+ if (now - created > ttlMs) {
1474
+ try {
1475
+ await this.updateMemory(mem.id, { isActive: false });
1476
+ expiredMemories.push(mem.id);
1477
+ } catch (e) {
1478
+ console.error(`Memory expiration failed for ID ${mem.id}:`, {
1479
+ error: e.message,
1480
+ memoryId: mem.id,
1481
+ createdAt: this.getCreationTimestamp(mem),
1482
+ character: mem.character
1483
+ });
1484
+ // Continue with other memories even if one fails
1485
+ }
1486
+ }
1487
+ }
1488
+
1489
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY && expiredMemories.length > 0) {
1490
+ console.log(`Successfully expired ${expiredMemories.length} memories:`, expiredMemories);
1491
+ }
1492
+
1493
+ // Refresh active memories after TTL purge
1494
+ const activeMemories = (await this.getAllMemories()).filter(m => m.isActive);
1495
+
1496
+ // If still more than maxEntries, mark lowest-priority ones inactive (soft delete)
1497
+ if (activeMemories.length > maxEntries) {
1498
+ // Sort by a combined score: low importance + old timestamp + low access
1499
+ activeMemories.sort((a, b) => {
1500
+ const scoreA =
1501
+ (a.importance || 0.5) * -1 + (a.accessCount || 0) * 0.01 + new Date(this.getCreationTimestamp(a)).getTime() / (1000 * 60 * 60 * 24);
1502
+ const scoreB =
1503
+ (b.importance || 0.5) * -1 + (b.accessCount || 0) * 0.01 + new Date(this.getCreationTimestamp(b)).getTime() / (1000 * 60 * 60 * 24);
1504
+ return scoreB - scoreA;
1505
+ });
1506
+
1507
+ const toDeactivate = activeMemories.slice(maxEntries);
1508
+ const deactivatedMemories = [];
1509
+ const failedDeactivations = [];
1510
+
1511
+ for (const mem of toDeactivate) {
1512
+ try {
1513
+ await this.updateMemory(mem.id, { isActive: false });
1514
+ deactivatedMemories.push(mem.id);
1515
+ } catch (e) {
1516
+ console.error(`Memory deactivation failed for ID ${mem.id}:`, {
1517
+ error: e.message,
1518
+ memoryId: mem.id,
1519
+ importance: mem.importance,
1520
+ character: mem.character
1521
+ });
1522
+ failedDeactivations.push(mem.id);
1523
+ }
1524
+ }
1525
+
1526
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1527
+ console.log(`Memory cleanup: ${deactivatedMemories.length} deactivated, ${failedDeactivations.length} failed`);
1528
+ }
1529
+ }
1530
+ } catch (error) {
1531
+ console.error("Error cleaning up old memories:", error);
1532
+ }
1533
+ }
1534
+
1535
+ // MEMORY RETRIEVAL FOR LLM
1536
+ async getRelevantMemories(context = "", limit = 10) {
1537
+ if (!this.memoryEnabled) return [];
1538
+
1539
+ try {
1540
+ const allMemories = await this.getAllMemories();
1541
+
1542
+ if (allMemories.length === 0) return [];
1543
+
1544
+ if (!context) {
1545
+ // Return most important and recent memories
1546
+ const res = this.selectMostImportantMemories(allMemories, limit);
1547
+ // touch top results to update access metrics
1548
+ this._touchMemories(res, limit).catch(() => {});
1549
+ return res;
1550
+ }
1551
+
1552
+ // Score memories based on relevance to context
1553
+ const scoredMemories = allMemories.map(memory => ({
1554
+ ...memory,
1555
+ relevanceScore: this.calculateRelevance(memory, context)
1556
+ }));
1557
+
1558
+ // Sort by relevance and return top results
1559
+ scoredMemories.sort((a, b) => b.relevanceScore - a.relevanceScore);
1560
+
1561
+ // Filter out very low relevance memories
1562
+ const relevantMemories = scoredMemories.filter(m => m.relevanceScore > 0.1);
1563
+
1564
+ const out = relevantMemories.slice(0, limit).map(r => r);
1565
+ // touch top results to update access metrics
1566
+ this._touchMemories(
1567
+ out.map(r => r),
1568
+ limit
1569
+ ).catch(() => {});
1570
+ return out;
1571
+ } catch (error) {
1572
+ console.error("Error getting relevant memories:", error);
1573
+ return [];
1574
+ }
1575
+ }
1576
+
1577
+ // Select most important memories when no context is provided
1578
+ selectMostImportantMemories(memories, limit) {
1579
+ // Score by importance, recency, and access count
1580
+ const scoredMemories = memories.map(memory => {
1581
+ let score = memory.importance || 0.5;
1582
+
1583
+ // Boost recent memories
1584
+ const daysSinceCreation = this.getDaysSinceCreation(memory);
1585
+ score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost
1586
+
1587
+ // Boost frequently accessed memories
1588
+ const accessCount = memory.accessCount || 0;
1589
+ score += Math.min(accessCount / 10, 0.2); // Access boost
1590
+
1591
+ // Boost high confidence memories
1592
+ score += (memory.confidence || 0.5) * 0.1;
1593
+
1594
+ return { ...memory, importanceScore: score };
1595
+ });
1596
+
1597
+ scoredMemories.sort((a, b) => b.importanceScore - a.importanceScore);
1598
+ return scoredMemories.slice(0, limit);
1599
+ }
1600
+
1601
+ calculateRelevance(memory, context) {
1602
+ const contextWords = context
1603
+ .toLowerCase()
1604
+ .split(/\s+/)
1605
+ .filter(w => w.length > 2);
1606
+ const memoryWords = memory.content
1607
+ .toLowerCase()
1608
+ .split(/\s+/)
1609
+ .filter(w => w.length > 2);
1610
+
1611
+ let score = 0;
1612
+
1613
+ // Enhanced content similarity with keyword matching
1614
+ score += this.calculateSimilarity(memory.content, context) * this.config.relevance.contentSimilarity;
1615
+
1616
+ // Keyword overlap boost (derived keywords)
1617
+ try {
1618
+ const memKeys = memory.keywords || this.deriveKeywords(memory.content || "");
1619
+ const ctxKeys = this.deriveKeywords(context || "");
1620
+ const keyOverlap = ctxKeys.filter(k => memKeys.includes(k)).length;
1621
+ if (ctxKeys.length > 0) {
1622
+ score += (keyOverlap / ctxKeys.length) * this.config.relevance.keywordOverlap;
1623
+ }
1624
+ } catch (e) {
1625
+ // fallback to original keyword matching
1626
+ let keywordMatches = 0;
1627
+ for (const word of contextWords) {
1628
+ if (memoryWords.includes(word)) {
1629
+ keywordMatches++;
1630
+ }
1631
+ }
1632
+ if (contextWords.length > 0) {
1633
+ score += (keywordMatches / contextWords.length) * this.config.relevance.keywordOverlap;
1634
+ }
1635
+ }
1636
+
1637
+ // Category relevance bonus based on context
1638
+ score += this.getCategoryRelevance(memory.category, context) * this.config.relevance.categoryRelevance;
1639
+
1640
+ // Recent memories get bonus for current conversation
1641
+ const daysSinceCreation = this.getDaysSinceCreation(memory);
1642
+ score +=
1643
+ Math.max(0, (this.config.relevance.recentDaysThreshold - daysSinceCreation) / this.config.relevance.recentDaysThreshold) *
1644
+ this.config.relevance.recencyBonus;
1645
+
1646
+ // Confidence and importance boost
1647
+ score += (memory.confidence || 0.5) * this.config.relevance.confidenceBonus;
1648
+ score += (memory.importance || 0.5) * this.config.relevance.importanceBonus;
1649
+
1650
+ return Math.min(1.0, score);
1651
+ }
1652
+
1653
+ // Determine if memory category is relevant to current context
1654
+ getCategoryRelevance(category, context) {
1655
+ const contextLower = context.toLowerCase();
1656
+
1657
+ const categoryKeywords = {
1658
+ personal: ["name", "age", "live", "work", "job", "who", "am", "myself", "appelle", "nombre", "chiamo", "heiße", "名前", "名字", "我叫"],
1659
+ preferences: [
1660
+ "like",
1661
+ "love",
1662
+ "hate",
1663
+ "prefer",
1664
+ "enjoy",
1665
+ "favorite",
1666
+ "dislike",
1667
+ "j'aime",
1668
+ "j'adore",
1669
+ "je préfère",
1670
+ "je déteste",
1671
+ "me gusta",
1672
+ "prefiero",
1673
+ "odio",
1674
+ "mi piace",
1675
+ "preferisco",
1676
+ "ich mag",
1677
+ "ich bevorzuge",
1678
+ "hasse"
1679
+ ],
1680
+ relationships: [
1681
+ "family",
1682
+ "friend",
1683
+ "wife",
1684
+ "husband",
1685
+ "partner",
1686
+ "mother",
1687
+ "father",
1688
+ "girlfriend",
1689
+ "boyfriend",
1690
+ "anniversary",
1691
+ "date",
1692
+ "kiss",
1693
+ "move in",
1694
+ "famille",
1695
+ "ami",
1696
+ "copine",
1697
+ "copain",
1698
+ "anniversaire",
1699
+ "rendez-vous",
1700
+ "baiser",
1701
+ "emménagé",
1702
+ "pareja",
1703
+ "cita",
1704
+ "beso",
1705
+ "aniversario",
1706
+ "mudarnos",
1707
+ "fidanzata",
1708
+ "fidanzato",
1709
+ "anniversario",
1710
+ "bacio",
1711
+ "trasferiti",
1712
+ "freundin",
1713
+ "freund",
1714
+ "jahrestag",
1715
+ "kuss",
1716
+ "eingezogen"
1717
+ ],
1718
+ activities: [
1719
+ "play",
1720
+ "hobby",
1721
+ "sport",
1722
+ "activity",
1723
+ "practice",
1724
+ "do",
1725
+ "joue",
1726
+ "passe-temps",
1727
+ "hobby",
1728
+ "juego",
1729
+ "pasatiempo",
1730
+ "gioco",
1731
+ "passatempo",
1732
+ "spiele",
1733
+ "hobby"
1734
+ ],
1735
+ goals: [
1736
+ "want",
1737
+ "plan",
1738
+ "goal",
1739
+ "dream",
1740
+ "hope",
1741
+ "wish",
1742
+ "future",
1743
+ "veux",
1744
+ "objectif",
1745
+ "apprends",
1746
+ "aprendo",
1747
+ "voglio",
1748
+ "obiettivo",
1749
+ "lerne",
1750
+ "ziel"
1751
+ ],
1752
+ experiences: [
1753
+ "remember",
1754
+ "happened",
1755
+ "story",
1756
+ "experience",
1757
+ "time",
1758
+ "we met",
1759
+ "first date",
1760
+ "first kiss",
1761
+ "anniversary",
1762
+ "rencontré",
1763
+ "premier rendez-vous",
1764
+ "premier baiser",
1765
+ "anniversaire",
1766
+ "conocimos",
1767
+ "primera cita",
1768
+ "primer beso",
1769
+ "aniversario",
1770
+ "conosciuti",
1771
+ "primo appuntamento",
1772
+ "primo bacio",
1773
+ "anniversario",
1774
+ "kennengelernt",
1775
+ "erstes date",
1776
+ "erster kuss",
1777
+ "jahrestag"
1778
+ ],
1779
+ important: [
1780
+ "important",
1781
+ "remember",
1782
+ "special",
1783
+ "never forget",
1784
+ "important",
1785
+ "souvenir",
1786
+ "spécial",
1787
+ "importante",
1788
+ "recuerda",
1789
+ "importante",
1790
+ "ricorda",
1791
+ "wichtig",
1792
+ "erinnere"
1793
+ ]
1794
+ };
1795
+
1796
+ const keywords = categoryKeywords[category] || [];
1797
+ let relevance = 0;
1798
+
1799
+ for (const keyword of keywords) {
1800
+ if (contextLower.includes(keyword)) {
1801
+ relevance += 0.2;
1802
+ }
1803
+ }
1804
+
1805
+ return Math.min(1.0, relevance);
1806
+ }
1807
+
1808
+ // Update access count when memory is used
1809
+ async recordMemoryAccess(memoryId) {
1810
+ try {
1811
+ const memory = await this.db.db.memories.get(memoryId);
1812
+ if (memory) {
1813
+ memory.accessCount = (memory.accessCount || 0) + 1;
1814
+ memory.lastAccess = new Date();
1815
+ await this.db.db.memories.put(memory);
1816
+ }
1817
+ } catch (error) {
1818
+ console.error("Error recording memory access:", error);
1819
+ }
1820
+ }
1821
+
1822
+ // Touch multiple memories to update lastAccess and accessCount
1823
+ async _touchMemories(memories, limit = 5) {
1824
+ if (!this.db || !Array.isArray(memories) || memories.length === 0) return;
1825
+
1826
+ try {
1827
+ const top = memories.slice(0, limit);
1828
+ const now = new Date();
1829
+ const minMinutes = window.KIMI_MEMORY_TOUCH_MINUTES || 60;
1830
+ const minTouchInterval = minMinutes * 60 * 1000;
1831
+
1832
+ // Batch collection: gather all updates before executing
1833
+ const batchUpdates = [];
1834
+
1835
+ for (const m of top) {
1836
+ try {
1837
+ const id = m.id;
1838
+ const existing = await this.db.db.memories.get(id);
1839
+ if (existing) {
1840
+ const lastAccess = existing.lastAccess ? new Date(existing.lastAccess).getTime() : 0;
1841
+
1842
+ // Only touch if enough time has passed
1843
+ if (now.getTime() - lastAccess > minTouchInterval) {
1844
+ batchUpdates.push({
1845
+ key: id,
1846
+ changes: {
1847
+ accessCount: (existing.accessCount || 0) + 1,
1848
+ lastAccess: now
1849
+ }
1850
+ });
1851
+ }
1852
+ }
1853
+ } catch (e) {
1854
+ console.warn("Error preparing memory touch batch for", m && m.id, e);
1855
+ }
1856
+ }
1857
+
1858
+ // Execute all updates in a single batch operation
1859
+ if (batchUpdates.length > 0) {
1860
+ if (this.db.db.memories.bulkUpdate) {
1861
+ // Use bulkUpdate if available (Dexie 3.x+)
1862
+ await this.db.db.memories.bulkUpdate(batchUpdates);
1863
+ } else {
1864
+ // Fallback: parallel individual updates (still better than sequential)
1865
+ const updatePromises = batchUpdates.map(update => this.db.db.memories.update(update.key, update.changes));
1866
+ await Promise.all(updatePromises);
1867
+ }
1868
+
1869
+ if (window.KIMI_CONFIG?.DEBUG?.MEMORY) {
1870
+ console.log(`📊 Batch touched ${batchUpdates.length} memories`);
1871
+ }
1872
+ }
1873
+ } catch (e) {
1874
+ console.warn("Error in _touchMemories batch processing", e);
1875
+ }
1876
+ }
1877
+
1878
+ // ===== MEMORY SCORING & RANKING =====
1879
+ scoreMemory(memory) {
1880
+ // Factors: importance (0-1), recency, frequency, confidence
1881
+ const now = Date.now();
1882
+ const created = memory.createdAt ? new Date(memory.createdAt).getTime() : memory.timestamp ? new Date(memory.timestamp).getTime() : now;
1883
+ const lastAccess = memory.lastAccess ? new Date(memory.lastAccess).getTime() : created;
1884
+ const ageMs = Math.max(1, now - created);
1885
+ const sinceLastAccessMs = Math.max(1, now - lastAccess);
1886
+ // Recency: exponential decay
1887
+ const recency = Math.exp(-sinceLastAccessMs / (1000 * 60 * 60 * 24 * 14)); // 14-day half-life approx
1888
+ const freshness = Math.exp(-ageMs / (1000 * 60 * 60 * 24 * 60)); // 60-day aging
1889
+ const freq = Math.log10((memory.accessCount || 0) + 1) / Math.log10(50); // normalized frequency (cap ~50)
1890
+ const importance = typeof memory.importance === "number" ? memory.importance : 0.5;
1891
+ const confidence = typeof memory.confidence === "number" ? memory.confidence : 0.5;
1892
+ // Weighted sum using global knobs
1893
+ const wImportance = window.KIMI_WEIGHT_IMPORTANCE || 0.35;
1894
+ const wRecency = window.KIMI_WEIGHT_RECENCY || 0.2;
1895
+ const wFrequency = window.KIMI_WEIGHT_FREQUENCY || 0.15;
1896
+ const wConfidence = window.KIMI_WEIGHT_CONFIDENCE || 0.2;
1897
+ const wFreshness = window.KIMI_WEIGHT_FRESHNESS || 0.1;
1898
+
1899
+ const score = importance * wImportance + recency * wRecency + freq * wFrequency + confidence * wConfidence + freshness * wFreshness;
1900
+ return Number(score.toFixed(6));
1901
+ }
1902
+
1903
+ async getRankedMemories(contextText = "", limit = 7) {
1904
+ const all = await this.getAllMemories();
1905
+ if (!all.length) return [];
1906
+ // Optional basic context relevance boost
1907
+ const ctxLower = (contextText || "").toLowerCase();
1908
+ // Favor pinned memories by boosting their base score
1909
+ return all
1910
+ .map(m => {
1911
+ let baseScore = this.scoreMemory(m);
1912
+ if (m.tags && m.tags.includes && m.tags.includes("pinned")) baseScore += 0.2;
1913
+ if (ctxLower && m.content && ctxLower.includes(m.content.toLowerCase().split(" ")[0])) {
1914
+ baseScore += 0.05; // tiny relevance boost
1915
+ }
1916
+ return { memory: m, score: baseScore };
1917
+ })
1918
+ .sort((a, b) => b.score - a.score)
1919
+ .slice(0, limit)
1920
+ .map(r => r.memory);
1921
+ }
1922
+
1923
+ // Pin/unpin APIs to manually mark important memories
1924
+ async pinMemory(memoryId) {
1925
+ if (!this.db) return false;
1926
+ try {
1927
+ const m = await this.db.db.memories.get(memoryId);
1928
+ if (!m) return false;
1929
+ const tags = new Set([...(m.tags || []), "pinned"]);
1930
+ await this.db.db.memories.update(memoryId, { tags: [...tags], importance: Math.max(m.importance || 0.5, 0.95) });
1931
+ return true;
1932
+ } catch (e) {
1933
+ console.error("Error pinning memory", e);
1934
+ return false;
1935
+ }
1936
+ }
1937
+
1938
+ async unpinMemory(memoryId) {
1939
+ if (!this.db) return false;
1940
+ try {
1941
+ const m = await this.db.db.memories.get(memoryId);
1942
+ if (!m) return false;
1943
+ const tags = new Set([...(m.tags || [])]);
1944
+ tags.delete("pinned");
1945
+ await this.db.db.memories.update(memoryId, { tags: [...tags] });
1946
+ return true;
1947
+ } catch (e) {
1948
+ console.error("Error unpinning memory", e);
1949
+ return false;
1950
+ }
1951
+ }
1952
+
1953
+ // Summarize recent memories into a non-destructive summary memory
1954
+ async summarizeRecentMemories(days = 7, options = { category: null, archiveSources: false }) {
1955
+ if (!this.db) return null;
1956
+ try {
1957
+ const cutoff = Date.now() - (days || 7) * 24 * 60 * 60 * 1000;
1958
+ const all = await this.getAllMemories();
1959
+ // Exclude existing summaries to avoid summarizing summaries repeatedly
1960
+ const recent = all.filter(
1961
+ m => new Date(this.getCreationTimestamp(m)).getTime() >= cutoff && m.isActive && m.type !== "summary" && !(m.tags && m.tags.includes("summary"))
1962
+ );
1963
+ if (!recent.length) return null;
1964
+
1965
+ // Group by top keyword
1966
+ const groups = {};
1967
+ for (const m of recent) {
1968
+ const keys = m.keywords && m.keywords.length ? m.keywords : this.deriveKeywords(m.content || "");
1969
+ const top = keys[0] || "misc";
1970
+ groups[top] = groups[top] || [];
1971
+ groups[top].push(m);
1972
+ }
1973
+
1974
+ // Build a simple summary per group
1975
+ const summaries = [];
1976
+ for (const [k, items] of Object.entries(groups)) {
1977
+ const contents = items.map(i => i.content).slice(0, 6);
1978
+ summaries.push(`${k}: ${contents.join(" | ")}`);
1979
+ }
1980
+
1981
+ const summaryContent = `Summary (${days}d): ` + summaries.join(" \n");
1982
+
1983
+ const summaryJson = { summary: summaries };
1984
+
1985
+ const summaryMemory = {
1986
+ category: options.category || "experiences",
1987
+ type: "summary",
1988
+ content: summaryContent,
1989
+ sourceText: summaryContent,
1990
+ summaryJson: JSON.stringify(summaryJson),
1991
+ confidence: 0.9,
1992
+ createdAt: new Date(), // Use createdAt consistently
1993
+ character: this.selectedCharacter,
1994
+ isActive: true,
1995
+ tags: ["summary"]
1996
+ };
1997
+
1998
+ const saved = await this.addMemory(summaryMemory);
1999
+
2000
+ // Optionally archive sources (soft-deactivate)
2001
+ if (options.archiveSources) {
2002
+ for (const m of recent) {
2003
+ try {
2004
+ await this.updateMemory(m.id, { isActive: false });
2005
+ } catch (e) {
2006
+ console.warn("Failed to archive source memory", m.id, e);
2007
+ }
2008
+ }
2009
+ }
2010
+
2011
+ return saved;
2012
+ } catch (e) {
2013
+ console.error("Error summarizing memories", e);
2014
+ return null;
2015
+ }
2016
+ }
2017
+
2018
+ // Summarize recent memories and replace sources (hard delete) - destructive
2019
+ async summarizeAndReplace(days = 7, options = { category: null }) {
2020
+ if (!this.db) return null;
2021
+ try {
2022
+ const cutoff = Date.now() - (days || 7) * 24 * 60 * 60 * 1000;
2023
+ const all = await this.getAllMemories();
2024
+ // Exclude existing summaries to avoid recursive summarization
2025
+ const recent = all.filter(
2026
+ m => new Date(this.getCreationTimestamp(m)).getTime() >= cutoff && m.isActive && m.type !== "summary" && !(m.tags && m.tags.includes("summary"))
2027
+ );
2028
+ if (!recent.length) return null;
2029
+
2030
+ // Build aggregate content from readable fields in chronological order
2031
+ recent.sort((a, b) => new Date(this.getCreationTimestamp(a)) - new Date(this.getCreationTimestamp(b)));
2032
+ const texts = recent
2033
+ .map(r => {
2034
+ const raw = (r.title && r.title.trim()) || (r.sourceText && r.sourceText.trim()) || (r.content && r.content.trim()) || "";
2035
+ if (!raw) return "";
2036
+ // Normalize whitespace and remove stray leading punctuation
2037
+ let t = raw.replace(/\s+/g, " ").replace(/^[^\p{L}\p{N}]+/u, "");
2038
+ // Capitalize first meaningful letter
2039
+ if (t && t.length > 0) t = t.charAt(0).toUpperCase() + t.slice(1);
2040
+ return t;
2041
+ })
2042
+ .filter(Boolean)
2043
+ .slice(0, 200);
2044
+
2045
+ const summaryContent = `Summary (${days}d):\n` + texts.map(t => `- ${t}`).join("\n");
2046
+
2047
+ const summaryJson = { summary: texts };
2048
+
2049
+ const summaryMemory = {
2050
+ category: options.category || "experiences",
2051
+ type: "summary",
2052
+ title: `Summary - last ${days} days`,
2053
+ content: summaryContent,
2054
+ // Store the actual summary also in sourceText so editors/UIs show it
2055
+ sourceText: summaryContent,
2056
+ summaryJson: JSON.stringify(summaryJson),
2057
+ confidence: 0.95,
2058
+ timestamp: new Date(),
2059
+ character: this.selectedCharacter,
2060
+ isActive: true,
2061
+ tags: ["summary", "replaced"]
2062
+ };
2063
+
2064
+ // Add summary directly to DB to avoid addMemory's merge logic
2065
+ let saved = null;
2066
+ if (this.db && this.db.db && this.db.db.memories) {
2067
+ try {
2068
+ const id = await this.db.db.memories.add(summaryMemory);
2069
+ summaryMemory.id = id;
2070
+ saved = summaryMemory;
2071
+ console.log("Summary added with ID:", id);
2072
+ // Read back the saved record to verify stored fields
2073
+ try {
2074
+ const savedRec = await this.db.db.memories.get(id);
2075
+ console.log("Saved summary record:", { id, content: savedRec.content, sourceText: savedRec.sourceText });
2076
+ } catch (e) {
2077
+ console.warn("Unable to read back saved summary", e);
2078
+ }
2079
+ } catch (e) {
2080
+ console.error("Failed to write summary directly to DB", e);
2081
+ }
2082
+ } else {
2083
+ // Fallback to addMemory if DB not available
2084
+ saved = await this.addMemory(summaryMemory);
2085
+ }
2086
+
2087
+ // Hard-delete sources
2088
+ for (const m of recent) {
2089
+ try {
2090
+ if (this.db && this.db.db && this.db.db.memories) {
2091
+ await this.db.db.memories.delete(m.id);
2092
+ }
2093
+ } catch (e) {
2094
+ console.warn("Failed to delete source memory", m.id, e);
2095
+ }
2096
+ }
2097
+
2098
+ // Notify LLM to refresh context
2099
+ this.notifyLLMContextUpdate();
2100
+
2101
+ return saved;
2102
+ } catch (e) {
2103
+ console.error("Error in summarizeAndReplace", e);
2104
+ return null;
2105
+ }
2106
+ }
2107
+
2108
+ // MEMORY STATISTICS
2109
+ async getMemoryStats() {
2110
+ try {
2111
+ const memories = await this.getAllMemories();
2112
+ const stats = {
2113
+ total: memories.length,
2114
+ byCategory: {},
2115
+ averageConfidence: 0,
2116
+ oldestMemory: null,
2117
+ newestMemory: null
2118
+ };
2119
+
2120
+ if (memories.length > 0) {
2121
+ // Category breakdown
2122
+ for (const memory of memories) {
2123
+ stats.byCategory[memory.category] = (stats.byCategory[memory.category] || 0) + 1;
2124
+ }
2125
+
2126
+ // Average confidence
2127
+ stats.averageConfidence = memories.reduce((sum, m) => sum + m.confidence, 0) / memories.length;
2128
+
2129
+ // Oldest and newest
2130
+ const sortedByDate = [...memories].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
2131
+ stats.oldestMemory = sortedByDate[0];
2132
+ stats.newestMemory = sortedByDate[sortedByDate.length - 1];
2133
+ }
2134
+
2135
+ return stats;
2136
+ } catch (error) {
2137
+ console.error("Error getting memory stats:", error);
2138
+ return { total: 0, byCategory: {}, averageConfidence: 0 };
2139
+ }
2140
+ }
2141
+
2142
+ // MEMORY TOGGLE
2143
+ async toggleMemorySystem(enabled) {
2144
+ this.memoryEnabled = enabled;
2145
+ if (this.db) {
2146
+ await this.db.setPreference("memorySystemEnabled", enabled);
2147
+ }
2148
+ }
2149
+
2150
+ // EXPORT/IMPORT MEMORIES
2151
+ async exportMemories() {
2152
+ try {
2153
+ const memories = await this.getAllMemories();
2154
+ return {
2155
+ exportDate: new Date().toISOString(),
2156
+ character: this.selectedCharacter,
2157
+ memories: memories,
2158
+ version: "1.0"
2159
+ };
2160
+ } catch (error) {
2161
+ console.error("Error exporting memories:", error);
2162
+ return null;
2163
+ }
2164
+ }
2165
+
2166
+ async importMemories(importData) {
2167
+ if (!importData || !importData.memories) return false;
2168
+
2169
+ try {
2170
+ for (const memory of importData.memories) {
2171
+ await this.addMemory({
2172
+ ...memory,
2173
+ type: "imported",
2174
+ character: this.selectedCharacter
2175
+ });
2176
+ }
2177
+ return true;
2178
+ } catch (error) {
2179
+ console.error("Error importing memories:", error);
2180
+ return false;
2181
+ }
2182
+ }
2183
+
2184
+ // MIGRATION UTILITIES
2185
+ async migrateIncompatibleIDs() {
2186
+ if (!this.db) return false;
2187
+
2188
+ try {
2189
+ console.log("🔧 Début de la migration des IDs incompatibles...");
2190
+
2191
+ // Récupérer toutes les mémoires
2192
+ const allMemories = await this.db.db.memories.toArray();
2193
+ console.log(`📊 ${allMemories.length} mémoires trouvées`);
2194
+
2195
+ const incompatibleMemories = allMemories.filter(memory => {
2196
+ // Les IDs auto-increment sont des entiers séquentiels (1, 2, 3...)
2197
+ // Les anciens IDs manuels sont des nombres très grands (timestamps)
2198
+ return memory.id > 10000; // Seuil arbitraire pour détecter les anciens IDs
2199
+ });
2200
+
2201
+ if (incompatibleMemories.length === 0) {
2202
+ console.log("✅ Aucune migration nécessaire");
2203
+ return true;
2204
+ }
2205
+
2206
+ console.log(`🔄 Migration de ${incompatibleMemories.length} mémoires avec IDs incompatibles`);
2207
+
2208
+ // Sauvegarder les données avant suppression
2209
+ const dataToMigrate = incompatibleMemories.map(memory => {
2210
+ const { id, ...memoryData } = memory; // Enlever l'ancien ID
2211
+ return memoryData;
2212
+ });
2213
+
2214
+ // Supprimer les anciennes entrées
2215
+ await this.db.db.memories.bulkDelete(incompatibleMemories.map(m => m.id));
2216
+
2217
+ // Réinsérer avec de nouveaux IDs auto-générés
2218
+ const newIds = await this.db.db.memories.bulkAdd(dataToMigrate);
2219
+
2220
+ console.log(`✅ Migration terminée. Nouveaux IDs:`, newIds);
2221
+ return true;
2222
+ } catch (error) {
2223
+ console.error("❌ Erreur lors de la migration:", error);
2224
+ return false;
2225
+ }
2226
+ }
2227
+
2228
+ // Background migration: populate keywords for all existing memories if missing
2229
+ async populateKeywordsForAllMemories() {
2230
+ if (!this.db || !this.db.db.memories) return false;
2231
+ try {
2232
+ console.log("🔧 Starting background keyword population...");
2233
+ const all = await this.db.db.memories.toArray();
2234
+ const ops = [];
2235
+ for (const mem of all) {
2236
+ if (!mem.keywords || !Array.isArray(mem.keywords) || mem.keywords.length === 0) {
2237
+ const keys = this.deriveKeywords(mem.content || "");
2238
+ ops.push(this.db.db.memories.update(mem.id, { keywords: keys }));
2239
+ }
2240
+ // batch in small chunks to avoid blocking
2241
+ if (ops.length >= 50) {
2242
+ await Promise.all(ops);
2243
+ ops.length = 0;
2244
+ }
2245
+ }
2246
+ if (ops.length) await Promise.all(ops);
2247
+ console.log("✅ Keyword population complete");
2248
+ return true;
2249
+ } catch (e) {
2250
+ console.warn("Error populating keywords", e);
2251
+ return false;
2252
+ }
2253
+ }
2254
+ }
2255
+
2256
+ window.KimiMemorySystem = KimiMemorySystem;
2257
+ export default KimiMemorySystem;
kimi-js/kimi-memory-ui.js ADDED
@@ -0,0 +1,966 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI MEMORY UI MANAGER =====
2
+ class KimiMemoryUI {
3
+ constructor() {
4
+ this.memorySystem = null;
5
+ this.isInitialized = false;
6
+ // Debounce helpers for UI refresh to coalesce multiple DB reads
7
+ this._debounceTimers = {};
8
+ }
9
+
10
+ debounce(key, fn, wait = 350) {
11
+ if (this._debounceTimers[key]) clearTimeout(this._debounceTimers[key]);
12
+ this._debounceTimers[key] = setTimeout(() => {
13
+ fn();
14
+ delete this._debounceTimers[key];
15
+ }, wait);
16
+ }
17
+
18
+ async init() {
19
+ if (!window.kimiMemorySystem) {
20
+ console.warn("Memory system not available");
21
+ return;
22
+ }
23
+
24
+ this.memorySystem = window.kimiMemorySystem;
25
+ this.setupEventListeners();
26
+ await this.updateMemoryStats();
27
+ this.isInitialized = true;
28
+ }
29
+
30
+ setupEventListeners() {
31
+ // Memory toggle
32
+ const memoryToggle = document.getElementById("memory-toggle");
33
+ if (memoryToggle) {
34
+ memoryToggle.addEventListener("click", () => this.toggleMemorySystem());
35
+ }
36
+
37
+ // View memories button
38
+ const viewMemoriesBtn = document.getElementById("view-memories");
39
+ if (viewMemoriesBtn) {
40
+ viewMemoriesBtn.addEventListener("click", () => this.openMemoryModal());
41
+ }
42
+
43
+ // Add memory button
44
+ const addMemoryBtn = document.getElementById("add-memory");
45
+ if (addMemoryBtn) {
46
+ addMemoryBtn.addEventListener("click", () => {
47
+ this.addManualMemory();
48
+ ensureVideoNeutralOnUIChange();
49
+ });
50
+ }
51
+
52
+ // Memory modal close
53
+ const memoryClose = document.getElementById("memory-close");
54
+ if (memoryClose) {
55
+ memoryClose.addEventListener("click", () => {
56
+ this.closeMemoryModal();
57
+ ensureVideoNeutralOnUIChange();
58
+ });
59
+ }
60
+
61
+ // Memory export
62
+ const memoryExport = document.getElementById("memory-export");
63
+ if (memoryExport) {
64
+ memoryExport.addEventListener("click", () => this.exportMemories());
65
+ }
66
+
67
+ // Memory filter
68
+ const memoryFilter = document.getElementById("memory-filter-category");
69
+ if (memoryFilter) {
70
+ memoryFilter.addEventListener("change", () => {
71
+ this.filterMemories();
72
+ ensureVideoNeutralOnUIChange();
73
+ });
74
+ }
75
+
76
+ // Memory search
77
+ const memorySearch = document.getElementById("memory-search");
78
+ if (memorySearch) {
79
+ memorySearch.addEventListener("input", () => this.filterMemories());
80
+ }
81
+
82
+ // Close modal on overlay click
83
+ const memoryOverlay = document.getElementById("memory-overlay");
84
+ if (memoryOverlay) {
85
+ memoryOverlay.addEventListener("click", e => {
86
+ if (e.target === memoryOverlay) {
87
+ this.closeMemoryModal();
88
+ }
89
+ });
90
+ }
91
+
92
+ // Delegated handler for memory-source clicks / touch / keyboard
93
+ const memoryList = document.getElementById("memory-list");
94
+ if (memoryList) {
95
+ // Click and touch
96
+ memoryList.addEventListener("click", e => this.handleMemorySourceToggle(e));
97
+ memoryList.addEventListener("touchstart", e => this.handleMemorySourceToggle(e));
98
+
99
+ // General delegated click handler for memory actions (summarize, etc.)
100
+ memoryList.addEventListener("click", e => this.handleMemoryListClick(e));
101
+
102
+ // Keyboard accessibility: Enter / Space when focused on .memory-source
103
+ memoryList.addEventListener("keydown", e => {
104
+ const target = e.target;
105
+ if (target && target.classList && target.classList.contains("memory-source")) {
106
+ if (e.key === "Enter" || e.key === " ") {
107
+ e.preventDefault();
108
+ this.toggleSourceContentForElement(target);
109
+ }
110
+ }
111
+ });
112
+ }
113
+ }
114
+
115
+ // Delegated click handler for actions inside the memory list
116
+ async handleMemoryListClick(e) {
117
+ try {
118
+ const summarizeBtn = e.target.closest && e.target.closest("#memory-summarize-btn");
119
+ if (summarizeBtn) {
120
+ e.stopPropagation();
121
+ await this.handleSummarizeAction();
122
+ return;
123
+ }
124
+ } catch (err) {
125
+ console.error("Error handling memory list click", err);
126
+ }
127
+ }
128
+
129
+ async toggleMemorySystem() {
130
+ if (!this.memorySystem) return;
131
+
132
+ const toggle = document.getElementById("memory-toggle");
133
+ const enabled = !this.memorySystem.memoryEnabled;
134
+
135
+ await this.memorySystem.toggleMemorySystem(enabled);
136
+
137
+ if (toggle) {
138
+ toggle.setAttribute("aria-checked", enabled.toString());
139
+ toggle.classList.toggle("active", enabled);
140
+ }
141
+
142
+ // Show feedback
143
+ this.showFeedback(enabled ? "Memory system enabled" : "Memory system disabled");
144
+ }
145
+
146
+ async addManualMemory() {
147
+ const categorySelect = document.getElementById("memory-category");
148
+ const contentInput = document.getElementById("memory-content");
149
+
150
+ if (!categorySelect || !contentInput) return;
151
+
152
+ const category = categorySelect.value;
153
+ const content = contentInput.value.trim();
154
+
155
+ if (!content) {
156
+ this.showFeedback("Please enter memory content", "error");
157
+ return;
158
+ }
159
+
160
+ try {
161
+ await this.memorySystem.addMemory({
162
+ category: category,
163
+ content: content,
164
+ type: "manual",
165
+ confidence: 1.0
166
+ });
167
+
168
+ contentInput.value = "";
169
+ await this.updateMemoryStats();
170
+ this.showFeedback("Memory added successfully");
171
+ } catch (error) {
172
+ console.error("Error adding memory:", error);
173
+ this.showFeedback("Error adding memory", "error");
174
+ }
175
+ }
176
+
177
+ async openMemoryModal() {
178
+ const overlay = document.getElementById("memory-overlay");
179
+ if (!overlay) return;
180
+
181
+ overlay.style.display = "flex";
182
+ await this.loadMemories();
183
+ }
184
+
185
+ closeMemoryModal() {
186
+ const overlay = document.getElementById("memory-overlay");
187
+ if (overlay) {
188
+ overlay.style.display = "none";
189
+ // Ensure background video resumes after closing memory modal
190
+ const kv = window.kimiVideo;
191
+ if (kv && kv.activeVideo) {
192
+ try {
193
+ const v = kv.activeVideo;
194
+ if (v.ended) {
195
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
196
+ } else if (v.paused) {
197
+ // Use centralized video utility for play
198
+ window.KimiVideoManager.getVideoElement(v)
199
+ .play()
200
+ .catch(() => {
201
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
202
+ });
203
+ }
204
+ } catch {}
205
+ }
206
+ }
207
+ }
208
+
209
+ async loadMemories() {
210
+ if (!this.memorySystem) return;
211
+
212
+ try {
213
+ // Use debounce to avoid multiple rapid DB reads
214
+ this.debounce("loadMemories", async () => {
215
+ const memories = await this.memorySystem.getAllMemories();
216
+ console.log("Loading memories into UI:", memories.length);
217
+ this.renderMemories(memories);
218
+ });
219
+ } catch (error) {
220
+ console.error("Error loading memories:", error);
221
+ }
222
+ }
223
+
224
+ async filterMemories() {
225
+ const filterSelect = document.getElementById("memory-filter-category");
226
+ const searchInput = document.getElementById("memory-search");
227
+ if (!this.memorySystem) return;
228
+
229
+ try {
230
+ const category = filterSelect?.value;
231
+ const searchTerm = searchInput?.value.toLowerCase().trim();
232
+ let memories;
233
+
234
+ if (category) {
235
+ memories = await this.memorySystem.getMemoriesByCategory(category);
236
+ } else {
237
+ memories = await this.memorySystem.getAllMemories();
238
+ }
239
+
240
+ // Apply search filter if search term exists
241
+ if (searchTerm) {
242
+ memories = memories.filter(
243
+ memory =>
244
+ memory.content.toLowerCase().includes(searchTerm) ||
245
+ memory.category.toLowerCase().includes(searchTerm) ||
246
+ (memory.sourceText && memory.sourceText.toLowerCase().includes(searchTerm))
247
+ );
248
+ }
249
+
250
+ this.renderMemories(memories);
251
+ } catch (error) {
252
+ console.error("Error filtering memories:", error);
253
+ }
254
+ }
255
+
256
+ renderMemories(memories) {
257
+ const memoryList = document.getElementById("memory-list");
258
+ if (!memoryList) return;
259
+
260
+ console.log("Rendering memories:", memories); // Debug logging
261
+
262
+ if (memories.length === 0) {
263
+ memoryList.innerHTML = `
264
+ <div class="memory-empty">
265
+ <i class="fas fa-brain"></i>
266
+ <p>No memories found. Start chatting to build memories automatically, or add them manually.</p>
267
+ </div>
268
+ `;
269
+ return;
270
+ }
271
+
272
+ // Group memories by category for better organization
273
+ const groupedMemories = memories.reduce((groups, memory) => {
274
+ const category = memory.category || "other";
275
+ if (!groups[category]) groups[category] = [];
276
+ groups[category].push(memory);
277
+ return groups;
278
+ }, {});
279
+
280
+ let html = "";
281
+
282
+ // Toolbar with summarize action
283
+ html += `
284
+ <div class="memory-toolbar" style="display:flex; gap:8px; align-items:center; margin-bottom:12px;">
285
+ <button id="memory-summarize-btn" class="kimi-button" title="Summarize recent memories">📝 Summarize last 7 days</button>
286
+ </div>
287
+ `;
288
+ Object.entries(groupedMemories).forEach(([category, categoryMemories]) => {
289
+ html += `
290
+ <div class="memory-category-group">
291
+ <h4 class="memory-category-header">
292
+ ${this.getCategoryIcon(category)} ${this.formatCategoryName(category)}
293
+ <span class="memory-category-count">(${categoryMemories.length})</span>
294
+ </h4>
295
+ <div class="memory-category-items">
296
+ `;
297
+
298
+ categoryMemories.forEach(memory => {
299
+ const confidence = Math.round(memory.confidence * 100);
300
+ const isAutomatic = memory.type === "auto_extracted";
301
+ const previewLength = 120;
302
+ const isLongContent = memory.content.length > previewLength;
303
+ const previewText = isLongContent ? memory.content.substring(0, previewLength) + "..." : memory.content;
304
+ const wordCount = memory.content.split(/\s+/).length;
305
+ const importance = typeof memory.importance === "number" ? memory.importance : 0.5;
306
+ const importanceLevel = this.getImportanceLevelFromValue(importance);
307
+ const importancePct = Math.round(importance * 100);
308
+ const tagsHtml = this.renderTags(memory.tags || []);
309
+
310
+ html += `
311
+ <div class="memory-item ${isAutomatic ? "memory-auto" : "memory-manual"}" data-memory-id="${memory.id}">
312
+ <div class="memory-header">
313
+ <div class="memory-item-title">${window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(memory.title || "") : memory.title || ""}${!memory.title ? "" : ""}</div>
314
+ <div class="memory-badges">
315
+ <span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "🤖 Auto" : "✋ Manual"}</span>
316
+ <span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span>
317
+ ${memory.type === "summary" || (memory.tags && memory.tags.includes("summary")) ? `<span class="memory-summary-badge" style="background:var(--accent-color);color:white;padding:2px 6px;border-radius:12px;margin-left:8px;font-size:0.8em;">Summary</span>` : ""}
318
+ ${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""}
319
+ <span class="memory-importance importance-${importanceLevel}" title="Importance: ${importancePct}% (${importanceLevel})">${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)}</span>
320
+ </div>
321
+ </div>
322
+ ${
323
+ !memory.title
324
+ ? `
325
+ <div class="memory-preview">
326
+ <div class="memory-preview-text ${isLongContent ? "memory-preview-short" : ""}" id="preview-${memory.id}">
327
+ ${this.highlightMemoryContent(previewText)}
328
+ </div>
329
+ ${
330
+ isLongContent
331
+ ? `
332
+ <div class="memory-preview-full" id="full-${memory.id}" style="display: none;">
333
+ ${this.highlightMemoryContent(memory.content)}
334
+ </div>
335
+ <button class="memory-expand-btn" onclick="kimiMemoryUI.toggleMemoryContent('${memory.id}')">
336
+ <i class="fas fa-chevron-down" id="icon-${memory.id}"></i> <span data-i18n="view_more"></span>
337
+ </button>
338
+ `
339
+ : ""
340
+ }
341
+ </div>
342
+ `
343
+ : ""
344
+ }
345
+ ${tagsHtml}
346
+ <div class="memory-meta">
347
+ <span class="memory-date">${this.formatDate(
348
+ // Robust fallback chain: createdAt -> timestamp -> lastAccess -> lastModified -> Date.now()
349
+ memory.createdAt || memory.timestamp || memory.lastAccess || memory.lastModified || Date.now()
350
+ )}</span>
351
+ ${
352
+ memory.sourceText
353
+ ? `<span class="memory-source" data-i18n="memory_source_label" title="${
354
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
355
+ ? window.KimiValidationUtils.escapeHtml(memory.sourceText)
356
+ : memory.sourceText
357
+ }"></span>`
358
+ : `<span data-i18n="memory_manually_added"></span>`
359
+ }
360
+ </div>
361
+ ${
362
+ memory.sourceText
363
+ ? `<div class="memory-source-content" id="source-content-${memory.id}" style="display:none;">${
364
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
365
+ ? window.KimiValidationUtils.escapeHtml(memory.sourceText)
366
+ : memory.sourceText
367
+ }</div>`
368
+ : ""
369
+ }
370
+ <div class="memory-actions">
371
+ <button class="memory-edit-btn" onclick="kimiMemoryUI.editMemory('${memory.id}')" data-i18n-title="edit_memory_button_title">
372
+ <i class="fas fa-edit"></i>
373
+ </button>
374
+ <button class="memory-delete-btn" onclick="kimiMemoryUI.deleteMemory('${memory.id}')" data-i18n-title="delete_memory_button_title">
375
+ <i class="fas fa-trash"></i>
376
+ </button>
377
+ </div>
378
+ </div>
379
+ `;
380
+ });
381
+
382
+ html += `
383
+ </div>
384
+ </div>
385
+ `;
386
+ });
387
+
388
+ // Minimal runtime guard: block accidental <script> tags in generated HTML
389
+ // This is a non-intrusive safety check that prevents XSS when the
390
+ // assembled `html` somehow contains script tags. In normal operation
391
+ // the content is escaped via KimiValidationUtils and highlightMemoryContent(),
392
+ // so this will not run; it only activates on suspicious input.
393
+ try {
394
+ if (/\<\s*script\b/i.test(html)) {
395
+ console.warn("Blocked suspicious <script> tag in memory HTML rendering");
396
+ // Fallback: render as safe text to avoid executing injected scripts
397
+ memoryList.textContent = html;
398
+ } else {
399
+ memoryList.innerHTML = html;
400
+ }
401
+ } catch (e) {
402
+ // On any unexpected error, fallback to safe text rendering
403
+ console.error("Error while rendering memories, falling back to safe text:", e);
404
+ memoryList.textContent = html;
405
+ }
406
+
407
+ // Apply translations to dynamic content
408
+ if (window.applyTranslations && typeof window.applyTranslations === "function") {
409
+ window.applyTranslations();
410
+ }
411
+ }
412
+
413
+ // Map importance value [0..1] to level string
414
+ getImportanceLevelFromValue(value) {
415
+ if (value >= 0.8) return "high";
416
+ if (value >= 0.6) return "medium";
417
+ return "low";
418
+ }
419
+
420
+ // Render tags as compact chips; show up to 4 then "+N"
421
+ renderTags(tags) {
422
+ if (!Array.isArray(tags) || tags.length === 0) return "";
423
+ const maxVisible = 4;
424
+ const visible = tags.slice(0, maxVisible);
425
+ const moreCount = tags.length - visible.length;
426
+
427
+ const escape = txt =>
428
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(String(txt)) : String(txt);
429
+
430
+ const classify = tag => {
431
+ if (tag.startsWith("relationship:")) return "tag-relationship";
432
+ if (tag.startsWith("boundary:")) return "tag-boundary";
433
+ if (tag.startsWith("time:")) return "tag-time";
434
+ if (tag.startsWith("type:")) return "tag-type";
435
+ return "tag-generic";
436
+ };
437
+
438
+ const chips = visible.map(tag => `<span class="memory-tag ${classify(tag)}" title="${escape(tag)}">${escape(tag)}</span>`).join("");
439
+
440
+ const moreChip = moreCount > 0 ? `<span class="memory-tag tag-more" title="${moreCount} more">+${moreCount}</span>` : "";
441
+
442
+ return `<div class="memory-tags">${chips}${moreChip}</div>`;
443
+ }
444
+
445
+ formatCategoryName(category) {
446
+ // Try to resolve via i18n keys first, fallback to hardcoded English
447
+ const i18nKey = `memory_category_${category}`;
448
+ if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") {
449
+ const val = window.kimiI18nManager.t(i18nKey);
450
+ if (val && val !== i18nKey) return val;
451
+ }
452
+
453
+ const names = {
454
+ personal: "Personal Information",
455
+ preferences: "Likes & Dislikes",
456
+ relationships: "Relationships & People",
457
+ activities: "Activities & Hobbies",
458
+ goals: "Goals & Aspirations",
459
+ experiences: "Shared Experiences",
460
+ important: "Important Events"
461
+ };
462
+
463
+ return names[category] || category.charAt(0).toUpperCase() + category.slice(1);
464
+ }
465
+
466
+ getConfidenceLevel(confidence) {
467
+ if (confidence >= 80) return "high";
468
+ if (confidence >= 60) return "medium";
469
+ return "low";
470
+ }
471
+
472
+ formatDate(raw) {
473
+ if (!raw) return "";
474
+ let date = null;
475
+ // Accept Date object, number, string
476
+ if (raw instanceof Date) {
477
+ date = raw;
478
+ } else {
479
+ // Some legacy records may store ISO string or Date object stringified
480
+ try {
481
+ date = new Date(raw);
482
+ } catch {
483
+ date = null;
484
+ }
485
+ }
486
+ if (!date || isNaN(date.getTime())) {
487
+ return ""; // Hide instead of showing 'Invalid Date'
488
+ }
489
+ const now = new Date();
490
+ const diffTime = now - date;
491
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
492
+ if (diffDays === 0) return "Today";
493
+ if (diffDays === 1) return "Yesterday";
494
+ if (diffDays < 7) return `${diffDays} days ago`;
495
+ return date.toLocaleDateString();
496
+ }
497
+
498
+ highlightMemoryContent(content) {
499
+ // Escape HTML first using centralized util
500
+ const escapedContent = window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(content) : content;
501
+
502
+ // Simple highlighting for search terms if there's a search active
503
+ const searchInput = document.getElementById("memory-search");
504
+ if (searchInput && searchInput.value.trim()) {
505
+ const searchTerm = searchInput.value.trim();
506
+ const regex = new RegExp(`(${searchTerm})`, "gi");
507
+ return escapedContent.replace(
508
+ regex,
509
+ '<mark style="background: var(--primary-color); color: white; padding: 1px 3px; border-radius: 2px;">$1</mark>'
510
+ );
511
+ }
512
+
513
+ return escapedContent;
514
+ }
515
+
516
+ getCategoryIcon(category) {
517
+ const icons = {
518
+ personal: "👤",
519
+ preferences: "❤️",
520
+ relationships: "👨‍👩‍👧‍👦",
521
+ activities: "🎯",
522
+ goals: "🎯",
523
+ experiences: "⭐",
524
+ important: "📌"
525
+ };
526
+ return icons[category] || "📝";
527
+ }
528
+
529
+ toggleMemoryContent(memoryId) {
530
+ const previewShort = document.getElementById(`preview-${memoryId}`);
531
+ const previewFull = document.getElementById(`full-${memoryId}`);
532
+ const icon = document.getElementById(`icon-${memoryId}`);
533
+ const expandBtn = icon?.closest(".memory-expand-btn");
534
+
535
+ if (!previewShort || !previewFull || !icon || !expandBtn) return;
536
+
537
+ const isExpanded = previewFull.style.display !== "none";
538
+
539
+ if (isExpanded) {
540
+ previewShort.style.display = "block";
541
+ previewFull.style.display = "none";
542
+ icon.className = "fas fa-chevron-down";
543
+ expandBtn.innerHTML = '<i class="fas fa-chevron-down"></i> <span data-i18n="view_more"></span>';
544
+ } else {
545
+ previewShort.style.display = "none";
546
+ previewFull.style.display = "block";
547
+ icon.className = "fas fa-chevron-up";
548
+ expandBtn.innerHTML = '<i class="fas fa-chevron-up"></i> <span data-i18n="view_less"></span>';
549
+ }
550
+
551
+ // Re-apply translations for the new button label
552
+ if (window.applyTranslations && typeof window.applyTranslations === "function") {
553
+ window.applyTranslations();
554
+ }
555
+ }
556
+
557
+ // Handle delegated click/touch events for .memory-source
558
+ handleMemorySourceToggle(e) {
559
+ try {
560
+ const el = e.target.closest && e.target.closest(".memory-source");
561
+ if (!el) return;
562
+ // Prevent triggering parent handlers
563
+ e.stopPropagation();
564
+ e.preventDefault();
565
+ this.toggleSourceContentForElement(el);
566
+ } catch (err) {
567
+ console.error("Error handling memory-source toggle", err);
568
+ }
569
+ }
570
+
571
+ // Toggle the adjacent .memory-source-content for a given .memory-source element
572
+ toggleSourceContentForElement(sourceEl) {
573
+ if (!sourceEl) return;
574
+ // The memory id is present on the nearest .memory-item
575
+ const item = sourceEl.closest(".memory-item");
576
+ if (!item) return;
577
+ const id = item.getAttribute("data-memory-id");
578
+ if (!id) return;
579
+
580
+ const contentEl = document.getElementById(`source-content-${id}`);
581
+ if (!contentEl) return;
582
+
583
+ const isVisible = contentEl.style.display !== "none" && contentEl.style.display !== "";
584
+
585
+ // Close any other open source contents
586
+ document.querySelectorAll(".memory-source-content").forEach(el => {
587
+ if (el !== contentEl) el.style.display = "none";
588
+ });
589
+
590
+ if (isVisible) {
591
+ contentEl.style.display = "none";
592
+ sourceEl.setAttribute("aria-expanded", "false");
593
+ } else {
594
+ // simple sliding animation via max-height for smoother appearance
595
+ contentEl.style.display = "block";
596
+ sourceEl.setAttribute("aria-expanded", "true");
597
+ }
598
+ }
599
+
600
+ async editMemory(memoryId) {
601
+ if (!this.memorySystem) return;
602
+
603
+ try {
604
+ // Get the memory to edit
605
+ const memories = await this.memorySystem.getAllMemories();
606
+ const memory = memories.find(m => m.id == memoryId);
607
+ if (!memory) {
608
+ this.showFeedback("Memory not found", "error");
609
+ return;
610
+ }
611
+
612
+ // Create edit dialog
613
+ const overlay = document.createElement("div");
614
+ overlay.className = "memory-edit-overlay";
615
+ overlay.style.cssText = `
616
+ position: fixed;
617
+ top: 0;
618
+ left: 0;
619
+ width: 100%;
620
+ height: 100%;
621
+ background: rgba(0, 0, 0, 0.8);
622
+ display: flex;
623
+ justify-content: center;
624
+ align-items: center;
625
+ z-index: 10001;
626
+ `;
627
+
628
+ const dialog = document.createElement("div");
629
+ dialog.className = "memory-edit-dialog";
630
+ dialog.style.cssText = `
631
+ background: var(--background-secondary);
632
+ border-radius: 12px;
633
+ padding: 24px;
634
+ width: 90%;
635
+ max-width: 500px;
636
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
637
+ `;
638
+
639
+ dialog.innerHTML = `
640
+ <h3 style="margin: 0 0 20px 0; color: var(--text-primary);">
641
+ <i class="fas fa-edit"></i> <span data-i18n="edit_memory_title"></span>
642
+ </h3>
643
+ <div style="margin-bottom: 12px;">
644
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;" data-i18n="label_title"></label>
645
+ <input id="edit-memory-title" class="kimi-input" style="width:100%;" value="${
646
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
647
+ ? window.KimiValidationUtils.escapeHtml(memory.title || "")
648
+ : memory.title || ""
649
+ }" placeholder="Optional title for this memory" />
650
+ </div>
651
+ <div style="margin-bottom: 16px;">
652
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;" data-i18n="label_category"></label>
653
+ <select id="edit-memory-category" class="kimi-select" style="width: 100%;">
654
+ <option value="personal" ${memory.category === "personal" ? "selected" : ""}>${
655
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_personal") : "Personal Info"
656
+ }</option>
657
+ <option value="preferences" ${memory.category === "preferences" ? "selected" : ""}>${
658
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_preferences") : "Likes & Dislikes"
659
+ }</option>
660
+ <option value="relationships" ${memory.category === "relationships" ? "selected" : ""}>${
661
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_relationships") : "Relationships"
662
+ }</option>
663
+ <option value="activities" ${memory.category === "activities" ? "selected" : ""}>${
664
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_activities") : "Activities & Hobbies"
665
+ }</option>
666
+ <option value="goals" ${memory.category === "goals" ? "selected" : ""}>${
667
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_goals") : "Goals & Plans"
668
+ }</option>
669
+ <option value="experiences" ${memory.category === "experiences" ? "selected" : ""}>${
670
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_experiences") : "Experiences"
671
+ }</option>
672
+ <option value="important" ${memory.category === "important" ? "selected" : ""}>${
673
+ window.kimiI18nManager && window.kimiI18nManager.t ? window.kimiI18nManager.t("memory_category_important") : "Important Events"
674
+ }</option>
675
+ </select>
676
+ </div>
677
+ <div style="margin-bottom: 20px;">
678
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;" data-i18n="label_content"></label>
679
+ <textarea id="edit-memory-content" class="kimi-input" style="width: 100%; height: 100px; resize: vertical;" placeholder="Memory content...">${
680
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
681
+ ? window.KimiValidationUtils.escapeHtml(memory.sourceText || memory.content || "")
682
+ : memory.sourceText || memory.content || ""
683
+ }</textarea>
684
+ </div>
685
+ <div style="display: flex; gap: 12px; justify-content: flex-end;">
686
+ <button id="cancel-edit" class="kimi-button" style="background: #6c757d;" data-i18n="cancel">
687
+ <i class="fas fa-times"></i> <span data-i18n="cancel"></span>
688
+ </button>
689
+ <button id="save-edit" class="kimi-button">
690
+ <i class="fas fa-save"></i> <span data-i18n="save"></span>
691
+ </button>
692
+ </div>
693
+ `;
694
+
695
+ overlay.appendChild(dialog);
696
+ document.body.appendChild(overlay);
697
+
698
+ // Handle buttons
699
+ dialog.querySelector("#cancel-edit").addEventListener("click", () => {
700
+ document.body.removeChild(overlay);
701
+ });
702
+
703
+ dialog.querySelector("#save-edit").addEventListener("click", async () => {
704
+ const newTitle = dialog.querySelector("#edit-memory-title").value.trim();
705
+ const newCategory = dialog.querySelector("#edit-memory-category").value;
706
+ const newContent = dialog.querySelector("#edit-memory-content").value.trim();
707
+
708
+ if (!newContent) {
709
+ this.showFeedback(window.kimiI18nManager ? window.kimiI18nManager.t("validation_empty_message") : "Content cannot be empty", "error");
710
+ return;
711
+ }
712
+
713
+ console.log(`🔄 Attempting to update memory ID: ${memoryId}`);
714
+ console.log("New data:", { category: newCategory, content: newContent });
715
+
716
+ try {
717
+ // Also update sourceText so the "📝 Memory of the conversation" shows edited content
718
+ const result = await this.memorySystem.updateMemory(memoryId, {
719
+ title: newTitle,
720
+ category: newCategory,
721
+ content: newContent,
722
+ sourceText: newContent
723
+ });
724
+
725
+ console.log("Update result:", result);
726
+
727
+ if (result === true) {
728
+ // Close the modal
729
+ document.body.removeChild(overlay);
730
+
731
+ // Force full reload
732
+ await this.loadMemories();
733
+ await this.updateMemoryStats();
734
+
735
+ this.showFeedback(window.kimiI18nManager ? window.kimiI18nManager.t("saved") : "Saved");
736
+ console.log("✅ UI updated");
737
+ } else {
738
+ this.showFeedback(
739
+ window.kimiI18nManager ? window.kimiI18nManager.t("fallback_general_error") : "Error: unable to update memory",
740
+ "error"
741
+ );
742
+ console.error("❌ Update failed, result:", result);
743
+ }
744
+ } catch (error) {
745
+ console.error("Error updating memory:", error);
746
+ this.showFeedback(window.kimiI18nManager ? window.kimiI18nManager.t("fallback_general_error") : "Error updating memory", "error");
747
+ }
748
+ });
749
+
750
+ // Close on overlay click
751
+ overlay.addEventListener("click", e => {
752
+ if (e.target === overlay) {
753
+ document.body.removeChild(overlay);
754
+ }
755
+ });
756
+ } catch (error) {
757
+ console.error("Error editing memory:", error);
758
+ this.showFeedback("Error loading memory for editing", "error");
759
+ }
760
+ }
761
+
762
+ async deleteMemory(memoryId) {
763
+ if (!confirm("Are you sure you want to delete this memory?")) return;
764
+
765
+ try {
766
+ await this.memorySystem.deleteMemory(memoryId);
767
+ await this.loadMemories();
768
+ // Debounced stats update
769
+ this.debounce("updateStats", async () => await this.updateMemoryStats());
770
+ this.showFeedback("Memory deleted");
771
+ } catch (error) {
772
+ console.error("Error deleting memory:", error);
773
+ this.showFeedback("Error deleting memory", "error");
774
+ }
775
+ }
776
+
777
+ async exportMemories() {
778
+ if (!this.memorySystem) return;
779
+
780
+ try {
781
+ const exportData = await this.memorySystem.exportMemories();
782
+ if (exportData) {
783
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
784
+ type: "application/json"
785
+ });
786
+ const url = URL.createObjectURL(blob);
787
+ const a = document.createElement("a");
788
+ a.href = url;
789
+ a.download = `kimi-memories-${new Date().toISOString().split("T")[0]}.json`;
790
+ a.click();
791
+ URL.revokeObjectURL(url);
792
+ this.showFeedback("Memories exported successfully");
793
+ }
794
+ } catch (error) {
795
+ console.error("Error exporting memories:", error);
796
+ this.showFeedback("Error exporting memories", "error");
797
+ }
798
+ }
799
+
800
+ async handleSummarizeAction() {
801
+ if (!this.memorySystem) return;
802
+ try {
803
+ // Destructive confirmation modal
804
+ const confirmMsg = `This action will create a single summary memory for the last 7 days and permanently DELETE the source memories. This is irreversible. Do you want to continue?`;
805
+ if (!confirm(confirmMsg)) {
806
+ this.showFeedback("Summary canceled");
807
+ return;
808
+ }
809
+
810
+ this.showFeedback("Creating summary and replacing sources...");
811
+ const result = await this.memorySystem.summarizeAndReplace(7, {});
812
+ if (result) {
813
+ this.showFeedback("Summary created and sources deleted");
814
+ await this.loadMemories();
815
+ await this.updateMemoryStats();
816
+ } else {
817
+ this.showFeedback("No recent memories to summarize");
818
+ }
819
+ } catch (e) {
820
+ console.error("Error creating destructive summary", e);
821
+ this.showFeedback("Error creating summary", "error");
822
+ }
823
+ }
824
+
825
+ async updateMemoryStats() {
826
+ if (!this.memorySystem) return;
827
+
828
+ try {
829
+ const stats = await this.memorySystem.getMemoryStats();
830
+ const memoryCount = document.getElementById("memory-count");
831
+ const memoryToggle = document.getElementById("memory-toggle");
832
+
833
+ if (memoryCount) {
834
+ memoryCount.textContent = `${stats.total} memories`;
835
+ }
836
+
837
+ // Update toggle state
838
+ if (memoryToggle) {
839
+ const enabled = this.memorySystem.memoryEnabled;
840
+ memoryToggle.setAttribute("aria-checked", enabled.toString());
841
+ memoryToggle.classList.toggle("active", enabled);
842
+
843
+ // Add visual indicator for memory status
844
+ const indicator = memoryToggle.querySelector(".memory-indicator") || document.createElement("div");
845
+ if (!memoryToggle.querySelector(".memory-indicator")) {
846
+ indicator.className = "memory-indicator";
847
+ memoryToggle.appendChild(indicator);
848
+ }
849
+ indicator.style.cssText = `
850
+ position: absolute;
851
+ top: -2px;
852
+ right: -2px;
853
+ width: 8px;
854
+ height: 8px;
855
+ border-radius: 50%;
856
+ background: ${enabled ? "#27ae60" : "#e74c3c"};
857
+ border: 2px solid white;
858
+ `;
859
+ }
860
+ } catch (error) {
861
+ console.error("Error updating memory stats:", error);
862
+ }
863
+ }
864
+
865
+ // Force refresh of the memory UI (useful for debugging)
866
+ async forceRefresh() {
867
+ console.log("🔄 Force refresh of memory UI...");
868
+ try {
869
+ if (this.memorySystem) {
870
+ // Migrate IDs if necessary
871
+ await this.memorySystem.migrateIncompatibleIDs();
872
+
873
+ // Reload memories
874
+ await this.loadMemories();
875
+ await this.updateMemoryStats();
876
+
877
+ console.log("✅ Forced refresh completed");
878
+ }
879
+ } catch (error) {
880
+ console.error("❌ Error during forced refresh:", error);
881
+ }
882
+ }
883
+
884
+ showFeedback(message, type = "success") {
885
+ // Create feedback element
886
+ const feedback = document.createElement("div");
887
+ feedback.className = `memory-feedback memory-feedback-${type}`;
888
+ feedback.textContent = message;
889
+
890
+ // Style the feedback based on type
891
+ let backgroundColor;
892
+ switch (type) {
893
+ case "error":
894
+ backgroundColor = "#e74c3c";
895
+ break;
896
+ case "info":
897
+ backgroundColor = "#3498db";
898
+ break;
899
+ default:
900
+ backgroundColor = "#27ae60";
901
+ }
902
+
903
+ // Style the feedback
904
+ Object.assign(feedback.style, {
905
+ position: "fixed",
906
+ top: "20px",
907
+ right: "20px",
908
+ padding: "12px 20px",
909
+ borderRadius: "6px",
910
+ color: "white",
911
+ backgroundColor: backgroundColor,
912
+ boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
913
+ zIndex: "10000",
914
+ fontSize: "14px",
915
+ fontWeight: "500",
916
+ opacity: "0",
917
+ transform: "translateX(100%)",
918
+ transition: "all 0.3s ease"
919
+ });
920
+
921
+ document.body.appendChild(feedback);
922
+
923
+ // Animate in
924
+ setTimeout(() => {
925
+ feedback.style.opacity = "1";
926
+ feedback.style.transform = "translateX(0)";
927
+ }, 10);
928
+
929
+ // Remove after delay (longer for info messages, shorter for others)
930
+ const delay = type === "info" ? 2000 : 3000;
931
+ setTimeout(() => {
932
+ feedback.style.opacity = "0";
933
+ feedback.style.transform = "translateX(100%)";
934
+ setTimeout(() => {
935
+ if (feedback.parentNode) {
936
+ feedback.parentNode.removeChild(feedback);
937
+ }
938
+ }, 300);
939
+ }, delay);
940
+ }
941
+ }
942
+
943
+ // Initialize memory UI when DOM is ready
944
+ document.addEventListener("DOMContentLoaded", async () => {
945
+ window.kimiMemoryUI = new KimiMemoryUI();
946
+
947
+ // Wait for memory system to be ready
948
+ const waitForMemorySystem = () => {
949
+ if (window.kimiMemorySystem) {
950
+ window.kimiMemoryUI.init();
951
+ } else {
952
+ setTimeout(waitForMemorySystem, 100);
953
+ }
954
+ };
955
+
956
+ setTimeout(waitForMemorySystem, 1000); // Give time for initialization
957
+ });
958
+
959
+ window.KimiMemoryUI = KimiMemoryUI;
960
+
961
+ function ensureVideoNeutralOnUIChange() {
962
+ if (window.kimiVideo && window.kimiVideo.getCurrentVideoInfo) {
963
+ const info = window.kimiVideo.getCurrentVideoInfo();
964
+ if (info && info.ended) window.kimiVideo.returnToNeutral();
965
+ }
966
+ }
kimi-js/kimi-memory.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI MEMORY MANAGER =====
2
+ class KimiMemory {
3
+ constructor(database) {
4
+ this.db = database;
5
+ this.preferences = {
6
+ voiceRate: 1.1,
7
+ voicePitch: 1.1,
8
+ voiceVolume: 0.8,
9
+ lastInteraction: null,
10
+ totalInteractions: 0,
11
+ emotionalState: "neutral"
12
+ };
13
+ this.isReady = false;
14
+ // affectionTrait will be loaded from database during init()
15
+ this.affectionTrait = 50; // Temporary default until loaded from DB
16
+ }
17
+
18
+ async init() {
19
+ if (!this.db) {
20
+ console.warn("Database not available, using local mode");
21
+ return;
22
+ }
23
+ try {
24
+ this.selectedCharacter = await this.db.getSelectedCharacter();
25
+
26
+ // Load affection trait from personality database with unified defaults
27
+ const charDefAff = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[this.selectedCharacter]?.traits?.affection) || null;
28
+ const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { affection: 55 };
29
+ const defaultAff = typeof charDefAff === "number" ? charDefAff : unifiedDefaults.affection;
30
+ this.affectionTrait = await this.db.getPersonalityTrait("affection", defaultAff, this.selectedCharacter);
31
+
32
+ this.preferences = {
33
+ voiceRate: await this.db.getPreference("voiceRate", 1.1),
34
+ voicePitch: await this.db.getPreference("voicePitch", 1.1),
35
+ voiceVolume: await this.db.getPreference("voiceVolume", 0.8),
36
+ lastInteraction: await this.db.getPreference(`lastInteraction_${this.selectedCharacter}`, null),
37
+ totalInteractions: await this.db.getPreference(`totalInteractions_${this.selectedCharacter}`, 0),
38
+ emotionalState: await this.db.getPreference(`emotionalState_${this.selectedCharacter}`, "neutral")
39
+ };
40
+ // affectionTrait already loaded above with coherent default
41
+ this.isReady = true;
42
+ this.updateFavorabilityBar();
43
+ } catch (error) {
44
+ console.error("KimiMemory initialization error:", error);
45
+ }
46
+ }
47
+
48
+ async saveConversation(userText, kimiResponse, tokenInfo = null) {
49
+ if (!this.db) return;
50
+
51
+ try {
52
+ const character = await this.db.getSelectedCharacter();
53
+
54
+ // Use global personality average for conversation favorability score
55
+ let relationshipLevel = 50; // fallback
56
+ try {
57
+ const traits = window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character);
58
+ relationshipLevel = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
59
+ } catch (error) {
60
+ console.warn("Error calculating relationship level for conversation:", error);
61
+ }
62
+
63
+ await this.db.saveConversation(userText, kimiResponse, relationshipLevel, new Date(), character);
64
+
65
+ // Legacy interactions counter kept for backward compatibility (not shown in UI now)
66
+ let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
67
+ total = Number(total) + 1;
68
+ await this.db.setPreference(`totalInteractions_${character}`, total);
69
+ this.preferences.totalInteractions = total;
70
+
71
+ // Update tokens usage if provided (in/out)
72
+ if (tokenInfo && typeof tokenInfo.tokensIn === "number" && typeof tokenInfo.tokensOut === "number") {
73
+ const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0;
74
+ const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0;
75
+ await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokenInfo.tokensIn);
76
+ await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokenInfo.tokensOut);
77
+ }
78
+
79
+ let first = await this.db.getPreference(`firstInteraction_${character}`, null);
80
+ if (!first) {
81
+ first = new Date().toISOString();
82
+ await this.db.setPreference(`firstInteraction_${character}`, first);
83
+ }
84
+
85
+ this.preferences.lastInteraction = new Date().toISOString();
86
+ await this.db.setPreference(`lastInteraction_${character}`, this.preferences.lastInteraction);
87
+ } catch (error) {
88
+ console.error("Error saving conversation:", error);
89
+ }
90
+ }
91
+
92
+ async updateFavorability(change) {
93
+ try {
94
+ this.affectionTrait = Math.max(0, Math.min(100, this.affectionTrait + change));
95
+ if (this.db) {
96
+ await this.db.setPersonalityTrait("affection", this.affectionTrait, this.selectedCharacter);
97
+ }
98
+ this.updateFavorabilityBar();
99
+ } catch (error) {
100
+ console.error("Error updating favorability:", error);
101
+ }
102
+ }
103
+
104
+ async updateAffectionTrait() {
105
+ if (!this.db) return;
106
+
107
+ try {
108
+ this.selectedCharacter = await this.db.getSelectedCharacter();
109
+ // Use unified default that matches KimiEmotionSystem
110
+ const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || { affection: 55 };
111
+ this.affectionTrait = await this.db.getPersonalityTrait("affection", unifiedDefaults.affection, this.selectedCharacter);
112
+ this.updateFavorabilityBar();
113
+ } catch (error) {
114
+ console.error("Error updating affection trait:", error);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * @deprecated Use updateGlobalPersonalityUI().
120
+ * Thin wrapper retained for backward compatibility only.
121
+ */
122
+ updateFavorabilityBar() {
123
+ if (window.updateGlobalPersonalityUI) {
124
+ window.updateGlobalPersonalityUI();
125
+ }
126
+ }
127
+
128
+ async getGreeting() {
129
+ const i18n = window.kimiI18nManager;
130
+
131
+ // Use global personality average instead of just affection trait
132
+ let relationshipLevel = 50; // fallback
133
+ try {
134
+ if (this.db) {
135
+ const traits = window.getCharacterTraits
136
+ ? await window.getCharacterTraits(this.selectedCharacter)
137
+ : await this.db.getAllPersonalityTraits(this.selectedCharacter);
138
+ relationshipLevel = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
139
+ }
140
+ } catch (error) {
141
+ console.warn("Error calculating greeting level:", error);
142
+ }
143
+
144
+ if (relationshipLevel <= 10) {
145
+ return i18n?.t("greeting_low") || "Hello.";
146
+ }
147
+ if (relationshipLevel < 40) {
148
+ return i18n?.t("greeting_mid") || "Hi. How can I help you?";
149
+ }
150
+ return i18n?.t("greeting_high") || "Hello my love! 💕";
151
+ }
152
+ }
153
+
154
+ // Export to global scope
155
+ window.KimiMemory = KimiMemory;
156
+ export default KimiMemory;
kimi-js/kimi-module.js ADDED
@@ -0,0 +1,1949 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // KIMI MODULE SYSTEM
2
+ // KimiDataManager has been extracted to kimi-data-manager.js
3
+ // (kept global via window.KimiDataManager)
4
+
5
+ // Fonctions utilitaires et logique (référencent window.*)
6
+
7
+ // Helper function to avoid video logic duplication
8
+ async function handleVideoResponseWithFallback(sanitizedText, response, updatedTraits, selectedCharacter) {
9
+ if (window.kimiVideoController) {
10
+ // SIMPLE API
11
+ window.kimiVideoController.playVideo("user", sanitizedText);
12
+ if (response) {
13
+ window.kimiVideoController.playVideo("llm", response);
14
+ }
15
+ } else {
16
+ // Fallback to legacy logic
17
+ const lang = (await window.kimiDB?.getPreference("selectedLanguage", "en")) || "en";
18
+ const keywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {};
19
+ // Centralized dancing detection via hasKeywordCategory
20
+ const userAskedDance = (window.hasKeywordCategory && window.hasKeywordCategory("dancing", sanitizedText, lang)) || false;
21
+
22
+ if (userAskedDance && window.kimiVideo) {
23
+ window.kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection, true);
24
+ } else if (window.kimiVideo) {
25
+ const userEmotion = window.kimiEmotionSystem?.analyzeEmotionValidated(sanitizedText) || "neutral";
26
+ if (userEmotion === "negative") {
27
+ window.kimiVideo.respondWithEmotion("negative", updatedTraits, updatedTraits.affection);
28
+ } else if (response) {
29
+ const responseEmotion = window.kimiEmotionSystem?.analyzeEmotionValidated(response) || "positive";
30
+ // User negative override already handled; else use response emotion
31
+ window.kimiVideo.respondWithEmotion(responseEmotion, updatedTraits, updatedTraits.affection);
32
+ } else {
33
+ window.kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection);
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ function updateFavorabilityLabel(characterKey) {
40
+ const favorabilityLabel = document.getElementById("favorability-label");
41
+ if (favorabilityLabel && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[characterKey]) {
42
+ // New semantics: show overall personality average (independent display)
43
+ favorabilityLabel.removeAttribute("for"); // decouple from any specific slider
44
+ if (window.setI18n) window.setI18n(favorabilityLabel, "personality_average_of", { name: window.KIMI_CHARACTERS[characterKey].name });
45
+ favorabilityLabel.textContent = `💖 Personality average of ${window.KIMI_CHARACTERS[characterKey].name}`;
46
+ if (!favorabilityLabel.getAttribute("title")) {
47
+ favorabilityLabel.setAttribute("title", "Average of (Affection + Playfulness + Intelligence + Empathy + Humor + Romance) / 6");
48
+ }
49
+ applyTranslations();
50
+ }
51
+ }
52
+
53
+ // Simplified personality average computation using centralized system
54
+ function computePersonalityAverage(traits) {
55
+ return window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
56
+ }
57
+
58
+ // Update UI elements (bar + percentage text + label) based on overall personality average
59
+ async function updateGlobalPersonalityUI(characterKey = null) {
60
+ try {
61
+ const db = window.kimiDB;
62
+ if (!db) return;
63
+ const character = characterKey || (await db.getSelectedCharacter());
64
+ const traits = window.getCharacterTraits ? await window.getCharacterTraits(character) : await db.getAllPersonalityTraits(character);
65
+ const avg = computePersonalityAverage(traits);
66
+ // Reuse existing favorability bar elements for global average
67
+ const bar = document.getElementById("favorability-bar");
68
+ const text = document.getElementById("favorability-text");
69
+ if (bar) bar.style.width = `${avg}%`;
70
+ if (text) text.textContent = `${avg.toFixed(2)}%`;
71
+ // Update label content if character provided
72
+ updateFavorabilityLabel(character);
73
+ } catch (e) {
74
+ console.warn("Failed to update global personality UI", e);
75
+ }
76
+ }
77
+ window.updateGlobalPersonalityUI = updateGlobalPersonalityUI;
78
+
79
+ async function loadCharacterSection() {
80
+ const kimiDB = window.kimiDB;
81
+ if (!kimiDB) return;
82
+ const characterGrid = document.getElementById("character-grid");
83
+ if (!characterGrid) return;
84
+ while (characterGrid.firstChild) {
85
+ characterGrid.removeChild(characterGrid.firstChild);
86
+ }
87
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
88
+ for (const [key, info] of Object.entries(window.KIMI_CHARACTERS)) {
89
+ const card = document.createElement("div");
90
+ card.className = `character-card${key === selectedCharacter ? " selected" : ""}`;
91
+ card.dataset.character = key;
92
+
93
+ // Create character card elements safely
94
+ const img = document.createElement("img");
95
+ img.src = info.image;
96
+ img.alt = info.name;
97
+
98
+ const infoDiv = document.createElement("div");
99
+ infoDiv.className = "character-info";
100
+
101
+ const nameDiv = document.createElement("div");
102
+ nameDiv.className = "character-name";
103
+ nameDiv.textContent = info.name;
104
+
105
+ const detailsDiv = document.createElement("div");
106
+ detailsDiv.className = "character-details";
107
+
108
+ const ageDiv = document.createElement("div");
109
+ ageDiv.className = "character-age";
110
+ if (window.setI18n) window.setI18n(ageDiv, "character_age", { age: info.age });
111
+
112
+ const birthplaceDiv = document.createElement("div");
113
+ birthplaceDiv.className = "character-birthplace";
114
+ if (window.setI18n) window.setI18n(birthplaceDiv, "character_birthplace", { birthplace: info.birthplace });
115
+
116
+ const summaryDiv = document.createElement("div");
117
+ summaryDiv.className = "character-summary";
118
+ if (window.setI18n) window.setI18n(summaryDiv, `character_summary_${key}`);
119
+
120
+ detailsDiv.appendChild(ageDiv);
121
+ detailsDiv.appendChild(birthplaceDiv);
122
+ detailsDiv.appendChild(summaryDiv);
123
+
124
+ infoDiv.appendChild(nameDiv);
125
+ infoDiv.appendChild(detailsDiv);
126
+
127
+ const promptLabel = document.createElement("div");
128
+ promptLabel.className = "character-prompt-label";
129
+ if (window.setI18n) window.setI18n(promptLabel, "system_prompt");
130
+ promptLabel.textContent = "System Prompt";
131
+
132
+ const promptInput = document.createElement("textarea");
133
+ promptInput.className = "character-prompt-input";
134
+ promptInput.id = `prompt-${key}`;
135
+ promptInput.rows = 6;
136
+
137
+ // Create buttons container
138
+ const buttonsContainer = document.createElement("div");
139
+ buttonsContainer.className = "character-prompt-buttons";
140
+
141
+ // Save button
142
+ const saveButton = document.createElement("button");
143
+ saveButton.className = "kimi-button character-save-btn";
144
+ saveButton.id = `save-${key}`;
145
+ if (window.setI18n) window.setI18n(saveButton, "save");
146
+ saveButton.textContent = "Save";
147
+
148
+ // Reset button
149
+ const resetButton = document.createElement("button");
150
+ resetButton.className = "kimi-button character-reset-btn";
151
+ resetButton.id = `reset-${key}`;
152
+ if (window.setI18n) window.setI18n(resetButton, "reset_to_default");
153
+ resetButton.textContent = "Reset to Default";
154
+
155
+ buttonsContainer.appendChild(saveButton);
156
+ buttonsContainer.appendChild(resetButton);
157
+
158
+ card.appendChild(img);
159
+ card.appendChild(infoDiv);
160
+ card.appendChild(promptLabel);
161
+ card.appendChild(promptInput);
162
+ card.appendChild(buttonsContainer);
163
+ characterGrid.appendChild(card);
164
+ }
165
+ applyTranslations();
166
+
167
+ // Initialize prompt values and button event listeners
168
+ for (const key of Object.keys(window.KIMI_CHARACTERS)) {
169
+ const promptInput = document.getElementById(`prompt-${key}`);
170
+ const saveButton = document.getElementById(`save-${key}`);
171
+ const resetButton = document.getElementById(`reset-${key}`);
172
+
173
+ if (promptInput) {
174
+ const prompt = await kimiDB.getSystemPromptForCharacter(key);
175
+ promptInput.value = prompt;
176
+ promptInput.disabled = key !== selectedCharacter;
177
+ }
178
+
179
+ // Save button event listener
180
+ if (saveButton) {
181
+ saveButton.addEventListener("click", async () => {
182
+ if (promptInput) {
183
+ await kimiDB.setSystemPromptForCharacter(key, promptInput.value);
184
+
185
+ // Visual feedback
186
+ const originalText = saveButton.textContent;
187
+ saveButton.textContent = "Saved!";
188
+ saveButton.classList.add("success");
189
+ saveButton.disabled = true;
190
+
191
+ setTimeout(() => {
192
+ if (window.setI18n) window.setI18n(saveButton, "save");
193
+ applyTranslations();
194
+ saveButton.classList.remove("success");
195
+ saveButton.disabled = false;
196
+ }, 1500);
197
+
198
+ // Refresh personality if this is the selected character
199
+ if (key === selectedCharacter && window.kimiLLM && window.kimiLLM.refreshMemoryContext) {
200
+ await window.kimiLLM.refreshMemoryContext();
201
+ }
202
+ }
203
+ });
204
+ }
205
+
206
+ // Reset button event listener
207
+ if (resetButton) {
208
+ resetButton.addEventListener("click", async () => {
209
+ const defaultPrompt = window.KIMI_CHARACTERS[key]?.defaultPrompt || "";
210
+ if (promptInput) {
211
+ promptInput.value = defaultPrompt;
212
+ await kimiDB.setSystemPromptForCharacter(key, defaultPrompt);
213
+
214
+ // Visual feedback
215
+ const originalText = resetButton.textContent;
216
+ resetButton.textContent = "Reset!";
217
+ resetButton.classList.add("animated");
218
+ if (window.setI18n) window.setI18n(resetButton, "reset_done");
219
+ applyTranslations();
220
+
221
+ setTimeout(() => {
222
+ if (window.setI18n) window.setI18n(resetButton, "reset_to_default");
223
+ applyTranslations();
224
+ resetButton.classList.remove("animated");
225
+ }, 1500);
226
+
227
+ // Refresh personality if this is the selected character
228
+ if (key === selectedCharacter && window.kimiLLM && window.kimiLLM.refreshMemoryContext) {
229
+ await window.kimiLLM.refreshMemoryContext();
230
+ }
231
+ }
232
+ });
233
+ }
234
+ }
235
+ characterGrid.querySelectorAll(".character-card").forEach(card => {
236
+ card.addEventListener("click", async () => {
237
+ characterGrid.querySelectorAll(".character-card").forEach(c => c.classList.remove("selected"));
238
+ card.classList.add("selected");
239
+ const charKey = card.dataset.character;
240
+ for (const key of Object.keys(window.KIMI_CHARACTERS)) {
241
+ const promptInput = document.getElementById(`prompt-${key}`);
242
+ const saveButton = document.getElementById(`save-${key}`);
243
+ const resetButton = document.getElementById(`reset-${key}`);
244
+
245
+ if (promptInput) promptInput.disabled = key !== charKey;
246
+ if (saveButton) saveButton.disabled = key !== charKey;
247
+ if (resetButton) resetButton.disabled = key !== charKey;
248
+ }
249
+ updateFavorabilityLabel(charKey);
250
+ const chatHeaderName = document.querySelector(".chat-header span[data-i18n]");
251
+ if (chatHeaderName) {
252
+ const info = window.KIMI_CHARACTERS[charKey] || window.KIMI_CHARACTERS.kimi;
253
+ if (window.setI18n) window.setI18n(chatHeaderName, `chat_with_${charKey}`);
254
+ applyTranslations();
255
+ }
256
+
257
+ // Update personality trait sliders with selected character's traits
258
+ await updatePersonalitySliders(charKey);
259
+ });
260
+ });
261
+
262
+ // Initialize personality sliders with current selected character's traits
263
+ await updatePersonalitySliders(selectedCharacter);
264
+ }
265
+
266
+ async function getBasicResponse(reaction) {
267
+ // Use centralized fallback manager instead of duplicated logic
268
+ if (window.KimiFallbackManager) {
269
+ return await window.KimiFallbackManager.getEmotionalResponse(reaction);
270
+ }
271
+
272
+ // Fallback to legacy system if KimiFallbackManager not available
273
+ const i18n = window.kimiI18nManager;
274
+ return i18n ? i18n.t("fallback_technical_error") : "Sorry, I'm having technical difficulties! 💕";
275
+ }
276
+
277
+ // Déporté vers KimiEmotionSystem: utiliser window.updatePersonalityTraitsFromEmotion
278
+
279
+ async function analyzeAndReact(text, useAdvancedLLM = true, onStreamToken = null) {
280
+ const kimiDB = window.kimiDB;
281
+ const kimiLLM = window.kimiLLM;
282
+ const kimiVideo = window.kimiVideo;
283
+ const kimiMemory = window.kimiMemory;
284
+ const isSystemReady = window.isSystemReady;
285
+
286
+ try {
287
+ // Validate and sanitize input
288
+ if (!text || typeof text !== "string") {
289
+ throw new Error("Invalid input text");
290
+ }
291
+
292
+ const sanitizedText = window.KimiSecurityUtils?.sanitizeInput(text) || text.trim();
293
+ if (!sanitizedText) {
294
+ throw new Error("Empty input after sanitization");
295
+ }
296
+
297
+ const lowerText = sanitizedText.toLowerCase();
298
+ let reaction = window.kimiAnalyzeEmotion(sanitizedText, "auto");
299
+ let emotionIntensity = 0;
300
+ let response;
301
+
302
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
303
+ const traits = window.getCharacterTraits ? await window.getCharacterTraits(selectedCharacter) : await kimiDB.getAllPersonalityTraits(selectedCharacter);
304
+ const avg = window.getPersonalityAverage ? window.getPersonalityAverage(traits) : 50;
305
+ const affection = typeof traits.affection === "number" ? traits.affection : 55;
306
+ const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
307
+
308
+ // Only trigger listening videos for voice input, NOT for text chat
309
+ // Text chat should keep neutral videos until LLM response processing begins
310
+
311
+ if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
312
+ await window.updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
313
+ }
314
+
315
+ if (useAdvancedLLM && isSystemReady && kimiLLM) {
316
+ try {
317
+ const providerPref = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
318
+ const apiKey = kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref) : null;
319
+
320
+ if (apiKey && apiKey.trim() !== "") {
321
+ try {
322
+ if (window.dispatchEvent) {
323
+ window.emitAppEvent && window.emitAppEvent("chat:typing:start");
324
+ }
325
+ } catch (e) {}
326
+
327
+ // Use streaming if onStreamToken callback is provided
328
+ if (onStreamToken && typeof kimiLLM.chatStreaming === "function") {
329
+ response = await kimiLLM.chatStreaming(sanitizedText, onStreamToken);
330
+ } else {
331
+ response = await kimiLLM.chat(sanitizedText);
332
+ }
333
+
334
+ try {
335
+ if (window.dispatchEvent) {
336
+ window.emitAppEvent && window.emitAppEvent("chat:typing:stop");
337
+ }
338
+ } catch (e) {}
339
+
340
+ const updatedTraits = window.getCharacterTraits
341
+ ? await window.getCharacterTraits(selectedCharacter)
342
+ : await kimiDB.getAllPersonalityTraits(selectedCharacter);
343
+
344
+ // Use centralized video handling to avoid duplication
345
+ await handleVideoResponseWithFallback(sanitizedText, response, updatedTraits, selectedCharacter);
346
+
347
+ if (kimiLLM.updatePersonalityFromResponse) {
348
+ await kimiLLM.updatePersonalityFromResponse(sanitizedText, response);
349
+ const selectedCharacter2 = await kimiDB.getSelectedCharacter();
350
+ const traits2 = window.getCharacterTraits
351
+ ? await window.getCharacterTraits(selectedCharacter2)
352
+ : await kimiDB.getAllPersonalityTraits(selectedCharacter2);
353
+ if (kimiVideo && kimiVideo.setMoodByPersonality) {
354
+ kimiVideo.setMoodByPersonality(traits2);
355
+ }
356
+ }
357
+ } else {
358
+ // No API key configured - use centralized fallback
359
+ response = window.KimiFallbackManager
360
+ ? window.KimiFallbackManager.getFallbackMessage("api_missing")
361
+ : "To chat with me, add your API key in settings! 💕";
362
+ const updatedTraits = window.getCharacterTraits
363
+ ? await window.getCharacterTraits(selectedCharacter)
364
+ : await kimiDB.getAllPersonalityTraits(selectedCharacter);
365
+ kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection);
366
+ }
367
+ } catch (error) {
368
+ console.warn("LLM not available:", error.message);
369
+ try {
370
+ if (window.dispatchEvent) {
371
+ window.emitAppEvent && window.emitAppEvent("chat:typing:stop");
372
+ }
373
+ } catch (e) {}
374
+ // Still show API key message if no key is configured
375
+ const providerPref2 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
376
+ const apiKey = kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref2) : null;
377
+ if (!apiKey || apiKey.trim() === "") {
378
+ response = window.KimiFallbackManager
379
+ ? window.KimiFallbackManager.getFallbackMessage("api_missing")
380
+ : "To chat with me, add your API key in settings! 💕";
381
+ } else {
382
+ response = await getBasicResponse(reaction);
383
+ }
384
+ const updatedTraits = window.getCharacterTraits
385
+ ? await window.getCharacterTraits(selectedCharacter)
386
+ : await kimiDB.getAllPersonalityTraits(selectedCharacter);
387
+
388
+ // Use centralized video handling to avoid duplication
389
+ await handleVideoResponseWithFallback(sanitizedText, response, updatedTraits, selectedCharacter);
390
+ }
391
+ } else {
392
+ // System not ready - check if it's because of missing API key
393
+ const providerPref3 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
394
+ const apiKey = kimiDB && window.KimiProviderUtils ? await window.KimiProviderUtils.getApiKey(kimiDB, providerPref3) : null;
395
+ if (!apiKey || apiKey.trim() === "") {
396
+ response = window.KimiFallbackManager
397
+ ? window.KimiFallbackManager.getFallbackMessage("api_missing")
398
+ : "To chat with me, add your API key in settings! 💕";
399
+ } else {
400
+ response = await getBasicResponse(reaction);
401
+ }
402
+ const updatedTraits = window.getCharacterTraits
403
+ ? await window.getCharacterTraits(selectedCharacter)
404
+ : await kimiDB.getAllPersonalityTraits(selectedCharacter);
405
+
406
+ // Use centralized video handling to avoid duplication
407
+ await handleVideoResponseWithFallback(sanitizedText, null, updatedTraits, selectedCharacter);
408
+ }
409
+
410
+ // Use token usage collected by LLM manager if available
411
+ let tokenInfo = null;
412
+ if (window._lastKimiTokenUsage) {
413
+ tokenInfo = window._lastKimiTokenUsage;
414
+ window._lastKimiTokenUsage = null; // consume once
415
+ } else if (window.KimiTokenUtils) {
416
+ // Fallback approximate (no system prompt included)
417
+ try {
418
+ const est = window.KimiTokenUtils.estimate;
419
+ tokenInfo = { tokensIn: est(sanitizedText), tokensOut: est(response) };
420
+ } catch {}
421
+ }
422
+ await kimiMemory.saveConversation(sanitizedText, response, tokenInfo);
423
+ if (typeof updateStats === "function") {
424
+ updateStats();
425
+ }
426
+
427
+ // Extract memories automatically from conversation if system is enabled
428
+ if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
429
+ try {
430
+ const extractedMemories = await window.kimiMemorySystem.extractMemoryFromText(sanitizedText, response);
431
+ if (extractedMemories && extractedMemories.length > 0) {
432
+ // Update memory stats in UI
433
+ if (window.kimiMemoryUI && window.kimiMemoryUI.isInitialized) {
434
+ await window.kimiMemoryUI.updateMemoryStats();
435
+ // Show subtle notification for extracted memories
436
+ window.kimiMemoryUI.showFeedback(
437
+ `💭 ${extractedMemories.length} new ${extractedMemories.length === 1 ? "memory" : "memories"} learned`,
438
+ "info"
439
+ );
440
+ }
441
+ }
442
+ } catch (error) {
443
+ console.warn("Memory extraction error:", error);
444
+ }
445
+ }
446
+
447
+ return response;
448
+ } catch (error) {
449
+ console.error("Error in analyzeAndReact:", error);
450
+
451
+ // Use centralized fallback response
452
+ const fallbackResponse = window.KimiFallbackManager
453
+ ? window.KimiFallbackManager.getFallbackMessage("technical_error")
454
+ : "I'm sorry, I encountered an issue processing your message. Please try again.";
455
+
456
+ try {
457
+ // Attempt to save the error for debugging while still providing user feedback
458
+ if (kimiMemory && kimiMemory.saveConversation) {
459
+ await kimiMemory.saveConversation(text || "Error", fallbackResponse);
460
+ }
461
+ } catch (saveError) {
462
+ console.error("Failed to save error conversation:", saveError);
463
+ }
464
+
465
+ return fallbackResponse;
466
+ }
467
+ }
468
+
469
+ function addMessageToChat(sender, text, conversationId = null) {
470
+ const chatMessages = document.getElementById("chat-messages");
471
+ // Allow empty text for streaming (we'll update it progressively)
472
+ if (text === undefined || text === null) return;
473
+
474
+ const messageDiv = document.createElement("div");
475
+ messageDiv.className = `message ${sender}`;
476
+
477
+ const time = new Date().toLocaleTimeString("en-US", {
478
+ hour: "2-digit",
479
+ minute: "2-digit"
480
+ });
481
+
482
+ const messageTimeDiv = document.createElement("div");
483
+ messageTimeDiv.className = "message-time";
484
+ messageTimeDiv.style.display = "flex";
485
+ messageTimeDiv.style.justifyContent = "space-between";
486
+ messageTimeDiv.style.alignItems = "center";
487
+
488
+ const timeSpan = document.createElement("span");
489
+ timeSpan.textContent = time;
490
+ timeSpan.style.flex = "1";
491
+
492
+ const deleteBtn = document.createElement("button");
493
+ deleteBtn.className = "delete-message-btn";
494
+ const icon = document.createElement("i");
495
+ icon.className = "fas fa-trash";
496
+ deleteBtn.appendChild(icon);
497
+ deleteBtn.style.background = "none";
498
+ deleteBtn.style.border = "none";
499
+ deleteBtn.style.cursor = "pointer";
500
+ deleteBtn.style.color = "#aaa";
501
+ deleteBtn.style.fontSize = "1em";
502
+ deleteBtn.style.marginLeft = "8px";
503
+ deleteBtn.setAttribute("aria-label", "Delete message");
504
+ deleteBtn.addEventListener("click", async function (e) {
505
+ e.stopPropagation();
506
+ messageDiv.remove();
507
+ if (conversationId && window.kimiDB && window.kimiDB.deleteSingleMessage) {
508
+ await window.kimiDB.deleteSingleMessage(conversationId, sender);
509
+ }
510
+ });
511
+
512
+ messageTimeDiv.appendChild(timeSpan);
513
+ messageTimeDiv.appendChild(deleteBtn);
514
+
515
+ const textDiv = document.createElement("div");
516
+ // Use formatted text with HTML support (secure formatting)
517
+ if (text && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) {
518
+ textDiv.innerHTML = window.KimiValidationUtils.formatChatText(text);
519
+ } else {
520
+ textDiv.textContent = text || ""; // Fallback to plain text
521
+ }
522
+
523
+ messageDiv.appendChild(textDiv);
524
+ messageDiv.appendChild(messageTimeDiv);
525
+
526
+ chatMessages.appendChild(messageDiv);
527
+ chatMessages.scrollTop = chatMessages.scrollHeight;
528
+
529
+ // Return an object that allows updating the message content for streaming
530
+ return {
531
+ updateText: newText => {
532
+ // Use formatted text for streaming updates too
533
+ if (newText && window.KimiValidationUtils && window.KimiValidationUtils.formatChatText) {
534
+ textDiv.innerHTML = window.KimiValidationUtils.formatChatText(newText);
535
+ } else {
536
+ textDiv.textContent = newText;
537
+ }
538
+ // Throttle scrolling to prevent visual stuttering during streaming
539
+ if (!textDiv._scrollTimeout) {
540
+ textDiv._scrollTimeout = setTimeout(() => {
541
+ chatMessages.scrollTop = chatMessages.scrollHeight;
542
+ textDiv._scrollTimeout = null;
543
+ }, 50); // Throttle to 20 FPS max
544
+ }
545
+ },
546
+ element: messageDiv,
547
+ textElement: textDiv
548
+ };
549
+ }
550
+
551
+ async function loadChatHistory() {
552
+ const kimiDB = window.kimiDB;
553
+ const kimiMemory = window.kimiMemory;
554
+ const chatMessages = document.getElementById("chat-messages");
555
+
556
+ while (chatMessages.firstChild) {
557
+ chatMessages.removeChild(chatMessages.firstChild);
558
+ }
559
+
560
+ if (kimiDB) {
561
+ try {
562
+ const recent = await kimiDB.getRecentConversations(10);
563
+
564
+ if (recent.length === 0) {
565
+ const greeting = await kimiMemory.getGreeting();
566
+ addMessageToChat("kimi", greeting);
567
+ } else {
568
+ recent.forEach(conv => {
569
+ addMessageToChat("user", conv.user, conv.id);
570
+ addMessageToChat("kimi", conv.kimi, conv.id);
571
+ });
572
+ }
573
+ } catch (error) {
574
+ console.error("Error while loading history:", error);
575
+ const greeting = await kimiMemory.getGreeting();
576
+ addMessageToChat("kimi", greeting);
577
+ }
578
+ } else {
579
+ const greeting = await kimiMemory.getGreeting();
580
+ addMessageToChat("kimi", greeting);
581
+ }
582
+ }
583
+
584
+ async function loadSettingsData() {
585
+ const kimiDB = window.kimiDB;
586
+ const kimiLLM = window.kimiLLM;
587
+ if (!kimiDB) return;
588
+ try {
589
+ // Batch load preferences for better performance
590
+ const preferenceKeys = [
591
+ "voiceRate",
592
+ "voicePitch",
593
+ "voiceVolume",
594
+ "selectedLanguage",
595
+ "providerApiKey",
596
+ "llmProvider",
597
+ "llmModelId",
598
+ "selectedCharacter",
599
+ "llmTemperature",
600
+ "llmMaxTokens",
601
+ "llmTopP",
602
+ "llmFrequencyPenalty",
603
+ "llmPresencePenalty",
604
+ "enableStreaming"
605
+ ];
606
+ const preferences = await kimiDB.getPreferencesBatch(preferenceKeys);
607
+
608
+ // Set default values for missing preferences
609
+ const voiceRate = preferences.voiceRate !== undefined ? preferences.voiceRate : 1.1;
610
+ const voicePitch = preferences.voicePitch !== undefined ? preferences.voicePitch : 1.1;
611
+ const voiceVolume = preferences.voiceVolume !== undefined ? preferences.voiceVolume : 0.8;
612
+ const selectedLanguage = preferences.selectedLanguage || "en";
613
+ // Normalize legacy formats to primary subtag (e.g., 'en-US' -> 'en')
614
+ const normSelectedLanguage = (function (raw) {
615
+ if (!raw) return "en";
616
+ let r = String(raw).toLowerCase();
617
+ if (r.includes(":")) r = r.split(":").pop();
618
+ r = r.replace("_", "-");
619
+ return r.includes("-") ? r.split("-")[0] : r;
620
+ })(selectedLanguage);
621
+ const apiKey = preferences.providerApiKey || "";
622
+ const provider = preferences.llmProvider || "openrouter";
623
+ // Resolve baseUrl based on provider-specific stored preferences to avoid cross-provider leaks
624
+ const placeholders = window.KimiProviderPlaceholders || {};
625
+ let baseUrl;
626
+ if (provider === "openai-compatible" || provider === "ollama") {
627
+ const key = `llmBaseUrl_${provider}`;
628
+ try {
629
+ baseUrl = await kimiDB.getPreference(key, provider === "openai-compatible" ? "" : placeholders[provider]);
630
+ } catch (e) {
631
+ baseUrl = provider === "openai-compatible" ? "" : placeholders[provider];
632
+ }
633
+ } else {
634
+ baseUrl = placeholders[provider] || placeholders.openai;
635
+ }
636
+ const modelId = preferences.llmModelId || (window.kimiLLM ? window.kimiLLM.currentModel : "");
637
+ const selectedCharacter = preferences.selectedCharacter || "kimi";
638
+ const llmTemperature = preferences.llmTemperature !== undefined ? preferences.llmTemperature : 0.9;
639
+ const llmMaxTokens = preferences.llmMaxTokens !== undefined ? preferences.llmMaxTokens : 400;
640
+ const llmTopP = preferences.llmTopP !== undefined ? preferences.llmTopP : 0.9;
641
+ const llmFrequencyPenalty = preferences.llmFrequencyPenalty !== undefined ? preferences.llmFrequencyPenalty : 0.9;
642
+ const llmPresencePenalty = preferences.llmPresencePenalty !== undefined ? preferences.llmPresencePenalty : 0.8;
643
+ const enableStreaming = preferences.enableStreaming !== undefined ? preferences.enableStreaming : true;
644
+
645
+ // Update UI with voice settings
646
+ const languageSelect = document.getElementById("language-selection");
647
+ if (languageSelect) languageSelect.value = normSelectedLanguage;
648
+ updateSlider("voice-rate", voiceRate);
649
+ updateSlider("voice-pitch", voicePitch);
650
+ updateSlider("voice-volume", voiceVolume);
651
+
652
+ // Update LLM settings
653
+ updateSlider("llm-temperature", llmTemperature);
654
+ updateSlider("llm-max-tokens", llmMaxTokens);
655
+ updateSlider("llm-top-p", llmTopP);
656
+ updateSlider("llm-frequency-penalty", llmFrequencyPenalty);
657
+ updateSlider("llm-presence-penalty", llmPresencePenalty);
658
+
659
+ // Update streaming toggle
660
+ const streamingToggle = document.getElementById("enable-streaming");
661
+ if (streamingToggle) {
662
+ if (enableStreaming) {
663
+ streamingToggle.classList.add("active");
664
+ } else {
665
+ streamingToggle.classList.remove("active");
666
+ }
667
+ streamingToggle.setAttribute("aria-checked", String(enableStreaming));
668
+ }
669
+
670
+ // Batch load personality traits
671
+ const traitNames = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
672
+ const personality = await kimiDB.getPersonalityTraitsBatch(traitNames, selectedCharacter);
673
+ const defaults = [65, 55, 70, 75, 60, 50];
674
+
675
+ traitNames.forEach((trait, index) => {
676
+ const value = typeof personality[trait] === "number" ? personality[trait] : defaults[index];
677
+ updateSlider(`trait-${trait}`, value);
678
+
679
+ // Update memory cache for affection
680
+ if (trait === "affection" && window.kimiMemory) {
681
+ window.kimiMemory.affectionTrait = value;
682
+ }
683
+ });
684
+
685
+ // Sync personality traits to ensure consistency
686
+ await syncPersonalityTraits(selectedCharacter);
687
+
688
+ await updateStats();
689
+
690
+ // Update API key input
691
+ const apiKeyInput = document.getElementById("provider-api-key");
692
+ if (apiKeyInput) {
693
+ // Get the API key for the current provider
694
+ let providerKey = "";
695
+ if (window.KimiProviderUtils) {
696
+ providerKey = await window.KimiProviderUtils.getApiKey(kimiDB, provider);
697
+ } else {
698
+ providerKey = apiKey; // fallback to old method
699
+ }
700
+ apiKeyInput.value = providerKey || "";
701
+ }
702
+ const providerSelect = document.getElementById("llm-provider");
703
+ if (providerSelect) providerSelect.value = provider;
704
+ const baseUrlInput = document.getElementById("llm-base-url");
705
+ if (baseUrlInput) {
706
+ // Determine whether base URL should be editable for this provider
707
+ const isModifiable = provider === "openai-compatible" || provider === "ollama";
708
+ // Provider-specific defaults/placeholders
709
+ const placeholders = {
710
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
711
+ openai: "https://api.openai.com/v1/chat/completions",
712
+ groq: "https://api.groq.com/openai/v1/chat/completions",
713
+ together: "https://api.together.xyz/v1/chat/completions",
714
+ deepseek: "https://api.deepseek.com/chat/completions",
715
+ "openai-compatible": "",
716
+ ollama: "http://localhost:11434/api/chat"
717
+ };
718
+ const placeholder = placeholders[provider] || placeholders.openai;
719
+ baseUrlInput.placeholder = provider === "openai-compatible" ? "" : placeholder;
720
+
721
+ if (isModifiable) {
722
+ // Show stored baseUrl for modifiable providers (could be empty)
723
+ baseUrlInput.value = baseUrl || "";
724
+ baseUrlInput.disabled = false;
725
+ baseUrlInput.style.opacity = "1";
726
+ } else {
727
+ // For fixed providers show the provider URL as value and make input readonly
728
+ baseUrlInput.value = placeholder;
729
+ baseUrlInput.disabled = true;
730
+ baseUrlInput.style.opacity = "0.6";
731
+ }
732
+ }
733
+ const modelIdInput = document.getElementById("llm-model-id");
734
+ if (modelIdInput) {
735
+ if (provider === "openrouter") {
736
+ modelIdInput.value = modelId;
737
+ } else {
738
+ modelIdInput.value = ""; // only placeholder for non-OpenRouter providers
739
+ }
740
+ }
741
+ // For non-OpenRouter providers we keep placeholder per provider; the value is already set above.
742
+ const apiKeyLabel = document.getElementById("api-key-label");
743
+ if (apiKeyLabel) {
744
+ apiKeyLabel.textContent = window.KimiProviderUtils ? window.KimiProviderUtils.getLabelForProvider(provider) : "API Key";
745
+ }
746
+
747
+ loadAvailableModels();
748
+ } catch (error) {
749
+ console.error("Error while loading settings:", error);
750
+ }
751
+ }
752
+
753
+ function updateSlider(id, value) {
754
+ const slider = document.getElementById(id);
755
+ const valueSpan = document.getElementById(`${id}-value`);
756
+ if (slider && valueSpan) {
757
+ slider.value = value;
758
+ valueSpan.textContent = value;
759
+ }
760
+ }
761
+
762
+ async function updatePersonalitySliders(characterKey) {
763
+ const kimiDB = window.kimiDB;
764
+ if (!kimiDB) return;
765
+
766
+ try {
767
+ // Get current traits from database for this character
768
+ const savedTraits = window.getCharacterTraits ? await window.getCharacterTraits(characterKey) : await kimiDB.getAllPersonalityTraits(characterKey);
769
+
770
+ // Get default traits from KIMI_CHARACTERS constants
771
+ const characterDefaults = window.KIMI_CHARACTERS[characterKey]?.traits || {};
772
+
773
+ // Get unified defaults
774
+ const unifiedDefaults = window.kimiEmotionSystem?.TRAIT_DEFAULTS || {
775
+ affection: 55,
776
+ playfulness: 55,
777
+ intelligence: 70,
778
+ empathy: 75,
779
+ humor: 60,
780
+ romance: 50
781
+ };
782
+
783
+ // Use saved traits if they exist, otherwise fall back to character defaults, then unified defaults
784
+ const traits = {
785
+ affection: savedTraits.affection ?? characterDefaults.affection ?? unifiedDefaults.affection,
786
+ playfulness: savedTraits.playfulness ?? characterDefaults.playfulness ?? unifiedDefaults.playfulness,
787
+ intelligence: savedTraits.intelligence ?? characterDefaults.intelligence ?? unifiedDefaults.intelligence,
788
+ empathy: savedTraits.empathy ?? characterDefaults.empathy ?? unifiedDefaults.empathy,
789
+ humor: savedTraits.humor ?? characterDefaults.humor ?? unifiedDefaults.humor,
790
+ romance: savedTraits.romance ?? characterDefaults.romance ?? unifiedDefaults.romance
791
+ };
792
+
793
+ // Check if sliders exist before updating them
794
+ const sliderUpdates = [
795
+ { id: "trait-affection", value: traits.affection },
796
+ { id: "trait-playfulness", value: traits.playfulness },
797
+ { id: "trait-intelligence", value: traits.intelligence },
798
+ { id: "trait-empathy", value: traits.empathy },
799
+ { id: "trait-humor", value: traits.humor },
800
+ { id: "trait-romance", value: traits.romance }
801
+ ];
802
+
803
+ for (const update of sliderUpdates) {
804
+ const slider = document.getElementById(update.id);
805
+ if (slider) {
806
+ updateSlider(update.id, update.value);
807
+ }
808
+ }
809
+ } catch (error) {
810
+ console.error("Error updating personality sliders:", error);
811
+ }
812
+ }
813
+
814
+ async function updateStats() {
815
+ const kimiDB = window.kimiDB;
816
+ if (!kimiDB) return;
817
+ const character = await kimiDB.getSelectedCharacter();
818
+ // Retrieve token usage (fallback to 0)
819
+ const tokensIn = await kimiDB.getPreference(`totalTokensIn_${character}`, 0);
820
+ const tokensOut = await kimiDB.getPreference(`totalTokensOut_${character}`, 0);
821
+ const charDefAff = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]?.traits?.affection) || null;
822
+ const genericAff = (window.getTraitDefaults && window.getTraitDefaults().affection) || 55;
823
+ const defaultAff = typeof charDefAff === "number" ? charDefAff : genericAff;
824
+ const affectionTrait = await kimiDB.getPersonalityTrait("affection", defaultAff, character);
825
+ const conversations = await kimiDB.getAllConversations(character);
826
+ let firstInteraction = await kimiDB.getPreference(`firstInteraction_${character}`);
827
+ if (!firstInteraction && conversations.length > 0) {
828
+ firstInteraction = conversations[0].timestamp;
829
+ await kimiDB.setPreference(`firstInteraction_${character}`, firstInteraction);
830
+ }
831
+ const tokensEl = document.getElementById("tokens-usage");
832
+ const favorabilityEl = document.getElementById("current-favorability");
833
+ const conversationsEl = document.getElementById("conversations-count");
834
+ const daysEl = document.getElementById("days-together");
835
+ if (tokensEl) tokensEl.textContent = `${tokensIn} / ${tokensOut}`;
836
+ if (favorabilityEl) {
837
+ const v = Number(affectionTrait) || 0;
838
+ favorabilityEl.textContent = `${Math.max(0, Math.min(100, v)).toFixed(2)}%`;
839
+ }
840
+ if (conversationsEl) conversationsEl.textContent = conversations.length;
841
+ if (firstInteraction && daysEl) {
842
+ const days = Math.floor((new Date() - new Date(firstInteraction)) / (1000 * 60 * 60 * 24));
843
+ daysEl.textContent = days;
844
+ }
845
+ }
846
+
847
+ function initializeAllSliders() {
848
+ const sliders = [
849
+ "voice-rate",
850
+ "voice-pitch",
851
+ "voice-volume",
852
+ "trait-affection",
853
+ "trait-playfulness",
854
+ "trait-intelligence",
855
+ "trait-empathy",
856
+ "trait-humor",
857
+ "trait-romance",
858
+ "llm-temperature",
859
+ "llm-max-tokens",
860
+ "llm-top-p",
861
+ "llm-frequency-penalty",
862
+ "llm-presence-penalty",
863
+ "interface-opacity"
864
+ ];
865
+
866
+ sliders.forEach(sliderId => {
867
+ const slider = document.getElementById(sliderId);
868
+ const valueSpan = document.getElementById(`${sliderId}-value`);
869
+ if (slider && valueSpan) {
870
+ valueSpan.textContent = slider.value;
871
+ }
872
+ });
873
+ }
874
+
875
+ async function syncLLMMaxTokensSlider() {
876
+ const kimiDB = window.kimiDB;
877
+ const llmMaxTokensSlider = document.getElementById("llm-max-tokens");
878
+ const llmMaxTokensValue = document.getElementById("llm-max-tokens-value");
879
+ if (llmMaxTokensSlider && llmMaxTokensValue && kimiDB) {
880
+ const saved = await kimiDB.getPreference("llmMaxTokens", 400);
881
+ llmMaxTokensSlider.value = saved;
882
+ llmMaxTokensValue.textContent = saved;
883
+ }
884
+ }
885
+
886
+ async function syncLLMTemperatureSlider() {
887
+ const kimiDB = window.kimiDB;
888
+ const llmTemperatureSlider = document.getElementById("llm-temperature");
889
+ const llmTemperatureValue = document.getElementById("llm-temperature-value");
890
+ if (llmTemperatureSlider && llmTemperatureValue && kimiDB) {
891
+ const saved = await kimiDB.getPreference("llmTemperature", 0.8);
892
+ llmTemperatureSlider.value = saved;
893
+ llmTemperatureValue.textContent = saved;
894
+ }
895
+ }
896
+
897
+ function updateTabsScrollIndicator() {
898
+ const tabsContainer = document.querySelector(".settings-tabs");
899
+ if (!tabsContainer) return;
900
+
901
+ const isOverflowing = tabsContainer.scrollWidth > tabsContainer.clientWidth;
902
+
903
+ if (isOverflowing) {
904
+ tabsContainer.classList.remove("no-overflow");
905
+ } else {
906
+ tabsContainer.classList.add("no-overflow");
907
+ }
908
+ }
909
+
910
+ async function loadAvailableModels() {
911
+ // Prevent multiple simultaneous calls
912
+ if (loadAvailableModels._loading) {
913
+ return;
914
+ }
915
+ loadAvailableModels._loading = true;
916
+
917
+ const kimiLLM = window.kimiLLM;
918
+ if (!kimiLLM) {
919
+ console.warn("❌ KimiLLM not yet initialized for loadAvailableModels");
920
+ loadAvailableModels._loading = false;
921
+ return;
922
+ }
923
+
924
+ const modelsContainer = document.getElementById("models-container");
925
+ if (!modelsContainer) {
926
+ console.warn("❌ Models container not found");
927
+ loadAvailableModels._loading = false;
928
+ return;
929
+ }
930
+
931
+ try {
932
+ const stats = await kimiLLM.getModelStats();
933
+
934
+ const signature = JSON.stringify(Object.keys(stats.available || {}).sort());
935
+
936
+ if (loadAvailableModels._rendered && loadAvailableModels._signature === signature) {
937
+ const currentId = stats.current && stats.current.id;
938
+ const cards = modelsContainer.querySelectorAll(".model-card");
939
+ cards.forEach(card => {
940
+ if (card.dataset.modelId === currentId) {
941
+ card.classList.add("selected");
942
+ } else {
943
+ card.classList.remove("selected");
944
+ }
945
+ });
946
+ loadAvailableModels._loading = false;
947
+ return;
948
+ }
949
+
950
+ while (modelsContainer.firstChild) {
951
+ modelsContainer.removeChild(modelsContainer.firstChild);
952
+ }
953
+
954
+ // Check if we have available models
955
+ if (!stats.available || Object.keys(stats.available).length === 0) {
956
+ console.warn("⚠️ No models available in stats");
957
+ const noModelsDiv = document.createElement("div");
958
+ noModelsDiv.className = "no-models-message";
959
+ noModelsDiv.innerHTML = `
960
+ <p>⚠️ No models available. Please check your API key.</p>
961
+ `;
962
+ modelsContainer.appendChild(noModelsDiv);
963
+ loadAvailableModels._loading = false;
964
+ return;
965
+ }
966
+
967
+ // Only log once when models are loaded, not repeated calls
968
+ if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) {
969
+ if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
970
+ console.log(`✅ Loaded ${Object.keys(stats.available).length} LLM models`);
971
+ }
972
+ loadAvailableModels._lastLoadTime = Date.now();
973
+ }
974
+ const createCard = (id, model) => {
975
+ const modelDiv = document.createElement("div");
976
+ modelDiv.className = `model-card ${id === stats.current.id ? "selected" : ""}`;
977
+ modelDiv.dataset.modelId = id;
978
+ const searchable = [model.name || "", model.provider || "", id, (model.strengths || []).join(" ")].join(" ").toLowerCase();
979
+ modelDiv.dataset.search = searchable;
980
+
981
+ // Create model card elements safely
982
+ const modelHeader = document.createElement("div");
983
+ modelHeader.className = "model-header";
984
+
985
+ const modelName = document.createElement("div");
986
+ modelName.className = "model-name";
987
+ modelName.textContent = model.name;
988
+
989
+ const modelProvider = document.createElement("div");
990
+ modelProvider.className = "model-provider";
991
+ modelProvider.textContent = model.provider;
992
+
993
+ modelHeader.appendChild(modelName);
994
+ modelHeader.appendChild(modelProvider);
995
+
996
+ const modelDescription = document.createElement("div");
997
+ modelDescription.className = "model-description";
998
+ const rawIn = model.pricing && typeof model.pricing.input !== "undefined" ? model.pricing.input : "N/A";
999
+ const rawOut = model.pricing && typeof model.pricing.output !== "undefined" ? model.pricing.output : "N/A";
1000
+ const inNum = typeof rawIn === "number" ? rawIn : typeof rawIn === "string" ? Number(rawIn) : NaN;
1001
+ const outNum = typeof rawOut === "number" ? rawOut : typeof rawOut === "string" ? Number(rawOut) : NaN;
1002
+ const inIsNum = Number.isFinite(inNum);
1003
+ const outIsNum = Number.isFinite(outNum);
1004
+ const bothNA = !inIsNum && !outIsNum;
1005
+ const bothZero = inIsNum && outIsNum && inNum === 0 && outNum === 0;
1006
+ const isFreeName =
1007
+ /free/i.test(model.name || "") || /free/i.test(id || "") || (Array.isArray(model.strengths) && model.strengths.some(s => /free/i.test(s)));
1008
+ const fmt = n => {
1009
+ if (!Number.isFinite(n)) return "N/A";
1010
+ const roundedInt = Math.round(n);
1011
+ if (Math.abs(n - roundedInt) < 1e-6) return `${roundedInt}$`;
1012
+ return `${n.toFixed(2)}$`;
1013
+ };
1014
+ let inStr = inIsNum ? (inNum === 0 ? "Free" : fmt(inNum)) : "N/A";
1015
+ let outStr = outIsNum ? (outNum === 0 ? "Free" : fmt(outNum)) : "N/A";
1016
+ let priceText;
1017
+ if (bothZero || isFreeName) {
1018
+ priceText = "Price: Free";
1019
+ } else if (bothNA) {
1020
+ priceText = "Price: N/A";
1021
+ } else {
1022
+ priceText = `Price: ${inStr} per 1M input tokens, ${outStr} per 1M output tokens`;
1023
+ }
1024
+ modelDescription.textContent = `Context: ${model.contextWindow.toLocaleString()} tokens | ${priceText}`;
1025
+
1026
+ const modelStrengths = document.createElement("div");
1027
+ modelStrengths.className = "model-strengths";
1028
+ if (priceText === "Price: Free") {
1029
+ const badge = document.createElement("span");
1030
+ badge.className = "strength-tag";
1031
+ badge.textContent = "Free";
1032
+ modelStrengths.appendChild(badge);
1033
+ }
1034
+ model.strengths.forEach(strength => {
1035
+ const strengthTag = document.createElement("span");
1036
+ strengthTag.className = "strength-tag";
1037
+ strengthTag.textContent = strength;
1038
+ modelStrengths.appendChild(strengthTag);
1039
+ });
1040
+
1041
+ modelDiv.appendChild(modelHeader);
1042
+ modelDiv.appendChild(modelDescription);
1043
+ modelDiv.appendChild(modelStrengths);
1044
+
1045
+ modelDiv.addEventListener("click", async () => {
1046
+ try {
1047
+ await kimiLLM.setCurrentModel(id);
1048
+ document.querySelectorAll(".model-card").forEach(card => card.classList.remove("selected"));
1049
+ modelDiv.classList.add("selected");
1050
+ console.log(`🤖 Model switched to: ${model.name}`);
1051
+
1052
+ // Show brief feedback to user
1053
+ const feedback = document.createElement("div");
1054
+ feedback.textContent = `Model changed to ${model.name}`;
1055
+ feedback.style.cssText = `
1056
+ position: fixed; top: 20px; right: 20px; z-index: 10000;
1057
+ background: #27ae60; color: white; padding: 12px 20px;
1058
+ border-radius: 6px; font-size: 14px; font-weight: 500;
1059
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1060
+ `;
1061
+ document.body.appendChild(feedback);
1062
+ setTimeout(() => feedback.remove(), 3000);
1063
+ } catch (error) {
1064
+ console.error("Error while changing model:", error);
1065
+ // Show error feedback
1066
+ const errorFeedback = document.createElement("div");
1067
+ errorFeedback.textContent = `Error changing model: ${error.message}`;
1068
+ errorFeedback.style.cssText = `
1069
+ position: fixed; top: 20px; right: 20px; z-index: 10000;
1070
+ background: #e74c3c; color: white; padding: 12px 20px;
1071
+ border-radius: 6px; font-size: 14px; font-weight: 500;
1072
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1073
+ `;
1074
+ document.body.appendChild(errorFeedback);
1075
+ setTimeout(() => errorFeedback.remove(), 5000);
1076
+ }
1077
+ });
1078
+
1079
+ return modelDiv;
1080
+ };
1081
+
1082
+ const recommendedIds = window.kimiLLM && Array.isArray(window.kimiLLM.recommendedModelIds) ? window.kimiLLM.recommendedModelIds : [];
1083
+
1084
+ const recommendedEntries = recommendedIds.map(id => [id, stats.available[id]]).filter(([, model]) => !!model);
1085
+
1086
+ const otherEntries = Object.entries(stats.available)
1087
+ .filter(([id]) => !recommendedIds.includes(id))
1088
+ .sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0]));
1089
+
1090
+ const searchWrap = document.createElement("div");
1091
+ searchWrap.className = "models-search-container";
1092
+ const searchInput = document.createElement("input");
1093
+ searchInput.type = "text";
1094
+ searchInput.className = "kimi-input";
1095
+ searchInput.id = "models-search";
1096
+ // use i18n placeholder key
1097
+ if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") {
1098
+ searchInput.setAttribute("data-i18n-placeholder", "models_search_placeholder");
1099
+ } else {
1100
+ searchInput.placeholder = "Filter models...";
1101
+ }
1102
+ searchWrap.appendChild(searchInput);
1103
+ modelsContainer.appendChild(searchWrap);
1104
+ if (typeof loadAvailableModels._searchValue === "string") {
1105
+ searchInput.value = loadAvailableModels._searchValue;
1106
+ }
1107
+
1108
+ if (recommendedEntries.length > 0) {
1109
+ const recSection = document.createElement("div");
1110
+ recSection.className = "models-section recommended-models";
1111
+ const title = document.createElement("div");
1112
+ title.className = "models-section-title";
1113
+ // i18n aware title
1114
+ if (window.setI18n) window.setI18n(title, "models_recommended_title");
1115
+ recSection.appendChild(title);
1116
+ const list = document.createElement("div");
1117
+ list.className = "models-list";
1118
+ recommendedEntries.forEach(([id, model]) => {
1119
+ list.appendChild(createCard(id, model));
1120
+ });
1121
+ recSection.appendChild(list);
1122
+ modelsContainer.appendChild(recSection);
1123
+ }
1124
+
1125
+ if (otherEntries.length > 0) {
1126
+ const allSection = document.createElement("div");
1127
+ allSection.className = "models-section all-models";
1128
+ const header = document.createElement("div");
1129
+ header.className = "models-section-title";
1130
+ const toggleBtn = document.createElement("button");
1131
+ toggleBtn.type = "button";
1132
+ toggleBtn.className = "kimi-button";
1133
+ toggleBtn.style.marginLeft = "8px";
1134
+ // toggle show/hide label via i18n when available
1135
+ if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") {
1136
+ const currentKey = loadAvailableModels._allCollapsed === false ? "button_hide" : "button_show";
1137
+ if (window.setI18n) window.setI18n(toggleBtn, currentKey);
1138
+ toggleBtn.textContent = window.kimiI18nManager.t(currentKey);
1139
+ } else {
1140
+ toggleBtn.textContent = loadAvailableModels._allCollapsed === false ? "Hide" : "Show";
1141
+ }
1142
+ const label = document.createElement("span");
1143
+ if (window.setI18n) window.setI18n(label, "models_all_title");
1144
+ header.appendChild(label);
1145
+ header.appendChild(toggleBtn);
1146
+ const refreshBtn = document.createElement("button");
1147
+ refreshBtn.type = "button";
1148
+ refreshBtn.className = "kimi-button";
1149
+ refreshBtn.style.marginLeft = "8px";
1150
+ if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") {
1151
+ if (window.setI18n) window.setI18n(refreshBtn, "button_refresh");
1152
+ } else {
1153
+ refreshBtn.textContent = "Refresh";
1154
+ }
1155
+ refreshBtn.addEventListener("click", async () => {
1156
+ try {
1157
+ refreshBtn.disabled = true;
1158
+ const oldText = refreshBtn.textContent;
1159
+ refreshBtn.textContent = "Refreshing...";
1160
+ if (window.kimiLLM && window.kimiLLM.refreshRemoteModels) {
1161
+ await window.kimiLLM.refreshRemoteModels();
1162
+ }
1163
+ loadAvailableModels._signature = null;
1164
+ loadAvailableModels._rendered = false;
1165
+ const savedSearch = searchInput.value;
1166
+ loadAvailableModels._searchValue = savedSearch;
1167
+ await loadAvailableModels();
1168
+ } catch (e) {
1169
+ console.error("Error refreshing models:", e);
1170
+ } finally {
1171
+ refreshBtn.disabled = false;
1172
+ refreshBtn.textContent = "Refresh";
1173
+ }
1174
+ });
1175
+ header.appendChild(refreshBtn);
1176
+ const list = document.createElement("div");
1177
+ list.className = "models-list";
1178
+ otherEntries.forEach(([id, model]) => {
1179
+ list.appendChild(createCard(id, model));
1180
+ });
1181
+ const collapsed = loadAvailableModels._allCollapsed !== false;
1182
+ list.style.display = collapsed ? "none" : "block";
1183
+ toggleBtn.addEventListener("click", () => {
1184
+ const nowCollapsed = list.style.display !== "none";
1185
+ list.style.display = nowCollapsed ? "none" : "block";
1186
+ loadAvailableModels._allCollapsed = nowCollapsed;
1187
+ if (window.kimiI18nManager && typeof window.kimiI18nManager.t === "function") {
1188
+ const key = nowCollapsed ? "button_show" : "button_hide";
1189
+ if (window.setI18n) window.setI18n(toggleBtn, key);
1190
+ toggleBtn.textContent = window.kimiI18nManager.t(key);
1191
+ } else {
1192
+ toggleBtn.textContent = nowCollapsed ? "Show" : "Hide";
1193
+ }
1194
+ });
1195
+ allSection.appendChild(header);
1196
+ allSection.appendChild(list);
1197
+ modelsContainer.appendChild(allSection);
1198
+ }
1199
+
1200
+ const applyFilter = term => {
1201
+ const q = (term || "").toLowerCase().trim();
1202
+ const cards = modelsContainer.querySelectorAll(".model-card");
1203
+ cards.forEach(card => {
1204
+ const hay = card.dataset.search || "";
1205
+ card.style.display = q && !hay.includes(q) ? "none" : "";
1206
+ });
1207
+ };
1208
+ searchInput.addEventListener("input", e => {
1209
+ loadAvailableModels._searchValue = e.target.value;
1210
+ applyFilter(e.target.value);
1211
+ });
1212
+ if (searchInput.value) {
1213
+ applyFilter(searchInput.value);
1214
+ }
1215
+
1216
+ loadAvailableModels._rendered = true;
1217
+ loadAvailableModels._signature = signature;
1218
+ } catch (error) {
1219
+ console.error("Error loading available models:", error);
1220
+ const errorDiv = document.createElement("div");
1221
+ errorDiv.className = "models-error-message";
1222
+ // Escape any content from error.message to prevent XSS when inserted into innerHTML
1223
+ const safeMsg =
1224
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
1225
+ ? window.KimiValidationUtils.escapeHtml(error.message || String(error))
1226
+ : String(error.message || error);
1227
+ errorDiv.innerHTML = `
1228
+ <p>❌ Error loading models: ${safeMsg}</p>
1229
+ `;
1230
+ modelsContainer.appendChild(errorDiv);
1231
+ } finally {
1232
+ loadAvailableModels._loading = false;
1233
+ }
1234
+ }
1235
+
1236
+ // Debug utilities removed for production optimization
1237
+
1238
+ async function sendMessage() {
1239
+ const chatInput = document.getElementById("chat-input");
1240
+ const waitingIndicator = document.getElementById("waiting-indicator");
1241
+ let message = chatInput.value;
1242
+
1243
+ // Enhanced input validation using our new validation utils
1244
+ const validation = window.KimiValidationUtils?.validateMessage(message);
1245
+ if (!validation || !validation.valid) {
1246
+ // Show error to user
1247
+ if (validation?.error) {
1248
+ addMessageToChat("system", `❌ ${validation.error}`);
1249
+ }
1250
+ // Use sanitized version if available
1251
+ if (validation?.sanitized) {
1252
+ chatInput.value = validation.sanitized;
1253
+ }
1254
+ return;
1255
+ }
1256
+
1257
+ message = validation.sanitized || message.trim();
1258
+ if (!message) return;
1259
+
1260
+ addMessageToChat("user", message);
1261
+ chatInput.value = "";
1262
+ if (waitingIndicator) waitingIndicator.style.display = "inline-block";
1263
+
1264
+ try {
1265
+ // Check if streaming is enabled (you can add a preference for this)
1266
+ const streamingEnabled = await window.kimiDB?.getPreference("enableStreaming", window.KIMI_CONFIG?.DEFAULTS?.ENABLE_STREAMING ?? true);
1267
+
1268
+ if (streamingEnabled && window.kimiLLM && typeof window.kimiLLM.chatStreaming === "function") {
1269
+ // Use streaming through analyzeAndReact
1270
+ let streamingResponse = "";
1271
+ const messageObj = addMessageToChat("kimi", ""); // Start with empty message
1272
+
1273
+ // Safety check: ensure messageObj is valid
1274
+ if (!messageObj || typeof messageObj.updateText !== "function") {
1275
+ console.error("Failed to create streaming message object, falling back to non-streaming");
1276
+ const response = await analyzeAndReact(message);
1277
+ let finalResponse = response;
1278
+ if (!finalResponse || typeof finalResponse !== "string" || finalResponse.trim().length < 2) {
1279
+ finalResponse = window.getLocalizedEmotionalResponse ? window.getLocalizedEmotionalResponse("neutral") : "I'm here for you!";
1280
+ }
1281
+ addMessageToChat("kimi", finalResponse);
1282
+ if (window.voiceManager && !message.startsWith("Vous:")) {
1283
+ window.voiceManager.speak(finalResponse);
1284
+ }
1285
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1286
+ return;
1287
+ }
1288
+
1289
+ try {
1290
+ // Start streaming response processing
1291
+ if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
1292
+ console.log("🔄 Starting streaming response...");
1293
+ }
1294
+ let emotionDetected = false;
1295
+
1296
+ const response = await analyzeAndReact(message, true, token => {
1297
+ streamingResponse += token;
1298
+ if (messageObj && messageObj.updateText) {
1299
+ messageObj.updateText(streamingResponse);
1300
+ }
1301
+ // Progressive analysis disabled to prevent UI flickering during streaming
1302
+ // All analysis will be done after streaming completes
1303
+ });
1304
+ // Streaming completed
1305
+ if (window.KIMI_CONFIG?.DEBUG?.ENABLED) {
1306
+ console.log("✅ Streaming completed, final response length:", streamingResponse.length);
1307
+ }
1308
+
1309
+ // Final processing after streaming completes
1310
+ let finalResponse = streamingResponse || response;
1311
+ if (!finalResponse || finalResponse.trim().length < 2) {
1312
+ finalResponse = window.getLocalizedEmotionalResponse ? window.getLocalizedEmotionalResponse("neutral") : "I'm here for you!";
1313
+ if (messageObj && messageObj.updateText) {
1314
+ messageObj.updateText(finalResponse);
1315
+ }
1316
+ } else {
1317
+ if (messageObj && messageObj.updateText) {
1318
+ messageObj.updateText(finalResponse);
1319
+ }
1320
+ }
1321
+
1322
+ // Voice synthesis after streaming completes (if not started during streaming)
1323
+ if (window.voiceManager && !message.startsWith("Vous:") && finalResponse.length > 20) {
1324
+ // Check if voice synthesis should happen
1325
+ const shouldSpeak = await window.kimiDB?.getPreference("voiceEnabled", window.KIMI_CONFIG?.DEFAULTS?.VOICE_ENABLED ?? true);
1326
+ if (shouldSpeak) {
1327
+ window.voiceManager.speak(finalResponse);
1328
+ }
1329
+ }
1330
+
1331
+ // Final comprehensive system updates
1332
+ try {
1333
+ // Final emotion analysis if not done during streaming
1334
+ if (!emotionDetected && window.kimiAnalyzeEmotion) {
1335
+ const finalEmotion = window.kimiAnalyzeEmotion(finalResponse);
1336
+ if (finalEmotion && finalEmotion !== "neutral") {
1337
+ emotionDetected = true;
1338
+ }
1339
+ }
1340
+
1341
+ // Final personality update
1342
+ if (window.updatePersonalityTraitsFromEmotion && finalResponse.length > 50) {
1343
+ const finalEmotion = window.kimiAnalyzeEmotion ? window.kimiAnalyzeEmotion(finalResponse) : "neutral";
1344
+ await window.updatePersonalityTraitsFromEmotion(finalEmotion, finalResponse);
1345
+ }
1346
+
1347
+ // Final memory extraction
1348
+ if (window.kimiMemory && typeof window.kimiMemory.extractMemoriesFromConversation === "function") {
1349
+ await window.kimiMemory.extractMemoriesFromConversation(message, finalResponse);
1350
+ }
1351
+
1352
+ // Final video state adjustment
1353
+ if (window.kimiVideo && window.kimiDB) {
1354
+ const selectedCharacter = await window.kimiDB.getSelectedCharacter();
1355
+ const traits = window.getCharacterTraits
1356
+ ? await window.getCharacterTraits(selectedCharacter)
1357
+ : await window.kimiDB.getAllPersonalityTraits(selectedCharacter);
1358
+ if (traits && emotionDetected) {
1359
+ window.kimiVideo.setMoodByPersonality(traits);
1360
+ }
1361
+ }
1362
+ } catch (finalError) {
1363
+ console.warn("Final system updates failed:", finalError);
1364
+ }
1365
+
1366
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1367
+ } catch (streamingError) {
1368
+ console.warn("Streaming failed, falling back to non-streaming:", streamingError);
1369
+ // Fallback to non-streaming
1370
+ const response = await analyzeAndReact(message);
1371
+ let finalResponse = response;
1372
+ if (!finalResponse || typeof finalResponse !== "string" || finalResponse.trim().length < 2) {
1373
+ finalResponse = window.getLocalizedEmotionalResponse ? window.getLocalizedEmotionalResponse("neutral") : "I'm here for you!";
1374
+ }
1375
+ if (messageObj && messageObj.updateText) {
1376
+ messageObj.updateText(finalResponse);
1377
+ }
1378
+
1379
+ if (window.voiceManager && !message.startsWith("Vous:")) {
1380
+ window.voiceManager.speak(finalResponse);
1381
+ }
1382
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1383
+ }
1384
+ } else {
1385
+ // Use non-streaming (original behavior)
1386
+ const response = await analyzeAndReact(message);
1387
+ let finalResponse = response;
1388
+ // If the LLM's response is empty, null, or too short, use the emotional fallback.
1389
+ if (!finalResponse || typeof finalResponse !== "string" || finalResponse.trim().length < 2) {
1390
+ finalResponse = window.getLocalizedEmotionalResponse ? window.getLocalizedEmotionalResponse("neutral") : "I'm here for you!";
1391
+ }
1392
+ setTimeout(() => {
1393
+ addMessageToChat("kimi", finalResponse);
1394
+ if (window.voiceManager && !message.startsWith("Vous:")) {
1395
+ window.voiceManager.speak(finalResponse);
1396
+ }
1397
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1398
+ }, 1000);
1399
+ }
1400
+ } catch (error) {
1401
+ console.error("Error while generating response:", error);
1402
+ const i18n = window.kimiI18nManager;
1403
+ const fallbackResponse = i18n ? i18n.t("fallback_general_error") : "Sorry my love, I am having a little technical issue! 💕";
1404
+ addMessageToChat("kimi", fallbackResponse);
1405
+ if (window.voiceManager) {
1406
+ window.voiceManager.speak(fallbackResponse);
1407
+ }
1408
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1409
+ }
1410
+ }
1411
+
1412
+ function setupSettingsListeners(kimiDB, kimiMemory) {
1413
+ const voiceRateSlider = document.getElementById("voice-rate");
1414
+ const voicePitchSlider = document.getElementById("voice-pitch");
1415
+ const voiceVolumeSlider = document.getElementById("voice-volume");
1416
+ const languageSelect = document.getElementById("language-selection");
1417
+ const voiceSelect = document.getElementById("voice-selection");
1418
+ // Affection restored as editable trait.
1419
+ const traitSliders = ["trait-affection", "trait-playfulness", "trait-intelligence", "trait-empathy", "trait-humor", "trait-romance"];
1420
+ const llmTemperatureSlider = document.getElementById("llm-temperature");
1421
+ const llmMaxTokensSlider = document.getElementById("llm-max-tokens");
1422
+ const llmTopPSlider = document.getElementById("llm-top-p");
1423
+ const llmFrequencyPenaltySlider = document.getElementById("llm-frequency-penalty");
1424
+ const llmPresencePenaltySlider = document.getElementById("llm-presence-penalty");
1425
+ const enableStreamingToggle = document.getElementById("enable-streaming");
1426
+ const colorThemeSelect = document.getElementById("color-theme");
1427
+ const interfaceOpacitySlider = document.getElementById("interface-opacity");
1428
+
1429
+ // SIMPLE FIX: Initialize _kimiListenerCleanup to prevent undefined error
1430
+ if (!window._kimiListenerCleanup) {
1431
+ window._kimiListenerCleanup = [];
1432
+ }
1433
+
1434
+ // Create debounced functions for better performance
1435
+ const debouncedVoiceRateUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1436
+ if (kimiDB) await kimiDB.setPreference("voiceRate", parseFloat(value));
1437
+ if (kimiMemory && kimiMemory.preferences) {
1438
+ kimiMemory.preferences.voiceRate = parseFloat(value);
1439
+ }
1440
+ }, 300);
1441
+
1442
+ const debouncedVoicePitchUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1443
+ if (kimiDB) await kimiDB.setPreference("voicePitch", parseFloat(value));
1444
+ if (kimiMemory && kimiMemory.preferences) {
1445
+ kimiMemory.preferences.voicePitch = parseFloat(value);
1446
+ }
1447
+ }, 300);
1448
+
1449
+ const debouncedVoiceVolumeUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1450
+ if (kimiDB) await kimiDB.setPreference("voiceVolume", parseFloat(value));
1451
+ if (kimiMemory && kimiMemory.preferences) {
1452
+ kimiMemory.preferences.voiceVolume = parseFloat(value);
1453
+ }
1454
+ }, 300);
1455
+
1456
+ const debouncedLLMTempUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1457
+ if (kimiDB) await kimiDB.setPreference("llmTemperature", parseFloat(value));
1458
+ if (window.kimiLLMManager) window.kimiLLMManager.temperature = parseFloat(value);
1459
+ }, 300);
1460
+
1461
+ const debouncedLLMTokensUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1462
+ if (kimiDB) await kimiDB.setPreference("llmMaxTokens", parseInt(value));
1463
+ if (window.kimiLLMManager) window.kimiLLMManager.maxTokens = parseInt(value);
1464
+ }, 300);
1465
+
1466
+ const debouncedLLMTopPUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1467
+ if (kimiDB) await kimiDB.setPreference("llmTopP", parseFloat(value));
1468
+ if (window.kimiLLMManager) window.kimiLLMManager.topP = parseFloat(value);
1469
+ }, 300);
1470
+
1471
+ const debouncedLLMFrequencyPenaltyUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1472
+ if (kimiDB) await kimiDB.setPreference("llmFrequencyPenalty", parseFloat(value));
1473
+ if (window.kimiLLMManager) window.kimiLLMManager.frequencyPenalty = parseFloat(value);
1474
+ }, 300);
1475
+
1476
+ const debouncedLLMPresencePenaltyUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1477
+ if (kimiDB) await kimiDB.setPreference("llmPresencePenalty", parseFloat(value));
1478
+ if (window.kimiLLMManager) window.kimiLLMManager.presencePenalty = parseFloat(value);
1479
+ }, 300);
1480
+
1481
+ const debouncedOpacityUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1482
+ if (kimiDB) await kimiDB.setPreference("interfaceOpacity", parseFloat(value));
1483
+ if (window.kimiAppearanceManager && window.kimiAppearanceManager.changeInterfaceOpacity)
1484
+ await window.kimiAppearanceManager.changeInterfaceOpacity(parseFloat(value));
1485
+ }, 300);
1486
+
1487
+ if (voiceRateSlider) {
1488
+ const listener = e => {
1489
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate");
1490
+ // Preserve legitimate zero values (avoid using || which treats 0 as falsy)
1491
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1492
+ if (isNaN(value)) value = 1.1;
1493
+
1494
+ document.getElementById("voice-rate-value").textContent = value;
1495
+ e.target.value = value; // Ensure slider shows validated value
1496
+ debouncedVoiceRateUpdate(value);
1497
+ };
1498
+ voiceRateSlider.addEventListener("input", listener);
1499
+ window._kimiListenerCleanup.push(() => voiceRateSlider.removeEventListener("input", listener));
1500
+ }
1501
+ if (voicePitchSlider) {
1502
+ const listener = e => {
1503
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch");
1504
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1505
+ if (isNaN(value)) value = 1.1;
1506
+
1507
+ document.getElementById("voice-pitch-value").textContent = value;
1508
+ e.target.value = value;
1509
+ debouncedVoicePitchUpdate(value);
1510
+ };
1511
+ voicePitchSlider.addEventListener("input", listener);
1512
+ window._kimiListenerCleanup.push(() => voicePitchSlider.removeEventListener("input", listener));
1513
+ }
1514
+ if (voiceVolumeSlider) {
1515
+ const listener = e => {
1516
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume");
1517
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1518
+ if (isNaN(value)) value = 0.8;
1519
+
1520
+ document.getElementById("voice-volume-value").textContent = value;
1521
+ e.target.value = value;
1522
+ debouncedVoiceVolumeUpdate(value);
1523
+ };
1524
+ voiceVolumeSlider.addEventListener("input", listener);
1525
+ window._kimiListenerCleanup.push(() => voiceVolumeSlider.removeEventListener("input", listener));
1526
+ }
1527
+ // Note: Language selector event listener is now handled by VoiceManager.setupLanguageSelector()
1528
+ // This prevents duplicate event listeners and ensures proper voice/language coordination
1529
+
1530
+ // Note: Voice selector event listener is now handled by VoiceManager.updateVoiceSelector()
1531
+ // This prevents duplicate event listeners and ensures proper voice preference coordination
1532
+
1533
+ // Batch personality traits optimization
1534
+ let personalityBatchTimeout = null;
1535
+ const pendingTraitChanges = {};
1536
+
1537
+ traitSliders.forEach(traitId => {
1538
+ const traitSlider = document.getElementById(traitId);
1539
+ if (traitSlider) {
1540
+ traitSlider.removeEventListener("input", window["_kimiTraitListener_" + traitId]);
1541
+ window["_kimiTraitListener_" + traitId] = async e => {
1542
+ const trait = traitId.replace("trait-", "");
1543
+ const value = parseInt(e.target.value, 10);
1544
+
1545
+ // Update UI immediately for responsive feel
1546
+ const valueSpan = document.getElementById(traitId + "-value");
1547
+ if (valueSpan) {
1548
+ valueSpan.textContent = value;
1549
+ }
1550
+
1551
+ // Store pending change for batch processing
1552
+ pendingTraitChanges[trait] = value;
1553
+
1554
+ // Clear existing timeout and set new one for batch save
1555
+ if (personalityBatchTimeout) {
1556
+ clearTimeout(personalityBatchTimeout);
1557
+ }
1558
+
1559
+ personalityBatchTimeout = setTimeout(async () => {
1560
+ if (kimiDB && Object.keys(pendingTraitChanges).length > 0) {
1561
+ try {
1562
+ // Use batch operation for all pending changes (affection included)
1563
+ await kimiDB.setPersonalityBatch(pendingTraitChanges);
1564
+
1565
+ // Side-effects handled by central 'personality:updated' listener.
1566
+ } catch (error) {
1567
+ console.error("Error batch saving personality traits:", error);
1568
+ }
1569
+
1570
+ // Clear pending changes
1571
+ Object.keys(pendingTraitChanges).forEach(key => delete pendingTraitChanges[key]);
1572
+ }
1573
+ }, 500); // Debounce for 500ms to batch multiple rapid changes
1574
+ };
1575
+ traitSlider.addEventListener("input", window["_kimiTraitListener_" + traitId]);
1576
+ }
1577
+ });
1578
+ if (llmTemperatureSlider) {
1579
+ const listener = e => {
1580
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature");
1581
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1582
+ if (isNaN(value)) value = 0.9;
1583
+
1584
+ document.getElementById("llm-temperature-value").textContent = value;
1585
+ e.target.value = value;
1586
+ debouncedLLMTempUpdate(value);
1587
+ };
1588
+ llmTemperatureSlider.addEventListener("input", listener);
1589
+ window._kimiListenerCleanup.push(() => llmTemperatureSlider.removeEventListener("input", listener));
1590
+ }
1591
+ if (llmMaxTokensSlider) {
1592
+ const listener = e => {
1593
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens");
1594
+ const value = validation?.value || parseInt(e.target.value) || 400;
1595
+
1596
+ document.getElementById("llm-max-tokens-value").textContent = value;
1597
+ e.target.value = value;
1598
+ debouncedLLMTokensUpdate(value);
1599
+ };
1600
+ llmMaxTokensSlider.addEventListener("input", listener);
1601
+ window._kimiListenerCleanup.push(() => llmMaxTokensSlider.removeEventListener("input", listener));
1602
+ }
1603
+ if (llmTopPSlider) {
1604
+ const listener = e => {
1605
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP");
1606
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1607
+ if (isNaN(value)) value = 0.9;
1608
+
1609
+ document.getElementById("llm-top-p-value").textContent = value;
1610
+ e.target.value = value;
1611
+ debouncedLLMTopPUpdate(value);
1612
+ };
1613
+ llmTopPSlider.addEventListener("input", listener);
1614
+ window._kimiListenerCleanup.push(() => llmTopPSlider.removeEventListener("input", listener));
1615
+ }
1616
+ if (llmFrequencyPenaltySlider) {
1617
+ const listener = e => {
1618
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty");
1619
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1620
+ if (isNaN(value)) value = 0.9;
1621
+
1622
+ document.getElementById("llm-frequency-penalty-value").textContent = value;
1623
+ e.target.value = value;
1624
+ debouncedLLMFrequencyPenaltyUpdate(value);
1625
+ };
1626
+ llmFrequencyPenaltySlider.addEventListener("input", listener);
1627
+ window._kimiListenerCleanup.push(() => llmFrequencyPenaltySlider.removeEventListener("input", listener));
1628
+ }
1629
+ if (llmPresencePenaltySlider) {
1630
+ const listener = e => {
1631
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty");
1632
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1633
+ if (isNaN(value)) value = 0.8;
1634
+
1635
+ document.getElementById("llm-presence-penalty-value").textContent = value;
1636
+ e.target.value = value;
1637
+ debouncedLLMPresencePenaltyUpdate(value);
1638
+ };
1639
+ llmPresencePenaltySlider.addEventListener("input", listener);
1640
+ window._kimiListenerCleanup.push(() => llmPresencePenaltySlider.removeEventListener("input", listener));
1641
+ }
1642
+ if (enableStreamingToggle) {
1643
+ const listener = async () => {
1644
+ try {
1645
+ const isEnabled = enableStreamingToggle.classList.contains("active");
1646
+ const newState = !isEnabled;
1647
+ enableStreamingToggle.classList.toggle("active", newState);
1648
+ enableStreamingToggle.setAttribute("aria-checked", newState ? "true" : "false");
1649
+ if (kimiDB) await kimiDB.setPreference("enableStreaming", newState);
1650
+ } catch (error) {
1651
+ console.error("Error toggling streaming:", error);
1652
+ }
1653
+ };
1654
+ enableStreamingToggle.addEventListener("click", listener);
1655
+ window._kimiListenerCleanup.push(() => enableStreamingToggle.removeEventListener("click", listener));
1656
+ }
1657
+ if (colorThemeSelect) {
1658
+ colorThemeSelect.removeEventListener("change", window._kimiColorThemeListener);
1659
+ window._kimiColorThemeListener = async e => {
1660
+ if (kimiDB) await kimiDB.setPreference("colorTheme", e.target.value);
1661
+ if (window.kimiAppearanceManager && window.kimiAppearanceManager.changeTheme) await window.kimiAppearanceManager.changeTheme(e.target.value);
1662
+ };
1663
+ colorThemeSelect.addEventListener("change", window._kimiColorThemeListener);
1664
+ }
1665
+ if (interfaceOpacitySlider) {
1666
+ const listener = e => {
1667
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity");
1668
+ let value = validation && !isNaN(validation.value) ? validation.value : parseFloat(e.target.value);
1669
+ if (isNaN(value)) value = 0.8;
1670
+
1671
+ document.getElementById("interface-opacity-value").textContent = value;
1672
+ e.target.value = value;
1673
+ debouncedOpacityUpdate(value);
1674
+ };
1675
+ interfaceOpacitySlider.addEventListener("input", listener);
1676
+ window._kimiListenerCleanup.push(() => interfaceOpacitySlider.removeEventListener("input", listener));
1677
+ }
1678
+ // Animation toggle is handled by KimiAppearanceManager
1679
+ // Remove the duplicate handler to prevent conflicts
1680
+ // Real-time transcript toggle (shows live speech transcription and AI responses)
1681
+ const transcriptToggle = document.getElementById("transcript-toggle");
1682
+ if (transcriptToggle) {
1683
+ if (kimiDB && kimiDB.getPreference) {
1684
+ kimiDB.getPreference("showTranscript", window.KIMI_CONFIG?.DEFAULTS?.SHOW_TRANSCRIPT ?? true).then(showTranscript => {
1685
+ transcriptToggle.classList.toggle("active", showTranscript);
1686
+ transcriptToggle.setAttribute("aria-checked", showTranscript ? "true" : "false");
1687
+ });
1688
+ }
1689
+ const onToggle = async () => {
1690
+ const enabled = !transcriptToggle.classList.contains("active");
1691
+ transcriptToggle.classList.toggle("active", enabled);
1692
+ transcriptToggle.setAttribute("aria-checked", enabled ? "true" : "false");
1693
+ // Save transcript display preference
1694
+ if (kimiDB && kimiDB.setPreference) {
1695
+ await kimiDB.setPreference("showTranscript", enabled);
1696
+ }
1697
+ // Apply change immediately if transcript is currently visible
1698
+ if (window.kimiVoiceManager && window.kimiVoiceManager.updateTranscriptVisibility) {
1699
+ if (!enabled) {
1700
+ // Hide transcript immediately if disabled (uses centralized logic)
1701
+ await window.kimiVoiceManager.updateTranscriptVisibility(false);
1702
+ }
1703
+ // If enabled, transcript will show naturally during next voice interaction
1704
+ }
1705
+ };
1706
+ transcriptToggle.onclick = onToggle;
1707
+ transcriptToggle.onkeydown = async e => {
1708
+ if (e.key === " " || e.key === "Enter") {
1709
+ e.preventDefault();
1710
+ await onToggle();
1711
+ }
1712
+ };
1713
+ }
1714
+ }
1715
+
1716
+ // Exposer globalement (KimiDataManager already exposed in kimi-data-manager.js)
1717
+ window.updateFavorabilityLabel = updateFavorabilityLabel;
1718
+ window.loadCharacterSection = loadCharacterSection;
1719
+ window.getBasicResponse = getBasicResponse;
1720
+ window.analyzeAndReact = analyzeAndReact;
1721
+ window.addMessageToChat = addMessageToChat;
1722
+ window.loadChatHistory = loadChatHistory;
1723
+ window.loadSettingsData = loadSettingsData;
1724
+ window.updateSlider = updateSlider;
1725
+
1726
+ // DYNAMIC SLIDER SYNC
1727
+ async function refreshAllSliders() {
1728
+ if (!window.kimiDB) return;
1729
+ const prefMap = [
1730
+ ["voice-rate", "voiceRate", "VOICE_RATE"],
1731
+ ["voice-pitch", "voicePitch", "VOICE_PITCH"],
1732
+ ["voice-volume", "voiceVolume", "VOICE_VOLUME"],
1733
+ ["llm-temperature", "llmTemperature", "LLM_TEMPERATURE"],
1734
+ ["llm-max-tokens", "llmMaxTokens", "LLM_MAX_TOKENS"],
1735
+ ["llm-top-p", "llmTopP", "LLM_TOP_P"],
1736
+ ["llm-frequency-penalty", "llmFrequencyPenalty", "LLM_FREQUENCY_PENALTY"],
1737
+ ["llm-presence-penalty", "llmPresencePenalty", "LLM_PRESENCE_PENALTY"],
1738
+ ["interface-opacity", "interfaceOpacity", "INTERFACE_OPACITY"]
1739
+ ];
1740
+ for (const [sliderId, prefKey, defaultKey] of prefMap) {
1741
+ try {
1742
+ const el = document.getElementById(sliderId);
1743
+ if (!el) continue;
1744
+ const stored = await window.kimiDB.getPreference(prefKey, window.KIMI_CONFIG?.DEFAULTS?.[defaultKey]);
1745
+ if (typeof stored === "number" || (typeof stored === "string" && stored !== null)) {
1746
+ updateSlider(sliderId, stored);
1747
+ }
1748
+ } catch {}
1749
+ }
1750
+
1751
+ // Load streaming preference
1752
+ try {
1753
+ const enableStreamingToggle = document.getElementById("enable-streaming");
1754
+ if (enableStreamingToggle) {
1755
+ const streamingEnabled = await window.kimiDB.getPreference("enableStreaming", window.KIMI_CONFIG?.DEFAULTS?.ENABLE_STREAMING ?? true);
1756
+ enableStreamingToggle.classList.toggle("active", streamingEnabled);
1757
+ enableStreamingToggle.setAttribute("aria-checked", streamingEnabled ? "true" : "false");
1758
+ }
1759
+ } catch {}
1760
+ }
1761
+ window.refreshAllSliders = refreshAllSliders;
1762
+
1763
+ const _debouncedPrefUpdate = window.KimiPerformanceUtils
1764
+ ? window.KimiPerformanceUtils.debounce(evt => {
1765
+ const key = evt.detail?.key;
1766
+ if (!key) return;
1767
+ const keyToSlider = {
1768
+ voiceRate: "voice-rate",
1769
+ voicePitch: "voice-pitch",
1770
+ voiceVolume: "voice-volume",
1771
+ llmTemperature: "llm-temperature",
1772
+ llmMaxTokens: "llm-max-tokens",
1773
+ llmTopP: "llm-top-p",
1774
+ llmFrequencyPenalty: "llm-frequency-penalty",
1775
+ llmPresencePenalty: "llm-presence-penalty",
1776
+ interfaceOpacity: "interface-opacity"
1777
+ };
1778
+ const sliderId = keyToSlider[key];
1779
+ if (sliderId && typeof evt.detail.value !== "undefined") {
1780
+ updateSlider(sliderId, evt.detail.value);
1781
+ }
1782
+ }, 120)
1783
+ : null;
1784
+ window.addEventListener("preferenceUpdated", evt => {
1785
+ if (_debouncedPrefUpdate) _debouncedPrefUpdate(evt);
1786
+ });
1787
+ window.updatePersonalitySliders = updatePersonalitySliders;
1788
+ window.updateStats = updateStats;
1789
+ window.initializeAllSliders = initializeAllSliders;
1790
+ window.syncLLMMaxTokensSlider = syncLLMMaxTokensSlider;
1791
+ window.syncLLMTemperatureSlider = syncLLMTemperatureSlider;
1792
+ window.updateTabsScrollIndicator = updateTabsScrollIndicator;
1793
+ window.loadAvailableModels = loadAvailableModels;
1794
+ window.sendMessage = sendMessage;
1795
+ window.setupSettingsListeners = setupSettingsListeners;
1796
+ window.syncPersonalityTraits = syncPersonalityTraits;
1797
+ window.validateEmotionContext = validateEmotionContext;
1798
+ window.ensureVideoContextConsistency = ensureVideoContextConsistency;
1799
+
1800
+ document.addEventListener("DOMContentLoaded", function () {
1801
+ const toggleBtn = document.getElementById("toggle-personality-traits");
1802
+ const cheatPanel = document.getElementById("personality-traits-panel");
1803
+ if (toggleBtn && cheatPanel) {
1804
+ toggleBtn.addEventListener("click", function () {
1805
+ const expanded = toggleBtn.getAttribute("aria-expanded") === "true";
1806
+ toggleBtn.setAttribute("aria-expanded", !expanded);
1807
+ cheatPanel.classList.toggle("open", !expanded);
1808
+ });
1809
+ }
1810
+
1811
+ // Refresh UI models list when the LLM model changes programmatically
1812
+ try {
1813
+ window.addEventListener("llmModelChanged", () => {
1814
+ if (typeof window.loadAvailableModels === "function") {
1815
+ window.loadAvailableModels();
1816
+ }
1817
+ });
1818
+ } catch (e) {}
1819
+
1820
+ // Typing indicator wiring
1821
+ try {
1822
+ // Soft tweak of API key input attributes shortly after load to reduce password manager prompts
1823
+ setTimeout(() => {
1824
+ const apiInput = document.getElementById("openrouter-api-key");
1825
+ if (apiInput) {
1826
+ apiInput.setAttribute("autocomplete", "new-password");
1827
+ apiInput.setAttribute("name", "openrouter_api_key");
1828
+ apiInput.setAttribute("data-lpignore", "true");
1829
+ apiInput.setAttribute("data-1p-ignore", "true");
1830
+ apiInput.setAttribute("data-bwignore", "true");
1831
+ apiInput.setAttribute("data-form-type", "other");
1832
+ apiInput.setAttribute("autocapitalize", "none");
1833
+ apiInput.setAttribute("autocorrect", "off");
1834
+ apiInput.setAttribute("spellcheck", "false");
1835
+ }
1836
+ }, 300);
1837
+
1838
+ window.addEventListener("chat:typing:start", () => {
1839
+ const waitingIndicator = document.getElementById("waiting-indicator");
1840
+ const globalTyping = document.getElementById("global-typing-indicator");
1841
+ clearTimeout(window._kimiTypingDelayTimer);
1842
+ window._kimiTypingDelayTimer = setTimeout(() => {
1843
+ if (waitingIndicator) waitingIndicator.classList.add("visible");
1844
+ if (globalTyping) globalTyping.classList.add("visible");
1845
+ }, 150);
1846
+ // Safety auto-hide after 10s in case stop event is blocked
1847
+ clearTimeout(window._kimiTypingSafetyTimer);
1848
+ window._kimiTypingSafetyTimer = setTimeout(() => {
1849
+ if (waitingIndicator) waitingIndicator.classList.remove("visible");
1850
+ if (globalTyping) globalTyping.classList.remove("visible");
1851
+ }, 10000);
1852
+ });
1853
+ window.addEventListener("chat:typing:stop", () => {
1854
+ const waitingIndicator = document.getElementById("waiting-indicator");
1855
+ const globalTyping = document.getElementById("global-typing-indicator");
1856
+ if (waitingIndicator) waitingIndicator.classList.remove("visible");
1857
+ if (globalTyping) globalTyping.classList.remove("visible");
1858
+ clearTimeout(window._kimiTypingSafetyTimer);
1859
+ clearTimeout(window._kimiTypingDelayTimer);
1860
+ });
1861
+ } catch (e) {}
1862
+ });
1863
+
1864
+ // Function to sync all personality traits with database and UI
1865
+ async function syncPersonalityTraits(characterName = null) {
1866
+ const kimiDB = window.kimiDB;
1867
+ if (!kimiDB) return;
1868
+
1869
+ const selectedCharacter = characterName || (await kimiDB.getSelectedCharacter());
1870
+ const traits = window.getCharacterTraits ? await window.getCharacterTraits(selectedCharacter) : await kimiDB.getAllPersonalityTraits(selectedCharacter);
1871
+
1872
+ // Build required traits prioritizing character-specific defaults (fallback to generic)
1873
+ const getRequiredTraits = () => {
1874
+ const charDefaults = (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]?.traits) || {};
1875
+ let generic = {};
1876
+ if (window.KimiEmotionSystem) {
1877
+ const emotionSystem = new window.KimiEmotionSystem(kimiDB);
1878
+ generic = emotionSystem.TRAIT_DEFAULTS;
1879
+ } else if (window.getTraitDefaults) {
1880
+ generic = window.getTraitDefaults();
1881
+ } else {
1882
+ generic = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
1883
+ }
1884
+ // Character defaults take precedence over generic defaults
1885
+ return { ...generic, ...charDefaults };
1886
+ };
1887
+
1888
+ const requiredTraits = getRequiredTraits();
1889
+ let needsUpdate = false;
1890
+ const updatedTraits = {};
1891
+
1892
+ for (const [trait, defaultValue] of Object.entries(requiredTraits)) {
1893
+ const currentValue = traits[trait];
1894
+ if (typeof currentValue !== "number" || currentValue < 0 || currentValue > 100) {
1895
+ updatedTraits[trait] = defaultValue;
1896
+ needsUpdate = true;
1897
+ } else {
1898
+ updatedTraits[trait] = currentValue;
1899
+ }
1900
+ }
1901
+
1902
+ // Update database if needed
1903
+ if (needsUpdate) {
1904
+ await kimiDB.setPersonalityBatch(updatedTraits, selectedCharacter);
1905
+ }
1906
+
1907
+ // Update UI sliders
1908
+ for (const [trait, value] of Object.entries(updatedTraits)) {
1909
+ updateSlider(`trait-${trait}`, value);
1910
+ }
1911
+
1912
+ // Update memory cache
1913
+ if (window.kimiMemory && updatedTraits.affection) {
1914
+ window.kimiMemory.affectionTrait = updatedTraits.affection;
1915
+ if (window.updateGlobalPersonalityUI) {
1916
+ window.updateGlobalPersonalityUI();
1917
+ } else if (window.kimiMemory.updateFavorabilityBar) {
1918
+ // Fallback (will internally compute average now)
1919
+ window.kimiMemory.updateFavorabilityBar();
1920
+ }
1921
+ }
1922
+
1923
+ // Video/voice updates are centralized in the 'personality:updated' listener.
1924
+
1925
+ return updatedTraits;
1926
+ }
1927
+
1928
+ // Simplified validation using centralized emotion system
1929
+ function validateEmotionContext(emotion) {
1930
+ return window.kimiEmotionSystem?.validateEmotion(emotion) || "neutral";
1931
+ }
1932
+
1933
+ // Simplified video context consistency check using centralized system
1934
+ async function ensureVideoContextConsistency() {
1935
+ if (!window.kimiVideo || !window.kimiDB) return;
1936
+
1937
+ const selectedCharacter = await window.kimiDB.getSelectedCharacter();
1938
+ const traits = window.getCharacterTraits
1939
+ ? await window.getCharacterTraits(selectedCharacter)
1940
+ : await window.kimiDB.getAllPersonalityTraits(selectedCharacter);
1941
+
1942
+ // Validate current video context using centralized validation
1943
+ const currentInfo = window.kimiVideo.getCurrentVideoInfo();
1944
+ const validatedEmotion = validateEmotionContext(currentInfo.emotion);
1945
+
1946
+ if (validatedEmotion !== currentInfo.emotion) {
1947
+ window.kimiVideo.switchToContext("neutral", "neutral", null, traits, traits.affection);
1948
+ }
1949
+ }
kimi-js/kimi-plugin-manager.js ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class KimiPluginManager {
2
+ constructor() {
3
+ this.plugins = [];
4
+ this.pluginsRoot = "kimi-plugins/";
5
+ }
6
+
7
+ // Common security validation for plugin file paths
8
+ isValidPluginPath(path) {
9
+ return (
10
+ typeof path === "string" &&
11
+ /^[-a-zA-Z0-9_\/.]+$/.test(path) &&
12
+ !path.startsWith("/") &&
13
+ !path.includes("..") &&
14
+ !/^https?:\/\//i.test(path) &&
15
+ path.startsWith("kimi-plugins/")
16
+ );
17
+ }
18
+ async loadPlugins() {
19
+ const pluginDirs = await this.getPluginDirs();
20
+ this.plugins = [];
21
+ let pluginThemeActive = false;
22
+ for (const dir of pluginDirs) {
23
+ try {
24
+ const manifest = await fetch(this.pluginsRoot + dir + "/manifest.json").then(r => r.json());
25
+ manifest._dir = dir;
26
+ manifest.enabled = this.isPluginEnabled(dir, manifest.enabled);
27
+
28
+ // Basic manifest validation and path sanitization (deny external or absolute URLs)
29
+ const validTypes = new Set(["theme", "voice", "behavior"]);
30
+ const isSafePath = p =>
31
+ typeof p === "string" &&
32
+ /^[-a-zA-Z0-9_\/.]+$/.test(p) &&
33
+ !p.startsWith("/") &&
34
+ !p.includes("..") &&
35
+ !/^https?:\/\//i.test(p);
36
+
37
+ if (!manifest.name || !manifest.type || !validTypes.has(manifest.type)) {
38
+ console.warn(`Invalid plugin manifest in ${dir}: missing name or invalid type`);
39
+ continue;
40
+ }
41
+ if (manifest.style && !isSafePath(manifest.style)) {
42
+ console.warn(`Blocked unsafe style path in ${dir}: ${manifest.style}`);
43
+ delete manifest.style;
44
+ }
45
+ if (manifest.main && !isSafePath(manifest.main)) {
46
+ console.warn(`Blocked unsafe main path in ${dir}: ${manifest.main}`);
47
+ delete manifest.main;
48
+ }
49
+
50
+ this.plugins.push(manifest);
51
+
52
+ if (manifest.enabled && manifest.style) {
53
+ this.loadCSS(this.pluginsRoot + dir + "/" + manifest.style);
54
+ }
55
+ if (manifest.enabled && manifest.main) {
56
+ this.loadJS(this.pluginsRoot + dir + "/" + manifest.main);
57
+ }
58
+ if (manifest.enabled && manifest.type === "theme" && dir === "sample-theme") {
59
+ pluginThemeActive = true;
60
+ }
61
+ } catch (e) {
62
+ console.warn("Failed loading plugin:", dir, e);
63
+ }
64
+ }
65
+ if (pluginThemeActive) {
66
+ document.documentElement.setAttribute("data-theme", "plugin-sample-theme");
67
+ } else {
68
+ // Restore previous or default theme depuis Dexie
69
+ if (window.kimiDB && window.kimiDB.getPreference) {
70
+ const userTheme = await window.kimiDB.getPreference("colorTheme", "dark");
71
+ document.documentElement.setAttribute("data-theme", userTheme);
72
+ } else {
73
+ document.documentElement.setAttribute("data-theme", "dark");
74
+ }
75
+ }
76
+ this.renderPluginList();
77
+ }
78
+ async getPluginDirs() {
79
+ return ["sample-theme", "sample-voice", "sample-behavior"];
80
+ }
81
+ loadCSS(href) {
82
+ if (!window.KimiDOMUtils) {
83
+ console.error("KimiDOMUtils not available for loadCSS");
84
+ return;
85
+ }
86
+ if (!window.KimiDOMUtils.get('link[href="' + href + '"]')) {
87
+ if (!this.isValidPluginPath(href)) {
88
+ console.error(`Blocked unsafe CSS path: ${href}`);
89
+ return;
90
+ }
91
+
92
+ const link = document.createElement("link");
93
+ link.rel = "stylesheet";
94
+ link.type = "text/css";
95
+ link.href = href;
96
+
97
+ link.onerror = function () {
98
+ console.error(`Failed to load plugin CSS: ${href}`);
99
+ };
100
+
101
+ document.head.appendChild(link);
102
+ }
103
+ }
104
+ loadJS(src) {
105
+ if (!window.KimiDOMUtils) {
106
+ console.error("KimiDOMUtils not available for loadJS");
107
+ return;
108
+ }
109
+ if (!window.KimiDOMUtils.get('script[src="' + src + '"]')) {
110
+ if (!this.isValidPluginPath(src)) {
111
+ console.error(`Blocked unsafe script path: ${src}`);
112
+ return;
113
+ }
114
+
115
+ const script = document.createElement("script");
116
+ script.src = src;
117
+ script.type = "text/javascript";
118
+
119
+ script.onerror = function () {
120
+ console.error(`Failed to load plugin script: ${src}`);
121
+ };
122
+
123
+ if (window.CSP_NONCE) {
124
+ script.nonce = window.CSP_NONCE;
125
+ }
126
+
127
+ document.body.appendChild(script);
128
+ }
129
+ }
130
+ renderPluginList() {
131
+ if (!window.KimiDOMUtils) {
132
+ console.error("KimiDOMUtils not available");
133
+ return;
134
+ }
135
+ const container = window.KimiDOMUtils.get("#plugin-list");
136
+ if (!container) return;
137
+ while (container.firstChild) {
138
+ container.removeChild(container.firstChild);
139
+ }
140
+ for (const plugin of this.plugins) {
141
+ const div = document.createElement("div");
142
+ div.className = "plugin-card";
143
+ // Left: info
144
+ const info = document.createElement("div");
145
+ info.className = "plugin-info";
146
+ const title = document.createElement("div");
147
+ title.className = "plugin-title";
148
+ title.textContent = plugin.name;
149
+ const type = document.createElement("span");
150
+ type.className = "plugin-type";
151
+ type.textContent = plugin.type;
152
+ title.appendChild(type);
153
+ const desc = document.createElement("div");
154
+ desc.className = "plugin-desc";
155
+ desc.textContent = plugin.description;
156
+ const author = document.createElement("div");
157
+ author.className = "plugin-author";
158
+ author.textContent = plugin.author;
159
+ info.appendChild(title);
160
+ info.appendChild(desc);
161
+ info.appendChild(author);
162
+ div.appendChild(info);
163
+ // Center: badges/swatch
164
+ const centerCol = document.createElement("div");
165
+ centerCol.className = "plugin-card-center";
166
+ const typeBadge = document.createElement("span");
167
+ typeBadge.className = "plugin-type-badge";
168
+ typeBadge.textContent =
169
+ plugin.type === "theme" ? "Theme" : plugin.type.charAt(0).toUpperCase() + plugin.type.slice(1);
170
+ centerCol.appendChild(typeBadge);
171
+ if (plugin.type === "theme") {
172
+ const swatch = document.createElement("div");
173
+ swatch.className = "plugin-theme-swatch";
174
+
175
+ // Create color spans safely
176
+ const colors = ["#3b82f6", "#a5b4fc", "#6366f1"];
177
+ colors.forEach(color => {
178
+ const span = document.createElement("span");
179
+ span.style.background = color;
180
+ swatch.appendChild(span);
181
+ });
182
+ centerCol.appendChild(swatch);
183
+ if (plugin.enabled) {
184
+ const activeBadge = document.createElement("span");
185
+ activeBadge.className = "plugin-active-badge";
186
+ activeBadge.textContent = "Active Theme";
187
+ centerCol.appendChild(activeBadge);
188
+ }
189
+ }
190
+ div.appendChild(centerCol);
191
+ // Right: switch
192
+ const rightCol = document.createElement("div");
193
+ rightCol.className = "plugin-card-switch";
194
+ const switchLabel = document.createElement("label");
195
+ switchLabel.className = "toggle-switch";
196
+ const input = document.createElement("input");
197
+ input.type = "checkbox";
198
+ input.checked = !!plugin.enabled;
199
+ input.style.display = "none";
200
+ input.addEventListener("change", () => {
201
+ plugin.enabled = input.checked;
202
+ this.savePluginState(plugin._dir, plugin.enabled);
203
+ this.loadPlugins();
204
+ if (input.checked) {
205
+ switchLabel.classList.add("active");
206
+ } else {
207
+ switchLabel.classList.remove("active");
208
+ }
209
+ });
210
+ const slider = document.createElement("span");
211
+ slider.className = "slider";
212
+ switchLabel.appendChild(input);
213
+ switchLabel.appendChild(slider);
214
+ if (input.checked) switchLabel.classList.add("active");
215
+ rightCol.appendChild(switchLabel);
216
+ div.appendChild(rightCol);
217
+ container.appendChild(div);
218
+ }
219
+ }
220
+ savePluginState(dir, enabled) {
221
+ const key = "kimi-plugin-enabled-" + dir;
222
+ localStorage.setItem(key, enabled ? "1" : "0");
223
+ }
224
+ isPluginEnabled(dir, defaultValue) {
225
+ const key = "kimi-plugin-enabled-" + dir;
226
+ const val = localStorage.getItem(key);
227
+ if (val === null) return defaultValue;
228
+ return val === "1";
229
+ }
230
+ }
231
+
232
+ window.KimiPluginManager = new KimiPluginManager();
233
+
234
+ document.addEventListener("DOMContentLoaded", () => {
235
+ if (window.KimiPluginManager) window.KimiPluginManager.loadPlugins();
236
+ const refreshBtn = document.getElementById("refresh-plugins");
237
+ if (refreshBtn) {
238
+ refreshBtn.onclick = async () => {
239
+ const originalText = refreshBtn.innerHTML;
240
+ refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Refreshing...';
241
+ refreshBtn.disabled = true;
242
+
243
+ try {
244
+ await window.KimiPluginManager.loadPlugins();
245
+ refreshBtn.innerHTML = '<i class="fas fa-check"></i> Refreshed!';
246
+ setTimeout(() => {
247
+ refreshBtn.innerHTML = originalText;
248
+ refreshBtn.disabled = false;
249
+ }, 1500);
250
+ } catch (error) {
251
+ console.error("Error refreshing plugins:", error);
252
+ refreshBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Error';
253
+ setTimeout(() => {
254
+ refreshBtn.innerHTML = originalText;
255
+ refreshBtn.disabled = false;
256
+ }, 2000);
257
+ }
258
+ };
259
+ }
260
+ });
kimi-js/kimi-script.js ADDED
@@ -0,0 +1,1250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import { KimiDataManager } from "./kimi-data-manager.js"; // Explicit import (phasing out window.KimiDataManager)
7
+ import { initializeVideoController } from "./kimi-video-controller.js"; // Unified video control
8
+
9
+ document.addEventListener("DOMContentLoaded", async function () {
10
+ const DEFAULT_SYSTEM_PROMPT = window.DEFAULT_SYSTEM_PROMPT;
11
+
12
+ let kimiDB = null;
13
+ let kimiLLM = null;
14
+ let isSystemReady = false;
15
+
16
+ // Global debug flag for sync/log verbosity (default: false)
17
+ if (typeof window.KIMI_DEBUG_SYNC === "undefined") {
18
+ window.KIMI_DEBUG_SYNC = false;
19
+ }
20
+
21
+ const kimiInit = new KimiInitManager();
22
+ let kimiVideo = null;
23
+
24
+ // Error manager is already initialized in kimi-error-manager.js
25
+
26
+ try {
27
+ kimiDB = new KimiDatabase();
28
+ await kimiDB.init();
29
+
30
+ // Expose globally as soon as available
31
+ window.kimiDB = kimiDB;
32
+
33
+ const selectedCharacter = await kimiDB.getPreference("selectedCharacter", "kimi");
34
+ const favorabilityLabel = window.KimiDOMUtils.get("#favorability-label");
35
+ if (favorabilityLabel && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]) {
36
+ favorabilityLabel.removeAttribute("for");
37
+ if (window.setI18n) window.setI18n(favorabilityLabel, "personality_average_of");
38
+ favorabilityLabel.setAttribute("data-i18n-params", JSON.stringify({ name: window.KIMI_CHARACTERS[selectedCharacter].name }));
39
+ favorabilityLabel.textContent = `💖 Personality average of ${window.KIMI_CHARACTERS[selectedCharacter].name}`;
40
+ }
41
+ const chatHeaderName = window.KimiDOMUtils.get(".chat-header span[data-i18n]");
42
+ if (chatHeaderName && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]) {
43
+ if (window.setI18n) window.setI18n(chatHeaderName, `chat_with_${selectedCharacter}`);
44
+ }
45
+ kimiLLM = new KimiLLMManager(kimiDB);
46
+ window.kimiLLM = kimiLLM;
47
+ await kimiLLM.init();
48
+
49
+ // Initialize unified emotion system
50
+ window.kimiEmotionSystem = new KimiEmotionSystem(kimiDB);
51
+
52
+ // Initialize the new memory system
53
+ window.kimiMemorySystem = new KimiMemorySystem(kimiDB);
54
+ await window.kimiMemorySystem.init();
55
+
56
+ // Initialize legacy memory for favorability
57
+ const kimiMemory = new KimiMemory(kimiDB);
58
+ await kimiMemory.init();
59
+ window.kimiMemory = kimiMemory;
60
+
61
+ // Expose globally (already set before init)
62
+
63
+ // Load available models now that LLM is ready
64
+ if (window.loadAvailableModels) {
65
+ setTimeout(() => window.loadAvailableModels(), 500);
66
+ }
67
+
68
+ isSystemReady = true;
69
+ window.isSystemReady = true;
70
+ // API config UI will be initialized after ApiUi is defined
71
+ // Update global personality average UI once initial traits are loaded
72
+ if (window.updateGlobalPersonalityUI) {
73
+ try {
74
+ await window.updateGlobalPersonalityUI(selectedCharacter);
75
+ } catch {}
76
+ }
77
+ if (window.refreshAllSliders) {
78
+ try {
79
+ await window.refreshAllSliders();
80
+ } catch {}
81
+ }
82
+ } catch (error) {
83
+ console.error("Initialization error:", error);
84
+ // Log initialization error to error manager
85
+ if (window.kimiErrorManager) {
86
+ window.kimiErrorManager.logInitError("KimiApp", error, {
87
+ selectedCharacter: selectedCharacter,
88
+ stage: "main_initialization"
89
+ });
90
+ }
91
+ }
92
+ // Centralized helpers for API config UI
93
+ const ApiUi = {
94
+ presenceDot: () => document.getElementById("api-key-presence"),
95
+ presenceDotTest: () => document.getElementById("api-key-presence-test"),
96
+ apiKeyInput: () => document.getElementById("provider-api-key"),
97
+ toggleBtn: () => document.getElementById("toggle-api-key"),
98
+ providerSelect: () => document.getElementById("llm-provider"),
99
+ baseUrlInput: () => document.getElementById("llm-base-url"),
100
+ modelIdInput: () => document.getElementById("llm-model-id"),
101
+ savedBadge: () => document.getElementById("api-key-saved"),
102
+ statusSpan: () => document.getElementById("api-status"),
103
+ testBtn: () => document.getElementById("test-api"),
104
+ // Saved key indicator (left dot)
105
+ setPresence(color) {
106
+ const dot = this.presenceDot();
107
+ if (dot) dot.style.backgroundColor = color;
108
+ },
109
+ // Test result indicator (right dot)
110
+ setTestPresence(color) {
111
+ const dot2 = this.presenceDotTest();
112
+ if (dot2) dot2.style.backgroundColor = color;
113
+ },
114
+ clearStatus() {
115
+ const s = this.statusSpan();
116
+ if (s) {
117
+ s.textContent = "";
118
+ s.style.color = "";
119
+ }
120
+ },
121
+ setTestEnabled(enabled) {
122
+ const b = this.testBtn();
123
+ if (b) b.disabled = !enabled;
124
+ }
125
+ };
126
+
127
+ // Initial presence state based on current input value
128
+ {
129
+ const currentVal = (ApiUi.apiKeyInput() || {}).value || "";
130
+ const colorInit = currentVal && currentVal.length > 0 ? "#4caf50" : "#9e9e9e";
131
+ ApiUi.setPresence(colorInit);
132
+ // On load, test status is unknown
133
+ ApiUi.setTestPresence("#9e9e9e");
134
+ }
135
+
136
+ // Initialize API config UI from saved preferences
137
+ async function initializeApiConfigUI() {
138
+ try {
139
+ if (!window.kimiDB) return;
140
+ const provider = await window.kimiDB.getPreference("llmProvider", "openrouter");
141
+ // Resolve base URL preference: prefer provider-specific stored key for modifiable
142
+ let baseUrl;
143
+ const shared = window.KimiProviderPlaceholders || {};
144
+ if (provider === "openai-compatible" || provider === "ollama") {
145
+ const key = `llmBaseUrl_${provider}`;
146
+ const defaultForProvider = provider === "openai-compatible" ? "" : shared[provider];
147
+ baseUrl = await window.kimiDB.getPreference(key, defaultForProvider);
148
+ } else {
149
+ baseUrl = shared[provider] || shared.openai;
150
+ }
151
+ const modelId = await window.kimiDB.getPreference("llmModelId", window.kimiLLM ? window.kimiLLM.currentModel : "model-id");
152
+ const providerSelect = ApiUi.providerSelect();
153
+ if (providerSelect) providerSelect.value = provider;
154
+ const baseUrlInput = ApiUi.baseUrlInput();
155
+ const modelIdInput = ApiUi.modelIdInput();
156
+ const apiKeyInput = ApiUi.apiKeyInput();
157
+
158
+ // Set base URL based on modifiability
159
+ if (baseUrlInput) {
160
+ const isModifiable = isUrlModifiable(provider);
161
+ baseUrlInput.value = baseUrl || "";
162
+ baseUrlInput.disabled = !isModifiable;
163
+ baseUrlInput.style.opacity = isModifiable ? "1" : "0.6";
164
+ }
165
+ // Only prefill model for OpenRouter, others should show placeholder only
166
+ if (modelIdInput) {
167
+ if (provider === "openrouter") {
168
+ if (!modelIdInput.value) modelIdInput.value = modelId;
169
+ } else {
170
+ modelIdInput.value = "";
171
+ }
172
+ }
173
+ // Load the provider-specific key
174
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "providerApiKey";
175
+ const storedKey = await window.kimiDB.getPreference(keyPref, "");
176
+ if (apiKeyInput) apiKeyInput.value = storedKey || "";
177
+ ApiUi.setPresence(storedKey ? "#4caf50" : "#9e9e9e");
178
+ ApiUi.setTestPresence("#9e9e9e");
179
+ const savedBadge = ApiUi.savedBadge();
180
+ if (savedBadge) {
181
+ // Show only if provider requires a key and key exists
182
+ if (provider !== "ollama" && storedKey) {
183
+ savedBadge.style.display = "inline";
184
+ } else {
185
+ savedBadge.style.display = "none";
186
+ }
187
+ }
188
+ ApiUi.clearStatus();
189
+ // Enable/disable Test button according to validation (Ollama does not require API key)
190
+ const valid = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(storedKey || ""));
191
+ ApiUi.setTestEnabled(provider === "ollama" ? true : valid);
192
+ // Update dynamic label and placeholders using change handler logic
193
+ if (providerSelect && typeof providerSelect.dispatchEvent === "function") {
194
+ const ev = new Event("change");
195
+ providerSelect.dispatchEvent(ev);
196
+ }
197
+ } catch (e) {
198
+ console.warn("Failed to initialize API config UI:", e);
199
+ // Log UI initialization error
200
+ if (window.kimiErrorManager) {
201
+ window.kimiErrorManager.logUIError("ApiConfigUI", e, {
202
+ stage: "api_config_initialization"
203
+ });
204
+ }
205
+ }
206
+ }
207
+ // Hydrate API config UI from DB after ApiUi is defined and function declared
208
+ initializeApiConfigUI();
209
+
210
+ // Listen for model changes and update the UI only for OpenRouter
211
+ window.addEventListener("llmModelChanged", function (event) {
212
+ const modelIdInput = ApiUi.modelIdInput();
213
+ const providerSelect = ApiUi.providerSelect();
214
+
215
+ // Only update the field if current provider is OpenRouter
216
+ if (modelIdInput && event.detail && event.detail.id && providerSelect && providerSelect.value === "openrouter") {
217
+ modelIdInput.value = event.detail.id;
218
+ }
219
+ });
220
+
221
+ // Helper function to check if URL is modifiable for current provider
222
+ function isUrlModifiable(provider) {
223
+ return provider === "openai-compatible" || provider === "ollama";
224
+ }
225
+
226
+ const providerSelectEl = document.getElementById("llm-provider");
227
+ if (providerSelectEl) {
228
+ providerSelectEl.addEventListener("change", async function (e) {
229
+ const provider = e.target.value;
230
+ const baseUrlInput = ApiUi.baseUrlInput();
231
+ const modelIdInput = ApiUi.modelIdInput();
232
+ const apiKeyInput = ApiUi.apiKeyInput();
233
+
234
+ const shared = window.KimiProviderPlaceholders || {};
235
+ const p = {
236
+ url: shared[provider] || "",
237
+ keyPh: provider === "ollama" ? "" : "your-key",
238
+ model: provider === "openrouter" && window.kimiLLM ? window.kimiLLM.currentModel : "model-id"
239
+ };
240
+ if (baseUrlInput) {
241
+ // Set placeholder: for openai-compatible we want an empty placeholder
242
+ baseUrlInput.placeholder = provider === "openai-compatible" ? "" : p.url;
243
+ // Only allow URL modification for custom and ollama providers
244
+ const isModifiable = isUrlModifiable(provider);
245
+
246
+ if (isModifiable) {
247
+ // For custom and ollama: load saved URL or use sensible default per provider
248
+ const defaultForProvider = provider === "openai-compatible" ? "" : p.url;
249
+ const key = `llmBaseUrl_${provider}`;
250
+ const savedUrl = await window.kimiDB.getPreference(key, defaultForProvider);
251
+ baseUrlInput.value = savedUrl || "";
252
+ baseUrlInput.disabled = false;
253
+ baseUrlInput.style.opacity = "1";
254
+ } else {
255
+ // For other providers: fixed URL, not modifiable
256
+ baseUrlInput.value = p.url;
257
+ baseUrlInput.disabled = true;
258
+ baseUrlInput.style.opacity = "0.6";
259
+ }
260
+ }
261
+ if (apiKeyInput) {
262
+ apiKeyInput.placeholder = p.keyPh;
263
+ // Masquer/désactiver le champ pour Ollama/local
264
+ if (provider === "ollama") {
265
+ apiKeyInput.value = "";
266
+ apiKeyInput.disabled = true;
267
+ apiKeyInput.style.display = "none";
268
+ } else {
269
+ apiKeyInput.disabled = false;
270
+ apiKeyInput.style.display = "";
271
+ }
272
+ }
273
+ if (modelIdInput) {
274
+ modelIdInput.placeholder = p.model;
275
+ // Only populate the field for OpenRouter since those are the models we have in the list
276
+ // For other providers, user must manually enter the provider-specific model ID
277
+ modelIdInput.value = provider === "openrouter" && window.kimiLLM ? window.kimiLLM.currentModel : "";
278
+ }
279
+ if (window.kimiDB) {
280
+ await window.kimiDB.setPreference("llmProvider", provider);
281
+
282
+ const apiKeyLabel = document.getElementById("api-key-label");
283
+ // Load provider-specific key into the input for clarity
284
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "providerApiKey";
285
+ const storedKey = await window.kimiDB.getPreference(keyPref, "");
286
+ if (apiKeyInput && provider !== "ollama") apiKeyInput.value = storedKey || "";
287
+ const color = provider === "ollama" ? "#9e9e9e" : storedKey && storedKey.length > 0 ? "#4caf50" : "#9e9e9e";
288
+ ApiUi.setPresence(color);
289
+ // Changing provider invalidates previous test state
290
+ ApiUi.setTestPresence("#9e9e9e");
291
+ ApiUi.setTestEnabled(provider === "ollama" ? true : !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(storedKey || "")));
292
+
293
+ // Dynamic label per provider
294
+ if (apiKeyLabel) {
295
+ apiKeyLabel.textContent = window.KimiProviderUtils ? window.KimiProviderUtils.getLabelForProvider(provider) : "API Key";
296
+ }
297
+ const savedBadge = ApiUi.savedBadge();
298
+ if (savedBadge) {
299
+ if (provider !== "ollama" && storedKey) {
300
+ savedBadge.style.display = "inline";
301
+ } else {
302
+ savedBadge.style.display = "none";
303
+ }
304
+ }
305
+ ApiUi.clearStatus();
306
+
307
+ // Save URL after all UI updates are complete
308
+ const isModifiableFinal = isUrlModifiable(provider);
309
+ // Only persist provider-specific llmBaseUrl when the provider allows modification.
310
+ if (isModifiableFinal && baseUrlInput) {
311
+ const key = `llmBaseUrl_${provider}`;
312
+ await window.kimiDB.setPreference(key, baseUrlInput.value || "");
313
+ }
314
+ }
315
+ });
316
+
317
+ // Listen for model ID changes and update the current model
318
+ const modelIdInput = ApiUi.modelIdInput();
319
+ if (modelIdInput) {
320
+ modelIdInput.addEventListener("blur", async function (e) {
321
+ const newModelId = e.target.value.trim();
322
+ if (newModelId && window.kimiLLM && newModelId !== window.kimiLLM.currentModel) {
323
+ try {
324
+ await window.kimiLLM.setCurrentModel(newModelId);
325
+ } catch (error) {
326
+ console.warn("Failed to set model:", error.message);
327
+ // Reset to current model if setting failed
328
+ e.target.value = window.kimiLLM.currentModel || "";
329
+ }
330
+ }
331
+ });
332
+ }
333
+
334
+ // Listen for Base URL changes and save for modifiable providers
335
+ const baseUrlInput = ApiUi.baseUrlInput();
336
+ if (baseUrlInput) {
337
+ baseUrlInput.addEventListener("blur", async function (e) {
338
+ const providerSelect = ApiUi.providerSelect();
339
+ const provider = providerSelect ? providerSelect.value : "openrouter";
340
+ const isModifiable = isUrlModifiable(provider);
341
+
342
+ if (isModifiable && window.kimiDB) {
343
+ const newUrl = e.target.value.trim();
344
+ try {
345
+ const key = `llmBaseUrl_${provider}`;
346
+ // Allow empty string to be saved for openai-compatible (user may clear it)
347
+ await window.kimiDB.setPreference(key, newUrl || "");
348
+ } catch (error) {
349
+ console.warn("Failed to save base URL:", error.message);
350
+ }
351
+ }
352
+ });
353
+ }
354
+ }
355
+
356
+ // Loading screen management
357
+ const hideLoadingScreen = () => {
358
+ const loadingScreen = document.getElementById("loading-screen");
359
+ if (loadingScreen) {
360
+ loadingScreen.style.opacity = "0";
361
+ setTimeout(() => {
362
+ loadingScreen.style.display = "none";
363
+ }, 500);
364
+ }
365
+ };
366
+
367
+ // Hide loading screen when resources are loaded
368
+ if (document.readyState === "complete") {
369
+ setTimeout(hideLoadingScreen, 1000);
370
+ } else {
371
+ window.addEventListener("load", () => {
372
+ setTimeout(hideLoadingScreen, 1000);
373
+ });
374
+ }
375
+
376
+ // Use centralized video utilities
377
+ let video1 = window.KimiVideoManager.getVideoElement("#video1");
378
+ let video2 = window.KimiVideoManager.getVideoElement("#video2");
379
+ if (!video1 || !video2) {
380
+ const videoContainer = document.querySelector(".video-container");
381
+ if (videoContainer) {
382
+ video1 = window.KimiVideoManager.createVideoElement("video1", "bg-video active");
383
+ video2 = window.KimiVideoManager.createVideoElement("video2", "bg-video");
384
+ videoContainer.appendChild(video1);
385
+ videoContainer.appendChild(video2);
386
+ }
387
+ }
388
+ let activeVideo = video1;
389
+ let inactiveVideo = video2;
390
+ kimiVideo = new window.KimiVideoManager(video1, video2);
391
+ await kimiVideo.init(kimiDB);
392
+ window.kimiVideo = kimiVideo;
393
+
394
+ // Initialize unified video controller
395
+ window.kimiVideoController = initializeVideoController(kimiVideo, window.kimiEmotionSystem, kimiDB);
396
+
397
+ if (video1 && video2 && kimiDB && kimiDB.getSelectedCharacter) {
398
+ try {
399
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
400
+ if (selectedCharacter && window.KIMI_CHARACTERS) {
401
+ kimiVideo.setCharacter(selectedCharacter);
402
+ const folder = window.KIMI_CHARACTERS[selectedCharacter].videoFolder;
403
+ const neutralVideo = `${folder}neutral/neutral-gentle-breathing.mp4`;
404
+ const video1Source = video1.querySelector("source");
405
+ if (video1Source) {
406
+ video1Source.setAttribute("src", neutralVideo);
407
+ video1.load();
408
+ }
409
+ }
410
+ if (kimiVideo && kimiVideo.switchToContext) {
411
+ kimiVideo.switchToContext("neutral");
412
+ }
413
+ } catch (e) {
414
+ console.warn("Error loading initial video:", e);
415
+ }
416
+ }
417
+
418
+ async function attachCharacterSection() {
419
+ let saveCharacterBtn = window.KimiDOMUtils.get("#save-character-btn");
420
+ if (saveCharacterBtn) {
421
+ saveCharacterBtn.addEventListener("click", async e => {
422
+ const settingsPanel = window.KimiDOMUtils.get(".settings-panel");
423
+ let scrollTop = settingsPanel ? settingsPanel.scrollTop : null;
424
+ const characterGrid = window.KimiDOMUtils.get("#character-grid");
425
+ const selectedCard = characterGrid ? characterGrid.querySelector(".character-card.selected") : null;
426
+ if (!selectedCard) return;
427
+ const charKey = selectedCard.dataset.character;
428
+ // Character save should not toggle the API key saved indicator.
429
+ const promptInput = window.KimiDOMUtils.get(`#prompt-${charKey}`);
430
+ const prompt = promptInput ? promptInput.value : "";
431
+
432
+ await window.kimiDB.setSelectedCharacter(charKey);
433
+ await window.kimiDB.setSystemPromptForCharacter(charKey, prompt);
434
+ // Ensure memory system uses the correct character
435
+ if (window.kimiMemorySystem) {
436
+ window.kimiMemorySystem.selectedCharacter = charKey;
437
+ }
438
+ if (window.kimiVideo && window.kimiVideo.setCharacter) {
439
+ window.kimiVideo.setCharacter(charKey);
440
+ if (window.kimiVideo.switchToContext) {
441
+ window.kimiVideo.switchToContext("neutral");
442
+ }
443
+ }
444
+ if (window.voiceManager && window.voiceManager.updateSelectedCharacter) {
445
+ await window.voiceManager.updateSelectedCharacter();
446
+ }
447
+
448
+ await window.loadCharacterSection();
449
+ if (settingsPanel && scrollTop !== null) {
450
+ requestAnimationFrame(() => {
451
+ settingsPanel.scrollTop = scrollTop;
452
+ });
453
+ }
454
+ // Refresh memory tab after character selection
455
+ if (window.kimiMemoryUI && typeof window.kimiMemoryUI.updateMemoryStats === "function") {
456
+ await window.kimiMemoryUI.updateMemoryStats();
457
+ }
458
+ if (window.setI18n) window.setI18n(saveCharacterBtn, "saved");
459
+ saveCharacterBtn.classList.add("success");
460
+ saveCharacterBtn.disabled = true;
461
+
462
+ setTimeout(() => {
463
+ if (window.setI18n) window.setI18n(saveCharacterBtn, "save");
464
+ saveCharacterBtn.classList.remove("success");
465
+ saveCharacterBtn.disabled = false;
466
+ }, 1000);
467
+
468
+ // Force full UI refresh to ensure all character-specific modules reinitialize.
469
+ // Full page refresh to reinitialize all character-dependent modules.
470
+ setTimeout(() => {
471
+ try {
472
+ window.location.reload();
473
+ } catch (e) {
474
+ console.warn("Page reload failed", e);
475
+ }
476
+ }, 1200); // slightly after button reset to allow visual feedback
477
+ });
478
+ }
479
+ let settingsButton2 = window.KimiDOMUtils.get("#settings-button");
480
+ if (settingsButton2) {
481
+ settingsButton2.addEventListener("click", window.loadCharacterSection);
482
+ }
483
+ }
484
+ await attachCharacterSection();
485
+
486
+ const chatContainer = document.getElementById("chat-container");
487
+ const chatButton = document.getElementById("chat-button");
488
+ const chatToggle = document.getElementById("chat-toggle");
489
+ const chatMessages = document.getElementById("chat-messages");
490
+ const chatInput = document.getElementById("chat-input");
491
+ const sendButton = document.getElementById("send-button");
492
+ const chatDelete = document.getElementById("chat-delete");
493
+ const waitingIndicator = document.getElementById("waiting-indicator");
494
+
495
+ if (!chatContainer || !chatButton || !chatMessages) {
496
+ console.error("Critical chat elements missing from DOM");
497
+ return;
498
+ }
499
+
500
+ window.kimiOverlayManager = new window.KimiOverlayManager();
501
+
502
+ chatButton.addEventListener("click", () => {
503
+ window.kimiOverlayManager.toggle("chat-container");
504
+ if (window.kimiOverlayManager.isOpen("chat-container")) {
505
+ window.loadChatHistory();
506
+ }
507
+ });
508
+
509
+ if (chatToggle) {
510
+ chatToggle.addEventListener("click", () => {
511
+ window.kimiOverlayManager.close("chat-container");
512
+ });
513
+ }
514
+
515
+ // Setup chat input and send button event listeners
516
+ if (sendButton) {
517
+ sendButton.addEventListener("click", () => {
518
+ if (typeof window.sendMessage === "function") {
519
+ window.sendMessage();
520
+ } else {
521
+ console.error("sendMessage function not available");
522
+ }
523
+ });
524
+ console.log("✅ Send button event listener attached");
525
+ } else {
526
+ console.error("Send button not found");
527
+ }
528
+
529
+ if (chatInput) {
530
+ chatInput.addEventListener("keydown", e => {
531
+ if (e.key === "Enter" && !e.shiftKey) {
532
+ e.preventDefault();
533
+ if (typeof window.sendMessage === "function") {
534
+ window.sendMessage();
535
+ } else {
536
+ console.error("sendMessage function not available");
537
+ }
538
+ }
539
+ });
540
+ (function (el) {
541
+ if (!el) return;
542
+ const pad = (p => (p ? parseFloat(p) : 0))(getComputedStyle(el).paddingTop) + (p => (p ? parseFloat(p) : 0))(getComputedStyle(el).paddingBottom);
543
+ const lh = parseFloat(getComputedStyle(el).lineHeight) || 18,
544
+ max = lh * 4 + pad;
545
+ const a = () => {
546
+ el.style.height = "auto";
547
+ el.style.height = Math.min(el.scrollHeight, max) + "px";
548
+ };
549
+ el.addEventListener("input", a);
550
+ el.addEventListener("focus", a);
551
+ setTimeout(a, 0);
552
+ })(chatInput);
553
+ console.log("✅ Chat input event listener attached");
554
+ } else {
555
+ console.error("Chat input not found");
556
+ }
557
+
558
+ const settingsOverlay = document.getElementById("settings-overlay");
559
+ const settingsButton = document.getElementById("settings-button");
560
+ const settingsClose = document.getElementById("settings-close");
561
+
562
+ const helpOverlay = document.getElementById("help-overlay");
563
+ const helpButton = document.getElementById("help-button");
564
+ const helpClose = document.getElementById("help-close");
565
+ const globalHelpButton = document.getElementById("global-help-button");
566
+
567
+ if (!settingsButton || !helpButton) {
568
+ console.error("Critical UI buttons missing from DOM");
569
+ return;
570
+ }
571
+
572
+ const openHelp = () => window.kimiOverlayManager.open("help-overlay");
573
+
574
+ helpButton.addEventListener("click", openHelp);
575
+ if (globalHelpButton) {
576
+ globalHelpButton.addEventListener("click", openHelp);
577
+ }
578
+
579
+ if (helpClose) {
580
+ helpClose.addEventListener("click", () => {
581
+ window.kimiOverlayManager.close("help-overlay");
582
+ });
583
+ }
584
+
585
+ settingsButton.addEventListener("click", () => {
586
+ window.kimiOverlayManager.open("settings-overlay");
587
+
588
+ // Prevent multiple settings loading
589
+ if (!window._settingsLoading) {
590
+ window._settingsLoading = true;
591
+ window.loadSettingsData();
592
+
593
+ setTimeout(() => {
594
+ window.updateTabsScrollIndicator();
595
+ if (window.initializeAllSliders) window.initializeAllSliders();
596
+ if (window.syncLLMMaxTokensSlider) window.syncLLMMaxTokensSlider();
597
+ if (window.syncLLMTemperatureSlider) window.syncLLMTemperatureSlider();
598
+ if (window.setupSettingsListeners) window.setupSettingsListeners(window.kimiDB, window.kimiMemory);
599
+ if (window.syncPersonalityTraits) window.syncPersonalityTraits();
600
+ if (window.ensureVideoContextConsistency) window.ensureVideoContextConsistency();
601
+
602
+ // Only retry loading models if not already done
603
+ if (window.loadAvailableModels && !loadAvailableModels._loading) {
604
+ setTimeout(() => window.loadAvailableModels(), 100);
605
+ }
606
+
607
+ window._settingsLoading = false;
608
+ }, 200);
609
+ }
610
+ });
611
+
612
+ if (settingsClose) {
613
+ settingsClose.addEventListener("click", () => {
614
+ window.kimiOverlayManager.close("settings-overlay");
615
+ });
616
+ }
617
+
618
+ // Initialisation unifiée de la gestion des tabs
619
+ window.kimiTabManager = new window.KimiTabManager({
620
+ onTabChange: async tabName => {
621
+ if (tabName === "personality") {
622
+ await window.loadCharacterSection();
623
+ }
624
+ }
625
+ });
626
+
627
+ window.kimiUIEventManager = new window.KimiUIEventManager();
628
+ window.kimiUIEventManager.addEvent(window, "resize", window.updateTabsScrollIndicator);
629
+
630
+ window.kimiFormManager = new window.KimiFormManager({ db: window.kimiDB, memory: window.kimiMemory });
631
+
632
+ const testVoiceButton = document.getElementById("test-voice");
633
+ if (testVoiceButton) {
634
+ testVoiceButton.addEventListener("click", () => {
635
+ if (voiceManager) {
636
+ const rate = parseFloat(document.getElementById("voice-rate").value);
637
+ const pitch = parseFloat(document.getElementById("voice-pitch").value);
638
+ const volume = parseFloat(document.getElementById("voice-volume").value);
639
+
640
+ if (window.kimiMemory.preferences) {
641
+ window.kimiMemory.preferences.voiceRate = rate;
642
+ window.kimiMemory.preferences.voicePitch = pitch;
643
+ window.kimiMemory.preferences.voiceVolume = volume;
644
+ }
645
+
646
+ const testMessage =
647
+ window.kimiI18nManager?.t("voice_test_message") || "Hello my love! Here is my new voice configured with all the settings! Do you like it?";
648
+ voiceManager.speak(testMessage, {
649
+ rate,
650
+ pitch,
651
+ volume
652
+ });
653
+ } else {
654
+ console.warn("Voice manager not initialized");
655
+ }
656
+ });
657
+ }
658
+
659
+ const testApiButton = document.getElementById("test-api");
660
+ if (testApiButton) {
661
+ testApiButton.addEventListener("click", async () => {
662
+ const statusSpan = ApiUi.statusSpan();
663
+ const apiKeyInput = ApiUi.apiKeyInput();
664
+ const apiKey = apiKeyInput ? apiKeyInput.value.trim() : "";
665
+ const providerSelect = ApiUi.providerSelect();
666
+ const baseUrlInput = ApiUi.baseUrlInput();
667
+ const modelIdInput = ApiUi.modelIdInput();
668
+ const provider = providerSelect ? providerSelect.value : "openrouter";
669
+ const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : "";
670
+ const modelId = modelIdInput ? modelIdInput.value.trim() : "";
671
+
672
+ if (!statusSpan) return;
673
+
674
+ if (provider !== "ollama" && !apiKey) {
675
+ statusSpan.textContent = window.kimiI18nManager?.t("api_key_missing") || "API key missing";
676
+ statusSpan.style.color = "#ff6b6b";
677
+ return;
678
+ }
679
+
680
+ // Validate API key format before saving/testing
681
+ if (provider !== "ollama") {
682
+ const isValid = (window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(apiKey)) || false;
683
+ if (!isValid) {
684
+ statusSpan.textContent = window.kimiI18nManager?.t("api_key_invalid_format") || "Invalid API key format (must start with sk-or-v1-)";
685
+ statusSpan.style.color = "#ff6b6b";
686
+ return;
687
+ }
688
+ }
689
+
690
+ if (window.kimiDB) {
691
+ // Save API key under provider-specific preference key (skip for Ollama)
692
+ if (provider !== "ollama") {
693
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "providerApiKey";
694
+ await window.kimiDB.setPreference(keyPref, apiKey);
695
+ }
696
+ await window.kimiDB.setPreference("llmProvider", provider);
697
+ if (baseUrl) {
698
+ // Save under provider-specific key to avoid cross-provider contamination
699
+ const key = `llmBaseUrl_${provider}`;
700
+ await window.kimiDB.setPreference(key, baseUrl);
701
+ }
702
+ if (modelId) await window.kimiDB.setPreference("llmModelId", modelId);
703
+ }
704
+ statusSpan.textContent = "Testing in progress...";
705
+ statusSpan.style.color = "#ffa726";
706
+
707
+ try {
708
+ if (window.kimiLLM) {
709
+ // Test API minimal et centralisé pour tous les providers
710
+ const result = await window.kimiLLM.testApiKeyMinimal(modelId);
711
+ if (result.success) {
712
+ statusSpan.textContent = "Connection successful!";
713
+ statusSpan.style.color = "#4caf50";
714
+ // Only show saved badge if an actual non-empty API key is stored and provider requires one
715
+ const savedBadge = ApiUi.savedBadge();
716
+ if (savedBadge) {
717
+ const apiKeyInputEl = ApiUi.apiKeyInput();
718
+ const hasKey = apiKeyInputEl && apiKeyInputEl.value.trim().length > 0;
719
+ if (provider !== "ollama" && hasKey) {
720
+ savedBadge.textContent = (window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
721
+ savedBadge.style.display = "inline";
722
+ } else {
723
+ savedBadge.style.display = "none";
724
+ }
725
+ }
726
+
727
+ if (result.response) {
728
+ setTimeout(() => {
729
+ statusSpan.textContent = `Test response: \"${result.response.substring(0, 50)}...\"`;
730
+ }, 1000);
731
+ }
732
+ // Mark test success explicitly
733
+ ApiUi.setTestPresence("#4caf50");
734
+ } else {
735
+ statusSpan.textContent = `${result.error}`;
736
+ statusSpan.style.color = "#ff6b6b";
737
+ ApiUi.setTestPresence("#9e9e9e");
738
+ if (result.error.includes("similaires disponibles")) {
739
+ setTimeout(() => {}, 1000);
740
+ }
741
+ }
742
+ } else {
743
+ statusSpan.textContent = "LLM manager not initialized";
744
+ statusSpan.style.color = "#ff6b6b";
745
+ ApiUi.setTestPresence("#9e9e9e");
746
+ }
747
+ } catch (error) {
748
+ console.error("Error while testing API:", error);
749
+ statusSpan.textContent = `Error: ${error.message}`;
750
+ statusSpan.style.color = "#ff6b6b";
751
+ ApiUi.setTestPresence("#9e9e9e");
752
+
753
+ if (error.message.includes("non disponible")) {
754
+ setTimeout(() => {}, 1000);
755
+ }
756
+ }
757
+ });
758
+ }
759
+
760
+ // Global, single handler for API key input to save and update presence in real-time
761
+ (function setupApiKeyInputHandler() {
762
+ const input = ApiUi.apiKeyInput();
763
+ if (!input) return;
764
+ let t;
765
+ input.addEventListener("input", () => {
766
+ clearTimeout(t);
767
+ t = setTimeout(async () => {
768
+ const providerEl = ApiUi.providerSelect();
769
+ const provider = providerEl ? providerEl.value : "openrouter";
770
+ const keyPref = window.KimiProviderUtils ? window.KimiProviderUtils.getKeyPrefForProvider(provider) : "providerApiKey";
771
+ const value = input.value.trim();
772
+ // Update Test button state immediately
773
+ const validNow = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(value));
774
+ ApiUi.setTestEnabled(provider === "ollama" ? true : validNow);
775
+ if (window.kimiDB) {
776
+ try {
777
+ await window.kimiDB.setPreference(keyPref, value);
778
+ const savedBadge = ApiUi.savedBadge();
779
+ if (savedBadge) {
780
+ if (value) {
781
+ savedBadge.textContent = (window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
782
+ savedBadge.style.display = "inline";
783
+ } else {
784
+ savedBadge.style.display = "none";
785
+ }
786
+ }
787
+ ApiUi.setPresence(value ? "#4caf50" : "#9e9e9e");
788
+ // Any key change invalidates previous test state
789
+ ApiUi.setTestPresence("#9e9e9e");
790
+ ApiUi.clearStatus();
791
+ } catch (e) {
792
+ // Validation error from DB
793
+ const s = ApiUi.statusSpan();
794
+ if (s) {
795
+ s.textContent = e?.message || "Invalid API key";
796
+ s.style.color = "#ff6b6b";
797
+ }
798
+ ApiUi.setTestEnabled(false);
799
+ ApiUi.setTestPresence("#9e9e9e");
800
+ }
801
+ }
802
+ }, window.KIMI_SECURITY_CONFIG?.DEBOUNCE_DELAY || 300);
803
+ });
804
+ })();
805
+
806
+ // Toggle show/hide for API key
807
+ (function setupToggleEye() {
808
+ const btn = ApiUi.toggleBtn();
809
+ const input = ApiUi.apiKeyInput();
810
+ if (!btn || !input) return;
811
+ btn.addEventListener("click", () => {
812
+ const showing = input.type === "text";
813
+ input.type = showing ? "password" : "text";
814
+ btn.setAttribute("aria-pressed", String(!showing));
815
+ const icon = btn.querySelector("i");
816
+ if (icon) {
817
+ icon.classList.toggle("fa-eye");
818
+ icon.classList.toggle("fa-eye-slash");
819
+ }
820
+ btn.setAttribute("aria-label", showing ? "Show API key" : "Hide API key");
821
+ });
822
+ })();
823
+
824
+ kimiInit.register(
825
+ "appearanceManager",
826
+ async () => {
827
+ const manager = new KimiAppearanceManager(window.kimiDB);
828
+ await manager.init();
829
+ window.kimiAppearanceManager = manager;
830
+ return manager;
831
+ },
832
+ [],
833
+ 500
834
+ );
835
+ kimiInit.register(
836
+ "dataManager",
837
+ async () => {
838
+ const manager = new KimiDataManager(window.kimiDB);
839
+ await manager.init();
840
+ window.kimiDataManager = manager;
841
+ return manager;
842
+ },
843
+ [],
844
+ 600
845
+ );
846
+
847
+ kimiInit.register(
848
+ "voiceManager",
849
+ async () => {
850
+ if (window.KimiVoiceManager) {
851
+ const manager = new KimiVoiceManager(window.kimiDB, window.kimiMemory);
852
+ const success = await manager.init();
853
+ if (success) {
854
+ manager.setOnSpeechAnalysis(window.analyzeAndReact);
855
+ return manager;
856
+ }
857
+ }
858
+ return null;
859
+ },
860
+ [],
861
+ 1000
862
+ );
863
+
864
+ try {
865
+ await kimiInit.initializeAll();
866
+ window.voiceManager = kimiInit.getInstance("voiceManager");
867
+ window.kimiMemory.updateFavorabilityBar();
868
+ } catch (error) {
869
+ console.error("Initialization error:", error);
870
+ }
871
+
872
+ // Setup unified event handlers to prevent duplicates
873
+ setupUnifiedEventHandlers();
874
+
875
+ // Initialize language and UI
876
+ await initializeLanguageAndUI();
877
+
878
+ // Setup message handling
879
+ setupMessageHandling();
880
+
881
+ // Function definitions
882
+ function setupUnifiedEventHandlers() {
883
+ // SIMPLE FIX: Initialize _kimiEventCleanup to prevent undefined error
884
+ if (!window._kimiEventCleanup) {
885
+ window._kimiEventCleanup = [];
886
+ }
887
+
888
+ // Helper function to safely add event listeners
889
+ function safeAddEventListener(element, event, handler, identifier) {
890
+ if (element && !element[identifier]) {
891
+ element.addEventListener(event, handler);
892
+ element[identifier] = true;
893
+
894
+ // Simple cleanup system
895
+ const cleanupFn = () => {
896
+ element.removeEventListener(event, handler);
897
+ element[identifier] = false;
898
+ };
899
+
900
+ // Store cleanup function in the simple array
901
+ window._kimiEventCleanup.push(cleanupFn);
902
+ }
903
+ }
904
+
905
+ // Chat event handlers
906
+ const chatDelete = document.getElementById("chat-delete");
907
+ if (chatDelete) {
908
+ const handler = async () => {
909
+ if (confirm("Do you really want to delete all chat messages? This cannot be undone.")) {
910
+ const chatMessages = document.getElementById("chat-messages");
911
+ if (chatMessages) {
912
+ chatMessages.textContent = "";
913
+ }
914
+ if (window.kimiDB && window.kimiDB.db) {
915
+ try {
916
+ await window.kimiDB.db.conversations.clear();
917
+ } catch (error) {
918
+ console.error("Error deleting conversations:", error);
919
+ }
920
+ }
921
+ }
922
+ };
923
+ safeAddEventListener(chatDelete, "click", handler, "_kimiChatDeleteHandlerAttached");
924
+ }
925
+ }
926
+
927
+ async function initializeLanguageAndUI() {
928
+ // Language initialization
929
+ window.kimiI18nManager = new window.KimiI18nManager();
930
+ const lang = await kimiDB.getPreference("selectedLanguage", "en");
931
+ await window.kimiI18nManager.setLanguage(lang);
932
+ // Note: Language selector event listener is now handled by VoiceManager.setupLanguageSelector()
933
+ // This prevents duplicate event listeners and ensures proper coordination between voice and i18n systems
934
+
935
+ window.kimiUIStateManager = new window.KimiUIStateManager();
936
+ }
937
+
938
+ function setupMessageHandling() {
939
+ // Chat event handlers are already attached in the main script
940
+ // No need to reattach them here to avoid duplicates
941
+ }
942
+
943
+ // ==== BATCHED EVENT AGGREGATOR (personality + preferences) ====
944
+ const batchedUpdates = {
945
+ personality: null,
946
+ preferences: new Set()
947
+ };
948
+ let batchTimer = null;
949
+
950
+ function scheduleFlush() {
951
+ if (batchTimer) return;
952
+ batchTimer = setTimeout(flushBatchedUpdates, 100); // 100ms coalescing window
953
+ }
954
+
955
+ async function flushBatchedUpdates() {
956
+ const personalityPayload = batchedUpdates.personality;
957
+ const prefKeys = Array.from(batchedUpdates.preferences);
958
+ batchedUpdates.personality = null;
959
+ batchedUpdates.preferences.clear();
960
+ batchTimer = null;
961
+
962
+ // Apply personality update once (last-wins)
963
+ if (personalityPayload) {
964
+ const { character, traits } = personalityPayload;
965
+ const defaults = (window.getTraitDefaults && window.getTraitDefaults()) || {
966
+ affection: 55, // Baseline affection default
967
+ romance: 50,
968
+ empathy: 75,
969
+ playfulness: 55,
970
+ humor: 60,
971
+ intelligence: 70
972
+ };
973
+
974
+ // Prefer persisted DB traits over defaults to avoid temporary inconsistencies.
975
+ let dbTraits = null;
976
+ try {
977
+ if (window.kimiDB && typeof window.kimiDB.getAllPersonalityTraits === "function") {
978
+ dbTraits = window.getCharacterTraits
979
+ ? await window.getCharacterTraits(character || null)
980
+ : await window.kimiDB.getAllPersonalityTraits(character || null);
981
+ }
982
+ } catch (e) {
983
+ dbTraits = null;
984
+ }
985
+
986
+ const baseline = { ...defaults, ...(dbTraits || {}) };
987
+ const safeTraits = {};
988
+ for (const key of Object.keys(defaults)) {
989
+ // If incoming payload provides the key, use it; otherwise use baseline (DB -> defaults)
990
+ let raw = Object.prototype.hasOwnProperty.call(traits || {}, key) ? traits[key] : baseline[key];
991
+ let v = Number(raw);
992
+ if (!isFinite(v) || isNaN(v)) v = Number(baseline[key]);
993
+ v = Math.max(0, Math.min(100, v));
994
+ safeTraits[key] = v;
995
+ }
996
+ if (window.KIMI_DEBUG_SYNC) {
997
+ window.KIMI_CONFIG?.debugLog("SYNC", `Personality updated for ${character}:`, safeTraits);
998
+ }
999
+ // Centralize side-effects elsewhere; aggregator remains a coalesced logger only.
1000
+ }
1001
+
1002
+ // Preference keys batch (currently UI refresh for sliders already handled elsewhere)
1003
+ if (prefKeys.length > 0) {
1004
+ // Potential future hook: log or perform aggregated operations
1005
+ // console.log("⚙️ Batched preference keys:", prefKeys);
1006
+ }
1007
+ }
1008
+
1009
+ // Also listen to the DB-wrapped event name to preserve batched logging
1010
+ window.addEventListener("personality:updated", event => {
1011
+ batchedUpdates.personality = event.detail; // last event wins
1012
+ scheduleFlush();
1013
+ });
1014
+ window.addEventListener("preferenceUpdated", event => {
1015
+ if (event.detail?.key) batchedUpdates.preferences.add(event.detail.key);
1016
+ scheduleFlush();
1017
+ });
1018
+
1019
+ // Add global keyboard event listener for microphone toggle (F8)
1020
+ let f8KeyPressed = false;
1021
+
1022
+ document.addEventListener("keydown", function (event) {
1023
+ // Check if F8 key is pressed and no input field is focused
1024
+ if (event.key === "F8" && !f8KeyPressed) {
1025
+ f8KeyPressed = true;
1026
+ const activeElement = document.activeElement;
1027
+ const isInputFocused =
1028
+ activeElement && (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable);
1029
+
1030
+ // Only trigger if no input field is focused
1031
+ if (!isInputFocused && window.voiceManager && window.voiceManager.toggleMicrophone) {
1032
+ event.preventDefault();
1033
+ window.voiceManager.toggleMicrophone();
1034
+ }
1035
+ }
1036
+ });
1037
+
1038
+ // Refresh sliders when character or language preference changes
1039
+ window.addEventListener("preferenceUpdated", evt => {
1040
+ const k = evt.detail?.key;
1041
+ if (!k) return;
1042
+ if (k === "selectedCharacter" || k === "selectedLanguage") {
1043
+ if (window.refreshAllSliders) {
1044
+ setTimeout(() => window.refreshAllSliders(), 50);
1045
+ }
1046
+ }
1047
+ });
1048
+
1049
+ document.addEventListener("keyup", function (event) {
1050
+ if (event.key === "F8") {
1051
+ f8KeyPressed = false;
1052
+ }
1053
+ });
1054
+
1055
+ // Monitor for consistency and errors
1056
+ setInterval(async () => {
1057
+ if (window.ensureVideoContextConsistency) {
1058
+ await window.ensureVideoContextConsistency();
1059
+ }
1060
+ }, 30000); // Check every 30 seconds
1061
+
1062
+ // Personality sync: global event and wrappers
1063
+ (function setupPersonalitySync() {
1064
+ // Guard to avoid multiple initializations
1065
+ if (window._kimiPersonalitySyncReady) return;
1066
+ window._kimiPersonalitySyncReady = true;
1067
+
1068
+ const dispatchUpdated = async (partialTraits, characterHint = null) => {
1069
+ try {
1070
+ const character = characterHint || (window.kimiDB && (await window.kimiDB.getSelectedCharacter())) || null;
1071
+ window.dispatchEvent(
1072
+ new CustomEvent("personality:updated", {
1073
+ detail: { character, traits: { ...partialTraits } }
1074
+ })
1075
+ );
1076
+ } catch (e) {}
1077
+ };
1078
+
1079
+ const tryWrapDB = () => {
1080
+ const db = window.kimiDB;
1081
+ if (!db) return false;
1082
+
1083
+ const wrapOnce = (obj, methodName, buildTraitsFromArgs) => {
1084
+ if (!obj || typeof obj[methodName] !== "function") return;
1085
+ if (obj[methodName]._kimiWrapped) return;
1086
+ const original = obj[methodName].bind(obj);
1087
+ obj[methodName] = async function (...args) {
1088
+ const res = await original(...args);
1089
+ try {
1090
+ const { traits, character } = await buildTraitsFromArgs(args, res);
1091
+ if (traits && Object.keys(traits).length > 0) {
1092
+ await dispatchUpdated(traits, character);
1093
+ }
1094
+ } catch (e) {}
1095
+ return res;
1096
+ };
1097
+ obj[methodName]._kimiWrapped = true;
1098
+ };
1099
+
1100
+ // setPersonalityTrait(trait, value, character?)
1101
+ wrapOnce(db, "setPersonalityTrait", async args => {
1102
+ const [trait, value, character] = args;
1103
+ return { traits: { [String(trait)]: Number(value) }, character: character || null };
1104
+ });
1105
+
1106
+ // setPersonalityBatch(traitsObj, character?)
1107
+ wrapOnce(db, "setPersonalityBatch", async args => {
1108
+ const [traitsObj, character] = args;
1109
+ const traits = {};
1110
+ if (traitsObj && typeof traitsObj === "object") {
1111
+ for (const [k, v] of Object.entries(traitsObj)) {
1112
+ traits[String(k)] = Number(v);
1113
+ }
1114
+ }
1115
+ return { traits, character: character || null };
1116
+ });
1117
+
1118
+ // savePersonality(personalityObj, character?)
1119
+ wrapOnce(db, "savePersonality", async args => {
1120
+ const [personalityObj, character] = args;
1121
+ const traits = {};
1122
+ if (personalityObj && typeof personalityObj === "object") {
1123
+ for (const [k, v] of Object.entries(personalityObj)) {
1124
+ traits[String(k)] = Number(v);
1125
+ }
1126
+ }
1127
+ return { traits, character: character || null };
1128
+ });
1129
+
1130
+ return true;
1131
+ };
1132
+
1133
+ // Try immediately and then retry a few times if DB not yet ready
1134
+ if (!tryWrapDB()) {
1135
+ let attempts = 0;
1136
+ const maxAttempts = 20;
1137
+ const interval = setInterval(() => {
1138
+ attempts++;
1139
+ if (tryWrapDB() || attempts >= maxAttempts) {
1140
+ clearInterval(interval);
1141
+ }
1142
+ }, 250);
1143
+ }
1144
+
1145
+ // Central listener: debounce UI/video sync to avoid thrashing
1146
+ let syncTimer = null;
1147
+ let lastTraits = {};
1148
+ window.addEventListener("personality:updated", async e => {
1149
+ try {
1150
+ if (e && e.detail && e.detail.traits) {
1151
+ // Merge incremental updates
1152
+ lastTraits = { ...lastTraits, ...e.detail.traits };
1153
+ }
1154
+ } catch {}
1155
+
1156
+ if (syncTimer) clearTimeout(syncTimer);
1157
+ syncTimer = setTimeout(async () => {
1158
+ try {
1159
+ const db = window.kimiDB;
1160
+ const character = (e && e.detail && e.detail.character) || (db && (await db.getSelectedCharacter())) || null;
1161
+ let traits = lastTraits;
1162
+ if (!traits || Object.keys(traits).length === 0) {
1163
+ // Fallback: fetch all traits if partial not provided
1164
+ traits = db && (window.getCharacterTraits ? await window.getCharacterTraits(character) : await db.getAllPersonalityTraits(character));
1165
+ }
1166
+
1167
+ // 1) Update UI sliders if available
1168
+ if (typeof window.updateSlider === "function" && traits) {
1169
+ for (const [trait, value] of Object.entries(traits)) {
1170
+ const id = `trait-${trait}`;
1171
+ if (document.getElementById(id)) {
1172
+ try {
1173
+ window.updateSlider(id, value);
1174
+ } catch {}
1175
+ }
1176
+ }
1177
+ }
1178
+ if (typeof window.syncPersonalityTraits === "function") {
1179
+ try {
1180
+ await window.syncPersonalityTraits(character);
1181
+ } catch {}
1182
+ }
1183
+
1184
+ // 2) Update memory cache affection bar if available
1185
+ if (window.kimiMemory && typeof window.kimiMemory.updateAffectionTrait === "function") {
1186
+ try {
1187
+ await window.kimiMemory.updateAffectionTrait();
1188
+ } catch {}
1189
+ }
1190
+
1191
+ // 3) Update video mood by personality
1192
+ if (window.kimiVideo && typeof window.kimiVideo.setMoodByPersonality === "function") {
1193
+ const allTraits =
1194
+ traits && Object.keys(traits).length > 0
1195
+ ? { ...traits }
1196
+ : (db &&
1197
+ (window.getCharacterTraits ? await window.getCharacterTraits(character) : await db.getAllPersonalityTraits(character))) ||
1198
+ {};
1199
+ try {
1200
+ window.kimiVideo.setMoodByPersonality(allTraits);
1201
+ } catch {}
1202
+ // 3b) Update voice modulation based on personality
1203
+ try {
1204
+ if (window.voiceManager && typeof window.voiceManager.updatePersonalityModulation === "function") {
1205
+ window.voiceManager.updatePersonalityModulation(allTraits);
1206
+ }
1207
+ } catch {}
1208
+ }
1209
+
1210
+ // 4) Ensure current video context is valid (lightweight guard)
1211
+ let beforeInfo = null;
1212
+ try {
1213
+ if (window.kimiVideo && typeof window.kimiVideo.getCurrentVideoInfo === "function") {
1214
+ beforeInfo = window.kimiVideo.getCurrentVideoInfo();
1215
+ }
1216
+ } catch {}
1217
+
1218
+ if (typeof window.ensureVideoContextConsistency === "function") {
1219
+ try {
1220
+ await window.ensureVideoContextConsistency();
1221
+ } catch {}
1222
+ }
1223
+
1224
+ try {
1225
+ if (window.KIMI_DEBUG_SYNC && window.kimiVideo && typeof window.kimiVideo.getCurrentVideoInfo === "function") {
1226
+ const afterInfo = window.kimiVideo.getCurrentVideoInfo();
1227
+ if (
1228
+ beforeInfo &&
1229
+ afterInfo &&
1230
+ (beforeInfo.context !== afterInfo.context ||
1231
+ beforeInfo.emotion !== afterInfo.emotion ||
1232
+ beforeInfo.category !== afterInfo.category)
1233
+ ) {
1234
+ console.log("🔧 SyncGuard: corrected video context", { from: beforeInfo, to: afterInfo });
1235
+ }
1236
+ }
1237
+ } catch {}
1238
+ } catch {
1239
+ } finally {
1240
+ lastTraits = {};
1241
+ if (window.updateGlobalPersonalityUI) {
1242
+ try {
1243
+ await window.updateGlobalPersonalityUI();
1244
+ } catch {}
1245
+ }
1246
+ }
1247
+ }, 120); // small debounce
1248
+ });
1249
+ })();
1250
+ });
kimi-js/kimi-security.js ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI SECURITY & VALIDATION CONFIGURATION =====
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
+
9
+ // Security settings
10
+ API_KEY_PATTERNS: [
11
+ /^sk-or-v1-[a-zA-Z0-9]{16,}$/, // OpenRouter pattern (relaxed length)
12
+ /^sk-[a-zA-Z0-9_\-]{16,}$/, // OpenAI and similar (relaxed)
13
+ /^[a-zA-Z0-9_\-]{16,}$/ // Generic API key fallback
14
+ ],
15
+
16
+ // Cache settings
17
+ CACHE_MAX_AGE: 300000, // 5 minutes
18
+ CACHE_MAX_SIZE: 100,
19
+
20
+ // Performance settings
21
+ DEBOUNCE_DELAY: 300,
22
+ BATCH_DELAY: 800,
23
+ THROTTLE_LIMIT: 1000,
24
+
25
+ // Error messages
26
+ ERRORS: {
27
+ INVALID_INPUT: "Invalid input provided",
28
+ MESSAGE_TOO_LONG: "Message too long. Please keep it under {max} characters.",
29
+ INVALID_API_KEY: "Invalid API key format",
30
+ NETWORK_ERROR: "Network error. Please check your connection.",
31
+ SYSTEM_ERROR: "System error occurred. Please try again."
32
+ }
33
+ };
34
+
35
+ // Validation utilities using the configuration
36
+ window.KIMI_VALIDATORS = {
37
+ validateMessage: message => {
38
+ if (!message || typeof message !== "string") return { valid: false, error: "INVALID_INPUT" };
39
+ if (message.length > window.KIMI_SECURITY_CONFIG.MAX_MESSAGE_LENGTH) {
40
+ return {
41
+ valid: false,
42
+ error: "MESSAGE_TOO_LONG",
43
+ params: { max: window.KIMI_SECURITY_CONFIG.MAX_MESSAGE_LENGTH }
44
+ };
45
+ }
46
+ return { valid: true };
47
+ },
48
+
49
+ validateApiKey: key => {
50
+ if (!key || typeof key !== "string") return false;
51
+ if (key.length < window.KIMI_SECURITY_CONFIG.MIN_API_KEY_LENGTH) return false;
52
+ if (key.length > window.KIMI_SECURITY_CONFIG.MAX_API_KEY_LENGTH) return false;
53
+
54
+ return window.KIMI_SECURITY_CONFIG.API_KEY_PATTERNS.some(pattern => pattern.test(key));
55
+ },
56
+
57
+ validateSliderValue: (value, type) => {
58
+ // Use centralized config from KIMI_CONFIG
59
+ if (window.KIMI_CONFIG && window.KIMI_CONFIG.validate) {
60
+ return window.KIMI_CONFIG.validate(value, type);
61
+ }
62
+
63
+ // Fallback if config not available
64
+ const num = parseFloat(value);
65
+ if (isNaN(num)) return { valid: false, value: 0 };
66
+
67
+ return { valid: true, value: num };
68
+ }
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 ADDED
@@ -0,0 +1,1118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // CENTRALIZED KIMI UTILITIES
2
+ // Lightweight additional helpers (Phase 1 consolidation):
3
+ // 1. emitAppEvent(name, detail) -> standard CustomEvent emitter with safe try/catch.
4
+ // 2. getCharacterTraits(char?) -> single place to fetch all personality traits (reduces repetition).
5
+ // 3. setI18n(element, key, params?) -> uniform attribute assignment for i18n keys.
6
+ // These are intentionally minimal & backward-compatible.
7
+
8
+ if (!window.emitAppEvent) {
9
+ window.emitAppEvent = function emitAppEvent(name, detail = {}) {
10
+ if (!name) return false;
11
+ try {
12
+ window.dispatchEvent(new CustomEvent(name, { detail }));
13
+ return true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ };
18
+ }
19
+
20
+ if (!window.getCharacterTraits) {
21
+ window.getCharacterTraits = async function getCharacterTraits(character = null) {
22
+ try {
23
+ const db = window.kimiDB;
24
+ if (!db || typeof db.getAllPersonalityTraits !== "function") return {};
25
+ // Init cache structure
26
+ if (!window.__KIMI_TRAITS_CACHE__) {
27
+ const cfgTtl = (window.KIMI_TRAITS_CACHE_CONFIG && window.KIMI_TRAITS_CACHE_CONFIG.ttlMs) || window.KIMI_CONFIG?.traitsCacheTtlMs;
28
+ const ttl = typeof cfgTtl === "number" && cfgTtl > 0 ? cfgTtl : 120000;
29
+ window.__KIMI_TRAITS_CACHE__ = {
30
+ data: new Map(),
31
+ ts: new Map(),
32
+ ttl,
33
+ metrics: { hits: 0, misses: 0, invalidations: 0 }
34
+ }; // TTL configurable (default 2 min)
35
+ // Centralized trait keys constant
36
+ window.__KIMI_TRAIT_KEYS__ = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
37
+ }
38
+ const cache = window.__KIMI_TRAITS_CACHE__;
39
+ let char = character;
40
+ if (!char && typeof db.getSelectedCharacter === "function") {
41
+ char = await db.getSelectedCharacter();
42
+ }
43
+ if (!char) return {};
44
+ const now = Date.now();
45
+ const cached = cache.data.get(char);
46
+ const ts = cache.ts.get(char) || 0;
47
+ if (cached && now - ts < cache.ttl) {
48
+ cache.metrics.hits++;
49
+ return cached;
50
+ }
51
+ cache.metrics.misses++;
52
+ const traits = (await db.getAllPersonalityTraits(char)) || {};
53
+ cache.data.set(char, traits);
54
+ cache.ts.set(char, now);
55
+ return traits;
56
+ } catch {
57
+ return {};
58
+ }
59
+ };
60
+ }
61
+
62
+ if (!window.setI18n) {
63
+ window.setI18n = function setI18n(el, key, params = null) {
64
+ if (!el || !key) return;
65
+ try {
66
+ el.setAttribute("data-i18n", key);
67
+ if (params && typeof params === "object") {
68
+ el.setAttribute("data-i18n-params", JSON.stringify(params));
69
+ }
70
+ } catch {}
71
+ };
72
+ }
73
+
74
+ // Invalidate trait cache on relevant preference updates (e.g., personality changes)
75
+ if (window.addEventListener && !window.__KIMI_TRAITS_CACHE_LISTENER__) {
76
+ window.__KIMI_TRAITS_CACHE_LISTENER__ = true;
77
+ window.addEventListener("preferenceUpdated", ev => {
78
+ try {
79
+ if (!window.__KIMI_TRAITS_CACHE__) return;
80
+ const key = ev?.detail?.key;
81
+ const traitKeys = window.__KIMI_TRAIT_KEYS__ || ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
82
+ const shouldInvalidate = !key || traitKeys.includes(String(key).toLowerCase()) || /trait|personality/i.test(key);
83
+ if (shouldInvalidate) {
84
+ window.__KIMI_TRAITS_CACHE__.data.clear();
85
+ window.__KIMI_TRAITS_CACHE__.ts.clear();
86
+ if (window.__KIMI_TRAITS_CACHE__.metrics) window.__KIMI_TRAITS_CACHE__.metrics.invalidations++;
87
+ }
88
+ } catch {}
89
+ });
90
+ }
91
+
92
+ // Debug helper
93
+ if (!window.kimiTraitCacheInfo) {
94
+ window.kimiTraitCacheInfo = function () {
95
+ const c = window.__KIMI_TRAITS_CACHE__;
96
+ if (!c) return { enabled: false };
97
+ return {
98
+ enabled: true,
99
+ entries: c.data.size,
100
+ ttlMs: c.ttl,
101
+ keys: [...c.data.keys()],
102
+ metrics: c.metrics ? { ...c.metrics } : null,
103
+ traitKeys: window.__KIMI_TRAIT_KEYS__ || []
104
+ };
105
+ };
106
+ }
107
+
108
+ // Simple runtime micro-test for cache invalidation
109
+ if (!window.testTraitCacheInvalidation) {
110
+ window.testTraitCacheInvalidation = async function () {
111
+ const report = { initialFetch: null, secondFetchCached: null, afterInvalidation: null, passed: false };
112
+ try {
113
+ const traits1 = await window.getCharacterTraits();
114
+ report.initialFetch = { keys: Object.keys(traits1).length };
115
+ const before = performance.now();
116
+ const traits2 = await window.getCharacterTraits();
117
+ const dur2 = performance.now() - before;
118
+ report.secondFetchCached = { ms: Math.round(dur2), keys: Object.keys(traits2).length };
119
+ // Emit fake preferenceUpdated affecting traits
120
+ window.emitAppEvent && window.emitAppEvent("preferenceUpdated", { key: "affection" });
121
+ const traits3 = await window.getCharacterTraits();
122
+ report.afterInvalidation = { keys: Object.keys(traits3).length };
123
+ report.passed = report.initialFetch.keys === report.afterInvalidation.keys;
124
+ } catch (e) {
125
+ report.error = String(e);
126
+ }
127
+ return report;
128
+ };
129
+ }
130
+
131
+ // Input validation and sanitization utilities
132
+ window.KimiValidationUtils = {
133
+ validateMessage(message) {
134
+ if (!message || typeof message !== "string") {
135
+ return { valid: false, error: "Message must be a non-empty string" };
136
+ }
137
+ const trimmed = message.trim();
138
+ if (!trimmed) return { valid: false, error: "Message cannot be empty" };
139
+ const MAX = (window.KIMI_SECURITY_CONFIG && window.KIMI_SECURITY_CONFIG.MAX_MESSAGE_LENGTH) || 5000;
140
+ if (trimmed.length > MAX) {
141
+ return { valid: false, error: `Message too long (max ${MAX} characters)` };
142
+ }
143
+ return { valid: true, sanitized: this.escapeHtml(trimmed) };
144
+ },
145
+ escapeHtml(text) {
146
+ const div = document.createElement("div");
147
+ div.textContent = text;
148
+ return div.innerHTML;
149
+ },
150
+ /**
151
+ * Format chat text with simple markdown-like syntax (secure)
152
+ * Supports: **bold**, *italic*, and preserves line breaks
153
+ * Security: All text is escaped first, then selective formatting is applied
154
+ */
155
+ formatChatText(text) {
156
+ if (!text || typeof text !== "string") return "";
157
+
158
+ // First: Escape all HTML to prevent XSS
159
+ let escaped = this.escapeHtml(text);
160
+
161
+ // Optional: Replace em-dash with regular dash if preferred
162
+ escaped = escaped.replace(/—/g, "-");
163
+
164
+ // Second: Apply simple formatting (only on escaped text)
165
+ // **bold** -> <strong>bold</strong>
166
+ escaped = escaped.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
167
+
168
+ // *italic* -> <em>italic</em> (but not if already inside **)
169
+ escaped = escaped.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, "<em>$1</em>");
170
+
171
+ // Smart paragraph handling: only create <p> for double line breaks or real paragraphs
172
+ // Split by double line breaks (\n\n) to identify real paragraphs
173
+ const realParagraphs = escaped.split(/\n\s*\n/).filter(para => para.trim().length > 0);
174
+
175
+ if (realParagraphs.length > 1) {
176
+ // Multiple paragraphs found - wrap each in <p>
177
+ escaped = realParagraphs.map(p => `<p>${p.trim().replace(/\n/g, " ")}</p>`).join("");
178
+ } else {
179
+ // Single paragraph - just convert single \n to spaces (natural text flow)
180
+ escaped = escaped.replace(/\n/g, " ");
181
+ }
182
+
183
+ return escaped;
184
+ },
185
+ validateRange(value, key) {
186
+ const bounds = {
187
+ voiceRate: { min: 0.5, max: 2, def: 1.1 },
188
+ voicePitch: { min: 0.5, max: 2, def: 1.1 },
189
+ voiceVolume: { min: 0, max: 1, def: 0.8 },
190
+ llmTemperature: { min: 0, max: 1, def: 0.9 },
191
+ llmMaxTokens: { min: 1, max: 8192, def: 400 },
192
+ llmTopP: { min: 0, max: 1, def: 0.9 },
193
+ llmFrequencyPenalty: { min: 0, max: 2, def: 0.9 },
194
+ llmPresencePenalty: { min: 0, max: 2, def: 0.8 },
195
+ interfaceOpacity: { min: 0.1, max: 1, def: 0.8 }
196
+ };
197
+ const b = bounds[key] || { min: 0, max: 100, def: 0 };
198
+ const v = window.KimiSecurityUtils
199
+ ? window.KimiSecurityUtils.validateRange(value, b.min, b.max, b.def)
200
+ : isNaN(parseFloat(value))
201
+ ? b.def
202
+ : Math.max(b.min, Math.min(b.max, parseFloat(value)));
203
+ return { value: v, clamped: v !== parseFloat(value) };
204
+ }
205
+ };
206
+
207
+ // Provider utilities used across the app
208
+ const KimiProviderUtils = {
209
+ getKeyPrefForProvider(provider) {
210
+ // Each provider should have its own separate API key storage
211
+ const providerKeys = {
212
+ openrouter: "openrouterApiKey",
213
+ openai: "openaiApiKey",
214
+ groq: "groqApiKey",
215
+ together: "togetherApiKey",
216
+ deepseek: "deepseekApiKey",
217
+ "openai-compatible": "customApiKey",
218
+ ollama: null
219
+ };
220
+ return providerKeys[provider] || "providerApiKey";
221
+ },
222
+ async getApiKey(db, provider) {
223
+ if (!db) return null;
224
+ if (provider === "ollama") return "__local__";
225
+ const keyPref = this.getKeyPrefForProvider(provider);
226
+ return await db.getPreference(keyPref);
227
+ },
228
+ getLabelForProvider(provider) {
229
+ const labels = {
230
+ openrouter: "OpenRouter API Key",
231
+ openai: "OpenAI API Key",
232
+ groq: "Groq API Key",
233
+ together: "Together API Key",
234
+ deepseek: "DeepSeek API Key",
235
+ custom: "Custom API Key",
236
+ "openai-compatible": "API Key",
237
+ ollama: "API Key"
238
+ };
239
+ return labels[provider] || "API Key";
240
+ }
241
+ };
242
+ window.KimiProviderUtils = KimiProviderUtils;
243
+ // Shared provider placeholders used by UI and LLM manager. Keep in window for backward compatibility.
244
+ const KimiProviderPlaceholders = {
245
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
246
+ openai: "https://api.openai.com/v1/chat/completions",
247
+ groq: "https://api.groq.com/openai/v1/chat/completions",
248
+ together: "https://api.together.xyz/v1/chat/completions",
249
+ deepseek: "https://api.deepseek.com/chat/completions",
250
+ "openai-compatible": "",
251
+ ollama: "http://localhost:11434/api/chat"
252
+ };
253
+ window.KimiProviderPlaceholders = KimiProviderPlaceholders;
254
+ export { KimiProviderUtils, KimiProviderPlaceholders };
255
+
256
+ // Performance utility functions for debouncing and throttling
257
+ window.KimiPerformanceUtils = {
258
+ debounce: function (func, wait, immediate = false, context = null) {
259
+ let timeout;
260
+ let result;
261
+
262
+ return function executedFunction(...args) {
263
+ const later = () => {
264
+ timeout = null;
265
+ if (!immediate) {
266
+ result = func.apply(context || this, args);
267
+ }
268
+ };
269
+
270
+ const callNow = immediate && !timeout;
271
+ clearTimeout(timeout);
272
+ timeout = setTimeout(later, wait);
273
+
274
+ if (callNow) {
275
+ result = func.apply(context || this, args);
276
+ }
277
+
278
+ return result;
279
+ };
280
+ },
281
+
282
+ throttle: function (func, limit, options = {}) {
283
+ const { leading = true, trailing = true } = options;
284
+ let inThrottle;
285
+ let lastFunc;
286
+ let lastRan;
287
+
288
+ return function (...args) {
289
+ if (!inThrottle) {
290
+ if (leading) {
291
+ func.apply(this, args);
292
+ }
293
+ lastRan = Date.now();
294
+ inThrottle = true;
295
+ } else {
296
+ clearTimeout(lastFunc);
297
+ lastFunc = setTimeout(
298
+ () => {
299
+ if (trailing && Date.now() - lastRan >= limit) {
300
+ func.apply(this, args);
301
+ lastRan = Date.now();
302
+ }
303
+ },
304
+ limit - (Date.now() - lastRan)
305
+ );
306
+ }
307
+
308
+ setTimeout(() => (inThrottle = false), limit);
309
+ };
310
+ }
311
+ };
312
+
313
+ // Language management utilities
314
+ window.KimiLanguageUtils = {
315
+ // Default language priority: auto -> user preference -> browser -> fr
316
+ async getLanguage() {
317
+ if (window.kimiDB && window.kimiDB.getPreference) {
318
+ const userLang = await window.kimiDB.getPreference("selectedLanguage", null);
319
+ if (userLang && userLang !== "auto") {
320
+ return userLang;
321
+ }
322
+ }
323
+
324
+ // Auto-detect from browser
325
+ const browserLang = navigator.language?.split("-")[0] || "en";
326
+ const supportedLangs = ["en", "fr", "es", "de", "it", "ja", "zh"];
327
+ return supportedLangs.includes(browserLang) ? browserLang : "en";
328
+ },
329
+
330
+ // Auto-detect language from text content
331
+ detectLanguage(text) {
332
+ if (!text) return "en";
333
+
334
+ if (/[àâäéèêëîïôöùûüÿç]/i.test(text)) return "fr";
335
+ if (/[äöüß]/i.test(text)) return "de";
336
+ if (/[ñáéíóúü]/i.test(text)) return "es";
337
+ if (/[àèìòù]/i.test(text)) return "it";
338
+ if (/[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]/i.test(text)) return "ja";
339
+ if (/[\u4e00-\u9fff]/i.test(text)) return "zh";
340
+ return "en";
341
+ },
342
+ // Normalize language codes to a primary subtag (e.g. 'en-US' -> 'en', 'us:en' -> 'en')
343
+ normalizeLanguageCode(raw) {
344
+ if (!raw) return "";
345
+ try {
346
+ let norm = String(raw).toLowerCase();
347
+ if (norm.includes(":")) {
348
+ const parts = norm.split(":");
349
+ norm = parts[parts.length - 1];
350
+ }
351
+ norm = norm.replace("_", "-");
352
+ if (norm.includes("-")) norm = norm.split("-")[0];
353
+ return norm;
354
+ } catch (e) {
355
+ return "";
356
+ }
357
+ }
358
+ };
359
+
360
+ // Security and validation utilities
361
+ class KimiSecurityUtils {
362
+ static sanitizeInput(input, type = "text") {
363
+ if (typeof input !== "string") return "";
364
+
365
+ switch (type) {
366
+ case "html":
367
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#x27;");
368
+ case "number":
369
+ const num = parseFloat(input);
370
+ return isNaN(num) ? 0 : num;
371
+ case "integer":
372
+ const int = parseInt(input, 10);
373
+ return isNaN(int) ? 0 : int;
374
+ case "url":
375
+ try {
376
+ new URL(input);
377
+ return input;
378
+ } catch {
379
+ return "";
380
+ }
381
+ default:
382
+ return input.trim();
383
+ }
384
+ }
385
+
386
+ static validateRange(value, min, max, defaultValue = 0) {
387
+ const num = parseFloat(value);
388
+ if (isNaN(num)) return defaultValue;
389
+ return Math.max(min, Math.min(max, num));
390
+ }
391
+
392
+ static validateApiKey(key) {
393
+ if (!key || typeof key !== "string") return false;
394
+ if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
395
+ return !!window.KIMI_VALIDATORS.validateApiKey(key.trim());
396
+ }
397
+ return key.trim().length > 10 && (key.startsWith("sk-") || key.startsWith("sk-or-"));
398
+ }
399
+ }
400
+
401
+ // Cache management for better performance
402
+ class KimiCacheManager {
403
+ constructor(maxAge = 300000) {
404
+ // 5 minutes default
405
+ this.cache = new Map();
406
+ this.maxAge = maxAge;
407
+ }
408
+
409
+ set(key, value, customMaxAge = null) {
410
+ const maxAge = customMaxAge || this.maxAge;
411
+ this.cache.set(key, {
412
+ value,
413
+ timestamp: Date.now(),
414
+ maxAge
415
+ });
416
+
417
+ // Clean old entries periodically
418
+ if (this.cache.size > 100) {
419
+ this.cleanup();
420
+ }
421
+ }
422
+
423
+ get(key) {
424
+ const entry = this.cache.get(key);
425
+ if (!entry) return null;
426
+
427
+ const now = Date.now();
428
+ if (now - entry.timestamp > entry.maxAge) {
429
+ this.cache.delete(key);
430
+ return null;
431
+ }
432
+
433
+ return entry.value;
434
+ }
435
+
436
+ has(key) {
437
+ return this.get(key) !== null;
438
+ }
439
+
440
+ delete(key) {
441
+ return this.cache.delete(key);
442
+ }
443
+
444
+ clear() {
445
+ this.cache.clear();
446
+ }
447
+
448
+ cleanup() {
449
+ const now = Date.now();
450
+ for (const [key, entry] of this.cache.entries()) {
451
+ if (now - entry.timestamp > entry.maxAge) {
452
+ this.cache.delete(key);
453
+ }
454
+ }
455
+ }
456
+
457
+ getStats() {
458
+ return {
459
+ size: this.cache.size,
460
+ keys: Array.from(this.cache.keys())
461
+ };
462
+ }
463
+ }
464
+
465
+ class KimiBaseManager {
466
+ constructor() {
467
+ // Common base for all managers
468
+ }
469
+
470
+ // Utility method to format file size
471
+ formatFileSize(bytes) {
472
+ if (bytes === 0) return "0 Bytes";
473
+ const k = 1024;
474
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
475
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
476
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
477
+ }
478
+
479
+ // Utility method for error handling
480
+ handleError(error, context = "Operation") {
481
+ console.error(`Error in ${context}:`, error);
482
+ }
483
+
484
+ // Utility method to wait
485
+ async delay(ms) {
486
+ return new Promise(resolve => setTimeout(resolve, ms));
487
+ }
488
+ }
489
+
490
+ // KimiVideoManager implementation moved to ./kimi-videos.js
491
+ // Ensure the video manager module is evaluated so it registers itself on window
492
+ import "./kimi-videos.js";
493
+
494
+ function getMoodCategoryFromPersonality(traits) {
495
+ // Use unified emotion system
496
+ if (window.kimiEmotionSystem) {
497
+ return window.kimiEmotionSystem.getMoodCategoryFromPersonality(traits);
498
+ }
499
+
500
+ // Fallback (should not be reached) - must match emotion system calculation
501
+ const keys = ["affection", "romance", "empathy", "playfulness", "humor", "intelligence"];
502
+ let sum = 0;
503
+ let count = 0;
504
+ keys.forEach(key => {
505
+ if (typeof traits[key] === "number") {
506
+ sum += traits[key];
507
+ count++;
508
+ }
509
+ });
510
+ const avg = count > 0 ? sum / count : 50;
511
+
512
+ if (avg >= 80) return "speakingPositive";
513
+ if (avg >= 60) return "neutral";
514
+ if (avg >= 40) return "neutral";
515
+ if (avg >= 20) return "speakingNegative";
516
+ return "speakingNegative";
517
+ }
518
+
519
+ // Expose personality → mood helper for video manager
520
+ window.getMoodCategoryFromPersonality = getMoodCategoryFromPersonality;
521
+
522
+ // Centralized initialization manager
523
+ class KimiInitManager {
524
+ constructor() {
525
+ this.managers = new Map();
526
+ this.initOrder = [];
527
+ this.isInitialized = false;
528
+ }
529
+
530
+ register(name, managerFactory, dependencies = [], delay = 0) {
531
+ this.managers.set(name, {
532
+ factory: managerFactory,
533
+ dependencies,
534
+ delay,
535
+ instance: null,
536
+ initialized: false
537
+ });
538
+ }
539
+
540
+ async initializeAll() {
541
+ if (this.isInitialized) return;
542
+
543
+ // Sort by dependencies and delays
544
+ const sortedManagers = this.topologicalSort();
545
+
546
+ for (const managerName of sortedManagers) {
547
+ await this.initializeManager(managerName);
548
+ }
549
+
550
+ this.isInitialized = true;
551
+ }
552
+ async initializeManager(name) {
553
+ const manager = this.managers.get(name);
554
+ if (!manager || manager.initialized) return;
555
+
556
+ // Wait for dependencies
557
+ for (const dep of manager.dependencies) {
558
+ await this.initializeManager(dep);
559
+ }
560
+
561
+ // Apply delay if necessary
562
+ if (manager.delay > 0) {
563
+ await new Promise(resolve => setTimeout(resolve, manager.delay));
564
+ }
565
+
566
+ try {
567
+ manager.instance = await manager.factory();
568
+ manager.initialized = true;
569
+ } catch (error) {
570
+ console.error(`Error during initialization of ${name}:`, error);
571
+ throw error;
572
+ }
573
+ }
574
+
575
+ topologicalSort() {
576
+ // Simple implementation of topological sort
577
+ const sorted = [];
578
+ const visited = new Set();
579
+ const temp = new Set();
580
+
581
+ const visit = name => {
582
+ if (temp.has(name)) {
583
+ throw new Error(`Circular dependency detected: ${name}`);
584
+ }
585
+ if (visited.has(name)) return;
586
+
587
+ temp.add(name);
588
+ const manager = this.managers.get(name);
589
+
590
+ for (const dep of manager.dependencies) {
591
+ visit(dep);
592
+ }
593
+
594
+ temp.delete(name);
595
+ visited.add(name);
596
+ sorted.push(name);
597
+ };
598
+
599
+ for (const name of this.managers.keys()) {
600
+ visit(name);
601
+ }
602
+
603
+ return sorted;
604
+ }
605
+
606
+ getInstance(name) {
607
+ const manager = this.managers.get(name);
608
+ return manager ? manager.instance : null;
609
+ }
610
+ }
611
+
612
+ // Utility class for DOM manipulations
613
+ class KimiDOMUtils {
614
+ static get(selector) {
615
+ return document.querySelector(selector);
616
+ }
617
+ static getAll(selector) {
618
+ return document.querySelectorAll(selector);
619
+ }
620
+ static setText(selector, text) {
621
+ const el = this.get(selector);
622
+ if (el) el.textContent = text;
623
+ }
624
+ static setValue(selector, value) {
625
+ const el = this.get(selector);
626
+ if (el) el.value = value;
627
+ }
628
+ static show(selector) {
629
+ const el = this.get(selector);
630
+ if (el) el.style.display = "";
631
+ }
632
+ static hide(selector) {
633
+ const el = this.get(selector);
634
+ if (el) el.style.display = "none";
635
+ }
636
+ static toggle(selector) {
637
+ const el = this.get(selector);
638
+ if (el) el.style.display = el.style.display === "none" ? "" : "none";
639
+ }
640
+ static addClass(selector, className) {
641
+ const el = this.get(selector);
642
+ if (el) el.classList.add(className);
643
+ }
644
+ static removeClass(selector, className) {
645
+ const el = this.get(selector);
646
+ if (el) el.classList.remove(className);
647
+ }
648
+ static transition(selector, property, value, duration = 300) {
649
+ const el = this.get(selector);
650
+ if (el) {
651
+ el.style.transition = property + " " + duration + "ms";
652
+ el.style[property] = value;
653
+ }
654
+ }
655
+ }
656
+
657
+ // Déclaration complète de la classe KimiOverlayManager
658
+ class KimiOverlayManager {
659
+ constructor() {
660
+ this.overlays = {};
661
+ this._initAll();
662
+ }
663
+ _initAll() {
664
+ const overlayIds = ["chat-container", "settings-overlay", "help-overlay"];
665
+ overlayIds.forEach(id => {
666
+ const el = document.getElementById(id);
667
+ if (el) {
668
+ this.overlays[id] = el;
669
+ if (id !== "chat-container") {
670
+ el.addEventListener("click", e => {
671
+ if (e.target === el) {
672
+ this.close(id);
673
+ }
674
+ });
675
+ }
676
+ }
677
+ });
678
+ }
679
+ open(name) {
680
+ const el = this.overlays[name];
681
+ if (el) el.classList.add("visible");
682
+ }
683
+ close(name) {
684
+ const el = this.overlays[name];
685
+ if (el) el.classList.remove("visible");
686
+ // Ensure background video resumes after closing any overlay
687
+ const kv = window.kimiVideo;
688
+ if (kv && kv.activeVideo) {
689
+ try {
690
+ const v = kv.activeVideo;
691
+ if (v.ended) {
692
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
693
+ } else if (v.paused) {
694
+ v.play().catch(() => {
695
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
696
+ });
697
+ }
698
+ } catch {}
699
+ }
700
+ }
701
+ toggle(name) {
702
+ const el = this.overlays[name];
703
+ if (el) el.classList.toggle("visible");
704
+ }
705
+ isOpen(name) {
706
+ const el = this.overlays[name];
707
+ return el ? el.classList.contains("visible") : false;
708
+ }
709
+ }
710
+
711
+ function getCharacterInfo(characterName) {
712
+ return window.KIMI_CHARACTERS[characterName] || window.KIMI_CHARACTERS.kimi;
713
+ }
714
+
715
+ // Restauration de la classe KimiTabManager
716
+ class KimiTabManager {
717
+ constructor(options = {}) {
718
+ this.settingsOverlay = document.getElementById("settings-overlay");
719
+ this.settingsTabs = document.querySelectorAll(".settings-tab");
720
+ this.tabContents = document.querySelectorAll(".tab-content");
721
+ this.settingsContent = document.querySelector(".settings-content");
722
+ this.onTabChange = options.onTabChange || null;
723
+ this.resizeObserver = null;
724
+ // Guard flag to batch ResizeObserver callbacks within a frame
725
+ this._resizeRafScheduled = false;
726
+ this.init();
727
+ }
728
+
729
+ init() {
730
+ this.settingsTabs.forEach(tab => {
731
+ tab.addEventListener("click", () => {
732
+ this.activateTab(tab.dataset.tab);
733
+ });
734
+ });
735
+ const activeTab = document.querySelector(".settings-tab.active");
736
+ if (activeTab) this.activateTab(activeTab.dataset.tab);
737
+ this.setupResizeObserver();
738
+ this.setupModalObserver();
739
+ }
740
+
741
+ activateTab(tabName) {
742
+ this.settingsTabs.forEach(tab => {
743
+ if (tab.dataset.tab === tabName) tab.classList.add("active");
744
+ else tab.classList.remove("active");
745
+ });
746
+ this.tabContents.forEach(content => {
747
+ if (content.dataset.tab === tabName) content.classList.add("active");
748
+ else content.classList.remove("active");
749
+ });
750
+ // Ensure the content scroll resets to the top when changing tabs
751
+ if (this.settingsContent) {
752
+ this.settingsContent.scrollTop = 0;
753
+ // Defer once to handle layout updates after class toggles
754
+ window.requestAnimationFrame(() => {
755
+ this.settingsContent.scrollTop = 0;
756
+ });
757
+ }
758
+ if (this.onTabChange) this.onTabChange(tabName);
759
+ setTimeout(() => this.adjustTabsForScrollbar(), 100);
760
+ if (window.innerWidth <= 768) {
761
+ const tab = Array.from(this.settingsTabs).find(t => t.dataset.tab === tabName);
762
+ if (tab) tab.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
763
+ }
764
+ }
765
+
766
+ setupResizeObserver() {
767
+ if ("ResizeObserver" in window && this.settingsContent) {
768
+ this.resizeObserver = new ResizeObserver(() => {
769
+ // Defer to next animation frame to avoid ResizeObserver loop warnings
770
+ if (this._resizeRafScheduled) return;
771
+ this._resizeRafScheduled = true;
772
+ window.requestAnimationFrame(() => {
773
+ this._resizeRafScheduled = false;
774
+ this.adjustTabsForScrollbar();
775
+ });
776
+ });
777
+ this.resizeObserver.observe(this.settingsContent);
778
+ }
779
+ }
780
+
781
+ setupModalObserver() {
782
+ if (!this.settingsOverlay) return;
783
+ const observer = new MutationObserver(mutations => {
784
+ mutations.forEach(mutation => {
785
+ if (mutation.type === "attributes" && mutation.attributeName === "class") {
786
+ if (this.settingsOverlay.classList.contains("visible")) {
787
+ // Reset scroll to top when the settings modal opens
788
+ if (this.settingsContent) {
789
+ this.settingsContent.scrollTop = 0;
790
+ window.requestAnimationFrame(() => {
791
+ this.settingsContent.scrollTop = 0;
792
+ });
793
+ }
794
+ }
795
+ }
796
+ });
797
+ });
798
+ observer.observe(this.settingsOverlay, { attributes: true, attributeFilter: ["class"] });
799
+ }
800
+
801
+ adjustTabsForScrollbar() {
802
+ if (!this.settingsContent || !this.settingsTabs.length) return;
803
+ const tabsContainer = document.querySelector(".settings-tabs");
804
+ const hasVerticalScrollbar = this.settingsContent.scrollHeight > this.settingsContent.clientHeight;
805
+ if (hasVerticalScrollbar) {
806
+ const scrollbarWidth = this.settingsContent.offsetWidth - this.settingsContent.clientWidth;
807
+ tabsContainer.classList.add("compressed");
808
+ tabsContainer.style.paddingRight = `${Math.max(scrollbarWidth, 8)}px`;
809
+ tabsContainer.style.boxSizing = "border-box";
810
+ const tabs = tabsContainer.querySelectorAll(".settings-tab");
811
+ const availableWidth = tabsContainer.clientWidth - scrollbarWidth;
812
+ const tabCount = tabs.length;
813
+ const idealTabWidth = availableWidth / tabCount;
814
+ tabs.forEach(tab => {
815
+ if (idealTabWidth < 140) {
816
+ tab.style.fontSize = "0.85rem";
817
+ tab.style.padding = "14px 10px";
818
+ } else if (idealTabWidth < 160) {
819
+ tab.style.fontSize = "0.95rem";
820
+ tab.style.padding = "15px 12px";
821
+ } else {
822
+ tab.style.fontSize = "1rem";
823
+ tab.style.padding = "16px 16px";
824
+ }
825
+ });
826
+ } else {
827
+ tabsContainer.classList.remove("compressed");
828
+ tabsContainer.style.paddingRight = "";
829
+ tabsContainer.style.boxSizing = "";
830
+ const tabs = tabsContainer.querySelectorAll(".settings-tab");
831
+ tabs.forEach(tab => {
832
+ tab.style.fontSize = "";
833
+ tab.style.padding = "";
834
+ });
835
+ }
836
+ }
837
+ }
838
+
839
+ class KimiUIEventManager {
840
+ constructor() {
841
+ this.events = [];
842
+ }
843
+ addEvent(target, type, handler, options) {
844
+ target.addEventListener(type, handler, options);
845
+ this.events.push({ target, type, handler, options });
846
+ }
847
+ removeAll() {
848
+ for (const { target, type, handler, options } of this.events) {
849
+ target.removeEventListener(type, handler, options);
850
+ }
851
+ this.events = [];
852
+ }
853
+ }
854
+
855
+ class KimiFormManager {
856
+ constructor(options = {}) {
857
+ this.db = options.db || null;
858
+ this.memory = options.memory || null;
859
+ this._autoInit = options.autoInit === true;
860
+ if (this._autoInit) {
861
+ this._initSliders();
862
+ }
863
+ }
864
+ init() {
865
+ this._initSliders();
866
+ }
867
+ _initSliders() {
868
+ document.querySelectorAll(".kimi-slider").forEach(slider => {
869
+ const valueSpan = document.getElementById(slider.id + "-value");
870
+ if (valueSpan) valueSpan.textContent = slider.value;
871
+ // Only update visible value; side-effects handled by specialized listeners
872
+ slider.addEventListener("input", () => {
873
+ if (valueSpan) valueSpan.textContent = slider.value;
874
+ });
875
+ });
876
+ }
877
+ }
878
+
879
+ class KimiUIStateManager {
880
+ constructor() {
881
+ this.state = {
882
+ overlays: {},
883
+ activeTab: null,
884
+ favorability: 65,
885
+ transcript: "",
886
+ chatOpen: false,
887
+ settingsOpen: false,
888
+ micActive: false,
889
+ sliders: {}
890
+ };
891
+ this.overlayManager = window.kimiOverlayManager || null;
892
+ this.tabManager = window.kimiTabManager || null;
893
+ this.formManager = window.kimiFormManager || null;
894
+ }
895
+ setOverlay(name, visible) {
896
+ this.state.overlays[name] = visible;
897
+ if (this.overlayManager) {
898
+ if (visible) this.overlayManager.open(name);
899
+ else this.overlayManager.close(name);
900
+ }
901
+ }
902
+ setActiveTab(tabName) {
903
+ this.state.activeTab = tabName;
904
+ if (this.tabManager) this.tabManager.activateTab(tabName);
905
+ }
906
+ /**
907
+ * @deprecated Prefer calling updateGlobalPersonalityUI() after updating traits.
908
+ * This direct setter will be removed in a future cleanup.
909
+ */
910
+ setPersonalityAverage(value) {
911
+ const v = Number(value) || 0;
912
+ const clamped = Math.max(0, Math.min(100, v));
913
+ this.state.favorability = clamped;
914
+ window.KimiDOMUtils.setText("#favorability-text", `${clamped.toFixed(2)}%`);
915
+ window.KimiDOMUtils.get("#favorability-bar").style.width = `${clamped}%`;
916
+ }
917
+ /**
918
+ * @deprecated Use setPersonalityAverage() (itself deprecated) or updateGlobalPersonalityUI().
919
+ */
920
+ setFavorability(value) {
921
+ this.setPersonalityAverage(value);
922
+ }
923
+ async setTranscript(text) {
924
+ this.state.transcript = text;
925
+ // Always use the proper transcript management via VoiceManager
926
+ if (window.kimiVoiceManager && window.kimiVoiceManager.updateTranscriptVisibility) {
927
+ await window.kimiVoiceManager.updateTranscriptVisibility(!!text, text);
928
+ } else {
929
+ console.warn("VoiceManager not available - transcript display may not work properly");
930
+ }
931
+ }
932
+ setChatOpen(open) {
933
+ this.state.chatOpen = open;
934
+ this.setOverlay("chat-container", open);
935
+ }
936
+ setSettingsOpen(open) {
937
+ this.state.settingsOpen = open;
938
+ this.setOverlay("settings-overlay", open);
939
+ }
940
+ setMicActive(active) {
941
+ this.state.micActive = active;
942
+ window.KimiDOMUtils.get("#mic-button").classList.toggle("active", active);
943
+ }
944
+ setSlider(id, value) {
945
+ this.state.sliders[id] = value;
946
+ if (this.formManager) {
947
+ const slider = document.getElementById(id);
948
+ if (slider) slider.value = value;
949
+ const valueSpan = document.getElementById(id + "-value");
950
+ if (valueSpan) valueSpan.textContent = value;
951
+ }
952
+ }
953
+ getState() {
954
+ return { ...this.state };
955
+ }
956
+ }
957
+
958
+ // SIMPLE Fallback management - BASIC ONLY
959
+ window.KimiFallbackManager = {
960
+ getFallbackMessage: function (errorType, customMessage = null) {
961
+ const i18n = window.kimiI18nManager;
962
+
963
+ // If i18n is available, try to get translated message
964
+ if (i18n && typeof i18n.t === "function") {
965
+ if (customMessage) {
966
+ const translated = i18n.t(customMessage);
967
+ if (translated && translated !== customMessage) {
968
+ return translated;
969
+ }
970
+ }
971
+
972
+ const translationKey = `fallback_${errorType}`;
973
+ const translated = i18n.t(translationKey);
974
+ if (translated && translated !== translationKey) {
975
+ return translated;
976
+ }
977
+ }
978
+
979
+ // Fallback to hardcoded messages in multiple languages
980
+ const fallbacks = {
981
+ api_missing: {
982
+ fr: "Pour discuter avec moi, ajoute ta clé API du provider choisi dans les paramètres ! 💕",
983
+ en: "To chat with me, add your selected provider API key in settings! 💕",
984
+ es: "Para chatear conmigo, agrega la clave API de tu proveedor en configuración! 💕",
985
+ de: "Um mit mir zu chatten, füge deinen Anbieter-API-Schlüssel in den Einstellungen hinzu! 💕",
986
+ it: "Per chattare con me, aggiungi la chiave API del provider nelle impostazioni! 💕"
987
+ },
988
+ api_error: {
989
+ fr: "Désolée, le service IA est temporairement indisponible. Veuillez réessayer plus tard.",
990
+ en: "Sorry, the AI service is temporarily unavailable. Please try again later.",
991
+ es: "Lo siento, el servicio de IA no está disponible temporalmente. Inténtalo de nuevo más tarde.",
992
+ de: "Entschuldigung, der KI-Service ist vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.",
993
+ it: "Spiacente, il servizio IA è temporaneamente non disponibile. Riprova più tardi."
994
+ },
995
+ model_error: {
996
+ fr: "Désolée, le modèle sélectionné n'est pas disponible. Veuillez choisir un autre modèle.",
997
+ en: "Sorry, the selected model is not available. Please choose another model.",
998
+ es: "Lo siento, el modelo seleccionado no está disponible. Elige otro modelo.",
999
+ de: "Entschuldigung, das ausgewählte Modell ist nicht verfügbar. Bitte wählen Sie ein anderes Modell.",
1000
+ it: "Spiacente, il modello selezionato non è disponibile. Scegli un altro modello."
1001
+ }
1002
+ };
1003
+
1004
+ // Detect current language (fallback detection)
1005
+ const currentLang = this.detectCurrentLanguage();
1006
+
1007
+ if (fallbacks[errorType] && fallbacks[errorType][currentLang]) {
1008
+ return fallbacks[errorType][currentLang];
1009
+ }
1010
+
1011
+ // Ultimate fallback to English
1012
+ if (fallbacks[errorType] && fallbacks[errorType].en) {
1013
+ return fallbacks[errorType].en;
1014
+ }
1015
+
1016
+ switch (errorType) {
1017
+ case "api_missing":
1018
+ return "To chat with me, add your API key in settings! 💕";
1019
+ case "api_error":
1020
+ case "api":
1021
+ return "Sorry, the AI service is temporarily unavailable. Please try again later.";
1022
+ case "model_error":
1023
+ case "model":
1024
+ return "Sorry, the selected model is not available. Please choose another model or check your configuration.";
1025
+ case "network_error":
1026
+ case "network":
1027
+ return "Sorry, I cannot respond because there is no internet connection.";
1028
+ case "technical_error":
1029
+ case "technical":
1030
+ return "Sorry, I am unable to answer due to a technical issue.";
1031
+ case "general_error":
1032
+ default:
1033
+ return "Sorry my love, I am having a little technical issue! 💕";
1034
+ }
1035
+ },
1036
+
1037
+ detectCurrentLanguage: function () {
1038
+ // Try to get language from various sources
1039
+
1040
+ // 1. Try from language selector if available
1041
+ const langSelect = document.getElementById("language-selection");
1042
+ if (langSelect && langSelect.value) {
1043
+ return langSelect.value;
1044
+ }
1045
+
1046
+ // 2. Try from HTML lang attribute
1047
+ const htmlLang = document.documentElement.lang;
1048
+ if (htmlLang) {
1049
+ return htmlLang.split("-")[0]; // Get just the language part
1050
+ }
1051
+
1052
+ // 3. Try from browser language
1053
+ const browserLang = navigator.language || navigator.userLanguage;
1054
+ if (browserLang) {
1055
+ return browserLang.split("-")[0];
1056
+ }
1057
+
1058
+ // 4. Default to English (as seems to be the default for this app)
1059
+ return "en";
1060
+ },
1061
+
1062
+ showFallbackResponse: async function (errorType, customMessage = null) {
1063
+ const message = this.getFallbackMessage(errorType, customMessage);
1064
+
1065
+ // Add to chat
1066
+ if (window.addMessageToChat) {
1067
+ window.addMessageToChat("kimi", message);
1068
+ }
1069
+
1070
+ // Speak if available
1071
+ if (window.voiceManager && window.voiceManager.speak) {
1072
+ window.voiceManager.speak(message);
1073
+ }
1074
+
1075
+ // SIMPLE: Always show neutral videos in fallback mode
1076
+ if (window.kimiVideo && window.kimiVideo.switchToContext) {
1077
+ window.kimiVideo.switchToContext("neutral", "neutral");
1078
+ }
1079
+
1080
+ return message;
1081
+ }
1082
+ };
1083
+
1084
+ window.KimiBaseManager = KimiBaseManager;
1085
+ // KimiVideoManager is provided by the separate module `kimi-videos.js` which sets
1086
+ // `window.KimiVideoManager` when executed. Do not reference the symbol here to
1087
+ // avoid ReferenceError during module evaluation.
1088
+ window.KimiSecurityUtils = KimiSecurityUtils;
1089
+ window.KimiCacheManager = new KimiCacheManager(); // Create global instance
1090
+ // Expose helper used by the video manager
1091
+ window.getCharacterInfo = getCharacterInfo;
1092
+ window.KimiInitManager = KimiInitManager;
1093
+ window.KimiDOMUtils = KimiDOMUtils;
1094
+ window.KimiOverlayManager = KimiOverlayManager;
1095
+ window.KimiTabManager = KimiTabManager;
1096
+ window.KimiUIEventManager = KimiUIEventManager;
1097
+ window.KimiFormManager = KimiFormManager;
1098
+ window.KimiUIStateManager = KimiUIStateManager;
1099
+
1100
+ window.KimiTokenUtils = {
1101
+ // Approximate token estimation (heuristic):
1102
+ // Base: 1 token ~ 4 chars (English average). We refine by word count and punctuation density.
1103
+ estimate(text) {
1104
+ if (!text || typeof text !== "string") return 0;
1105
+ const trimmed = text.trim();
1106
+ if (!trimmed) return 0;
1107
+ const charLen = trimmed.length;
1108
+ const words = trimmed.split(/\s+/).length;
1109
+ // Base estimates
1110
+ let estimateByChars = Math.ceil(charLen / 4);
1111
+ const estimateByWords = Math.ceil(words * 1.3); // average 1.3 tokens per word
1112
+ // Blend and adjust for punctuation heavy content
1113
+ const punctCount = (trimmed.match(/[.,!?;:]/g) || []).length;
1114
+ const punctFactor = 1 + Math.min(punctCount / Math.max(words, 1) / 5, 0.15); // cap at +15%
1115
+ const blended = Math.round((estimateByChars * 0.55 + estimateByWords * 0.45) * punctFactor);
1116
+ return Math.max(1, blended);
1117
+ }
1118
+ };