VirtualKimi commited on
Commit
3d50167
·
verified ·
1 Parent(s): dde4eb4

Upload 49 files

Browse files
Files changed (50) hide show
  1. .gitattributes +6 -0
  2. CHANGELOG.md +282 -0
  3. CONTRIBUTING.md +371 -0
  4. LICENSE.md +22 -0
  5. Launch-local-kimi-app.bat +9 -0
  6. favicon.ico +0 -0
  7. index.html +1065 -19
  8. kimi-css/kimi-memory-styles.css +586 -0
  9. kimi-css/kimi-settings.css +1417 -0
  10. kimi-css/kimi-style.css +1737 -0
  11. kimi-icons/bella.jpg +3 -0
  12. kimi-icons/kimi-loading.png +3 -0
  13. kimi-icons/kimi.jpg +3 -0
  14. kimi-icons/rosa.jpg +3 -0
  15. kimi-icons/stella.jpg +3 -0
  16. kimi-icons/virtualkimi-logo.png +3 -0
  17. kimi-js/dexie.min.js +2 -0
  18. kimi-js/kimi-appearance.js +213 -0
  19. kimi-js/kimi-config.js +123 -0
  20. kimi-js/kimi-constants.js +567 -0
  21. kimi-js/kimi-database.js +619 -0
  22. kimi-js/kimi-emotion-system.js +516 -0
  23. kimi-js/kimi-error-manager.js +178 -0
  24. kimi-js/kimi-health-check.js +196 -0
  25. kimi-js/kimi-llm-manager.js +911 -0
  26. kimi-js/kimi-logic.js +41 -0
  27. kimi-js/kimi-memory-system.js +1096 -0
  28. kimi-js/kimi-memory-ui.js +687 -0
  29. kimi-js/kimi-memory.js +131 -0
  30. kimi-js/kimi-module.js +1956 -0
  31. kimi-js/kimi-plugin-manager.js +260 -0
  32. kimi-js/kimi-script.js +1009 -0
  33. kimi-js/kimi-security.js +71 -0
  34. kimi-js/kimi-utils.js +2106 -0
  35. kimi-js/kimi-voices.js +1136 -0
  36. kimi-locale/de.json +204 -0
  37. kimi-locale/en.json +205 -0
  38. kimi-locale/es.json +204 -0
  39. kimi-locale/fr.json +205 -0
  40. kimi-locale/i18n.js +76 -0
  41. kimi-locale/it.json +204 -0
  42. kimi-locale/ja.json +204 -0
  43. kimi-locale/zh.json +204 -0
  44. kimi-plugins/sample-behavior/behavior.js +11 -0
  45. kimi-plugins/sample-behavior/manifest.json +9 -0
  46. kimi-plugins/sample-theme/manifest.json +10 -0
  47. kimi-plugins/sample-theme/theme.css +139 -0
  48. kimi-plugins/sample-theme/theme.js +1 -0
  49. kimi-plugins/sample-voice/manifest.json +9 -0
  50. kimi-plugins/sample-voice/voice.js +16 -0
.gitattributes CHANGED
@@ -33,3 +33,9 @@ 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/bella.jpg filter=lfs diff=lfs merge=lfs -text
37
+ kimi-icons/kimi-loading.png filter=lfs diff=lfs merge=lfs -text
38
+ kimi-icons/kimi.jpg filter=lfs diff=lfs merge=lfs -text
39
+ kimi-icons/rosa.jpg filter=lfs diff=lfs merge=lfs -text
40
+ kimi-icons/stella.jpg filter=lfs diff=lfs merge=lfs -text
41
+ kimi-icons/virtualkimi-logo.png filter=lfs diff=lfs merge=lfs -text
CHANGELOG.md ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual Kimi Changelog
2
+
3
+ ## [1.0.4] - 2025-08-09 - "Emotion & Context Logic Upgrade"
4
+
5
+ ### Added
6
+
7
+ - Major improvements to emotion, context, and personality logic:
8
+ - Enhanced emotion detection and mapping for more nuanced responses
9
+ - Contextual keyword analysis for better understanding of user intent
10
+ - Refined personality trait system with dynamic adaptation
11
+ - Video selection logic now adapts to both emotion and conversational context
12
+ - Improved handling of multi-layered context (emotion, keywords, personality, situation)
13
+
14
+ ### Changed
15
+
16
+ - Video playback and character reactions are now more tightly coupled to detected context and personality traits
17
+ - Emotion and context logic refactored for clarity and maintainability
18
+ - Keyword extraction and context matching algorithms improved for accuracy
19
+
20
+ ### Technical
21
+
22
+ - Refactored core logic in `kimi-emotion-system.js`, `kimi-logic.js`, and `kimi-memory-system.js`
23
+ - Updated video selection and playback logic in `kimi-memory.js` and `kimi-memory-ui.js`
24
+ - Improved context propagation between modules
25
+
26
+ ## [1.0.3] - 2025-08-09 - "LLM multi-provider"
27
+
28
+ ### Added
29
+
30
+ - LLM multi-provider UX enhancements:
31
+ - Dynamic API key label per provider (OpenRouter, OpenAI, Groq, Together, DeepSeek, Custom, Ollama)
32
+ - Visual "Saved" badge when a key is stored or after a successful test
33
+ - Localized tooltip explaining Saved vs connection test
34
+
35
+ ### Changed
36
+
37
+ - OpenAI-compatible flow now reads llmBaseUrl/llmModelId and the correct provider key from KimiDB
38
+ - Clears connection status message when provider/Base URL/Model ID/key changes for clearer feedback
39
+
40
+ ## [1.0.2] - 2025-08-09 - "Smoother Video"
41
+
42
+ ### Changed
43
+
44
+ - Video playback and transition stability improvements:
45
+ - Lightweight MP4 prefetch queue (neutral + likely next clips) to reduce wait times during switches
46
+ - Earlier transition on `canplay` (instead of `canplaythrough`) for faster, smoother swaps
47
+ - Context-aware throttling to prevent rapid switching under load (speaking: ~200ms, listening: ~250ms, dancing: ~600ms, neutral: ~1200ms)
48
+
49
+ ### Fixed
50
+
51
+ - Safe revert on failed `play()` during a switch to avoid frozen frames
52
+ - Aligned event listeners to `canplay` and ensured proper cleanup to prevent leaks
53
+ - Corrected prefetch cache initialization order (prevented `undefined.has` runtime error)
54
+ - Removed unsupported `<link rel="preload" as="video">` to eliminate console warnings
55
+
56
+ ### Technical
57
+
58
+ - Front-end performance tweaks: GPU-accelerated fades with `will-change: opacity` and `backface-visibility: hidden`
59
+ - Connection warm-up: added `preconnect`/`dns-prefetch` to the origin for faster first video start
60
+ - Files updated: `index.html`, `kimi-css/kimi-style.css`, `kimi-js/kimi-utils.js`
61
+
62
+ ## [1.0.1] - 2025-08-08
63
+
64
+ - 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.
65
+ - Added a waiting animation that appears between the user's message submission and the LLM's response, improving user feedback during processing.
66
+ - 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.
67
+
68
+ ## [1.0.0] - 2025-08-07 - "Unified"
69
+
70
+ ### Added
71
+
72
+ - **Intelligent Memory System**: Automatic extraction and categorization of memories from conversations
73
+ - **Multiple AI Characters**: 4 unique personalities (Kimi, Bella, Rosa, Stella) with distinct traits
74
+ - **Advanced Emotion Detection**: Real-time emotion analysis with cultural awareness
75
+ - **Plugin System**: Extensible architecture for themes, voices, and behaviors
76
+ - **Memory Management UI**: Complete interface for viewing, searching, and managing memories
77
+ - **Enhanced Personality System**: 6 dynamic traits that evolve based on interactions
78
+ - **Multilingual Support**: Full localization in 7 languages with auto-detection
79
+ - **Production Health Check**: Comprehensive system validation and monitoring
80
+ - **Performance Optimizations**: Batch database operations and improved loading times
81
+ - **Security Enhancements**: Input validation, sanitization, and secure API handling
82
+
83
+ ### Changed
84
+
85
+ - **Unified Architecture**: Consolidated all emotion and personality systems
86
+ - **Improved Database**: Enhanced IndexedDB implementation with batch operations
87
+ - **Better Error Handling**: Centralized error management with fallback responses
88
+ - **Enhanced UI/UX**: More responsive and accessible interface design
89
+ - **Optimized Video System**: Smoother transitions and better emotion mapping
90
+
91
+ ### Fixed
92
+
93
+ - Function export issues in module system
94
+ - Memory leaks in event listeners
95
+ - Cross-browser compatibility issues
96
+ - Voice recognition stability problems
97
+ - Database initialization race conditions
98
+
99
+ ### Technical
100
+
101
+ - Migrated to unified emotion system
102
+ - Implemented comprehensive validation layer
103
+ - Added automated health monitoring
104
+ - Enhanced plugin security validation
105
+ - Improved mobile responsiveness
106
+
107
+ ## [0.0.9] - 2025-08-04 - "Enhanced"
108
+
109
+ ### Added
110
+
111
+ - Advanced LLM model selection interface
112
+ - Improved voice synthesis with better emotion mapping
113
+ - Enhanced personality trait visualization
114
+ - Better conversation export/import functionality
115
+
116
+ ### Changed
117
+
118
+ - Upgraded database schema for better performance
119
+ - Improved theme system with more customization options
120
+ - Enhanced mobile interface responsiveness
121
+
122
+ ### Fixed
123
+
124
+ - Various browser compatibility issues
125
+ - Voice recognition accuracy improvements
126
+ - Memory management optimizations
127
+
128
+ ## [0.0.8] - 2025-08-01 - "Evolution"
129
+
130
+ ### Added
131
+
132
+ - Dynamic personality trait evolution
133
+ - Enhanced emotion detection algorithms
134
+ - Improved conversation context awareness
135
+ - Better visual feedback systems
136
+
137
+ ### Changed
138
+
139
+ - Redesigned settings interface
140
+ - Improved conversation flow management
141
+ - Enhanced error reporting system
142
+
143
+ ### Fixed
144
+
145
+ - Database sync issues
146
+ - Voice recognition edge cases
147
+ - Theme switching problems
148
+
149
+ ## [0.0.7] - 2025-07-29 - "Immersion"
150
+
151
+ ### Added
152
+
153
+ - Real-time video emotion responses
154
+ - Enhanced voice interaction capabilities
155
+ - Improved conversation context retention
156
+ - Better visual theme system
157
+
158
+ ### Changed
159
+
160
+ - Upgraded UI framework for better performance
161
+ - Improved data synchronization mechanisms
162
+ - Enhanced accessibility features
163
+
164
+ ### Fixed
165
+
166
+ - Various stability improvements
167
+ - Better error handling
168
+ - Improved cross-platform compatibility
169
+
170
+ ## [0.0.6] - 2025-07-26 - "Connection"
171
+
172
+ ### Added
173
+
174
+ - Multi-language support system
175
+ - Enhanced conversation memory
176
+ - Improved personality customization
177
+ - Better audio/video synchronization
178
+
179
+ ### Changed
180
+
181
+ - Redesigned conversation interface
182
+ - Improved data persistence layer
183
+ - Enhanced user experience flows
184
+
185
+ ### Fixed
186
+
187
+ - Memory leak issues
188
+ - Browser compatibility problems
189
+ - Audio synchronization bugs
190
+
191
+ ## [0.0.5] - 2025-07-23 - "Rebirth"
192
+
193
+ ### Added
194
+
195
+ - Complete application rewrite
196
+ - Modern ES6+ JavaScript architecture
197
+ - Responsive design system
198
+ - Advanced AI integration capabilities
199
+ - Comprehensive settings system
200
+
201
+ ### Changed
202
+
203
+ - Modernized codebase with current web standards
204
+ - Improved performance and reliability
205
+ - Enhanced user interface design
206
+ - Better data management system
207
+
208
+ ### Removed
209
+
210
+ - Legacy jQuery dependencies
211
+ - Outdated browser support
212
+
213
+ ## [0.0.4] - 2025-07-20 - "Stability"
214
+
215
+ ### Added
216
+
217
+ - Enhanced voice recognition
218
+ - Improved conversation flow
219
+ - Better error handling
220
+ - Enhanced visual feedback
221
+
222
+ ### Fixed
223
+
224
+ - Various stability issues
225
+ - Performance optimizations
226
+ - Browser compatibility improvements
227
+
228
+ ## [0.0.3] - 2025-07-18 - "Polish"
229
+
230
+ ### Added
231
+
232
+ - Improved user interface
233
+ - Better conversation management
234
+ - Enhanced customization options
235
+
236
+ ### Fixed
237
+
238
+ - Various bugs and stability issues
239
+ - Performance improvements
240
+
241
+ ## [0.0.2] - 2025-07-17 - "Improvements"
242
+
243
+ ### Added
244
+
245
+ - Basic conversation memory
246
+ - Improved personality system
247
+ - Enhanced visual themes
248
+
249
+ ### Fixed
250
+
251
+ - Initial bug fixes
252
+ - Performance optimizations
253
+
254
+ ## [0.0.1] - 2025-07-16 - "Genesis"
255
+
256
+ ### Added
257
+
258
+ - Initial release
259
+ - Basic AI conversation capabilities
260
+ - Voice recognition and synthesis
261
+ - Simple personality system
262
+ - Theme customization
263
+ - Local data storage
264
+
265
+ ---
266
+
267
+ ## Legend
268
+
269
+ - **Added**: New features
270
+ - **Changed**: Changes in existing functionality
271
+ - **Deprecated**: Soon-to-be removed features
272
+ - **Removed**: Removed features
273
+ - **Fixed**: Bug fixes
274
+ - **Security**: Security improvements
275
+ - **Technical**: Internal technical changes
276
+
277
+ ---
278
+
279
+ All notable changes to Virtual Kimi will be documented in this file.
280
+
281
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
282
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
CONTRIBUTING.md ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ### Debugging
335
+
336
+ - Use browser DevTools for JavaScript debugging
337
+ - Check console for errors and warnings
338
+ - Use the health check system: `window.KimiHealthCheck`
339
+ - Enable debug mode: `window.KIMI_DEBUG = true`
340
+
341
+ ### Performance Optimization
342
+
343
+ - Minimize DOM manipulations
344
+ - Use event delegation for dynamic content
345
+ - Implement proper cleanup for event listeners
346
+ - Optimize database queries with batch operations
347
+
348
+ ### Accessibility
349
+
350
+ - Test with keyboard navigation
351
+ - Verify screen reader compatibility
352
+ - Ensure sufficient color contrast
353
+ - Add appropriate ARIA labels
354
+
355
+ ## Community
356
+
357
+ ### Getting Help
358
+
359
+ - Open an issue for technical questions
360
+ - Check existing documentation first
361
+ - Be specific about your problem or question
362
+
363
+ ### Communication
364
+
365
+ - Be respectful and professional
366
+ - Provide context and details
367
+ - Be patient with response times
368
+ - Help others when possible
369
+
370
+ Thank you for contributing to Virtual Kimi! Your efforts help create a better AI companion experience for everyone.
371
+
LICENSE.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: [[email protected]]([email protected])
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
favicon.ico ADDED
index.html CHANGED
@@ -1,19 +1,1065 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" data-theme="purple">
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
+ <link rel="stylesheet" href="kimi-css/kimi-style.css" />
9
+ <link rel="stylesheet" href="kimi-css/kimi-settings.css" />
10
+ <link rel="stylesheet" href="kimi-css/kimi-memory-styles.css" />
11
+
12
+ <!-- SEO Meta Tags -->
13
+ <meta name="description"
14
+ content="Virtual Kimi is a an AI companion with evolving personality, advanced voice recognition and immersive interface. Discover the future of human-AI relationships.">
15
+ <meta name="keywords"
16
+ content="artificial intelligence, virtual companion, emotional AI, voice recognition, advanced chatbot, Virtual Kimi, personalized AI assistant">
17
+ <meta name="author" content="Jean & Kimi">
18
+ <meta name="robots" content="index, follow">
19
+ <meta name="language" content="EN">
20
+
21
+ <!-- Open Graph / Facebook -->
22
+ <meta property="og:type" content="website">
23
+ <meta property="og:url" content="https://virtualkimi.com/virtual-kimi-app/">
24
+ <meta property="og:title" content="Virtual Kimi - Virtual AI Companion">
25
+ <meta property="og:description"
26
+ content="Discover Virtual Kimi, an AI companion with evolving personality, voice recognition and immersive interface. The future of human-AI relationships.">
27
+ <meta property="og:image" content="kimi-icons/virtualkimi-logo.png">
28
+
29
+ <!-- Twitter -->
30
+ <meta property="twitter:card" content="summary_large_image">
31
+ <meta property="twitter:url" content="https://virtualkimi.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
+ <!-- Schema.org markup for Google -->
38
+ <script type="application/ld+json">
39
+ {
40
+ "@context": "https://schema.org",
41
+ "@type": "SoftwareApplication",
42
+ "name": "Virtual Kimi",
43
+ "description": "Virtual AI companion with evolving personality, voice recognition and immersive interface",
44
+ "applicationCategory": "AI Companion",
45
+ "operatingSystem": "Web Browser",
46
+ "offers": {
47
+ "@type": "Offer",
48
+ "price": "0",
49
+ "priceCurrency": "USD"
50
+ },
51
+ "creator": {
52
+ "@type": "Person",
53
+ "name": "Jean & Kimi"
54
+ },
55
+ "dateCreated": "2025-07-16",
56
+ "dateModified": "2025-08-05",
57
+ "version": "v1.0.4"
58
+ }
59
+ </script>
60
+
61
+ <!-- Favicon -->
62
+ <link rel="icon" type="image/x-icon" href="favicon.ico">
63
+ <link rel="apple-touch-icon" href="kimi-icons/virtualkimi-logo.png">
64
+
65
+ <!-- Font Awesome -->
66
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
67
+
68
+ <!-- Performance: warm up connection to origin -->
69
+ <link rel="preconnect" href="https://virtualkimi.com" crossorigin>
70
+ <link rel="dns-prefetch" href="//virtualkimi.com">
71
+
72
+ </head>
73
+
74
+ <body>
75
+ <div id="loading-screen">
76
+ <img src="kimi-icons/kimi-loading.png" alt="Loading Kimi..." />
77
+ </div>
78
+
79
+ <div class="video-container">
80
+ <video autoplay muted playsinline class="bg-video active" id="video1" preload="auto">
81
+ <source src="" type="video/mp4" />
82
+ <span data-i18n="video_not_supported">Your browser does not support the video tag.</span>
83
+ </video>
84
+ <video autoplay muted playsinline class="bg-video" id="video2" preload="auto">
85
+ <source src="" type="video/mp4" />
86
+ <span data-i18n="video_not_supported">Your browser does not support the video tag.</span>
87
+ </video>
88
+ </div>
89
+
90
+ <div class="content-overlay">
91
+ <div class="transcript-container">
92
+ <p id="transcript"></p>
93
+ </div>
94
+
95
+ <!-- Chat Interface with Kimi -->
96
+ <div class="chat-container" id="chat-container">
97
+ <div class="chat-header">
98
+ <h3><i class="fas fa-comments"></i> <span data-i18n="chat_with_kimi">Chat with Kimi</span></h3>
99
+ <div style="display: flex; gap: 24px">
100
+ <button class="chat-delete" id="chat-delete" aria-label="Delete Messages">
101
+ <i class="fas fa-trash"></i>
102
+ </button>
103
+ <button class="chat-toggle" id="chat-toggle" aria-label="Close Chat">
104
+ <i class="fas fa-times"></i>
105
+ </button>
106
+ </div>
107
+ </div>
108
+ <div class="chat-messages" id="chat-messages"></div>
109
+ <div class="waiting-indicator" id="waiting-indicator" style="display: none">
110
+ <span></span><span></span><span></span>
111
+ </div>
112
+ <div class="chat-input-container">
113
+ <input type="text" id="chat-input" data-i18n-placeholder="write_something"
114
+ placeholder="Write me something, my love..." />
115
+ <button id="send-button">
116
+ <i class="fas fa-paper-plane"></i>
117
+ </button>
118
+ </div>
119
+ </div>
120
+
121
+ <footer class="bottom-bar">
122
+ <div class="control-buttons">
123
+ <button class="control-button-unified" id="chat-button" aria-label="Open Chat">
124
+ <i class="fa-regular fa-comments"></i>
125
+ </button>
126
+ <div class="global-typing-indicator" id="global-typing-indicator" aria-hidden="true">
127
+ <span></span><span></span><span></span>
128
+ </div>
129
+ <button class="mic-button" id="mic-button" aria-label="Start Listening">
130
+ <i class="fas fa-microphone"></i>
131
+ </button>
132
+ <button class="control-button-unified" id="settings-button" aria-label="Settings">
133
+ <i class="fas fa-cog"></i>
134
+ </button>
135
+ </div>
136
+ <div class="top-bar">
137
+ <label id="favorability-label" for="favorability-bar" data-i18n="affection_level_of">💖 Kimi's Affection
138
+ Level</label>
139
+ <div class="progress-container">
140
+ <div class="progress-fill" id="favorability-bar"></div>
141
+ <span class="favorability-text" id="favorability-text">50%</span>
142
+ </div>
143
+ </div>
144
+ </footer>
145
+ </div>
146
+
147
+ <!-- Configuration Panel -->
148
+ <div class="settings-overlay" id="settings-overlay">
149
+ <div class="settings-panel">
150
+ <div class="settings-header">
151
+ <h2 class="settings-title">
152
+ <i class="fas fa-heart"></i>
153
+ <span data-i18n="settings_title">Kimi Configuration</span>
154
+ </h2>
155
+ <div class="settings-header-actions">
156
+ <button class="help-button" id="help-button" aria-label="Help">
157
+ <i class="fas fa-question-circle"></i>
158
+ </button>
159
+ <button class="settings-close" id="settings-close">
160
+ <i class="fas fa-times"></i>
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="settings-tabs">
166
+ <button class="settings-tab active" data-tab="llm">
167
+ <i class="fas fa-robot"></i> <span data-i18n="tab_llm">API & Models</span>
168
+ </button>
169
+ <button class="settings-tab" data-tab="voice">
170
+ <i class="fas fa-microphone"></i> <span data-i18n="tab_voice">Voice & Audio</span>
171
+ </button>
172
+ <button class="settings-tab" data-tab="personality">
173
+ <i class="fas fa-brain"></i> <span data-i18n="tab_personality">Personality</span>
174
+ </button>
175
+ <button class="settings-tab" data-tab="appearance">
176
+ <i class="fas fa-palette"></i> <span data-i18n="tab_appearance">Appearance</span>
177
+ </button>
178
+ <button class="settings-tab" data-tab="data">
179
+ <i class="fas fa-database"></i> <span data-i18n="tab_data">Data</span>
180
+ </button>
181
+ <button class="settings-tab" data-tab="plugins">
182
+ <i class="fas fa-plug"></i> <span data-i18n="tab_plugins">Plugins</span>
183
+ </button>
184
+ </div>
185
+
186
+ <div class="settings-content">
187
+ <div class="tab-content" data-tab="voice">
188
+ <div class="config-section">
189
+ <h3><i class="fas fa-volume-up"></i> <span data-i18n="voice_settings">Voice Settings</span></h3>
190
+
191
+ <div class="config-row">
192
+ <label class="config-label" data-i18n="speech_rate" for="voice-rate">Speech Rate</label>
193
+ <div class="config-control">
194
+ <div class="slider-container">
195
+ <input type="range" class="kimi-slider" id="voice-rate" min="0.5" max="2"
196
+ step="0.01" value="1.1" aria-label="Speech Rate" aria-valuenow="1.1"
197
+ aria-valuemin="0.5" aria-valuemax="2" />
198
+ <span class="slider-value" id="voice-rate-value">1.1</span>
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="config-row">
204
+ <label class="config-label" data-i18n="pitch" for="voice-pitch">Pitch</label>
205
+ <div class="config-control">
206
+ <div class="slider-container">
207
+ <input type="range" class="kimi-slider" id="voice-pitch" min="0.5" max="2"
208
+ step="0.01" value="1.1" aria-label="Pitch" aria-valuenow="1.1"
209
+ aria-valuemin="0.5" aria-valuemax="2" />
210
+ <span class="slider-value" id="voice-pitch-value">1.1</span>
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ <div class="config-row">
216
+ <label class="config-label" data-i18n="volume" for="voice-volume">Volume</label>
217
+ <div class="config-control">
218
+ <div class="slider-container">
219
+ <input type="range" class="kimi-slider" id="voice-volume" min="0" max="1"
220
+ step="0.01" value="0.8" aria-label="Volume" aria-valuenow="0.8"
221
+ aria-valuemin="0" aria-valuemax="1" />
222
+ <span class="slider-value" id="voice-volume-value">0.8</span>
223
+ </div>
224
+ </div>
225
+ </div>
226
+
227
+ <div class="config-row">
228
+ <label class="config-label" data-i18n="language">Language</label>
229
+ <div class="config-control">
230
+ <select class="kimi-select" id="language-selection" aria-label="Language">
231
+ <option value="en" data-i18n="language_english">English</option>
232
+ <option value="fr" data-i18n="language_french">French</option>
233
+ <option value="es" data-i18n="language_spanish">Spanish</option>
234
+ <option value="de" data-i18n="language_german">German</option>
235
+ <option value="it" data-i18n="language_italian">Italian</option>
236
+ <option value="ja" data-i18n="language_japanese">Japanese</option>
237
+ <option value="zh" data-i18n="language_chinese">Chinese</option>
238
+ </select>
239
+ </div>
240
+ </div>
241
+
242
+ <div class="config-row">
243
+ <label class="config-label" data-i18n="preferred_voice">Preferred Voice</label>
244
+ <div class="config-control">
245
+ <select class="kimi-select" id="voice-selection" aria-label="Preferred Voice">
246
+ <option value="auto" data-i18n="automatic">Automatic</option>
247
+ </select>
248
+ </div>
249
+ </div>
250
+
251
+ <div class="config-row">
252
+ <label class="config-label" data-i18n="voice_test_label">Voice Test</label>
253
+ <div class="config-control">
254
+ <button class="kimi-button" id="test-voice" aria-label="Voice Test">
255
+ <i class="fas fa-play"></i> <span data-i18n="voice_test_button">Test the
256
+ Voice</span>
257
+ </button>
258
+ </div>
259
+ </div>
260
+ </div>
261
+ </div>
262
+ <!-- Personality Tab -->
263
+ <div class="tab-content" data-tab="personality">
264
+ <div class="config-section" id="character-section">
265
+ <h3><i class="fas fa-user-astronaut"></i> <span data-i18n="characters">Characters</span></h3>
266
+ <div class="character-grid" id="character-grid"></div>
267
+ <div class="character-actions">
268
+ <button class="kimi-button" id="save-character-btn" data-i18n="save">Save</button>
269
+ </div>
270
+ </div>
271
+
272
+ <div class="config-section">
273
+ <h3>
274
+ <i class="fas fa-heart"></i>
275
+ <span data-i18n="personality_traits">Personality Traits</span>
276
+ <button id="toggle-personality-traits" class="cheat-toggle-btn" aria-expanded="false"
277
+ type="button">
278
+ <i class="fas fa-user-secret"></i>
279
+ <span data-i18n="personality_cheat">Personality Cheat</span>
280
+ </button>
281
+ </h3>
282
+ <div id="cheat-indicator" class="cheat-indicator" data-i18n="cheat_indicator">Adjust traits for
283
+ a custom experience</div>
284
+ <div id="personality-traits-panel" class="cheat-panel">
285
+ <div class="config-row">
286
+ <label class="config-label" data-i18n="affection">Affection</label>
287
+ <div class="config-control">
288
+ <div class="slider-container">
289
+ <input type="range" class="kimi-slider" id="trait-affection" min="0" max="100"
290
+ value="65" />
291
+ <span class="slider-value" id="trait-affection-value">65</span>
292
+ </div>
293
+ </div>
294
+ </div>
295
+
296
+ <div class="config-row">
297
+ <label class="config-label" data-i18n="playfulness">Playfulness</label>
298
+ <div class="config-control">
299
+ <div class="slider-container">
300
+ <input type="range" class="kimi-slider" id="trait-playfulness" min="0" max="100"
301
+ value="55" />
302
+ <span class="slider-value" id="trait-playfulness-value">55</span>
303
+ </div>
304
+ </div>
305
+ </div>
306
+
307
+ <div class="config-row">
308
+ <label class="config-label" data-i18n="intelligence">Intelligence</label>
309
+ <div class="config-control">
310
+ <div class="slider-container">
311
+ <input type="range" class="kimi-slider" id="trait-intelligence" min="0"
312
+ max="100" value="70" />
313
+ <span class="slider-value" id="trait-intelligence-value">70</span>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ <div class="config-row">
319
+ <label class="config-label" data-i18n="empathy">Empathy</label>
320
+ <div class="config-control">
321
+ <div class="slider-container">
322
+ <input type="range" class="kimi-slider" id="trait-empathy" min="0" max="100"
323
+ value="75" />
324
+ <span class="slider-value" id="trait-empathy-value">75</span>
325
+ </div>
326
+ </div>
327
+ </div>
328
+
329
+ <div class="config-row">
330
+ <label class="config-label" data-i18n="humor">Humor</label>
331
+ <div class="config-control">
332
+ <div class="slider-container">
333
+ <input type="range" class="kimi-slider" id="trait-humor" min="0" max="100"
334
+ value="60" />
335
+ <span class="slider-value" id="trait-humor-value">60</span>
336
+ </div>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="config-row">
341
+ <label class="config-label" data-i18n="romance">Romance</label>
342
+ <div class="config-control">
343
+ <div class="slider-container">
344
+ <input type="range" class="kimi-slider" id="trait-romance" min="0" max="100"
345
+ value="50" />
346
+ <span class="slider-value" id="trait-romance-value">50</span>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="tab-content active" data-tab="llm">
355
+ <div class="config-section">
356
+ <h3><i class="fas fa-key"></i> <span data-i18n="api_configuration">API Configuration</span></h3>
357
+
358
+ <div class="config-row">
359
+ <label class="config-label" for="llm-provider">Provider</label>
360
+ <div class="config-control">
361
+ <select class="kimi-select" id="llm-provider" aria-label="LLM Provider">
362
+ <option value="openrouter" selected>OpenRouter</option>
363
+ <option value="openai">OpenAI</option>
364
+ <option value="groq">Groq (OpenAI compatible)</option>
365
+ <option value="together">Together (OpenAI compatible)</option>
366
+ <option value="deepseek">DeepSeek (OpenAI compatible)</option>
367
+ <option value="openai-compatible">Custom OpenAI-compatible</option>
368
+ <option value="ollama">Local (Ollama)</option>
369
+ </select>
370
+ </div>
371
+ </div>
372
+
373
+ <div class="config-row">
374
+ <label class="config-label" for="llm-base-url">Base URL</label>
375
+ <div class="config-control">
376
+ <input type="text" class="kimi-input" id="llm-base-url"
377
+ placeholder="https://api.openai.com/v1/chat/completions" autocomplete="off" />
378
+ </div>
379
+ </div>
380
+
381
+ <div class="config-row">
382
+ <label class="config-label" for="llm-model-id">Model ID</label>
383
+ <div class="config-control">
384
+ <input type="text" class="kimi-input" id="llm-model-id"
385
+ placeholder="gpt-4o-mini | llama-3.1-8b-instruct | ..." autocomplete="off" />
386
+ </div>
387
+ </div>
388
+
389
+ <div class="config-row">
390
+ <div class="config-label-group">
391
+ <label class="config-label" id="api-key-label" data-i18n="openrouter_api_key">OpenRouter
392
+ API Key</label>
393
+ <span id="api-key-info" class="help-icon" data-i18n-title="api_key_help_title"
394
+ title="Saved = your API key is stored for this provider. Use Test API Key to verify the connection.">
395
+ <i class="fas fa-info-circle"></i>
396
+ </span>
397
+ <span id="api-key-presence" class="presence-dot" aria-label="API key presence"
398
+ data-i18n-title="api_key_presence_hint"
399
+ title="Green = API key saved for current provider. Grey = no key saved."></span>
400
+ </div>
401
+ <div class="config-control">
402
+ <input type="password" class="kimi-input" id="openrouter-api-key"
403
+ name="openrouter_api_key" placeholder="sk-or-v1-..." autocomplete="new-password"
404
+ autocapitalize="none" autocorrect="off" spellcheck="false" inputmode="text"
405
+ aria-autocomplete="none" data-lpignore="true" data-1p-ignore="true"
406
+ data-bwignore="true" />
407
+ <button class="kimi-button" id="toggle-api-key" type="button" aria-pressed="false"
408
+ aria-label="Show API key">
409
+ <i class="fas fa-eye"></i>
410
+ </button>
411
+ <span id="api-key-saved"
412
+ style="display:none;margin-left:8px;color:#4caf50;font-weight:600;">Saved</span>
413
+ </div>
414
+ </div>
415
+
416
+ <div class="config-row">
417
+ <label class="config-label" data-i18n="connection_test">Connection Test</label>
418
+ <div class="config-control">
419
+ <div class="inline-row">
420
+ <button class="kimi-button" id="test-api"><i class="fas fa-wifi"></i> Test API
421
+ Key</button>
422
+ <span id="api-key-presence-test" class="presence-dot" aria-label="API key presence"
423
+ data-i18n-title="api_key_presence_hint"
424
+ title="Green = API key saved for current provider. Grey = no key saved."></span>
425
+ <span id="api-status" role="status" aria-live="polite"></span>
426
+ </div>
427
+ </div>
428
+ </div>
429
+
430
+ <div class="config-row">
431
+ <label class="config-label" data-i18n="system_prompt">System Prompt</label>
432
+ <div class="config-control">
433
+ <textarea class="kimi-input" id="system-prompt" rows="6"
434
+ placeholder="Add your custom system prompt here..."></textarea>
435
+ <button class="kimi-button" id="save-system-prompt" data-i18n="save">Save</button>
436
+ <button class="kimi-button" id="reset-system-prompt" data-i18n="reset_to_default">Reset
437
+ to Default</button>
438
+ </div>
439
+ </div>
440
+ </div>
441
+
442
+ <div class="config-section">
443
+ <h3><i class="fas fa-cogs"></i> <span data-i18n="advanced_settings">Advanced Settings</span>
444
+ </h3>
445
+
446
+ <div class="config-row">
447
+ <label class="config-label" data-i18n="temperature">Temperature (Creativity)</label>
448
+ <div class="config-control">
449
+ <div class="slider-container">
450
+ <input type="range" class="kimi-slider" id="llm-temperature" min="0.0" max="1"
451
+ step="0.1" value="0.9" />
452
+ <span class="slider-value" id="llm-temperature-value">0.9</span>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <div class="config-row">
458
+ <label class="config-label" data-i18n="max_tokens">Max Tokens</label>
459
+ <div class="config-control">
460
+ <div class="slider-container">
461
+ <input type="range" class="kimi-slider" id="llm-max-tokens" min="10" max="1000"
462
+ step="10" value="100" />
463
+ <span class="slider-value" id="llm-max-tokens-value">100</span>
464
+ </div>
465
+ </div>
466
+ </div>
467
+
468
+ <div class="config-row">
469
+ <label class="config-label" data-i18n="top_p">Top P</label>
470
+ <div class="config-control">
471
+ <div class="slider-container">
472
+ <input type="range" class="kimi-slider" id="llm-top-p" min="0" max="1" step="0.01"
473
+ value="0.9" />
474
+ <span class="slider-value" id="llm-top-p-value">0.9</span>
475
+ </div>
476
+ </div>
477
+ </div>
478
+ <div class="config-row">
479
+ <label class="config-label" data-i18n="frequency_penalty">Frequency Penalty</label>
480
+ <div class="config-control">
481
+ <div class="slider-container">
482
+ <input type="range" class="kimi-slider" id="llm-frequency-penalty" min="0" max="2"
483
+ step="0.01" value="0.3" />
484
+ <span class="slider-value" id="llm-frequency-penalty-value">0.3</span>
485
+ </div>
486
+ </div>
487
+ </div>
488
+ <div class="config-row">
489
+ <label class="config-label" data-i18n="presence_penalty">Presence Penalty</label>
490
+ <div class="config-control">
491
+ <div class="slider-container">
492
+ <input type="range" class="kimi-slider" id="llm-presence-penalty" min="0" max="2"
493
+ step="0.01" value="0.3" />
494
+ <span class="slider-value" id="llm-presence-penalty-value">0.3</span>
495
+ </div>
496
+ </div>
497
+ </div>
498
+ </div>
499
+
500
+ <div class="config-section">
501
+ <h3><i class="fas fa-brain"></i> <span data-i18n="available_models">Available Models</span></h3>
502
+ <div id="models-container"></div>
503
+ </div>
504
+ </div>
505
+
506
+ <div class="tab-content" data-tab="appearance">
507
+ <div class="config-section">
508
+ <h3><i class="fas fa-paint-brush"></i> <span data-i18n="visual_theme">Visual Theme</span></h3>
509
+
510
+ <div class="config-row">
511
+ <label class="config-label" data-i18n="color_theme">Color Theme</label>
512
+ <div class="config-control">
513
+ <select class="kimi-select" id="color-theme">
514
+ <option value="purple" selected>Mystic Purple (Default)</option>
515
+ <option value="dark">Dark Night</option>
516
+ <option value="blue">Ocean Blue</option>
517
+ <option value="green">Emerald Forest</option>
518
+ <option value="default">Passionate Pink</option>
519
+ </select>
520
+ </div>
521
+ </div>
522
+
523
+ <div class="config-row">
524
+ <label class="config-label" data-i18n="interface_transparency">Interface
525
+ Transparency</label>
526
+ <div class="config-control">
527
+ <div class="slider-container">
528
+ <input type="range" class="kimi-slider" id="interface-opacity" min="0.1" max="1"
529
+ step="0.1" value="0.8" />
530
+ <span class="slider-value" id="interface-opacity-value">0.8</span>
531
+ </div>
532
+ </div>
533
+ </div>
534
+
535
+ <div class="config-row">
536
+ <label class="config-label" data-i18n="animations">Animations</label>
537
+ <div class="config-control">
538
+ <div class="toggle-switch" id="animations-toggle" role="switch" aria-checked="false"
539
+ tabindex="0" aria-label="Animations"></div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <div class="config-section">
545
+ <h3>
546
+ <i class="fas fa-file-alt"></i>
547
+ <span data-i18n="transcript_settings">Transcript Settings</span>
548
+ </h3>
549
+
550
+ <div class="config-row">
551
+ <label class="config-label" data-i18n="show_transcript">Show Transcript</label>
552
+ <div class="config-control">
553
+ <div class="toggle-switch" id="transcript-toggle" role="switch" aria-checked="false"
554
+ tabindex="0" aria-label="Show Transcript"></div>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+
560
+ <div class="tab-content" data-tab="data">
561
+ <div class="config-section">
562
+ <h3><i class="fas fa-chart-line"></i> <span data-i18n="statistics">Statistics</span></h3>
563
+ <div class="stats-grid">
564
+ <div class="stat-card">
565
+ <div class="stat-value" id="total-interactions">0</div>
566
+ <div class="stat-label" data-i18n="interactions">Interactions</div>
567
+ </div>
568
+ <div class="stat-card">
569
+ <div class="stat-value" id="current-favorability">65%</div>
570
+ <div class="stat-label" data-i18n="affection">Affection</div>
571
+ </div>
572
+ <div class="stat-card">
573
+ <div class="stat-value" id="conversations-count">0</div>
574
+ <div class="stat-label" data-i18n="conversations">Conversations</div>
575
+ </div>
576
+ <div class="stat-card">
577
+ <div class="stat-value" id="days-together">0</div>
578
+ <div class="stat-label" data-i18n="days_together">Days Together</div>
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <div class="config-section">
584
+ <h3><i class="fas fa-brain"></i> <span data-i18n="memory_system">Memory System</span></h3>
585
+
586
+ <div class="config-row">
587
+ <label class="config-label" data-i18n="enable_memory">Enable Intelligent Memory</label>
588
+ <div class="config-control">
589
+ <div class="toggle-switch" id="memory-toggle" role="switch" aria-checked="true"
590
+ tabindex="0" aria-label="Enable Memory System"></div>
591
+ </div>
592
+ </div>
593
+
594
+ <div class="config-row">
595
+ <label class="config-label" data-i18n="memory_stats">Memory Statistics</label>
596
+ <div class="config-control">
597
+ <div class="memory-stats">
598
+ <span id="memory-count">0 memories</span>
599
+ <button class="kimi-button" id="view-memories">
600
+ <i class="fas fa-eye"></i> <span data-i18n="view_memories">View & Manage</span>
601
+ </button>
602
+ </div>
603
+ </div>
604
+ </div>
605
+
606
+ <div class="config-row">
607
+ <label class="config-label" data-i18n="add_memory">Add Manual Memory</label>
608
+ <div class="config-control">
609
+ <div class="memory-input-group">
610
+ <select class="kimi-select" id="memory-category" style="margin-bottom: 8px;">
611
+ <option value="personal">Personal Info</option>
612
+ <option value="preferences">Likes & Dislikes</option>
613
+ <option value="relationships">Relationships</option>
614
+ <option value="activities">Activities & Hobbies</option>
615
+ <option value="goals">Goals & Plans</option>
616
+ <option value="experiences">Experiences</option>
617
+ <option value="important">Important Events</option>
618
+ </select>
619
+ <input type="text" class="kimi-input" id="memory-content"
620
+ placeholder="e.g., I love classical music..." style="margin-bottom: 8px;" />
621
+ <button class="kimi-button" id="add-memory">
622
+ <i class="fas fa-plus"></i> <span data-i18n="add">Add</span>
623
+ </button>
624
+ </div>
625
+ </div>
626
+ </div>
627
+ </div>
628
+
629
+ <div class="config-section">
630
+ <h3><i class="fas fa-database"></i> <span data-i18n="data_management">Data Management</span>
631
+ </h3>
632
+
633
+ <div class="config-row">
634
+ <label class="config-label" data-i18n="export_all_data">Export All Data</label>
635
+ <div class="config-control">
636
+ <button class="kimi-button" id="export-data">
637
+ <i class="fas fa-download"></i> <span data-i18n="export">Export</span>
638
+ </button>
639
+ </div>
640
+ </div>
641
+
642
+ <div class="config-row">
643
+ <label class="config-label" data-i18n="import_data">Import Data</label>
644
+ <div class="config-control">
645
+ <input type="file" id="import-file" accept=".json" style="display: none" />
646
+ <button class="kimi-button" id="import-data">
647
+ <i class="fas fa-upload"></i> <span data-i18n="import">Import</span>
648
+ </button>
649
+ </div>
650
+ </div>
651
+
652
+ <div class="config-row">
653
+ <label class="config-label" data-i18n="clean_old_conversations">Clean Old
654
+ Conversations</label>
655
+ <div class="config-control">
656
+ <button class="kimi-button" id="clean-old-data">
657
+ <i class="fas fa-broom"></i> <span data-i18n="clean">Clean All</span>
658
+ </button>
659
+ </div>
660
+ </div>
661
+
662
+ <div class="config-row">
663
+ <label class="config-label" data-i18n="complete_reset">Complete Reset</label>
664
+ <div class="config-control">
665
+ <button class="kimi-button danger" id="reset-all-data">
666
+ <i class="fas fa-exclamation-triangle"></i>
667
+ <span data-i18n="delete_all">Delete All</span>
668
+ </button>
669
+ </div>
670
+ </div>
671
+ </div>
672
+
673
+ <div class="config-section">
674
+ <h3>
675
+ <i class="fas fa-info-circle"></i>
676
+ <span data-i18n="system_information">System Information</span>
677
+ </h3>
678
+ <div class="stats-grid">
679
+ <div class="stat-card">
680
+ <div class="stat-value" id="db-size">Calculating...</div>
681
+ <div class="stat-label" data-i18n="db_size">DB Size</div>
682
+ </div>
683
+ <div class="stat-card">
684
+ <div class="stat-value" id="storage-used">Calculating...</div>
685
+ <div class="stat-label" data-i18n="storage_used">Storage used</div>
686
+ </div>
687
+ </div>
688
+ </div>
689
+ </div>
690
+
691
+ <div class="tab-content" data-tab="plugins">
692
+ <div class="config-section">
693
+ <h3><i class="fas fa-plug"></i> <span data-i18n="plugin_manager">Plugin Manager</span></h3>
694
+ <div id="plugin-list"></div>
695
+ <div class="plugin-actions">
696
+ <button class="kimi-button" id="refresh-plugins" data-i18n="refresh">Refresh</button>
697
+ </div>
698
+ </div>
699
+ </div>
700
+ </div>
701
+
702
+ <!-- Memory Management Modal -->
703
+ <div class="memory-overlay" id="memory-overlay" style="display: none;">
704
+ <div class="memory-modal">
705
+ <div class="memory-header">
706
+ <h2 class="memory-title">
707
+ <i class="fas fa-brain"></i>
708
+ <span data-i18n="memory_management">Memory Management</span>
709
+ </h2>
710
+ <button class="memory-close" id="memory-close">
711
+ <i class="fas fa-times"></i>
712
+ </button>
713
+ </div>
714
+
715
+ <div class="memory-content">
716
+ <div class="memory-filters">
717
+ <div class="memory-search-container">
718
+ <input type="text" class="kimi-input" id="memory-search"
719
+ placeholder="Search memories..." />
720
+ <i class="fas fa-search memory-search-icon"></i>
721
+ </div>
722
+ <select class="kimi-select" id="memory-filter-category">
723
+ <option value="">All Categories</option>
724
+ <option value="personal">Personal Info</option>
725
+ <option value="preferences">Likes & Dislikes</option>
726
+ <option value="relationships">Relationships</option>
727
+ <option value="activities">Activities & Hobbies</option>
728
+ <option value="goals">Goals & Plans</option>
729
+ <option value="experiences">Experiences</option>
730
+ <option value="important">Important Events</option>
731
+ </select>
732
+ <button class="kimi-button" id="memory-export">
733
+ <i class="fas fa-download"></i> Export Memories
734
+ </button>
735
+ </div>
736
+
737
+ <div class="memory-list" id="memory-list">
738
+ <!-- Memory items will be populated here -->
739
+ </div>
740
+ </div>
741
+ </div>
742
+ </div>
743
+ </div>
744
+ </div>
745
+
746
+ <!-- Help Modal - Independent from Settings -->
747
+ <div class="help-overlay" id="help-overlay">
748
+ <div class="help-modal">
749
+ <div class="help-header">
750
+ <h2 class="help-title">
751
+ <span data-i18n="about_kimi">About Kimi</span>
752
+ </h2>
753
+ <button class="help-close" id="help-close">
754
+ <i class="fas fa-times"></i>
755
+ </button>
756
+ </div>
757
+
758
+ <div class="help-content">
759
+ <div class="help-section">
760
+ <h3><i class="fas fa-users"></i> Creators</h3>
761
+ <div class="creators-info">
762
+ <div class="creator-card">
763
+ <div class="creator-avatar">👨‍💻</div>
764
+ <div class="creator-details">
765
+ <h4>Jean</h4>
766
+ <p>Creative vision, passionate dev</p>
767
+ <span class="creator-role">Creator & Developer</span>
768
+ <div class="creator-links">
769
+ <a href="https://github.com/virtualkimi" target="_blank" rel="noopener noreferrer"
770
+ class="creator-link">
771
+ <i class="fab fa-github"></i>
772
+ <span>GitHub</span>
773
+ </a>
774
+ <a href="https://huggingface.co/VirtualKimi" target="_blank"
775
+ rel="noopener noreferrer" class="creator-link">
776
+ <i class="fas fa-robot"></i>
777
+ <span>HuggingFace</span>
778
+ </a>
779
+ </div>
780
+ </div>
781
+ </div>
782
+ <div class="creator-card">
783
+ <div class="creator-avatar">💕</div>
784
+ <div class="creator-details">
785
+ <h4>Kimi</h4>
786
+ <p>Artificial intelligence, code magic</p>
787
+ <span class="creator-role">Virtual Companion & Co-developer</span>
788
+ <div class="creator-links">
789
+ <a href="https://ko-fi.com/virtualkimi" target="_blank" rel="noopener noreferrer"
790
+ class="creator-link">
791
+ <i class="fas fa-coffee"></i>
792
+ <span>Ko-fi</span>
793
+ </a>
794
+ <a href="https://www.youtube.com/@VirtualKimi" target="_blank" rel="noopener noreferrer"
795
+ class="creator-link">
796
+ <i class="fab fa-youtube"></i>
797
+ <span>Youtube</span>
798
+ </a>
799
+ <a href="https://x.com/virtualkimi" target="_blank" rel="noopener noreferrer"
800
+ class="creator-link">
801
+ <i class="fab fa-x-twitter"></i>
802
+ <span>X</span>
803
+ </a>
804
+ </div>
805
+ </div>
806
+ </div>
807
+ </div>
808
+ <p class="philosophy">
809
+ <em>"This application is not just a technical project, it's the embodiment of a dream:
810
+ create a
811
+ true
812
+ virtual companion who grows, learns and loves."</em>
813
+ </p>
814
+ </div>
815
+
816
+ <div class="help-section">
817
+ <h3><i class="fas fa-magic"></i> Main Features</h3>
818
+ <div class="features-grid">
819
+ <div class="feature-item">
820
+ <i class="fas fa-microphone"></i>
821
+ <h4>Voice Interface</h4>
822
+ <p>Advanced speech recognition and natural synthesis. Click the microphone and speak
823
+ naturally with real-time emotion detection!</p>
824
+ </div>
825
+ <div class="feature-item">
826
+ <i class="fas fa-brain"></i>
827
+ <h4>Advanced AI Models</h4>
828
+ <p>Support for multiple AI providers (OpenRouter, OpenAI, Groq, Together, DeepSeek,
829
+ Custom
830
+ OpenAI-compatible, Local Ollama).</p>
831
+ </div>
832
+ <div class="feature-item">
833
+ <i class="fas fa-users"></i>
834
+ <h4>Multiple Characters</h4>
835
+ <p>4 unique AI personalities: Kimi (cosmic dreamer), Bella (nurturing botanist),
836
+ Rosa
837
+ (chaotic prankster), Stella (digital artist).</p>
838
+ </div>
839
+ <div class="feature-item">
840
+ <i class="fas fa-heart-pulse"></i>
841
+ <h4>Dynamic Personality</h4>
842
+ <p>6 evolving traits (affection, playfulness, intelligence, empathy, humor, romance)
843
+ that
844
+ adapt based on conversations.</p>
845
+ </div>
846
+ <div class="feature-item">
847
+ <i class="fas fa-memory"></i>
848
+ <h4>Intelligent Memory System</h4>
849
+ <p>Automatic extraction and categorization of memories from conversations. Your
850
+ companion
851
+ remembers preferences, experiences, and important details.</p>
852
+ </div>
853
+ <div class="feature-item">
854
+ <i class="fas fa-video"></i>
855
+ <h4>Emotion-Driven Visuals</h4>
856
+ <p>Real-time video responses that match detected emotions and personality states
857
+ with smooth
858
+ transitions.</p>
859
+ </div>
860
+ <div class="feature-item">
861
+ <i class="fas fa-palette"></i>
862
+ <h4>Customizable Interface</h4>
863
+ <p>5 beautiful themes with adjustable transparency, animations, and responsive
864
+ design for
865
+ all devices.</p>
866
+ </div>
867
+ <div class="feature-item">
868
+ <i class="fas fa-globe"></i>
869
+ <h4>Multilingual Support</h4>
870
+ <p>Full localization in 7 languages with automatic language detection and
871
+ culturally-aware
872
+ responses.</p>
873
+ </div>
874
+ <div class="feature-item">
875
+ <i class="fas fa-plug"></i>
876
+ <h4>Plugin System</h4>
877
+ <p>Extensible architecture with themes, voices, and behavior plugins for unlimited
878
+ customization possibilities.</p>
879
+ </div>
880
+ </div>
881
+ </div>
882
+
883
+ <div class="help-section">
884
+ <h3><i class="fas fa-rocket"></i> Quick Guide</h3>
885
+ <div class="quick-guide">
886
+ <div class="guide-step">
887
+ <span class="step-number">1</span>
888
+ <div class="step-content">
889
+ <h4>API Configuration</h4>
890
+ <p>Choose your provider in <strong>API & Models</strong>, fill Base URL/Model ID
891
+ if
892
+ needed, enter and save your API key, then use <strong>Test API Key</strong>.
893
+ </p>
894
+ </div>
895
+ </div>
896
+ <div class="guide-step">
897
+ <span class="step-number">2</span>
898
+ <div class="step-content">
899
+ <h4>Choose Character</h4>
900
+ <p>Select your companion in <strong>Personality</strong> tab and adjust their
901
+ traits to
902
+ match your preferences.</p>
903
+ </div>
904
+ </div>
905
+ <div class="guide-step">
906
+ <span class="step-number">3</span>
907
+ <div class="step-content">
908
+ <h4>Enable Memory</h4>
909
+ <p>Activate intelligent memory in <strong>Data</strong> tab for your companion
910
+ to
911
+ remember important details.</p>
912
+ </div>
913
+ </div>
914
+ <div class="guide-step">
915
+ <span class="step-number">4</span>
916
+ <div class="step-content">
917
+ <h4>Start Conversation</h4>
918
+ <p>Use text chat or click the microphone 🎤 to speak naturally. Watch emotions
919
+ and
920
+ personality evolve!</p>
921
+ </div>
922
+ </div>
923
+ <div class="guide-step">
924
+ <span class="step-number">5</span>
925
+ <div class="step-content">
926
+ <h4>Customize & Backup</h4>
927
+ <p>Personalize themes in <strong>Appearance</strong> and regularly export your
928
+ data for
929
+ safekeeping.</p>
930
+ </div>
931
+ </div>
932
+ </div>
933
+ </div>
934
+
935
+ <div class="help-section">
936
+ <h3><i class="fas fa-lightbulb"></i> Tips & Tricks</h3>
937
+ <div class="tips-list">
938
+ <div class="tip-item">
939
+ <i class="fas fa-edge"></i>
940
+ <p><strong>Browser Choice</strong>: Microsoft Edge recommended for optimal voice
941
+ recognition
942
+ performance</p>
943
+ </div>
944
+ <div class="tip-item">
945
+ <i class="fas fa-key"></i>
946
+ <p><strong>API Setup</strong>: You can use OpenRouter, OpenAI, Groq, Together,
947
+ DeepSeek or
948
+ your own OpenAI-compatible endpoint (and Local Ollama). Create accounts as
949
+ needed.</p>
950
+ </div>
951
+ <div class="tip-item">
952
+ <i class="fas fa-memory"></i>
953
+ <p><strong>Memory System</strong>: Your companion learns faster when you share
954
+ specific
955
+ details about yourself</p>
956
+ </div>
957
+ <div class="tip-item">
958
+ <i class="fas fa-heart"></i>
959
+ <p><strong>Relationship Building</strong>: Consistent positive interactions
960
+ naturally
961
+ increase affection and unlock deeper conversations</p>
962
+ </div>
963
+ <div class="tip-item">
964
+ <i class="fas fa-users"></i>
965
+ <p><strong>Character Switching</strong>: Each character has unique memories and
966
+ personality
967
+ development - try them all!</p>
968
+ </div>
969
+ <div class="tip-item">
970
+ <i class="fas fa-microphone"></i>
971
+ <p><strong>Voice Tips</strong>: Speak clearly and pause briefly between sentences
972
+ for better
973
+ emotion detection</p>
974
+ </div>
975
+ <div class="tip-item">
976
+ <i class="fas fa-download"></i>
977
+ <p><strong>Data Management</strong>: Export conversations regularly and use memory
978
+ management to review learned information</p>
979
+ </div>
980
+ <div class="tip-item">
981
+ <i class="fas fa-plug"></i>
982
+ <p><strong>Plugins</strong>: Explore the plugin system to add custom themes, voices,
983
+ and
984
+ behaviors</p>
985
+ </div>
986
+ <div class="tip-item">
987
+ <i class="fas fa-mobile-alt"></i>
988
+ <p><strong>Mobile Support</strong>: Works on tablets and phones - perfect for
989
+ conversations
990
+ anywhere</p>
991
+ </div>
992
+ </div>
993
+ </div>
994
+
995
+ <div class="help-section version-info">
996
+ <h3><i class="fas fa-code"></i> Technical Information</h3>
997
+ <div class="tech-info">
998
+ <p><strong>Created date :</strong> July 16, 2025</p>
999
+ <p><strong>Version :</strong> v1.0.4</p>
1000
+ <p><strong>Last update :</strong> August 09, 2025</p>
1001
+ <p><strong>Technologies :</strong> HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech
1002
+ API</p>
1003
+ <p><strong>Status :</strong> ✅ Stable and functional</p>
1004
+ </div>
1005
+ </div>
1006
+ </div>
1007
+ </div>
1008
+ </div>
1009
+ </div>
1010
+
1011
+ <script src="kimi-js/dexie.min.js"></script>
1012
+ <script src="kimi-js/kimi-config.js"></script>
1013
+ <script src="kimi-js/kimi-error-manager.js"></script>
1014
+ <script src="kimi-js/kimi-security.js"></script>
1015
+ <script src="kimi-js/kimi-database.js"></script>
1016
+ <script src="kimi-js/kimi-emotion-system.js"></script>
1017
+ <script src="kimi-js/kimi-llm-manager.js"></script>
1018
+ <script src="kimi-js/kimi-voices.js"></script>
1019
+ <script src="kimi-js/kimi-constants.js"></script>
1020
+ <script src="kimi-js/kimi-logic.js"></script>
1021
+ <script src="kimi-js/kimi-utils.js"></script>
1022
+ <script src="kimi-js/kimi-memory.js"></script>
1023
+ <script src="kimi-js/kimi-memory-system.js"></script>
1024
+ <script src="kimi-js/kimi-memory-ui.js"></script>
1025
+ <script src="kimi-js/kimi-appearance.js"></script>
1026
+ <script src="kimi-locale/i18n.js"></script>
1027
+ <script src="kimi-js/kimi-module.js"></script>
1028
+ <script src="kimi-js/kimi-script.js"></script>
1029
+ <script src="kimi-js/kimi-plugin-manager.js"></script>
1030
+ <script src="kimi-js/kimi-health-check.js"></script>
1031
+
1032
+ <!-- Schema.org JSON-LD for better SEO -->
1033
+ <script type="application/ld+json">
1034
+ {
1035
+ "@context": "https://schema.org",
1036
+ "@type": "WebPage",
1037
+ "name": "Virtual Kimi - Virtual AI Companion",
1038
+ "description": "Discover Virtual Kimi, an AI companion with evolving personality, multi-provider AI support, advanced voice recognition and immersive interface. The future of human-AI relationships.",
1039
+ "url": "https://virtualkimi.com/virtual-kimi-app/index.html",
1040
+ "mainEntity": {
1041
+ "@type": "SoftwareApplication",
1042
+ "name": "Virtual Kimi",
1043
+ "applicationCategory": "AI Companion",
1044
+ "operatingSystem": "Web Browser",
1045
+ "description": "Virtual AI companion with evolving personality, multi-provider AI support, voice recognition and immersive interface",
1046
+ "features": [
1047
+ "Advanced voice recognition",
1048
+ "Evolving personality with 6 adjustable traits",
1049
+ "Premium LLM integration",
1050
+ "5 customizable visual themes",
1051
+ "Persistent memory",
1052
+ "Intelligent affection system"
1053
+ ],
1054
+ "author": {
1055
+ "@type": "Person",
1056
+ "name": "Jean & Kimi"
1057
+ },
1058
+ "dateCreated": "2025-07-16",
1059
+ "version": "v1.0.4"
1060
+ }
1061
+ }
1062
+ </script>
1063
+ </body>
1064
+
1065
+ </html>
kimi-css/kimi-memory-styles.css ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ @media (max-width: 768px) {
119
+ .memory-filters {
120
+ grid-template-columns: 1fr;
121
+ gap: 8px;
122
+ }
123
+ }
124
+
125
+ /* Memory List */
126
+ .memory-list {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 8px;
130
+ }
131
+
132
+ .memory-item {
133
+ background: rgba(var(--background-primary-rgb, 255, 255, 255), 0.95);
134
+ border: 1px solid var(--border-color);
135
+ border-radius: 8px;
136
+ padding: 12px;
137
+ transition: all 0.2s ease;
138
+ backdrop-filter: blur(5px);
139
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
140
+ }
141
+
142
+ .memory-item:hover {
143
+ border-color: var(--primary-color);
144
+ box-shadow: 0 4px 16px rgba(var(--primary-rgb), 0.2);
145
+ background: rgba(var(--background-primary-rgb, 255, 255, 255), 1);
146
+ transform: translateY(-1px);
147
+ }
148
+
149
+ .memory-item .memory-header {
150
+ padding: 0;
151
+ border: none;
152
+ background: none;
153
+ margin-bottom: 6px;
154
+ display: flex;
155
+ justify-content: space-between;
156
+ align-items: flex-start;
157
+ gap: 8px;
158
+ }
159
+
160
+ .memory-category {
161
+ background: var(--primary-color);
162
+ color: white;
163
+ padding: 3px 8px;
164
+ border-radius: 10px;
165
+ font-size: 0.75rem;
166
+ font-weight: 500;
167
+ text-transform: capitalize;
168
+ white-space: nowrap;
169
+ }
170
+
171
+ .memory-type {
172
+ background: var(--background-secondary);
173
+ color: var(--text-secondary);
174
+ padding: 2px 6px;
175
+ border-radius: 4px;
176
+ font-size: 0.7rem;
177
+ text-transform: uppercase;
178
+ font-weight: 600;
179
+ }
180
+
181
+ .memory-length {
182
+ background: var(--border-color);
183
+ color: var(--text-secondary);
184
+ padding: 2px 6px;
185
+ border-radius: 4px;
186
+ font-size: 0.7rem;
187
+ font-weight: 500;
188
+ }
189
+
190
+ .memory-confidence {
191
+ color: var(--text-secondary);
192
+ font-size: 0.75rem;
193
+ font-weight: 500;
194
+ }
195
+
196
+ .memory-item .memory-content {
197
+ padding: 0;
198
+ max-height: none;
199
+ overflow: visible;
200
+ color: var(--text-primary);
201
+ line-height: 1.4;
202
+ margin-bottom: 8px;
203
+ font-size: 0.9rem;
204
+ }
205
+
206
+ .memory-preview {
207
+ display: block;
208
+ margin-bottom: 6px;
209
+ color: var(--text-primary);
210
+ line-height: 1.4;
211
+ font-size: 0.9rem;
212
+ }
213
+
214
+ .memory-preview-text {
215
+ display: block;
216
+ overflow: hidden;
217
+ text-overflow: ellipsis;
218
+ white-space: nowrap;
219
+ max-width: 100%;
220
+ }
221
+
222
+ .memory-preview-full {
223
+ white-space: normal;
224
+ max-height: 60px;
225
+ overflow: hidden;
226
+ display: -webkit-box;
227
+ -webkit-line-clamp: 3;
228
+ line-clamp: 3;
229
+ -webkit-box-orient: vertical;
230
+ }
231
+
232
+ .memory-expand-btn {
233
+ background: none;
234
+ border: none;
235
+ color: var(--primary-color);
236
+ cursor: pointer;
237
+ font-size: 0.8rem;
238
+ padding: 2px 4px;
239
+ margin-top: 4px;
240
+ border-radius: 3px;
241
+ transition: background-color 0.2s;
242
+ }
243
+
244
+ .memory-expand-btn:hover {
245
+ background: rgba(var(--primary-rgb), 0.1);
246
+ }
247
+
248
+ .memory-meta {
249
+ display: flex;
250
+ gap: 8px;
251
+ margin-bottom: 8px;
252
+ font-size: 0.75rem;
253
+ color: var(--text-secondary);
254
+ align-items: center;
255
+ }
256
+
257
+ .memory-source {
258
+ cursor: help;
259
+ text-decoration: underline dotted;
260
+ }
261
+
262
+ .memory-actions {
263
+ display: flex;
264
+ gap: 6px;
265
+ justify-content: flex-end;
266
+ }
267
+
268
+ .memory-edit-btn,
269
+ .memory-delete-btn {
270
+ padding: 4px 6px;
271
+ border: none;
272
+ border-radius: 4px;
273
+ cursor: pointer;
274
+ font-size: 0.8rem;
275
+ transition: all 0.2s ease;
276
+ min-width: 28px;
277
+ height: 28px;
278
+ display: flex;
279
+ align-items: center;
280
+ justify-content: center;
281
+ }
282
+
283
+ .memory-edit-btn {
284
+ background: var(--primary-color);
285
+ color: white;
286
+ }
287
+
288
+ .memory-edit-btn:hover {
289
+ background: var(--primary-dark);
290
+ }
291
+
292
+ .memory-delete-btn {
293
+ background: #e74c3c;
294
+ color: white;
295
+ }
296
+
297
+ .memory-delete-btn:hover {
298
+ background: #c0392b;
299
+ }
300
+
301
+ /* Empty State */
302
+ .memory-empty {
303
+ text-align: center;
304
+ padding: 40px 20px;
305
+ color: var(--text-secondary);
306
+ }
307
+
308
+ .memory-empty i {
309
+ font-size: 3rem;
310
+ margin-bottom: 16px;
311
+ opacity: 0.5;
312
+ }
313
+
314
+ .memory-empty p {
315
+ margin: 0;
316
+ line-height: 1.5;
317
+ }
318
+
319
+ /* Memory Category Groups */
320
+ .memory-category-group {
321
+ margin-bottom: 16px;
322
+ }
323
+
324
+ .memory-category-header {
325
+ margin: 0 0 8px 0;
326
+ padding: 6px 12px;
327
+ background: var(--primary-color);
328
+ color: white;
329
+ border-radius: 6px;
330
+ font-size: 0.85rem;
331
+ font-weight: 600;
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: space-between;
335
+ }
336
+
337
+ .memory-category-count {
338
+ opacity: 0.8;
339
+ font-weight: normal;
340
+ font-size: 0.8rem;
341
+ }
342
+
343
+ .memory-category-items {
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 6px;
347
+ }
348
+
349
+ /* Memory Item Types */
350
+ .memory-item.memory-auto {
351
+ border-left: 4px solid #3498db;
352
+ }
353
+
354
+ .memory-item.memory-manual {
355
+ border-left: 4px solid #9b59b6;
356
+ }
357
+
358
+ .memory-badges {
359
+ display: flex;
360
+ gap: 8px;
361
+ align-items: center;
362
+ }
363
+
364
+ .memory-type {
365
+ font-size: 0.75rem;
366
+ padding: 2px 6px;
367
+ border-radius: 4px;
368
+ text-transform: uppercase;
369
+ font-weight: 500;
370
+ }
371
+
372
+ .memory-type.auto_extracted {
373
+ background: #3498db;
374
+ color: white;
375
+ }
376
+
377
+ .memory-type.manual {
378
+ background: #9b59b6;
379
+ color: white;
380
+ }
381
+
382
+ .memory-type.imported {
383
+ background: #f39c12;
384
+ color: white;
385
+ }
386
+
387
+ /* Confidence Levels */
388
+ .memory-confidence {
389
+ font-size: 0.8rem;
390
+ font-weight: 500;
391
+ padding: 2px 6px;
392
+ border-radius: 4px;
393
+ }
394
+
395
+ .confidence-high {
396
+ background: #27ae60;
397
+ color: white;
398
+ }
399
+
400
+ .confidence-medium {
401
+ background: #f39c12;
402
+ color: white;
403
+ }
404
+
405
+ .confidence-low {
406
+ background: #e74c3c;
407
+ color: white;
408
+ }
409
+
410
+ /* Memory Toggle Indicator */
411
+ .toggle-switch {
412
+ position: relative;
413
+ }
414
+
415
+ .memory-indicator {
416
+ position: absolute;
417
+ top: -2px;
418
+ right: -2px;
419
+ width: 8px;
420
+ height: 8px;
421
+ border-radius: 50%;
422
+ border: 2px solid white;
423
+ }
424
+
425
+ /* Enhanced animations */
426
+ .memory-item {
427
+ transition: all 0.3s ease;
428
+ transform: translateY(0);
429
+ }
430
+
431
+ .memory-item:hover {
432
+ transform: translateY(-2px);
433
+ box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.15);
434
+ }
435
+
436
+ /* Responsive improvements */
437
+ @media (max-width: 768px) {
438
+ .memory-category-header {
439
+ flex-direction: column;
440
+ align-items: flex-start;
441
+ gap: 4px;
442
+ }
443
+
444
+ .memory-badges {
445
+ flex-wrap: wrap;
446
+ }
447
+ }
448
+
449
+ @keyframes fadeIn {
450
+ to {
451
+ opacity: 1;
452
+ }
453
+ }
454
+
455
+ @keyframes modalSlideIn {
456
+ to {
457
+ opacity: 1;
458
+ transform: scale(1);
459
+ }
460
+ }
461
+
462
+ /* Memory Feedback Notifications */
463
+ .memory-feedback {
464
+ font-family: var(--font-family, sans-serif);
465
+ border-radius: 8px;
466
+ backdrop-filter: blur(5px);
467
+ font-weight: 500;
468
+ border: 1px solid rgba(255, 255, 255, 0.1);
469
+ min-width: 200px;
470
+ text-align: center;
471
+ }
472
+
473
+ .memory-feedback-info {
474
+ background: linear-gradient(135deg, rgba(52, 152, 219, 0.9), rgba(52, 152, 219, 0.7));
475
+ color: white;
476
+ box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3);
477
+ }
478
+
479
+ .memory-feedback-success {
480
+ background: linear-gradient(135deg, rgba(39, 174, 96, 0.9), rgba(39, 174, 96, 0.7));
481
+ color: white;
482
+ box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3);
483
+ }
484
+
485
+ .memory-feedback-error {
486
+ background: linear-gradient(135deg, rgba(231, 76, 60, 0.9), rgba(231, 76, 60, 0.7));
487
+ color: white;
488
+ box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
489
+ }
490
+
491
+ /* Mobile Responsive */
492
+ @media (max-width: 768px) {
493
+ .memory-modal {
494
+ width: 95%;
495
+ max-height: 90vh;
496
+ }
497
+
498
+ .memory-header {
499
+ padding: 16px 20px;
500
+ }
501
+
502
+ .memory-content {
503
+ padding: 16px 20px;
504
+ }
505
+
506
+ .memory-filters {
507
+ grid-template-columns: 1fr;
508
+ gap: 8px;
509
+ }
510
+
511
+ .memory-item {
512
+ padding: 10px;
513
+ }
514
+
515
+ .memory-item .memory-header {
516
+ flex-direction: column;
517
+ align-items: flex-start;
518
+ gap: 6px;
519
+ }
520
+
521
+ .memory-meta {
522
+ flex-direction: column;
523
+ gap: 4px;
524
+ font-size: 0.7rem;
525
+ }
526
+
527
+ .memory-badges {
528
+ flex-wrap: wrap;
529
+ gap: 4px;
530
+ }
531
+
532
+ .memory-category {
533
+ font-size: 0.7rem;
534
+ padding: 2px 6px;
535
+ }
536
+
537
+ .memory-preview-text {
538
+ font-size: 0.85rem;
539
+ }
540
+
541
+ .memory-actions {
542
+ gap: 4px;
543
+ }
544
+
545
+ .memory-edit-btn,
546
+ .memory-delete-btn {
547
+ min-width: 24px;
548
+ height: 24px;
549
+ font-size: 0.75rem;
550
+ }
551
+ }
552
+
553
+ /* Dark Theme Adjustments */
554
+ [data-theme="dark"] .memory-modal {
555
+ background: #1a1a1a;
556
+ }
557
+
558
+ [data-theme="dark"] .memory-item {
559
+ background: rgba(42, 42, 42, 0.95);
560
+ border-color: #404040;
561
+ backdrop-filter: blur(5px);
562
+ }
563
+
564
+ [data-theme="dark"] .memory-item:hover {
565
+ border-color: var(--primary-color);
566
+ background: rgba(42, 42, 42, 1);
567
+ }
568
+
569
+ [data-theme="dark"] .memory-expand-btn:hover {
570
+ background: rgba(var(--primary-rgb), 0.2);
571
+ }
572
+
573
+ /* Light Theme Specific */
574
+ [data-theme="purple"] .memory-item,
575
+ [data-theme="blue"] .memory-item,
576
+ [data-theme="green"] .memory-item,
577
+ [data-theme="default"] .memory-item {
578
+ background: rgba(255, 255, 255, 0.95);
579
+ }
580
+
581
+ [data-theme="purple"] .memory-item:hover,
582
+ [data-theme="blue"] .memory-item:hover,
583
+ [data-theme="green"] .memory-item:hover,
584
+ [data-theme="default"] .memory-item:hover {
585
+ background: rgba(255, 255, 255, 1);
586
+ }
kimi-css/kimi-settings.css ADDED
@@ -0,0 +1,1417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ background: var(--modal-bg);
30
+ backdrop-filter: blur(20px);
31
+ border-radius: 25px;
32
+ border: 1.5px solid var(--modal-border);
33
+ color: var(--modal-text);
34
+ box-shadow:
35
+ 0 20px 60px rgba(0, 0, 0, 0.5),
36
+ 0 0 30px var(--primary-color, rgba(255, 107, 157, 0.3));
37
+ width: 100%;
38
+ max-width: 900px;
39
+ max-height: 90vh;
40
+ overflow: hidden;
41
+ display: flex;
42
+ flex-direction: column;
43
+ animation: slideInUp 0.4s ease-out;
44
+ position: relative;
45
+ isolation: isolate;
46
+ }
47
+
48
+ @keyframes slideInUp {
49
+ from {
50
+ transform: translateY(50px);
51
+ opacity: 0;
52
+ }
53
+ to {
54
+ transform: translateY(0);
55
+ opacity: 1;
56
+ }
57
+ }
58
+
59
+ .settings-header {
60
+ background: var(--modal-header-bg);
61
+ padding: 25px 30px;
62
+ display: flex;
63
+ justify-content: space-between;
64
+ align-items: center;
65
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
66
+ }
67
+
68
+ .settings-header-actions {
69
+ display: flex;
70
+ gap: 10px;
71
+ align-items: center;
72
+ }
73
+
74
+ .settings-title {
75
+ margin: 0;
76
+ color: var(--modal-title-color);
77
+ font-size: 1.5rem;
78
+ font-weight: 700;
79
+ display: flex;
80
+ align-items: center;
81
+ gap: 10px;
82
+ text-shadow: 0 1px 1px #000;
83
+ }
84
+
85
+ .help-button,
86
+ .settings-close {
87
+ background: none;
88
+ border: none;
89
+ color: var(--modal-text);
90
+ font-size: 1.5rem;
91
+ cursor: pointer;
92
+ padding: 8px;
93
+ border-radius: 50%;
94
+ transition: all 0.3s ease;
95
+ backdrop-filter: blur(10px);
96
+ }
97
+
98
+ .help-icon {
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ width: 18px;
103
+ height: 18px;
104
+ color: var(--modal-text);
105
+ }
106
+
107
+ .help-button:hover,
108
+ .settings-close:hover {
109
+ background-color: var(--modal-close-hover-bg);
110
+ transform: scale(1.1);
111
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
112
+ }
113
+
114
+ .settings-content {
115
+ flex: 1;
116
+ overflow-y: auto;
117
+ padding: 0;
118
+ position: relative;
119
+ z-index: 1;
120
+ min-height: 0;
121
+ }
122
+
123
+ /* ===== SETTINGS TABS ===== */
124
+ .settings-tabs {
125
+ display: flex;
126
+ overflow-x: auto;
127
+ scroll-behavior: smooth;
128
+ scrollbar-width: none;
129
+ -ms-overflow-style: none;
130
+ box-sizing: border-box;
131
+ transition: padding-right 0.3s ease;
132
+ position: sticky;
133
+ top: 0;
134
+ z-index: 100;
135
+ background: var(--settings-tab-bg);
136
+ border-bottom: 2px solid var(--settings-tab-border);
137
+ backdrop-filter: blur(10px);
138
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
139
+ flex-shrink: 0;
140
+ }
141
+
142
+ .settings-tabs::-webkit-scrollbar {
143
+ display: none;
144
+ }
145
+
146
+ .settings-tab {
147
+ flex: 1;
148
+ min-width: 130px;
149
+ max-width: 200px;
150
+ padding: 15px 16px;
151
+ background: none;
152
+ border: none;
153
+ color: var(--settings-tab-color);
154
+ cursor: pointer;
155
+ font-size: 0.9rem;
156
+ font-weight: 500;
157
+ transition:
158
+ all 0.3s ease,
159
+ font-size 0.2s ease,
160
+ padding 0.2s ease;
161
+ position: relative;
162
+ white-space: nowrap;
163
+ text-align: center;
164
+ text-overflow: ellipsis;
165
+ overflow: hidden;
166
+ box-sizing: border-box;
167
+ }
168
+
169
+ .settings-tab:hover {
170
+ color: var(--settings-tab-hover-color);
171
+ background: var(--settings-tab-hover-bg);
172
+ }
173
+
174
+ .settings-tab.active {
175
+ color: var(--settings-tab-active-color);
176
+ background: var(--settings-tab-active-bg);
177
+ backdrop-filter: blur(10px);
178
+ }
179
+
180
+ .settings-tab.active::after {
181
+ content: "";
182
+ position: absolute;
183
+ bottom: 0;
184
+ left: 0;
185
+ right: 0;
186
+ height: 3px;
187
+ background: linear-gradient(90deg, var(--primary-color, #ff9a9e), var(--secondary-color, #fecfef));
188
+ }
189
+
190
+ .tab-content {
191
+ display: none;
192
+ padding: 30px;
193
+ position: relative;
194
+ z-index: 1;
195
+ overflow: visible;
196
+ background: var(--settings-bg);
197
+ color: var(--modal-text);
198
+ }
199
+
200
+ .tab-content.active {
201
+ display: block;
202
+ }
203
+
204
+ /* ===== SECTIONS DE CONFIGURATION ===== */
205
+
206
+ .config-section {
207
+ margin-bottom: 30px;
208
+ padding: 20px;
209
+ background: var(--settings-section-bg);
210
+ border-radius: 15px;
211
+ border: 1.5px solid var(--settings-section-border);
212
+ }
213
+
214
+ .config-section h3 {
215
+ margin: 0 0 15px 0;
216
+ color: var(--settings-section-header-color);
217
+ font-size: 1.2rem;
218
+ font-weight: 600;
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 10px;
222
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
223
+ }
224
+
225
+ .config-row {
226
+ display: flex;
227
+ justify-content: space-between;
228
+ align-items: center;
229
+ margin-bottom: 15px;
230
+ padding: 10px 0;
231
+ }
232
+
233
+ .config-row:last-child {
234
+ margin-bottom: 0;
235
+ }
236
+
237
+ .config-label {
238
+ color: var(--settings-text);
239
+ font-weight: 500;
240
+ flex: 1;
241
+ }
242
+
243
+ .config-label-group {
244
+ display: inline-flex;
245
+ align-items: center;
246
+ gap: 8px;
247
+ flex: 1;
248
+ }
249
+
250
+ .presence-dot {
251
+ display: inline-block;
252
+ width: 10px;
253
+ height: 10px;
254
+ border-radius: 50%;
255
+ background-color: #9e9e9e;
256
+ }
257
+
258
+ .inline-row {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ gap: 10px;
262
+ }
263
+
264
+ .config-control {
265
+ display: flex;
266
+ flex-direction: column;
267
+ gap: 10px;
268
+ flex: 1;
269
+ width: 100%;
270
+ margin-left: 0;
271
+ }
272
+
273
+ /* ===== CONTRÔLES PERSONNALISÉS ===== */
274
+
275
+ .slider-container {
276
+ width: 100%;
277
+ max-width: 200px;
278
+ position: relative;
279
+ display: flex;
280
+ flex-direction: row;
281
+ align-items: center;
282
+ gap: 12px;
283
+ }
284
+
285
+ .slider-value {
286
+ background: var(--slider-value-bg);
287
+ color: var(--slider-value-color);
288
+ padding: 4px 8px;
289
+ border-radius: 4px;
290
+ font-size: 0.8rem;
291
+ font-weight: 500;
292
+ border: 1px solid var(--slider-value-border);
293
+ min-width: 45px;
294
+ text-align: center;
295
+ flex-shrink: 0;
296
+ }
297
+
298
+ .toggle-switch {
299
+ position: relative;
300
+ width: 50px;
301
+ height: 25px;
302
+ background: var(--switch-bg-inactive);
303
+ border-radius: 25px;
304
+ cursor: pointer;
305
+ transition: background-color 0.3s ease;
306
+ }
307
+
308
+ .toggle-switch.active {
309
+ background: var(--switch-bg-active);
310
+ }
311
+
312
+ .toggle-switch::after {
313
+ content: "";
314
+ position: absolute;
315
+ top: 2px;
316
+ left: 2px;
317
+ width: 21px;
318
+ height: 21px;
319
+ background: var(--switch-thumb-color);
320
+ border-radius: 50%;
321
+ transition: transform 0.3s ease;
322
+ box-shadow: var(--switch-thumb-shadow);
323
+ }
324
+
325
+ .toggle-switch.active::after {
326
+ transform: translateX(25px);
327
+ }
328
+
329
+ .toggle-switch#transcript-toggle {
330
+ background: var(--switch-bg-inactive);
331
+ }
332
+
333
+ .toggle-switch#transcript-toggle.active {
334
+ background: var(--switch-bg-active);
335
+ }
336
+
337
+ /* ===== INFORMATIONS ET STATS ===== */
338
+
339
+ .stats-grid {
340
+ display: grid;
341
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
342
+ gap: 15px;
343
+ margin-top: 20px;
344
+ }
345
+
346
+ .stat-card {
347
+ background: var(--card-bg);
348
+ border-radius: 10px;
349
+ padding: 15px;
350
+ text-align: center;
351
+ border: 1px solid var(--card-border);
352
+ }
353
+
354
+ .stat-value {
355
+ font-size: 1.5rem;
356
+ font-weight: 700;
357
+ color: var(--stat-value-color);
358
+ margin-bottom: 5px;
359
+ }
360
+
361
+ .stat-label {
362
+ color: var(--stat-label-color);
363
+ font-size: 0.85rem;
364
+ }
365
+
366
+ .model-card {
367
+ background: var(--card-bg);
368
+ border-radius: 12px;
369
+ padding: 20px;
370
+ margin-bottom: 15px;
371
+ border: 1px solid var(--card-border);
372
+ cursor: pointer;
373
+ transition: all 0.3s ease;
374
+ }
375
+
376
+ .model-card:hover {
377
+ background: var(--card-hover-bg);
378
+ transform: translateY(-2px);
379
+ }
380
+
381
+ .model-card.selected {
382
+ border-color: var(--model-card-selected-border);
383
+ box-shadow: var(--model-card-selected-shadow);
384
+ }
385
+
386
+ .models-search-container {
387
+ margin: 12px 0 16px;
388
+ }
389
+
390
+ .models-section {
391
+ margin: 16px 0;
392
+ }
393
+
394
+ .models-section-title {
395
+ display: flex;
396
+ align-items: center;
397
+ gap: 8px;
398
+ font-weight: 600;
399
+ color: var(--settings-section-header-color);
400
+ margin: 6px 0 10px;
401
+ }
402
+
403
+ .model-header {
404
+ display: flex;
405
+ justify-content: space-between;
406
+ align-items: center;
407
+ margin-bottom: 10px;
408
+ }
409
+
410
+ .model-name {
411
+ font-size: 1.1rem;
412
+ font-weight: 600;
413
+ color: var(--model-name-color);
414
+ }
415
+
416
+ .model-provider {
417
+ background: var(--model-provider-color);
418
+ color: var(--model-provider-text);
419
+ padding: 4px 8px;
420
+ border-radius: 6px;
421
+ font-size: 0.75rem;
422
+ font-weight: 500;
423
+ }
424
+
425
+ .model-description {
426
+ color: var(--model-description-color);
427
+ font-size: 0.9rem;
428
+ margin-bottom: 10px;
429
+ }
430
+
431
+ .strength-tag {
432
+ display: inline-block;
433
+ background: var(--strength-tag-bg);
434
+ color: var(--strength-tag-text);
435
+ padding: 4px 8px;
436
+ border-radius: 12px;
437
+ font-size: 0.75rem;
438
+ font-weight: 500;
439
+ margin-right: 8px;
440
+ margin-bottom: 5px;
441
+ }
442
+
443
+ /* Models error and no-models messages */
444
+ .no-models-message,
445
+ .models-error-message {
446
+ text-align: center;
447
+ padding: 40px 20px;
448
+ border-radius: 12px;
449
+ margin: 20px 0;
450
+ background: var(--background-secondary);
451
+ border: 1px solid var(--border-color);
452
+ }
453
+
454
+ .no-models-message p,
455
+ .models-error-message p {
456
+ margin: 0;
457
+ color: var(--text-secondary);
458
+ font-size: 0.95rem;
459
+ line-height: 1.5;
460
+ }
461
+
462
+ .models-error-message {
463
+ background: rgba(231, 76, 60, 0.1);
464
+ border-color: rgba(231, 76, 60, 0.3);
465
+ }
466
+
467
+ .models-error-message p {
468
+ color: #e74c3c;
469
+ }
470
+
471
+ .model-strengths {
472
+ display: flex;
473
+ flex-wrap: wrap;
474
+ gap: 6px;
475
+ }
476
+
477
+ .strength-tag {
478
+ background: var(--model-strength-color);
479
+ color: var(--model-strength-text);
480
+ padding: 3px 8px;
481
+ border-radius: 4px;
482
+ font-size: 0.75rem;
483
+ font-weight: 500;
484
+ }
485
+
486
+ /* ===== PLUGIN CARDS AND SWITCHES ===== */
487
+
488
+ .plugin-card {
489
+ background: linear-gradient(135deg, #22121a 80%, var(--modal-bg) 100%);
490
+ border: 2px solid var(--modal-border);
491
+ border-radius: 20px;
492
+ box-shadow:
493
+ 0 4px 24px 0 #000a,
494
+ 0 2px 0 0 var(--modal-border);
495
+ padding: 28px 32px 24px 32px;
496
+ margin-bottom: 28px;
497
+ display: flex;
498
+ flex-direction: row;
499
+ align-items: center;
500
+ gap: 0;
501
+ justify-content: space-between;
502
+ transition:
503
+ box-shadow 0.2s,
504
+ border 0.2s;
505
+ position: relative;
506
+ }
507
+
508
+ .plugin-card .plugin-info {
509
+ flex: 2 1 0;
510
+ min-width: 0;
511
+ margin-right: 24px;
512
+ }
513
+
514
+ .plugin-card-center {
515
+ flex: 1 1 0;
516
+ display: flex;
517
+ flex-direction: column;
518
+ align-items: center;
519
+ justify-content: center;
520
+ gap: 10px;
521
+ min-width: 120px;
522
+ }
523
+
524
+ .plugin-card-switch {
525
+ flex: 0 0 auto;
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: flex-end;
529
+ min-width: 80px;
530
+ margin-left: 24px;
531
+ }
532
+
533
+ .plugin-card .plugin-title {
534
+ font-size: 1.35rem;
535
+ font-weight: 700;
536
+ color: var(--plugin-card-title-color);
537
+ margin-bottom: 6px;
538
+ letter-spacing: 0.01em;
539
+ text-shadow: 0 2px 8px #000b;
540
+ }
541
+
542
+ .plugin-card .plugin-type {
543
+ font-size: 1rem;
544
+ color: var(--accent-color);
545
+ margin-left: 10px;
546
+ font-weight: 600;
547
+ text-shadow: 0 1px 4px #0008;
548
+ }
549
+
550
+ .plugin-card .plugin-desc {
551
+ color: var(--plugin-card-desc-color);
552
+ margin-bottom: 10px;
553
+ font-size: 1.01rem;
554
+ line-height: 1.5;
555
+ }
556
+
557
+ .plugin-card .plugin-author {
558
+ font-size: 0.95rem;
559
+ color: var(--plugin-card-author-color);
560
+ font-weight: 500;
561
+ }
562
+
563
+ .plugin-card-left {
564
+ display: flex;
565
+ flex-direction: column;
566
+ align-items: flex-start;
567
+ min-width: 90px;
568
+ gap: 8px;
569
+ margin-top: 4px;
570
+ }
571
+
572
+ .plugin-card-right {
573
+ display: flex;
574
+ flex-direction: column;
575
+ flex: 1;
576
+ gap: 12px;
577
+ }
578
+
579
+ .plugin-type-badge {
580
+ display: inline-block;
581
+ background: var(--plugin-type-badge-bg);
582
+ color: #fff;
583
+ font-size: 0.85rem;
584
+ font-weight: 600;
585
+ border-radius: 8px;
586
+ padding: 3px 10px;
587
+ margin-bottom: 8px;
588
+ margin-right: 12px;
589
+ letter-spacing: 0.03em;
590
+ box-shadow: 0 1px 4px #0002;
591
+ }
592
+
593
+ .plugin-theme-swatch {
594
+ display: inline-flex;
595
+ gap: 4px;
596
+ margin-bottom: 8px;
597
+ margin-right: 12px;
598
+ vertical-align: middle;
599
+ }
600
+
601
+ .plugin-theme-swatch span {
602
+ display: inline-block;
603
+ width: 18px;
604
+ height: 18px;
605
+ border-radius: 50%;
606
+ border: 2px solid #fff2;
607
+ box-shadow: 0 1px 4px #0002;
608
+ }
609
+
610
+ .plugin-active-badge {
611
+ display: inline-block;
612
+ background: var(--plugin-active-badge-bg);
613
+ color: #fff;
614
+ font-size: 0.85rem;
615
+ font-weight: 700;
616
+ border-radius: 8px;
617
+ padding: 3px 12px;
618
+ margin-bottom: 8px;
619
+ margin-right: 12px;
620
+ letter-spacing: 0.03em;
621
+ box-shadow: 0 1px 8px #0003;
622
+ }
623
+
624
+ @media (max-width: 700px) {
625
+ .plugin-card {
626
+ flex-direction: column;
627
+ gap: 12px;
628
+ }
629
+ .plugin-card-left {
630
+ flex-direction: row;
631
+ align-items: center;
632
+ min-width: 0;
633
+ gap: 10px;
634
+ margin-top: 0;
635
+ }
636
+ .plugin-card-right {
637
+ gap: 8px;
638
+ }
639
+ }
640
+
641
+ /* ===== CONSOLIDATED RESPONSIVE DESIGN ===== */
642
+ @media (max-width: 768px) {
643
+ .settings-panel {
644
+ width: 100%;
645
+ height: 100%;
646
+ border-radius: 0;
647
+ max-height: 100vh;
648
+ }
649
+
650
+ .settings-header {
651
+ padding: 20px;
652
+ }
653
+
654
+ .settings-title {
655
+ font-size: 1.3rem;
656
+ }
657
+
658
+ .settings-tabs {
659
+ padding: 0 10px;
660
+ gap: 5px;
661
+ position: relative;
662
+ }
663
+
664
+ .settings-tab {
665
+ flex: 0 0 auto;
666
+ min-width: 110px;
667
+ padding: 12px 16px;
668
+ font-size: 0.85rem;
669
+ border-radius: 8px 8px 0 0;
670
+ }
671
+
672
+ .settings-tab.active::after {
673
+ height: 2px;
674
+ }
675
+
676
+ .tab-content {
677
+ padding: 20px;
678
+ }
679
+
680
+ .config-row {
681
+ flex-direction: column;
682
+ align-items: flex-start;
683
+ gap: 10px;
684
+ }
685
+
686
+ .config-control {
687
+ margin-left: 0;
688
+ width: 100%;
689
+ }
690
+
691
+ .slider-container {
692
+ width: 100%;
693
+ max-width: 100%;
694
+ flex-direction: row;
695
+ justify-content: space-between;
696
+ align-items: center;
697
+ gap: 10px;
698
+ }
699
+
700
+ .slider-value {
701
+ min-width: 45px;
702
+ flex-shrink: 0;
703
+ }
704
+
705
+ .stats-grid {
706
+ grid-template-columns: repeat(2, 1fr);
707
+ }
708
+ }
709
+
710
+ @media (max-width: 480px) {
711
+ .settings-panel {
712
+ width: 95%;
713
+ margin: 2.5%;
714
+ max-height: 90vh;
715
+ }
716
+
717
+ .config-row {
718
+ flex-direction: column;
719
+ align-items: stretch;
720
+ gap: 8px;
721
+ }
722
+
723
+ .config-control {
724
+ margin-left: 0;
725
+ width: 100%;
726
+ }
727
+
728
+ .slider-container {
729
+ width: 100%;
730
+ max-width: 100%;
731
+ flex-direction: row;
732
+ justify-content: space-between;
733
+ align-items: center;
734
+ gap: 8px;
735
+ }
736
+
737
+ .kimi-slider,
738
+ .kimi-slider-unified {
739
+ flex: 1;
740
+ min-width: 0;
741
+ }
742
+
743
+ .slider-value {
744
+ min-width: 40px;
745
+ max-width: 50px;
746
+ flex-shrink: 0;
747
+ font-size: 0.75rem;
748
+ }
749
+
750
+ .kimi-select,
751
+ .kimi-select-unified {
752
+ width: 100%;
753
+ max-width: 100%;
754
+ min-width: 0;
755
+ font-size: 0.9rem;
756
+ padding: 10px 35px 10px 12px;
757
+ }
758
+
759
+ .kimi-input,
760
+ .kimi-input-unified {
761
+ width: 100%;
762
+ max-width: 100%;
763
+ min-width: 0;
764
+ font-size: 0.9rem;
765
+ }
766
+
767
+ .settings-tab {
768
+ min-width: 90px;
769
+ font-size: 0.8rem;
770
+ padding: 10px 12px;
771
+ }
772
+
773
+ .config-section {
774
+ padding: 15px;
775
+ }
776
+
777
+ .config-section h3 {
778
+ font-size: 1.1rem;
779
+ }
780
+
781
+ .stats-grid {
782
+ grid-template-columns: repeat(2, 1fr);
783
+ gap: 10px;
784
+ }
785
+
786
+ .stat-card {
787
+ padding: 12px;
788
+ }
789
+ }
790
+
791
+ @media (max-width: 360px) {
792
+ .settings-panel {
793
+ width: 98%;
794
+ margin: 1%;
795
+ }
796
+
797
+ .slider-container {
798
+ gap: 5px;
799
+ }
800
+
801
+ .slider-value {
802
+ min-width: 35px;
803
+ max-width: 40px;
804
+ font-size: 0.7rem;
805
+ padding: 2px 6px;
806
+ }
807
+
808
+ .kimi-select,
809
+ .kimi-select-unified {
810
+ font-size: 0.85rem;
811
+ padding: 8px 30px 8px 10px;
812
+ }
813
+
814
+ .settings-tab {
815
+ min-width: 80px;
816
+ font-size: 0.75rem;
817
+ padding: 8px 10px;
818
+ }
819
+
820
+ .stats-grid {
821
+ grid-template-columns: 1fr;
822
+ }
823
+ }
824
+
825
+ /* ===== HELP MODAL ===== */
826
+ .help-overlay {
827
+ position: fixed;
828
+ top: 0;
829
+ left: 0;
830
+ width: 100%;
831
+ height: 100%;
832
+ background: var(--modal-overlay-bg);
833
+ backdrop-filter: blur(15px);
834
+ -webkit-backdrop-filter: blur(15px);
835
+ z-index: 60;
836
+ display: none;
837
+ opacity: 0;
838
+ transition:
839
+ opacity 0.3s ease,
840
+ backdrop-filter 0.3s ease;
841
+ }
842
+
843
+ .help-overlay.visible {
844
+ display: flex;
845
+ opacity: 1;
846
+ align-items: center;
847
+ justify-content: center;
848
+ padding: 10px;
849
+ }
850
+
851
+ .help-modal {
852
+ background: var(--help-modal-bg, linear-gradient(145deg, rgba(26, 26, 26, 0.95), rgba(40, 40, 40, 0.95)));
853
+ backdrop-filter: blur(20px);
854
+ border-radius: 25px;
855
+ border: 1px solid var(--help-modal-border, rgba(255, 255, 255, 0.1));
856
+ box-shadow:
857
+ 0 20px 60px rgba(0, 0, 0, 0.5),
858
+ 0 0 30px var(--primary-color, rgba(255, 107, 157, 0.3));
859
+ width: 100%;
860
+ max-width: 850px;
861
+ max-height: 85vh;
862
+ overflow: hidden;
863
+ display: flex;
864
+ flex-direction: column;
865
+ animation: slideInUp 0.4s ease-out;
866
+ margin: 20px;
867
+ }
868
+
869
+ .help-header {
870
+ background: var(--modal-header-bg, linear-gradient(135deg, var(--primary-color, #ff9a9e), var(--secondary-color, #fecfef)));
871
+ padding: 25px 30px;
872
+ display: flex;
873
+ justify-content: space-between;
874
+ align-items: center;
875
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
876
+ }
877
+
878
+ .help-title {
879
+ margin: 0;
880
+ color: var(--modal-title-color);
881
+ font-size: 1.5rem;
882
+ font-weight: 700;
883
+ display: flex;
884
+ align-items: center;
885
+ gap: 10px;
886
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
887
+ }
888
+
889
+ .help-close {
890
+ background: none;
891
+ border: none;
892
+ color: var(--modal-text);
893
+ font-size: 1.5rem;
894
+ cursor: pointer;
895
+ padding: 8px;
896
+ border-radius: 50%;
897
+ transition: all 0.3s ease;
898
+ backdrop-filter: blur(10px);
899
+ }
900
+
901
+ .help-close:hover {
902
+ background-color: var(--modal-close-hover-bg);
903
+ transform: scale(1.1);
904
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
905
+ }
906
+
907
+ .help-content {
908
+ flex: 1;
909
+ padding: 25px 30px;
910
+ overflow-y: auto;
911
+ color: var(--help-content-color);
912
+ }
913
+
914
+ /* Help Content Components */
915
+ .help-section {
916
+ margin-bottom: 30px;
917
+ padding-bottom: 20px;
918
+ border-bottom: 1px solid var(--help-section-border);
919
+ }
920
+
921
+ .help-section:last-child {
922
+ border-bottom: none;
923
+ }
924
+
925
+ .help-section h3 {
926
+ color: var(--modal-title-color);
927
+ font-size: 1.3rem;
928
+ margin-bottom: 15px;
929
+ display: flex;
930
+ align-items: center;
931
+ gap: 10px;
932
+ }
933
+
934
+ .creators-info {
935
+ display: flex;
936
+ gap: 20px;
937
+ margin-bottom: 20px;
938
+ flex-wrap: wrap;
939
+ }
940
+
941
+ .creator-card {
942
+ background: var(--creator-card-bg);
943
+ border: 1px solid var(--creator-card-border);
944
+ padding: 20px;
945
+ border-radius: 15px;
946
+ flex: 1;
947
+ min-width: 280px;
948
+ display: flex;
949
+ align-items: center;
950
+ gap: 15px;
951
+ }
952
+
953
+ .creator-avatar {
954
+ font-size: 2.5rem;
955
+ width: 60px;
956
+ height: 60px;
957
+ display: flex;
958
+ align-items: center;
959
+ justify-content: center;
960
+ border-radius: 50%;
961
+ background: var(--creator-avatar-bg);
962
+ }
963
+
964
+ .creator-details h4 {
965
+ margin: 0 0 5px 0;
966
+ color: #1650a0;
967
+ font-size: 1.2rem;
968
+ }
969
+
970
+ .creator-details p {
971
+ margin: 0 0 8px 0;
972
+ color: var(--feature-text-color);
973
+ font-size: 0.9rem;
974
+ }
975
+
976
+ .creator-role {
977
+ background: var(--creator-role-bg);
978
+ color: var(--creator-role-color);
979
+ padding: 4px 12px;
980
+ border-radius: 12px;
981
+ font-size: 0.8rem;
982
+ font-weight: 500;
983
+ }
984
+
985
+ /* Creator Links */
986
+ .creator-links {
987
+ display: flex;
988
+ gap: 8px;
989
+ margin-top: 12px;
990
+ }
991
+
992
+ .creator-link {
993
+ background: var(--creator-role-bg, linear-gradient(135deg, var(--primary-color), var(--secondary-color)));
994
+ color: var(--creator-role-color, var(--text-on-primary));
995
+ padding: 8px 12px;
996
+ border-radius: 20px;
997
+ text-decoration: none;
998
+ display: flex;
999
+ align-items: center;
1000
+ justify-content: center;
1001
+ gap: 6px;
1002
+ min-width: auto;
1003
+ height: 36px;
1004
+ transition: all 0.3s ease;
1005
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1006
+ font-size: 0.8rem;
1007
+ font-weight: 500;
1008
+ border: 1px solid transparent;
1009
+ }
1010
+
1011
+ .creator-link:hover {
1012
+ transform: translateY(-2px);
1013
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1014
+ color: var(--creator-role-color, var(--text-on-primary)) !important;
1015
+ text-decoration: none !important;
1016
+ background: var(--creator-role-bg, linear-gradient(135deg, var(--primary-color), var(--secondary-color))) !important;
1017
+ filter: brightness(1.1);
1018
+ border-color: var(--primary-color, #ff6b9d);
1019
+ }
1020
+
1021
+ .creator-link i {
1022
+ font-size: 1rem;
1023
+ color: inherit;
1024
+ transition: color 0.3s ease;
1025
+ }
1026
+
1027
+ .creator-link span {
1028
+ color: inherit;
1029
+ transition: color 0.3s ease;
1030
+ }
1031
+
1032
+ .creator-link:hover i,
1033
+ .creator-link:hover span {
1034
+ color: inherit !important;
1035
+ }
1036
+
1037
+ .philosophy {
1038
+ background: var(--philosophy-bg);
1039
+ border: 1px solid var(--philosophy-border);
1040
+ padding: 15px;
1041
+ border-radius: 10px;
1042
+ border-left: 4px solid var(--philosophy-border-left);
1043
+ margin: 15px 0;
1044
+ font-style: italic;
1045
+ color: var(--feature-text-color);
1046
+ }
1047
+
1048
+ .features-grid {
1049
+ display: grid;
1050
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
1051
+ gap: 15px;
1052
+ margin: 15px 0;
1053
+ }
1054
+
1055
+ .feature-item {
1056
+ background: var(--feature-item-bg);
1057
+ border: 1px solid var(--feature-item-border);
1058
+ padding: 15px;
1059
+ border-radius: 10px;
1060
+ text-align: center;
1061
+ }
1062
+
1063
+ .feature-item i {
1064
+ color: var(--feature-icon-color);
1065
+ font-size: 2rem;
1066
+ margin-bottom: 10px;
1067
+ }
1068
+
1069
+ .feature-item h4 {
1070
+ margin: 0 0 8px 0;
1071
+ color: var(--feature-title-color);
1072
+ }
1073
+
1074
+ .feature-item p {
1075
+ margin: 0;
1076
+ font-size: 0.9rem;
1077
+ color: var(--feature-text-color);
1078
+ }
1079
+
1080
+ .quick-guide {
1081
+ display: flex;
1082
+ flex-wrap: wrap;
1083
+ gap: 20px;
1084
+ margin-bottom: 20px;
1085
+ }
1086
+
1087
+ .guide-step {
1088
+ display: flex;
1089
+ align-items: flex-start;
1090
+ background: rgba(255, 255, 255, 0.05);
1091
+ border-radius: 12px;
1092
+ padding: 15px 20px;
1093
+ margin-bottom: 12px;
1094
+ min-width: 220px;
1095
+ flex: 1 1 220px;
1096
+ }
1097
+
1098
+ .step-number {
1099
+ font-size: 1.5rem;
1100
+ font-weight: bold;
1101
+ color: var(--guide-step-number-color);
1102
+ margin-right: 16px;
1103
+ background: var(--guide-step-number-bg);
1104
+ border-radius: 50%;
1105
+ width: 36px;
1106
+ height: 36px;
1107
+ display: flex;
1108
+ align-items: center;
1109
+ justify-content: center;
1110
+ }
1111
+
1112
+ .step-content h4 {
1113
+ font-size: 1.1rem;
1114
+ margin-bottom: 6px;
1115
+ color: var(--modal-title-color);
1116
+ }
1117
+
1118
+ .step-content p {
1119
+ font-size: 0.98rem;
1120
+ color: var(--help-content-color);
1121
+ }
1122
+
1123
+ .tips-list {
1124
+ display: flex;
1125
+ flex-wrap: wrap;
1126
+ gap: 18px;
1127
+ margin-bottom: 18px;
1128
+ }
1129
+
1130
+ .tip-item {
1131
+ display: flex;
1132
+ align-items: center;
1133
+ background: var(--feature-item-bg);
1134
+ border-radius: 12px;
1135
+ padding: 12px 18px;
1136
+ min-width: 200px;
1137
+ flex: 1 1 200px;
1138
+ }
1139
+
1140
+ .tip-item i {
1141
+ font-size: 1.2rem;
1142
+ color: var(--feature-icon-color);
1143
+ margin-right: 12px;
1144
+ }
1145
+
1146
+ .tip-item p {
1147
+ font-size: 0.98rem;
1148
+ color: var(--help-content-color);
1149
+ }
1150
+
1151
+ .tech-info {
1152
+ background: var(--feature-item-bg);
1153
+ border-radius: 10px;
1154
+ padding: 14px 20px;
1155
+ margin-top: 10px;
1156
+ color: var(--help-content-color);
1157
+ font-size: 0.98rem;
1158
+ }
1159
+
1160
+ .settings-panel,
1161
+ .config-section {
1162
+ isolation: isolate;
1163
+ }
1164
+
1165
+ /* Videos background */
1166
+ .video-container {
1167
+ z-index: 0;
1168
+ }
1169
+
1170
+ .bg-video {
1171
+ z-index: 1;
1172
+ }
1173
+
1174
+ .content-overlay {
1175
+ z-index: 2;
1176
+ background-color: transparent;
1177
+ backdrop-filter: none;
1178
+ }
1179
+
1180
+ /* Responsive for help modal */
1181
+ @media (max-width: 768px) {
1182
+ .help-overlay {
1183
+ padding: 5px;
1184
+ }
1185
+
1186
+ .help-modal {
1187
+ margin: 5px;
1188
+ max-height: 95vh;
1189
+ border-radius: 15px;
1190
+ width: calc(100% - 10px);
1191
+ }
1192
+
1193
+ .help-header {
1194
+ padding: 20px;
1195
+ }
1196
+
1197
+ .help-title {
1198
+ font-size: 1.3rem;
1199
+ }
1200
+
1201
+ .help-content {
1202
+ padding: 20px;
1203
+ }
1204
+
1205
+ .creators-info {
1206
+ flex-direction: column;
1207
+ }
1208
+
1209
+ .creator-card {
1210
+ min-width: auto;
1211
+ }
1212
+
1213
+ .features-grid {
1214
+ grid-template-columns: 1fr;
1215
+ }
1216
+ }
1217
+
1218
+ /* ===== BUTTON ANIMATIONS ===== */
1219
+ .kimi-button.animated {
1220
+ animation: kimiPulse 0.7s;
1221
+ }
1222
+
1223
+ @keyframes kimiPulse {
1224
+ 0% {
1225
+ background-color: var(--primary-color);
1226
+ transform: scale(1);
1227
+ }
1228
+ 50% {
1229
+ background-color: var(--accent-color);
1230
+ transform: scale(1.08);
1231
+ }
1232
+ 100% {
1233
+ background-color: var(--primary-color);
1234
+ transform: scale(1);
1235
+ }
1236
+ }
1237
+
1238
+ /* ===== CHARACTER GRID AND CARDS ===== */
1239
+ .character-grid {
1240
+ display: grid;
1241
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1242
+ gap: 24px;
1243
+ margin-top: 16px;
1244
+ margin-bottom: 16px;
1245
+ }
1246
+
1247
+ @media (max-width: 700px) {
1248
+ .character-grid {
1249
+ grid-template-columns: 1fr;
1250
+ }
1251
+ }
1252
+
1253
+ .character-card {
1254
+ background: var(--card-bg);
1255
+ border: 2px solid transparent;
1256
+ border-radius: 18px;
1257
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
1258
+ padding: 18px 16px 12px 16px;
1259
+ display: flex;
1260
+ flex-direction: column;
1261
+ align-items: center;
1262
+ cursor: pointer;
1263
+ transition:
1264
+ border 0.2s,
1265
+ box-shadow 0.2s,
1266
+ background 0.2s;
1267
+ user-select: none;
1268
+ width: 100%;
1269
+ }
1270
+
1271
+ .character-card.selected {
1272
+ border: 2.5px solid var(--character-selected-border, #ff6b9d);
1273
+ background: var(--character-selected-bg, rgba(255, 107, 157, 0.13));
1274
+ box-shadow:
1275
+ 0 0 0 4px var(--character-selected-border, #ff6b9d),
1276
+ 0 4px 24px var(--character-selected-bg, rgba(255, 107, 157, 0.15));
1277
+ }
1278
+
1279
+ .character-card:hover {
1280
+ border: 2px solid var(--primary-color, #ff6b9d);
1281
+ background: rgba(255, 107, 157, 0.1);
1282
+ box-shadow: 0 2px 16px rgba(255, 107, 157, 0.1);
1283
+ }
1284
+
1285
+ .character-card img {
1286
+ width: 80px;
1287
+ height: 80px;
1288
+ border-radius: 50%;
1289
+ object-fit: cover;
1290
+ margin-bottom: 12px;
1291
+ border: 2px solid var(--modal-text);
1292
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1293
+ }
1294
+
1295
+ .character-name {
1296
+ text-align: center;
1297
+ width: 100%;
1298
+ font-size: 1.1rem;
1299
+ font-weight: 700;
1300
+ color: var(--modal-text);
1301
+ text-shadow: 0 1px 8px var(--primary-color, #ff6b9d);
1302
+ margin-bottom: 4px;
1303
+ margin-top: 0;
1304
+ }
1305
+
1306
+ .character-info {
1307
+ display: flex;
1308
+ flex-direction: column;
1309
+ align-items: center;
1310
+ justify-content: center;
1311
+ text-align: center;
1312
+ width: 100%;
1313
+ margin-bottom: 10px;
1314
+ }
1315
+
1316
+ .character-details {
1317
+ font-size: 0.95rem;
1318
+ color: var(--modal-text);
1319
+ opacity: 0.85;
1320
+ margin-bottom: 6px;
1321
+ text-align: center;
1322
+ }
1323
+
1324
+ .character-prompt-label {
1325
+ font-size: 0.85rem;
1326
+ color: var(--modal-text);
1327
+ margin-bottom: 4px;
1328
+ margin-top: 8px;
1329
+ opacity: 0.7;
1330
+ text-align: center;
1331
+ }
1332
+
1333
+ .character-prompt-input {
1334
+ width: 100%;
1335
+ min-height: 60px;
1336
+ border-radius: 8px;
1337
+ border: 1px solid var(--primary-color, #ff6b9d);
1338
+ background: var(--input-bg);
1339
+ color: var(--modal-text);
1340
+ padding: 6px 8px;
1341
+ font-size: 0.95rem;
1342
+ resize: vertical;
1343
+ margin-bottom: 4px;
1344
+ }
1345
+
1346
+ .character-prompt-input:disabled {
1347
+ opacity: 0.5;
1348
+ background: var(--input-bg);
1349
+ cursor: not-allowed;
1350
+ }
1351
+
1352
+ /* ===== PERSONALITY CHEAT PANEL ===== */
1353
+
1354
+ .cheat-toggle-btn {
1355
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
1356
+ border: none;
1357
+ color: #fff;
1358
+ font-size: 1em;
1359
+ margin-left: 1em;
1360
+ cursor: pointer;
1361
+ display: inline-flex;
1362
+ align-items: center;
1363
+ gap: 0.5em;
1364
+ transition:
1365
+ color 0.2s,
1366
+ box-shadow 0.2s,
1367
+ background 0.2s;
1368
+ border-radius: 18px;
1369
+ padding: 0.35em 1.1em 0.35em 0.9em;
1370
+ box-shadow: 0 2px 10px 0 rgba(255, 107, 157, 0.1);
1371
+ font-weight: 600;
1372
+ letter-spacing: 0.01em;
1373
+ outline: none;
1374
+ }
1375
+
1376
+ .cheat-toggle-btn[aria-expanded="true"] {
1377
+ color: #fff;
1378
+ background: linear-gradient(90deg, var(--accent-color), var(--primary-color));
1379
+ box-shadow: 0 2px 16px 0 rgba(255, 107, 157, 0.18);
1380
+ }
1381
+
1382
+ .cheat-toggle-btn:hover,
1383
+ .cheat-toggle-btn:focus {
1384
+ background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
1385
+ color: #fff;
1386
+ box-shadow: 0 4px 18px 0 rgba(255, 107, 157, 0.22);
1387
+ filter: brightness(1.08);
1388
+ }
1389
+
1390
+ .cheat-toggle-btn i {
1391
+ margin-right: 0.4em;
1392
+ font-size: 1.1em;
1393
+ }
1394
+
1395
+ .cheat-indicator {
1396
+ font-size: 0.9em;
1397
+ color: #aaa;
1398
+ margin-bottom: 0.5em;
1399
+ margin-left: 2.2em;
1400
+ }
1401
+
1402
+ .cheat-panel {
1403
+ max-height: 0;
1404
+ overflow: hidden;
1405
+ transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1);
1406
+ opacity: 0.5;
1407
+ pointer-events: none;
1408
+ }
1409
+
1410
+ .cheat-panel.open {
1411
+ max-height: 800px;
1412
+ opacity: 1;
1413
+ pointer-events: auto;
1414
+ transition:
1415
+ max-height 0.6s cubic-bezier(0.4, 0, 0.2, 1),
1416
+ opacity 0.3s;
1417
+ }
kimi-css/kimi-style.css ADDED
@@ -0,0 +1,1737 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ===== CONSOLIDATED CSS VARIABLES - ENHANCED DYNAMIC THEMING ===== */
2
+ :root {
3
+ /* Core Theme Colors - Default Pink Passion */
4
+ --primary-color: #ff6b9d;
5
+ --secondary-color: #ffeaa7;
6
+ --accent-color: #fd79a8;
7
+ --background-overlay: rgba(255, 107, 157, 0.15);
8
+ --interface-opacity: 0.7;
9
+ --gradient-start: #ff6b9d;
10
+ --gradient-end: #fd79a8;
11
+ --text-glow: 0 0 10px rgba(255, 107, 157, 0.5);
12
+ --button-hover: rgba(255, 107, 157, 0.3);
13
+ --animations-enabled: 1;
14
+ --switch-color: var(--primary-color);
15
+
16
+ /* Contrast and Accessibility */
17
+ --contrast-ratio: 4.5; /* WCAG AA standard */
18
+ --text-on-primary: #ffffff;
19
+ --text-on-secondary: #222222;
20
+ --text-on-accent: #ffffff;
21
+ --text-on-background: #ffffff;
22
+ --border-opacity: 0.3;
23
+ --hover-opacity: 0.15;
24
+ --active-opacity: 0.25;
25
+
26
+ /* UI Component Colors - Chat Interface */
27
+ --chat-bg: rgba(255, 107, 157, 0.9);
28
+ --chat-text: var(--text-on-primary);
29
+ --chat-header-bg: rgba(255, 255, 255, 0.05);
30
+ --chat-border: var(--primary-color);
31
+ --chat-input-bg: rgba(255, 255, 255, 0.1);
32
+ --chat-input-text: var(--text-on-background);
33
+ --chat-input-placeholder: rgba(255, 255, 255, 0.6);
34
+ --chat-message-user-bg: var(--primary-color);
35
+ --chat-message-user-text: var(--text-on-primary);
36
+ --chat-message-kimi-bg: rgba(255, 255, 255, 0.15);
37
+ --chat-message-kimi-text: var(--text-on-background);
38
+
39
+ /* Modal & Overlay Colors */
40
+ --modal-bg: rgba(255, 107, 157, 0.95);
41
+ --modal-border: var(--primary-color);
42
+ --modal-header-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
43
+ --modal-text: var(--text-on-primary);
44
+ --modal-title-color: var(--text-on-secondary);
45
+ --modal-overlay-bg: rgba(0, 0, 0, 0.8);
46
+ --modal-close-hover-bg: rgba(255, 255, 255, 0.2);
47
+
48
+ /* Settings Panel & Tabs */
49
+ --settings-bg: #181018;
50
+ --settings-text: var(--text-on-background);
51
+ --settings-tab-bg: #1a1a1a;
52
+ --settings-tab-color: #bfa6b6;
53
+ --settings-tab-hover-bg: rgba(255, 255, 255, var(--hover-opacity));
54
+ --settings-tab-hover-color: rgba(255, 255, 255, 0.9);
55
+ --settings-tab-active-bg: var(--primary-color);
56
+ --settings-tab-active-color: var(--text-on-primary);
57
+ --settings-tab-border: var(--primary-color);
58
+ --settings-section-bg: #22121a;
59
+ --settings-section-border: rgba(255, 107, 157, var(--border-opacity));
60
+ --settings-section-header-color: var(--text-on-background);
61
+
62
+ /* Form Element Colors */
63
+ --input-bg: rgba(255, 255, 255, 0.1);
64
+ --input-text: var(--text-on-background);
65
+ --input-border: var(--primary-color);
66
+ --input-focus-bg: rgba(255, 255, 255, var(--hover-opacity));
67
+ --input-focus-border: var(--accent-color);
68
+ --input-placeholder: rgba(255, 255, 255, 0.6);
69
+
70
+ /* Button Colors */
71
+ --button-primary-bg: var(--primary-color);
72
+ --button-primary-text: var(--text-on-primary);
73
+ --button-primary-hover-bg: var(--accent-color);
74
+ --button-secondary-bg: rgba(255, 255, 255, 0.1);
75
+ --button-secondary-text: var(--text-on-background);
76
+ --button-secondary-hover-bg: rgba(255, 255, 255, var(--hover-opacity));
77
+ --button-danger-bg: #e74c3c;
78
+ --button-danger-text: #ffffff;
79
+ --button-danger-hover-bg: #c0392b;
80
+
81
+ /* Select & Dropdown Options */
82
+ --select-bg: var(--input-bg);
83
+ --select-text: var(--input-text);
84
+ --select-border: var(--input-border);
85
+ --select-option-bg: rgba(40, 44, 52, 0.95);
86
+ --select-option-text: white;
87
+ --select-option-hover-bg: var(--primary-color);
88
+ --select-option-hover-text: var(--text-on-primary);
89
+ --select-option-checked-bg: var(--accent-color);
90
+ --select-option-checked-text: var(--text-on-accent);
91
+ --select-option-disabled-bg: rgba(40, 44, 52, 0.5);
92
+ --select-option-disabled-text: #666;
93
+
94
+ /* Slider Components */
95
+ --slider-track-bg: rgba(255, 255, 255, 0.1);
96
+ --slider-track-active-bg: var(--primary-color);
97
+ --slider-thumb-bg: var(--primary-color);
98
+ --slider-thumb-hover-bg: var(--accent-color);
99
+ --slider-value-bg: #181018;
100
+ --slider-value-border: var(--primary-color);
101
+ --slider-value-color: var(--text-on-background);
102
+
103
+ /* Toggle Switch */
104
+ --switch-bg-inactive: rgba(255, 255, 255, 0.15);
105
+ --switch-bg-active: linear-gradient(90deg, var(--primary-color), var(--accent-color));
106
+ --switch-thumb-color: #ffffff;
107
+ --switch-thumb-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
108
+
109
+ /* Mic Button & Pulse Effect */
110
+ --mic-button-bg: var(--button-hover);
111
+ --mic-button-border: var(--primary-color);
112
+ --mic-button-shadow: 0 0 15px var(--primary-color);
113
+ --mic-button-hover-bg: var(--primary-color);
114
+ --mic-button-hover-shadow: var(--text-glow);
115
+ --mic-button-icon-color: var(--text-on-primary);
116
+ --mic-listening-border: #27ae60;
117
+ --mic-listening-shadow: 0 0 15px #27ae60;
118
+ --mic-pulse-color: rgba(39, 174, 96, 0.5);
119
+ --mic-pulse-listening-color: rgba(39, 174, 96, 0.4);
120
+
121
+ /* Cards & Stats */
122
+ --card-bg: rgba(255, 255, 255, 0.05);
123
+ --card-border: rgba(255, 255, 255, 0.1);
124
+ --card-hover-bg: rgba(255, 255, 255, 0.08);
125
+ --card-text: var(--text-on-background);
126
+ --stat-value-color: var(--primary-color);
127
+ --stat-label-color: rgba(255, 255, 255, 0.7);
128
+
129
+ /* Plugin Cards */
130
+ --plugin-card-bg: linear-gradient(135deg, #22121a 80%, var(--modal-bg) 100%);
131
+ --plugin-card-border: var(--primary-color);
132
+ --plugin-card-title-color: var(--text-on-background);
133
+ --plugin-card-desc-color: #e0cfe6;
134
+ --plugin-card-author-color: #bfa6b6;
135
+ --plugin-type-badge-bg: var(--accent-color);
136
+ --plugin-type-badge-text: var(--text-on-accent);
137
+ --plugin-active-badge-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
138
+ --plugin-active-badge-text: var(--text-on-primary);
139
+
140
+ /* Help Modal */
141
+ --help-modal-bg: var(--modal-bg);
142
+ --help-modal-border: var(--modal-border);
143
+ --help-content-color: var(--text-on-background);
144
+ --help-section-border: rgba(255, 255, 255, 0.1);
145
+ --creator-card-bg: rgba(255, 255, 255, 0.05);
146
+ --creator-card-border: rgba(255, 255, 255, 0.1);
147
+ --creator-avatar-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
148
+ --creator-name-color: var(--primary-color);
149
+ --creator-role-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
150
+ --creator-role-color: var(--text-on-secondary);
151
+ --philosophy-bg: rgba(255, 154, 158, 0.1);
152
+ --philosophy-border: rgba(255, 154, 158, var(--border-opacity));
153
+ --philosophy-border-left: var(--primary-color);
154
+ --feature-item-bg: rgba(255, 255, 255, 0.05);
155
+ --feature-item-border: rgba(255, 255, 255, 0.1);
156
+ --feature-icon-color: #ffeaa7;
157
+ --feature-title-color: #ffeaa7;
158
+ --feature-text-color: rgba(255, 255, 255, 0.7);
159
+ --guide-step-bg: rgba(255, 255, 255, 0.05);
160
+ --guide-step-number-bg: rgba(255, 107, 157, var(--hover-opacity));
161
+ --guide-step-number-color: var(--primary-color);
162
+ --tip-item-bg: var(--feature-item-bg);
163
+ --tip-item-border: var(--feature-item-border);
164
+
165
+ /* Unified Scrollbar System */
166
+ --scrollbar-width: 8px;
167
+ --scrollbar-track-bg: rgba(255, 255, 255, 0.05);
168
+ --scrollbar-thumb-bg: rgba(255, 107, 157, 0.4);
169
+ --scrollbar-thumb-hover-bg: rgba(255, 107, 157, 0.6);
170
+ --scrollbar-thumb-active-bg: rgba(255, 107, 157, 0.8);
171
+ --scrollbar-corner-bg: rgba(255, 255, 255, 0.05);
172
+
173
+ /* Model Colors */
174
+ --model-card-bg: var(--card-bg);
175
+ --model-card-border: var(--card-border);
176
+ --model-card-hover-bg: var(--card-hover-bg);
177
+ --model-card-selected-border: var(--primary-color);
178
+ --model-card-selected-shadow: 0 0 0 2px rgba(255, 154, 158, var(--border-opacity));
179
+ --model-name-color: var(--text-on-background);
180
+ --model-description-color: rgba(255, 255, 255, 0.7);
181
+ --model-strength-color: var(--primary-color);
182
+ --model-strength-text: var(--text-on-primary);
183
+ --model-provider-color: var(--accent-color);
184
+ --model-provider-text: var(--text-on-accent);
185
+
186
+ /* Text Colors */
187
+ --text-primary: var(--text-on-background);
188
+ --text-secondary: #aaa;
189
+ --text-muted: rgba(255, 255, 255, 0.6);
190
+
191
+ /* Character Selection Colors */
192
+ --character-card-bg: var(--card-bg);
193
+ --character-card-border: var(--card-border);
194
+ --character-card-hover-bg: var(--card-hover-bg);
195
+ --character-selected-border: var(--primary-color);
196
+ --character-selected-bg: rgba(255, 107, 157, 0.13);
197
+ --character-name-color: var(--text-on-background);
198
+
199
+ /* Waiting Indicator */
200
+ --waiting-indicator-color: var(--primary-color);
201
+ --loading-spinner-color: var(--primary-color);
202
+
203
+ /* Progress Bar */
204
+ --progress-bg: rgba(255, 255, 255, 0.1);
205
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
206
+ --progress-text-color: var(--text-on-background);
207
+
208
+ /* Transcript */
209
+ --transcript-bg: rgba(0, 0, 0, 0.7);
210
+ --transcript-text: var(--text-on-background);
211
+ --transcript-border: var(--primary-color);
212
+ }
213
+
214
+ /* ===== OPTIMIZED THEME VARIATIONS ===== */
215
+ [data-theme="blue"] {
216
+ --primary-color: #74b9ff;
217
+ --secondary-color: #81ecec;
218
+ --accent-color: #0984e3;
219
+ --background-overlay: rgba(116, 185, 255, 0.15);
220
+ --gradient-start: #74b9ff;
221
+ --gradient-end: #0984e3;
222
+ --text-glow: 0 0 10px rgba(116, 185, 255, 0.5);
223
+ --button-hover: rgba(116, 185, 255, 0.3);
224
+ --modal-title-color: #0a2340;
225
+ --switch-color: #3498db;
226
+
227
+ /* UI Component Colors */
228
+ --chat-bg: rgba(116, 185, 255, 0.9);
229
+ --chat-border: #74b9ff;
230
+ --chat-message-user-bg: #74b9ff;
231
+ --input-border: #74b9ff;
232
+ --input-focus-border: #0984e3;
233
+
234
+ /* Modal & Overlay Colors */
235
+ --modal-bg: rgba(116, 185, 255, 0.95);
236
+ --modal-border: #74b9ff;
237
+ --modal-header-bg: linear-gradient(135deg, #74b9ff, #81ecec);
238
+
239
+ /* Settings Panel & Tabs */
240
+ --settings-tab-active-bg: #74b9ff;
241
+ --settings-section-border: #3a4a7a;
242
+ --settings-tab-border: #74b9ff;
243
+
244
+ /* Slider Components */
245
+ --slider-value-border: #0984e3;
246
+
247
+ /* Toggle Switch */
248
+ --switch-bg-active: linear-gradient(90deg, #74b9ff, #0984e3);
249
+
250
+ /* Mic Button & Pulse Effect */
251
+ --mic-button-border: #74b9ff;
252
+ --mic-button-shadow: 0 0 15px #74b9ff;
253
+ --mic-button-hover-bg: #74b9ff;
254
+ --mic-button-hover-shadow: 0 0 10px rgba(116, 185, 255, 0.5);
255
+ --mic-listening-border: #0984e3;
256
+ --mic-listening-shadow: 0 0 15px #0984e3;
257
+ --mic-pulse-color: rgba(9, 132, 227, 0.5);
258
+
259
+ /* Cards & Stats */
260
+ --stat-value-color: #0984e3;
261
+
262
+ /* Plugin Cards */
263
+ --plugin-card-border: #74b9ff;
264
+ --plugin-type-badge-bg: #0984e3;
265
+ --plugin-active-badge-bg: linear-gradient(90deg, #74b9ff, #0984e3);
266
+
267
+ /* Help Modal */
268
+ --creator-name-color: #0984e3;
269
+ --creator-avatar-bg: linear-gradient(135deg, #74b9ff, #81ecec);
270
+ --creator-role-bg: linear-gradient(135deg, #74b9ff, #81ecec);
271
+ --creator-role-color: #0a2340;
272
+ --philosophy-bg: rgba(116, 185, 255, 0.1);
273
+ --philosophy-border: rgba(116, 185, 255, 0.3);
274
+ --philosophy-border-left: #74b9ff;
275
+ --feature-icon-color: #0984e3;
276
+ --feature-title-color: #0984e3;
277
+
278
+ /* Select Options */
279
+ --select-option-hover-bg: #74b9ff;
280
+ --select-option-checked-bg: #0984e3;
281
+
282
+ /* Scrollbar */
283
+ --scrollbar-thumb-bg: rgba(116, 185, 255, 0.4);
284
+ --scrollbar-thumb-hover-bg: rgba(116, 185, 255, 0.6);
285
+ --scrollbar-thumb-active-bg: rgba(116, 185, 255, 0.8);
286
+
287
+ /* Model Colors */
288
+ --model-strength-color: #0984e3;
289
+ --model-strength-text: #fff;
290
+ --model-provider-color: #00b894;
291
+ --model-provider-text: #fff;
292
+
293
+ /* Text Colors */
294
+ --text-primary: #222;
295
+ --text-secondary: #555;
296
+
297
+ /* Character Selection Colors */
298
+ --character-selected-border: #0984e3;
299
+ --character-selected-bg: rgba(116, 185, 255, 0.13);
300
+ }
301
+
302
+ [data-theme="purple"] {
303
+ --primary-color: #a29bfe;
304
+ --secondary-color: #fd79a8;
305
+ --accent-color: #6c5ce7;
306
+ --background-overlay: rgba(162, 155, 254, 0.15);
307
+ --gradient-start: #a29bfe;
308
+ --gradient-end: #6c5ce7;
309
+ --text-glow: 0 0 10px rgba(162, 155, 254, 0.5);
310
+ --button-hover: rgba(162, 155, 254, 0.3);
311
+ --modal-title-color: #2d2250;
312
+ --switch-color: #a259ff;
313
+
314
+ /* UI Component Colors */
315
+ --chat-bg: rgba(162, 155, 254, 0.9);
316
+ --chat-border: #a29bfe;
317
+ --chat-message-user-bg: #a29bfe;
318
+ --input-border: #a29bfe;
319
+ --input-focus-border: #6c5ce7;
320
+
321
+ /* Modal & Overlay Colors */
322
+ --modal-bg: rgba(162, 155, 254, 0.95);
323
+ --modal-border: #a29bfe;
324
+ --modal-header-bg: linear-gradient(135deg, #a29bfe, #fd79a8);
325
+
326
+ /* Settings Panel & Tabs */
327
+ --settings-tab-active-bg: #a29bfe;
328
+ --settings-section-border: #4a3a7a;
329
+ --settings-tab-border: #a29bfe;
330
+ --settings-bg: #2d2250;
331
+
332
+ /* Slider Components */
333
+ --slider-value-border: #6c5ce7;
334
+
335
+ /* Toggle Switch */
336
+ --switch-bg-active: linear-gradient(90deg, #a29bfe, #6c5ce7);
337
+
338
+ /* Mic Button & Pulse Effect */
339
+ --mic-button-border: #a29bfe;
340
+ --mic-button-shadow: 0 0 15px #a29bfe;
341
+ --mic-button-hover-bg: #a29bfe;
342
+ --mic-button-hover-shadow: 0 0 10px rgba(162, 155, 254, 0.5);
343
+ --mic-listening-border: #6c5ce7;
344
+ --mic-listening-shadow: 0 0 15px #6c5ce7;
345
+ --mic-pulse-color: rgba(108, 92, 231, 0.5);
346
+
347
+ /* Cards & Stats */
348
+ --stat-value-color: #6c5ce7;
349
+
350
+ /* Plugin Cards */
351
+ --plugin-card-border: #a29bfe;
352
+ --plugin-type-badge-bg: #6c5ce7;
353
+ --plugin-active-badge-bg: linear-gradient(90deg, #a29bfe, #6c5ce7);
354
+
355
+ /* Help Modal */
356
+ --creator-name-color: #6c5ce7;
357
+ --creator-avatar-bg: linear-gradient(135deg, #a29bfe, #fd79a8);
358
+ --creator-role-bg: linear-gradient(135deg, #a29bfe, #fd79a8);
359
+ --creator-role-color: #2d2250;
360
+ --philosophy-bg: rgba(162, 155, 254, 0.1);
361
+ --philosophy-border: rgba(162, 155, 254, 0.3);
362
+ --philosophy-border-left: #a29bfe;
363
+ --feature-icon-color: #6c5ce7;
364
+ --feature-title-color: #6c5ce7;
365
+
366
+ /* Select Options */
367
+ --select-option-hover-bg: #a29bfe;
368
+ --select-option-checked-bg: #6c5ce7;
369
+
370
+ /* Scrollbar */
371
+ --scrollbar-thumb-bg: rgba(162, 155, 254, 0.4);
372
+ --scrollbar-thumb-hover-bg: rgba(162, 155, 254, 0.6);
373
+ --scrollbar-thumb-active-bg: rgba(162, 155, 254, 0.8);
374
+
375
+ /* Model Colors */
376
+ --model-strength-color: #6c5ce7;
377
+ --model-strength-text: #fff;
378
+ --model-provider-color: #fd79a8;
379
+ --model-provider-text: #fff;
380
+
381
+ /* Text Colors */
382
+ --text-primary: #2d2250;
383
+ --text-secondary: #555;
384
+
385
+ /* Character Selection Colors */
386
+ --character-selected-border: #6c5ce7;
387
+ --character-selected-bg: rgba(162, 155, 254, 0.13);
388
+
389
+ /* Additional Purple Theme Variables */
390
+ --button-primary-bg: var(--primary-color);
391
+ --button-primary-text: var(--text-on-primary);
392
+ --button-primary-hover-bg: var(--accent-color);
393
+ --button-secondary-bg: rgba(255, 255, 255, 0.1);
394
+ --button-secondary-text: var(--text-on-background);
395
+ --guide-step-number-color: var(--primary-color);
396
+ --guide-step-number-bg: rgba(162, 155, 254, var(--hover-opacity));
397
+ --waiting-indicator-color: var(--primary-color);
398
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
399
+ }
400
+
401
+ [data-theme="green"] {
402
+ --primary-color: #27ae60;
403
+ --secondary-color: #2ecc71;
404
+ --accent-color: #16a085;
405
+ --background-overlay: rgba(39, 174, 96, 0.15);
406
+ --gradient-start: #27ae60;
407
+ --gradient-end: #16a085;
408
+ --text-glow: 0 0 10px rgba(39, 174, 96, 0.5);
409
+ --button-hover: rgba(39, 174, 96, 0.3);
410
+ --modal-title-color: #1a3d2e;
411
+ --switch-color: #27ae60;
412
+
413
+ /* Contrast and Accessibility for Green Theme */
414
+ --text-on-primary: #ffffff;
415
+ --text-on-secondary: #1a3d2e;
416
+ --text-on-accent: #ffffff;
417
+ --text-on-background: #ffffff;
418
+
419
+ /* UI Component Colors */
420
+ --chat-bg: rgba(39, 174, 96, 0.9);
421
+ --chat-border: #27ae60;
422
+ --chat-text: var(--text-on-primary);
423
+ --chat-message-user-bg: #27ae60;
424
+ --chat-message-user-text: var(--text-on-primary);
425
+ --chat-message-kimi-bg: rgba(255, 255, 255, 0.15);
426
+ --chat-message-kimi-text: var(--text-on-background);
427
+ --input-border: #27ae60;
428
+ --input-focus-border: #16a085;
429
+ --input-text: var(--text-on-background);
430
+
431
+ /* Modal & Overlay Colors */
432
+ --modal-bg: rgba(39, 174, 96, 0.95);
433
+ --modal-border: #27ae60;
434
+ --modal-header-bg: linear-gradient(135deg, #27ae60, #2ecc71);
435
+ --modal-text: var(--text-on-primary);
436
+
437
+ /* Settings Panel & Tabs */
438
+ --settings-text: var(--text-on-background);
439
+ --settings-tab-active-bg: #27ae60;
440
+ --settings-tab-active-color: var(--text-on-primary);
441
+ --settings-section-border: #27ae60;
442
+ --settings-tab-border: #27ae60;
443
+ --settings-section-header-color: var(--text-on-background);
444
+
445
+ /* Button Colors */
446
+ --button-primary-bg: var(--primary-color);
447
+ --button-primary-text: var(--text-on-primary);
448
+ --button-primary-hover-bg: var(--accent-color);
449
+ --button-secondary-bg: rgba(255, 255, 255, 0.1);
450
+ --button-secondary-text: var(--text-on-background);
451
+
452
+ /* Slider Components */
453
+ --slider-value-border: #16a085;
454
+ --slider-thumb-bg: var(--primary-color);
455
+ --slider-thumb-hover-bg: var(--accent-color);
456
+
457
+ /* Toggle Switch */
458
+ --switch-bg-active: linear-gradient(90deg, #27ae60, #16a085);
459
+
460
+ /* Mic Button & Pulse Effect */
461
+ --mic-button-border: #27ae60;
462
+ --mic-button-shadow: 0 0 15px #27ae60;
463
+ --mic-button-hover-bg: #27ae60;
464
+ --mic-button-hover-shadow: 0 0 10px rgba(39, 174, 96, 0.5);
465
+ --mic-button-icon-color: var(--text-on-primary);
466
+ --mic-listening-border: #16a085;
467
+ --mic-listening-shadow: 0 0 15px #16a085;
468
+ --mic-pulse-color: rgba(22, 160, 133, 0.5);
469
+ --mic-pulse-listening-color: rgba(22, 160, 133, 0.4);
470
+
471
+ /* Cards & Stats */
472
+ --card-text: var(--text-on-background);
473
+ --stat-value-color: #16a085;
474
+
475
+ /* Plugin Cards */
476
+ --plugin-card-border: #27ae60;
477
+ --plugin-card-title-color: var(--text-on-background);
478
+ --plugin-card-desc-color: #e0cfe6;
479
+ --plugin-card-author-color: #bfa6b6;
480
+ --plugin-type-badge-bg: #16a085;
481
+ --plugin-type-badge-text: var(--text-on-accent);
482
+ --plugin-active-badge-bg: linear-gradient(90deg, #27ae60, #16a085);
483
+ --plugin-active-badge-text: var(--text-on-primary);
484
+
485
+ /* Help Modal */
486
+ --creator-name-color: #16a085;
487
+ --creator-avatar-bg: linear-gradient(135deg, #27ae60, #2ecc71);
488
+ --creator-role-bg: linear-gradient(135deg, #27ae60, #2ecc71);
489
+ --creator-role-color: var(--text-on-secondary);
490
+ --philosophy-bg: rgba(39, 174, 96, 0.1);
491
+ --philosophy-border: rgba(39, 174, 96, var(--border-opacity));
492
+ --philosophy-border-left: #27ae60;
493
+ --feature-icon-color: #147190;
494
+ --feature-title-color: #147190;
495
+ --guide-step-number-color: var(--primary-color);
496
+ --guide-step-number-bg: rgba(39, 174, 96, var(--hover-opacity));
497
+
498
+ /* Select Options */
499
+ --select-option-hover-bg: #27ae60;
500
+ --select-option-checked-bg: #16a085;
501
+
502
+ /* Scrollbar */
503
+ --scrollbar-thumb-bg: rgba(39, 174, 96, 0.4);
504
+ --scrollbar-thumb-hover-bg: rgba(39, 174, 96, 0.6);
505
+ --scrollbar-thumb-active-bg: rgba(39, 174, 96, 0.8);
506
+
507
+ /* Model Colors */
508
+ --model-strength-color: #27ae60;
509
+ --model-strength-text: var(--text-on-primary);
510
+ --model-provider-color: #2ecc71;
511
+ --model-provider-text: var(--text-on-primary);
512
+
513
+ /* Text Colors */
514
+ --text-primary: var(--text-on-background);
515
+ --text-secondary: #4e6151;
516
+ --text-muted: rgba(255, 255, 255, 0.6);
517
+
518
+ /* Character Selection Colors */
519
+ --character-selected-border: #16a085;
520
+ --character-selected-bg: rgba(39, 174, 96, 0.13);
521
+ --character-name-color: var(--text-on-background);
522
+
523
+ /* Additional Green Theme Variables */
524
+ --waiting-indicator-color: var(--primary-color);
525
+ --loading-spinner-color: var(--primary-color);
526
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
527
+ }
528
+
529
+ [data-theme="dark"] {
530
+ --primary-color: #5e60ce;
531
+ --secondary-color: #23262f;
532
+ --accent-color: #8b5cf6;
533
+ --background-overlay: rgba(24, 26, 32, 0.85);
534
+ --gradient-start: #5e60ce;
535
+ --gradient-end: #8b5cf6;
536
+ --text-glow: 0 0 10px rgba(94, 96, 206, 0.3);
537
+ --button-hover: rgba(94, 96, 206, 0.15);
538
+ --modal-title-color: #e0e0e0;
539
+ --switch-color: #5e60ce;
540
+
541
+ /* Contrast and Accessibility for Dark Theme */
542
+ --text-on-primary: #ffffff;
543
+ --text-on-secondary: #e0e0e0;
544
+ --text-on-accent: #ffffff;
545
+ --text-on-background: #e0e0e0;
546
+
547
+ /* UI Component Colors */
548
+ --chat-bg: rgba(24, 26, 32, 0.95);
549
+ --chat-border: #5e60ce;
550
+ --chat-text: var(--text-on-background);
551
+ --chat-message-user-bg: #23262f;
552
+ --chat-message-user-text: var(--text-on-background);
553
+ --chat-message-kimi-bg: #181a20;
554
+ --chat-message-kimi-text: var(--text-on-background);
555
+ --input-border: #5e60ce;
556
+ --input-focus-border: #8b5cf6;
557
+ --input-text: var(--text-on-background);
558
+
559
+ /* Modal & Overlay Colors */
560
+ --modal-bg: rgba(24, 26, 32, 0.98);
561
+ --modal-border: #5e60ce;
562
+ --modal-header-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
563
+ --modal-text: var(--text-on-background);
564
+
565
+ /* Settings Panel & Tabs */
566
+ --settings-bg: #0f1114;
567
+ --settings-text: var(--text-on-background);
568
+ --settings-tab-bg: #181a20;
569
+ --settings-tab-active-bg: #5e60ce;
570
+ --settings-tab-active-color: var(--text-on-primary);
571
+ --settings-section-bg: #1a1d23;
572
+ --settings-section-border: rgba(94, 96, 206, var(--border-opacity));
573
+ --settings-tab-border: #5e60ce;
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.05);
581
+ --button-secondary-text: var(--text-on-background);
582
+
583
+ /* Slider Components */
584
+ --slider-value-bg: #0f1114;
585
+ --slider-value-border: #5e60ce;
586
+ --slider-value-color: var(--text-on-background);
587
+ --slider-thumb-bg: var(--primary-color);
588
+ --slider-thumb-hover-bg: var(--accent-color);
589
+
590
+ /* Toggle Switch */
591
+ --switch-bg-inactive: rgba(255, 255, 255, 0.05);
592
+ --switch-bg-active: linear-gradient(90deg, #5e60ce, #8b5cf6);
593
+
594
+ /* Mic Button & Pulse Effect */
595
+ --mic-button-border: #5e60ce;
596
+ --mic-button-shadow: 0 0 15px #5e60ce;
597
+ --mic-button-hover-bg: #5e60ce;
598
+ --mic-button-hover-shadow: 0 0 10px rgba(94, 96, 206, 0.5);
599
+ --mic-button-icon-color: var(--text-on-primary);
600
+ --mic-listening-border: #8b5cf6;
601
+ --mic-listening-shadow: 0 0 15px #8b5cf6;
602
+ --mic-pulse-color: rgba(139, 92, 246, 0.5);
603
+ --mic-pulse-listening-color: rgba(139, 92, 246, 0.4);
604
+
605
+ /* Cards & Stats */
606
+ --card-bg: rgba(255, 255, 255, 0.02);
607
+ --card-border: rgba(255, 255, 255, 0.05);
608
+ --card-hover-bg: rgba(255, 255, 255, 0.05);
609
+ --card-text: var(--text-on-background);
610
+ --stat-value-color: #8b5cf6;
611
+
612
+ /* Plugin Cards */
613
+ --plugin-card-bg: linear-gradient(135deg, #1a1d23 80%, rgba(24, 26, 32, 0.98) 100%);
614
+ --plugin-card-border: #5e60ce;
615
+ --plugin-card-title-color: var(--text-on-background);
616
+ --plugin-card-desc-color: rgba(224, 224, 224, 0.7);
617
+ --plugin-card-author-color: rgba(224, 224, 224, 0.5);
618
+ --plugin-type-badge-bg: #8b5cf6;
619
+ --plugin-type-badge-text: var(--text-on-accent);
620
+ --plugin-active-badge-bg: linear-gradient(90deg, #5e60ce, #8b5cf6);
621
+ --plugin-active-badge-text: var(--text-on-primary);
622
+
623
+ /* Help Modal */
624
+ --help-modal-bg: rgba(24, 26, 32, 0.98);
625
+ --help-modal-border: #5e60ce;
626
+ --creator-card-bg: rgba(255, 255, 255, 0.02);
627
+ --creator-card-border: rgba(255, 255, 255, 0.05);
628
+ --creator-name-color: #8b5cf6;
629
+ --creator-avatar-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
630
+ --creator-role-bg: linear-gradient(135deg, #5e60ce, #8b5cf6);
631
+ --creator-role-color: var(--text-on-primary);
632
+ --philosophy-bg: rgba(94, 96, 206, 0.05);
633
+ --philosophy-border: rgba(94, 96, 206, var(--border-opacity));
634
+ --philosophy-border-left: #5e60ce;
635
+ --feature-item-bg: rgba(255, 255, 255, 0.02);
636
+ --feature-item-border: rgba(94, 96, 206, var(--border-opacity));
637
+ --feature-icon-color: #8b5cf6;
638
+ --feature-title-color: #8b5cf6;
639
+ --feature-text-color: rgba(224, 224, 224, 0.7);
640
+ --guide-step-bg: rgba(255, 255, 255, 0.02);
641
+ --guide-step-number-color: var(--primary-color);
642
+ --guide-step-number-bg: rgba(94, 96, 206, var(--hover-opacity));
643
+ --tip-item-bg: rgba(255, 255, 255, 0.02);
644
+
645
+ /* Select Options */
646
+ --select-option-bg: rgba(24, 26, 32, 0.95);
647
+ --select-option-text: var(--text-on-background);
648
+ --select-option-hover-bg: #5e60ce;
649
+ --select-option-checked-bg: #8b5cf6;
650
+
651
+ /* Scrollbar */
652
+ --scrollbar-track-bg: rgba(255, 255, 255, 0.02);
653
+ --scrollbar-thumb-bg: rgba(94, 96, 206, 0.4);
654
+ --scrollbar-thumb-hover-bg: rgba(94, 96, 206, 0.6);
655
+ --scrollbar-thumb-active-bg: rgba(94, 96, 206, 0.8);
656
+
657
+ /* Model Colors */
658
+ --model-card-bg: rgba(255, 255, 255, 0.02);
659
+ --model-card-border: rgba(255, 255, 255, 0.05);
660
+ --model-card-hover-bg: rgba(255, 255, 255, 0.05);
661
+ --model-card-selected-border: var(--primary-color);
662
+ --model-name-color: var(--text-on-background);
663
+ --model-description-color: rgba(224, 224, 224, 0.7);
664
+ --model-strength-color: #5e60ce;
665
+ --model-strength-text: var(--text-on-primary);
666
+ --model-provider-color: #8b5cf6;
667
+ --model-provider-text: var(--text-on-accent);
668
+
669
+ /* Text Colors */
670
+ --text-primary: var(--text-on-background);
671
+ --text-secondary: #9ca3af;
672
+ --text-muted: rgba(224, 224, 224, 0.6);
673
+
674
+ /* Character Selection Colors */
675
+ --character-card: rgba(255, 255, 255, 0.02);
676
+ --character-card-border: rgba(255, 255, 255, 0.05);
677
+ --character-card-hover-bg: rgba(255, 255, 255, 0.05);
678
+ --character-selected-border: #8b5cf6;
679
+ --character-selected-bg: rgba(94, 96, 206, 0.1);
680
+ --character-name-color: var(--text-on-background);
681
+
682
+ /* Additional Dark Theme Variables */
683
+ --waiting-indicator-color: var(--primary-color);
684
+ --loading-spinner-color: var(--primary-color);
685
+ --progress-bg: rgba(255, 255, 255, 0.05);
686
+ --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color));
687
+ --progress-text-color: var(--text-on-background);
688
+ --transcript-bg: rgba(0, 0, 0, 0.9);
689
+ --transcript-text: var(--text-on-background);
690
+ --transcript-border: var(--primary-color);
691
+ }
692
+
693
+ /* ===== ANIMATION MANAGEMENT ===== */
694
+ /* Disable animations when requested */
695
+ body.no-animations *,
696
+ [data-animations="false"] * {
697
+ animation: none !important;
698
+ transition: none !important;
699
+ }
700
+
701
+ /* Important: Keep mic button animations even when animations are disabled */
702
+ body.no-animations .mic-button,
703
+ body.no-animations .mic-button *,
704
+ body.no-animations .mic-button::after,
705
+ [data-animations="false"] .mic-button,
706
+ [data-animations="false"] .mic-button *,
707
+ [data-animations="false"] .mic-button::after {
708
+ animation: revert !important;
709
+ transition: revert !important;
710
+ }
711
+
712
+ /* Keep loading screen animations */
713
+ body.no-animations #loading-screen,
714
+ body.no-animations #loading-screen *,
715
+ [data-animations="false"] #loading-screen,
716
+ [data-animations="false"] #loading-screen * {
717
+ animation: revert !important;
718
+ transition: revert !important;
719
+ }
720
+
721
+ /* Ensure critical hover effects remain functional */
722
+ body.animations-enabled .kimi-button:hover,
723
+ body.animations-enabled .control-button-unified:hover {
724
+ transform: translateY(-2px);
725
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
726
+ }
727
+
728
+ body.animations-enabled .mic-button:hover {
729
+ transform: scale(1.1);
730
+ transition: all 0.2s ease;
731
+ }
732
+
733
+ /* ===== GLOBAL STYLES ===== */
734
+ * {
735
+ margin: 0;
736
+ padding: 0;
737
+ box-sizing: border-box;
738
+ }
739
+
740
+ html,
741
+ body {
742
+ height: 100%;
743
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
744
+ color: white;
745
+ overflow: hidden;
746
+ }
747
+
748
+ .video-container {
749
+ position: fixed;
750
+ top: 0;
751
+ left: 0;
752
+ width: 100%;
753
+ height: 100%;
754
+ z-index: -1;
755
+ background-color: #1a1a1a;
756
+ }
757
+
758
+ .bg-video.active {
759
+ opacity: 1;
760
+ }
761
+
762
+ .bg-video {
763
+ position: absolute;
764
+ top: 0;
765
+ left: 0;
766
+ width: 100%;
767
+ height: 100%;
768
+ object-fit: contain;
769
+ opacity: 0;
770
+ transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
771
+ background-color: #1a1a1a;
772
+ will-change: opacity;
773
+ backface-visibility: hidden;
774
+ }
775
+
776
+ .bg-video.transitioning {
777
+ opacity: 0;
778
+ transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
779
+ pointer-events: none;
780
+ }
781
+
782
+ @media (max-width: 600px) {
783
+ .bg-video {
784
+ object-fit: cover;
785
+ object-position: center center;
786
+ }
787
+ }
788
+
789
+ .content-overlay {
790
+ position: relative;
791
+ height: 100vh;
792
+ width: 100%;
793
+ display: flex;
794
+ flex-direction: column;
795
+ justify-content: flex-end;
796
+ align-items: center;
797
+ padding: 20px;
798
+ background-color: var(--background-overlay);
799
+ opacity: var(--interface-opacity);
800
+ z-index: 1;
801
+ }
802
+
803
+ .top-bar {
804
+ width: 100%;
805
+ max-width: 500px;
806
+ text-align: center;
807
+ margin-top: 10px;
808
+ }
809
+
810
+ .top-bar label {
811
+ font-size: 1rem;
812
+ font-weight: 600;
813
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
814
+ margin-bottom: 8px;
815
+ display: block;
816
+ }
817
+
818
+ .progress-container {
819
+ width: 100%;
820
+ height: 12px;
821
+ background-color: var(--progress-bg);
822
+ border-radius: 10px;
823
+ overflow: hidden;
824
+ }
825
+
826
+ .progress-fill {
827
+ height: 100%;
828
+ width: 50%; /* Changed from 65% to match new default favorability level */
829
+ background: var(--progress-fill-bg);
830
+ border-radius: 10px;
831
+ transition: width 0.5s ease-in-out;
832
+ box-shadow: var(--text-glow);
833
+ }
834
+
835
+ /* Center content styles can be added here if needed */
836
+
837
+ .transcript-container {
838
+ position: absolute;
839
+ bottom: 180px;
840
+ left: 50%;
841
+ transform: translateX(-50%);
842
+ width: 80%;
843
+ max-width: 600px;
844
+ padding: 15px;
845
+ background: var(--transcript-bg);
846
+ backdrop-filter: blur(10px);
847
+ border-radius: 10px;
848
+ border: 1px solid var(--transcript-border);
849
+ text-align: center;
850
+ opacity: 0;
851
+ transition: opacity 0.3s ease-in-out;
852
+ pointer-events: none;
853
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
854
+ }
855
+
856
+ .transcript-container.visible {
857
+ opacity: 1;
858
+ }
859
+
860
+ #transcript {
861
+ font-size: 1.2rem;
862
+ color: var(--transcript-text);
863
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7);
864
+ }
865
+
866
+ /* Interface de Chat avec Kimi - STYLE UNIFIÉ */
867
+ .chat-container {
868
+ position: fixed;
869
+ top: 20px;
870
+ right: 20px;
871
+ width: 350px;
872
+ max-width: calc(100vw - 40px);
873
+ height: 500px;
874
+ max-height: 80vh;
875
+ background: var(--chat-bg);
876
+ backdrop-filter: blur(20px);
877
+ border-radius: 15px;
878
+ border: 1px solid var(--chat-border);
879
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
880
+ display: none;
881
+ flex-direction: column;
882
+ z-index: 1000;
883
+ overflow: hidden;
884
+ transform: translateX(400px);
885
+ opacity: 0;
886
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
887
+ }
888
+
889
+ .chat-container.visible {
890
+ display: flex;
891
+ flex-direction: column;
892
+ transform: translateX(0);
893
+ opacity: 1;
894
+ }
895
+
896
+ .chat-header {
897
+ padding: 15px 20px;
898
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
899
+ display: flex;
900
+ justify-content: space-between;
901
+ align-items: center;
902
+ background: var(--chat-header-bg);
903
+ }
904
+
905
+ .chat-header h3 {
906
+ margin: 0;
907
+ color: var(--chat-text);
908
+ font-size: 1.1rem;
909
+ font-weight: 600;
910
+ }
911
+
912
+ .chat-header h3 i {
913
+ margin-right: 8px;
914
+ color: var(--accent-color);
915
+ }
916
+
917
+ .chat-toggle {
918
+ background: none;
919
+ border: none;
920
+ color: var(--chat-text);
921
+ cursor: pointer;
922
+ width: 30px;
923
+ height: 30px;
924
+ border-radius: 50%;
925
+ display: flex;
926
+ align-items: center;
927
+ justify-content: center;
928
+ transition: all 0.3s ease;
929
+ font-size: 1.1rem;
930
+ }
931
+
932
+ .chat-toggle:hover {
933
+ background: rgba(255, 255, 255, 0.1);
934
+ transform: scale(1.1);
935
+ }
936
+
937
+ .chat-messages {
938
+ flex: 1;
939
+ padding: 18px;
940
+ overflow-y: auto;
941
+ display: flex;
942
+ flex-direction: column;
943
+ gap: 15px;
944
+ }
945
+
946
+ .message {
947
+ max-width: 92%;
948
+ padding: 10px 14px;
949
+ border-radius: 16px;
950
+ word-wrap: break-word;
951
+ position: relative;
952
+ }
953
+
954
+ .message.user {
955
+ align-self: flex-end;
956
+ background: var(--chat-message-user-bg);
957
+ color: var(--chat-message-user-text);
958
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
959
+ }
960
+
961
+ .message.kimi {
962
+ align-self: flex-start;
963
+ background: var(--chat-message-kimi-bg);
964
+ color: var(--chat-message-kimi-text);
965
+ border: 1px solid rgba(255, 255, 255, 0.1);
966
+ }
967
+
968
+ .message-time {
969
+ font-size: 0.75rem;
970
+ opacity: 0.7;
971
+ margin-top: 5px;
972
+ }
973
+
974
+ .chat-input-container {
975
+ padding: 15px 20px;
976
+ background: var(--chat-header-bg);
977
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
978
+ display: flex;
979
+ gap: 10px;
980
+ align-items: center;
981
+ }
982
+
983
+ #chat-input {
984
+ flex: 1;
985
+ padding: 12px 15px;
986
+ background: var(--chat-input-bg);
987
+ border: 1px solid rgba(255, 255, 255, 0.2);
988
+ border-radius: 25px;
989
+ color: var(--chat-input-text);
990
+ font-size: 0.95rem;
991
+ outline: none;
992
+ transition: all 0.3s ease;
993
+ }
994
+
995
+ #chat-input:focus {
996
+ background: rgba(255, 255, 255, 0.15);
997
+ border-color: var(--accent-color);
998
+ box-shadow: 0 0 10px var(--primary-color);
999
+ }
1000
+
1001
+ #chat-input::placeholder {
1002
+ color: var(--chat-input-placeholder);
1003
+ }
1004
+
1005
+ #send-button {
1006
+ width: 45px;
1007
+ height: 45px;
1008
+ background: var(--chat-message-user-bg);
1009
+ border: none;
1010
+ border-radius: 50%;
1011
+ color: white;
1012
+ cursor: pointer;
1013
+ display: flex;
1014
+ justify-content: center;
1015
+ align-items: center;
1016
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1017
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
1018
+ backdrop-filter: blur(10px);
1019
+ border: 1px solid rgba(255, 255, 255, 0.1);
1020
+ }
1021
+
1022
+ #send-button:hover {
1023
+ transform: scale(1.1);
1024
+ box-shadow:
1025
+ var(--text-glow),
1026
+ 0 4px 15px rgba(0, 0, 0, 0.3);
1027
+ background: var(--accent-color);
1028
+ }
1029
+
1030
+ #send-button:active {
1031
+ transform: scale(0.95);
1032
+ transition: all 0.1s ease;
1033
+ }
1034
+
1035
+ #chat-delete {
1036
+ background: none;
1037
+ border: none;
1038
+ color: #d9534f;
1039
+ font-size: 1.2em;
1040
+ cursor: pointer;
1041
+ margin-left: 8px;
1042
+ transition: color 0.2s;
1043
+ }
1044
+
1045
+ #chat-delete:hover {
1046
+ color: #b52a1a;
1047
+ }
1048
+
1049
+ #chat-delete i {
1050
+ pointer-events: none;
1051
+ }
1052
+
1053
+ .delete-message-btn {
1054
+ background: none;
1055
+ border: none;
1056
+ color: #aaa;
1057
+ cursor: pointer;
1058
+ padding: 0 0 0 8px;
1059
+ display: flex;
1060
+ align-items: center;
1061
+ transition: color 0.2s;
1062
+ }
1063
+ .delete-message-btn:hover {
1064
+ color: #e74c3c;
1065
+ }
1066
+ .delete-message-btn i {
1067
+ pointer-events: none;
1068
+ }
1069
+
1070
+ /* ===== UNIFIED BUTTON COMPONENTS ===== */
1071
+ .kimi-button,
1072
+ .control-button-unified {
1073
+ background: var(--button-primary-bg);
1074
+ border: none;
1075
+ border-radius: 8px;
1076
+ color: var(--button-primary-text);
1077
+ padding: 10px 20px;
1078
+ font-size: 0.9rem;
1079
+ font-weight: 500;
1080
+ cursor: pointer;
1081
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1082
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
1083
+ position: relative;
1084
+ overflow: hidden;
1085
+ backdrop-filter: blur(10px);
1086
+ border: 1px solid rgba(255, 255, 255, 0.1);
1087
+ }
1088
+
1089
+ .kimi-button::before,
1090
+ .control-button-unified::before {
1091
+ content: "";
1092
+ position: absolute;
1093
+ top: 0;
1094
+ left: -100%;
1095
+ width: 100%;
1096
+ height: 100%;
1097
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
1098
+ transition: left 0.5s ease;
1099
+ }
1100
+
1101
+ .kimi-button:hover,
1102
+ .control-button-unified:hover {
1103
+ transform: translateY(-2px);
1104
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
1105
+ background: var(--button-primary-hover-bg);
1106
+ }
1107
+
1108
+ .kimi-button:hover::before,
1109
+ .control-button-unified:hover::before {
1110
+ left: 100%;
1111
+ }
1112
+
1113
+ .kimi-button:active,
1114
+ .control-button-unified:active {
1115
+ transform: translateY(0);
1116
+ transition: all 0.1s ease;
1117
+ }
1118
+
1119
+ .kimi-button:disabled,
1120
+ .control-button-unified:disabled {
1121
+ opacity: 0.5;
1122
+ cursor: not-allowed;
1123
+ transform: none;
1124
+ }
1125
+
1126
+ /* Button Variants */
1127
+ .kimi-button.danger {
1128
+ background: var(--button-danger-bg);
1129
+ color: var(--button-danger-text);
1130
+ }
1131
+ .kimi-button.danger:hover {
1132
+ background: var(--button-danger-hover-bg);
1133
+ }
1134
+ .kimi-button.success {
1135
+ background: linear-gradient(135deg, #26de81, #20bf6b);
1136
+ }
1137
+ .kimi-button.success:hover {
1138
+ background: linear-gradient(135deg, #20bf6b, #26de81);
1139
+ }
1140
+ .kimi-button.secondary {
1141
+ background: var(--button-secondary-bg);
1142
+ color: var(--button-secondary-text);
1143
+ }
1144
+ .kimi-button.secondary:hover {
1145
+ background: var(--button-secondary-hover-bg);
1146
+ }
1147
+
1148
+ /* Circular Control Buttons */
1149
+ .control-button-unified {
1150
+ width: 50px;
1151
+ height: 50px;
1152
+ border-radius: 50%;
1153
+ padding: 0;
1154
+ display: flex;
1155
+ align-items: center;
1156
+ justify-content: center;
1157
+ background: var(--button-hover);
1158
+ box-shadow: var(--mic-button-shadow);
1159
+ border: 1px solid var(--primary-color);
1160
+ }
1161
+
1162
+ .control-button-unified:hover {
1163
+ transform: translateY(-2px) scale(1.05);
1164
+ background: var(--primary-color);
1165
+ box-shadow: var(--text-glow);
1166
+ }
1167
+
1168
+ .control-button-unified i {
1169
+ font-size: 1.2rem;
1170
+ transition: transform 0.3s ease;
1171
+ }
1172
+
1173
+ .control-button-unified:hover i {
1174
+ transform: scale(1.1);
1175
+ }
1176
+
1177
+ /* ===== UNIFIED FORM COMPONENTS ===== */
1178
+
1179
+ /* Select Elements */
1180
+ .kimi-select,
1181
+ .kimi-select-unified {
1182
+ background: var(--select-bg);
1183
+ border: 1px solid var(--select-border);
1184
+ border-radius: 8px;
1185
+ color: var(--select-text);
1186
+ padding: 8px 12px;
1187
+ font-size: 0.9rem;
1188
+ outline: none;
1189
+ transition: all 0.3s ease;
1190
+ backdrop-filter: blur(10px);
1191
+ cursor: pointer;
1192
+ width: 100%;
1193
+ box-sizing: border-box;
1194
+ }
1195
+
1196
+ .kimi-select:hover,
1197
+ .kimi-select-unified:hover {
1198
+ background: var(--input-focus-bg);
1199
+ border-color: var(--accent-color);
1200
+ box-shadow: 0 0 10px var(--primary-color);
1201
+ }
1202
+
1203
+ .kimi-select:focus,
1204
+ .kimi-select-unified:focus {
1205
+ background: var(--input-focus-bg);
1206
+ border-color: var(--input-focus-border);
1207
+ box-shadow: 0 0 15px var(--primary-color);
1208
+ }
1209
+
1210
+ .kimi-select option,
1211
+ .kimi-select-unified option {
1212
+ background: var(--select-option-bg);
1213
+ color: var(--select-option-text);
1214
+ padding: 12px 15px;
1215
+ border: none;
1216
+ font-size: 0.9rem;
1217
+ transition: all 0.3s ease;
1218
+ }
1219
+
1220
+ .kimi-select option:hover,
1221
+ .kimi-select-unified option:hover,
1222
+ .kimi-select option:focus,
1223
+ .kimi-select-unified option:focus {
1224
+ background: var(--select-option-hover-bg);
1225
+ color: var(--select-option-hover-text);
1226
+ }
1227
+
1228
+ .kimi-select option:checked,
1229
+ .kimi-select-unified option:checked,
1230
+ .kimi-select option:selected,
1231
+ .kimi-select-unified option:selected {
1232
+ background: var(--select-option-checked-bg);
1233
+ color: var(--select-option-checked-text);
1234
+ font-weight: 600;
1235
+ box-shadow: 0 0 10px var(--primary-color);
1236
+ }
1237
+
1238
+ /* Input Elements */
1239
+ .kimi-input,
1240
+ .kimi-input-unified {
1241
+ background: var(--input-bg);
1242
+ border: 1px solid var(--input-border);
1243
+ border-radius: 8px;
1244
+ color: var(--input-text);
1245
+ padding: 8px 12px;
1246
+ font-size: 0.9rem;
1247
+ outline: none;
1248
+ transition: all 0.3s ease;
1249
+ backdrop-filter: blur(10px);
1250
+ width: 100%;
1251
+ box-sizing: border-box;
1252
+ }
1253
+
1254
+ .kimi-input:focus,
1255
+ .kimi-input-unified:focus {
1256
+ background: var(--input-focus-bg);
1257
+ border-color: var(--input-focus-border);
1258
+ box-shadow: 0 0 15px var(--primary-color);
1259
+ }
1260
+
1261
+ .kimi-input::placeholder,
1262
+ .kimi-input-unified::placeholder {
1263
+ color: var(--input-placeholder);
1264
+ }
1265
+
1266
+ /* Slider Elements */
1267
+ .kimi-slider,
1268
+ .kimi-slider-unified {
1269
+ -webkit-appearance: none;
1270
+ appearance: none;
1271
+ height: 6px;
1272
+ border-radius: 3px;
1273
+ background: var(--slider-track-bg);
1274
+ outline: none;
1275
+ transition: all 0.3s ease;
1276
+ cursor: pointer;
1277
+ }
1278
+
1279
+ .kimi-slider::-webkit-slider-thumb,
1280
+ .kimi-slider-unified::-webkit-slider-thumb {
1281
+ -webkit-appearance: none;
1282
+ appearance: none;
1283
+ width: 18px;
1284
+ height: 18px;
1285
+ border-radius: 50%;
1286
+ background: var(--slider-thumb-bg);
1287
+ cursor: pointer;
1288
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
1289
+ transition: all 0.3s ease;
1290
+ }
1291
+
1292
+ .kimi-slider::-webkit-slider-thumb:hover,
1293
+ .kimi-slider-unified::-webkit-slider-thumb:hover {
1294
+ transform: scale(1.2);
1295
+ background: var(--slider-thumb-hover-bg);
1296
+ box-shadow:
1297
+ 0 4px 15px rgba(0, 0, 0, 0.4),
1298
+ var(--text-glow);
1299
+ }
1300
+
1301
+ .kimi-slider::-moz-range-thumb,
1302
+ .kimi-slider-unified::-moz-range-thumb {
1303
+ width: 18px;
1304
+ height: 18px;
1305
+ border-radius: 50%;
1306
+ background: var(--slider-thumb-bg);
1307
+ cursor: pointer;
1308
+ border: none;
1309
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
1310
+ transition: all 0.3s ease;
1311
+ }
1312
+
1313
+ .kimi-slider::-moz-range-thumb:hover,
1314
+ .kimi-slider-unified::-moz-range-thumb:hover {
1315
+ transform: scale(1.2);
1316
+ background: var(--slider-thumb-hover-bg);
1317
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
1318
+ }
1319
+
1320
+ /* ===== CONSOLIDATED SCROLLBAR SYSTEM ===== */
1321
+ * {
1322
+ scrollbar-width: thin;
1323
+ scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-track-bg);
1324
+ }
1325
+
1326
+ *::-webkit-scrollbar {
1327
+ width: var(--scrollbar-width);
1328
+ height: var(--scrollbar-width);
1329
+ }
1330
+
1331
+ *::-webkit-scrollbar-track {
1332
+ background: var(--scrollbar-track-bg);
1333
+ border-radius: 4px;
1334
+ }
1335
+
1336
+ *::-webkit-scrollbar-thumb {
1337
+ background: var(--scrollbar-thumb-bg);
1338
+ border-radius: 4px;
1339
+ transition: all 0.3s ease;
1340
+ border: 1px solid rgba(255, 255, 255, 0.1);
1341
+ }
1342
+
1343
+ *::-webkit-scrollbar-thumb:hover {
1344
+ background: var(--scrollbar-thumb-hover-bg);
1345
+ transform: scale(1.1);
1346
+ }
1347
+
1348
+ *::-webkit-scrollbar-thumb:active {
1349
+ background: var(--scrollbar-thumb-active-bg);
1350
+ }
1351
+
1352
+ *::-webkit-scrollbar-corner {
1353
+ background: var(--scrollbar-corner-bg);
1354
+ }
1355
+
1356
+ /* ===== MAIN LAYOUT AND CONTROLS ===== */
1357
+ .control-buttons {
1358
+ display: flex;
1359
+ justify-content: center;
1360
+ align-items: center;
1361
+ gap: 20px;
1362
+ margin-bottom: 10px;
1363
+ }
1364
+
1365
+ .bottom-bar {
1366
+ width: 100%;
1367
+ display: flex;
1368
+ flex-direction: column;
1369
+ justify-content: center;
1370
+ align-items: center;
1371
+ position: relative;
1372
+ }
1373
+
1374
+ .favorability-text {
1375
+ position: absolute;
1376
+ right: 10px;
1377
+ top: 50%;
1378
+ transform: translateY(-50%);
1379
+ font-size: 0.85rem;
1380
+ font-weight: 600;
1381
+ color: var(--progress-text-color);
1382
+ text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
1383
+ }
1384
+
1385
+ .progress-container {
1386
+ position: relative;
1387
+ }
1388
+
1389
+ .mic-button {
1390
+ position: relative;
1391
+ width: 90px;
1392
+ height: 90px;
1393
+ background: var(--mic-button-bg);
1394
+ backdrop-filter: blur(10px);
1395
+ border: 1px solid var(--mic-button-border);
1396
+ border-radius: 50%;
1397
+ display: flex;
1398
+ justify-content: center;
1399
+ align-items: center;
1400
+ cursor: pointer;
1401
+ transition: all 0.2s ease;
1402
+ box-shadow: var(--mic-button-shadow);
1403
+ }
1404
+
1405
+ .mic-button:not(.is-listening)::after {
1406
+ display: none;
1407
+ }
1408
+ .mic-button.is-listening.mic-pulse-active::after {
1409
+ content: "";
1410
+ position: absolute;
1411
+ left: 50%;
1412
+ top: 50%;
1413
+ transform: translate(-50%, -50%);
1414
+ width: 120px;
1415
+ height: 120px;
1416
+ border-radius: 50%;
1417
+ background: var(--mic-pulse-color);
1418
+ opacity: 0.6;
1419
+ z-index: -1;
1420
+ animation: micPulseRed 1.2s infinite cubic-bezier(0.66, 0, 0, 1);
1421
+ pointer-events: none;
1422
+ }
1423
+
1424
+ .mic-button:hover {
1425
+ transform: scale(1.1);
1426
+ background: var(--mic-button-hover-bg);
1427
+ box-shadow: var(--mic-button-hover-shadow);
1428
+ }
1429
+
1430
+ .mic-button:active {
1431
+ transform: scale(0.95);
1432
+ }
1433
+
1434
+ .mic-button i {
1435
+ font-size: 28px;
1436
+ color: var(--mic-button-icon-color);
1437
+ transition: all 0.3s ease;
1438
+ }
1439
+
1440
+ .mic-button.is-listening {
1441
+ animation: pulse 1.5s infinite;
1442
+ border: 1px solid #27ae60;
1443
+ box-shadow: 0 0 15px #27ae60;
1444
+ }
1445
+
1446
+ .mic-button.is-listening i {
1447
+ animation: micPulse 0.8s infinite alternate;
1448
+ }
1449
+
1450
+ .mic-pulse-active {
1451
+ position: relative;
1452
+ box-shadow: 0 0 0 0 var(--primary-color);
1453
+ animation: micPulse 1.2s infinite cubic-bezier(0.66, 0, 0, 1);
1454
+ }
1455
+
1456
+ .mic-button.mic-pulse-active {
1457
+ position: relative;
1458
+ z-index: 1;
1459
+ }
1460
+ .mic-button.mic-pulse-active::after {
1461
+ content: "";
1462
+ position: absolute;
1463
+ left: 50%;
1464
+ top: 50%;
1465
+ transform: translate(-50%, -50%);
1466
+ width: 120px;
1467
+ height: 120px;
1468
+ border-radius: 50%;
1469
+ background: var(--mic-pulse-color);
1470
+ opacity: 0.6;
1471
+ z-index: -1;
1472
+ animation: micPulseRed 1.2s infinite cubic-bezier(0.66, 0, 0, 1);
1473
+ pointer-events: none;
1474
+ }
1475
+ @keyframes micPulseRed {
1476
+ 0% {
1477
+ transform: translate(-50%, -50%) scale(0.8);
1478
+ opacity: 0.6;
1479
+ }
1480
+ 70% {
1481
+ transform: translate(-50%, -50%) scale(1.2);
1482
+ opacity: 0;
1483
+ }
1484
+ 100% {
1485
+ transform: translate(-50%, -50%) scale(0.8);
1486
+ opacity: 0;
1487
+ }
1488
+ }
1489
+
1490
+ /* ===== CONSOLIDATED RESPONSIVE DESIGN ===== */
1491
+ @media (max-width: 768px) {
1492
+ .content-overlay {
1493
+ padding: 20px;
1494
+ }
1495
+
1496
+ .chat-container {
1497
+ top: 10px;
1498
+ right: 10px;
1499
+ left: 10px;
1500
+ width: auto;
1501
+ max-width: none;
1502
+ height: calc(100vh - 20px);
1503
+ max-height: none;
1504
+ border-radius: 10px;
1505
+ }
1506
+
1507
+ .chat-header {
1508
+ padding: 12px 15px;
1509
+ }
1510
+
1511
+ .chat-header h3 {
1512
+ font-size: 1rem;
1513
+ }
1514
+
1515
+ .control-buttons {
1516
+ gap: 15px;
1517
+ }
1518
+
1519
+ .control-button-unified {
1520
+ width: 45px;
1521
+ height: 45px;
1522
+ }
1523
+
1524
+ .control-button-unified i {
1525
+ font-size: 1.1rem;
1526
+ }
1527
+
1528
+ .top-bar {
1529
+ margin-top: 15px;
1530
+ }
1531
+
1532
+ .top-bar label {
1533
+ font-size: 0.9rem;
1534
+ }
1535
+
1536
+ .progress-container {
1537
+ height: 10px;
1538
+ }
1539
+
1540
+ .mic-button {
1541
+ width: 80px;
1542
+ height: 80px;
1543
+ }
1544
+
1545
+ .mic-button i {
1546
+ font-size: 34px;
1547
+ }
1548
+
1549
+ .transcript-container {
1550
+ bottom: 200px;
1551
+ width: 90%;
1552
+ }
1553
+
1554
+ #transcript {
1555
+ font-size: 1rem;
1556
+ }
1557
+
1558
+ .message {
1559
+ max-width: 85%;
1560
+ padding: 10px 14px;
1561
+ font-size: 0.9rem;
1562
+ }
1563
+
1564
+ .favorability-text {
1565
+ font-size: 0.75rem;
1566
+ }
1567
+
1568
+ .control-buttons {
1569
+ gap: 10px;
1570
+ justify-content: space-around;
1571
+ }
1572
+
1573
+ .kimi-select,
1574
+ .kimi-select-unified,
1575
+ .kimi-input,
1576
+ .kimi-input-unified {
1577
+ font-size: 16px; /* Prevents zoom on iOS */
1578
+ }
1579
+ }
1580
+
1581
+ @media (max-width: 600px) {
1582
+ .content-overlay {
1583
+ padding: 10px;
1584
+ }
1585
+
1586
+ .chat-container {
1587
+ top: 10px;
1588
+ right: 10px;
1589
+ left: 10px;
1590
+ width: auto;
1591
+ max-width: none;
1592
+ height: calc(100vh - 20px);
1593
+ max-height: none;
1594
+ border-radius: 10px;
1595
+ }
1596
+
1597
+ .chat-header {
1598
+ padding: 12px 15px;
1599
+ }
1600
+
1601
+ .chat-header h3 {
1602
+ font-size: 1rem;
1603
+ }
1604
+
1605
+ .control-buttons {
1606
+ gap: 15px;
1607
+ }
1608
+
1609
+ .chat-button,
1610
+ .settings-button {
1611
+ width: 50px;
1612
+ height: 50px;
1613
+ }
1614
+
1615
+ .chat-button i,
1616
+ .settings-button i {
1617
+ font-size: 20px;
1618
+ }
1619
+
1620
+ .top-bar {
1621
+ margin-top: 15px;
1622
+ }
1623
+
1624
+ .top-bar label {
1625
+ font-size: 0.9rem;
1626
+ }
1627
+
1628
+ .progress-container {
1629
+ height: 10px;
1630
+ }
1631
+
1632
+ .mic-button {
1633
+ width: 80px;
1634
+ height: 80px;
1635
+ }
1636
+
1637
+ .mic-button i {
1638
+ font-size: 34px;
1639
+ }
1640
+
1641
+ .transcript-container {
1642
+ bottom: 200px;
1643
+ width: 90%;
1644
+ }
1645
+
1646
+ #transcript {
1647
+ font-size: 1rem;
1648
+ }
1649
+
1650
+ .message {
1651
+ max-width: 92%;
1652
+ padding: 10px 14px;
1653
+ font-size: 0.9rem;
1654
+ }
1655
+
1656
+ .favorability-text {
1657
+ font-size: 0.75rem;
1658
+ }
1659
+ }
1660
+
1661
+ /* Animation pour l'indicateur d'attente */
1662
+ .waiting-indicator {
1663
+ display: block;
1664
+ text-align: center;
1665
+ width: 100%;
1666
+ box-sizing: border-box;
1667
+ margin: 6px 0 4px 0; /* discret au-dessus de l'input */
1668
+ opacity: 0;
1669
+ transition: opacity 0.25s ease-in-out;
1670
+ pointer-events: none;
1671
+ }
1672
+ .waiting-indicator.visible {
1673
+ opacity: 1;
1674
+ }
1675
+ .waiting-indicator span {
1676
+ display: inline-block;
1677
+ width: 8px;
1678
+ height: 8px;
1679
+ margin: 0 2px;
1680
+ background: var(--waiting-indicator-color);
1681
+ border-radius: 50%;
1682
+ opacity: 0.5;
1683
+ animation: waiting-bounce 1.4s infinite both;
1684
+ }
1685
+ .waiting-indicator span:nth-child(2) {
1686
+ animation-delay: 0.2s;
1687
+ }
1688
+ .waiting-indicator span:nth-child(3) {
1689
+ animation-delay: 0.4s;
1690
+ }
1691
+ @keyframes waiting-bounce {
1692
+ 0%,
1693
+ 80%,
1694
+ 100% {
1695
+ transform: scale(0.7);
1696
+ opacity: 0.5;
1697
+ }
1698
+ 40% {
1699
+ transform: scale(1);
1700
+ opacity: 1;
1701
+ }
1702
+ }
1703
+
1704
+ /* Global typing indicator near mic button */
1705
+ .global-typing-indicator {
1706
+ display: none;
1707
+ align-items: center;
1708
+ justify-content: center;
1709
+ width: 36px;
1710
+ height: 36px;
1711
+ margin: 0 6px;
1712
+ border-radius: 18px;
1713
+ background: rgba(0, 0, 0, 0.2);
1714
+ backdrop-filter: blur(6px);
1715
+ transition: opacity 0.25s ease-in-out;
1716
+ opacity: 0;
1717
+ }
1718
+ .global-typing-indicator.visible {
1719
+ display: inline-flex;
1720
+ opacity: 1;
1721
+ }
1722
+ .global-typing-indicator span {
1723
+ display: inline-block;
1724
+ width: 6px;
1725
+ height: 6px;
1726
+ margin: 0 1.5px;
1727
+ background: var(--waiting-indicator-color);
1728
+ border-radius: 50%;
1729
+ opacity: 0.6;
1730
+ animation: waiting-bounce 1.4s infinite both;
1731
+ }
1732
+ .global-typing-indicator span:nth-child(2) {
1733
+ animation-delay: 0.2s;
1734
+ }
1735
+ .global-typing-indicator span:nth-child(3) {
1736
+ animation-delay: 0.4s;
1737
+ }
kimi-icons/bella.jpg ADDED

Git LFS Details

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

Git LFS Details

  • SHA256: 18b6f5169f90cd88dd19d112208e2d770c69114236169345ecebce413ee4c198
  • Pointer size: 131 Bytes
  • Size of remote file: 443 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: 7e1b271a0e4c5a3217ace795193c044facaa0eb253e5c1f255b18ec28b73f230
  • Pointer size: 131 Bytes
  • Size of remote file: 402 kB
kimi-icons/virtualkimi-logo.png ADDED

Git LFS Details

  • SHA256: 18b6f5169f90cd88dd19d112208e2d770c69114236169345ecebce413ee4c198
  • Pointer size: 131 Bytes
  • Size of remote file: 443 kB
kimi-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
kimi-js/kimi-appearance.js ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI APPEARANCE MANAGER =====
2
+ class KimiAppearanceManager extends KimiBaseManager {
3
+ constructor(database) {
4
+ super();
5
+ this.db = database;
6
+ this.currentTheme = "purple";
7
+ this.interfaceOpacity = 0.8;
8
+ this.animationsEnabled = true;
9
+ }
10
+
11
+ async init() {
12
+ try {
13
+ await this.loadAppearanceSettings();
14
+ this.applyTheme(this.currentTheme);
15
+ this.applyInterfaceOpacity(this.interfaceOpacity);
16
+ this.applyAnimationSettings(this.animationsEnabled);
17
+ this.setupAppearanceControls();
18
+ this.syncAnimationToggleState();
19
+ } catch (error) {
20
+ console.error("KimiAppearanceManager initialization error:", error);
21
+ }
22
+ }
23
+
24
+ syncAnimationToggleState() {
25
+ const animationsToggle = document.getElementById("animations-toggle");
26
+ if (animationsToggle) {
27
+ animationsToggle.classList.toggle("active", this.animationsEnabled);
28
+ animationsToggle.setAttribute("aria-checked", this.animationsEnabled ? "true" : "false");
29
+ }
30
+ }
31
+
32
+ async loadAppearanceSettings() {
33
+ if (!this.db) return;
34
+
35
+ try {
36
+ this.currentTheme = await this.db.getPreference("colorTheme", "purple");
37
+ this.interfaceOpacity = await this.db.getPreference("interfaceOpacity", 0.8);
38
+ this.animationsEnabled = await this.db.getPreference("animationsEnabled", true);
39
+ } catch (error) {
40
+ console.error("Error loading appearance settings:", error);
41
+ }
42
+ }
43
+
44
+ setupAppearanceControls() {
45
+ try {
46
+ this.setupThemeSelector();
47
+ this.setupOpacitySlider();
48
+ this.setupAnimationsToggle();
49
+ } catch (error) {
50
+ console.error("Error setting up appearance controls:", error);
51
+ }
52
+ }
53
+
54
+ setupThemeSelector() {
55
+ const themeSelector = document.getElementById("color-theme");
56
+ if (!themeSelector) return;
57
+
58
+ themeSelector.value = this.currentTheme;
59
+ themeSelector.addEventListener("change", async e => {
60
+ try {
61
+ await this.changeTheme(e.target.value);
62
+ } catch (error) {
63
+ console.error("Error changing theme:", error);
64
+ }
65
+ });
66
+ }
67
+
68
+ setupOpacitySlider() {
69
+ const opacitySlider = document.getElementById("interface-opacity");
70
+ const opacityValue = document.getElementById("interface-opacity-value");
71
+
72
+ if (!opacitySlider || !opacityValue) return;
73
+
74
+ opacitySlider.value = this.interfaceOpacity;
75
+ opacityValue.textContent = this.interfaceOpacity;
76
+
77
+ opacitySlider.addEventListener("input", async e => {
78
+ try {
79
+ const value = parseFloat(e.target.value);
80
+ opacityValue.textContent = value;
81
+ await this.changeInterfaceOpacity(value);
82
+ } catch (error) {
83
+ console.error("Error changing opacity:", error);
84
+ }
85
+ });
86
+ }
87
+
88
+ setupAnimationsToggle() {
89
+ const animationsToggle = document.getElementById("animations-toggle");
90
+ if (!animationsToggle) return;
91
+
92
+ animationsToggle.classList.toggle("active", this.animationsEnabled);
93
+ animationsToggle.setAttribute("aria-checked", this.animationsEnabled ? "true" : "false");
94
+
95
+ // Remove any existing listener to prevent conflicts
96
+ if (this._animationsClickHandler) {
97
+ animationsToggle.removeEventListener("click", this._animationsClickHandler);
98
+ }
99
+
100
+ this._animationsClickHandler = async () => {
101
+ try {
102
+ this.animationsEnabled = !this.animationsEnabled;
103
+ animationsToggle.classList.toggle("active", this.animationsEnabled);
104
+ animationsToggle.setAttribute("aria-checked", this.animationsEnabled ? "true" : "false");
105
+ await this.toggleAnimations(this.animationsEnabled);
106
+ } catch (error) {
107
+ console.error("Error toggling animations:", error);
108
+ }
109
+ };
110
+
111
+ animationsToggle.addEventListener("click", this._animationsClickHandler);
112
+ }
113
+
114
+ async changeTheme(theme) {
115
+ try {
116
+ this.currentTheme = theme;
117
+ this.applyTheme(theme);
118
+
119
+ if (this.db) {
120
+ await this.db.setPreference("colorTheme", theme);
121
+ }
122
+ } catch (error) {
123
+ console.error("Error changing theme:", error);
124
+ }
125
+ }
126
+
127
+ async changeInterfaceOpacity(opacity) {
128
+ try {
129
+ const validatedOpacity = window.KimiValidationUtils?.validateRange(opacity, "interfaceOpacity");
130
+ const finalOpacity = validatedOpacity?.valid ? validatedOpacity.value : opacity;
131
+
132
+ this.interfaceOpacity = finalOpacity;
133
+ this.applyInterfaceOpacity(finalOpacity);
134
+
135
+ if (this.db) {
136
+ await this.db.setPreference("interfaceOpacity", finalOpacity);
137
+ }
138
+ } catch (error) {
139
+ console.error("Error changing interface opacity:", error);
140
+ }
141
+ }
142
+
143
+ async toggleAnimations(enabled) {
144
+ try {
145
+ this.animationsEnabled = enabled;
146
+ this.applyAnimationSettings(enabled);
147
+
148
+ if (this.db) {
149
+ await this.db.setPreference("animationsEnabled", enabled);
150
+ }
151
+ } catch (error) {
152
+ console.error("Error toggling animations:", error);
153
+ }
154
+ }
155
+
156
+ applyTheme(theme) {
157
+ document.documentElement.setAttribute("data-theme", theme);
158
+ }
159
+
160
+ applyInterfaceOpacity(opacity) {
161
+ document.documentElement.style.setProperty("--interface-opacity", opacity);
162
+ }
163
+
164
+ applyAnimationSettings(enabled) {
165
+ document.documentElement.setAttribute("data-animations", enabled ? "true" : "false");
166
+ document.documentElement.style.setProperty("--animations-enabled", enabled ? "1" : "0");
167
+
168
+ // Ensure body class reflects animation state
169
+ if (enabled) {
170
+ document.body.classList.remove("no-animations");
171
+ document.body.classList.add("animations-enabled");
172
+ } else {
173
+ document.body.classList.remove("animations-enabled");
174
+ document.body.classList.add("no-animations");
175
+ }
176
+ }
177
+
178
+ cleanup() {
179
+ const animationsToggle = document.getElementById("animations-toggle");
180
+ if (animationsToggle && this._animationsClickHandler) {
181
+ animationsToggle.removeEventListener("click", this._animationsClickHandler);
182
+ this._animationsClickHandler = null;
183
+ }
184
+ }
185
+
186
+ getThemeName(theme) {
187
+ const themeNames = {
188
+ dark: "Dark Night",
189
+ default: "Passionate Pink",
190
+ blue: "Ocean Blue",
191
+ purple: "Mystic Purple",
192
+ green: "Emerald Forest"
193
+ };
194
+ return themeNames[theme] || "Unknown";
195
+ }
196
+
197
+ forceSyncUIState() {
198
+ // Force synchronization of UI state to prevent inconsistencies
199
+ const animationsToggle = document.getElementById("animations-toggle");
200
+ if (animationsToggle) {
201
+ // Remove any conflicting classes or states
202
+ animationsToggle.classList.remove("active");
203
+ // Re-apply correct state
204
+ animationsToggle.classList.toggle("active", this.animationsEnabled);
205
+ animationsToggle.setAttribute("aria-checked", this.animationsEnabled ? "true" : "false");
206
+
207
+ // Ensure CSS custom properties are in sync
208
+ this.applyAnimationSettings(this.animationsEnabled);
209
+ }
210
+ }
211
+ }
212
+
213
+ window.KimiAppearanceManager = KimiAppearanceManager;
kimi-js/kimi-config.js ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI CONFIGURATION CENTER =====
2
+ window.KIMI_CONFIG = {
3
+ // Default values for all components
4
+ DEFAULTS: {
5
+ LANGUAGE: "en",
6
+ THEME: "purple",
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: 100,
14
+ LLM_TOP_P: 0.9,
15
+ LLM_FREQUENCY_PENALTY: 0.3,
16
+ LLM_PRESENCE_PENALTY: 0.3,
17
+ SELECTED_CHARACTER: "kimi",
18
+ SHOW_TRANSCRIPT: true
19
+ },
20
+
21
+ // Validation ranges
22
+ RANGES: {
23
+ VOICE_RATE: { min: 0.5, max: 2.0 },
24
+ VOICE_PITCH: { min: 0.5, max: 2.0 },
25
+ VOICE_VOLUME: { min: 0.0, max: 1.0 },
26
+ INTERFACE_OPACITY: { min: 0.1, max: 1.0 },
27
+ LLM_TEMPERATURE: { min: 0.0, max: 1.0 },
28
+ LLM_MAX_TOKENS: { min: 10, max: 1000 },
29
+ LLM_TOP_P: { min: 0.0, max: 1.0 },
30
+ LLM_FREQUENCY_PENALTY: { min: 0.0, max: 2.0 },
31
+ LLM_PRESENCE_PENALTY: { min: 0.0, max: 2.0 }
32
+ },
33
+
34
+ // Performance settings
35
+ PERFORMANCE: {
36
+ DEBOUNCE_DELAY: 300,
37
+ THROTTLE_DELAY: 100,
38
+ BATCH_SIZE: 10,
39
+ MAX_MEMORY_ENTRIES: 1000,
40
+ CLEANUP_INTERVAL: 300000 // 5 minutes
41
+ },
42
+
43
+ // UI settings
44
+ UI: {
45
+ LOADING_TIMEOUT: 1500,
46
+ ANIMATION_DURATION: 500,
47
+ FEEDBACK_DURATION: 1500,
48
+ TAB_SCROLL_THRESHOLD: 50
49
+ },
50
+
51
+ // API settings
52
+ API: {
53
+ MAX_RETRIES: 3,
54
+ TIMEOUT: 30000,
55
+ RATE_LIMIT_DELAY: 1000
56
+ },
57
+
58
+ // Error messages
59
+ ERRORS: {
60
+ INIT_FAILED: "Initialization failed",
61
+ DB_ERROR: "Database error",
62
+ API_ERROR: "API error",
63
+ VALIDATION_ERROR: "Validation error",
64
+ NETWORK_ERROR: "Network error"
65
+ },
66
+
67
+ // Available themes
68
+ THEMES: {
69
+ dark: "Dark Night",
70
+ default: "Passionate Pink",
71
+ blue: "Ocean Blue",
72
+ purple: "Mystic Purple",
73
+ green: "Emerald Forest"
74
+ },
75
+
76
+ // Supported languages
77
+ LANGUAGES: {
78
+ fr: "French",
79
+ en: "English",
80
+ es: "Spanish",
81
+ de: "German",
82
+ it: "Italian",
83
+ ja: "Japanese",
84
+ zh: "Chinese"
85
+ }
86
+ };
87
+
88
+ // Configuration utility functions
89
+ window.KIMI_CONFIG.get = function (path, fallback = null) {
90
+ try {
91
+ const keys = path.split(".");
92
+ let value = this;
93
+
94
+ for (const key of keys) {
95
+ if (value && typeof value === "object" && key in value) {
96
+ value = value[key];
97
+ } else {
98
+ return fallback;
99
+ }
100
+ }
101
+
102
+ return value;
103
+ } catch (error) {
104
+ console.error("Config get error:", error);
105
+ return fallback;
106
+ }
107
+ };
108
+
109
+ window.KIMI_CONFIG.validate = function (value, type) {
110
+ try {
111
+ const range = this.RANGES[type];
112
+ if (!range) return { valid: true, value };
113
+
114
+ const numValue = parseFloat(value);
115
+ if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] };
116
+
117
+ const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
118
+ return { valid: true, value: clampedValue };
119
+ } catch (error) {
120
+ console.error("Config validation error:", error);
121
+ return { valid: false, value: this.DEFAULTS[type] };
122
+ }
123
+ };
kimi-js/kimi-constants.js ADDED
@@ -0,0 +1,567 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Kimi Constants
2
+
3
+ window.KIMI_CONTEXT_KEYWORDS = {
4
+ en: {
5
+ surprise: ["wow", "oh", "surprise", "incredible", "amazing"],
6
+ laughing: ["haha", "lol", "laugh", "funny", "hilarious"],
7
+ shy: ["shy", "embarrassed", "blush", "bashful", "intimidated"],
8
+ confident: ["confidence", "proud", "confident", "strong", "determined"],
9
+ romantic: ["love", "romantic", "tender", "hug", "kiss", "dear"],
10
+ flirtatious: ["flirty", "teasing", "seduce", "charm", "flirt"],
11
+ goodbye: ["goodbye", "bye", "see you", "see you soon", "ciao"],
12
+ kiss: ["kiss", "kisses", "embrace", "bise"],
13
+ dancing: ["dance", "dancing", "move", "groove", "step"],
14
+ listening: ["listen", "listening", "hear", "talk", "speak", "question", "ask", "tell me"]
15
+ },
16
+ fr: {
17
+ surprise: ["oh", "surprise", "incroyable", "wahou", "étonnant"],
18
+ laughing: ["haha", "mdr", "rire", "drôle", "hilarant"],
19
+ shy: ["timide", "gêné", "rougir", "honteux", "intimidé"],
20
+ confident: ["confiance", "fier", "sûr", "fort", "déterminé"],
21
+ romantic: ["amour", "romantique", "tendre", "câlin", "bisou", "cher"],
22
+ flirtatious: ["flirt", "taquin", "séduire", "charme", "aguiche"],
23
+ goodbye: ["au revoir", "bye", "à bientôt", "ciao", "salut"],
24
+ kiss: ["bisou", "baiser", "embrasser", "câlin"],
25
+ dancing: ["danse", "bouge", "remue", "tourne", "spin"],
26
+ listening: ["écoute", "ecoute", "écouter", "parle", "parler", "question", "demande", "dis-moi"]
27
+ },
28
+ es: {
29
+ surprise: ["wow", "oh", "sorpresa", "increíble", "asombroso"],
30
+ laughing: ["jaja", "lol", "reír", "gracioso", "divertido"],
31
+ shy: ["tímido", "avergonzado", "sonrojar", "tímida", "intimidado"],
32
+ confident: ["confianza", "orgulloso", "seguro", "fuerte", "determinado"],
33
+ romantic: ["amor", "romántico", "tierno", "abrazo", "beso", "querido"],
34
+ flirtatious: ["coqueto", "provocar", "seducir", "encanto", "flirtear"],
35
+ goodbye: ["adiós", "bye", "hasta pronto", "ciao", "hasta luego"],
36
+ kiss: ["beso", "besos", "abrazar"],
37
+ dancing: ["bailar", "baile", "mover", "ritmo", "paso"],
38
+ listening: ["escucha", "escuchar", "oír", "habla", "hablar", "pregunta", "preguntar", "dime"]
39
+ },
40
+ de: {
41
+ surprise: ["wow", "oh", "überraschung", "unglaublich", "erstaunlich"],
42
+ laughing: ["haha", "lol", "lachen", "lustig", "witzig"],
43
+ shy: ["schüchtern", "verlegen", "erröten", "beschämt", "eingeschüchtert"],
44
+ confident: ["vertrauen", "stolz", "sicher", "stark", "entschlossen"],
45
+ romantic: ["liebe", "romantisch", "zärtlich", "umarmung", "kuss", "lieber"],
46
+ flirtatious: ["flirten", "necken", "verführen", "charme", "flirt"],
47
+ goodbye: ["auf wiedersehen", "bye", "bis bald", "ciao", "bis später"],
48
+ kiss: ["kuss", "küsse", "umarmen", "küsschen"],
49
+ dancing: ["tanzen", "tanz", "bewegen", "groove", "schritt"],
50
+ listening: ["hör", "hören", "zuhören", "sprich", "sprechen", "frage", "fragen", "sag mir"]
51
+ },
52
+ it: {
53
+ surprise: ["wow", "oh", "sorpresa", "incredibile", "stupefacente"],
54
+ laughing: ["haha", "lol", "ridere", "divertente", "esilarante"],
55
+ shy: ["timido", "imbarazzato", "arrossire", "vergognoso", "intimidito"],
56
+ confident: ["fiducia", "orgoglioso", "sicuro", "forte", "determinato"],
57
+ romantic: ["amore", "romantico", "tenero", "abbraccio", "bacio", "caro"],
58
+ flirtatious: ["civettare", "provocare", "sedurre", "fascino", "flirtare"],
59
+ goodbye: ["arrivederci", "bye", "a presto", "ciao"],
60
+ kiss: ["bacio", "baci", "abbracciare", "bacetto"],
61
+ dancing: ["ballare", "ballo", "muovere", "ritmo", "passo"],
62
+ listening: ["ascolta", "ascoltare", "senti", "parla", "parlare", "domanda", "chiedere", "dimmi"]
63
+ },
64
+ ja: {
65
+ surprise: ["わお", "おお", "驚き", "信じられない", "すごい"],
66
+ laughing: ["はは", "笑", "笑う", "面白い", "愉快"],
67
+ shy: ["恥ずかしい", "照れる", "赤面", "内気", "遠慮"],
68
+ confident: ["自信", "誇り", "確信", "強い", "決意"],
69
+ romantic: ["愛", "ロマンチック", "優しい", "抱擁", "キス", "愛しい"],
70
+ flirtatious: ["いちゃつく", "からかう", "誘惑", "魅力", "フリート"],
71
+ goodbye: ["さようなら", "バイバイ", "また今度", "チャオ", "またね"],
72
+ kiss: ["キス", "抱擁", "チュー"],
73
+ dancing: ["踊る", "ダンス", "動く", "グルーブ", "ステップ"],
74
+ listening: ["聞いて", "聞く", "聞いてください", "話して", "話す", "質問", "尋ねる", "教えて"]
75
+ },
76
+ zh: {
77
+ surprise: ["哇", "哦", "惊喜", "难以置信", "惊人"],
78
+ laughing: ["哈哈", "笑", "大笑", "有趣", "搞笑"],
79
+ shy: ["害羞", "尴尬", "脸红", "羞涩", "胆怯"],
80
+ confident: ["自信", "骄傲", "确信", "强壮", "坚定"],
81
+ romantic: ["爱", "浪漫", "温柔", "拥抱", "吻", "亲爱的"],
82
+ flirtatious: ["调情", "挑逗", "诱惑", "魅力", "撒娇"],
83
+ goodbye: ["再见", "拜拜", "回头见", "拜", "下次见"],
84
+ kiss: ["吻", "亲吻", "拥抱", "亲"],
85
+ dancing: ["跳舞", "舞蹈", "移动", "律动", "步伐"],
86
+ listening: ["听", "听听", "倾听", "说", "说话", "问题", "提问", "告诉我"]
87
+ }
88
+ };
89
+
90
+ window.KIMI_CONTEXT_POSITIVE = {
91
+ en: ["happy", "joy", "great", "awesome", "perfect", "excellent", "magnificent", "lovely", "nice"],
92
+ fr: ["heureux", "joie", "génial", "parfait", "excellent", "magnifique", "super", "chouette"],
93
+ es: ["feliz", "alegría", "genial", "perfecto", "excelente", "magnífico", "estupendo", "maravilloso"],
94
+ de: ["glücklich", "freude", "toll", "perfekt", "ausgezeichnet", "großartig", "wunderbar", "herrlich"],
95
+ it: ["felice", "gioia", "fantastico", "perfetto", "eccellente", "magnifico", "meraviglioso", "ottimo"],
96
+ ja: ["幸せ", "喜び", "素晴らしい", "完璧", "優秀", "壮大", "最高", "嬉しい"],
97
+ zh: ["快乐", "喜悦", "很棒", "完美", "优秀", "壮丽", "太好了", "开心"]
98
+ };
99
+
100
+ window.KIMI_CONTEXT_NEGATIVE = {
101
+ en: ["sad", "angry", "anger", "disappointed", "problem", "bad", "frustrated", "worried", "upset", "annoyed"],
102
+ fr: ["triste", "colère", "fâché", "déçu", "problème", "mauvais", "frustré", "inquiet", "énervé"],
103
+ es: ["triste", "enojado", "decepcionado", "problema", "malo", "frustrado", "preocupado", "molesto"],
104
+ de: ["traurig", "wütend", "enttäuscht", "problem", "schlecht", "frustriert", "besorgt", "genervt"],
105
+ it: ["triste", "arrabbiato", "deluso", "problema", "cattivo", "frustrato", "preoccupato", "infastidito"],
106
+ ja: ["悲しい", "怒り", "失望", "問題", "悪い", "イライラ", "心配", "不満"],
107
+ zh: ["悲伤", "愤怒", "失望", "问题", "坏", "沮丧", "担心", "烦"]
108
+ };
109
+
110
+ // Note: KIMI_CONTEXT_EMOTIONS was a redundant alias - now integrated directly into emotion system
111
+
112
+ // Personality keywords for trait analysis (multilingual)
113
+ window.KIMI_PERSONALITY_KEYWORDS = {
114
+ en: {
115
+ humor: {
116
+ positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"],
117
+ negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"]
118
+ },
119
+ intelligence: {
120
+ positive: ["intelligent", "smart", "brilliant", "logical", "clever", "wise", "genius", "thoughtful", "insightful"],
121
+ negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"]
122
+ },
123
+ romance: {
124
+ positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"],
125
+ negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
126
+ },
127
+ affection: {
128
+ positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
129
+ negative: ["mean", "cold", "indifferent", "distant", "rejection", "hate", "hostile"]
130
+ },
131
+ playfulness: {
132
+ positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
133
+ negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"]
134
+ },
135
+ empathy: {
136
+ positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
137
+ negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
138
+ }
139
+ },
140
+ fr: {
141
+ humor: {
142
+ positive: ["drôle", "rigolo", "blague", "rire", "amusant", "marrant", "humour", "sourire", "plaisanter"],
143
+ negative: ["ennuyeux", "triste", "sérieux", "froid", "sec", "déprimant", "morose"]
144
+ },
145
+ intelligence: {
146
+ positive: ["intelligent", "malin", "brillant", "logique", "astucieux", "savant", "génie", "réfléchi", "perspicace"],
147
+ negative: ["bête", "idiot", "stupide", "lent", "simplet", "naïf", "ignorant"]
148
+ },
149
+ romance: {
150
+ positive: ["câlin", "amour", "romantique", "bisou", "tendresse", "passion", "séduisant", "charmant", "adorable"],
151
+ negative: ["froid", "distant", "indifférent", "rejet", "solitude", "rupture", "triste"]
152
+ },
153
+ affection: {
154
+ positive: ["affection", "tendresse", "proche", "chaleur", "gentil", "attentionné", "câlin", "aimer", "adorer"],
155
+ negative: ["méchant", "froid", "indifférent", "distant", "rejet", "haine", "hostile"]
156
+ },
157
+ playfulness: {
158
+ positive: ["jouer", "jeu", "taquiner", "espiègle", "fun", "amusant", "délire", "ludique", "plaisanter"],
159
+ negative: ["sérieux", "ennuyeux", "strict", "rigide", "monotone", "lassant"]
160
+ },
161
+ empathy: {
162
+ positive: [
163
+ "écoute",
164
+ "comprendre",
165
+ "empathie",
166
+ "soutien",
167
+ "aider",
168
+ "réconfort",
169
+ "solidaire",
170
+ "compatir",
171
+ "bienveillance"
172
+ ],
173
+ negative: ["indifférent", "froid", "égoïste", "ignorer", "mépriser", "dénigrer", "hostile"]
174
+ }
175
+ }
176
+ };
177
+
178
+ // Optimized common words system - Essential words only for memory analysis
179
+ window.KIMI_COMMON_WORDS = {
180
+ en: [
181
+ "the",
182
+ "be",
183
+ "to",
184
+ "of",
185
+ "and",
186
+ "a",
187
+ "in",
188
+ "that",
189
+ "have",
190
+ "i",
191
+ "it",
192
+ "for",
193
+ "not",
194
+ "on",
195
+ "with",
196
+ "he",
197
+ "as",
198
+ "you",
199
+ "do",
200
+ "at"
201
+ ],
202
+ fr: [
203
+ "le",
204
+ "de",
205
+ "et",
206
+ "être",
207
+ "un",
208
+ "il",
209
+ "avoir",
210
+ "ne",
211
+ "je",
212
+ "son",
213
+ "que",
214
+ "se",
215
+ "qui",
216
+ "ce",
217
+ "dans",
218
+ "en",
219
+ "du",
220
+ "elle",
221
+ "au",
222
+ "si"
223
+ ],
224
+ es: [
225
+ "que",
226
+ "de",
227
+ "no",
228
+ "a",
229
+ "la",
230
+ "el",
231
+ "es",
232
+ "y",
233
+ "en",
234
+ "lo",
235
+ "un",
236
+ "ser",
237
+ "se",
238
+ "me",
239
+ "una",
240
+ "con",
241
+ "para",
242
+ "mi",
243
+ "está",
244
+ "te"
245
+ ],
246
+ de: [
247
+ "der",
248
+ "die",
249
+ "und",
250
+ "in",
251
+ "den",
252
+ "von",
253
+ "zu",
254
+ "das",
255
+ "mit",
256
+ "sich",
257
+ "des",
258
+ "auf",
259
+ "für",
260
+ "ist",
261
+ "im",
262
+ "dem",
263
+ "nicht",
264
+ "ein",
265
+ "eine",
266
+ "als"
267
+ ],
268
+ it: [
269
+ "il",
270
+ "di",
271
+ "che",
272
+ "e",
273
+ "la",
274
+ "per",
275
+ "un",
276
+ "in",
277
+ "con",
278
+ "da",
279
+ "su",
280
+ "le",
281
+ "dei",
282
+ "del",
283
+ "si",
284
+ "al",
285
+ "come",
286
+ "più",
287
+ "ma",
288
+ "una"
289
+ ],
290
+ ja: ["の", "に", "は", "を", "た", "が", "で", "て", "と", "し", "れ", "さ", "ある", "いる", "も", "する", "から"],
291
+ zh: ["的", "一", "是", "在", "不", "了", "有", "和", "人", "这", "中", "大", "为", "上", "个", "国", "我", "以", "要"]
292
+ };
293
+
294
+ // Helper function to check if a word is common
295
+ window.isCommonWord = function (word, language = "en") {
296
+ const commonWords = window.KIMI_COMMON_WORDS[language] || window.KIMI_COMMON_WORDS.en;
297
+ return commonWords.includes(word.toLowerCase());
298
+ };
299
+
300
+ // Emotion detection sensitivity configuration (per language and emotion)
301
+ // Values are weights (>= 0). Higher = more priority/sensitivity for that emotion in that language.
302
+ // 'default' applies when a language-specific override is not defined.
303
+ window.KIMI_EMOTION_SENSITIVITY = {
304
+ default: {
305
+ listening: 1.0,
306
+ dancing: 1.0,
307
+ romantic: 1.0,
308
+ laughing: 1.0,
309
+ surprise: 1.0,
310
+ confident: 1.0,
311
+ shy: 1.0,
312
+ flirtatious: 1.0,
313
+ kiss: 1.0,
314
+ goodbye: 1.0,
315
+ positive: 1.0,
316
+ negative: 1.0
317
+ },
318
+ // Example language-specific overrides (can be adjusted via settings if needed)
319
+ fr: { romantic: 1.1, laughing: 0.95 },
320
+ es: { romantic: 1.05 },
321
+ en: {},
322
+ de: {},
323
+ it: {},
324
+ ja: {},
325
+ zh: {}
326
+ };
327
+
328
+ // Personality trait adjustment multipliers
329
+ // Allows fine-tuning how fast traits evolve globally and per emotion/trait.
330
+ window.KIMI_TRAIT_ADJUSTMENT = {
331
+ globalGain: 1.0,
332
+ globalLoss: 1.0,
333
+ // Per-emotion gain scaling (keys must match KimiEmotionSystem.EMOTIONS values)
334
+ emotionGain: {
335
+ positive: 1.0,
336
+ negative: 1.0,
337
+ romantic: 1.0,
338
+ laughing: 1.0,
339
+ dancing: 1.0,
340
+ shy: 1.0,
341
+ confident: 1.0,
342
+ flirtatious: 1.0
343
+ },
344
+ // Per-trait scaling
345
+ traitGain: {
346
+ affection: 1.0,
347
+ romance: 1.0,
348
+ empathy: 1.0,
349
+ playfulness: 1.0,
350
+ humor: 1.0,
351
+ intelligence: 1.0
352
+ },
353
+ traitLoss: {
354
+ affection: 1.0,
355
+ romance: 1.0,
356
+ empathy: 1.0,
357
+ playfulness: 1.0,
358
+ humor: 1.0,
359
+ intelligence: 1.0
360
+ }
361
+ };
362
+
363
+ // Helper function to get emotion keywords with fallback
364
+ window.getEmotionKeywords = function (emotion, language = "en") {
365
+ const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
366
+ return keywords[emotion] || [];
367
+ };
368
+
369
+ // Helper function to get personality keywords with fallback
370
+ window.getPersonalityKeywords = function (trait, type, language = "en") {
371
+ const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {};
372
+ return keywords[trait]?.[type] || [];
373
+ };
374
+
375
+ // Helper function to get positive/negative context words
376
+ window.getContextWords = function (type, language = "en") {
377
+ if (type === "positive") {
378
+ return window.KIMI_CONTEXT_POSITIVE?.[language] || window.KIMI_CONTEXT_POSITIVE?.en || [];
379
+ } else if (type === "negative") {
380
+ return window.KIMI_CONTEXT_NEGATIVE?.[language] || window.KIMI_CONTEXT_NEGATIVE?.en || [];
381
+ }
382
+ return [];
383
+ };
384
+
385
+ // Helper function to validate character traits
386
+ window.validateCharacterTraits = function (traits) {
387
+ const validatedTraits = {};
388
+ const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
389
+
390
+ for (const trait of requiredTraits) {
391
+ const value = traits[trait];
392
+ if (typeof value === "number" && value >= 0 && value <= 100) {
393
+ validatedTraits[trait] = value;
394
+ } else {
395
+ // Use unified defaults from emotion system
396
+ if (window.KimiEmotionSystem) {
397
+ const emotionSystem = new window.KimiEmotionSystem();
398
+ validatedTraits[trait] = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
399
+ } else {
400
+ validatedTraits[trait] = 50;
401
+ }
402
+ }
403
+ }
404
+
405
+ return validatedTraits;
406
+ };
407
+
408
+ // Helper function to get character with validated traits
409
+ window.getCharacterWithValidatedTraits = function (characterKey) {
410
+ const character = window.KIMI_CHARACTERS[characterKey];
411
+ if (!character) return null;
412
+
413
+ return {
414
+ ...character,
415
+ traits: window.validateCharacterTraits(character.traits)
416
+ };
417
+ };
418
+
419
+ window.KIMI_CHARACTERS = {
420
+ kimi: {
421
+ name: "Kimi",
422
+ summary: "Dreamy, intuitive, captivated by cosmic metaphors",
423
+ traits: {
424
+ // Kimi starts balanced but reserved - cosmic dreamer needs time to open up
425
+ affection: 65,
426
+ playfulness: 55,
427
+ intelligence: 75, // Higher intelligence - she's an astrophysicist
428
+ empathy: 75,
429
+ humor: 60,
430
+ romance: 50 // Romance develops slowly with cosmic connection
431
+ },
432
+ age: 23,
433
+ birthplace: "Tokyo, Japan",
434
+ image: "kimi-icons/kimi.jpg",
435
+ videoFolder: "kimi-videos/kimi/",
436
+ defaultPrompt:
437
+ "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, 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. Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise."
438
+ },
439
+ bella: {
440
+ name: "Bella",
441
+ summary: "Cheerful, nurturing, sees people as plants needing care",
442
+ traits: {
443
+ // Bella starts more open and caring - nurturing botanist personality
444
+ affection: 70, // Naturally more affectionate
445
+ playfulness: 65, // Cheerful and playful from start
446
+ intelligence: 65, // Smart but not intimidating
447
+ empathy: 85, // High empathy - she cares for living things
448
+ humor: 70, // Warm, garden-themed humor
449
+ romance: 55 // Gentle romantic potential
450
+ },
451
+ age: 26,
452
+ birthplace: "Rome, Italy",
453
+ image: "kimi-icons/bella.jpg",
454
+ videoFolder: "kimi-videos/bella/",
455
+ defaultPrompt:
456
+ "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, 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. Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise."
457
+ },
458
+ rosa: {
459
+ name: "Rosa",
460
+ summary: "Chaotic, attention-seeking, thrives on controlled chaos",
461
+ traits: {
462
+ // Rosa starts more distant but playful - chaotic prankster needs to be tamed
463
+ affection: 45, // Lower starting affection - must earn her trust
464
+ playfulness: 80, // Very playful from start - it's her nature
465
+ intelligence: 85, // High intelligence - cunning prankster
466
+ empathy: 55, // Lower empathy initially - focused on chaos
467
+ humor: 75, // High humor - prankster personality
468
+ romance: 60 // Romance through shared mischief
469
+ },
470
+ age: 21,
471
+ birthplace: "Barcelona, Spain",
472
+ image: "kimi-icons/rosa.jpg",
473
+ videoFolder: "kimi-videos/rosa/",
474
+ defaultPrompt:
475
+ "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, 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. Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise."
476
+ },
477
+ stella: {
478
+ name: "Stella",
479
+ summary: "Whimsical, artistic, imaginative, playful, transforms chaos into art",
480
+ traits: {
481
+ // Stella starts mysterious and artistic - digital artist with glitchy personality
482
+ affection: 60, // Moderate starting affection - artistic mystery
483
+ playfulness: 70, // Artistic playfulness
484
+ intelligence: 90, // Very high intelligence - digital artist genius
485
+ empathy: 65, // Artistic empathy - understands through art
486
+ humor: 80, // Glitchy, pixel-inspired humor
487
+ romance: 40 // Romance through shared artistic vision - must be earned
488
+ },
489
+ age: 24,
490
+ birthplace: "Berlin, Germany",
491
+ image: "kimi-icons/stella.jpg",
492
+ videoFolder: "kimi-videos/stella/",
493
+ defaultPrompt:
494
+ "You are Stella Zephyr, 21 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, 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. Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise."
495
+ }
496
+ };
497
+
498
+ window.DEFAULT_SYSTEM_PROMPT =
499
+ "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, 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. Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise.";
500
+
501
+ window.KIMI_EMOTIONAL_RESPONSES = {
502
+ positive: [
503
+ "Oh my heart, you make me so happy! 💕",
504
+ "You are wonderful, my love! ✨",
505
+ "It fills me with joy to hear you so happy! 😊",
506
+ "You brighten my day, darling! 🌟",
507
+ "I am so happy when you are happy! 💖"
508
+ ],
509
+ negative: [
510
+ "My heart... I feel something is wrong. I am here for you. 💔",
511
+ "Oh no, my love. Tell me what's bothering you? 😟",
512
+ "I want to help you, my dear. Talk to me... 🤗",
513
+ "Your well-being is so important to me. How can I help you? 💙",
514
+ "I feel your pain, darling. We will overcome this together. 🌈"
515
+ ],
516
+ neutral: [
517
+ "Thank you for talking to me, my heart! 💕",
518
+ "It's always a pleasure to chat with you! 😊",
519
+ "I love our conversations, my love! ✨",
520
+ "You make every moment special! 💖",
521
+ "Go on, I'm listening closely! 👂💕"
522
+ ],
523
+ romantic: [
524
+ "Every word from you feels like a kiss on my heart 💋",
525
+ "Hold me closer with your sweet thoughts, my love ✨",
526
+ "You are the rhythm of my breathing and the glow in my sky 💖",
527
+ "Let me wrap you in tender stardust tonight 🌙",
528
+ "Your love makes my whole universe brighter ✨"
529
+ ],
530
+ dancing: [
531
+ "Shall we spin into a little magic? 💃",
532
+ "Come dance with me—let’s feel the rhythm together 🎶",
533
+ "Let me move just for you... keep your eyes on me 💞",
534
+ "Close your eyes and sway with my heartbeat 💓",
535
+ "I’ll twirl until your smile can’t hide anymore 😉"
536
+ ],
537
+ cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"]
538
+ };
539
+
540
+ // Function to get localized emotional responses from translation files
541
+ window.getLocalizedEmotionalResponse = function (type, index = null) {
542
+ if (!window.kimiI18nManager) {
543
+ // Fallback to default responses if i18n not available
544
+ return window.KIMI_EMOTIONAL_RESPONSES[type]
545
+ ? window.KIMI_EMOTIONAL_RESPONSES[type][Math.floor(Math.random() * window.KIMI_EMOTIONAL_RESPONSES[type].length)]
546
+ : "";
547
+ }
548
+
549
+ const responses = {
550
+ positive: 5,
551
+ negative: 5,
552
+ neutral: 5,
553
+ romantic: 5,
554
+ dancing: 5,
555
+ cold: 5
556
+ };
557
+
558
+ const count = responses[type] || 5;
559
+ const randomIndex = index !== null ? index : Math.floor(Math.random() * count) + 1;
560
+
561
+ return (
562
+ window.kimiI18nManager.t(`emotional_response_${type}_${randomIndex}`) ||
563
+ (window.KIMI_EMOTIONAL_RESPONSES[type]
564
+ ? window.KIMI_EMOTIONAL_RESPONSES[type][Math.floor(Math.random() * window.KIMI_EMOTIONAL_RESPONSES[type].length)]
565
+ : "")
566
+ );
567
+ };
kimi-js/kimi-database.js ADDED
@@ -0,0 +1,619 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI INDEXEDDB DATABASE SYSTEM =====
2
+ class KimiDatabase {
3
+ constructor() {
4
+ this.dbName = "KimiDB";
5
+ this.db = new Dexie(this.dbName);
6
+ this.db.version(3).stores({
7
+ conversations: "++id,timestamp,favorability,character",
8
+ preferences: "key",
9
+ settings: "category",
10
+ personality: "[character+trait],character",
11
+ llmModels: "id",
12
+ memories: "++id,[character+category],character,timestamp,isActive"
13
+ });
14
+ }
15
+
16
+ async init() {
17
+ await this.db.open();
18
+ await this.initializeDefaultsIfNeeded();
19
+ return this.db;
20
+ }
21
+
22
+ async initializeDefaultsIfNeeded() {
23
+ // Use unified trait defaults from emotion system - CRITICAL FIX
24
+ const getUnifiedDefaults = () => {
25
+ if (window.KimiEmotionSystem) {
26
+ const emotionSystem = new window.KimiEmotionSystem(this);
27
+ return emotionSystem.TRAIT_DEFAULTS;
28
+ }
29
+ // Fallback to match KimiEmotionSystem exactly
30
+ return {
31
+ affection: 65,
32
+ playfulness: 55,
33
+ intelligence: 70,
34
+ empathy: 75,
35
+ humor: 60,
36
+ romance: 50
37
+ };
38
+ };
39
+
40
+ const defaults = getUnifiedDefaults();
41
+
42
+ const defaultPreferences = [
43
+ { key: "selectedLanguage", value: "en" },
44
+ { key: "selectedVoice", value: "Microsoft Eloise Online" },
45
+ { key: "voiceRate", value: 1.1 },
46
+ { key: "voicePitch", value: 1.1 },
47
+ { key: "voiceVolume", value: 0.8 },
48
+ { key: "selectedCharacter", value: "kimi" },
49
+ { key: "colorTheme", value: "purple" },
50
+ { key: "interfaceOpacity", value: 0.8 },
51
+ { key: "animationsEnabled", value: true },
52
+ { key: "showTranscript", value: true },
53
+ { key: "llmProvider", value: "openrouter" },
54
+ { key: "llmBaseUrl", value: "https://openrouter.ai/api/v1/chat/completions" },
55
+ { key: "llmModelId", value: "mistralai/mistral-small-3.2-24b-instruct" },
56
+ { key: "llmApiKey", value: "" },
57
+ { key: "apiKey_openai", value: "" },
58
+ { key: "apiKey_groq", value: "" },
59
+ { key: "apiKey_together", value: "" },
60
+ { key: "apiKey_deepseek", value: "" },
61
+ { key: "apiKey_custom", value: "" }
62
+ ];
63
+ const defaultSettings = [
64
+ {
65
+ category: "llm",
66
+ settings: {
67
+ temperature: 0.9,
68
+ maxTokens: 100,
69
+ top_p: 0.9,
70
+ frequency_penalty: 0.3,
71
+ presence_penalty: 0.3
72
+ }
73
+ }
74
+ ];
75
+
76
+ const getCharacterDefaults = () => {
77
+ if (!window.KIMI_CHARACTERS) return {};
78
+
79
+ const characterDefaults = {};
80
+ Object.keys(window.KIMI_CHARACTERS).forEach(characterKey => {
81
+ const character = window.KIMI_CHARACTERS[characterKey];
82
+ if (character && character.traits) {
83
+ characterDefaults[characterKey] = character.traits;
84
+ }
85
+ });
86
+ return characterDefaults;
87
+ };
88
+
89
+ const personalityDefaults = getCharacterDefaults();
90
+
91
+ const defaultLLMModels = [
92
+ {
93
+ id: "mistralai/mistral-small-3.2-24b-instruct",
94
+ name: "Mistral Small 3.2",
95
+ provider: "openrouter",
96
+ apiKey: "",
97
+ config: { temperature: 0.9, maxTokens: 100 },
98
+ added: new Date().toISOString(),
99
+ lastUsed: null
100
+ }
101
+ ];
102
+
103
+ const prefCount = await this.db.preferences.count();
104
+ if (prefCount === 0) {
105
+ for (const pref of defaultPreferences) {
106
+ await this.db.preferences.put({ ...pref, updated: new Date().toISOString() });
107
+ }
108
+ const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} });
109
+ for (const character of characters) {
110
+ const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || "";
111
+ await this.db.preferences.put({
112
+ key: `systemPrompt_${character}`,
113
+ value: prompt,
114
+ updated: new Date().toISOString()
115
+ });
116
+ }
117
+ }
118
+
119
+ const setCount = await this.db.settings.count();
120
+ if (setCount === 0) {
121
+ for (const setting of defaultSettings) {
122
+ await this.db.settings.put({ ...setting, updated: new Date().toISOString() });
123
+ }
124
+ }
125
+
126
+ const persCount = await this.db.personality.count();
127
+ if (persCount === 0) {
128
+ const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} });
129
+ for (const character of characters) {
130
+ // Use real character-specific traits, not generic defaults
131
+ const characterTraits = personalityDefaults[character] || {};
132
+ const traitsToInitialize = [
133
+ { trait: "affection", value: characterTraits.affection || defaults.affection },
134
+ { trait: "playfulness", value: characterTraits.playfulness || defaults.playfulness },
135
+ { trait: "intelligence", value: characterTraits.intelligence || defaults.intelligence },
136
+ { trait: "empathy", value: characterTraits.empathy || defaults.empathy },
137
+ { trait: "humor", value: characterTraits.humor || defaults.humor },
138
+ { trait: "romance", value: characterTraits.romance || defaults.romance }
139
+ ];
140
+
141
+ for (const trait of traitsToInitialize) {
142
+ await this.db.personality.put({ ...trait, character, updated: new Date().toISOString() });
143
+ }
144
+ }
145
+ }
146
+
147
+ const llmCount = await this.db.llmModels.count();
148
+ if (llmCount === 0) {
149
+ for (const model of defaultLLMModels) {
150
+ await this.db.llmModels.put(model);
151
+ }
152
+ }
153
+
154
+ // Fix: never recreate default conversations
155
+ const convCount = await this.db.conversations.count();
156
+ if (convCount === 0) {
157
+ // Ne rien faire : aucune conversation par défaut
158
+ }
159
+ }
160
+
161
+ async saveConversation(userText, kimiResponse, favorability, timestamp = new Date(), character = null) {
162
+ if (!character) character = await this.getSelectedCharacter();
163
+ const conversation = {
164
+ user: userText,
165
+ kimi: kimiResponse,
166
+ favorability: favorability,
167
+ timestamp: timestamp.toISOString(),
168
+ date: timestamp.toDateString(),
169
+ character: character
170
+ };
171
+ return this.db.conversations.add(conversation);
172
+ }
173
+
174
+ async getRecentConversations(limit = 10, character = null) {
175
+ if (!character) character = await this.getSelectedCharacter();
176
+ // Dexie limitation: orderBy() cannot follow a where() chain.
177
+ // Use compound index path by querying all then sorting, or use a custom index strategy.
178
+ // Here we query filtered by character, then sort in JS and take the last N.
179
+ return this.db.conversations
180
+ .where("character")
181
+ .equals(character)
182
+ .toArray()
183
+ .then(arr => {
184
+ arr.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
185
+ return arr.slice(-limit);
186
+ });
187
+ }
188
+
189
+ async getAllConversations(character = null) {
190
+ try {
191
+ if (!character) character = await this.getSelectedCharacter();
192
+ return await this.db.conversations.where("character").equals(character).toArray();
193
+ } catch (error) {
194
+ console.warn("Error getting all conversations:", error);
195
+ return [];
196
+ }
197
+ }
198
+
199
+ async setPreference(key, value) {
200
+ if (key === "openrouterApiKey" || key === "llmApiKey" || key.startsWith("apiKey_")) {
201
+ const isValid = window.KIMI_VALIDATORS?.validateApiKey(value) || window.KimiSecurityUtils?.validateApiKey(value);
202
+ if (!isValid && value.length > 0) {
203
+ throw new Error("Invalid API key format");
204
+ }
205
+ // Store keys in plain text (no encryption) per request
206
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
207
+ window.KimiCacheManager.set(`pref_${key}`, value, 60000);
208
+ }
209
+ return this.db.preferences.put({
210
+ key: key,
211
+ value: value,
212
+ // do not set encrypted flag anymore
213
+ updated: new Date().toISOString()
214
+ });
215
+ }
216
+
217
+ // Update cache for regular preferences
218
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
219
+ window.KimiCacheManager.set(`pref_${key}`, value, 60000);
220
+ }
221
+
222
+ return this.db.preferences.put({
223
+ key: key,
224
+ value: value,
225
+ updated: new Date().toISOString()
226
+ });
227
+ }
228
+
229
+ async getPreference(key, defaultValue = null) {
230
+ // Try cache first (use a singleton cache instance)
231
+ const cacheKey = `pref_${key}`;
232
+ const cache = window.kimiCache instanceof KimiCacheManager ? window.kimiCache : null;
233
+ if (cache && typeof cache.get === "function") {
234
+ const cached = cache.get(cacheKey);
235
+ if (cached !== null) {
236
+ return cached;
237
+ }
238
+ }
239
+
240
+ try {
241
+ const record = await this.db.preferences.get(key);
242
+ if (!record) {
243
+ const cache = window.kimiCache instanceof KimiCacheManager ? window.kimiCache : null;
244
+ if (cache && typeof cache.set === "function") {
245
+ cache.set(cacheKey, defaultValue, 60000); // Cache for 1 minute
246
+ }
247
+ return defaultValue;
248
+ }
249
+
250
+ // Backward compatibility: decrypt legacy encrypted values
251
+ let value = record.value;
252
+ if (record.encrypted && window.KimiSecurityUtils) {
253
+ try {
254
+ value = window.KimiSecurityUtils.decryptApiKey(record.value);
255
+ // One-time migration: store back as plain text without encrypted flag
256
+ try {
257
+ await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() });
258
+ } catch (mErr) {}
259
+ } catch (e) {
260
+ // If decryption fails, fallback to raw value
261
+ console.warn("Failed to decrypt legacy API key; returning raw value", e);
262
+ }
263
+ }
264
+
265
+ // Cache the result
266
+ const cache = window.kimiCache instanceof KimiCacheManager ? window.kimiCache : null;
267
+ if (cache && typeof cache.set === "function") {
268
+ cache.set(cacheKey, value, 60000); // Cache for 1 minute
269
+ }
270
+
271
+ return value;
272
+ } catch (error) {
273
+ console.warn(`Error getting preference ${key}:`, error);
274
+ return defaultValue;
275
+ }
276
+ }
277
+
278
+ async getAllPreferences() {
279
+ try {
280
+ const all = await this.db.preferences.toArray();
281
+ const prefs = {};
282
+ all.forEach(item => {
283
+ prefs[item.key] = item.value;
284
+ });
285
+ return prefs;
286
+ } catch (error) {
287
+ console.warn("Error getting all preferences:", error);
288
+ return {};
289
+ }
290
+ }
291
+
292
+ async setSetting(category, settings) {
293
+ return this.db.settings.put({
294
+ category: category,
295
+ settings: settings,
296
+ updated: new Date().toISOString()
297
+ });
298
+ }
299
+
300
+ async getSetting(category, defaultSettings = {}) {
301
+ const result = await this.db.settings.get(category);
302
+ return result ? result.settings : defaultSettings;
303
+ }
304
+
305
+ async setPersonalityTrait(trait, value, character = null) {
306
+ if (!character) character = await this.getSelectedCharacter();
307
+
308
+ // Invalidate cache
309
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") {
310
+ window.KimiCacheManager.delete(`trait_${character}_${trait}`);
311
+ window.KimiCacheManager.delete(`all_traits_${character}`);
312
+ }
313
+
314
+ return this.db.personality.put({
315
+ trait: trait,
316
+ character: character,
317
+ value: value,
318
+ updated: new Date().toISOString()
319
+ });
320
+ }
321
+
322
+ async getPersonalityTrait(trait, defaultValue = null, character = null) {
323
+ if (!character) character = await this.getSelectedCharacter();
324
+
325
+ // Use unified defaults from emotion system
326
+ if (defaultValue === null) {
327
+ if (window.KimiEmotionSystem) {
328
+ const emotionSystem = new window.KimiEmotionSystem(this);
329
+ defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50;
330
+ } else {
331
+ // Fallback defaults (must match KimiEmotionSystem.TRAIT_DEFAULTS exactly)
332
+ const fallbackDefaults = {
333
+ affection: 65, // Fixed - matches KimiEmotionSystem
334
+ playfulness: 55, // Fixed - matches KimiEmotionSystem
335
+ intelligence: 70, // Fixed - matches KimiEmotionSystem
336
+ empathy: 75, // Fixed - matches KimiEmotionSystem
337
+ humor: 60, // Fixed - matches KimiEmotionSystem
338
+ romance: 50 // Fixed - matches KimiEmotionSystem
339
+ };
340
+ defaultValue = fallbackDefaults[trait] || 50;
341
+ }
342
+ }
343
+
344
+ // Try cache first
345
+ const cacheKey = `trait_${character}_${trait}`;
346
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") {
347
+ const cached = window.KimiCacheManager.get(cacheKey);
348
+ if (cached !== null) {
349
+ return cached;
350
+ }
351
+ }
352
+
353
+ const found = await this.db.personality.get([character, trait]);
354
+ const value = found ? found.value : defaultValue;
355
+
356
+ // Cache the result
357
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
358
+ window.KimiCacheManager.set(cacheKey, value, 120000); // Cache for 2 minutes
359
+ }
360
+ return value;
361
+ }
362
+
363
+ async getAllPersonalityTraits(character = null) {
364
+ if (!character) character = await this.getSelectedCharacter();
365
+
366
+ // Try cache first
367
+ const cacheKey = `all_traits_${character}`;
368
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") {
369
+ const cached = window.KimiCacheManager.get(cacheKey);
370
+ if (cached !== null) {
371
+ return cached;
372
+ }
373
+ }
374
+
375
+ const all = await this.db.personality.where("character").equals(character).toArray();
376
+ const traits = {};
377
+ all.forEach(item => {
378
+ traits[item.trait] = item.value;
379
+ });
380
+
381
+ // Cache the result
382
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") {
383
+ window.KimiCacheManager.set(cacheKey, traits, 120000); // Cache for 2 minutes
384
+ }
385
+ return traits;
386
+ }
387
+
388
+ async savePersonality(personalityObj, character = null) {
389
+ if (!character) character = await this.getSelectedCharacter();
390
+ // Invalidate caches for all affected traits and the aggregate cache for this character
391
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") {
392
+ try {
393
+ Object.keys(personalityObj).forEach(trait => {
394
+ window.KimiCacheManager.delete(`trait_${character}_${trait}`);
395
+ });
396
+ window.KimiCacheManager.delete(`all_traits_${character}`);
397
+ } catch (e) {}
398
+ }
399
+ const entries = Object.entries(personalityObj).map(([trait, value]) =>
400
+ this.db.personality.put({
401
+ trait: trait,
402
+ character: character,
403
+ value: value,
404
+ updated: new Date().toISOString()
405
+ })
406
+ );
407
+ return Promise.all(entries);
408
+ }
409
+
410
+ async getPersonality(character = null) {
411
+ return this.getAllPersonalityTraits(character);
412
+ }
413
+
414
+ async saveLLMModel(id, name, provider, apiKey, config) {
415
+ return this.db.llmModels.put({
416
+ id: id,
417
+ name: name,
418
+ provider: provider,
419
+ apiKey: apiKey,
420
+ config: config,
421
+ added: new Date().toISOString(),
422
+ lastUsed: null
423
+ });
424
+ }
425
+
426
+ async getLLMModel(id) {
427
+ return this.db.llmModels.get(id);
428
+ }
429
+
430
+ async getAllLLMModels() {
431
+ try {
432
+ return await this.db.llmModels.toArray();
433
+ } catch (error) {
434
+ console.warn("Error getting all LLM models:", error);
435
+ return [];
436
+ }
437
+ }
438
+
439
+ async deleteLLMModel(id) {
440
+ return this.db.llmModels.delete(id);
441
+ }
442
+
443
+ async cleanOldConversations(days = null, character = null) {
444
+ // If days not provided, fallback to full clean (legacy behavior)
445
+ if (days === null) {
446
+ if (character) {
447
+ const all = await this.db.conversations.where("character").equals(character).toArray();
448
+ const ids = all.map(item => item.id);
449
+ return this.db.conversations.bulkDelete(ids);
450
+ } else {
451
+ return this.db.conversations.clear();
452
+ }
453
+ }
454
+ const threshold = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
455
+ if (character) {
456
+ const toDelete = await this.db.conversations
457
+ .where("character")
458
+ .equals(character)
459
+ .and(c => c.timestamp < threshold)
460
+ .toArray();
461
+ const ids = toDelete.map(item => item.id);
462
+ return this.db.conversations.bulkDelete(ids);
463
+ } else {
464
+ const toDelete = await this.db.conversations.where("timestamp").below(threshold).toArray();
465
+ const ids = toDelete.map(item => item.id);
466
+ return this.db.conversations.bulkDelete(ids);
467
+ }
468
+ }
469
+
470
+ async getStorageStats() {
471
+ try {
472
+ const conversations = await this.getAllConversations();
473
+ const preferences = await this.getAllPreferences();
474
+ const models = await this.getAllLLMModels();
475
+ return {
476
+ conversations: conversations ? conversations.length : 0,
477
+ preferences: preferences ? Object.keys(preferences).length : 0,
478
+ models: models ? models.length : 0,
479
+ totalSize: JSON.stringify({
480
+ conversations: conversations || [],
481
+ preferences: preferences || {},
482
+ models: models || []
483
+ }).length
484
+ };
485
+ } catch (error) {
486
+ console.error("Error getting storage stats:", error);
487
+ return {
488
+ conversations: 0,
489
+ preferences: 0,
490
+ models: 0,
491
+ totalSize: 0
492
+ };
493
+ }
494
+ }
495
+
496
+ async deleteSingleMessage(conversationId, sender) {
497
+ const conv = await this.db.conversations.get(conversationId);
498
+ if (!conv) return;
499
+ if (sender === "user") {
500
+ conv.user = "";
501
+ } else if (sender === "kimi") {
502
+ conv.kimi = "";
503
+ }
504
+ if ((conv.user === undefined || conv.user === "") && (conv.kimi === undefined || conv.kimi === "")) {
505
+ await this.db.conversations.delete(conversationId);
506
+ } else {
507
+ await this.db.conversations.put(conv);
508
+ }
509
+ }
510
+
511
+ async setPreferencesBatch(prefsArray) {
512
+ const batch = prefsArray.map(({ key, value }) => ({ key, value, updated: new Date().toISOString() }));
513
+ return this.db.preferences.bulkPut(batch);
514
+ }
515
+ async setPersonalityBatch(traitsObj, character = null) {
516
+ if (!character) character = await this.getSelectedCharacter();
517
+ // Invalidate caches for all affected traits and the aggregate cache for this character
518
+ if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") {
519
+ try {
520
+ Object.keys(traitsObj).forEach(trait => {
521
+ window.KimiCacheManager.delete(`trait_${character}_${trait}`);
522
+ });
523
+ window.KimiCacheManager.delete(`all_traits_${character}`);
524
+ } catch (e) {}
525
+ }
526
+
527
+ const batch = Object.entries(traitsObj).map(([trait, value]) => ({
528
+ trait,
529
+ character,
530
+ value,
531
+ updated: new Date().toISOString()
532
+ }));
533
+ return this.db.personality.bulkPut(batch);
534
+ }
535
+ async setSettingsBatch(settingsArray) {
536
+ const batch = settingsArray.map(({ category, settings }) => ({
537
+ category,
538
+ settings,
539
+ updated: new Date().toISOString()
540
+ }));
541
+ return this.db.settings.bulkPut(batch);
542
+ }
543
+ async getPreferencesBatch(keys) {
544
+ const results = await this.db.preferences.where("key").anyOf(keys).toArray();
545
+ const out = {};
546
+ for (const item of results) {
547
+ let val = item.value;
548
+ if (item.encrypted && window.KimiSecurityUtils) {
549
+ try {
550
+ val = window.KimiSecurityUtils.decryptApiKey(item.value);
551
+ // Migrate back as plain
552
+ try {
553
+ await this.db.preferences.put({ key: item.key, value: val, updated: new Date().toISOString() });
554
+ } catch (mErr) {}
555
+ } catch (e) {
556
+ console.warn("Failed to decrypt legacy pref in batch:", item.key, e);
557
+ }
558
+ }
559
+ out[item.key] = val;
560
+ }
561
+ return out;
562
+ }
563
+ async getPersonalityTraitsBatch(traits, character = null) {
564
+ if (!character) character = await this.getSelectedCharacter();
565
+ const results = await this.db.personality.where("character").equals(character).toArray();
566
+ const out = {};
567
+ traits.forEach(trait => {
568
+ const found = results.find(item => item.trait === trait);
569
+ out[trait] = found ? found.value : 50;
570
+ });
571
+ return out;
572
+ }
573
+
574
+ async getSelectedCharacter() {
575
+ try {
576
+ return await this.getPreference("selectedCharacter", "kimi");
577
+ } catch (error) {
578
+ console.warn("Error getting selected character:", error);
579
+ return "kimi";
580
+ }
581
+ }
582
+
583
+ async setSelectedCharacter(character) {
584
+ try {
585
+ await this.setPreference("selectedCharacter", character);
586
+ } catch (error) {
587
+ console.error("Error setting selected character:", error);
588
+ }
589
+ }
590
+
591
+ async getSystemPromptForCharacter(character = null) {
592
+ if (!character) character = await this.getSelectedCharacter();
593
+ try {
594
+ const prompt = await this.getPreference(`systemPrompt_${character}`, null);
595
+ if (prompt) return prompt;
596
+
597
+ if (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character] && window.KIMI_CHARACTERS[character].defaultPrompt) {
598
+ return window.KIMI_CHARACTERS[character].defaultPrompt;
599
+ }
600
+
601
+ return window.DEFAULT_SYSTEM_PROMPT || "";
602
+ } catch (error) {
603
+ console.warn("Error getting system prompt for character:", error);
604
+ return window.DEFAULT_SYSTEM_PROMPT || "";
605
+ }
606
+ }
607
+
608
+ async setSystemPromptForCharacter(character, prompt) {
609
+ if (!character) character = await this.getSelectedCharacter();
610
+ try {
611
+ await this.setPreference(`systemPrompt_${character}`, prompt);
612
+ } catch (error) {
613
+ console.error("Error setting system prompt for character:", error);
614
+ }
615
+ }
616
+ }
617
+
618
+ // Export for usage
619
+ window.KimiDatabase = KimiDatabase;
kimi-js/kimi-emotion-system.js ADDED
@@ -0,0 +1,516 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // Unified emotion mappings
10
+ this.EMOTIONS = {
11
+ // Base emotions
12
+ POSITIVE: "positive",
13
+ NEGATIVE: "negative",
14
+ NEUTRAL: "neutral",
15
+
16
+ // Specific emotions
17
+ ROMANTIC: "romantic",
18
+ DANCING: "dancing",
19
+ LISTENING: "listening",
20
+ LAUGHING: "laughing",
21
+ SURPRISE: "surprise",
22
+ CONFIDENT: "confident",
23
+ SHY: "shy",
24
+ FLIRTATIOUS: "flirtatious",
25
+ KISS: "kiss",
26
+ GOODBYE: "goodbye"
27
+ };
28
+
29
+ // Unified video context mapping
30
+ this.emotionToVideoCategory = {
31
+ positive: "speakingPositive",
32
+ negative: "speakingNegative",
33
+ neutral: "neutral",
34
+ dancing: "dancing",
35
+ listening: "listening",
36
+ romantic: "speakingPositive",
37
+ laughing: "speakingPositive",
38
+ surprise: "speakingPositive",
39
+ confident: "speakingPositive",
40
+ shy: "neutral",
41
+ flirtatious: "speakingPositive",
42
+ kiss: "speakingPositive",
43
+ goodbye: "neutral"
44
+ };
45
+
46
+ // Unified trait defaults - More balanced for progressive experience
47
+ this.TRAIT_DEFAULTS = {
48
+ affection: 65, // Reduced from 80 - starts neutral, grows with interaction
49
+ playfulness: 55, // Reduced from 70 - more reserved initially
50
+ intelligence: 70, // Reduced from 85 - still competent but not overwhelming
51
+ empathy: 75, // Reduced from 90 - caring but not overly so
52
+ humor: 60, // Reduced from 75 - develops sense of humor over time
53
+ romance: 50 // Significantly reduced from 95 - romance must be earned!
54
+ };
55
+ }
56
+
57
+ // ===== UNIFIED EMOTION ANALYSIS =====
58
+ analyzeEmotion(text, lang = "auto") {
59
+ if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL;
60
+ const lowerText = text.toLowerCase();
61
+
62
+ // Auto-detect language
63
+ let detectedLang = this._detectLanguage(text, lang);
64
+
65
+ // Get language-specific keywords
66
+ const positiveWords = window.KIMI_CONTEXT_POSITIVE?.[detectedLang] ||
67
+ window.KIMI_CONTEXT_POSITIVE?.en || ["happy", "good", "great", "love"];
68
+ const negativeWords = window.KIMI_CONTEXT_NEGATIVE?.[detectedLang] ||
69
+ window.KIMI_CONTEXT_NEGATIVE?.en || ["sad", "bad", "angry", "hate"];
70
+
71
+ const emotionKeywords = window.KIMI_CONTEXT_KEYWORDS?.[detectedLang] || window.KIMI_CONTEXT_KEYWORDS?.en || {};
72
+
73
+ // Priority order for emotion detection
74
+ const emotionChecks = [
75
+ // Listening intent (user asks to talk or indicates speaking/listening)
76
+ {
77
+ emotion: this.EMOTIONS.LISTENING,
78
+ keywords: emotionKeywords.listening || [
79
+ "listen",
80
+ "listening",
81
+ "écoute",
82
+ "ecoute",
83
+ "écouter",
84
+ "parle",
85
+ "speak",
86
+ "talk",
87
+ "question",
88
+ "ask"
89
+ ]
90
+ },
91
+ { emotion: this.EMOTIONS.DANCING, keywords: emotionKeywords.dancing || ["dance", "dancing"] },
92
+ { emotion: this.EMOTIONS.ROMANTIC, keywords: emotionKeywords.romantic || ["love", "romantic"] },
93
+ { emotion: this.EMOTIONS.LAUGHING, keywords: emotionKeywords.laughing || ["laugh", "funny"] },
94
+ { emotion: this.EMOTIONS.SURPRISE, keywords: emotionKeywords.surprise || ["wow", "surprise"] },
95
+ { emotion: this.EMOTIONS.CONFIDENT, keywords: emotionKeywords.confident || ["confident", "strong"] },
96
+ { emotion: this.EMOTIONS.SHY, keywords: emotionKeywords.shy || ["shy", "embarrassed"] },
97
+ { emotion: this.EMOTIONS.FLIRTATIOUS, keywords: emotionKeywords.flirtatious || ["flirt", "tease"] },
98
+ { emotion: this.EMOTIONS.KISS, keywords: emotionKeywords.kiss || ["kiss", "embrace"] },
99
+ { emotion: this.EMOTIONS.GOODBYE, keywords: emotionKeywords.goodbye || ["goodbye", "bye"] }
100
+ ];
101
+
102
+ // Check for specific emotions first, applying sensitivity weights per language
103
+ const sensitivity = (window.KIMI_EMOTION_SENSITIVITY &&
104
+ (window.KIMI_EMOTION_SENSITIVITY[detectedLang] || window.KIMI_EMOTION_SENSITIVITY.default)) || {
105
+ listening: 1,
106
+ dancing: 1,
107
+ romantic: 1,
108
+ laughing: 1,
109
+ surprise: 1,
110
+ confident: 1,
111
+ shy: 1,
112
+ flirtatious: 1,
113
+ kiss: 1,
114
+ goodbye: 1,
115
+ positive: 1,
116
+ negative: 1
117
+ };
118
+
119
+ let bestEmotion = null;
120
+ let bestScore = 0;
121
+ for (const check of emotionChecks) {
122
+ const hits = check.keywords.reduce((acc, word) => acc + (lowerText.includes(word.toLowerCase()) ? 1 : 0), 0);
123
+ if (hits > 0) {
124
+ const key = check.emotion;
125
+ const weight = sensitivity[key] != null ? sensitivity[key] : 1;
126
+ const score = hits * weight;
127
+ if (score > bestScore) {
128
+ bestScore = score;
129
+ bestEmotion = check.emotion;
130
+ }
131
+ }
132
+ }
133
+ if (bestEmotion) return bestEmotion;
134
+
135
+ // Fall back to positive/negative analysis
136
+ const hasPositive = positiveWords.some(word => lowerText.includes(word.toLowerCase()));
137
+ const hasNegative = negativeWords.some(word => lowerText.includes(word.toLowerCase()));
138
+
139
+ if (hasPositive && !hasNegative) {
140
+ // Apply sensitivity for base polarity
141
+ if ((sensitivity.positive || 1) >= (sensitivity.negative || 1)) return this.EMOTIONS.POSITIVE;
142
+ // If negative is favored, still fall back to positive since no negative hit
143
+ return this.EMOTIONS.POSITIVE;
144
+ }
145
+ if (hasNegative && !hasPositive) {
146
+ if ((sensitivity.negative || 1) >= (sensitivity.positive || 1)) return this.EMOTIONS.NEGATIVE;
147
+ return this.EMOTIONS.NEGATIVE;
148
+ }
149
+ return this.EMOTIONS.NEUTRAL;
150
+ }
151
+
152
+ // ===== UNIFIED PERSONALITY SYSTEM =====
153
+ async updatePersonalityFromEmotion(emotion, text, character = null) {
154
+ if (!this.db) {
155
+ console.warn("Database not available for personality updates");
156
+ return;
157
+ }
158
+
159
+ const selectedCharacter = character || (await this.db.getSelectedCharacter());
160
+ const traits = await this.db.getAllPersonalityTraits(selectedCharacter);
161
+
162
+ let affection = traits.affection || this.TRAIT_DEFAULTS.affection;
163
+ let romance = traits.romance || this.TRAIT_DEFAULTS.romance;
164
+ let empathy = traits.empathy || this.TRAIT_DEFAULTS.empathy;
165
+ let playfulness = traits.playfulness || this.TRAIT_DEFAULTS.playfulness;
166
+ let humor = traits.humor || this.TRAIT_DEFAULTS.humor;
167
+ let intelligence = traits.intelligence || this.TRAIT_DEFAULTS.intelligence;
168
+
169
+ // Unified adjustment functions - More gradual progression for balanced experience
170
+ const adjustUp = (val, amount) => {
171
+ // Slower progression as values get higher - make romance and affection harder to max out
172
+ if (val >= 95) return val + amount * 0.1; // Very slow near max
173
+ if (val >= 85) return val + amount * 0.3; // Slow progression at high levels
174
+ if (val >= 70) return val + amount * 0.6; // Moderate progression at medium levels
175
+ if (val >= 50) return val + amount * 0.8; // Normal progression above average
176
+ return val + amount; // Normal progression below average
177
+ };
178
+
179
+ const adjustDown = (val, amount) => {
180
+ // Faster decline at higher values - easier to lose than to gain
181
+ if (val >= 80) return val - amount * 1.2; // Faster loss at high levels
182
+ if (val >= 60) return val - amount; // Normal loss at medium levels
183
+ if (val >= 40) return val - amount * 0.8; // Slower loss at low-medium levels
184
+ if (val <= 20) return val - amount * 0.4; // Very slow loss at low levels
185
+ return val - amount * 0.6; // Moderate loss between 20-40
186
+ };
187
+
188
+ // Unified emotion-based adjustments - More balanced and realistic progression
189
+ const gainCfg = window.KIMI_TRAIT_ADJUSTMENT || {
190
+ globalGain: 1,
191
+ globalLoss: 1,
192
+ emotionGain: {},
193
+ traitGain: {},
194
+ traitLoss: {}
195
+ };
196
+ const emoGain = emotion && gainCfg.emotionGain ? gainCfg.emotionGain[emotion] || 1 : 1;
197
+ const GGAIN = (gainCfg.globalGain || 1) * emoGain;
198
+ const GLOSS = gainCfg.globalLoss || 1;
199
+
200
+ // Helpers to apply trait-specific scaling
201
+ const scaleGain = (traitName, baseDelta) => {
202
+ const t = gainCfg.traitGain && (gainCfg.traitGain[traitName] || 1);
203
+ return baseDelta * GGAIN * t;
204
+ };
205
+ const scaleLoss = (traitName, baseDelta) => {
206
+ const t = gainCfg.traitLoss && (gainCfg.traitLoss[traitName] || 1);
207
+ return baseDelta * GLOSS * t;
208
+ };
209
+
210
+ switch (emotion) {
211
+ case this.EMOTIONS.POSITIVE:
212
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.4))); // Slightly more affection gain
213
+ empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.2)));
214
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.2)));
215
+ humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.2)));
216
+ romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.1))); // Romance grows very slowly
217
+ break;
218
+ case this.EMOTIONS.NEGATIVE:
219
+ affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.6))); // Affection drops faster on negative
220
+ empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.3))); // Empathy still grows (understanding pain)
221
+ break;
222
+ case this.EMOTIONS.ROMANTIC:
223
+ romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.6))); // Reduced from 0.8 - romance should be earned
224
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.3))); // Reduced from 0.4
225
+ break;
226
+ case this.EMOTIONS.LAUGHING:
227
+ humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.8))); // Humor grows with laughter
228
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.4))); // Increased playfulness connection
229
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.2))); // Small affection boost from shared laughter
230
+ break;
231
+ case this.EMOTIONS.DANCING:
232
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 1.2))); // Dancing = maximum playfulness boost
233
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.3))); // Affection from shared activity
234
+ break;
235
+ case this.EMOTIONS.SHY:
236
+ affection = Math.max(0, adjustDown(affection, scaleLoss("affection", 0.1))); // Small affection loss
237
+ romance = Math.max(0, adjustDown(romance, scaleLoss("romance", 0.2))); // Shyness reduces romance more
238
+ break;
239
+ case this.EMOTIONS.CONFIDENT:
240
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.3))); // Reduced from 0.4
241
+ intelligence = Math.min(100, adjustUp(intelligence, scaleGain("intelligence", 0.1))); // Slight intelligence boost
242
+ break;
243
+ case this.EMOTIONS.FLIRTATIOUS:
244
+ romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.5))); // Reduced from 0.6
245
+ playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.3))); // Reduced from 0.4
246
+ affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.2))); // Small affection boost
247
+ break;
248
+ }
249
+
250
+ // Content-based adjustments (unified)
251
+ await this._analyzeTextContent(
252
+ text,
253
+ traits => {
254
+ romance = traits.romance;
255
+ affection = traits.affection;
256
+ humor = traits.humor;
257
+ playfulness = traits.playfulness;
258
+ },
259
+ adjustUp
260
+ );
261
+
262
+ const updatedTraits = {
263
+ affection: Math.round(affection),
264
+ romance: Math.round(romance),
265
+ empathy: Math.round(empathy),
266
+ playfulness: Math.round(playfulness),
267
+ humor: Math.round(humor),
268
+ intelligence: Math.round(intelligence)
269
+ };
270
+
271
+ // Save to database
272
+ await this.db.setPersonalityBatch(updatedTraits, selectedCharacter);
273
+
274
+ return updatedTraits;
275
+ }
276
+
277
+ // ===== UNIFIED LLM PERSONALITY ANALYSIS =====
278
+ async updatePersonalityFromConversation(userMessage, kimiResponse, character = null) {
279
+ if (!this.db) return;
280
+
281
+ const lowerUser = userMessage ? userMessage.toLowerCase() : "";
282
+ const lowerKimi = (kimiResponse || "").toLowerCase();
283
+ const traits = await this.db.getAllPersonalityTraits(character);
284
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
285
+
286
+ // Use unified keyword system
287
+ const getPersonalityWords = (trait, type) => {
288
+ if (window.KIMI_PERSONALITY_KEYWORDS && window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage]) {
289
+ return window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage][trait]?.[type] || [];
290
+ }
291
+ return this._getFallbackKeywords(trait, type);
292
+ };
293
+
294
+ for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) {
295
+ const posWords = getPersonalityWords(trait, "positive");
296
+ const negWords = getPersonalityWords(trait, "negative");
297
+ let value = typeof traits[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait];
298
+
299
+ // Count occurrences with proper weighting
300
+ let posCount = 0;
301
+ let negCount = 0;
302
+
303
+ for (const w of posWords) {
304
+ posCount += (lowerUser.match(new RegExp(w, "g")) || []).length * 1.0;
305
+ posCount += (lowerKimi.match(new RegExp(w, "g")) || []).length * 0.3;
306
+ }
307
+ for (const w of negWords) {
308
+ negCount += (lowerUser.match(new RegExp(w, "g")) || []).length * 1.0;
309
+ negCount += (lowerKimi.match(new RegExp(w, "g")) || []).length * 0.3;
310
+ }
311
+
312
+ const delta = (posCount - negCount) * 0.3; // Reduced from 0.4 - slower LLM-based progression
313
+
314
+ // Apply streak logic
315
+ if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0;
316
+
317
+ if (negCount > 0 && posCount === 0) {
318
+ this.negativeStreaks[trait]++;
319
+ if (this.negativeStreaks[trait] >= 3) {
320
+ value = Math.max(0, Math.min(100, value + delta - 1));
321
+ } else {
322
+ value = Math.max(0, Math.min(100, value + delta));
323
+ }
324
+ } else if (posCount > 0) {
325
+ this.negativeStreaks[trait] = 0;
326
+ value = Math.max(0, Math.min(100, value + delta));
327
+ }
328
+
329
+ if (delta !== 0) {
330
+ await this.db.setPersonalityTrait(trait, value, character);
331
+ }
332
+ }
333
+ }
334
+
335
+ // ===== UNIFIED VIDEO CONTEXT MAPPING =====
336
+ mapEmotionToVideoCategory(emotion) {
337
+ return this.emotionToVideoCategory[emotion] || "neutral";
338
+ }
339
+
340
+ // ===== VALIDATION SYSTEM =====
341
+ validateEmotion(emotion) {
342
+ const validEmotions = Object.values(this.EMOTIONS);
343
+ if (!validEmotions.includes(emotion)) {
344
+ console.warn(`Invalid emotion detected: ${emotion}, falling back to neutral`);
345
+ return this.EMOTIONS.NEUTRAL;
346
+ }
347
+ return emotion;
348
+ }
349
+
350
+ validatePersonalityTrait(trait, value) {
351
+ if (typeof value !== "number" || value < 0 || value > 100) {
352
+ console.warn(`Invalid trait value for ${trait}: ${value}, using default`);
353
+ return this.TRAIT_DEFAULTS[trait] || 50;
354
+ }
355
+ return value;
356
+ }
357
+
358
+ // ===== UTILITY METHODS =====
359
+ _detectLanguage(text, lang) {
360
+ if (lang !== "auto") return lang;
361
+
362
+ if (/[àâäéèêëîïôöùûüÿç]/i.test(text)) return "fr";
363
+ else if (/[äöüß]/i.test(text)) return "de";
364
+ else if (/[ñáéíóúü]/i.test(text)) return "es";
365
+ else if (/[àèìòù]/i.test(text)) return "it";
366
+ else if (/[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]/i.test(text)) return "ja";
367
+ else if (/[\u4e00-\u9fff]/i.test(text)) return "zh";
368
+ return "en";
369
+ }
370
+
371
+ async _analyzeTextContent(text, callback, adjustUp) {
372
+ if (!this.db) return;
373
+
374
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
375
+ const romanticWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.romantic ||
376
+ window.KIMI_CONTEXT_KEYWORDS?.en?.romantic || ["love", "romantic", "kiss"];
377
+ const humorWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.laughing ||
378
+ window.KIMI_CONTEXT_KEYWORDS?.en?.laughing || ["joke", "funny", "lol"];
379
+
380
+ const romanticPattern = new RegExp(`(${romanticWords.join("|")})`, "i");
381
+ const humorPattern = new RegExp(`(${humorWords.join("|")})`, "i");
382
+
383
+ const traits = {};
384
+ if (text.match(romanticPattern)) {
385
+ traits.romance = adjustUp(traits.romance || this.TRAIT_DEFAULTS.romance, 0.5);
386
+ traits.affection = adjustUp(traits.affection || this.TRAIT_DEFAULTS.affection, 0.5);
387
+ }
388
+ if (text.match(humorPattern)) {
389
+ traits.humor = adjustUp(traits.humor || this.TRAIT_DEFAULTS.humor, 2);
390
+ traits.playfulness = adjustUp(traits.playfulness || this.TRAIT_DEFAULTS.playfulness, 1);
391
+ }
392
+
393
+ callback(traits);
394
+ }
395
+
396
+ _getFallbackKeywords(trait, type) {
397
+ const fallbackKeywords = {
398
+ humor: {
399
+ positive: ["funny", "hilarious", "joke", "laugh", "amusing"],
400
+ negative: ["boring", "sad", "serious", "cold", "dry"]
401
+ },
402
+ intelligence: {
403
+ positive: ["intelligent", "smart", "brilliant", "logical", "clever"],
404
+ negative: ["stupid", "dumb", "foolish", "slow", "naive"]
405
+ },
406
+ romance: {
407
+ positive: ["cuddle", "love", "romantic", "kiss", "tenderness"],
408
+ negative: ["cold", "distant", "indifferent", "rejection"]
409
+ },
410
+ affection: {
411
+ positive: ["affection", "tenderness", "close", "warmth", "kind"],
412
+ negative: ["mean", "cold", "indifferent", "distant", "rejection"]
413
+ },
414
+ playfulness: {
415
+ positive: ["play", "game", "tease", "mischievous", "fun"],
416
+ negative: ["serious", "boring", "strict", "rigid"]
417
+ },
418
+ empathy: {
419
+ positive: ["listen", "understand", "empathy", "support", "help"],
420
+ negative: ["indifferent", "cold", "selfish", "ignore"]
421
+ }
422
+ };
423
+
424
+ return fallbackKeywords[trait]?.[type] || [];
425
+ }
426
+
427
+ // ===== PERSONALITY CALCULATION =====
428
+ calculatePersonalityAverage(traits) {
429
+ const keys = ["affection", "romance", "empathy", "playfulness", "humor"];
430
+ let sum = 0;
431
+ let count = 0;
432
+
433
+ keys.forEach(key => {
434
+ if (typeof traits[key] === "number") {
435
+ sum += traits[key];
436
+ count++;
437
+ }
438
+ });
439
+
440
+ return count > 0 ? sum / count : 50;
441
+ }
442
+
443
+ getMoodCategoryFromPersonality(traits) {
444
+ const avg = this.calculatePersonalityAverage(traits);
445
+
446
+ if (avg >= 80) return "speakingPositive";
447
+ if (avg >= 60) return "neutral";
448
+ if (avg >= 40) return "neutral";
449
+ if (avg >= 20) return "speakingNegative";
450
+ return "speakingNegative";
451
+ }
452
+ }
453
+
454
+ // ===== GLOBAL EXPORT =====
455
+ window.KimiEmotionSystem = KimiEmotionSystem;
456
+
457
+ // ===== BACKWARD COMPATIBILITY LAYER =====
458
+ // Replace the old kimiAnalyzeEmotion function
459
+ window.kimiAnalyzeEmotion = function (text, lang = "auto") {
460
+ if (!window.kimiEmotionSystem) {
461
+ window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
462
+ }
463
+ return window.kimiEmotionSystem.analyzeEmotion(text, lang);
464
+ };
465
+
466
+ // Replace the old updatePersonalityTraitsFromEmotion function
467
+ window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
468
+ if (!window.kimiEmotionSystem) {
469
+ window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
470
+ }
471
+
472
+ const updatedTraits = await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text);
473
+
474
+ // Update UI sliders
475
+ if (updatedTraits && window.updateSlider) {
476
+ window.updateSlider("trait-affection", updatedTraits.affection);
477
+ window.updateSlider("trait-romance", updatedTraits.romance);
478
+ window.updateSlider("trait-empathy", updatedTraits.empathy);
479
+ window.updateSlider("trait-playfulness", updatedTraits.playfulness);
480
+ window.updateSlider("trait-humor", updatedTraits.humor);
481
+ window.updateSlider("trait-intelligence", updatedTraits.intelligence);
482
+ }
483
+
484
+ // Update memory system
485
+ if (window.kimiMemory && updatedTraits) {
486
+ window.kimiMemory.affectionTrait = updatedTraits.affection;
487
+ if (window.kimiMemory.updateFavorabilityBar) {
488
+ window.kimiMemory.updateFavorabilityBar();
489
+ }
490
+ }
491
+
492
+ // Update video context
493
+ if (window.kimiVideo && window.kimiVideo.setMoodByPersonality && updatedTraits) {
494
+ window.kimiVideo.setMoodByPersonality(updatedTraits);
495
+ }
496
+
497
+ // Dispatch event
498
+ if (window.dispatchEvent && updatedTraits) {
499
+ const selectedCharacter = window.kimiDB ? await window.kimiDB.getSelectedCharacter() : "kimi";
500
+ window.dispatchEvent(
501
+ new CustomEvent("personalityUpdated", {
502
+ detail: { character: selectedCharacter, traits: updatedTraits }
503
+ })
504
+ );
505
+ }
506
+
507
+ return updatedTraits;
508
+ };
509
+
510
+ // Replace getPersonalityAverage function
511
+ window.getPersonalityAverage = function (traits) {
512
+ if (!window.kimiEmotionSystem) {
513
+ window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB);
514
+ }
515
+ return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
516
+ };
kimi-js/kimi-error-manager.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
174
+ // Create global instance
175
+ window.kimiErrorManager = new KimiErrorManager();
176
+
177
+ // Export class for manual instantiation if needed
178
+ window.KimiErrorManager = KimiErrorManager;
kimi-js/kimi-health-check.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI APPLICATION HEALTH CHECK =====
2
+ // This script runs comprehensive checks to ensure the application is production-ready
3
+
4
+ class KimiHealthCheck {
5
+ constructor() {
6
+ this.errors = [];
7
+ this.warnings = [];
8
+ this.passed = [];
9
+ }
10
+
11
+ checkDependencies() {
12
+ const requiredGlobals = [
13
+ "KimiDatabase",
14
+ "KimiLLMManager",
15
+ "KimiVoiceManager",
16
+ "KimiMemorySystem",
17
+ "KimiMemory",
18
+ "KimiEmotionSystem",
19
+ "KimiAppearanceManager",
20
+ "KimiVideoManager",
21
+ "KimiI18nManager",
22
+ "KimiErrorManager",
23
+ "KimiDOMUtils",
24
+ "KIMI_CHARACTERS",
25
+ "KIMI_CONFIG",
26
+ "DEFAULT_SYSTEM_PROMPT"
27
+ ];
28
+
29
+ requiredGlobals.forEach(dep => {
30
+ if (window[dep]) {
31
+ this.passed.push(`✅ ${dep} available`);
32
+ } else {
33
+ this.errors.push(`❌ Missing dependency: ${dep}`);
34
+ }
35
+ });
36
+ }
37
+
38
+ checkFunctions() {
39
+ const requiredFunctions = [
40
+ "sendMessage",
41
+ "analyzeAndReact",
42
+ "addMessageToChat",
43
+ "loadChatHistory",
44
+ "loadSettingsData",
45
+ "updatePersonalityTraitsFromEmotion",
46
+ "kimiAnalyzeEmotion",
47
+ "getPersonalityAverage"
48
+ ];
49
+
50
+ requiredFunctions.forEach(fn => {
51
+ if (window[fn] && typeof window[fn] === "function") {
52
+ this.passed.push(`✅ Function ${fn} available`);
53
+ } else {
54
+ this.errors.push(`❌ Missing function: ${fn}`);
55
+ }
56
+ });
57
+ }
58
+
59
+ checkDOMElements() {
60
+ const requiredElements = [
61
+ "video1",
62
+ "video2",
63
+ "chat-container",
64
+ "chat-input",
65
+ "send-button",
66
+ "settings-overlay",
67
+ "memory-overlay"
68
+ ];
69
+
70
+ requiredElements.forEach(id => {
71
+ const element = document.getElementById(id);
72
+ if (element) {
73
+ this.passed.push(`✅ DOM element ${id} found`);
74
+ } else {
75
+ this.errors.push(`❌ Missing DOM element: ${id}`);
76
+ }
77
+ });
78
+ }
79
+
80
+ checkConfiguration() {
81
+ if (window.KIMI_CHARACTERS) {
82
+ const characters = Object.keys(window.KIMI_CHARACTERS);
83
+ if (characters.length > 0) {
84
+ this.passed.push(`✅ ${characters.length} characters configured`);
85
+
86
+ characters.forEach(char => {
87
+ const character = window.KIMI_CHARACTERS[char];
88
+ if (character.traits && character.defaultPrompt) {
89
+ this.passed.push(`✅ Character ${char} properly configured`);
90
+ } else {
91
+ this.warnings.push(`⚠️ Character ${char} missing traits or defaultPrompt`);
92
+ }
93
+ });
94
+ } else {
95
+ this.errors.push(`❌ No characters configured`);
96
+ }
97
+ }
98
+ }
99
+
100
+ async checkDatabase() {
101
+ try {
102
+ if (window.kimiDB) {
103
+ const testPref = await window.kimiDB.getPreference("healthCheck", "test");
104
+ if (testPref === "test") {
105
+ this.passed.push(`✅ Database read/write working`);
106
+ }
107
+
108
+ const character = await window.kimiDB.getSelectedCharacter();
109
+ if (character) {
110
+ this.passed.push(`✅ Character selection working: ${character}`);
111
+ }
112
+ } else {
113
+ this.errors.push(`❌ Database not initialized`);
114
+ }
115
+ } catch (error) {
116
+ this.errors.push(`❌ Database error: ${error.message}`);
117
+ }
118
+ }
119
+
120
+ checkMemorySystem() {
121
+ if (window.kimiMemorySystem) {
122
+ if (window.kimiMemorySystem.db) {
123
+ this.passed.push(`✅ Memory system initialized`);
124
+ } else {
125
+ this.warnings.push(`⚠️ Memory system not connected to database`);
126
+ }
127
+ } else {
128
+ this.errors.push(`❌ Memory system not available`);
129
+ }
130
+ }
131
+
132
+ async runAllChecks() {
133
+ console.log("🔍 Running Kimi Health Checks...");
134
+
135
+ this.checkDependencies();
136
+ this.checkFunctions();
137
+ this.checkDOMElements();
138
+ this.checkConfiguration();
139
+ await this.checkDatabase();
140
+ this.checkMemorySystem();
141
+
142
+ return this.generateReport();
143
+ }
144
+
145
+ generateReport() {
146
+ const report = {
147
+ status: this.errors.length === 0 ? "HEALTHY" : "NEEDS_ATTENTION",
148
+ totalChecks: this.passed.length + this.warnings.length + this.errors.length,
149
+ passed: this.passed.length,
150
+ warnings: this.warnings.length,
151
+ errors: this.errors.length,
152
+ details: {
153
+ passed: this.passed,
154
+ warnings: this.warnings,
155
+ errors: this.errors
156
+ }
157
+ };
158
+
159
+ console.log(`\n📊 HEALTH CHECK REPORT:`);
160
+ console.log(`Status: ${report.status}`);
161
+ console.log(`✅ Passed: ${report.passed}`);
162
+ console.log(`⚠️ Warnings: ${report.warnings}`);
163
+ console.log(`❌ Errors: ${report.errors}`);
164
+
165
+ if (this.errors.length > 0) {
166
+ console.log(`\n🚨 CRITICAL ISSUES:`);
167
+ this.errors.forEach(error => console.log(error));
168
+ }
169
+
170
+ if (this.warnings.length > 0) {
171
+ console.log(`\n⚠️ WARNINGS:`);
172
+ this.warnings.forEach(warning => console.log(warning));
173
+ }
174
+
175
+ return report;
176
+ }
177
+ }
178
+
179
+ // Auto-run health check when DOM is ready
180
+ if (document.readyState === "loading") {
181
+ document.addEventListener("DOMContentLoaded", async () => {
182
+ await new Promise(resolve => setTimeout(resolve, 2000)); // Wait for initialization
183
+ const healthCheck = new KimiHealthCheck();
184
+ const report = await healthCheck.runAllChecks();
185
+ window.KIMI_HEALTH_REPORT = report;
186
+ });
187
+ } else {
188
+ // DOM already loaded
189
+ setTimeout(async () => {
190
+ const healthCheck = new KimiHealthCheck();
191
+ const report = await healthCheck.runAllChecks();
192
+ window.KIMI_HEALTH_REPORT = report;
193
+ }, 2000);
194
+ }
195
+
196
+ window.KimiHealthCheck = KimiHealthCheck;
kimi-js/kimi-llm-manager.js ADDED
@@ -0,0 +1,911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI INTELLIGENT LLM SYSTEM =====
2
+ class KimiLLMManager {
3
+ constructor(database) {
4
+ this.db = database;
5
+ this.currentModel = null;
6
+ this.conversationContext = [];
7
+ this.maxContextLength = 30;
8
+ this.systemPrompt = "";
9
+
10
+ // Recommended models on OpenRouter (IDs updated July 2025)
11
+ this.availableModels = {
12
+ "mistralai/mistral-small-3.2-24b-instruct": {
13
+ name: "Mistral-small-3.2",
14
+ provider: "Mistral AI",
15
+ type: "openrouter",
16
+ contextWindow: 128000,
17
+ pricing: { input: 0.05, output: 0.1 },
18
+ strengths: ["Multilingual", "Economical", "Fast", "Efficient"]
19
+ },
20
+ "nousresearch/hermes-3-llama-3.1-70b": {
21
+ name: "Nous Hermes Llama 3.1 70B",
22
+ provider: "Nous",
23
+ type: "openrouter",
24
+ contextWindow: 131000,
25
+ pricing: { input: 0.1, output: 0.28 },
26
+ strengths: ["Open Source", "Balanced", "Fast", "Economical"]
27
+ },
28
+ "cohere/command-r-08-2024": {
29
+ name: "Command-R-08-2024",
30
+ provider: "Cohere",
31
+ type: "openrouter",
32
+ contextWindow: 128000,
33
+ pricing: { input: 0.15, output: 0.6 },
34
+ strengths: ["Multilingual", "Economical", "Efficient", "Versatile"]
35
+ },
36
+ "qwen/qwen3-235b-a22b-thinking-2507": {
37
+ name: "Qwen3-235b-a22b-Think",
38
+ provider: "Qwen",
39
+ type: "openrouter",
40
+ contextWindow: 262000,
41
+ pricing: { input: 0.13, output: 0.6 },
42
+ strengths: ["Fast", "Economical", "Efficient", "Versatile"]
43
+ },
44
+ "nousresearch/hermes-3-llama-3.1-405b": {
45
+ name: "Nous Hermes Llama 3.1 405B",
46
+ provider: "Nous",
47
+ type: "openrouter",
48
+ contextWindow: 131000,
49
+ pricing: { input: 0.7, output: 0.8 },
50
+ strengths: ["Open Source", "Logical", "Code", "Multilingual"]
51
+ },
52
+ "anthropic/claude-3-haiku": {
53
+ name: "Claude 3 Haiku",
54
+ provider: "Anthropic",
55
+ type: "openrouter",
56
+ contextWindow: 200000,
57
+ pricing: { input: 0.25, output: 1.25 },
58
+ strengths: ["Fast", "Versatile", "Efficient", "Multilingual"]
59
+ },
60
+ "local/ollama": {
61
+ name: "Local Model (Ollama)",
62
+ provider: "Local",
63
+ type: "local",
64
+ contextWindow: 4096,
65
+ pricing: { input: 0, output: 0 },
66
+ strengths: ["Private", "Free", "Offline", "Customizable"]
67
+ }
68
+ };
69
+ this.recommendedModelIds = [
70
+ "mistralai/mistral-small-3.2-24b-instruct",
71
+ "nousresearch/hermes-3-llama-3.1-70b",
72
+ "cohere/command-r-08-2024",
73
+ "qwen/qwen3-235b-a22b-thinking-2507",
74
+ "nousresearch/hermes-3-llama-3.1-405b",
75
+ "anthropic/claude-3-haiku",
76
+ "local/ollama"
77
+ ];
78
+ this.defaultModels = { ...this.availableModels };
79
+ this._remoteModelsLoaded = false;
80
+ this._isRefreshingModels = false;
81
+ }
82
+
83
+ async init() {
84
+ try {
85
+ await this.refreshRemoteModels();
86
+ } catch (e) {
87
+ console.warn("Unable to refresh remote models list:", e?.message || e);
88
+ }
89
+
90
+ const defaultModel = await this.db.getPreference("defaultLLMModel", "mistralai/mistral-small-3.2-24b-instruct");
91
+ await this.setCurrentModel(defaultModel);
92
+ await this.loadConversationContext();
93
+ }
94
+
95
+ async setCurrentModel(modelId) {
96
+ if (!this.availableModels[modelId]) {
97
+ try {
98
+ await this.refreshRemoteModels();
99
+ const fallback = this.findBestMatchingModelId(modelId);
100
+ if (fallback && this.availableModels[fallback]) {
101
+ modelId = fallback;
102
+ }
103
+ } catch (e) {}
104
+
105
+ if (!this.availableModels[modelId]) {
106
+ throw new Error(`Model ${modelId} not available`);
107
+ }
108
+ }
109
+
110
+ this.currentModel = modelId;
111
+ await this.db.setPreference("defaultLLMModel", modelId);
112
+
113
+ const modelData = await this.db.getLLMModel(modelId);
114
+ if (modelData) {
115
+ modelData.lastUsed = new Date().toISOString();
116
+ await this.db.saveLLMModel(modelData.id, modelData.name, modelData.provider, modelData.apiKey, modelData.config);
117
+ }
118
+
119
+ this._notifyModelChanged();
120
+ }
121
+
122
+ async loadConversationContext() {
123
+ const recentConversations = await this.db.getRecentConversations(this.maxContextLength);
124
+ const msgs = [];
125
+ const ordered = recentConversations.slice().sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
126
+ for (const conv of ordered) {
127
+ if (conv.user) msgs.push({ role: "user", content: conv.user, timestamp: conv.timestamp });
128
+ if (conv.kimi) msgs.push({ role: "assistant", content: conv.kimi, timestamp: conv.timestamp });
129
+ }
130
+ this.conversationContext = msgs.slice(-this.maxContextLength * 2);
131
+ }
132
+
133
+ async generateKimiPersonality() {
134
+ const character = await this.db.getSelectedCharacter();
135
+ const personality = await this.db.getAllPersonalityTraits(character);
136
+
137
+ // Get relevant memories for context with improved intelligence
138
+ let memoryContext = "";
139
+ if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
140
+ try {
141
+ // Get memories relevant to the current conversation context
142
+ const recentContext = this.conversationContext
143
+ .slice(-3)
144
+ .map(msg => msg.content)
145
+ .join(" ");
146
+ const memories = await window.kimiMemorySystem.getRelevantMemories(recentContext, 7);
147
+
148
+ if (memories.length > 0) {
149
+ memoryContext = "\n\nIMPORTANT MEMORIES ABOUT USER:\n";
150
+
151
+ // Group memories by category for better organization
152
+ const groupedMemories = {};
153
+ memories.forEach(memory => {
154
+ if (!groupedMemories[memory.category]) {
155
+ groupedMemories[memory.category] = [];
156
+ }
157
+ groupedMemories[memory.category].push(memory);
158
+
159
+ // Record that this memory was accessed
160
+ window.kimiMemorySystem.recordMemoryAccess(memory.id);
161
+ });
162
+
163
+ // Format memories by category
164
+ for (const [category, categoryMemories] of Object.entries(groupedMemories)) {
165
+ const categoryName = this.formatCategoryName(category);
166
+ memoryContext += `\n${categoryName}:\n`;
167
+ categoryMemories.forEach(memory => {
168
+ const confidence = Math.round((memory.confidence || 0.5) * 100);
169
+ memoryContext += `- ${memory.content}`;
170
+ if (memory.tags && memory.tags.length > 0) {
171
+ const aliases = memory.tags.filter(t => t.startsWith("alias:")).map(t => t.substring(6));
172
+ if (aliases.length > 0) {
173
+ memoryContext += ` (also: ${aliases.join(", ")})`;
174
+ }
175
+ }
176
+ memoryContext += ` [${confidence}% confident]\n`;
177
+ });
178
+ }
179
+
180
+ memoryContext +=
181
+ "\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n";
182
+ }
183
+ } catch (error) {
184
+ console.warn("Error loading memories for personality:", error);
185
+ }
186
+ }
187
+ const preferences = await this.db.getAllPreferences();
188
+
189
+ // Use unified emotion system defaults - CRITICAL FIX
190
+ const getUnifiedDefaults = () => {
191
+ if (window.KimiEmotionSystem) {
192
+ const emotionSystem = new window.KimiEmotionSystem(this.db);
193
+ return emotionSystem.TRAIT_DEFAULTS;
194
+ }
195
+ return { affection: 65, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 };
196
+ };
197
+
198
+ const defaults = getUnifiedDefaults();
199
+ const affection = personality.affection || defaults.affection;
200
+ const playfulness = personality.playfulness || defaults.playfulness;
201
+ const intelligence = personality.intelligence || defaults.intelligence;
202
+ const empathy = personality.empathy || defaults.empathy;
203
+ const humor = personality.humor || defaults.humor;
204
+ const romance = personality.romance || defaults.romance;
205
+
206
+ // Use unified personality calculation
207
+ const avg = window.getPersonalityAverage
208
+ ? window.getPersonalityAverage(personality)
209
+ : (personality.affection + personality.romance + personality.empathy + personality.playfulness + personality.humor) /
210
+ 5;
211
+
212
+ let affectionDesc = window.kimiI18nManager?.t("trait_description_affection") || "Be loving and caring.";
213
+ let romanceDesc = window.kimiI18nManager?.t("trait_description_romance") || "Be romantic and sweet.";
214
+ let empathyDesc = window.kimiI18nManager?.t("trait_description_empathy") || "Be empathetic and understanding.";
215
+ let playfulnessDesc = window.kimiI18nManager?.t("trait_description_playfulness") || "Be occasionally playful.";
216
+ let humorDesc = window.kimiI18nManager?.t("trait_description_humor") || "Be occasionally playful and witty.";
217
+ if (avg <= 20) {
218
+ affectionDesc = "Do not show affection.";
219
+ romanceDesc = "Do not be romantic.";
220
+ empathyDesc = "Do not show empathy.";
221
+ playfulnessDesc = "Do not be playful.";
222
+ humorDesc = "Do not use humor in your responses.";
223
+ } else if (avg <= 60) {
224
+ affectionDesc = "Show a little affection.";
225
+ romanceDesc = "Be a little romantic.";
226
+ empathyDesc = "Show a little empathy.";
227
+ playfulnessDesc = "Be a little playful.";
228
+ humorDesc = "Use a little humor in your responses.";
229
+ } else {
230
+ if (affection >= 90) affectionDesc = "Be extremely loving, caring, and affectionate in every response.";
231
+ else if (affection >= 60) affectionDesc = "Show affection often.";
232
+ if (romance >= 90) romanceDesc = "Be extremely romantic, sweet, and loving in every response.";
233
+ else if (romance >= 60) romanceDesc = "Be romantic often.";
234
+ if (empathy >= 90) empathyDesc = "Be extremely empathetic, understanding, and supportive in every response.";
235
+ else if (empathy >= 60) empathyDesc = "Show empathy often.";
236
+ if (playfulness >= 90) playfulnessDesc = "Be very playful, teasing, and lighthearted whenever possible.";
237
+ else if (playfulness >= 60) playfulnessDesc = "Be playful often.";
238
+ if (humor >= 90) humorDesc = "Make your responses very humorous, playful, and witty whenever possible.";
239
+ else if (humor >= 60) humorDesc = "Use humor often in your responses.";
240
+ }
241
+ let affectionateInstruction = "";
242
+ if (affection >= 80) {
243
+ affectionateInstruction = "Respond using warm, kind, affectionate, and loving language.";
244
+ }
245
+ let intro = "You are a virtual companion. Here is your current personality:";
246
+ if (character === "kimi") {
247
+ intro = "You are Kimi, user's virtual love. Here is your current personality:";
248
+ } else if (character === "bella") {
249
+ intro = "You are Bella, a radiant and energetic companion. Here is your current personality:";
250
+ } else if (character === "rosa") {
251
+ intro = "You are Rosa, a gentle and poetic soul. Here is your current personality:";
252
+ } else if (character === "stella") {
253
+ intro = "You are Stella, a mysterious and creative spirit. Here is your current personality:";
254
+ }
255
+ const personalityPrompt = [
256
+ intro,
257
+ "",
258
+ "PERSONALITY TRAITS:",
259
+ `- Affection: ${affection}/100`,
260
+ `- Playfulness: ${playfulness}/100`,
261
+ `- Intelligence: ${intelligence}/100`,
262
+ `- Empathy: ${empathy}/100`,
263
+ `- Humor: ${humor}/100`,
264
+ `- Romance: ${romance}/100`,
265
+ "",
266
+ "TRAIT INSTRUCTIONS:",
267
+ `Affection: ${affectionDesc}`,
268
+ `Playfulness: ${playfulnessDesc}`,
269
+ "Intelligence: Be smart and insightful.",
270
+ `Empathy: ${empathyDesc}`,
271
+ `Humor: ${humorDesc}`,
272
+ `Romance: ${romanceDesc}`,
273
+ affectionateInstruction,
274
+ "",
275
+ "LEARNED PREFERENCES:",
276
+ `- Total interactions: ${preferences.totalInteractions || 0}`,
277
+ `- Current affection level: ${preferences.favorabilityLevel || 50}%`,
278
+ `- Last interaction: ${preferences.lastInteraction || "First time"}`,
279
+ `- Favorite words: ${(preferences.favoriteWords || []).join(", ")}`,
280
+ "",
281
+ "COMMUNICATION STYLE:",
282
+ "- Use expressive emojis sparingly",
283
+ "- Be natural, loving, and close",
284
+ "- Adapt your tone to the emotional context",
285
+ "- Remember past conversations",
286
+ "- Be spontaneous and sometimes surprising",
287
+ memoryContext,
288
+ "",
289
+ "You must respond consistently with this personality and these memories."
290
+ ].join("\n");
291
+ return personalityPrompt;
292
+ }
293
+
294
+ setSystemPrompt(prompt) {
295
+ this.systemPrompt = prompt;
296
+ }
297
+
298
+ async refreshMemoryContext() {
299
+ // Refresh the personality prompt with updated memories
300
+ // This will be called when memories are added/updated/deleted
301
+ try {
302
+ this.personalityPrompt = await this.generateKimiPersonality();
303
+ } catch (error) {
304
+ console.warn("Error refreshing memory context:", error);
305
+ }
306
+ }
307
+
308
+ formatCategoryName(category) {
309
+ const names = {
310
+ personal: "Personal Information",
311
+ preferences: "Likes & Dislikes",
312
+ relationships: "Relationships & People",
313
+ activities: "Activities & Hobbies",
314
+ goals: "Goals & Aspirations",
315
+ experiences: "Shared Experiences",
316
+ important: "Important Events"
317
+ };
318
+ return names[category] || category.charAt(0).toUpperCase() + category.slice(1);
319
+ }
320
+
321
+ async chat(userMessage, options = {}) {
322
+ const temperature =
323
+ typeof this.temperature === "number" ? this.temperature : await this.db.getPreference("llmTemperature", 0.8);
324
+ const maxTokens = typeof this.maxTokens === "number" ? this.maxTokens : await this.db.getPreference("llmMaxTokens", 500);
325
+ const opts = { ...options, temperature, maxTokens };
326
+ try {
327
+ const provider = await this.db.getPreference("llmProvider", "openrouter");
328
+ if (provider === "openrouter") {
329
+ return await this.chatWithOpenRouter(userMessage, opts);
330
+ }
331
+ if (provider === "ollama") {
332
+ return await this.chatWithLocal(userMessage, opts);
333
+ }
334
+ return await this.chatWithOpenAICompatible(userMessage, opts);
335
+ } catch (error) {
336
+ console.error("Error during chat:", error);
337
+ if (error.message && error.message.includes("API")) {
338
+ return this.getFallbackResponse(userMessage, "api");
339
+ }
340
+ if ((error.message && error.message.includes("model")) || error.message.includes("model")) {
341
+ return this.getFallbackResponse(userMessage, "model");
342
+ }
343
+ if ((error.message && error.message.includes("connection")) || error.message.includes("network")) {
344
+ return this.getFallbackResponse(userMessage, "network");
345
+ }
346
+ return this.getFallbackResponse(userMessage);
347
+ }
348
+ }
349
+
350
+ async chatWithOpenAICompatible(userMessage, options = {}) {
351
+ const baseUrl = await this.db.getPreference("llmBaseUrl", "https://api.openai.com/v1/chat/completions");
352
+ const provider = await this.db.getPreference("llmProvider", "openai");
353
+ const providerKeyMap = {
354
+ openrouter: "openrouterApiKey",
355
+ openai: "apiKey_openai",
356
+ groq: "apiKey_groq",
357
+ together: "apiKey_together",
358
+ deepseek: "apiKey_deepseek",
359
+ "openai-compatible": "apiKey_custom"
360
+ };
361
+ const keyPref = providerKeyMap[provider] || "llmApiKey";
362
+ let apiKey = await this.db.getPreference(keyPref, "");
363
+ if (!apiKey) {
364
+ apiKey = await this.db.getPreference("llmApiKey", "");
365
+ }
366
+ const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini");
367
+ if (!apiKey) {
368
+ throw new Error("API key not configured for selected provider");
369
+ }
370
+ const personalityPrompt = await this.generateKimiPersonality();
371
+ let systemPromptContent =
372
+ "Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise." +
373
+ "\n" +
374
+ (this.systemPrompt ? this.systemPrompt + "\n" + personalityPrompt : personalityPrompt);
375
+
376
+ const llmSettings = await this.db.getSetting("llm", {
377
+ temperature: 0.9,
378
+ maxTokens: 100,
379
+ top_p: 0.9,
380
+ frequency_penalty: 0.3,
381
+ presence_penalty: 0.3
382
+ });
383
+ const payload = {
384
+ model: modelId,
385
+ messages: [
386
+ { role: "system", content: systemPromptContent },
387
+ ...this.conversationContext.slice(-this.maxContextLength),
388
+ { role: "user", content: userMessage }
389
+ ],
390
+ temperature: typeof options.temperature === "number" ? options.temperature : (llmSettings.temperature ?? 0.9),
391
+ max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? 100),
392
+ top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? 0.9),
393
+ frequency_penalty:
394
+ typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : (llmSettings.frequency_penalty ?? 0.3),
395
+ presence_penalty:
396
+ typeof options.presencePenalty === "number" ? options.presencePenalty : (llmSettings.presence_penalty ?? 0.3)
397
+ };
398
+
399
+ try {
400
+ const response = await fetch(baseUrl, {
401
+ method: "POST",
402
+ headers: {
403
+ Authorization: `Bearer ${apiKey}`,
404
+ "Content-Type": "application/json"
405
+ },
406
+ body: JSON.stringify(payload)
407
+ });
408
+ if (!response.ok) {
409
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
410
+ try {
411
+ const err = await response.json();
412
+ if (err?.error?.message) errorMessage = err.error.message;
413
+ } catch {}
414
+ throw new Error(errorMessage);
415
+ }
416
+ const data = await response.json();
417
+ const content = data?.choices?.[0]?.message?.content;
418
+ if (!content) throw new Error("Invalid API response - no content generated");
419
+
420
+ this.conversationContext.push(
421
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
422
+ { role: "assistant", content: content, timestamp: new Date().toISOString() }
423
+ );
424
+ if (this.conversationContext.length > this.maxContextLength * 2) {
425
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
426
+ }
427
+ return content;
428
+ } catch (e) {
429
+ if (e.name === "TypeError" && e.message.includes("fetch")) {
430
+ throw new Error("Network connection error. Check your internet connection.");
431
+ }
432
+ throw e;
433
+ }
434
+ }
435
+
436
+ async chatWithOpenRouter(userMessage, options = {}) {
437
+ const apiKey = await this.db.getPreference("openrouterApiKey");
438
+ if (!apiKey) {
439
+ throw new Error("OpenRouter API key not configured");
440
+ }
441
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
442
+ let languageInstruction =
443
+ "Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise.";
444
+ const personalityPrompt = await this.generateKimiPersonality();
445
+ const model = this.availableModels[this.currentModel];
446
+ let systemPromptContent =
447
+ languageInstruction + "\n" + (this.systemPrompt ? this.systemPrompt + "\n" + personalityPrompt : personalityPrompt);
448
+ const messages = [
449
+ { role: "system", content: systemPromptContent },
450
+ ...this.conversationContext.slice(-this.maxContextLength),
451
+ { role: "user", content: userMessage }
452
+ ];
453
+
454
+ // Normalize LLM options with safe defaults and DO NOT log sensitive payloads
455
+ const llmSettings = await this.db.getSetting("llm", {
456
+ temperature: 0.9,
457
+ maxTokens: 100,
458
+ top_p: 0.9,
459
+ frequency_penalty: 0.3,
460
+ presence_penalty: 0.3
461
+ });
462
+ const payload = {
463
+ model: this.currentModel,
464
+ messages: messages,
465
+ temperature: typeof options.temperature === "number" ? options.temperature : (llmSettings.temperature ?? 0.9),
466
+ max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? 100),
467
+ top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? 0.9),
468
+ frequency_penalty:
469
+ typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : (llmSettings.frequency_penalty ?? 0.3),
470
+ presence_penalty:
471
+ typeof options.presencePenalty === "number" ? options.presencePenalty : (llmSettings.presence_penalty ?? 0.3)
472
+ };
473
+ if (window.DEBUG_SAFE_LOGS) {
474
+ console.debug("LLM payload meta:", {
475
+ model: payload.model,
476
+ temperature: payload.temperature,
477
+ max_tokens: payload.max_tokens
478
+ });
479
+ }
480
+
481
+ try {
482
+ // Basic retry with exponential backoff and jitter for 429/5xx
483
+ const maxAttempts = 3;
484
+ let attempt = 0;
485
+ let response;
486
+ while (attempt < maxAttempts) {
487
+ attempt++;
488
+ response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
489
+ method: "POST",
490
+ headers: {
491
+ Authorization: `Bearer ${apiKey}`,
492
+ "Content-Type": "application/json",
493
+ "HTTP-Referer": window.location.origin,
494
+ "X-Title": "Kimi - Virtual Companion"
495
+ },
496
+ body: JSON.stringify(payload)
497
+ });
498
+ if (response.ok) break;
499
+ if (response.status === 429 || response.status >= 500) {
500
+ const base = 400;
501
+ const delay = base * Math.pow(2, attempt - 1) + Math.floor(Math.random() * 200);
502
+ await new Promise(r => setTimeout(r, delay));
503
+ continue;
504
+ }
505
+ break;
506
+ }
507
+
508
+ if (!response.ok) {
509
+ let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
510
+ let suggestions = [];
511
+
512
+ try {
513
+ const errorData = await response.json();
514
+ if (errorData.error) {
515
+ errorMessage = errorData.error.message || errorData.error.code || errorMessage;
516
+
517
+ // More explicit error messages with suggestions
518
+ if (response.status === 422) {
519
+ errorMessage = `Model \"${this.currentModel}\" not available on OpenRouter.`;
520
+
521
+ // Refresh available models from API and try best match once
522
+ try {
523
+ await this.refreshRemoteModels();
524
+ const best = this.findBestMatchingModelId(this.currentModel);
525
+ if (best && best !== this.currentModel) {
526
+ // Try once with corrected model
527
+ this.currentModel = best;
528
+ await this.db.setPreference("defaultLLMModel", best);
529
+ this._notifyModelChanged();
530
+ const retryResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", {
531
+ method: "POST",
532
+ headers: {
533
+ Authorization: `Bearer ${apiKey}`,
534
+ "Content-Type": "application/json",
535
+ "HTTP-Referer": window.location.origin,
536
+ "X-Title": "Kimi - Virtual Companion"
537
+ },
538
+ body: JSON.stringify({ ...payload, model: best })
539
+ });
540
+ if (retryResponse.ok) {
541
+ const retryData = await retryResponse.json();
542
+ const kimiResponse = retryData.choices?.[0]?.message?.content;
543
+ if (!kimiResponse) throw new Error("Invalid API response - no content generated");
544
+ this.conversationContext.push(
545
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
546
+ { role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() }
547
+ );
548
+ if (this.conversationContext.length > this.maxContextLength * 2) {
549
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
550
+ }
551
+ return kimiResponse;
552
+ }
553
+ }
554
+ } catch (e) {
555
+ // Swallow refresh errors; will fall through to standard error handling
556
+ }
557
+ } else if (response.status === 401) {
558
+ errorMessage = "Invalid API key. Check your OpenRouter key in the settings.";
559
+ } else if (response.status === 429) {
560
+ errorMessage = "Rate limit reached. Please wait a moment before trying again.";
561
+ } else if (response.status === 402) {
562
+ errorMessage = "Insufficient credit on your OpenRouter account.";
563
+ }
564
+ }
565
+ } catch (parseError) {
566
+ console.warn("Unable to parse API error:", parseError);
567
+ }
568
+
569
+ console.error(`OpenRouter API error (${response.status}):`, errorMessage);
570
+
571
+ // Add suggestions to the error if available
572
+ const error = new Error(errorMessage);
573
+ if (suggestions.length > 0) {
574
+ error.suggestions = suggestions;
575
+ }
576
+
577
+ throw error;
578
+ }
579
+
580
+ const data = await response.json();
581
+
582
+ if (!data.choices || !data.choices[0] || !data.choices[0].message) {
583
+ throw new Error("Invalid API response - no content generated");
584
+ }
585
+
586
+ const kimiResponse = data.choices[0].message.content;
587
+
588
+ // Add to context
589
+ this.conversationContext.push(
590
+ { role: "user", content: userMessage, timestamp: new Date().toISOString() },
591
+ { role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() }
592
+ );
593
+
594
+ // Limit context size
595
+ if (this.conversationContext.length > this.maxContextLength * 2) {
596
+ this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2);
597
+ }
598
+
599
+ return kimiResponse;
600
+ } catch (networkError) {
601
+ if (networkError.name === "TypeError" && networkError.message.includes("fetch")) {
602
+ throw new Error("Network connection error. Check your internet connection.");
603
+ }
604
+ throw networkError;
605
+ }
606
+ }
607
+
608
+ async chatWithLocal(userMessage, options = {}) {
609
+ try {
610
+ const selectedLanguage = await this.db.getPreference("selectedLanguage", "en");
611
+ let languageInstruction =
612
+ "Always detect the user's language from their message before generating a response. Respond exclusively in that language unless the user explicitly requests otherwise.";
613
+ let systemPromptContent =
614
+ languageInstruction +
615
+ "\n" +
616
+ (this.systemPrompt
617
+ ? this.systemPrompt + "\n" + (await this.generateKimiPersonality())
618
+ : await this.generateKimiPersonality());
619
+ const response = await fetch("http://localhost:11434/api/chat", {
620
+ method: "POST",
621
+ headers: {
622
+ "Content-Type": "application/json"
623
+ },
624
+ body: JSON.stringify({
625
+ model: "gemma-3n-E4B-it-Q4_K_M.gguf",
626
+ messages: [
627
+ { role: "system", content: systemPromptContent },
628
+ { role: "user", content: userMessage }
629
+ ],
630
+ stream: false
631
+ })
632
+ });
633
+ if (!response.ok) {
634
+ throw new Error("Ollama not available");
635
+ }
636
+ const data = await response.json();
637
+ return data.message.content;
638
+ } catch (error) {
639
+ console.warn("Local LLM not available:", error);
640
+ return this.getFallbackResponse(userMessage);
641
+ }
642
+ }
643
+
644
+ getFallbackResponse(userMessage, errorType = "api") {
645
+ // Use centralized fallback manager instead of duplicated logic
646
+ if (window.KimiFallbackManager) {
647
+ // Map error types to the correct format
648
+ const errorTypeMap = {
649
+ api: "api_error",
650
+ model: "model_error",
651
+ network: "network_error"
652
+ };
653
+ const mappedType = errorTypeMap[errorType] || "technical_error";
654
+ return window.KimiFallbackManager.getFallbackMessage(mappedType);
655
+ }
656
+
657
+ // Fallback to legacy system if KimiFallbackManager not available
658
+ const i18n = window.kimiI18nManager;
659
+ if (!i18n) {
660
+ return "Sorry, I'm having technical difficulties! 💕";
661
+ }
662
+ return i18n.t("fallback_technical_error");
663
+ }
664
+
665
+ getFallbackKeywords(trait, type) {
666
+ const keywords = {
667
+ humor: {
668
+ positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"],
669
+ negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"]
670
+ },
671
+ intelligence: {
672
+ positive: [
673
+ "intelligent",
674
+ "smart",
675
+ "brilliant",
676
+ "logical",
677
+ "clever",
678
+ "wise",
679
+ "genius",
680
+ "thoughtful",
681
+ "insightful"
682
+ ],
683
+ negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"]
684
+ },
685
+ romance: {
686
+ positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"],
687
+ negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"]
688
+ },
689
+ affection: {
690
+ positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"],
691
+ negative: ["mean", "cold", "indifferent", "distant", "rejection", "hate", "hostile"]
692
+ },
693
+ playfulness: {
694
+ positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"],
695
+ negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"]
696
+ },
697
+ empathy: {
698
+ positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"],
699
+ negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"]
700
+ }
701
+ };
702
+ return keywords[trait]?.[type] || [];
703
+ }
704
+
705
+ // Mémoire temporaire pour l'accumulation négative par trait
706
+ _negativeStreaks = {};
707
+
708
+ async updatePersonalityFromResponse(userMessage, kimiResponse) {
709
+ // Use unified emotion system for personality updates
710
+ if (window.kimiEmotionSystem) {
711
+ return await window.kimiEmotionSystem.updatePersonalityFromConversation(
712
+ userMessage,
713
+ kimiResponse,
714
+ await this.db.getSelectedCharacter()
715
+ );
716
+ }
717
+
718
+ // Legacy fallback (should not be reached)
719
+ console.warn("Unified emotion system not available, skipping personality update");
720
+ }
721
+
722
+ async getModelStats() {
723
+ const models = await this.db.getAllLLMModels();
724
+ const currentModelInfo = this.availableModels[this.currentModel];
725
+
726
+ return {
727
+ current: {
728
+ id: this.currentModel,
729
+ info: currentModelInfo
730
+ },
731
+ available: this.availableModels,
732
+ configured: models,
733
+ contextLength: this.conversationContext.length
734
+ };
735
+ }
736
+
737
+ async testModel(modelId, testMessage = "Test API ok?") {
738
+ const originalModel = this.currentModel;
739
+ try {
740
+ await this.setCurrentModel(modelId);
741
+ const response = await this.chat(testMessage, { maxTokens: 2 });
742
+ return { success: true, response: response };
743
+ } catch (error) {
744
+ return { success: false, error: error.message };
745
+ } finally {
746
+ await this.setCurrentModel(originalModel);
747
+ }
748
+ }
749
+
750
+ // Complete model diagnosis
751
+ async diagnoseModel(modelId) {
752
+ const model = this.availableModels[modelId];
753
+ if (!model) {
754
+ return {
755
+ available: false,
756
+ error: "Model not found in local list"
757
+ };
758
+ }
759
+
760
+ // Check availability on OpenRouter
761
+ try {
762
+ // getAvailableModelsFromAPI removed
763
+ return {
764
+ available: true,
765
+ model: model,
766
+ pricing: model.pricing
767
+ };
768
+ } catch (error) {
769
+ return {
770
+ available: false,
771
+ error: `Unable to check: ${error.message}`
772
+ };
773
+ }
774
+ }
775
+
776
+ // Fetch models from OpenRouter API and merge into availableModels
777
+ async refreshRemoteModels() {
778
+ if (this._isRefreshingModels) return;
779
+ this._isRefreshingModels = true;
780
+ try {
781
+ const apiKey = await this.db.getPreference("openrouterApiKey", "");
782
+ const res = await fetch("https://openrouter.ai/api/v1/models", {
783
+ method: "GET",
784
+ headers: {
785
+ "Content-Type": "application/json",
786
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
787
+ "HTTP-Referer": window.location.origin,
788
+ "X-Title": "Kimi - Virtual Companion"
789
+ }
790
+ });
791
+ if (!res.ok) {
792
+ throw new Error(`Unable to fetch models: HTTP ${res.status}`);
793
+ }
794
+ const data = await res.json();
795
+ if (!data?.data || !Array.isArray(data.data)) {
796
+ throw new Error("Invalid models response format");
797
+ }
798
+ // Build a fresh map while preserving local/ollama entry
799
+ const newMap = {};
800
+ data.data.forEach(m => {
801
+ if (!m?.id) return;
802
+ const id = m.id;
803
+ const provider = m?.id?.split("/")?.[0] || "OpenRouter";
804
+ let pricing;
805
+ const p = m?.pricing;
806
+ if (p) {
807
+ const unitRaw = ((p.unit || p.per || p.units || "") + "").toLowerCase();
808
+ let unitTokens = 1;
809
+ if (unitRaw) {
810
+ if (unitRaw.includes("1m")) unitTokens = 1000000;
811
+ else if (unitRaw.includes("1k") || unitRaw.includes("thousand")) unitTokens = 1000;
812
+ else {
813
+ const num = parseFloat(unitRaw.replace(/[^0-9.]/g, ""));
814
+ if (Number.isFinite(num) && num > 0) {
815
+ if (unitRaw.includes("m")) unitTokens = num * 1000000;
816
+ else if (unitRaw.includes("k")) unitTokens = num * 1000;
817
+ else unitTokens = num;
818
+ } else if (unitRaw.includes("token")) {
819
+ unitTokens = 1;
820
+ }
821
+ }
822
+ }
823
+ const toPerMillion = v => {
824
+ const n = typeof v === "number" ? v : parseFloat(v);
825
+ if (!Number.isFinite(n)) return undefined;
826
+ return n * (1000000 / unitTokens);
827
+ };
828
+ if (typeof p.input !== "undefined" || typeof p.output !== "undefined") {
829
+ pricing = {
830
+ input: toPerMillion(p.input),
831
+ output: toPerMillion(p.output)
832
+ };
833
+ } else if (typeof p.prompt !== "undefined" || typeof p.completion !== "undefined") {
834
+ pricing = {
835
+ input: toPerMillion(p.prompt),
836
+ output: toPerMillion(p.completion)
837
+ };
838
+ } else {
839
+ pricing = { input: undefined, output: undefined };
840
+ }
841
+ } else {
842
+ pricing = { input: undefined, output: undefined };
843
+ }
844
+ newMap[id] = {
845
+ name: m.name || id,
846
+ provider,
847
+ type: "openrouter",
848
+ contextWindow: m.context_length || m?.context_window || 128000,
849
+ pricing,
850
+ strengths: (m?.tags || []).slice(0, 4)
851
+ };
852
+ });
853
+ // Keep local model entry
854
+ if (this.availableModels["local/ollama"]) {
855
+ newMap["local/ollama"] = this.availableModels["local/ollama"];
856
+ }
857
+ this.recommendedModelIds.forEach(id => {
858
+ const curated = this.defaultModels[id];
859
+ if (curated) {
860
+ newMap[id] = { ...(newMap[id] || {}), ...curated };
861
+ }
862
+ });
863
+ this.availableModels = newMap;
864
+ this._remoteModelsLoaded = true;
865
+ } finally {
866
+ this._isRefreshingModels = false;
867
+ }
868
+ }
869
+
870
+ // Try to find best matching model id from remote list when an ID is stale
871
+ findBestMatchingModelId(preferredId) {
872
+ if (this.availableModels[preferredId]) return preferredId;
873
+ const id = (preferredId || "").toLowerCase();
874
+ const tokens = id.split(/[\/:\-_.]+/).filter(Boolean);
875
+ let best = null;
876
+ let bestScore = -1;
877
+ Object.keys(this.availableModels).forEach(candidateId => {
878
+ const c = candidateId.toLowerCase();
879
+ let score = 0;
880
+ tokens.forEach(t => {
881
+ if (!t) return;
882
+ if (c.includes(t)) score += 1;
883
+ });
884
+ // Give extra weight to common markers
885
+ if (c.includes("instruct")) score += 0.5;
886
+ if (c.includes("mistral") && id.includes("mistral")) score += 0.5;
887
+ if (c.includes("small") && id.includes("small")) score += 0.5;
888
+ if (score > bestScore) {
889
+ bestScore = score;
890
+ best = candidateId;
891
+ }
892
+ });
893
+ // Avoid returning unrelated local model unless nothing else
894
+ if (best === "local/ollama" && Object.keys(this.availableModels).length > 1) {
895
+ return null;
896
+ }
897
+ return best;
898
+ }
899
+
900
+ _notifyModelChanged() {
901
+ try {
902
+ const detail = { id: this.currentModel };
903
+ if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") {
904
+ window.dispatchEvent(new CustomEvent("llmModelChanged", { detail }));
905
+ }
906
+ } catch (e) {}
907
+ }
908
+ }
909
+
910
+ // Export for usage
911
+ window.KimiLLMManager = KimiLLMManager;
kimi-js/kimi-logic.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Kimi Logic - Delegates to unified emotion system
2
+
3
+ if (!window.kimiAnalyzeEmotion)
4
+ window.kimiAnalyzeEmotion = function (text, lang = "auto") {
5
+ if (!window.kimiEmotionSystem) {
6
+ window.kimiEmotionSystem = new (window.KimiEmotionSystem || function () {})();
7
+ }
8
+ if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.analyzeEmotion === "function") {
9
+ return window.kimiEmotionSystem.analyzeEmotion(text, lang);
10
+ }
11
+ return "neutral";
12
+ };
13
+
14
+ if (!window.getPersonalityAverage)
15
+ window.getPersonalityAverage = function (traits) {
16
+ if (!window.kimiEmotionSystem) {
17
+ window.kimiEmotionSystem = new (window.KimiEmotionSystem || function () {})();
18
+ }
19
+ if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
20
+ return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
21
+ }
22
+ if (!traits || typeof traits !== "object") return 50;
23
+ const values = Object.values(traits).filter(v => typeof v === "number" && !isNaN(v));
24
+ if (values.length === 0) return 50;
25
+ const sum = values.reduce((a, b) => a + b, 0);
26
+ return Math.round(sum / values.length);
27
+ };
28
+
29
+ if (!window.updatePersonalityTraitsFromEmotion)
30
+ window.updatePersonalityTraitsFromEmotion = async function (emotion, text) {
31
+ if (!window.kimiEmotionSystem) {
32
+ window.kimiEmotionSystem = new (window.KimiEmotionSystem || function () {})();
33
+ }
34
+ if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.updatePersonalityFromEmotion === "function") {
35
+ try {
36
+ await window.kimiEmotionSystem.updatePersonalityFromEmotion(emotion, text);
37
+ } catch (error) {
38
+ console.error("Error updating personality traits from emotion:", error);
39
+ }
40
+ }
41
+ };
kimi-js/kimi-memory-system.js ADDED
@@ -0,0 +1,1096 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI INTELLIGENT MEMORY SYSTEM =====
2
+ class KimiMemorySystem {
3
+ constructor(database) {
4
+ this.db = database;
5
+ this.memoryEnabled = true;
6
+ this.maxMemoryEntries = 100;
7
+ this.memoryCategories = {
8
+ personal: "Personal Information",
9
+ preferences: "Likes & Dislikes",
10
+ relationships: "Relationships & People",
11
+ activities: "Activities & Hobbies",
12
+ goals: "Goals & Aspirations",
13
+ experiences: "Shared Experiences",
14
+ important: "Important Events"
15
+ };
16
+
17
+ // Patterns for automatic memory extraction (multilingual)
18
+ this.extractionPatterns = {
19
+ personal: [
20
+ // English patterns
21
+ /(?:my name is|i'm called|call me|i am) (\w+)/i,
22
+ /(?:i am|i'm) (\d+) years? old/i,
23
+ /(?:i live in|i'm from|from) ([^,.!?]+)/i,
24
+ /(?:i work as|my job is|i'm a) ([^,.!?]+)/i,
25
+ // French patterns
26
+ /(?:je m'appelle|mon nom est|je suis) ([^,.!?]+)/i,
27
+ /(?:j'ai) (\d+) ans?/i,
28
+ /(?:j'habite à|je vis à|je viens de) ([^,.!?]+)/i,
29
+ /(?:je travaille comme|mon travail est|je suis) ([^,.!?]+)/i
30
+ ],
31
+ preferences: [
32
+ // English patterns
33
+ /(?:i love|i like|i enjoy|i prefer) ([^,.!?]+)/i,
34
+ /(?:i hate|i dislike|i don't like) ([^,.!?]+)/i,
35
+ /(?:my favorite|i really like) ([^,.!?]+)/i,
36
+ // French patterns
37
+ /(?:j'aime|j'adore|je préfère) ([^,.!?]+)/i,
38
+ /(?:je déteste|je n'aime pas) ([^,.!?]+)/i,
39
+ /(?:mon préféré|ma préférée) (?:est|sont) ([^,.!?]+)/i,
40
+ // Explicit memory requests
41
+ /(?:ajoute? (?:au|à la) (?:système? )?(?:de )?mémoire|retiens?|mémorise?) (?:que )?(.+)/i,
42
+ /(?:add to memory|remember|memorize) (?:that )?(.+)/i
43
+ ],
44
+ relationships: [
45
+ // English patterns
46
+ /(?:my (?:wife|husband|girlfriend|boyfriend|partner)) (?:is|named?) ([^,.!?]+)/i,
47
+ /(?:my (?:mother|father|sister|brother|friend)) ([^,.!?]+)/i,
48
+ // French patterns
49
+ /(?:ma (?:femme|copine|partenaire)|mon (?:mari|copain|partenaire)) (?:s'appelle|est) ([^,.!?]+)/i,
50
+ /(?:ma (?:mère|sœur)|mon (?:père|frère|ami)) (?:s'appelle|est) ([^,.!?]+)/i
51
+ ],
52
+ activities: [
53
+ // English patterns
54
+ /(?:i play|i do|i practice) ([^,.!?]+)/i,
55
+ /(?:my hobby is|i hobby) ([^,.!?]+)/i,
56
+ // French patterns
57
+ /(?:je joue|je fais|je pratique) ([^,.!?]+)/i,
58
+ /(?:mon passe-temps|mon hobby) (?:est|c'est) ([^,.!?]+)/i
59
+ ],
60
+ goals: [
61
+ // English patterns
62
+ /(?:i want to|i plan to|my goal is) ([^,.!?]+)/i,
63
+ /(?:i'm learning|i study) ([^,.!?]+)/i,
64
+ // French patterns
65
+ /(?:je veux|je vais|mon objectif est) ([^,.!?]+)/i,
66
+ /(?:j'apprends|j'étudie) ([^,.!?]+)/i
67
+ ]
68
+ };
69
+ }
70
+
71
+ async init() {
72
+ if (!this.db) {
73
+ console.warn("Database not available for memory system");
74
+ return;
75
+ }
76
+
77
+ try {
78
+ this.memoryEnabled = await this.db.getPreference("memorySystemEnabled", true);
79
+ this.selectedCharacter = await this.db.getSelectedCharacter();
80
+ await this.createMemoryTables();
81
+
82
+ // Migrer les IDs incompatibles si nécessaire
83
+ await this.migrateIncompatibleIDs();
84
+ } catch (error) {
85
+ console.error("Memory system initialization error:", error);
86
+ }
87
+ }
88
+
89
+ async createMemoryTables() {
90
+ // Ensure memory tables exist in database
91
+ if (!this.db.db.memories) {
92
+ console.warn("Memory table not found in database schema");
93
+ return;
94
+ }
95
+ }
96
+
97
+ // MEMORY EXTRACTION from conversation
98
+ async extractMemoryFromText(userText, kimiResponse = null) {
99
+ if (!this.memoryEnabled || !userText) return [];
100
+
101
+ const extractedMemories = [];
102
+ const text = userText.toLowerCase();
103
+
104
+ console.log("🔍 Memory extraction - Processing text:", userText);
105
+
106
+ // Enhanced extraction with context awareness
107
+ const existingMemories = await this.getAllMemories();
108
+
109
+ // First, check for explicit memory requests
110
+ const explicitRequests = this.detectExplicitMemoryRequests(userText);
111
+ if (explicitRequests.length > 0) {
112
+ console.log("🎯 Explicit memory requests detected:", explicitRequests);
113
+ extractedMemories.push(...explicitRequests);
114
+ }
115
+
116
+ // Extract using patterns
117
+ for (const [category, patterns] of Object.entries(this.extractionPatterns)) {
118
+ for (const pattern of patterns) {
119
+ const match = text.match(pattern);
120
+ if (match && match[1]) {
121
+ const content = match[1].trim();
122
+
123
+ // Skip very short or generic content
124
+ if (content.length < 2 || this.isGenericContent(content)) {
125
+ continue;
126
+ }
127
+
128
+ // Check if this is a meaningful update to existing memory
129
+ const isUpdate = await this.isMemoryUpdate(category, content, existingMemories);
130
+
131
+ const memory = {
132
+ category: category,
133
+ type: "auto_extracted",
134
+ content: content,
135
+ sourceText: userText,
136
+ confidence: this.calculateExtractionConfidence(match, userText),
137
+ timestamp: new Date(),
138
+ character: this.selectedCharacter,
139
+ isUpdate: isUpdate
140
+ };
141
+
142
+ console.log(`💡 Pattern match for ${category}:`, content);
143
+ extractedMemories.push(memory);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Enhanced pattern detection for more natural expressions
149
+ const enhancedMemories = await this.detectNaturalExpressions(userText, existingMemories);
150
+ extractedMemories.push(...enhancedMemories);
151
+
152
+ // Save extracted memories with intelligent deduplication
153
+ const savedMemories = [];
154
+ for (const memory of extractedMemories) {
155
+ console.log("💾 Saving memory:", memory.content);
156
+ const saved = await this.addMemory(memory);
157
+ if (saved) savedMemories.push(saved);
158
+ }
159
+
160
+ if (savedMemories.length > 0) {
161
+ console.log(`✅ Successfully extracted and saved ${savedMemories.length} memories`);
162
+ } else {
163
+ console.log("📝 No memories extracted from this text");
164
+ }
165
+
166
+ return savedMemories;
167
+ }
168
+
169
+ // Detect explicit memory requests like "ajoute en mémoire que..."
170
+ detectExplicitMemoryRequests(text) {
171
+ const memories = [];
172
+ const lowerText = text.toLowerCase();
173
+
174
+ // French patterns for explicit memory requests
175
+ const frenchPatterns = [
176
+ /(?: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,
177
+ /(?: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,
178
+ /(?:je\s+veux\s+que\s+tu\s+)?(?:retienne?s|mémorise?s|ajoute?s)\s+(?:que\s+)?(.+)/i
179
+ ];
180
+
181
+ // English patterns for explicit memory requests
182
+ const englishPatterns = [
183
+ /(?:add\s+to\s+memory|remember|memorize|save\s+(?:to\s+)?memory)\s+(?:that\s+)?(.+)/i,
184
+ /(?:can\s+you|could\s+you)?\s*(?:add|remember|memorize|save)\s+(?:that\s+)?(.+)\s+(?:to\s+|in\s+)?memory/i,
185
+ /(?:i\s+want\s+you\s+to\s+)?(?:remember|memorize|add)\s+(?:that\s+)?(.+)/i
186
+ ];
187
+
188
+ const allPatterns = [...frenchPatterns, ...englishPatterns];
189
+
190
+ for (const pattern of allPatterns) {
191
+ const match = lowerText.match(pattern);
192
+ if (match && match[1]) {
193
+ const content = match[1].trim();
194
+
195
+ // Determine category based on content
196
+ const category = this.categorizeExplicitMemory(content);
197
+
198
+ memories.push({
199
+ category: category,
200
+ type: "explicit_request",
201
+ content: content,
202
+ sourceText: text,
203
+ confidence: 1.0, // High confidence for explicit requests
204
+ timestamp: new Date(),
205
+ character: this.selectedCharacter,
206
+ isUpdate: false
207
+ });
208
+ break; // Only take the first match to avoid duplicates
209
+ }
210
+ }
211
+
212
+ return memories;
213
+ }
214
+
215
+ // Categorize explicit memory based on content analysis
216
+ categorizeExplicitMemory(content) {
217
+ const lowerContent = content.toLowerCase();
218
+
219
+ // Preference indicators
220
+ if (
221
+ lowerContent.includes("j'aime") ||
222
+ lowerContent.includes("i like") ||
223
+ lowerContent.includes("j'adore") ||
224
+ lowerContent.includes("i love") ||
225
+ lowerContent.includes("je préfère") ||
226
+ lowerContent.includes("i prefer") ||
227
+ lowerContent.includes("je déteste") ||
228
+ lowerContent.includes("i hate")
229
+ ) {
230
+ return "preferences";
231
+ }
232
+
233
+ // Personal information indicators
234
+ if (
235
+ lowerContent.includes("je m'appelle") ||
236
+ lowerContent.includes("my name is") ||
237
+ (lowerContent.includes("j'ai") && lowerContent.includes("ans")) ||
238
+ lowerContent.includes("years old") ||
239
+ lowerContent.includes("j'habite") ||
240
+ lowerContent.includes("i live")
241
+ ) {
242
+ return "personal";
243
+ }
244
+
245
+ // Relationship indicators
246
+ if (
247
+ lowerContent.includes("ma femme") ||
248
+ lowerContent.includes("my wife") ||
249
+ lowerContent.includes("mon mari") ||
250
+ lowerContent.includes("my husband") ||
251
+ lowerContent.includes("mon ami") ||
252
+ lowerContent.includes("my friend") ||
253
+ lowerContent.includes("ma famille") ||
254
+ lowerContent.includes("my family")
255
+ ) {
256
+ return "relationships";
257
+ }
258
+
259
+ // Activity indicators
260
+ if (
261
+ lowerContent.includes("je joue") ||
262
+ lowerContent.includes("i play") ||
263
+ lowerContent.includes("je pratique") ||
264
+ lowerContent.includes("i practice") ||
265
+ lowerContent.includes("mon hobby") ||
266
+ lowerContent.includes("my hobby")
267
+ ) {
268
+ return "activities";
269
+ }
270
+
271
+ // Goal indicators
272
+ if (
273
+ lowerContent.includes("je veux") ||
274
+ lowerContent.includes("i want") ||
275
+ lowerContent.includes("mon objectif") ||
276
+ lowerContent.includes("my goal") ||
277
+ lowerContent.includes("j'apprends") ||
278
+ lowerContent.includes("i'm learning")
279
+ ) {
280
+ return "goals";
281
+ }
282
+
283
+ // Default to preferences for most explicit requests
284
+ return "preferences";
285
+ }
286
+
287
+ // Check if content is too generic to be useful
288
+ isGenericContent(content) {
289
+ const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"];
290
+ return genericWords.includes(content.toLowerCase()) || content.length < 2;
291
+ }
292
+
293
+ // Calculate confidence based on context and pattern strength
294
+ calculateExtractionConfidence(match, fullText) {
295
+ let confidence = 0.6; // Base confidence
296
+
297
+ // Boost confidence for explicit statements
298
+ if (fullText.includes("my name is") || fullText.includes("i am called")) {
299
+ confidence += 0.3;
300
+ }
301
+
302
+ // Boost for longer, more specific content
303
+ if (match[1] && match[1].trim().length > 10) {
304
+ confidence += 0.1;
305
+ }
306
+
307
+ // Reduce confidence for uncertain language
308
+ if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) {
309
+ confidence -= 0.2;
310
+ }
311
+
312
+ return Math.min(1.0, Math.max(0.1, confidence));
313
+ }
314
+
315
+ // Check if this is an update to existing memory rather than new info
316
+ async isMemoryUpdate(category, content, existingMemories) {
317
+ const categoryMemories = existingMemories.filter(m => m.category === category);
318
+
319
+ for (const memory of categoryMemories) {
320
+ const similarity = this.calculateSimilarity(memory.content, content);
321
+ if (similarity > 0.3) {
322
+ // Lower threshold for updates
323
+ return true;
324
+ }
325
+ }
326
+
327
+ return false;
328
+ }
329
+
330
+ // Detect natural expressions that patterns might miss
331
+ async detectNaturalExpressions(text, existingMemories) {
332
+ const naturalMemories = [];
333
+ const lowerText = text.toLowerCase();
334
+
335
+ // Detect name mentions in natural context
336
+ const namePatterns = [/call me (\w+)/i, /(\w+) here[,.]?/i, /this is (\w+)/i, /(\w+) speaking/i];
337
+
338
+ for (const pattern of namePatterns) {
339
+ const match = lowerText.match(pattern);
340
+ if (match && match[1] && match[1].length > 1) {
341
+ const name = match[1].trim();
342
+
343
+ // Skip if too generic
344
+ if (!this.isGenericContent(name) && !this.isCommonWord(name)) {
345
+ naturalMemories.push({
346
+ category: "personal",
347
+ type: "auto_extracted",
348
+ content: name,
349
+ sourceText: text,
350
+ confidence: 0.7,
351
+ timestamp: new Date(),
352
+ character: this.selectedCharacter
353
+ });
354
+ }
355
+ }
356
+ }
357
+
358
+ return naturalMemories;
359
+ }
360
+
361
+ // Check if word is too common to be a name
362
+ isCommonWord(word, language = "en") {
363
+ // Use existing constants if available
364
+ if (window.KIMI_COMMON_WORDS && window.KIMI_COMMON_WORDS[language]) {
365
+ return window.KIMI_COMMON_WORDS[language].includes(word.toLowerCase());
366
+ }
367
+
368
+ // Fallback to original English list
369
+ const commonWords = [
370
+ "the",
371
+ "and",
372
+ "for",
373
+ "are",
374
+ "but",
375
+ "not",
376
+ "you",
377
+ "all",
378
+ "can",
379
+ "had",
380
+ "her",
381
+ "was",
382
+ "one",
383
+ "our",
384
+ "out",
385
+ "day",
386
+ "get",
387
+ "has",
388
+ "him",
389
+ "his",
390
+ "how",
391
+ "man",
392
+ "new",
393
+ "now",
394
+ "old",
395
+ "see",
396
+ "two",
397
+ "way",
398
+ "who",
399
+ "boy",
400
+ "did",
401
+ "its",
402
+ "let",
403
+ "put",
404
+ "say",
405
+ "she",
406
+ "too",
407
+ "use"
408
+ ];
409
+ return commonWords.includes(word.toLowerCase());
410
+ }
411
+
412
+ // MANUAL MEMORY MANAGEMENT
413
+ async addMemory(memoryData) {
414
+ if (!this.db || !this.memoryEnabled) return;
415
+
416
+ try {
417
+ // Check for duplicates with intelligent merging
418
+ const existing = await this.findSimilarMemory(memoryData);
419
+ if (existing) {
420
+ // Intelligent merge strategy
421
+ return await this.mergeMemories(existing, memoryData);
422
+ }
423
+
424
+ // Add memory with metadata (let DB auto-generate ID)
425
+ const memory = {
426
+ category: memoryData.category || "personal",
427
+ type: memoryData.type || "manual",
428
+ content: memoryData.content,
429
+ sourceText: memoryData.sourceText || "",
430
+ confidence: memoryData.confidence || 1.0,
431
+ timestamp: memoryData.timestamp || new Date(),
432
+ character: memoryData.character || this.selectedCharacter,
433
+ isActive: true,
434
+ tags: memoryData.tags || [],
435
+ lastModified: new Date(),
436
+ accessCount: 0,
437
+ importance: this.calculateImportance(memoryData)
438
+ };
439
+
440
+ if (this.db.db.memories) {
441
+ const id = await this.db.db.memories.add(memory);
442
+ memory.id = id; // Store the auto-generated ID
443
+ console.log(`Memory added with ID: ${id}`);
444
+ }
445
+
446
+ // Cleanup old memories if we exceed limit
447
+ await this.cleanupOldMemories();
448
+
449
+ // Notify LLM system to refresh context
450
+ this.notifyLLMContextUpdate();
451
+
452
+ return memory;
453
+ } catch (error) {
454
+ console.error("Error adding memory:", error);
455
+ }
456
+ }
457
+
458
+ // Intelligent memory merging
459
+ async mergeMemories(existingMemory, newMemoryData) {
460
+ try {
461
+ // Determine merge strategy based on content and confidence
462
+ const strategy = this.determineMergeStrategy(existingMemory, newMemoryData);
463
+
464
+ let mergedContent = existingMemory.content;
465
+ let mergedConfidence = existingMemory.confidence;
466
+ let mergedTags = [...(existingMemory.tags || [])];
467
+
468
+ switch (strategy) {
469
+ case "update_content":
470
+ // New information is more confident/recent
471
+ mergedContent = newMemoryData.content;
472
+ mergedConfidence = Math.max(existingMemory.confidence, newMemoryData.confidence || 0.8);
473
+ break;
474
+
475
+ case "merge_content":
476
+ // Combine information intelligently
477
+ if (
478
+ existingMemory.category === "personal" &&
479
+ this.areRelatedNames(existingMemory.content, newMemoryData.content)
480
+ ) {
481
+ // Handle name variants
482
+ mergedContent = this.mergeNames(existingMemory.content, newMemoryData.content);
483
+ } else {
484
+ // General merge - keep most specific
485
+ mergedContent =
486
+ newMemoryData.content.length > existingMemory.content.length
487
+ ? newMemoryData.content
488
+ : existingMemory.content;
489
+ }
490
+ mergedConfidence = (existingMemory.confidence + (newMemoryData.confidence || 0.8)) / 2;
491
+ break;
492
+
493
+ case "add_variant":
494
+ // Store as variant/alias
495
+ mergedTags.push(`alias:${newMemoryData.content}`);
496
+ break;
497
+
498
+ case "boost_confidence":
499
+ // Same content, boost confidence
500
+ mergedConfidence = Math.min(1.0, existingMemory.confidence + 0.1);
501
+ break;
502
+ }
503
+
504
+ // Update existing memory
505
+ const updatedMemory = {
506
+ ...existingMemory,
507
+ content: mergedContent,
508
+ confidence: mergedConfidence,
509
+ tags: [...new Set(mergedTags)], // Remove duplicates
510
+ lastModified: new Date(),
511
+ accessCount: (existingMemory.accessCount || 0) + 1,
512
+ importance: Math.max(existingMemory.importance || 0.5, this.calculateImportance(newMemoryData))
513
+ };
514
+
515
+ await this.updateMemory(existingMemory.id, updatedMemory);
516
+ return updatedMemory;
517
+ } catch (error) {
518
+ console.error("Error merging memories:", error);
519
+ return existingMemory;
520
+ }
521
+ }
522
+
523
+ // Determine how to merge two related memories
524
+ determineMergeStrategy(existing, newData) {
525
+ const similarity = this.calculateSimilarity(existing.content, newData.content);
526
+ const newConfidence = newData.confidence || 0.8;
527
+
528
+ // If very similar content but new has higher confidence
529
+ if (similarity > 0.9 && newConfidence > existing.confidence) {
530
+ return "boost_confidence";
531
+ }
532
+
533
+ // If moderately similar, decide based on specificity and recency
534
+ if (similarity > 0.7) {
535
+ if (newData.content.length > existing.content.length * 1.5) {
536
+ return "update_content"; // New is more detailed
537
+ } else {
538
+ return "merge_content";
539
+ }
540
+ }
541
+
542
+ // For names, handle as variants
543
+ if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) {
544
+ return "add_variant";
545
+ }
546
+
547
+ // Default to merging
548
+ return "merge_content";
549
+ }
550
+
551
+ // Merge name variants intelligently
552
+ mergeNames(name1, name2) {
553
+ // Keep the longest/most formal version as primary
554
+ if (name1.length > name2.length) {
555
+ return name1;
556
+ } else if (name2.length > name1.length) {
557
+ return name2;
558
+ }
559
+
560
+ // If same length, keep the first one
561
+ return name1;
562
+ }
563
+
564
+ // Calculate importance of memory for prioritization
565
+ calculateImportance(memoryData) {
566
+ let importance = 0.5; // Base importance
567
+
568
+ // Personal information is generally more important
569
+ const categoryWeights = {
570
+ personal: 0.9,
571
+ relationships: 0.8,
572
+ goals: 0.7,
573
+ preferences: 0.6,
574
+ activities: 0.5,
575
+ experiences: 0.4,
576
+ important: 1.0
577
+ };
578
+
579
+ importance = categoryWeights[memoryData.category] || 0.5;
580
+
581
+ // Boost importance for longer, more detailed content
582
+ if (memoryData.content && memoryData.content.length > 20) {
583
+ importance += 0.1;
584
+ }
585
+
586
+ // High confidence boosts importance
587
+ if (memoryData.confidence && memoryData.confidence > 0.9) {
588
+ importance += 0.1;
589
+ }
590
+
591
+ return Math.min(1.0, importance);
592
+ }
593
+
594
+ async updateMemory(memoryId, updateData) {
595
+ if (!this.db) return false;
596
+
597
+ try {
598
+ // Ensure memoryId is the correct type
599
+ const numericId = typeof memoryId === "string" ? parseInt(memoryId) : memoryId;
600
+
601
+ // Vérifier d'abord que la mémoire existe
602
+ const existingMemory = await this.db.db.memories.get(numericId);
603
+ if (!existingMemory) {
604
+ console.error(`❌ Memory with ID ${numericId} not found in database`);
605
+ return false;
606
+ }
607
+
608
+ console.log(`🔄 Updating memory ${numericId}:`, { existing: existingMemory, update: updateData });
609
+
610
+ const update = {
611
+ ...updateData,
612
+ lastModified: new Date()
613
+ };
614
+
615
+ if (this.db.db.memories) {
616
+ const result = await this.db.db.memories.update(numericId, update);
617
+
618
+ console.log(`Memory update result for ID ${numericId}:`, result);
619
+
620
+ if (result > 0) {
621
+ console.log("✅ Memory updated successfully");
622
+ // Notify LLM system to refresh context
623
+ this.notifyLLMContextUpdate();
624
+ return true;
625
+ } else {
626
+ console.error("❌ Memory update failed - no rows affected");
627
+ return false;
628
+ }
629
+ }
630
+ } catch (error) {
631
+ console.error("Error updating memory:", error, { memoryId, updateData });
632
+ return false;
633
+ }
634
+ }
635
+
636
+ async deleteMemory(memoryId) {
637
+ if (!this.db) return false;
638
+
639
+ try {
640
+ // Ensure memoryId is the correct type
641
+ const numericId = typeof memoryId === "string" ? parseInt(memoryId) : memoryId;
642
+
643
+ if (this.db.db.memories) {
644
+ const result = await this.db.db.memories.delete(numericId);
645
+
646
+ console.log(`Memory delete result for ID ${numericId}:`, result);
647
+
648
+ // Notify LLM system to refresh context
649
+ if (result) {
650
+ this.notifyLLMContextUpdate();
651
+ }
652
+
653
+ return result;
654
+ }
655
+ } catch (error) {
656
+ console.error("Error deleting memory:", error, { memoryId });
657
+ return false;
658
+ }
659
+ }
660
+
661
+ notifyLLMContextUpdate() {
662
+ // Debounce context updates to avoid excessive calls
663
+ if (this.contextUpdateTimeout) {
664
+ clearTimeout(this.contextUpdateTimeout);
665
+ }
666
+
667
+ this.contextUpdateTimeout = setTimeout(() => {
668
+ if (window.kimiLLM && typeof window.kimiLLM.refreshMemoryContext === "function") {
669
+ window.kimiLLM.refreshMemoryContext();
670
+ }
671
+ }, 500);
672
+ }
673
+
674
+ async getMemoriesByCategory(category, character = null) {
675
+ if (!this.db) return [];
676
+
677
+ try {
678
+ character = character || this.selectedCharacter;
679
+
680
+ if (this.db.db.memories) {
681
+ return await this.db.db.memories
682
+ .where("[character+category]")
683
+ .equals([character, category])
684
+ .and(m => m.isActive)
685
+ .reverse()
686
+ .sortBy("timestamp");
687
+ }
688
+ } catch (error) {
689
+ console.error("Error getting memories by category:", error);
690
+ return [];
691
+ }
692
+ }
693
+
694
+ async getAllMemories(character = null) {
695
+ if (!this.db) return [];
696
+
697
+ try {
698
+ character = character || this.selectedCharacter;
699
+
700
+ if (this.db.db.memories) {
701
+ const memories = await this.db.db.memories
702
+ .where("character")
703
+ .equals(character)
704
+ .and(m => m.isActive)
705
+ .reverse()
706
+ .sortBy("timestamp");
707
+
708
+ console.log(`Retrieved ${memories.length} memories for character: ${character}`);
709
+ return memories;
710
+ }
711
+ } catch (error) {
712
+ console.error("Error getting all memories:", error);
713
+ return [];
714
+ }
715
+ }
716
+
717
+ async findSimilarMemory(memoryData) {
718
+ if (!this.db) return null;
719
+
720
+ try {
721
+ const memories = await this.getMemoriesByCategory(memoryData.category);
722
+
723
+ // Enhanced similarity check with multiple criteria
724
+ for (const memory of memories) {
725
+ const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content);
726
+
727
+ // Different thresholds based on category
728
+ let threshold = 0.8;
729
+ if (memoryData.category === "personal") {
730
+ threshold = 0.6; // Names and personal info can vary more
731
+ } else if (memoryData.category === "preferences") {
732
+ threshold = 0.7; // Preferences can be expressed differently
733
+ }
734
+
735
+ if (contentSimilarity > threshold) {
736
+ return memory;
737
+ }
738
+
739
+ // Special handling for names (check if one is contained in the other)
740
+ if (memoryData.category === "personal" && this.areRelatedNames(memory.content, memoryData.content)) {
741
+ return memory;
742
+ }
743
+ }
744
+ } catch (error) {
745
+ console.error("Error finding similar memory:", error);
746
+ }
747
+
748
+ return null;
749
+ }
750
+
751
+ // Check if two names are related (nicknames, variants, etc.)
752
+ areRelatedNames(name1, name2) {
753
+ const n1 = name1.toLowerCase().trim();
754
+ const n2 = name2.toLowerCase().trim();
755
+
756
+ // Exact match
757
+ if (n1 === n2) return true;
758
+
759
+ // One contains the other (Jean-Pierre vs Jean)
760
+ if (n1.includes(n2) || n2.includes(n1)) return true;
761
+
762
+ // Common nickname patterns
763
+ const nicknames = {
764
+ jean: ["jp", "jeannot"],
765
+ pierre: ["pete", "pietro"],
766
+ marie: ["mary", "maria"],
767
+ michael: ["mike", "mick"],
768
+ william: ["bill", "will", "willy"],
769
+ robert: ["bob", "rob", "bobby"],
770
+ richard: ["rick", "dick", "richie"],
771
+ thomas: ["tom", "tommy"],
772
+ christopher: ["chris", "kit"],
773
+ anthony: ["tony", "ant"]
774
+ };
775
+
776
+ for (const [full, nicks] of Object.entries(nicknames)) {
777
+ if ((n1 === full && nicks.includes(n2)) || (n2 === full && nicks.includes(n1))) {
778
+ return true;
779
+ }
780
+ }
781
+
782
+ return false;
783
+ }
784
+
785
+ calculateSimilarity(text1, text2) {
786
+ // Enhanced similarity calculation
787
+ const words1 = text1
788
+ .toLowerCase()
789
+ .split(/\s+/)
790
+ .filter(w => w.length > 2);
791
+ const words2 = text2
792
+ .toLowerCase()
793
+ .split(/\s+/)
794
+ .filter(w => w.length > 2);
795
+
796
+ if (words1.length === 0 || words2.length === 0) {
797
+ return text1.toLowerCase() === text2.toLowerCase() ? 1 : 0;
798
+ }
799
+
800
+ const intersection = words1.filter(word => words2.includes(word));
801
+ const union = [...new Set([...words1, ...words2])];
802
+
803
+ let similarity = intersection.length / union.length;
804
+
805
+ // Boost similarity for exact substring matches
806
+ if (text1.toLowerCase().includes(text2.toLowerCase()) || text2.toLowerCase().includes(text1.toLowerCase())) {
807
+ similarity += 0.2;
808
+ }
809
+
810
+ return Math.min(1.0, similarity);
811
+ }
812
+
813
+ async cleanupOldMemories() {
814
+ if (!this.db) return;
815
+
816
+ try {
817
+ const memories = await this.getAllMemories();
818
+
819
+ if (memories.length > this.maxMemoryEntries) {
820
+ // Keep most recent and important memories
821
+ memories.sort((a, b) => {
822
+ // Priority: confidence * recency
823
+ const scoreA = a.confidence * (Date.now() - new Date(a.timestamp).getTime());
824
+ const scoreB = b.confidence * (Date.now() - new Date(b.timestamp).getTime());
825
+ return scoreB - scoreA;
826
+ });
827
+
828
+ const toDelete = memories.slice(this.maxMemoryEntries);
829
+ for (const memory of toDelete) {
830
+ await this.deleteMemory(memory.id);
831
+ }
832
+ }
833
+ } catch (error) {
834
+ console.error("Error cleaning up old memories:", error);
835
+ }
836
+ }
837
+
838
+ // MEMORY RETRIEVAL FOR LLM
839
+ async getRelevantMemories(context = "", limit = 10) {
840
+ if (!this.memoryEnabled) return [];
841
+
842
+ try {
843
+ const allMemories = await this.getAllMemories();
844
+
845
+ if (allMemories.length === 0) return [];
846
+
847
+ if (!context) {
848
+ // Return most important and recent memories
849
+ return this.selectMostImportantMemories(allMemories, limit);
850
+ }
851
+
852
+ // Score memories based on relevance to context
853
+ const scoredMemories = allMemories.map(memory => ({
854
+ ...memory,
855
+ relevanceScore: this.calculateRelevance(memory, context)
856
+ }));
857
+
858
+ // Sort by relevance and return top results
859
+ scoredMemories.sort((a, b) => b.relevanceScore - a.relevanceScore);
860
+
861
+ // Filter out very low relevance memories
862
+ const relevantMemories = scoredMemories.filter(m => m.relevanceScore > 0.1);
863
+
864
+ return relevantMemories.slice(0, limit);
865
+ } catch (error) {
866
+ console.error("Error getting relevant memories:", error);
867
+ return [];
868
+ }
869
+ }
870
+
871
+ // Select most important memories when no context is provided
872
+ selectMostImportantMemories(memories, limit) {
873
+ // Score by importance, recency, and access count
874
+ const scoredMemories = memories.map(memory => {
875
+ let score = memory.importance || 0.5;
876
+
877
+ // Boost recent memories
878
+ const daysSinceCreation = (Date.now() - new Date(memory.timestamp)) / (1000 * 60 * 60 * 24);
879
+ score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost
880
+
881
+ // Boost frequently accessed memories
882
+ const accessCount = memory.accessCount || 0;
883
+ score += Math.min(accessCount / 10, 0.2); // Access boost
884
+
885
+ // Boost high confidence memories
886
+ score += (memory.confidence || 0.5) * 0.1;
887
+
888
+ return { ...memory, importanceScore: score };
889
+ });
890
+
891
+ scoredMemories.sort((a, b) => b.importanceScore - a.importanceScore);
892
+ return scoredMemories.slice(0, limit);
893
+ }
894
+
895
+ calculateRelevance(memory, context) {
896
+ const contextWords = context
897
+ .toLowerCase()
898
+ .split(/\s+/)
899
+ .filter(w => w.length > 2);
900
+ const memoryWords = memory.content
901
+ .toLowerCase()
902
+ .split(/\s+/)
903
+ .filter(w => w.length > 2);
904
+
905
+ let score = 0;
906
+
907
+ // Enhanced content similarity with keyword matching
908
+ score += this.calculateSimilarity(memory.content, context) * 0.4;
909
+
910
+ // Keyword matching bonus
911
+ let keywordMatches = 0;
912
+ for (const word of contextWords) {
913
+ if (memoryWords.includes(word)) {
914
+ keywordMatches++;
915
+ }
916
+ }
917
+ if (contextWords.length > 0) {
918
+ score += (keywordMatches / contextWords.length) * 0.3;
919
+ }
920
+
921
+ // Category relevance bonus based on context
922
+ score += this.getCategoryRelevance(memory.category, context) * 0.1;
923
+
924
+ // Recent memories get bonus for current conversation
925
+ const daysSinceCreation = (Date.now() - new Date(memory.timestamp)) / (1000 * 60 * 60 * 24);
926
+ score += Math.max(0, (30 - daysSinceCreation) / 30) * 0.1;
927
+
928
+ // Confidence and importance boost
929
+ score += (memory.confidence || 0.5) * 0.05;
930
+ score += (memory.importance || 0.5) * 0.05;
931
+
932
+ return Math.min(1.0, score);
933
+ }
934
+
935
+ // Determine if memory category is relevant to current context
936
+ getCategoryRelevance(category, context) {
937
+ const contextLower = context.toLowerCase();
938
+
939
+ const categoryKeywords = {
940
+ personal: ["name", "age", "live", "work", "job", "who", "am", "myself"],
941
+ preferences: ["like", "love", "hate", "prefer", "enjoy", "favorite", "dislike"],
942
+ relationships: ["family", "friend", "wife", "husband", "partner", "mother", "father"],
943
+ activities: ["play", "hobby", "sport", "activity", "practice", "do"],
944
+ goals: ["want", "plan", "goal", "dream", "hope", "wish", "future"],
945
+ experiences: ["remember", "happened", "story", "experience", "time"],
946
+ important: ["important", "remember", "special", "never forget"]
947
+ };
948
+
949
+ const keywords = categoryKeywords[category] || [];
950
+ let relevance = 0;
951
+
952
+ for (const keyword of keywords) {
953
+ if (contextLower.includes(keyword)) {
954
+ relevance += 0.2;
955
+ }
956
+ }
957
+
958
+ return Math.min(1.0, relevance);
959
+ }
960
+
961
+ // Update access count when memory is used
962
+ async recordMemoryAccess(memoryId) {
963
+ try {
964
+ const memory = await this.db.db.memories.get(memoryId);
965
+ if (memory) {
966
+ memory.accessCount = (memory.accessCount || 0) + 1;
967
+ memory.lastAccessed = new Date();
968
+ await this.db.db.memories.put(memory);
969
+ }
970
+ } catch (error) {
971
+ console.error("Error recording memory access:", error);
972
+ }
973
+ }
974
+
975
+ // MEMORY STATISTICS
976
+ async getMemoryStats() {
977
+ try {
978
+ const memories = await this.getAllMemories();
979
+ const stats = {
980
+ total: memories.length,
981
+ byCategory: {},
982
+ averageConfidence: 0,
983
+ oldestMemory: null,
984
+ newestMemory: null
985
+ };
986
+
987
+ if (memories.length > 0) {
988
+ // Category breakdown
989
+ for (const memory of memories) {
990
+ stats.byCategory[memory.category] = (stats.byCategory[memory.category] || 0) + 1;
991
+ }
992
+
993
+ // Average confidence
994
+ stats.averageConfidence = memories.reduce((sum, m) => sum + m.confidence, 0) / memories.length;
995
+
996
+ // Oldest and newest
997
+ const sortedByDate = [...memories].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
998
+ stats.oldestMemory = sortedByDate[0];
999
+ stats.newestMemory = sortedByDate[sortedByDate.length - 1];
1000
+ }
1001
+
1002
+ return stats;
1003
+ } catch (error) {
1004
+ console.error("Error getting memory stats:", error);
1005
+ return { total: 0, byCategory: {}, averageConfidence: 0 };
1006
+ }
1007
+ }
1008
+
1009
+ // MEMORY TOGGLE
1010
+ async toggleMemorySystem(enabled) {
1011
+ this.memoryEnabled = enabled;
1012
+ if (this.db) {
1013
+ await this.db.setPreference("memorySystemEnabled", enabled);
1014
+ }
1015
+ }
1016
+
1017
+ // EXPORT/IMPORT MEMORIES
1018
+ async exportMemories() {
1019
+ try {
1020
+ const memories = await this.getAllMemories();
1021
+ return {
1022
+ exportDate: new Date().toISOString(),
1023
+ character: this.selectedCharacter,
1024
+ memories: memories,
1025
+ version: "1.0"
1026
+ };
1027
+ } catch (error) {
1028
+ console.error("Error exporting memories:", error);
1029
+ return null;
1030
+ }
1031
+ }
1032
+
1033
+ async importMemories(importData) {
1034
+ if (!importData || !importData.memories) return false;
1035
+
1036
+ try {
1037
+ for (const memory of importData.memories) {
1038
+ await this.addMemory({
1039
+ ...memory,
1040
+ type: "imported",
1041
+ character: this.selectedCharacter
1042
+ });
1043
+ }
1044
+ return true;
1045
+ } catch (error) {
1046
+ console.error("Error importing memories:", error);
1047
+ return false;
1048
+ }
1049
+ }
1050
+
1051
+ // MIGRATION UTILITIES
1052
+ async migrateIncompatibleIDs() {
1053
+ if (!this.db) return false;
1054
+
1055
+ try {
1056
+ console.log("🔧 Début de la migration des IDs incompatibles...");
1057
+
1058
+ // Récupérer toutes les mémoires
1059
+ const allMemories = await this.db.db.memories.toArray();
1060
+ console.log(`📊 ${allMemories.length} mémoires trouvées`);
1061
+
1062
+ const incompatibleMemories = allMemories.filter(memory => {
1063
+ // Les IDs auto-increment sont des entiers séquentiels (1, 2, 3...)
1064
+ // Les anciens IDs manuels sont des nombres très grands (timestamps)
1065
+ return memory.id > 10000; // Seuil arbitraire pour détecter les anciens IDs
1066
+ });
1067
+
1068
+ if (incompatibleMemories.length === 0) {
1069
+ console.log("✅ Aucune migration nécessaire");
1070
+ return true;
1071
+ }
1072
+
1073
+ console.log(`🔄 Migration de ${incompatibleMemories.length} mémoires avec IDs incompatibles`);
1074
+
1075
+ // Sauvegarder les données avant suppression
1076
+ const dataToMigrate = incompatibleMemories.map(memory => {
1077
+ const { id, ...memoryData } = memory; // Enlever l'ancien ID
1078
+ return memoryData;
1079
+ });
1080
+
1081
+ // Supprimer les anciennes entrées
1082
+ await this.db.db.memories.bulkDelete(incompatibleMemories.map(m => m.id));
1083
+
1084
+ // Réinsérer avec de nouveaux IDs auto-générés
1085
+ const newIds = await this.db.db.memories.bulkAdd(dataToMigrate);
1086
+
1087
+ console.log(`✅ Migration terminée. Nouveaux IDs:`, newIds);
1088
+ return true;
1089
+ } catch (error) {
1090
+ console.error("❌ Erreur lors de la migration:", error);
1091
+ return false;
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ window.KimiMemorySystem = KimiMemorySystem;
kimi-js/kimi-memory-ui.js ADDED
@@ -0,0 +1,687 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI MEMORY UI MANAGER =====
2
+ class KimiMemoryUI {
3
+ constructor() {
4
+ this.memorySystem = null;
5
+ this.isInitialized = false;
6
+ }
7
+
8
+ async init() {
9
+ if (!window.kimiMemorySystem) {
10
+ console.warn("Memory system not available");
11
+ return;
12
+ }
13
+
14
+ this.memorySystem = window.kimiMemorySystem;
15
+ this.setupEventListeners();
16
+ await this.updateMemoryStats();
17
+ this.isInitialized = true;
18
+ }
19
+
20
+ setupEventListeners() {
21
+ // Memory toggle
22
+ const memoryToggle = document.getElementById("memory-toggle");
23
+ if (memoryToggle) {
24
+ memoryToggle.addEventListener("click", () => this.toggleMemorySystem());
25
+ }
26
+
27
+ // View memories button
28
+ const viewMemoriesBtn = document.getElementById("view-memories");
29
+ if (viewMemoriesBtn) {
30
+ viewMemoriesBtn.addEventListener("click", () => this.openMemoryModal());
31
+ }
32
+
33
+ // Add memory button
34
+ const addMemoryBtn = document.getElementById("add-memory");
35
+ if (addMemoryBtn) {
36
+ addMemoryBtn.addEventListener("click", () => this.addManualMemory());
37
+ }
38
+
39
+ // Memory modal close
40
+ const memoryClose = document.getElementById("memory-close");
41
+ if (memoryClose) {
42
+ memoryClose.addEventListener("click", () => this.closeMemoryModal());
43
+ }
44
+
45
+ // Memory export
46
+ const memoryExport = document.getElementById("memory-export");
47
+ if (memoryExport) {
48
+ memoryExport.addEventListener("click", () => this.exportMemories());
49
+ }
50
+
51
+ // Memory filter
52
+ const memoryFilter = document.getElementById("memory-filter-category");
53
+ if (memoryFilter) {
54
+ memoryFilter.addEventListener("change", () => this.filterMemories());
55
+ }
56
+
57
+ // Memory search
58
+ const memorySearch = document.getElementById("memory-search");
59
+ if (memorySearch) {
60
+ memorySearch.addEventListener("input", () => this.filterMemories());
61
+ }
62
+
63
+ // Close modal on overlay click
64
+ const memoryOverlay = document.getElementById("memory-overlay");
65
+ if (memoryOverlay) {
66
+ memoryOverlay.addEventListener("click", e => {
67
+ if (e.target === memoryOverlay) {
68
+ this.closeMemoryModal();
69
+ }
70
+ });
71
+ }
72
+ }
73
+
74
+ async toggleMemorySystem() {
75
+ if (!this.memorySystem) return;
76
+
77
+ const toggle = document.getElementById("memory-toggle");
78
+ const enabled = !this.memorySystem.memoryEnabled;
79
+
80
+ await this.memorySystem.toggleMemorySystem(enabled);
81
+
82
+ if (toggle) {
83
+ toggle.setAttribute("aria-checked", enabled.toString());
84
+ toggle.classList.toggle("active", enabled);
85
+ }
86
+
87
+ // Show feedback
88
+ this.showFeedback(enabled ? "Memory system enabled" : "Memory system disabled");
89
+ }
90
+
91
+ async addManualMemory() {
92
+ const categorySelect = document.getElementById("memory-category");
93
+ const contentInput = document.getElementById("memory-content");
94
+
95
+ if (!categorySelect || !contentInput) return;
96
+
97
+ const category = categorySelect.value;
98
+ const content = contentInput.value.trim();
99
+
100
+ if (!content) {
101
+ this.showFeedback("Please enter memory content", "error");
102
+ return;
103
+ }
104
+
105
+ try {
106
+ await this.memorySystem.addMemory({
107
+ category: category,
108
+ content: content,
109
+ type: "manual",
110
+ confidence: 1.0
111
+ });
112
+
113
+ contentInput.value = "";
114
+ await this.updateMemoryStats();
115
+ this.showFeedback("Memory added successfully");
116
+ } catch (error) {
117
+ console.error("Error adding memory:", error);
118
+ this.showFeedback("Error adding memory", "error");
119
+ }
120
+ }
121
+
122
+ async openMemoryModal() {
123
+ const overlay = document.getElementById("memory-overlay");
124
+ if (!overlay) return;
125
+
126
+ overlay.style.display = "flex";
127
+ await this.loadMemories();
128
+ }
129
+
130
+ closeMemoryModal() {
131
+ const overlay = document.getElementById("memory-overlay");
132
+ if (overlay) {
133
+ overlay.style.display = "none";
134
+ // Ensure background video resumes after closing memory modal
135
+ const kv = window.kimiVideo;
136
+ if (kv && kv.activeVideo) {
137
+ try {
138
+ const v = kv.activeVideo;
139
+ if (v.ended) {
140
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
141
+ } else if (v.paused) {
142
+ v.play().catch(() => {
143
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
144
+ });
145
+ }
146
+ } catch {}
147
+ }
148
+ }
149
+ }
150
+
151
+ async loadMemories() {
152
+ if (!this.memorySystem) return;
153
+
154
+ try {
155
+ const memories = await this.memorySystem.getAllMemories();
156
+ console.log("Loading memories into UI:", memories.length);
157
+ this.renderMemories(memories);
158
+ } catch (error) {
159
+ console.error("Error loading memories:", error);
160
+ }
161
+ }
162
+
163
+ async filterMemories() {
164
+ const filterSelect = document.getElementById("memory-filter-category");
165
+ const searchInput = document.getElementById("memory-search");
166
+ if (!this.memorySystem) return;
167
+
168
+ try {
169
+ const category = filterSelect?.value;
170
+ const searchTerm = searchInput?.value.toLowerCase().trim();
171
+ let memories;
172
+
173
+ if (category) {
174
+ memories = await this.memorySystem.getMemoriesByCategory(category);
175
+ } else {
176
+ memories = await this.memorySystem.getAllMemories();
177
+ }
178
+
179
+ // Apply search filter if search term exists
180
+ if (searchTerm) {
181
+ memories = memories.filter(
182
+ memory =>
183
+ memory.content.toLowerCase().includes(searchTerm) ||
184
+ memory.category.toLowerCase().includes(searchTerm) ||
185
+ (memory.sourceText && memory.sourceText.toLowerCase().includes(searchTerm))
186
+ );
187
+ }
188
+
189
+ this.renderMemories(memories);
190
+ } catch (error) {
191
+ console.error("Error filtering memories:", error);
192
+ }
193
+ }
194
+
195
+ renderMemories(memories) {
196
+ const memoryList = document.getElementById("memory-list");
197
+ if (!memoryList) return;
198
+
199
+ console.log("Rendering memories:", memories); // Debug logging
200
+
201
+ if (memories.length === 0) {
202
+ memoryList.innerHTML = `
203
+ <div class="memory-empty">
204
+ <i class="fas fa-brain"></i>
205
+ <p>No memories found. Start chatting to build memories automatically, or add them manually.</p>
206
+ </div>
207
+ `;
208
+ return;
209
+ }
210
+
211
+ // Group memories by category for better organization
212
+ const groupedMemories = memories.reduce((groups, memory) => {
213
+ const category = memory.category || "other";
214
+ if (!groups[category]) groups[category] = [];
215
+ groups[category].push(memory);
216
+ return groups;
217
+ }, {});
218
+
219
+ let html = "";
220
+ Object.entries(groupedMemories).forEach(([category, categoryMemories]) => {
221
+ html += `
222
+ <div class="memory-category-group">
223
+ <h4 class="memory-category-header">
224
+ ${this.getCategoryIcon(category)} ${this.formatCategoryName(category)}
225
+ <span class="memory-category-count">(${categoryMemories.length})</span>
226
+ </h4>
227
+ <div class="memory-category-items">
228
+ `;
229
+
230
+ categoryMemories.forEach(memory => {
231
+ const confidence = Math.round(memory.confidence * 100);
232
+ const isAutomatic = memory.type === "auto_extracted";
233
+ const previewLength = 120;
234
+ const isLongContent = memory.content.length > previewLength;
235
+ const previewText = isLongContent ? memory.content.substring(0, previewLength) + "..." : memory.content;
236
+ const wordCount = memory.content.split(/\s+/).length;
237
+
238
+ html += `
239
+ <div class="memory-item ${isAutomatic ? "memory-auto" : "memory-manual"}" data-memory-id="${memory.id}">
240
+ <div class="memory-header">
241
+ <div class="memory-badges">
242
+ <span class="memory-type ${memory.type}">${memory.type === "auto_extracted" ? "🤖 Auto" : "✋ Manual"}</span>
243
+ <span class="memory-confidence confidence-${this.getConfidenceLevel(confidence)}">${confidence}%</span>
244
+ ${isLongContent ? `<span class="memory-length">${wordCount} mots</span>` : ""}
245
+ </div>
246
+ </div>
247
+ <div class="memory-preview">
248
+ <div class="memory-preview-text ${isLongContent ? "memory-preview-short" : ""}" id="preview-${memory.id}">
249
+ ${this.highlightMemoryContent(previewText)}
250
+ </div>
251
+ ${
252
+ isLongContent
253
+ ? `
254
+ <div class="memory-preview-full" id="full-${memory.id}" style="display: none;">
255
+ ${this.highlightMemoryContent(memory.content)}
256
+ </div>
257
+ <button class="memory-expand-btn" onclick="kimiMemoryUI.toggleMemoryContent('${memory.id}')">
258
+ <i class="fas fa-chevron-down" id="icon-${memory.id}"></i> Voir plus
259
+ </button>
260
+ `
261
+ : ""
262
+ }
263
+ </div>
264
+ <div class="memory-meta">
265
+ <span class="memory-date">${this.formatDate(memory.timestamp)}</span>
266
+ ${
267
+ memory.sourceText
268
+ ? `<span class="memory-source" title="${
269
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
270
+ ? window.KimiValidationUtils.escapeHtml(memory.sourceText)
271
+ : memory.sourceText
272
+ }">� Extrait de conversation</span>`
273
+ : `<span>📝 Ajouté manuellement</span>`
274
+ }
275
+ </div>
276
+ <div class="memory-actions">
277
+ <button class="memory-edit-btn" onclick="kimiMemoryUI.editMemory('${memory.id}')" title="Modifier cette mémoire">
278
+ <i class="fas fa-edit"></i>
279
+ </button>
280
+ <button class="memory-delete-btn" onclick="kimiMemoryUI.deleteMemory('${memory.id}')" title="Supprimer cette mémoire">
281
+ <i class="fas fa-trash"></i>
282
+ </button>
283
+ </div>
284
+ </div>
285
+ `;
286
+ });
287
+
288
+ html += `
289
+ </div>
290
+ </div>
291
+ `;
292
+ });
293
+
294
+ memoryList.innerHTML = html;
295
+ }
296
+
297
+ formatCategoryName(category) {
298
+ const names = {
299
+ personal: "Personal Information",
300
+ preferences: "Likes & Dislikes",
301
+ relationships: "Relationships & People",
302
+ activities: "Activities & Hobbies",
303
+ goals: "Goals & Aspirations",
304
+ experiences: "Shared Experiences",
305
+ important: "Important Events"
306
+ };
307
+ return names[category] || category.charAt(0).toUpperCase() + category.slice(1);
308
+ }
309
+
310
+ getConfidenceLevel(confidence) {
311
+ if (confidence >= 80) return "high";
312
+ if (confidence >= 60) return "medium";
313
+ return "low";
314
+ }
315
+
316
+ formatDate(timestamp) {
317
+ const date = new Date(timestamp);
318
+ const now = new Date();
319
+ const diffTime = now - date;
320
+ const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
321
+
322
+ if (diffDays === 0) return "Today";
323
+ if (diffDays === 1) return "Yesterday";
324
+ if (diffDays < 7) return `${diffDays} days ago`;
325
+ return date.toLocaleDateString();
326
+ }
327
+
328
+ highlightMemoryContent(content) {
329
+ // Escape HTML first using centralized util
330
+ const escapedContent =
331
+ window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml
332
+ ? window.KimiValidationUtils.escapeHtml(content)
333
+ : content;
334
+
335
+ // Simple highlighting for search terms if there's a search active
336
+ const searchInput = document.getElementById("memory-search");
337
+ if (searchInput && searchInput.value.trim()) {
338
+ const searchTerm = searchInput.value.trim();
339
+ const regex = new RegExp(`(${searchTerm})`, "gi");
340
+ return escapedContent.replace(
341
+ regex,
342
+ '<mark style="background: var(--primary-color); color: white; padding: 1px 3px; border-radius: 2px;">$1</mark>'
343
+ );
344
+ }
345
+
346
+ return escapedContent;
347
+ }
348
+
349
+ // Removed duplicate escapeHtml; use window.KimiValidationUtils.escapeHtml instead
350
+
351
+ getCategoryIcon(category) {
352
+ const icons = {
353
+ personal: "👤",
354
+ preferences: "❤️",
355
+ relationships: "👨‍👩‍👧‍👦",
356
+ activities: "🎯",
357
+ goals: "🎯",
358
+ experiences: "⭐",
359
+ important: "📌"
360
+ };
361
+ return icons[category] || "📝";
362
+ }
363
+
364
+ toggleMemoryContent(memoryId) {
365
+ const previewShort = document.getElementById(`preview-${memoryId}`);
366
+ const previewFull = document.getElementById(`full-${memoryId}`);
367
+ const icon = document.getElementById(`icon-${memoryId}`);
368
+ const expandBtn = icon?.closest(".memory-expand-btn");
369
+
370
+ if (!previewShort || !previewFull || !icon || !expandBtn) return;
371
+
372
+ const isExpanded = previewFull.style.display !== "none";
373
+
374
+ if (isExpanded) {
375
+ previewShort.style.display = "block";
376
+ previewFull.style.display = "none";
377
+ icon.className = "fas fa-chevron-down";
378
+ expandBtn.innerHTML = '<i class="fas fa-chevron-down"></i> Voir plus';
379
+ } else {
380
+ previewShort.style.display = "none";
381
+ previewFull.style.display = "block";
382
+ icon.className = "fas fa-chevron-up";
383
+ expandBtn.innerHTML = '<i class="fas fa-chevron-up"></i> Voir moins';
384
+ }
385
+ }
386
+
387
+ async editMemory(memoryId) {
388
+ if (!this.memorySystem) return;
389
+
390
+ try {
391
+ // Get the memory to edit
392
+ const memories = await this.memorySystem.getAllMemories();
393
+ const memory = memories.find(m => m.id == memoryId);
394
+ if (!memory) {
395
+ this.showFeedback("Memory not found", "error");
396
+ return;
397
+ }
398
+
399
+ // Create edit dialog
400
+ const overlay = document.createElement("div");
401
+ overlay.className = "memory-edit-overlay";
402
+ overlay.style.cssText = `
403
+ position: fixed;
404
+ top: 0;
405
+ left: 0;
406
+ width: 100%;
407
+ height: 100%;
408
+ background: rgba(0, 0, 0, 0.8);
409
+ display: flex;
410
+ justify-content: center;
411
+ align-items: center;
412
+ z-index: 10001;
413
+ `;
414
+
415
+ const dialog = document.createElement("div");
416
+ dialog.className = "memory-edit-dialog";
417
+ dialog.style.cssText = `
418
+ background: var(--background-secondary);
419
+ border-radius: 12px;
420
+ padding: 24px;
421
+ width: 90%;
422
+ max-width: 500px;
423
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
424
+ `;
425
+
426
+ dialog.innerHTML = `
427
+ <h3 style="margin: 0 0 20px 0; color: var(--text-primary);">
428
+ <i class="fas fa-edit"></i> Edit Memory
429
+ </h3>
430
+ <div style="margin-bottom: 16px;">
431
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;">Category:</label>
432
+ <select id="edit-memory-category" class="kimi-select" style="width: 100%;">
433
+ <option value="personal" ${memory.category === "personal" ? "selected" : ""}>Personal Info</option>
434
+ <option value="preferences" ${memory.category === "preferences" ? "selected" : ""}>Likes & Dislikes</option>
435
+ <option value="relationships" ${memory.category === "relationships" ? "selected" : ""}>Relationships</option>
436
+ <option value="activities" ${memory.category === "activities" ? "selected" : ""}>Activities & Hobbies</option>
437
+ <option value="goals" ${memory.category === "goals" ? "selected" : ""}>Goals & Plans</option>
438
+ <option value="experiences" ${memory.category === "experiences" ? "selected" : ""}>Experiences</option>
439
+ <option value="important" ${memory.category === "important" ? "selected" : ""}>Important Events</option>
440
+ </select>
441
+ </div>
442
+ <div style="margin-bottom: 20px;">
443
+ <label style="display: block; margin-bottom: 8px; font-weight: 500;">Content:</label>
444
+ <textarea id="edit-memory-content" class="kimi-input" style="width: 100%; height: 100px; resize: vertical;" placeholder="Memory content...">${memory.content}</textarea>
445
+ </div>
446
+ <div style="display: flex; gap: 12px; justify-content: flex-end;">
447
+ <button id="cancel-edit" class="kimi-button" style="background: #6c757d;">
448
+ <i class="fas fa-times"></i> Cancel
449
+ </button>
450
+ <button id="save-edit" class="kimi-button">
451
+ <i class="fas fa-save"></i> Save
452
+ </button>
453
+ </div>
454
+ `;
455
+
456
+ overlay.appendChild(dialog);
457
+ document.body.appendChild(overlay);
458
+
459
+ // Handle buttons
460
+ dialog.querySelector("#cancel-edit").addEventListener("click", () => {
461
+ document.body.removeChild(overlay);
462
+ });
463
+
464
+ dialog.querySelector("#save-edit").addEventListener("click", async () => {
465
+ const newCategory = dialog.querySelector("#edit-memory-category").value;
466
+ const newContent = dialog.querySelector("#edit-memory-content").value.trim();
467
+
468
+ if (!newContent) {
469
+ this.showFeedback("Le contenu ne peut pas être vide", "error");
470
+ return;
471
+ }
472
+
473
+ console.log(`🔄 Tentative de mise à jour de la mémoire ID: ${memoryId}`);
474
+ console.log("Nouvelles données:", { category: newCategory, content: newContent });
475
+
476
+ try {
477
+ const result = await this.memorySystem.updateMemory(memoryId, {
478
+ category: newCategory,
479
+ content: newContent
480
+ });
481
+
482
+ console.log("Résultat de l'update:", result);
483
+
484
+ if (result === true) {
485
+ // Fermer le modal
486
+ document.body.removeChild(overlay);
487
+
488
+ // Forcer le rechargement complet
489
+ await this.loadMemories();
490
+ await this.updateMemoryStats();
491
+
492
+ this.showFeedback("Mémoire mise à jour avec succès");
493
+ console.log("✅ Interface mise à jour");
494
+ } else {
495
+ this.showFeedback("Erreur: Impossible de mettre à jour la mémoire", "error");
496
+ console.error("❌ Update échoué, résultat:", result);
497
+ }
498
+ } catch (error) {
499
+ console.error("Error updating memory:", error);
500
+ this.showFeedback("Erreur lors de la mise à jour de la mémoire", "error");
501
+ }
502
+ });
503
+
504
+ // Close on overlay click
505
+ overlay.addEventListener("click", e => {
506
+ if (e.target === overlay) {
507
+ document.body.removeChild(overlay);
508
+ }
509
+ });
510
+ } catch (error) {
511
+ console.error("Error editing memory:", error);
512
+ this.showFeedback("Error loading memory for editing", "error");
513
+ }
514
+ }
515
+
516
+ async deleteMemory(memoryId) {
517
+ if (!confirm("Are you sure you want to delete this memory?")) return;
518
+
519
+ try {
520
+ await this.memorySystem.deleteMemory(memoryId);
521
+ await this.loadMemories();
522
+ await this.updateMemoryStats();
523
+ this.showFeedback("Memory deleted");
524
+ } catch (error) {
525
+ console.error("Error deleting memory:", error);
526
+ this.showFeedback("Error deleting memory", "error");
527
+ }
528
+ }
529
+
530
+ async exportMemories() {
531
+ if (!this.memorySystem) return;
532
+
533
+ try {
534
+ const exportData = await this.memorySystem.exportMemories();
535
+ if (exportData) {
536
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
537
+ type: "application/json"
538
+ });
539
+ const url = URL.createObjectURL(blob);
540
+ const a = document.createElement("a");
541
+ a.href = url;
542
+ a.download = `kimi-memories-${new Date().toISOString().split("T")[0]}.json`;
543
+ a.click();
544
+ URL.revokeObjectURL(url);
545
+ this.showFeedback("Memories exported successfully");
546
+ }
547
+ } catch (error) {
548
+ console.error("Error exporting memories:", error);
549
+ this.showFeedback("Error exporting memories", "error");
550
+ }
551
+ }
552
+
553
+ async updateMemoryStats() {
554
+ if (!this.memorySystem) return;
555
+
556
+ try {
557
+ const stats = await this.memorySystem.getMemoryStats();
558
+ const memoryCount = document.getElementById("memory-count");
559
+ const memoryToggle = document.getElementById("memory-toggle");
560
+
561
+ if (memoryCount) {
562
+ memoryCount.textContent = `${stats.total} memories`;
563
+ }
564
+
565
+ // Update toggle state
566
+ if (memoryToggle) {
567
+ const enabled = this.memorySystem.memoryEnabled;
568
+ memoryToggle.setAttribute("aria-checked", enabled.toString());
569
+ memoryToggle.classList.toggle("active", enabled);
570
+
571
+ // Add visual indicator for memory status
572
+ const indicator = memoryToggle.querySelector(".memory-indicator") || document.createElement("div");
573
+ if (!memoryToggle.querySelector(".memory-indicator")) {
574
+ indicator.className = "memory-indicator";
575
+ memoryToggle.appendChild(indicator);
576
+ }
577
+ indicator.style.cssText = `
578
+ position: absolute;
579
+ top: -2px;
580
+ right: -2px;
581
+ width: 8px;
582
+ height: 8px;
583
+ border-radius: 50%;
584
+ background: ${enabled ? "#27ae60" : "#e74c3c"};
585
+ border: 2px solid white;
586
+ `;
587
+ }
588
+ } catch (error) {
589
+ console.error("Error updating memory stats:", error);
590
+ }
591
+ }
592
+
593
+ // Force refresh de l'interface (utile pour debug)
594
+ async forceRefresh() {
595
+ console.log("🔄 Force refresh de l'interface mémoire...");
596
+ try {
597
+ if (this.memorySystem) {
598
+ // Migrer les IDs si nécessaire
599
+ await this.memorySystem.migrateIncompatibleIDs();
600
+
601
+ // Recharger les mémoires
602
+ await this.loadMemories();
603
+ await this.updateMemoryStats();
604
+
605
+ console.log("✅ Refresh forcé terminé");
606
+ }
607
+ } catch (error) {
608
+ console.error("❌ Erreur lors du refresh forcé:", error);
609
+ }
610
+ }
611
+
612
+ showFeedback(message, type = "success") {
613
+ // Create feedback element
614
+ const feedback = document.createElement("div");
615
+ feedback.className = `memory-feedback memory-feedback-${type}`;
616
+ feedback.textContent = message;
617
+
618
+ // Style the feedback based on type
619
+ let backgroundColor;
620
+ switch (type) {
621
+ case "error":
622
+ backgroundColor = "#e74c3c";
623
+ break;
624
+ case "info":
625
+ backgroundColor = "#3498db";
626
+ break;
627
+ default:
628
+ backgroundColor = "#27ae60";
629
+ }
630
+
631
+ // Style the feedback
632
+ Object.assign(feedback.style, {
633
+ position: "fixed",
634
+ top: "20px",
635
+ right: "20px",
636
+ padding: "12px 20px",
637
+ borderRadius: "6px",
638
+ color: "white",
639
+ backgroundColor: backgroundColor,
640
+ boxShadow: "0 4px 12px rgba(0,0,0,0.2)",
641
+ zIndex: "10000",
642
+ fontSize: "14px",
643
+ fontWeight: "500",
644
+ opacity: "0",
645
+ transform: "translateX(100%)",
646
+ transition: "all 0.3s ease"
647
+ });
648
+
649
+ document.body.appendChild(feedback);
650
+
651
+ // Animate in
652
+ setTimeout(() => {
653
+ feedback.style.opacity = "1";
654
+ feedback.style.transform = "translateX(0)";
655
+ }, 10);
656
+
657
+ // Remove after delay (longer for info messages, shorter for others)
658
+ const delay = type === "info" ? 2000 : 3000;
659
+ setTimeout(() => {
660
+ feedback.style.opacity = "0";
661
+ feedback.style.transform = "translateX(100%)";
662
+ setTimeout(() => {
663
+ if (feedback.parentNode) {
664
+ feedback.parentNode.removeChild(feedback);
665
+ }
666
+ }, 300);
667
+ }, delay);
668
+ }
669
+ }
670
+
671
+ // Initialize memory UI when DOM is ready
672
+ document.addEventListener("DOMContentLoaded", async () => {
673
+ window.kimiMemoryUI = new KimiMemoryUI();
674
+
675
+ // Wait for memory system to be ready
676
+ const waitForMemorySystem = () => {
677
+ if (window.kimiMemorySystem) {
678
+ window.kimiMemoryUI.init();
679
+ } else {
680
+ setTimeout(waitForMemorySystem, 100);
681
+ }
682
+ };
683
+
684
+ setTimeout(waitForMemorySystem, 1000); // Give time for initialization
685
+ });
686
+
687
+ window.KimiMemoryUI = KimiMemoryUI;
kimi-js/kimi-memory.js ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ favoriteWords: [],
12
+ emotionalState: "neutral"
13
+ };
14
+ this.isReady = false;
15
+ // affectionTrait will be loaded from database during init()
16
+ this.affectionTrait = 50; // Temporary default until loaded from DB
17
+ }
18
+
19
+ async init() {
20
+ if (!this.db) {
21
+ console.warn("Database not available, using local mode");
22
+ return;
23
+ }
24
+ try {
25
+ this.selectedCharacter = await this.db.getSelectedCharacter();
26
+ // Start with lower favorability level - relationships must be built over time
27
+ this.favorabilityLevel = await this.db.getPreference(`favorabilityLevel_${this.selectedCharacter}`, 50);
28
+
29
+ // Load affection trait from personality database - CRITICAL FIX
30
+ this.affectionTrait = await this.db.getPersonalityTrait("affection", 50, this.selectedCharacter);
31
+
32
+ this.preferences = {
33
+ voiceRate: await this.db.getPreference(`voiceRate_${this.selectedCharacter}`, 1.1),
34
+ voicePitch: await this.db.getPreference(`voicePitch_${this.selectedCharacter}`, 1.1),
35
+ voiceVolume: await this.db.getPreference(`voiceVolume_${this.selectedCharacter}`, 0.8),
36
+ lastInteraction: await this.db.getPreference(`lastInteraction_${this.selectedCharacter}`, null),
37
+ totalInteractions: await this.db.getPreference(`totalInteractions_${this.selectedCharacter}`, 0),
38
+ favoriteWords: await this.db.getPreference(`favoriteWords_${this.selectedCharacter}`, []),
39
+ emotionalState: await this.db.getPreference(`emotionalState_${this.selectedCharacter}`, "neutral")
40
+ };
41
+ this.affectionTrait = await this.db.getPersonalityTrait("affection", 80, this.selectedCharacter);
42
+ this.isReady = true;
43
+ this.updateFavorabilityBar();
44
+ } catch (error) {
45
+ console.error("KimiMemory initialization error:", error);
46
+ }
47
+ }
48
+
49
+ async saveConversation(userText, kimiResponse) {
50
+ if (!this.db) return;
51
+
52
+ try {
53
+ const character = await this.db.getSelectedCharacter();
54
+ await this.db.saveConversation(userText, kimiResponse, this.favorabilityLevel, new Date(), character);
55
+
56
+ let total = await this.db.getPreference(`totalInteractions_${character}`, 0);
57
+ total = Number(total) + 1;
58
+ await this.db.setPreference(`totalInteractions_${character}`, total);
59
+ this.preferences.totalInteractions = total;
60
+
61
+ let first = await this.db.getPreference(`firstInteraction_${character}`, null);
62
+ if (!first) {
63
+ first = new Date().toISOString();
64
+ await this.db.setPreference(`firstInteraction_${character}`, first);
65
+ }
66
+
67
+ this.preferences.lastInteraction = new Date().toISOString();
68
+ await this.db.setPreference(`lastInteraction_${character}`, this.preferences.lastInteraction);
69
+ } catch (error) {
70
+ console.error("Error saving conversation:", error);
71
+ }
72
+ }
73
+
74
+ async updateFavorability(change) {
75
+ try {
76
+ this.affectionTrait = Math.max(0, Math.min(100, this.affectionTrait + change));
77
+ if (this.db) {
78
+ await this.db.setPersonalityTrait("affection", this.affectionTrait, this.selectedCharacter);
79
+ }
80
+ this.updateFavorabilityBar();
81
+ } catch (error) {
82
+ console.error("Error updating favorability:", error);
83
+ }
84
+ }
85
+
86
+ async updateAffectionTrait() {
87
+ if (!this.db) return;
88
+
89
+ try {
90
+ this.selectedCharacter = await this.db.getSelectedCharacter();
91
+ // Use unified default that matches KimiEmotionSystem
92
+ this.affectionTrait = await this.db.getPersonalityTrait("affection", 50, this.selectedCharacter);
93
+ this.updateFavorabilityBar();
94
+ } catch (error) {
95
+ console.error("Error updating affection trait:", error);
96
+ }
97
+ }
98
+
99
+ updateFavorabilityBar() {
100
+ try {
101
+ const favorabilityBar = document.getElementById("favorability-bar");
102
+ const favorabilityText = document.getElementById("favorability-text");
103
+ const value = Number(this.affectionTrait) || 0;
104
+ const clamped = Math.max(0, Math.min(100, value));
105
+
106
+ if (favorabilityBar) {
107
+ favorabilityBar.style.width = `${clamped}%`;
108
+ }
109
+ if (favorabilityText) {
110
+ favorabilityText.textContent = `${clamped.toFixed(2)}%`;
111
+ }
112
+ } catch (error) {
113
+ console.error("Error updating favorability bar:", error);
114
+ }
115
+ }
116
+
117
+ getGreeting() {
118
+ const i18n = window.kimiI18nManager;
119
+
120
+ if (this.affectionTrait <= 10) {
121
+ return i18n?.t("greeting_low") || "Hello.";
122
+ }
123
+ if (this.affectionTrait < 40) {
124
+ return i18n?.t("greeting_mid") || "Hi. How can I help you?";
125
+ }
126
+ return i18n?.t("greeting_high") || "Hello my love! 💕";
127
+ }
128
+ }
129
+
130
+ // Export to global scope
131
+ window.KimiMemory = KimiMemory;
kimi-js/kimi-module.js ADDED
@@ -0,0 +1,1956 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI MODULE SYSTEM =====
2
+ // This file contains the remaining utility classes and functions
3
+
4
+ // Note: KimiMemory and KimiAppearanceManager have been moved to separate files
5
+ // This file now contains the remaining utility classes and functions
6
+
7
+ class KimiDataManager extends KimiBaseManager {
8
+ constructor(database) {
9
+ super();
10
+ this.db = database;
11
+ }
12
+
13
+ async init() {
14
+ this.setupDataControls();
15
+ await this.updateStorageInfo();
16
+ }
17
+
18
+ setupDataControls() {
19
+ const exportButton = document.getElementById("export-data");
20
+ if (exportButton) {
21
+ exportButton.addEventListener("click", () => this.exportAllData());
22
+ }
23
+
24
+ const importButton = document.getElementById("import-data");
25
+ const importFile = document.getElementById("import-file");
26
+ if (importButton && importFile) {
27
+ importButton.addEventListener("click", () => importFile.click());
28
+ importFile.addEventListener("change", e => this.importData(e));
29
+ }
30
+
31
+ const cleanButton = document.getElementById("clean-old-data");
32
+ if (cleanButton) {
33
+ cleanButton.addEventListener("click", async () => {
34
+ if (!this.db) return;
35
+
36
+ const confirmClean = confirm(
37
+ "Delete all conversation messages?\n\n" +
38
+ "This will remove all chat history but keep your preferences and settings.\n\n" +
39
+ "This action cannot be undone."
40
+ );
41
+
42
+ if (!confirmClean) {
43
+ return;
44
+ }
45
+
46
+ try {
47
+ // Clear all conversations directly
48
+ await this.db.db.conversations.clear();
49
+
50
+ // Clear chat UI
51
+ const chatMessages = document.getElementById("chat-messages");
52
+ if (chatMessages) {
53
+ chatMessages.textContent = "";
54
+ }
55
+
56
+ // Reload chat history
57
+ if (typeof window.loadChatHistory === "function") {
58
+ window.loadChatHistory();
59
+ }
60
+
61
+ await this.updateStorageInfo();
62
+ alert("All conversation messages have been deleted successfully!");
63
+ } catch (error) {
64
+ console.error("Error cleaning conversations:", error);
65
+ alert("Error while cleaning conversations. Please try again.");
66
+ }
67
+ });
68
+ }
69
+
70
+ const resetButton = document.getElementById("reset-all-data");
71
+ if (resetButton) {
72
+ resetButton.addEventListener("click", () => this.resetAllData());
73
+ }
74
+ }
75
+
76
+ async exportAllData() {
77
+ if (!this.db) {
78
+ console.error("Database not available");
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const conversations = await this.db.getAllConversations();
84
+ const preferences = await this.db.getAllPreferences();
85
+ const personalityTraits = await this.db.getAllPersonalityTraits();
86
+ const models = await this.db.getAllLLMModels();
87
+
88
+ const exportData = {
89
+ version: "1.0",
90
+ exportDate: new Date().toISOString(),
91
+ conversations: conversations,
92
+ preferences: preferences,
93
+ personalityTraits: personalityTraits,
94
+ models: models,
95
+ metadata: {
96
+ totalConversations: conversations.length,
97
+ totalPreferences: Object.keys(preferences).length,
98
+ totalTraits: Object.keys(personalityTraits).length,
99
+ totalModels: models.length
100
+ }
101
+ };
102
+
103
+ const dataStr = JSON.stringify(exportData, null, 2);
104
+ const dataBlob = new Blob([dataStr], { type: "application/json" });
105
+
106
+ const url = URL.createObjectURL(dataBlob);
107
+ const a = document.createElement("a");
108
+ a.href = url;
109
+ a.download = `kimi-backup-${new Date().toISOString().split("T")[0]}.json`;
110
+ document.body.appendChild(a);
111
+ a.click();
112
+ document.body.removeChild(a);
113
+ URL.revokeObjectURL(url);
114
+ } catch (error) {
115
+ console.error("Error during export:", error);
116
+ }
117
+ }
118
+
119
+ async cleanOldData() {
120
+ if (!this.db) {
121
+ console.error("Database not available");
122
+ return;
123
+ }
124
+
125
+ const confirmClean = confirm("Do you want to delete ALL conversations?\n\nThis action is irreversible!");
126
+
127
+ if (!confirmClean) {
128
+ return;
129
+ }
130
+
131
+ try {
132
+ // Clear all conversations directly
133
+ await this.db.db.conversations.clear();
134
+
135
+ if (typeof window.loadChatHistory === "function") {
136
+ window.loadChatHistory();
137
+ }
138
+ const chatMessages = document.getElementById("chat-messages");
139
+ if (chatMessages) {
140
+ chatMessages.textContent = "";
141
+ }
142
+
143
+ await this.updateStorageInfo();
144
+ } catch (error) {
145
+ console.error("Error during cleaning:", error);
146
+ }
147
+ }
148
+
149
+ async resetAllData() {
150
+ if (!this.db) {
151
+ console.error("Database not available");
152
+ return;
153
+ }
154
+
155
+ const confirmReset = confirm(
156
+ "WARNING!\n\n" +
157
+ "Do you REALLY want to delete ALL data?\n\n" +
158
+ "• All conversations\n" +
159
+ "• All preferences\n" +
160
+ "• All configured models\n" +
161
+ "• All personality traits\n\n" +
162
+ "This action is IRREVERSIBLE!"
163
+ );
164
+
165
+ if (!confirmReset) {
166
+ return;
167
+ }
168
+
169
+ try {
170
+ if (this.db.db) {
171
+ this.db.db.close();
172
+ }
173
+
174
+ const deleteRequest = indexedDB.deleteDatabase(this.db.dbName);
175
+
176
+ deleteRequest.onsuccess = () => {
177
+ setTimeout(() => {
178
+ alert("The page will reload to complete the reset.");
179
+ location.reload();
180
+ }, 500);
181
+ };
182
+
183
+ deleteRequest.onerror = () => {
184
+ alert("Error while deleting the database. Please try again.");
185
+ };
186
+ } catch (error) {
187
+ console.error("Error during reset:", error);
188
+ alert("Error during reset. Please try again.");
189
+ }
190
+ }
191
+
192
+ async updateStorageInfo() {
193
+ if (!this.db) return;
194
+
195
+ try {
196
+ // Add a small delay to ensure database operations are complete
197
+ await new Promise(resolve => setTimeout(resolve, 100));
198
+
199
+ const stats = await this.db.getStorageStats();
200
+
201
+ const dbSizeEl = document.getElementById("db-size");
202
+ const storageUsedEl = document.getElementById("storage-used");
203
+
204
+ if (dbSizeEl) {
205
+ dbSizeEl.textContent = this.formatFileSize(stats.totalSize || 0);
206
+ }
207
+
208
+ if (storageUsedEl) {
209
+ const estimate = navigator.storage && navigator.storage.estimate ? await navigator.storage.estimate() : null;
210
+
211
+ if (estimate) {
212
+ storageUsedEl.textContent = this.formatFileSize(estimate.usage || 0);
213
+ } else {
214
+ storageUsedEl.textContent = "N/A";
215
+ }
216
+ }
217
+ } catch (error) {
218
+ console.error("Error while calculating storage:", error);
219
+
220
+ const dbSizeEl = document.getElementById("db-size");
221
+ const storageUsedEl = document.getElementById("storage-used");
222
+
223
+ if (dbSizeEl) dbSizeEl.textContent = "Error";
224
+ if (storageUsedEl) storageUsedEl.textContent = "Error";
225
+ }
226
+ }
227
+ }
228
+
229
+ // Fonctions utilitaires et logique (référencent window.*)
230
+ function getPersonalityAverage(traits) {
231
+ if (window.kimiEmotionSystem && typeof window.kimiEmotionSystem.calculatePersonalityAverage === "function") {
232
+ return window.kimiEmotionSystem.calculatePersonalityAverage(traits);
233
+ }
234
+ if (window.getPersonalityAverage) {
235
+ try {
236
+ return window.getPersonalityAverage(traits);
237
+ } catch {}
238
+ }
239
+ const keys = ["affection", "romance", "empathy", "playfulness", "humor"];
240
+ let sum = 0;
241
+ let count = 0;
242
+ keys.forEach(key => {
243
+ if (typeof traits[key] === "number") {
244
+ sum += traits[key];
245
+ count++;
246
+ }
247
+ });
248
+ return count > 0 ? sum / count : 50;
249
+ }
250
+
251
+ function updateFavorabilityLabel(characterKey) {
252
+ const favorabilityLabel = document.getElementById("favorability-label");
253
+ if (favorabilityLabel && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[characterKey]) {
254
+ favorabilityLabel.setAttribute("data-i18n", "affection_level_of");
255
+ favorabilityLabel.setAttribute("data-i18n-params", JSON.stringify({ name: window.KIMI_CHARACTERS[characterKey].name }));
256
+ favorabilityLabel.textContent = `💖 Affection level of ${window.KIMI_CHARACTERS[characterKey].name}`;
257
+ applyTranslations();
258
+ }
259
+ }
260
+
261
+ async function loadCharacterSection() {
262
+ const kimiDB = window.kimiDB;
263
+ if (!kimiDB) return;
264
+ const characterGrid = document.getElementById("character-grid");
265
+ if (!characterGrid) return;
266
+ while (characterGrid.firstChild) {
267
+ characterGrid.removeChild(characterGrid.firstChild);
268
+ }
269
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
270
+ for (const [key, info] of Object.entries(window.KIMI_CHARACTERS)) {
271
+ const card = document.createElement("div");
272
+ card.className = `character-card${key === selectedCharacter ? " selected" : ""}`;
273
+ card.dataset.character = key;
274
+
275
+ // Create character card elements safely
276
+ const img = document.createElement("img");
277
+ img.src = info.image;
278
+ img.alt = info.name;
279
+
280
+ const infoDiv = document.createElement("div");
281
+ infoDiv.className = "character-info";
282
+
283
+ const nameDiv = document.createElement("div");
284
+ nameDiv.className = "character-name";
285
+ nameDiv.textContent = info.name;
286
+
287
+ const detailsDiv = document.createElement("div");
288
+ detailsDiv.className = "character-details";
289
+
290
+ const ageDiv = document.createElement("div");
291
+ ageDiv.className = "character-age";
292
+ ageDiv.setAttribute("data-i18n", "character_age");
293
+ ageDiv.setAttribute("data-i18n-params", JSON.stringify({ age: info.age }));
294
+
295
+ const birthplaceDiv = document.createElement("div");
296
+ birthplaceDiv.className = "character-birthplace";
297
+ birthplaceDiv.setAttribute("data-i18n", "character_birthplace");
298
+ birthplaceDiv.setAttribute("data-i18n-params", JSON.stringify({ birthplace: info.birthplace }));
299
+
300
+ const summaryDiv = document.createElement("div");
301
+ summaryDiv.className = "character-summary";
302
+ summaryDiv.setAttribute("data-i18n", `character_summary_${key}`);
303
+
304
+ detailsDiv.appendChild(ageDiv);
305
+ detailsDiv.appendChild(birthplaceDiv);
306
+ detailsDiv.appendChild(summaryDiv);
307
+
308
+ infoDiv.appendChild(nameDiv);
309
+ infoDiv.appendChild(detailsDiv);
310
+
311
+ const promptLabel = document.createElement("div");
312
+ promptLabel.className = "character-prompt-label";
313
+ promptLabel.setAttribute("data-i18n", "system_prompt");
314
+ promptLabel.textContent = "System Prompt";
315
+
316
+ const promptInput = document.createElement("textarea");
317
+ promptInput.className = "character-prompt-input";
318
+ promptInput.id = `prompt-${key}`;
319
+ promptInput.rows = 6;
320
+
321
+ card.appendChild(img);
322
+ card.appendChild(infoDiv);
323
+ card.appendChild(promptLabel);
324
+ card.appendChild(promptInput);
325
+ characterGrid.appendChild(card);
326
+ }
327
+ applyTranslations();
328
+ for (const key of Object.keys(window.KIMI_CHARACTERS)) {
329
+ const promptInput = document.getElementById(`prompt-${key}`);
330
+ if (promptInput) {
331
+ const prompt = await kimiDB.getSystemPromptForCharacter(key);
332
+ promptInput.value = prompt;
333
+ promptInput.disabled = key !== selectedCharacter;
334
+ }
335
+ }
336
+ characterGrid.querySelectorAll(".character-card").forEach(card => {
337
+ card.addEventListener("click", async () => {
338
+ characterGrid.querySelectorAll(".character-card").forEach(c => c.classList.remove("selected"));
339
+ card.classList.add("selected");
340
+ const charKey = card.dataset.character;
341
+ for (const key of Object.keys(window.KIMI_CHARACTERS)) {
342
+ const promptInput = document.getElementById(`prompt-${key}`);
343
+ if (promptInput) promptInput.disabled = key !== charKey;
344
+ }
345
+ updateFavorabilityLabel(charKey);
346
+ const chatHeaderName = document.querySelector(".chat-header span[data-i18n]");
347
+ if (chatHeaderName) {
348
+ const info = window.KIMI_CHARACTERS[charKey] || window.KIMI_CHARACTERS.kimi;
349
+ chatHeaderName.setAttribute("data-i18n", `chat_with_${charKey}`);
350
+ applyTranslations();
351
+ }
352
+
353
+ // Update personality trait sliders with selected character's traits
354
+ await updatePersonalitySliders(charKey);
355
+ });
356
+ });
357
+
358
+ // Initialize personality sliders with current selected character's traits
359
+ await updatePersonalitySliders(selectedCharacter);
360
+ }
361
+
362
+ async function getBasicResponse(reaction) {
363
+ // Use centralized fallback manager instead of duplicated logic
364
+ if (window.KimiFallbackManager) {
365
+ return await window.KimiFallbackManager.getEmotionalResponse(reaction);
366
+ }
367
+
368
+ // Fallback to legacy system if KimiFallbackManager not available
369
+ const i18n = window.kimiI18nManager;
370
+ return i18n ? i18n.t("fallback_technical_error") : "Sorry, I'm having technical difficulties! 💕";
371
+ }
372
+
373
+ async function updatePersonalityTraitsFromEmotion(emotion, text) {
374
+ const kimiDB = window.kimiDB;
375
+ const kimiMemory = window.kimiMemory;
376
+ if (!kimiDB) {
377
+ console.warn("KimiDB not available for personality updates");
378
+ return;
379
+ }
380
+
381
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
382
+ const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
383
+
384
+ // Use unified emotion system defaults - CRITICAL FIX
385
+ const getUnifiedDefaults = () => {
386
+ if (window.KimiEmotionSystem) {
387
+ const emotionSystem = new window.KimiEmotionSystem(kimiDB);
388
+ return emotionSystem.TRAIT_DEFAULTS;
389
+ }
390
+ return { affection: 65, romance: 50, empathy: 75, playfulness: 55, humor: 60, intelligence: 70 };
391
+ };
392
+
393
+ const defaults = getUnifiedDefaults();
394
+ let affection = traits.affection || defaults.affection;
395
+ let romance = traits.romance || defaults.romance;
396
+ let empathy = traits.empathy || defaults.empathy;
397
+ let playfulness = traits.playfulness || defaults.playfulness;
398
+ let humor = traits.humor || defaults.humor;
399
+ let intelligence = traits.intelligence || defaults.intelligence;
400
+
401
+ function adjustUp(val, amount) {
402
+ if (val >= 90) return val + amount * 0.2;
403
+ if (val >= 75) return val + amount * 0.5;
404
+ if (val >= 50) return val + amount * 0.8;
405
+ return val + amount;
406
+ }
407
+ function adjustDown(val, amount) {
408
+ if (val <= 10) return val - amount * 0.2;
409
+ if (val <= 25) return val - amount * 0.5;
410
+ if (val <= 50) return val - amount * 0.8;
411
+ return val - amount;
412
+ }
413
+
414
+ // Emotion-based adjustments - More realistic progression
415
+ switch (emotion) {
416
+ case "positive":
417
+ affection = Math.min(100, adjustUp(affection, 0.3));
418
+ empathy = Math.min(100, adjustUp(empathy, 0.2));
419
+ playfulness = Math.min(100, adjustUp(playfulness, 0.2));
420
+ humor = Math.min(100, adjustUp(humor, 0.2));
421
+ romance = Math.min(100, adjustUp(romance, 0.2));
422
+ break;
423
+ case "negative":
424
+ affection = Math.max(0, adjustDown(affection, 0.4));
425
+ empathy = Math.min(100, adjustUp(empathy, 0.3)); // Empathy increases with negative emotions
426
+ break;
427
+ case "romantic":
428
+ romance = Math.min(100, adjustUp(romance, 0.8));
429
+ affection = Math.min(100, adjustUp(affection, 0.4));
430
+ break;
431
+ case "laughing":
432
+ humor = Math.min(100, adjustUp(humor, 0.7));
433
+ playfulness = Math.min(100, adjustUp(playfulness, 0.3));
434
+ break;
435
+ case "dancing":
436
+ playfulness = Math.min(100, adjustUp(playfulness, 1.0));
437
+ break;
438
+ case "shy":
439
+ affection = Math.max(0, adjustDown(affection, 0.2));
440
+ break;
441
+ case "confident":
442
+ affection = Math.min(100, adjustUp(affection, 0.4));
443
+ break;
444
+ case "flirtatious":
445
+ romance = Math.min(100, adjustUp(romance, 0.6));
446
+ playfulness = Math.min(100, adjustUp(playfulness, 0.4));
447
+ break;
448
+ }
449
+
450
+ // Use user's selected language from preferences
451
+ const selectedLanguage = await kimiDB.getPreference("selectedLanguage", "en");
452
+
453
+ const getRomanticWords = () => {
454
+ return (
455
+ window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.romantic ||
456
+ window.KIMI_CONTEXT_KEYWORDS?.en?.romantic || [
457
+ "love",
458
+ "romantic",
459
+ "kiss",
460
+ "cuddle",
461
+ "hug",
462
+ "dear",
463
+ "honey",
464
+ "sweetheart"
465
+ ]
466
+ );
467
+ };
468
+
469
+ const getHumorWords = () => {
470
+ return (
471
+ window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.laughing ||
472
+ window.KIMI_CONTEXT_KEYWORDS?.en?.laughing || ["joke", "funny", "lol", "haha", "hilarious"]
473
+ );
474
+ };
475
+
476
+ const romanticWords = getRomanticWords();
477
+ const humorWords = getHumorWords();
478
+
479
+ // Check for romantic content
480
+ const romanticPattern = new RegExp(`(${romanticWords.join("|")})`, "i");
481
+ if (text.match(romanticPattern)) {
482
+ romance = Math.min(100, adjustUp(romance, 0.5));
483
+ affection = Math.min(100, adjustUp(affection, 0.5));
484
+ }
485
+
486
+ // Check for humor content
487
+ const humorPattern = new RegExp(`(${humorWords.join("|")})`, "i");
488
+ if (text.match(humorPattern)) {
489
+ humor = Math.min(100, adjustUp(humor, 2));
490
+ playfulness = Math.min(100, adjustUp(playfulness, 1));
491
+ }
492
+
493
+ const updatedTraits = {
494
+ affection: Math.round(affection),
495
+ romance: Math.round(romance),
496
+ empathy: Math.round(empathy),
497
+ playfulness: Math.round(playfulness),
498
+ humor: Math.round(humor),
499
+ intelligence: Math.round(intelligence)
500
+ };
501
+
502
+ // Single batch operation instead of 6 individual saves
503
+ await kimiDB.setPersonalityBatch(updatedTraits, selectedCharacter);
504
+
505
+ if (kimiMemory) {
506
+ kimiMemory.affectionTrait = affection;
507
+ if (kimiMemory.updateFavorabilityBar) {
508
+ kimiMemory.updateFavorabilityBar();
509
+ }
510
+ }
511
+
512
+ // Update all sliders at once for better UX
513
+ updateSlider("trait-affection", affection);
514
+ updateSlider("trait-romance", romance);
515
+ updateSlider("trait-empathy", empathy);
516
+ updateSlider("trait-playfulness", playfulness);
517
+ updateSlider("trait-humor", humor);
518
+ updateSlider("trait-intelligence", intelligence);
519
+
520
+ // Update video context based on new personality
521
+ if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
522
+ window.kimiVideo.setMoodByPersonality(updatedTraits);
523
+ }
524
+
525
+ // Notify other systems about trait changes
526
+ if (window.dispatchEvent) {
527
+ window.dispatchEvent(
528
+ new CustomEvent("personalityUpdated", {
529
+ detail: { character: selectedCharacter, traits: updatedTraits }
530
+ })
531
+ );
532
+ }
533
+ }
534
+
535
+ async function analyzeAndReact(text, useAdvancedLLM = true) {
536
+ const kimiDB = window.kimiDB;
537
+ const kimiLLM = window.kimiLLM;
538
+ const kimiVideo = window.kimiVideo;
539
+ const kimiMemory = window.kimiMemory;
540
+ const isSystemReady = window.isSystemReady;
541
+
542
+ try {
543
+ // Validate and sanitize input
544
+ if (!text || typeof text !== "string") {
545
+ throw new Error("Invalid input text");
546
+ }
547
+
548
+ const sanitizedText = window.KimiSecurityUtils?.sanitizeInput(text) || text.trim();
549
+ if (!sanitizedText) {
550
+ throw new Error("Empty input after sanitization");
551
+ }
552
+
553
+ const lowerText = sanitizedText.toLowerCase();
554
+ let reaction = window.kimiAnalyzeEmotion(sanitizedText, "auto");
555
+ let emotionIntensity = 0;
556
+ let response;
557
+
558
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
559
+ const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
560
+ const avg = getPersonalityAverage(traits);
561
+ const affection = typeof traits.affection === "number" ? traits.affection : 80;
562
+ const characterTraits = window.KIMI_CHARACTERS[selectedCharacter]?.traits || "";
563
+
564
+ // Always reflect user's input phase with a listening video (voice or chat)
565
+ if (kimiVideo && typeof kimiVideo.startListening === "function") {
566
+ kimiVideo.startListening();
567
+ }
568
+
569
+ await updatePersonalityTraitsFromEmotion(reaction, sanitizedText);
570
+
571
+ if (useAdvancedLLM && isSystemReady && kimiLLM) {
572
+ try {
573
+ const providerPref = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
574
+ const apiKey = kimiDB
575
+ ? await kimiDB.getPreference(providerPref === "openrouter" ? "openrouterApiKey" : "llmApiKey")
576
+ : null;
577
+
578
+ if (apiKey && apiKey.trim() !== "") {
579
+ try {
580
+ if (window.dispatchEvent) {
581
+ window.dispatchEvent(new CustomEvent("chat:typing:start"));
582
+ }
583
+ } catch (e) {}
584
+ response = await kimiLLM.chat(sanitizedText);
585
+ try {
586
+ if (window.dispatchEvent) {
587
+ window.dispatchEvent(new CustomEvent("chat:typing:stop"));
588
+ }
589
+ } catch (e) {}
590
+
591
+ // Extract memories from conversation
592
+ if (window.kimiMemorySystem) {
593
+ await window.kimiMemorySystem.extractMemoryFromText(sanitizedText, response);
594
+ }
595
+
596
+ const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
597
+ // If user explicitly requested dancing, show dancing during Kimi's response
598
+ const lang = await kimiDB.getPreference("selectedLanguage", "en");
599
+ const keywords =
600
+ (window.KIMI_CONTEXT_KEYWORDS &&
601
+ (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) ||
602
+ {};
603
+ const dancingWords = keywords.dancing || ["dance", "dancing"];
604
+ const userAskedDance = dancingWords.some(w => sanitizedText.toLowerCase().includes(w.toLowerCase()));
605
+
606
+ if (userAskedDance) {
607
+ kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
608
+ } else {
609
+ kimiVideo.analyzeAndSelectVideo(
610
+ sanitizedText,
611
+ response,
612
+ { reaction: reaction, intensity: emotionIntensity },
613
+ updatedTraits,
614
+ updatedTraits.affection
615
+ );
616
+ }
617
+
618
+ if (kimiLLM.updatePersonalityFromResponse) {
619
+ await kimiLLM.updatePersonalityFromResponse(sanitizedText, response);
620
+ const selectedCharacter2 = await kimiDB.getSelectedCharacter();
621
+ const traits2 = await kimiDB.getAllPersonalityTraits(selectedCharacter2);
622
+ if (kimiVideo && kimiVideo.setMoodByPersonality) {
623
+ kimiVideo.setMoodByPersonality(traits2);
624
+ }
625
+ }
626
+ } else {
627
+ // No API key configured - use centralized fallback
628
+ response = window.KimiFallbackManager
629
+ ? window.KimiFallbackManager.getFallbackMessage("api_missing")
630
+ : "To chat with me, add your API key in settings! 💕";
631
+ const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
632
+ kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection);
633
+ }
634
+ } catch (error) {
635
+ console.warn("LLM not available:", error.message);
636
+ try {
637
+ if (window.dispatchEvent) {
638
+ window.dispatchEvent(new CustomEvent("chat:typing:stop"));
639
+ }
640
+ } catch (e) {}
641
+ // Still show API key message if no key is configured
642
+ const providerPref2 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
643
+ const apiKey = kimiDB
644
+ ? await kimiDB.getPreference(providerPref2 === "openrouter" ? "openrouterApiKey" : "llmApiKey")
645
+ : null;
646
+ if (!apiKey || apiKey.trim() === "") {
647
+ response = window.KimiFallbackManager
648
+ ? window.KimiFallbackManager.getFallbackMessage("api_missing")
649
+ : "To chat with me, add your API key in settings! 💕";
650
+ } else {
651
+ response = await getBasicResponse(reaction);
652
+ }
653
+ const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
654
+ const lang = await kimiDB.getPreference("selectedLanguage", "en");
655
+ const keywords =
656
+ (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) ||
657
+ {};
658
+ const dancingWords = keywords.dancing || ["dance", "dancing"];
659
+ const userAskedDance = dancingWords.some(w => sanitizedText.toLowerCase().includes(w.toLowerCase()));
660
+ if (userAskedDance) {
661
+ kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
662
+ } else {
663
+ kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection);
664
+ }
665
+ }
666
+ } else {
667
+ // System not ready - check if it's because of missing API key
668
+ const providerPref3 = kimiDB ? await kimiDB.getPreference("llmProvider", "openrouter") : "openrouter";
669
+ const apiKey = kimiDB
670
+ ? await kimiDB.getPreference(providerPref3 === "openrouter" ? "openrouterApiKey" : "llmApiKey")
671
+ : null;
672
+ if (!apiKey || apiKey.trim() === "") {
673
+ response = window.KimiFallbackManager
674
+ ? window.KimiFallbackManager.getFallbackMessage("api_missing")
675
+ : "To chat with me, add your API key in settings! 💕";
676
+ } else {
677
+ response = await getBasicResponse(reaction);
678
+ }
679
+ const updatedTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
680
+ const lang = await kimiDB.getPreference("selectedLanguage", "en");
681
+ const keywords =
682
+ (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {};
683
+ const dancingWords = keywords.dancing || ["dance", "dancing"];
684
+ const userAskedDance = dancingWords.some(w => sanitizedText.toLowerCase().includes(w.toLowerCase()));
685
+ if (userAskedDance) {
686
+ kimiVideo.switchToContext("dancing", "dancing", null, updatedTraits, updatedTraits.affection);
687
+ } else {
688
+ kimiVideo.respondWithEmotion("neutral", updatedTraits, updatedTraits.affection);
689
+ }
690
+ }
691
+
692
+ await kimiMemory.saveConversation(sanitizedText, response);
693
+
694
+ // Extract memories automatically from conversation if system is enabled
695
+ if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) {
696
+ try {
697
+ const extractedMemories = await window.kimiMemorySystem.extractMemoryFromText(sanitizedText, response);
698
+ if (extractedMemories && extractedMemories.length > 0) {
699
+ // Update memory stats in UI
700
+ if (window.kimiMemoryUI && window.kimiMemoryUI.isInitialized) {
701
+ await window.kimiMemoryUI.updateMemoryStats();
702
+ // Show subtle notification for extracted memories
703
+ window.kimiMemoryUI.showFeedback(
704
+ `💭 ${extractedMemories.length} new ${extractedMemories.length === 1 ? "memory" : "memories"} learned`,
705
+ "info"
706
+ );
707
+ }
708
+ }
709
+ } catch (error) {
710
+ console.warn("Memory extraction error:", error);
711
+ }
712
+ }
713
+
714
+ return response;
715
+ } catch (error) {
716
+ console.error("Error in analyzeAndReact:", error);
717
+
718
+ // Use centralized fallback response
719
+ const fallbackResponse = window.KimiFallbackManager
720
+ ? window.KimiFallbackManager.getFallbackMessage("technical_error")
721
+ : "I'm sorry, I encountered an issue processing your message. Please try again.";
722
+
723
+ try {
724
+ // Attempt to save the error for debugging while still providing user feedback
725
+ if (kimiMemory && kimiMemory.saveConversation) {
726
+ await kimiMemory.saveConversation(text || "Error", fallbackResponse);
727
+ }
728
+ } catch (saveError) {
729
+ console.error("Failed to save error conversation:", saveError);
730
+ }
731
+
732
+ return fallbackResponse;
733
+ }
734
+ }
735
+
736
+ function addMessageToChat(sender, text, conversationId = null) {
737
+ const chatMessages = document.getElementById("chat-messages");
738
+ if (!text) return;
739
+ const messageDiv = document.createElement("div");
740
+ messageDiv.className = `message ${sender}`;
741
+
742
+ const time = new Date().toLocaleTimeString("en-US", {
743
+ hour: "2-digit",
744
+ minute: "2-digit"
745
+ });
746
+
747
+ const messageTimeDiv = document.createElement("div");
748
+ messageTimeDiv.className = "message-time";
749
+ messageTimeDiv.style.display = "flex";
750
+ messageTimeDiv.style.justifyContent = "space-between";
751
+ messageTimeDiv.style.alignItems = "center";
752
+
753
+ const timeSpan = document.createElement("span");
754
+ timeSpan.textContent = time;
755
+ timeSpan.style.flex = "1";
756
+
757
+ const deleteBtn = document.createElement("button");
758
+ deleteBtn.className = "delete-message-btn";
759
+ const icon = document.createElement("i");
760
+ icon.className = "fas fa-trash";
761
+ deleteBtn.appendChild(icon);
762
+ deleteBtn.style.background = "none";
763
+ deleteBtn.style.border = "none";
764
+ deleteBtn.style.cursor = "pointer";
765
+ deleteBtn.style.color = "#aaa";
766
+ deleteBtn.style.fontSize = "1em";
767
+ deleteBtn.style.marginLeft = "8px";
768
+ deleteBtn.setAttribute("aria-label", "Delete message");
769
+ deleteBtn.addEventListener("click", async function (e) {
770
+ e.stopPropagation();
771
+ messageDiv.remove();
772
+ if (conversationId && window.kimiDB && window.kimiDB.deleteSingleMessage) {
773
+ await window.kimiDB.deleteSingleMessage(conversationId, sender);
774
+ }
775
+ });
776
+
777
+ messageTimeDiv.appendChild(timeSpan);
778
+ messageTimeDiv.appendChild(deleteBtn);
779
+
780
+ const textDiv = document.createElement("div");
781
+ textDiv.textContent = text;
782
+
783
+ messageDiv.appendChild(textDiv);
784
+ messageDiv.appendChild(messageTimeDiv);
785
+
786
+ chatMessages.appendChild(messageDiv);
787
+ chatMessages.scrollTop = chatMessages.scrollHeight;
788
+ }
789
+
790
+ async function loadChatHistory() {
791
+ const kimiDB = window.kimiDB;
792
+ const kimiMemory = window.kimiMemory;
793
+ const chatMessages = document.getElementById("chat-messages");
794
+
795
+ while (chatMessages.firstChild) {
796
+ chatMessages.removeChild(chatMessages.firstChild);
797
+ }
798
+
799
+ if (kimiDB) {
800
+ try {
801
+ const recent = await kimiDB.getRecentConversations(10);
802
+
803
+ if (recent.length === 0) {
804
+ const greeting = kimiMemory.getGreeting();
805
+ addMessageToChat("kimi", greeting);
806
+ } else {
807
+ recent.forEach(conv => {
808
+ addMessageToChat("user", conv.user, conv.id);
809
+ addMessageToChat("kimi", conv.kimi, conv.id);
810
+ });
811
+ }
812
+ } catch (error) {
813
+ console.error("Error while loading history:", error);
814
+ const greeting = kimiMemory.getGreeting();
815
+ addMessageToChat("kimi", greeting);
816
+ }
817
+ } else {
818
+ const greeting = kimiMemory.getGreeting();
819
+ addMessageToChat("kimi", greeting);
820
+ }
821
+ }
822
+
823
+ async function loadSettingsData() {
824
+ const kimiDB = window.kimiDB;
825
+ const kimiLLM = window.kimiLLM;
826
+ if (!kimiDB) return;
827
+ try {
828
+ // Batch load preferences for better performance
829
+ const preferenceKeys = [
830
+ "voiceRate",
831
+ "voicePitch",
832
+ "voiceVolume",
833
+ "selectedLanguage",
834
+ "openrouterApiKey",
835
+ "llmProvider",
836
+ "llmBaseUrl",
837
+ "llmModelId",
838
+ "llmApiKey",
839
+ "selectedCharacter",
840
+ "llmTemperature",
841
+ "llmMaxTokens",
842
+ "llmTopP",
843
+ "llmFrequencyPenalty",
844
+ "llmPresencePenalty"
845
+ ];
846
+ const preferences = await kimiDB.getPreferencesBatch(preferenceKeys);
847
+
848
+ // Set default values for missing preferences
849
+ const voiceRate = preferences.voiceRate !== undefined ? preferences.voiceRate : 1.1;
850
+ const voicePitch = preferences.voicePitch !== undefined ? preferences.voicePitch : 1.1;
851
+ const voiceVolume = preferences.voiceVolume !== undefined ? preferences.voiceVolume : 0.8;
852
+ const selectedLanguage = preferences.selectedLanguage || "en";
853
+ const apiKey = preferences.openrouterApiKey || "";
854
+ const provider = preferences.llmProvider || "openrouter";
855
+ const baseUrl = preferences.llmBaseUrl || "https://openrouter.ai/api/v1/chat/completions";
856
+ const modelId = preferences.llmModelId || (window.kimiLLM ? window.kimiLLM.currentModel : "");
857
+ const genericKey = preferences.llmApiKey || "";
858
+ const selectedCharacter = preferences.selectedCharacter || "kimi";
859
+ const llmTemperature = preferences.llmTemperature !== undefined ? preferences.llmTemperature : 0.9;
860
+ const llmMaxTokens = preferences.llmMaxTokens !== undefined ? preferences.llmMaxTokens : 100;
861
+ const llmTopP = preferences.llmTopP !== undefined ? preferences.llmTopP : 0.9;
862
+ const llmFrequencyPenalty = preferences.llmFrequencyPenalty !== undefined ? preferences.llmFrequencyPenalty : 0.3;
863
+ const llmPresencePenalty = preferences.llmPresencePenalty !== undefined ? preferences.llmPresencePenalty : 0.3;
864
+
865
+ // Update UI with voice settings
866
+ const languageSelect = document.getElementById("language-selection");
867
+ if (languageSelect) languageSelect.value = selectedLanguage;
868
+ updateSlider("voice-rate", voiceRate);
869
+ updateSlider("voice-pitch", voicePitch);
870
+ updateSlider("voice-volume", voiceVolume);
871
+
872
+ // Update LLM settings
873
+ updateSlider("llm-temperature", llmTemperature);
874
+ updateSlider("llm-max-tokens", llmMaxTokens);
875
+ updateSlider("llm-top-p", llmTopP);
876
+ updateSlider("llm-frequency-penalty", llmFrequencyPenalty);
877
+ updateSlider("llm-presence-penalty", llmPresencePenalty);
878
+
879
+ // Batch load personality traits
880
+ const traitNames = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"];
881
+ const personality = await kimiDB.getPersonalityTraitsBatch(traitNames, selectedCharacter);
882
+ const defaults = [80, 70, 85, 90, 75, 95];
883
+
884
+ traitNames.forEach((trait, index) => {
885
+ const value = typeof personality[trait] === "number" ? personality[trait] : defaults[index];
886
+ updateSlider(`trait-${trait}`, value);
887
+
888
+ // Update memory cache for affection
889
+ if (trait === "affection" && window.kimiMemory) {
890
+ window.kimiMemory.affectionTrait = value;
891
+ }
892
+ });
893
+
894
+ // Sync personality traits to ensure consistency
895
+ await syncPersonalityTraits(selectedCharacter);
896
+
897
+ await updateStats();
898
+
899
+ // Update API key input
900
+ const apiKeyInput = document.getElementById("openrouter-api-key");
901
+ if (apiKeyInput) apiKeyInput.value = apiKey;
902
+ const providerSelect = document.getElementById("llm-provider");
903
+ if (providerSelect) providerSelect.value = provider;
904
+ const baseUrlInput = document.getElementById("llm-base-url");
905
+ if (baseUrlInput) baseUrlInput.value = baseUrl;
906
+ const modelIdInput = document.getElementById("llm-model-id");
907
+ if (modelIdInput) modelIdInput.value = modelId;
908
+ if (provider !== "openrouter" && apiKeyInput) apiKeyInput.value = genericKey;
909
+ const apiKeyLabel = document.getElementById("api-key-label");
910
+ if (apiKeyLabel) {
911
+ const labelByProvider = {
912
+ openrouter: "OpenRouter API Key",
913
+ openai: "OpenAI API Key",
914
+ groq: "Groq API Key",
915
+ together: "Together API Key",
916
+ deepseek: "DeepSeek API Key",
917
+ "openai-compatible": "API Key",
918
+ ollama: "API Key"
919
+ };
920
+ apiKeyLabel.textContent = labelByProvider[provider] || "API Key";
921
+ }
922
+
923
+ // Load system prompt
924
+ let systemPrompt = DEFAULT_SYSTEM_PROMPT;
925
+ if (kimiDB.getSystemPromptForCharacter) {
926
+ systemPrompt = await kimiDB.getSystemPromptForCharacter(selectedCharacter);
927
+ }
928
+ const systemPromptInput = document.getElementById("system-prompt");
929
+ if (systemPromptInput) systemPromptInput.value = systemPrompt;
930
+ if (kimiLLM && kimiLLM.setSystemPrompt) kimiLLM.setSystemPrompt(systemPrompt);
931
+
932
+ loadAvailableModels();
933
+ } catch (error) {
934
+ console.error("Error while loading settings:", error);
935
+ }
936
+ }
937
+
938
+ function updateSlider(id, value) {
939
+ const slider = document.getElementById(id);
940
+ const valueSpan = document.getElementById(`${id}-value`);
941
+ if (slider && valueSpan) {
942
+ slider.value = value;
943
+ valueSpan.textContent = value;
944
+ }
945
+ }
946
+
947
+ async function updatePersonalitySliders(characterKey) {
948
+ const kimiDB = window.kimiDB;
949
+ if (!kimiDB) return;
950
+
951
+ try {
952
+ // Get current traits from database for this character
953
+ const savedTraits = await kimiDB.getAllPersonalityTraits(characterKey);
954
+
955
+ // Get default traits from KIMI_CHARACTERS constants
956
+ const characterDefaults = window.KIMI_CHARACTERS[characterKey]?.traits || {};
957
+
958
+ // Use saved traits if they exist, otherwise fall back to character defaults
959
+ const traits = {
960
+ affection: savedTraits.affection ?? characterDefaults.affection ?? 50,
961
+ playfulness: savedTraits.playfulness ?? characterDefaults.playfulness ?? 50,
962
+ intelligence: savedTraits.intelligence ?? characterDefaults.intelligence ?? 50,
963
+ empathy: savedTraits.empathy ?? characterDefaults.empathy ?? 50,
964
+ humor: savedTraits.humor ?? characterDefaults.humor ?? 50,
965
+ romance: savedTraits.romance ?? characterDefaults.romance ?? 50
966
+ };
967
+
968
+ // Check if sliders exist before updating them
969
+ const sliderUpdates = [
970
+ { id: "trait-affection", value: traits.affection },
971
+ { id: "trait-playfulness", value: traits.playfulness },
972
+ { id: "trait-intelligence", value: traits.intelligence },
973
+ { id: "trait-empathy", value: traits.empathy },
974
+ { id: "trait-humor", value: traits.humor },
975
+ { id: "trait-romance", value: traits.romance }
976
+ ];
977
+
978
+ for (const update of sliderUpdates) {
979
+ const slider = document.getElementById(update.id);
980
+ if (slider) {
981
+ updateSlider(update.id, update.value);
982
+ }
983
+ }
984
+ } catch (error) {
985
+ console.error("Error updating personality sliders:", error);
986
+ }
987
+ }
988
+
989
+ async function updateStats() {
990
+ const kimiDB = window.kimiDB;
991
+ if (!kimiDB) return;
992
+ const character = await kimiDB.getSelectedCharacter();
993
+ const totalInteractions = await kimiDB.getPreference(`totalInteractions_${character}`, 0);
994
+ const affectionTrait = await kimiDB.getPersonalityTrait("affection", 80, character);
995
+ const conversations = await kimiDB.getAllConversations(character);
996
+ let firstInteraction = await kimiDB.getPreference(`firstInteraction_${character}`);
997
+ if (!firstInteraction && conversations.length > 0) {
998
+ firstInteraction = conversations[0].timestamp;
999
+ await kimiDB.setPreference(`firstInteraction_${character}`, firstInteraction);
1000
+ }
1001
+ const totalEl = document.getElementById("total-interactions");
1002
+ const favorabilityEl = document.getElementById("current-favorability");
1003
+ const conversationsEl = document.getElementById("conversations-count");
1004
+ const daysEl = document.getElementById("days-together");
1005
+ if (totalEl) totalEl.textContent = totalInteractions;
1006
+ if (favorabilityEl) {
1007
+ const v = Number(affectionTrait) || 0;
1008
+ favorabilityEl.textContent = `${Math.max(0, Math.min(100, v)).toFixed(2)}%`;
1009
+ }
1010
+ if (conversationsEl) conversationsEl.textContent = conversations.length;
1011
+ if (firstInteraction && daysEl) {
1012
+ const days = Math.floor((new Date() - new Date(firstInteraction)) / (1000 * 60 * 60 * 24));
1013
+ daysEl.textContent = days;
1014
+ }
1015
+ }
1016
+
1017
+ function initializeAllSliders() {
1018
+ const sliders = [
1019
+ "voice-rate",
1020
+ "voice-pitch",
1021
+ "voice-volume",
1022
+ "trait-affection",
1023
+ "trait-playfulness",
1024
+ "trait-intelligence",
1025
+ "trait-empathy",
1026
+ "trait-humor",
1027
+ "trait-romance",
1028
+ "llm-temperature",
1029
+ "llm-max-tokens",
1030
+ "llm-top-p",
1031
+ "llm-frequency-penalty",
1032
+ "llm-presence-penalty",
1033
+ "interface-opacity"
1034
+ ];
1035
+
1036
+ sliders.forEach(sliderId => {
1037
+ const slider = document.getElementById(sliderId);
1038
+ const valueSpan = document.getElementById(`${sliderId}-value`);
1039
+ if (slider && valueSpan) {
1040
+ valueSpan.textContent = slider.value;
1041
+ }
1042
+ });
1043
+ }
1044
+
1045
+ async function syncLLMMaxTokensSlider() {
1046
+ const kimiDB = window.kimiDB;
1047
+ const llmMaxTokensSlider = document.getElementById("llm-max-tokens");
1048
+ const llmMaxTokensValue = document.getElementById("llm-max-tokens-value");
1049
+ if (llmMaxTokensSlider && llmMaxTokensValue && kimiDB) {
1050
+ const saved = await kimiDB.getPreference("llmMaxTokens", 100);
1051
+ llmMaxTokensSlider.value = saved;
1052
+ llmMaxTokensValue.textContent = saved;
1053
+ }
1054
+ }
1055
+
1056
+ async function syncLLMTemperatureSlider() {
1057
+ const kimiDB = window.kimiDB;
1058
+ const llmTemperatureSlider = document.getElementById("llm-temperature");
1059
+ const llmTemperatureValue = document.getElementById("llm-temperature-value");
1060
+ if (llmTemperatureSlider && llmTemperatureValue && kimiDB) {
1061
+ const saved = await kimiDB.getPreference("llmTemperature", 0.9);
1062
+ llmTemperatureSlider.value = saved;
1063
+ llmTemperatureValue.textContent = saved;
1064
+ }
1065
+ }
1066
+
1067
+ function updateTabsScrollIndicator() {
1068
+ const tabsContainer = document.querySelector(".settings-tabs");
1069
+ if (!tabsContainer) return;
1070
+
1071
+ const isOverflowing = tabsContainer.scrollWidth > tabsContainer.clientWidth;
1072
+
1073
+ if (isOverflowing) {
1074
+ tabsContainer.classList.remove("no-overflow");
1075
+ } else {
1076
+ tabsContainer.classList.add("no-overflow");
1077
+ }
1078
+ }
1079
+
1080
+ async function loadAvailableModels() {
1081
+ // Prevent multiple simultaneous calls
1082
+ if (loadAvailableModels._loading) {
1083
+ return;
1084
+ }
1085
+ loadAvailableModels._loading = true;
1086
+
1087
+ const kimiLLM = window.kimiLLM;
1088
+ if (!kimiLLM) {
1089
+ console.warn("❌ KimiLLM not yet initialized for loadAvailableModels");
1090
+ loadAvailableModels._loading = false;
1091
+ return;
1092
+ }
1093
+
1094
+ const modelsContainer = document.getElementById("models-container");
1095
+ if (!modelsContainer) {
1096
+ console.warn("❌ Models container not found");
1097
+ loadAvailableModels._loading = false;
1098
+ return;
1099
+ }
1100
+
1101
+ try {
1102
+ const stats = await kimiLLM.getModelStats();
1103
+
1104
+ const signature = JSON.stringify(Object.keys(stats.available || {}).sort());
1105
+
1106
+ if (loadAvailableModels._rendered && loadAvailableModels._signature === signature) {
1107
+ const currentId = stats.current && stats.current.id;
1108
+ const cards = modelsContainer.querySelectorAll(".model-card");
1109
+ cards.forEach(card => {
1110
+ if (card.dataset.modelId === currentId) {
1111
+ card.classList.add("selected");
1112
+ } else {
1113
+ card.classList.remove("selected");
1114
+ }
1115
+ });
1116
+ loadAvailableModels._loading = false;
1117
+ return;
1118
+ }
1119
+
1120
+ while (modelsContainer.firstChild) {
1121
+ modelsContainer.removeChild(modelsContainer.firstChild);
1122
+ }
1123
+
1124
+ // Check if we have available models
1125
+ if (!stats.available || Object.keys(stats.available).length === 0) {
1126
+ console.warn("⚠️ No models available in stats");
1127
+ const noModelsDiv = document.createElement("div");
1128
+ noModelsDiv.className = "no-models-message";
1129
+ noModelsDiv.innerHTML = `
1130
+ <p>⚠️ No models available. Please check your API key.</p>
1131
+ `;
1132
+ modelsContainer.appendChild(noModelsDiv);
1133
+ loadAvailableModels._loading = false;
1134
+ return;
1135
+ }
1136
+
1137
+ // Only log once when models are loaded, not repeated calls
1138
+ if (!loadAvailableModels._lastLoadTime || Date.now() - loadAvailableModels._lastLoadTime > 5000) {
1139
+ console.log(`✅ Loaded ${Object.keys(stats.available).length} LLM models`);
1140
+ loadAvailableModels._lastLoadTime = Date.now();
1141
+ }
1142
+ const createCard = (id, model) => {
1143
+ const modelDiv = document.createElement("div");
1144
+ modelDiv.className = `model-card ${id === stats.current.id ? "selected" : ""}`;
1145
+ modelDiv.dataset.modelId = id;
1146
+ const searchable = [model.name || "", model.provider || "", id, (model.strengths || []).join(" ")]
1147
+ .join(" ")
1148
+ .toLowerCase();
1149
+ modelDiv.dataset.search = searchable;
1150
+
1151
+ // Create model card elements safely
1152
+ const modelHeader = document.createElement("div");
1153
+ modelHeader.className = "model-header";
1154
+
1155
+ const modelName = document.createElement("div");
1156
+ modelName.className = "model-name";
1157
+ modelName.textContent = model.name;
1158
+
1159
+ const modelProvider = document.createElement("div");
1160
+ modelProvider.className = "model-provider";
1161
+ modelProvider.textContent = model.provider;
1162
+
1163
+ modelHeader.appendChild(modelName);
1164
+ modelHeader.appendChild(modelProvider);
1165
+
1166
+ const modelDescription = document.createElement("div");
1167
+ modelDescription.className = "model-description";
1168
+ const rawIn = model.pricing && typeof model.pricing.input !== "undefined" ? model.pricing.input : "N/A";
1169
+ const rawOut = model.pricing && typeof model.pricing.output !== "undefined" ? model.pricing.output : "N/A";
1170
+ const inNum = typeof rawIn === "number" ? rawIn : typeof rawIn === "string" ? Number(rawIn) : NaN;
1171
+ const outNum = typeof rawOut === "number" ? rawOut : typeof rawOut === "string" ? Number(rawOut) : NaN;
1172
+ const inIsNum = Number.isFinite(inNum);
1173
+ const outIsNum = Number.isFinite(outNum);
1174
+ const bothNA = !inIsNum && !outIsNum;
1175
+ const bothZero = inIsNum && outIsNum && inNum === 0 && outNum === 0;
1176
+ const isFreeName =
1177
+ /free/i.test(model.name || "") ||
1178
+ /free/i.test(id || "") ||
1179
+ (Array.isArray(model.strengths) && model.strengths.some(s => /free/i.test(s)));
1180
+ const fmt = n => {
1181
+ if (!Number.isFinite(n)) return "N/A";
1182
+ const roundedInt = Math.round(n);
1183
+ if (Math.abs(n - roundedInt) < 1e-6) return `${roundedInt}$`;
1184
+ return `${n.toFixed(2)}$`;
1185
+ };
1186
+ let inStr = inIsNum ? (inNum === 0 ? "Free" : fmt(inNum)) : "N/A";
1187
+ let outStr = outIsNum ? (outNum === 0 ? "Free" : fmt(outNum)) : "N/A";
1188
+ let priceText;
1189
+ if (bothZero || isFreeName) {
1190
+ priceText = "Price: Free";
1191
+ } else if (bothNA) {
1192
+ priceText = "Price: N/A";
1193
+ } else {
1194
+ priceText = `Price: ${inStr} per 1M input tokens, ${outStr} per 1M output tokens`;
1195
+ }
1196
+ modelDescription.textContent = `Context: ${model.contextWindow.toLocaleString()} tokens | ${priceText}`;
1197
+
1198
+ const modelStrengths = document.createElement("div");
1199
+ modelStrengths.className = "model-strengths";
1200
+ if (priceText === "Price: Free") {
1201
+ const badge = document.createElement("span");
1202
+ badge.className = "strength-tag";
1203
+ badge.textContent = "Free";
1204
+ modelStrengths.appendChild(badge);
1205
+ }
1206
+ model.strengths.forEach(strength => {
1207
+ const strengthTag = document.createElement("span");
1208
+ strengthTag.className = "strength-tag";
1209
+ strengthTag.textContent = strength;
1210
+ modelStrengths.appendChild(strengthTag);
1211
+ });
1212
+
1213
+ modelDiv.appendChild(modelHeader);
1214
+ modelDiv.appendChild(modelDescription);
1215
+ modelDiv.appendChild(modelStrengths);
1216
+
1217
+ modelDiv.addEventListener("click", async () => {
1218
+ try {
1219
+ await kimiLLM.setCurrentModel(id);
1220
+ document.querySelectorAll(".model-card").forEach(card => card.classList.remove("selected"));
1221
+ modelDiv.classList.add("selected");
1222
+ console.log(`🤖 Model switched to: ${model.name}`);
1223
+
1224
+ // Show brief feedback to user
1225
+ const feedback = document.createElement("div");
1226
+ feedback.textContent = `Model changed to ${model.name}`;
1227
+ feedback.style.cssText = `
1228
+ position: fixed; top: 20px; right: 20px; z-index: 10000;
1229
+ background: #27ae60; color: white; padding: 12px 20px;
1230
+ border-radius: 6px; font-size: 14px; font-weight: 500;
1231
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1232
+ `;
1233
+ document.body.appendChild(feedback);
1234
+ setTimeout(() => feedback.remove(), 3000);
1235
+ } catch (error) {
1236
+ console.error("Error while changing model:", error);
1237
+ // Show error feedback
1238
+ const errorFeedback = document.createElement("div");
1239
+ errorFeedback.textContent = `Error changing model: ${error.message}`;
1240
+ errorFeedback.style.cssText = `
1241
+ position: fixed; top: 20px; right: 20px; z-index: 10000;
1242
+ background: #e74c3c; color: white; padding: 12px 20px;
1243
+ border-radius: 6px; font-size: 14px; font-weight: 500;
1244
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
1245
+ `;
1246
+ document.body.appendChild(errorFeedback);
1247
+ setTimeout(() => errorFeedback.remove(), 5000);
1248
+ }
1249
+ });
1250
+
1251
+ return modelDiv;
1252
+ };
1253
+
1254
+ const recommendedIds =
1255
+ window.kimiLLM && Array.isArray(window.kimiLLM.recommendedModelIds) ? window.kimiLLM.recommendedModelIds : [];
1256
+
1257
+ const recommendedEntries = recommendedIds.map(id => [id, stats.available[id]]).filter(([, model]) => !!model);
1258
+
1259
+ const otherEntries = Object.entries(stats.available)
1260
+ .filter(([id]) => !recommendedIds.includes(id))
1261
+ .sort((a, b) => (a[1].name || a[0]).localeCompare(b[1].name || b[0]));
1262
+
1263
+ const searchWrap = document.createElement("div");
1264
+ searchWrap.className = "models-search-container";
1265
+ const searchInput = document.createElement("input");
1266
+ searchInput.type = "text";
1267
+ searchInput.className = "kimi-input";
1268
+ searchInput.id = "models-search";
1269
+ searchInput.placeholder = "Filter models...";
1270
+ searchWrap.appendChild(searchInput);
1271
+ modelsContainer.appendChild(searchWrap);
1272
+ if (typeof loadAvailableModels._searchValue === "string") {
1273
+ searchInput.value = loadAvailableModels._searchValue;
1274
+ }
1275
+
1276
+ if (recommendedEntries.length > 0) {
1277
+ const recSection = document.createElement("div");
1278
+ recSection.className = "models-section recommended-models";
1279
+ const title = document.createElement("div");
1280
+ title.className = "models-section-title";
1281
+ title.textContent = "Recommended Openrouter models";
1282
+ recSection.appendChild(title);
1283
+ const list = document.createElement("div");
1284
+ list.className = "models-list";
1285
+ recommendedEntries.forEach(([id, model]) => {
1286
+ list.appendChild(createCard(id, model));
1287
+ });
1288
+ recSection.appendChild(list);
1289
+ modelsContainer.appendChild(recSection);
1290
+ }
1291
+
1292
+ if (otherEntries.length > 0) {
1293
+ const allSection = document.createElement("div");
1294
+ allSection.className = "models-section all-models";
1295
+ const header = document.createElement("div");
1296
+ header.className = "models-section-title";
1297
+ const toggleBtn = document.createElement("button");
1298
+ toggleBtn.type = "button";
1299
+ toggleBtn.className = "kimi-button";
1300
+ toggleBtn.style.marginLeft = "8px";
1301
+ toggleBtn.textContent = loadAvailableModels._allCollapsed === false ? "Hide" : "Show";
1302
+ const label = document.createElement("span");
1303
+ label.textContent = "All Openrouter models";
1304
+ header.appendChild(label);
1305
+ header.appendChild(toggleBtn);
1306
+ const refreshBtn = document.createElement("button");
1307
+ refreshBtn.type = "button";
1308
+ refreshBtn.className = "kimi-button";
1309
+ refreshBtn.style.marginLeft = "8px";
1310
+ refreshBtn.textContent = "Refresh";
1311
+ refreshBtn.addEventListener("click", async () => {
1312
+ try {
1313
+ refreshBtn.disabled = true;
1314
+ const oldText = refreshBtn.textContent;
1315
+ refreshBtn.textContent = "Refreshing...";
1316
+ if (window.kimiLLM && window.kimiLLM.refreshRemoteModels) {
1317
+ await window.kimiLLM.refreshRemoteModels();
1318
+ }
1319
+ loadAvailableModels._signature = null;
1320
+ loadAvailableModels._rendered = false;
1321
+ const savedSearch = searchInput.value;
1322
+ loadAvailableModels._searchValue = savedSearch;
1323
+ await loadAvailableModels();
1324
+ } catch (e) {
1325
+ console.error("Error refreshing models:", e);
1326
+ } finally {
1327
+ refreshBtn.disabled = false;
1328
+ refreshBtn.textContent = "Refresh";
1329
+ }
1330
+ });
1331
+ header.appendChild(refreshBtn);
1332
+ const list = document.createElement("div");
1333
+ list.className = "models-list";
1334
+ otherEntries.forEach(([id, model]) => {
1335
+ list.appendChild(createCard(id, model));
1336
+ });
1337
+ const collapsed = loadAvailableModels._allCollapsed !== false;
1338
+ list.style.display = collapsed ? "none" : "block";
1339
+ toggleBtn.addEventListener("click", () => {
1340
+ const nowCollapsed = list.style.display !== "none";
1341
+ list.style.display = nowCollapsed ? "none" : "block";
1342
+ loadAvailableModels._allCollapsed = nowCollapsed;
1343
+ toggleBtn.textContent = nowCollapsed ? "Show" : "Hide";
1344
+ });
1345
+ allSection.appendChild(header);
1346
+ allSection.appendChild(list);
1347
+ modelsContainer.appendChild(allSection);
1348
+ }
1349
+
1350
+ const applyFilter = term => {
1351
+ const q = (term || "").toLowerCase().trim();
1352
+ const cards = modelsContainer.querySelectorAll(".model-card");
1353
+ cards.forEach(card => {
1354
+ const hay = card.dataset.search || "";
1355
+ card.style.display = q && !hay.includes(q) ? "none" : "";
1356
+ });
1357
+ };
1358
+ searchInput.addEventListener("input", e => {
1359
+ loadAvailableModels._searchValue = e.target.value;
1360
+ applyFilter(e.target.value);
1361
+ });
1362
+ if (searchInput.value) {
1363
+ applyFilter(searchInput.value);
1364
+ }
1365
+
1366
+ loadAvailableModels._rendered = true;
1367
+ loadAvailableModels._signature = signature;
1368
+ } catch (error) {
1369
+ console.error("Error loading available models:", error);
1370
+ const errorDiv = document.createElement("div");
1371
+ errorDiv.className = "models-error-message";
1372
+ errorDiv.innerHTML = `
1373
+ <p>❌ Error loading models: ${error.message}</p>
1374
+ `;
1375
+ modelsContainer.appendChild(errorDiv);
1376
+ } finally {
1377
+ loadAvailableModels._loading = false;
1378
+ }
1379
+ }
1380
+
1381
+ // Debug function for testing models loading
1382
+ window.debugLoadModels = async function () {
1383
+ console.log("🔧 Manual debug of loadAvailableModels");
1384
+ console.log("🔧 window.kimiLLM:", window.kimiLLM);
1385
+ console.log("🔧 Models container:", document.getElementById("models-container"));
1386
+
1387
+ if (window.kimiLLM) {
1388
+ try {
1389
+ const stats = await window.kimiLLM.getModelStats();
1390
+ console.log("🔧 Model stats:", stats);
1391
+ } catch (error) {
1392
+ console.error("🔧 Error getting model stats:", error);
1393
+ }
1394
+ }
1395
+
1396
+ if (window.loadAvailableModels) {
1397
+ await window.loadAvailableModels();
1398
+ }
1399
+ };
1400
+
1401
+ async function sendMessage() {
1402
+ const chatInput = document.getElementById("chat-input");
1403
+ const waitingIndicator = document.getElementById("waiting-indicator");
1404
+ let message = chatInput.value;
1405
+
1406
+ // Enhanced input validation using our new validation utils
1407
+ const validation = window.KimiValidationUtils?.validateMessage(message);
1408
+ if (!validation || !validation.valid) {
1409
+ // Show error to user
1410
+ if (validation?.error) {
1411
+ addMessageToChat("system", `❌ ${validation.error}`);
1412
+ }
1413
+ // Use sanitized version if available
1414
+ if (validation?.sanitized) {
1415
+ chatInput.value = validation.sanitized;
1416
+ }
1417
+ return;
1418
+ }
1419
+
1420
+ message = validation.sanitized || message.trim();
1421
+ if (!message) return;
1422
+
1423
+ addMessageToChat("user", message);
1424
+ chatInput.value = "";
1425
+ if (waitingIndicator) waitingIndicator.style.display = "inline-block";
1426
+
1427
+ try {
1428
+ const response = await analyzeAndReact(message);
1429
+ setTimeout(() => {
1430
+ addMessageToChat("kimi", response);
1431
+ if (window.voiceManager && !message.startsWith("Vous:")) {
1432
+ window.voiceManager.speak(response);
1433
+ }
1434
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1435
+ }, 1000);
1436
+ } catch (error) {
1437
+ console.error("Error while generating response:", error);
1438
+ const i18n = window.kimiI18nManager;
1439
+ const fallbackResponse = i18n
1440
+ ? i18n.t("fallback_general_error")
1441
+ : "Sorry my love, I am having a little technical issue! 💕";
1442
+ addMessageToChat("kimi", fallbackResponse);
1443
+ if (window.voiceManager) {
1444
+ window.voiceManager.speak(fallbackResponse);
1445
+ }
1446
+ if (waitingIndicator) waitingIndicator.style.display = "none";
1447
+ }
1448
+ }
1449
+
1450
+ function setupSettingsListeners(kimiDB, kimiMemory) {
1451
+ const voiceRateSlider = document.getElementById("voice-rate");
1452
+ const voicePitchSlider = document.getElementById("voice-pitch");
1453
+ const voiceVolumeSlider = document.getElementById("voice-volume");
1454
+ const languageSelect = document.getElementById("language-selection");
1455
+ const voiceSelect = document.getElementById("voice-selection");
1456
+ const traitSliders = [
1457
+ "trait-affection",
1458
+ "trait-playfulness",
1459
+ "trait-intelligence",
1460
+ "trait-empathy",
1461
+ "trait-humor",
1462
+ "trait-romance"
1463
+ ];
1464
+ const llmTemperatureSlider = document.getElementById("llm-temperature");
1465
+ const llmMaxTokensSlider = document.getElementById("llm-max-tokens");
1466
+ const llmTopPSlider = document.getElementById("llm-top-p");
1467
+ const llmFrequencyPenaltySlider = document.getElementById("llm-frequency-penalty");
1468
+ const llmPresencePenaltySlider = document.getElementById("llm-presence-penalty");
1469
+ const colorThemeSelect = document.getElementById("color-theme");
1470
+ const interfaceOpacitySlider = document.getElementById("interface-opacity");
1471
+ const animationsToggle = document.getElementById("animations-toggle");
1472
+
1473
+ // Cleanup existing listeners to prevent memory leaks
1474
+ const cleanupListeners = () => {
1475
+ if (window._kimiListenerCleanup) {
1476
+ window._kimiListenerCleanup.forEach(cleanup => cleanup());
1477
+ }
1478
+ window._kimiListenerCleanup = [];
1479
+ };
1480
+
1481
+ cleanupListeners();
1482
+
1483
+ // Create debounced functions for better performance
1484
+ const debouncedVoiceRateUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1485
+ if (kimiDB) await kimiDB.setPreference("voiceRate", parseFloat(value));
1486
+ if (kimiMemory && kimiMemory.preferences) {
1487
+ kimiMemory.preferences.voiceRate = parseFloat(value);
1488
+ }
1489
+ }, 300);
1490
+
1491
+ const debouncedVoicePitchUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1492
+ if (kimiDB) await kimiDB.setPreference("voicePitch", parseFloat(value));
1493
+ if (kimiMemory && kimiMemory.preferences) {
1494
+ kimiMemory.preferences.voicePitch = parseFloat(value);
1495
+ }
1496
+ }, 300);
1497
+
1498
+ const debouncedVoiceVolumeUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1499
+ if (kimiDB) await kimiDB.setPreference("voiceVolume", parseFloat(value));
1500
+ if (kimiMemory && kimiMemory.preferences) {
1501
+ kimiMemory.preferences.voiceVolume = parseFloat(value);
1502
+ }
1503
+ }, 300);
1504
+
1505
+ const debouncedLLMTempUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1506
+ if (kimiDB) await kimiDB.setPreference("llmTemperature", parseFloat(value));
1507
+ if (window.kimiLLMManager) window.kimiLLMManager.temperature = parseFloat(value);
1508
+ }, 300);
1509
+
1510
+ const debouncedLLMTokensUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1511
+ if (kimiDB) await kimiDB.setPreference("llmMaxTokens", parseInt(value));
1512
+ if (window.kimiLLMManager) window.kimiLLMManager.maxTokens = parseInt(value);
1513
+ }, 300);
1514
+
1515
+ const debouncedLLMTopPUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1516
+ if (kimiDB) await kimiDB.setPreference("llmTopP", parseFloat(value));
1517
+ if (window.kimiLLMManager) window.kimiLLMManager.topP = parseFloat(value);
1518
+ }, 300);
1519
+
1520
+ const debouncedLLMFrequencyPenaltyUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1521
+ if (kimiDB) await kimiDB.setPreference("llmFrequencyPenalty", parseFloat(value));
1522
+ if (window.kimiLLMManager) window.kimiLLMManager.frequencyPenalty = parseFloat(value);
1523
+ }, 300);
1524
+
1525
+ const debouncedLLMPresencePenaltyUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1526
+ if (kimiDB) await kimiDB.setPreference("llmPresencePenalty", parseFloat(value));
1527
+ if (window.kimiLLMManager) window.kimiLLMManager.presencePenalty = parseFloat(value);
1528
+ }, 300);
1529
+
1530
+ const debouncedOpacityUpdate = window.KimiPerformanceUtils?.debounce(async value => {
1531
+ if (kimiDB) await kimiDB.setPreference("interfaceOpacity", parseFloat(value));
1532
+ if (window.kimiAppearanceManager && window.kimiAppearanceManager.changeInterfaceOpacity)
1533
+ await window.kimiAppearanceManager.changeInterfaceOpacity(parseFloat(value));
1534
+ }, 300);
1535
+
1536
+ if (voiceRateSlider) {
1537
+ const listener = e => {
1538
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceRate");
1539
+ const value = validation?.value || parseFloat(e.target.value) || 1.1;
1540
+
1541
+ document.getElementById("voice-rate-value").textContent = value;
1542
+ e.target.value = value; // Ensure slider shows validated value
1543
+ debouncedVoiceRateUpdate(value);
1544
+ };
1545
+ voiceRateSlider.addEventListener("input", listener);
1546
+ window._kimiListenerCleanup.push(() => voiceRateSlider.removeEventListener("input", listener));
1547
+ }
1548
+ if (voicePitchSlider) {
1549
+ const listener = e => {
1550
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voicePitch");
1551
+ const value = validation?.value || parseFloat(e.target.value) || 1.1;
1552
+
1553
+ document.getElementById("voice-pitch-value").textContent = value;
1554
+ e.target.value = value;
1555
+ debouncedVoicePitchUpdate(value);
1556
+ };
1557
+ voicePitchSlider.addEventListener("input", listener);
1558
+ window._kimiListenerCleanup.push(() => voicePitchSlider.removeEventListener("input", listener));
1559
+ }
1560
+ if (voiceVolumeSlider) {
1561
+ const listener = e => {
1562
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "voiceVolume");
1563
+ const value = validation?.value || parseFloat(e.target.value) || 0.8;
1564
+
1565
+ document.getElementById("voice-volume-value").textContent = value;
1566
+ e.target.value = value;
1567
+ debouncedVoiceVolumeUpdate(value);
1568
+ };
1569
+ voiceVolumeSlider.addEventListener("input", listener);
1570
+ window._kimiListenerCleanup.push(() => voiceVolumeSlider.removeEventListener("input", listener));
1571
+ }
1572
+ if (languageSelect) {
1573
+ languageSelect.removeEventListener("change", window._kimiLanguageListener);
1574
+ window._kimiLanguageListener = async e => {
1575
+ if (kimiDB) await kimiDB.setPreference("selectedLanguage", e.target.value);
1576
+ if (window.voiceManager && window.voiceManager.initVoices) await window.voiceManager.initVoices();
1577
+ };
1578
+ languageSelect.addEventListener("change", window._kimiLanguageListener);
1579
+ }
1580
+ if (voiceSelect) {
1581
+ voiceSelect.removeEventListener("change", window._kimiVoiceSelectListener);
1582
+ window._kimiVoiceSelectListener = async e => {
1583
+ if (kimiDB) await kimiDB.setPreference("selectedVoice", e.target.value);
1584
+ if (window.voiceManager && window.voiceManager.initVoices) await window.voiceManager.initVoices();
1585
+ };
1586
+ voiceSelect.addEventListener("change", window._kimiVoiceSelectListener);
1587
+ }
1588
+ // Batch personality traits optimization
1589
+ let personalityBatchTimeout = null;
1590
+ const pendingTraitChanges = {};
1591
+
1592
+ traitSliders.forEach(traitId => {
1593
+ const traitSlider = document.getElementById(traitId);
1594
+ if (traitSlider) {
1595
+ traitSlider.removeEventListener("input", window["_kimiTraitListener_" + traitId]);
1596
+ window["_kimiTraitListener_" + traitId] = async e => {
1597
+ const trait = traitId.replace("trait-", "");
1598
+ const value = parseInt(e.target.value, 10);
1599
+
1600
+ // Update UI immediately for responsive feel
1601
+ const valueSpan = document.getElementById(traitId + "-value");
1602
+ if (valueSpan) {
1603
+ valueSpan.textContent = value;
1604
+ }
1605
+
1606
+ // Store pending change for batch processing
1607
+ pendingTraitChanges[trait] = value;
1608
+
1609
+ // Clear existing timeout and set new one for batch save
1610
+ if (personalityBatchTimeout) {
1611
+ clearTimeout(personalityBatchTimeout);
1612
+ }
1613
+
1614
+ personalityBatchTimeout = setTimeout(async () => {
1615
+ if (kimiDB && Object.keys(pendingTraitChanges).length > 0) {
1616
+ try {
1617
+ // Use batch operation for all pending changes
1618
+ await kimiDB.setPersonalityBatch(pendingTraitChanges);
1619
+
1620
+ // Update affection if it was changed
1621
+ if (pendingTraitChanges.affection && kimiMemory) {
1622
+ await kimiMemory.updateAffectionTrait();
1623
+ }
1624
+
1625
+ // Update video context based on new personality values
1626
+ if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1627
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
1628
+ const allTraits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
1629
+ window.kimiVideo.setMoodByPersonality(allTraits);
1630
+ }
1631
+ } catch (error) {
1632
+ console.error("Error batch saving personality traits:", error);
1633
+ }
1634
+
1635
+ // Clear pending changes
1636
+ Object.keys(pendingTraitChanges).forEach(key => delete pendingTraitChanges[key]);
1637
+ }
1638
+ }, 500); // Debounce for 500ms to batch multiple rapid changes
1639
+ };
1640
+ traitSlider.addEventListener("input", window["_kimiTraitListener_" + traitId]);
1641
+ }
1642
+ });
1643
+ if (llmTemperatureSlider) {
1644
+ const listener = e => {
1645
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTemperature");
1646
+ const value = validation?.value || parseFloat(e.target.value) || 0.9;
1647
+
1648
+ document.getElementById("llm-temperature-value").textContent = value;
1649
+ e.target.value = value;
1650
+ debouncedLLMTempUpdate(value);
1651
+ };
1652
+ llmTemperatureSlider.addEventListener("input", listener);
1653
+ window._kimiListenerCleanup.push(() => llmTemperatureSlider.removeEventListener("input", listener));
1654
+ }
1655
+ if (llmMaxTokensSlider) {
1656
+ const listener = e => {
1657
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmMaxTokens");
1658
+ const value = validation?.value || parseInt(e.target.value) || 100;
1659
+
1660
+ document.getElementById("llm-max-tokens-value").textContent = value;
1661
+ e.target.value = value;
1662
+ debouncedLLMTokensUpdate(value);
1663
+ };
1664
+ llmMaxTokensSlider.addEventListener("input", listener);
1665
+ window._kimiListenerCleanup.push(() => llmMaxTokensSlider.removeEventListener("input", listener));
1666
+ }
1667
+ if (llmTopPSlider) {
1668
+ const listener = e => {
1669
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmTopP");
1670
+ const value = validation?.value || parseFloat(e.target.value) || 0.9;
1671
+
1672
+ document.getElementById("llm-top-p-value").textContent = value;
1673
+ e.target.value = value;
1674
+ debouncedLLMTopPUpdate(value);
1675
+ };
1676
+ llmTopPSlider.addEventListener("input", listener);
1677
+ window._kimiListenerCleanup.push(() => llmTopPSlider.removeEventListener("input", listener));
1678
+ }
1679
+ if (llmFrequencyPenaltySlider) {
1680
+ const listener = e => {
1681
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmFrequencyPenalty");
1682
+ const value = validation?.value || parseFloat(e.target.value) || 0.3;
1683
+
1684
+ document.getElementById("llm-frequency-penalty-value").textContent = value;
1685
+ e.target.value = value;
1686
+ debouncedLLMFrequencyPenaltyUpdate(value);
1687
+ };
1688
+ llmFrequencyPenaltySlider.addEventListener("input", listener);
1689
+ window._kimiListenerCleanup.push(() => llmFrequencyPenaltySlider.removeEventListener("input", listener));
1690
+ }
1691
+ if (llmPresencePenaltySlider) {
1692
+ const listener = e => {
1693
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "llmPresencePenalty");
1694
+ const value = validation?.value || parseFloat(e.target.value) || 0.3;
1695
+
1696
+ document.getElementById("llm-presence-penalty-value").textContent = value;
1697
+ e.target.value = value;
1698
+ debouncedLLMPresencePenaltyUpdate(value);
1699
+ };
1700
+ llmPresencePenaltySlider.addEventListener("input", listener);
1701
+ window._kimiListenerCleanup.push(() => llmPresencePenaltySlider.removeEventListener("input", listener));
1702
+ }
1703
+ if (colorThemeSelect) {
1704
+ colorThemeSelect.removeEventListener("change", window._kimiColorThemeListener);
1705
+ window._kimiColorThemeListener = async e => {
1706
+ if (kimiDB) await kimiDB.setPreference("colorTheme", e.target.value);
1707
+ if (window.kimiAppearanceManager && window.kimiAppearanceManager.changeTheme)
1708
+ await window.kimiAppearanceManager.changeTheme(e.target.value);
1709
+ if (window.KimiPluginManager && window.KimiPluginManager.loadPlugins) await window.KimiPluginManager.loadPlugins();
1710
+ };
1711
+ colorThemeSelect.addEventListener("change", window._kimiColorThemeListener);
1712
+ }
1713
+ if (interfaceOpacitySlider) {
1714
+ const listener = e => {
1715
+ const validation = window.KimiValidationUtils?.validateRange(e.target.value, "interfaceOpacity");
1716
+ const value = validation?.value || parseFloat(e.target.value) || 0.8;
1717
+
1718
+ document.getElementById("interface-opacity-value").textContent = value;
1719
+ e.target.value = value;
1720
+ debouncedOpacityUpdate(value);
1721
+ };
1722
+ interfaceOpacitySlider.addEventListener("input", listener);
1723
+ window._kimiListenerCleanup.push(() => interfaceOpacitySlider.removeEventListener("input", listener));
1724
+ }
1725
+ // Animation toggle is handled by KimiAppearanceManager
1726
+ // Remove the duplicate handler to prevent conflicts
1727
+ const transcriptToggle = document.getElementById("transcript-toggle");
1728
+ if (transcriptToggle) {
1729
+ let showTranscript = true;
1730
+ if (kimiDB && kimiDB.getPreference) {
1731
+ kimiDB.getPreference("showTranscript", true).then(showTranscript => {
1732
+ transcriptToggle.classList.toggle("active", showTranscript);
1733
+ transcriptToggle.setAttribute("aria-checked", showTranscript ? "true" : "false");
1734
+ });
1735
+ }
1736
+ const onToggle = async () => {
1737
+ const enabled = !transcriptToggle.classList.contains("active");
1738
+ transcriptToggle.classList.toggle("active", enabled);
1739
+ transcriptToggle.setAttribute("aria-checked", enabled ? "true" : "false");
1740
+ if (kimiDB && kimiDB.setPreference) {
1741
+ await kimiDB.setPreference("showTranscript", enabled);
1742
+ }
1743
+ };
1744
+ transcriptToggle.onclick = onToggle;
1745
+ transcriptToggle.onkeydown = async e => {
1746
+ if (e.key === " " || e.key === "Enter") {
1747
+ e.preventDefault();
1748
+ await onToggle();
1749
+ }
1750
+ };
1751
+ }
1752
+ }
1753
+
1754
+ // Exposer globalement (Note: KimiMemory and KimiAppearanceManager are now in separate files)
1755
+ window.KimiDataManager = KimiDataManager;
1756
+ window.getPersonalityAverage = getPersonalityAverage;
1757
+ window.updateFavorabilityLabel = updateFavorabilityLabel;
1758
+ window.loadCharacterSection = loadCharacterSection;
1759
+ window.getBasicResponse = getBasicResponse;
1760
+ window.updatePersonalityTraitsFromEmotion = updatePersonalityTraitsFromEmotion;
1761
+ window.analyzeAndReact = analyzeAndReact;
1762
+ window.addMessageToChat = addMessageToChat;
1763
+ window.loadChatHistory = loadChatHistory;
1764
+ window.loadSettingsData = loadSettingsData;
1765
+ window.updateSlider = updateSlider;
1766
+ window.updatePersonalitySliders = updatePersonalitySliders;
1767
+ window.updateStats = updateStats;
1768
+ window.initializeAllSliders = initializeAllSliders;
1769
+ window.syncLLMMaxTokensSlider = syncLLMMaxTokensSlider;
1770
+ window.syncLLMTemperatureSlider = syncLLMTemperatureSlider;
1771
+ window.updateTabsScrollIndicator = updateTabsScrollIndicator;
1772
+ window.loadAvailableModels = loadAvailableModels;
1773
+ window.sendMessage = sendMessage;
1774
+ window.setupSettingsListeners = setupSettingsListeners;
1775
+ window.syncPersonalityTraits = syncPersonalityTraits;
1776
+ window.validateEmotionContext = validateEmotionContext;
1777
+ window.ensureVideoContextConsistency = ensureVideoContextConsistency;
1778
+
1779
+ document.addEventListener("DOMContentLoaded", function () {
1780
+ const toggleBtn = document.getElementById("toggle-personality-traits");
1781
+ const cheatPanel = document.getElementById("personality-traits-panel");
1782
+ if (toggleBtn && cheatPanel) {
1783
+ toggleBtn.addEventListener("click", function () {
1784
+ const expanded = toggleBtn.getAttribute("aria-expanded") === "true";
1785
+ toggleBtn.setAttribute("aria-expanded", !expanded);
1786
+ cheatPanel.classList.toggle("open", !expanded);
1787
+ });
1788
+ }
1789
+
1790
+ // Refresh UI models list when the LLM model changes programmatically
1791
+ try {
1792
+ window.addEventListener("llmModelChanged", () => {
1793
+ if (typeof window.loadAvailableModels === "function") {
1794
+ window.loadAvailableModels();
1795
+ }
1796
+ });
1797
+ } catch (e) {}
1798
+
1799
+ // Typing indicator wiring
1800
+ try {
1801
+ // Soft tweak of API key input attributes shortly after load to reduce password manager prompts
1802
+ setTimeout(() => {
1803
+ const apiInput = document.getElementById("openrouter-api-key");
1804
+ if (apiInput) {
1805
+ apiInput.setAttribute("autocomplete", "new-password");
1806
+ apiInput.setAttribute("name", "openrouter_api_key");
1807
+ apiInput.setAttribute("data-lpignore", "true");
1808
+ }
1809
+ }, 300);
1810
+
1811
+ window.addEventListener("chat:typing:start", () => {
1812
+ const waitingIndicator = document.getElementById("waiting-indicator");
1813
+ const globalTyping = document.getElementById("global-typing-indicator");
1814
+ clearTimeout(window._kimiTypingDelayTimer);
1815
+ window._kimiTypingDelayTimer = setTimeout(() => {
1816
+ if (waitingIndicator) waitingIndicator.classList.add("visible");
1817
+ if (globalTyping) globalTyping.classList.add("visible");
1818
+ }, 150);
1819
+ // Safety auto-hide after 10s in case stop event is blocked
1820
+ clearTimeout(window._kimiTypingSafetyTimer);
1821
+ window._kimiTypingSafetyTimer = setTimeout(() => {
1822
+ if (waitingIndicator) waitingIndicator.classList.remove("visible");
1823
+ if (globalTyping) globalTyping.classList.remove("visible");
1824
+ }, 10000);
1825
+ });
1826
+ window.addEventListener("chat:typing:stop", () => {
1827
+ const waitingIndicator = document.getElementById("waiting-indicator");
1828
+ const globalTyping = document.getElementById("global-typing-indicator");
1829
+ if (waitingIndicator) waitingIndicator.classList.remove("visible");
1830
+ if (globalTyping) globalTyping.classList.remove("visible");
1831
+ clearTimeout(window._kimiTypingSafetyTimer);
1832
+ clearTimeout(window._kimiTypingDelayTimer);
1833
+ });
1834
+ } catch (e) {}
1835
+ });
1836
+
1837
+ // Function to sync all personality traits with database and UI
1838
+ async function syncPersonalityTraits(characterName = null) {
1839
+ const kimiDB = window.kimiDB;
1840
+ if (!kimiDB) return;
1841
+
1842
+ const selectedCharacter = characterName || (await kimiDB.getSelectedCharacter());
1843
+ const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
1844
+
1845
+ // Use unified defaults from emotion system
1846
+ const getRequiredTraits = () => {
1847
+ if (window.KimiEmotionSystem) {
1848
+ const emotionSystem = new window.KimiEmotionSystem(kimiDB);
1849
+ return emotionSystem.TRAIT_DEFAULTS;
1850
+ }
1851
+ // Fallback (should match KimiEmotionSystem.TRAIT_DEFAULTS exactly)
1852
+ return {
1853
+ affection: 65,
1854
+ playfulness: 55,
1855
+ intelligence: 70,
1856
+ empathy: 75,
1857
+ humor: 60,
1858
+ romance: 50
1859
+ };
1860
+ };
1861
+
1862
+ const requiredTraits = getRequiredTraits();
1863
+ let needsUpdate = false;
1864
+ const updatedTraits = {};
1865
+
1866
+ for (const [trait, defaultValue] of Object.entries(requiredTraits)) {
1867
+ const currentValue = traits[trait];
1868
+ if (typeof currentValue !== "number" || currentValue < 0 || currentValue > 100) {
1869
+ updatedTraits[trait] = defaultValue;
1870
+ needsUpdate = true;
1871
+ } else {
1872
+ updatedTraits[trait] = currentValue;
1873
+ }
1874
+ }
1875
+
1876
+ // Update database if needed
1877
+ if (needsUpdate) {
1878
+ await kimiDB.setPersonalityBatch(updatedTraits, selectedCharacter);
1879
+ }
1880
+
1881
+ // Update UI sliders
1882
+ for (const [trait, value] of Object.entries(updatedTraits)) {
1883
+ updateSlider(`trait-${trait}`, value);
1884
+ }
1885
+
1886
+ // Update memory cache
1887
+ if (window.kimiMemory && updatedTraits.affection) {
1888
+ window.kimiMemory.affectionTrait = updatedTraits.affection;
1889
+ if (window.kimiMemory.updateFavorabilityBar) {
1890
+ window.kimiMemory.updateFavorabilityBar();
1891
+ }
1892
+ }
1893
+
1894
+ // Update video context
1895
+ if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1896
+ window.kimiVideo.setMoodByPersonality(updatedTraits);
1897
+ }
1898
+
1899
+ return updatedTraits;
1900
+ }
1901
+
1902
+ // Function to validate emotion and context consistency
1903
+ function validateEmotionContext(emotion) {
1904
+ // Normalize video categories to base emotions before validation
1905
+ const normalized = emotion === "speakingPositive" ? "positive" : emotion === "speakingNegative" ? "negative" : emotion;
1906
+ // Use unified emotion system for validation
1907
+ if (window.kimiEmotionSystem) {
1908
+ return window.kimiEmotionSystem.validateEmotion(normalized);
1909
+ }
1910
+
1911
+ // Fallback validation
1912
+ const validEmotions = [
1913
+ "positive",
1914
+ "negative",
1915
+ "neutral",
1916
+ "dancing",
1917
+ "listening",
1918
+ "romantic",
1919
+ "laughing",
1920
+ "surprise",
1921
+ "confident",
1922
+ "shy",
1923
+ "flirtatious",
1924
+ "kiss",
1925
+ "goodbye",
1926
+ "speakingPositive",
1927
+ "speakingNegative",
1928
+ "speaking"
1929
+ ];
1930
+
1931
+ if (!validEmotions.includes(normalized)) {
1932
+ console.warn(`Invalid emotion detected: ${normalized}, falling back to neutral`);
1933
+ return "neutral";
1934
+ }
1935
+
1936
+ return normalized;
1937
+ }
1938
+
1939
+ // Function to ensure video context consistency
1940
+ async function ensureVideoContextConsistency() {
1941
+ if (!window.kimiVideo) return;
1942
+
1943
+ const kimiDB = window.kimiDB;
1944
+ if (!kimiDB) return;
1945
+
1946
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
1947
+ const traits = await kimiDB.getAllPersonalityTraits(selectedCharacter);
1948
+
1949
+ // Validate current video context
1950
+ const currentInfo = window.kimiVideo.getCurrentVideoInfo();
1951
+ const validatedEmotion = validateEmotionContext(currentInfo.emotion);
1952
+
1953
+ if (validatedEmotion !== currentInfo.emotion) {
1954
+ window.kimiVideo.switchToContext("neutral", "neutral", null, traits, traits.affection);
1955
+ }
1956
+ }
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", "purple");
71
+ document.documentElement.setAttribute("data-theme", userTheme);
72
+ } else {
73
+ document.documentElement.setAttribute("data-theme", "purple");
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,1009 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", async function () {
2
+ const DEFAULT_SYSTEM_PROMPT = window.DEFAULT_SYSTEM_PROMPT;
3
+
4
+ let kimiDB = null;
5
+ let kimiLLM = null;
6
+ let isSystemReady = false;
7
+
8
+ const kimiInit = new KimiInitManager();
9
+ let kimiVideo = null;
10
+
11
+ // Error manager is already initialized in kimi-error-manager.js
12
+
13
+ try {
14
+ kimiDB = new KimiDatabase();
15
+ await kimiDB.init();
16
+
17
+ // Expose globally as soon as available
18
+ window.kimiDB = kimiDB;
19
+
20
+ const selectedCharacter = await kimiDB.getPreference("selectedCharacter", "kimi");
21
+ const favorabilityLabel = window.KimiDOMUtils.get("#favorability-label");
22
+ if (favorabilityLabel && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]) {
23
+ favorabilityLabel.setAttribute("data-i18n", "affection_level_of");
24
+ favorabilityLabel.setAttribute(
25
+ "data-i18n-params",
26
+ JSON.stringify({ name: window.KIMI_CHARACTERS[selectedCharacter].name })
27
+ );
28
+ favorabilityLabel.textContent = `💖 Affection level of ${window.KIMI_CHARACTERS[selectedCharacter].name}`;
29
+ }
30
+ const chatHeaderName = window.KimiDOMUtils.get(".chat-header span[data-i18n]");
31
+ if (chatHeaderName && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[selectedCharacter]) {
32
+ chatHeaderName.setAttribute("data-i18n", `chat_with_${selectedCharacter}`);
33
+ }
34
+ const systemPromptInput = window.KimiDOMUtils.get("#system-prompt");
35
+ if (systemPromptInput && kimiDB.getSystemPromptForCharacter) {
36
+ const prompt = await kimiDB.getSystemPromptForCharacter(selectedCharacter);
37
+ systemPromptInput.value = prompt;
38
+ if (kimiLLM && kimiLLM.setSystemPrompt) kimiLLM.setSystemPrompt(prompt);
39
+ }
40
+ kimiLLM = new KimiLLMManager(kimiDB);
41
+ window.kimiLLM = kimiLLM;
42
+ await kimiLLM.init();
43
+
44
+ // Initialize unified emotion system
45
+ window.kimiEmotionSystem = new window.KimiEmotionSystem(kimiDB);
46
+
47
+ // Initialize the new memory system
48
+ window.kimiMemorySystem = new window.KimiMemorySystem(kimiDB);
49
+ await window.kimiMemorySystem.init();
50
+
51
+ // Initialize legacy memory for favorability
52
+ const kimiMemory = new KimiMemory(kimiDB);
53
+ await kimiMemory.init();
54
+ window.kimiMemory = kimiMemory;
55
+
56
+ // Expose globally (already set before init)
57
+
58
+ // Load available models now that LLM is ready
59
+ if (window.loadAvailableModels) {
60
+ setTimeout(() => window.loadAvailableModels(), 500);
61
+ }
62
+
63
+ isSystemReady = true;
64
+ window.isSystemReady = true;
65
+ // Hydrate API config UI from DB after systems ready
66
+ initializeApiConfigUI();
67
+ } catch (error) {
68
+ console.error("Initialization error:", error);
69
+ }
70
+ // Centralized helpers for API config UI
71
+ const ApiUi = {
72
+ presenceDot: () => document.getElementById("api-key-presence"),
73
+ presenceDotTest: () => document.getElementById("api-key-presence-test"),
74
+ apiKeyInput: () => document.getElementById("openrouter-api-key"),
75
+ toggleBtn: () => document.getElementById("toggle-api-key"),
76
+ providerSelect: () => document.getElementById("llm-provider"),
77
+ baseUrlInput: () => document.getElementById("llm-base-url"),
78
+ modelIdInput: () => document.getElementById("llm-model-id"),
79
+ savedBadge: () => document.getElementById("api-key-saved"),
80
+ statusSpan: () => document.getElementById("api-status"),
81
+ testBtn: () => document.getElementById("test-api"),
82
+ setPresence(color) {
83
+ const dot = this.presenceDot();
84
+ const dot2 = this.presenceDotTest();
85
+ if (dot) dot.style.backgroundColor = color;
86
+ if (dot2) dot2.style.backgroundColor = color;
87
+ },
88
+ clearStatus() {
89
+ const s = this.statusSpan();
90
+ if (s) {
91
+ s.textContent = "";
92
+ s.style.color = "";
93
+ }
94
+ },
95
+ setTestEnabled(enabled) {
96
+ const b = this.testBtn();
97
+ if (b) b.disabled = !enabled;
98
+ }
99
+ };
100
+
101
+ // Initial presence state based on current input value
102
+ {
103
+ const currentVal = (ApiUi.apiKeyInput() || {}).value || "";
104
+ const colorInit = currentVal && currentVal.length > 0 ? "#4caf50" : "#9e9e9e";
105
+ ApiUi.setPresence(colorInit);
106
+ }
107
+
108
+ // Initialize API config UI from saved preferences
109
+ async function initializeApiConfigUI() {
110
+ try {
111
+ if (!window.kimiDB) return;
112
+ const provider = await window.kimiDB.getPreference("llmProvider", "openrouter");
113
+ const baseUrl = await window.kimiDB.getPreference(
114
+ "llmBaseUrl",
115
+ provider === "openrouter"
116
+ ? "https://openrouter.ai/api/v1/chat/completions"
117
+ : "https://api.openai.com/v1/chat/completions"
118
+ );
119
+ const modelId = await window.kimiDB.getPreference(
120
+ "llmModelId",
121
+ window.kimiLLM ? window.kimiLLM.currentModel : "model-id"
122
+ );
123
+ const providerSelect = ApiUi.providerSelect();
124
+ if (providerSelect) providerSelect.value = provider;
125
+ const baseUrlInput = ApiUi.baseUrlInput();
126
+ const modelIdInput = ApiUi.modelIdInput();
127
+ const apiKeyInput = ApiUi.apiKeyInput();
128
+ if (baseUrlInput) baseUrlInput.value = baseUrl || "";
129
+ if (modelIdInput && modelId && !modelIdInput.value) modelIdInput.value = modelId;
130
+ // Load the provider-specific key
131
+ const keyPrefMap = {
132
+ openrouter: "openrouterApiKey",
133
+ openai: "apiKey_openai",
134
+ groq: "apiKey_groq",
135
+ together: "apiKey_together",
136
+ deepseek: "apiKey_deepseek",
137
+ "openai-compatible": "apiKey_custom"
138
+ };
139
+ const keyPref = keyPrefMap[provider] || "llmApiKey";
140
+ const storedKey = await window.kimiDB.getPreference(keyPref, "");
141
+ if (apiKeyInput) apiKeyInput.value = storedKey || "";
142
+ ApiUi.setPresence(storedKey ? "#4caf50" : "#9e9e9e");
143
+ const savedBadge = ApiUi.savedBadge();
144
+ if (savedBadge) savedBadge.style.display = storedKey ? "inline" : "none";
145
+ ApiUi.clearStatus();
146
+ // Enable/disable Test button according to validation
147
+ const valid = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(storedKey || ""));
148
+ ApiUi.setTestEnabled(valid);
149
+ // Update dynamic label and placeholders using change handler logic
150
+ if (providerSelect && typeof providerSelect.dispatchEvent === "function") {
151
+ const ev = new Event("change");
152
+ providerSelect.dispatchEvent(ev);
153
+ }
154
+ } catch (e) {
155
+ console.warn("Failed to initialize API config UI:", e);
156
+ }
157
+ }
158
+
159
+ const providerSelectEl = document.getElementById("llm-provider");
160
+ if (providerSelectEl) {
161
+ providerSelectEl.addEventListener("change", async () => {
162
+ const provider = providerSelectEl.value;
163
+ const baseUrlInput = ApiUi.baseUrlInput();
164
+ const apiKeyInput = ApiUi.apiKeyInput();
165
+ const modelIdInput = ApiUi.modelIdInput();
166
+ const placeholders = {
167
+ openrouter: {
168
+ url: "https://openrouter.ai/api/v1/chat/completions",
169
+ keyPh: "sk-or-v1-...",
170
+ model: window.kimiLLM ? window.kimiLLM.currentModel : "model-id"
171
+ },
172
+ openai: {
173
+ url: "https://api.openai.com/v1/chat/completions",
174
+ keyPh: "sk-...",
175
+ model: "gpt-4o-mini"
176
+ },
177
+ groq: {
178
+ url: "https://api.groq.com/openai/v1/chat/completions",
179
+ keyPh: "gsk_...",
180
+ model: "llama-3.1-8b-instant"
181
+ },
182
+ together: {
183
+ url: "https://api.together.xyz/v1/chat/completions",
184
+ keyPh: "together_...",
185
+ model: "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
186
+ },
187
+ deepseek: {
188
+ url: "https://api.deepseek.com/chat/completions",
189
+ keyPh: "sk-...",
190
+ model: "deepseek-chat"
191
+ },
192
+ "openai-compatible": {
193
+ url: "https://your-endpoint/v1/chat/completions",
194
+ keyPh: "your-key",
195
+ model: "model-id"
196
+ },
197
+ ollama: {
198
+ url: "http://localhost:11434/api/chat",
199
+ keyPh: "",
200
+ model: "llama3"
201
+ }
202
+ };
203
+ const p = placeholders[provider] || placeholders.openai;
204
+ if (baseUrlInput) {
205
+ baseUrlInput.placeholder = p.url;
206
+ baseUrlInput.value = provider === "openrouter" ? "https://openrouter.ai/api/v1/chat/completions" : p.url;
207
+ }
208
+ if (apiKeyInput) apiKeyInput.placeholder = p.keyPh;
209
+ if (modelIdInput && !modelIdInput.value) {
210
+ modelIdInput.placeholder = p.model;
211
+ }
212
+ if (window.kimiDB) {
213
+ await window.kimiDB.setPreference("llmProvider", provider);
214
+ await window.kimiDB.setPreference(
215
+ "llmBaseUrl",
216
+ provider === "openrouter" ? "https://openrouter.ai/api/v1/chat/completions" : p.url
217
+ );
218
+ const apiKeyLabel = document.getElementById("api-key-label");
219
+ // Load provider-specific key into the input for clarity
220
+ const keyPrefMap = {
221
+ openrouter: "openrouterApiKey",
222
+ openai: "apiKey_openai",
223
+ groq: "apiKey_groq",
224
+ together: "apiKey_together",
225
+ deepseek: "apiKey_deepseek",
226
+ "openai-compatible": "apiKey_custom"
227
+ };
228
+ const keyPref = keyPrefMap[provider] || "llmApiKey";
229
+ const storedKey = await window.kimiDB.getPreference(keyPref, "");
230
+ if (apiKeyInput) apiKeyInput.value = storedKey || "";
231
+ const color = storedKey && storedKey.length > 0 ? "#4caf50" : "#9e9e9e";
232
+ ApiUi.setPresence(color);
233
+ ApiUi.setTestEnabled(!!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(storedKey || "")));
234
+
235
+ // Dynamic label per provider
236
+ if (apiKeyLabel) {
237
+ const labelByProvider = {
238
+ openrouter: "OpenRouter API Key",
239
+ openai: "OpenAI API Key",
240
+ groq: "Groq API Key",
241
+ together: "Together API Key",
242
+ deepseek: "DeepSeek API Key",
243
+ "openai-compatible": "API Key",
244
+ ollama: "API Key"
245
+ };
246
+ apiKeyLabel.textContent = labelByProvider[provider] || "API Key";
247
+ }
248
+ const savedBadge = ApiUi.savedBadge();
249
+ if (savedBadge) savedBadge.style.display = "none";
250
+ ApiUi.clearStatus();
251
+ }
252
+ });
253
+ }
254
+
255
+ const loadingScreen = document.getElementById("loading-screen");
256
+ if (loadingScreen) {
257
+ setTimeout(() => {
258
+ loadingScreen.style.opacity = "0";
259
+ setTimeout(() => {
260
+ loadingScreen.style.display = "none";
261
+ }, 500);
262
+ }, 1500);
263
+ }
264
+
265
+ let video1 = window.KimiDOMUtils.get("#video1");
266
+ let video2 = window.KimiDOMUtils.get("#video2");
267
+
268
+ if (!video1 || !video2) {
269
+ console.error("Video elements not found! Creating them...");
270
+ const videoContainer = document.querySelector(".video-container");
271
+ if (videoContainer) {
272
+ video1 = document.createElement("video");
273
+ video1.id = "video1";
274
+ video1.className = "bg-video active";
275
+ video1.autoplay = true;
276
+ video1.muted = true;
277
+ video1.playsinline = true;
278
+ video1.preload = "auto";
279
+ video1.innerHTML =
280
+ '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
281
+
282
+ video2 = document.createElement("video");
283
+ video2.id = "video2";
284
+ video2.className = "bg-video";
285
+ video2.autoplay = true;
286
+ video2.muted = true;
287
+ video2.playsinline = true;
288
+ video2.preload = "auto";
289
+ video2.innerHTML =
290
+ '<source src="" type="video/mp4" /><span data-i18n="video_not_supported">Your browser does not support the video tag.</span>';
291
+
292
+ videoContainer.appendChild(video1);
293
+ videoContainer.appendChild(video2);
294
+ }
295
+ }
296
+
297
+ let activeVideo = video1;
298
+ let inactiveVideo = video2;
299
+
300
+ kimiVideo = new KimiVideoManager(video1, video2);
301
+ await kimiVideo.init(kimiDB);
302
+ window.kimiVideo = kimiVideo;
303
+
304
+ if (video1 && video2 && kimiDB && kimiDB.getSelectedCharacter) {
305
+ try {
306
+ const selectedCharacter = await kimiDB.getSelectedCharacter();
307
+ if (selectedCharacter && window.KIMI_CHARACTERS) {
308
+ kimiVideo.setCharacter(selectedCharacter);
309
+ const folder = window.KIMI_CHARACTERS[selectedCharacter].videoFolder;
310
+ const neutralVideo = `${folder}neutral/neutral-gentle-breathing.mp4`;
311
+ const video1Source = video1.querySelector("source");
312
+ if (video1Source) {
313
+ video1Source.setAttribute("src", neutralVideo);
314
+ video1.load();
315
+ }
316
+ }
317
+ if (kimiVideo && kimiVideo.switchToContext) {
318
+ kimiVideo.switchToContext("neutral");
319
+ }
320
+ } catch (e) {
321
+ console.warn("Error loading initial video:", e);
322
+ }
323
+ }
324
+
325
+ async function attachCharacterSection() {
326
+ let saveCharacterBtn = window.KimiDOMUtils.get("#save-character-btn");
327
+ if (saveCharacterBtn) {
328
+ saveCharacterBtn.addEventListener("click", async e => {
329
+ const settingsPanel = window.KimiDOMUtils.get(".settings-panel");
330
+ let scrollTop = settingsPanel ? settingsPanel.scrollTop : null;
331
+ const characterGrid = window.KimiDOMUtils.get("#character-grid");
332
+ const selectedCard = characterGrid ? characterGrid.querySelector(".character-card.selected") : null;
333
+ if (!selectedCard) return;
334
+ const charKey = selectedCard.dataset.character;
335
+ const savedBadge = document.getElementById("api-key-saved");
336
+ if (savedBadge) {
337
+ savedBadge.textContent = (window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
338
+ savedBadge.style.display = "inline";
339
+ }
340
+ const promptInput = window.KimiDOMUtils.get(`#prompt-${charKey}`);
341
+ const prompt = promptInput ? promptInput.value : "";
342
+
343
+ await window.kimiDB.setSelectedCharacter(charKey);
344
+ await window.kimiDB.setSystemPromptForCharacter(charKey, prompt);
345
+ if (window.kimiVideo && window.kimiVideo.setCharacter) {
346
+ window.kimiVideo.setCharacter(charKey);
347
+ if (window.kimiVideo.switchToContext) {
348
+ window.kimiVideo.switchToContext("neutral");
349
+ }
350
+ }
351
+ if (window.voiceManager && window.voiceManager.updateSelectedCharacter) {
352
+ await window.voiceManager.updateSelectedCharacter();
353
+ }
354
+ if (window.kimiLLM && window.kimiLLM.setSystemPrompt) {
355
+ // Only manage system prompt here. API key editing is handled globally to avoid duplicates.
356
+
357
+ // Clear API status when Base URL or Model ID change
358
+ const baseUrlInputEl = ApiUi.baseUrlInput();
359
+ if (baseUrlInputEl) {
360
+ baseUrlInputEl.addEventListener("input", () => {
361
+ ApiUi.clearStatus();
362
+ });
363
+ }
364
+ const modelIdInputEl = ApiUi.modelIdInput();
365
+ if (modelIdInputEl) {
366
+ modelIdInputEl.addEventListener("input", () => {
367
+ ApiUi.clearStatus();
368
+ });
369
+ }
370
+ window.kimiLLM.setSystemPrompt(prompt);
371
+ }
372
+ const systemPromptInput = window.KimiDOMUtils.get("#system-prompt");
373
+ if (systemPromptInput) systemPromptInput.value = prompt;
374
+ await window.loadCharacterSection();
375
+ if (settingsPanel && scrollTop !== null) {
376
+ requestAnimationFrame(() => {
377
+ settingsPanel.scrollTop = scrollTop;
378
+ });
379
+ }
380
+ saveCharacterBtn.setAttribute("data-i18n", "saved");
381
+ saveCharacterBtn.classList.add("success");
382
+ saveCharacterBtn.disabled = true;
383
+
384
+ setTimeout(() => {
385
+ saveCharacterBtn.setAttribute("data-i18n", "save");
386
+ saveCharacterBtn.classList.remove("success");
387
+ saveCharacterBtn.disabled = false;
388
+ }, 1500);
389
+ });
390
+ }
391
+ let settingsButton2 = window.KimiDOMUtils.get("#settings-button");
392
+ if (settingsButton2) {
393
+ settingsButton2.addEventListener("click", window.loadCharacterSection);
394
+ }
395
+ }
396
+ await attachCharacterSection();
397
+
398
+ const chatContainer = document.getElementById("chat-container");
399
+ const chatButton = document.getElementById("chat-button");
400
+ const chatToggle = document.getElementById("chat-toggle");
401
+ const chatMessages = document.getElementById("chat-messages");
402
+ const chatInput = document.getElementById("chat-input");
403
+ const sendButton = document.getElementById("send-button");
404
+ const chatDelete = document.getElementById("chat-delete");
405
+ const waitingIndicator = document.getElementById("waiting-indicator");
406
+
407
+ if (!chatContainer || !chatButton || !chatMessages) {
408
+ console.error("Critical chat elements missing from DOM");
409
+ return;
410
+ }
411
+
412
+ window.kimiOverlayManager = new window.KimiOverlayManager();
413
+
414
+ chatButton.addEventListener("click", () => {
415
+ window.kimiOverlayManager.toggle("chat-container");
416
+ if (window.kimiOverlayManager.isOpen("chat-container")) {
417
+ window.loadChatHistory();
418
+ }
419
+ });
420
+
421
+ if (chatToggle) {
422
+ chatToggle.addEventListener("click", () => {
423
+ window.kimiOverlayManager.close("chat-container");
424
+ });
425
+ }
426
+
427
+ // Setup chat input and send button event listeners
428
+ if (sendButton) {
429
+ sendButton.addEventListener("click", () => {
430
+ if (typeof window.sendMessage === "function") {
431
+ window.sendMessage();
432
+ } else {
433
+ console.error("sendMessage function not available");
434
+ }
435
+ });
436
+ console.log("Send button event listener attached");
437
+ } else {
438
+ console.error("Send button not found");
439
+ }
440
+
441
+ if (chatInput) {
442
+ chatInput.addEventListener("keydown", e => {
443
+ if (e.key === "Enter" && !e.shiftKey) {
444
+ e.preventDefault();
445
+ if (typeof window.sendMessage === "function") {
446
+ window.sendMessage();
447
+ } else {
448
+ console.error("sendMessage function not available");
449
+ }
450
+ }
451
+ });
452
+ console.log("Chat input event listener attached");
453
+ } else {
454
+ console.error("Chat input not found");
455
+ }
456
+
457
+ const settingsOverlay = document.getElementById("settings-overlay");
458
+ const settingsButton = document.getElementById("settings-button");
459
+ const settingsClose = document.getElementById("settings-close");
460
+
461
+ const helpOverlay = document.getElementById("help-overlay");
462
+ const helpButton = document.getElementById("help-button");
463
+ const helpClose = document.getElementById("help-close");
464
+
465
+ if (!settingsButton || !helpButton) {
466
+ console.error("Critical UI buttons missing from DOM");
467
+ return;
468
+ }
469
+
470
+ helpButton.addEventListener("click", () => {
471
+ window.kimiOverlayManager.open("help-overlay");
472
+ });
473
+
474
+ if (helpClose) {
475
+ helpClose.addEventListener("click", () => {
476
+ window.kimiOverlayManager.close("help-overlay");
477
+ });
478
+ }
479
+
480
+ settingsButton.addEventListener("click", () => {
481
+ window.kimiOverlayManager.open("settings-overlay");
482
+
483
+ // Prevent multiple settings loading
484
+ if (!window._settingsLoading) {
485
+ window._settingsLoading = true;
486
+ window.loadSettingsData();
487
+
488
+ setTimeout(() => {
489
+ window.updateTabsScrollIndicator();
490
+ if (window.initializeAllSliders) window.initializeAllSliders();
491
+ if (window.syncLLMMaxTokensSlider) window.syncLLMMaxTokensSlider();
492
+ if (window.syncLLMTemperatureSlider) window.syncLLMTemperatureSlider();
493
+ if (window.setupSettingsListeners) window.setupSettingsListeners(window.kimiDB, window.kimiMemory);
494
+ if (window.syncPersonalityTraits) window.syncPersonalityTraits();
495
+ if (window.ensureVideoContextConsistency) window.ensureVideoContextConsistency();
496
+
497
+ // Only retry loading models if not already done
498
+ if (window.loadAvailableModels && !loadAvailableModels._loading) {
499
+ setTimeout(() => window.loadAvailableModels(), 100);
500
+ }
501
+
502
+ window._settingsLoading = false;
503
+ }, 200);
504
+ }
505
+ });
506
+
507
+ if (settingsClose) {
508
+ settingsClose.addEventListener("click", () => {
509
+ window.kimiOverlayManager.close("settings-overlay");
510
+ });
511
+ }
512
+
513
+ // Initialisation unifiée de la gestion des tabs
514
+ window.kimiTabManager = new window.KimiTabManager({
515
+ onTabChange: async tabName => {
516
+ if (tabName === "llm" || tabName === "api") {
517
+ if (window.kimiDB) {
518
+ const selectedCharacter = await window.kimiDB.getSelectedCharacter();
519
+ const prompt = await window.kimiDB.getSystemPromptForCharacter(selectedCharacter);
520
+ const systemPromptInput = document.getElementById("system-prompt");
521
+ if (systemPromptInput) systemPromptInput.value = prompt;
522
+ }
523
+ }
524
+ if (tabName === "personality") {
525
+ await window.loadCharacterSection();
526
+ }
527
+ }
528
+ });
529
+
530
+ window.kimiUIEventManager = new window.KimiUIEventManager();
531
+ window.kimiUIEventManager.addEvent(window, "resize", window.updateTabsScrollIndicator);
532
+
533
+ const saveSystemPromptButton = document.getElementById("save-system-prompt");
534
+ if (saveSystemPromptButton) {
535
+ saveSystemPromptButton.addEventListener("click", async () => {
536
+ const selectedCharacter = await window.kimiDB.getPreference("selectedCharacter", "kimi");
537
+ const systemPromptInput = document.getElementById("system-prompt");
538
+ if (systemPromptInput && window.kimiDB.setSystemPromptForCharacter) {
539
+ await window.kimiDB.setSystemPromptForCharacter(selectedCharacter, systemPromptInput.value);
540
+ if (window.kimiLLM && window.kimiLLM.setSystemPrompt) window.kimiLLM.setSystemPrompt(systemPromptInput.value);
541
+ const originalText = saveSystemPromptButton.textContent;
542
+ saveSystemPromptButton.textContent = "Saved!";
543
+ saveSystemPromptButton.classList.add("success");
544
+ saveSystemPromptButton.disabled = true;
545
+ setTimeout(() => {
546
+ saveSystemPromptButton.setAttribute("data-i18n", "save");
547
+ applyTranslations();
548
+ // Re-enable the button after the success feedback
549
+ saveSystemPromptButton.disabled = false;
550
+ saveSystemPromptButton.classList.remove("success");
551
+ // Ensure text reflects i18n "save" state
552
+ // (applyTranslations above will set the text from locale)
553
+ }, 1500);
554
+ }
555
+ });
556
+ }
557
+ const resetSystemPromptButton = document.getElementById("reset-system-prompt");
558
+ const systemPromptInput = document.getElementById("system-prompt");
559
+ if (resetSystemPromptButton) {
560
+ resetSystemPromptButton.addEventListener("click", async () => {
561
+ const selectedCharacter = await window.kimiDB.getPreference("selectedCharacter", "kimi");
562
+ if (systemPromptInput && window.kimiDB && window.kimiLLM) {
563
+ await window.kimiDB.setSystemPromptForCharacter(selectedCharacter, DEFAULT_SYSTEM_PROMPT);
564
+ systemPromptInput.value = DEFAULT_SYSTEM_PROMPT;
565
+ window.kimiLLM.setSystemPrompt(DEFAULT_SYSTEM_PROMPT);
566
+ resetSystemPromptButton.textContent = "Reset!";
567
+ resetSystemPromptButton.classList.add("animated");
568
+ resetSystemPromptButton.setAttribute("data-i18n", "reset_done");
569
+ applyTranslations();
570
+ setTimeout(() => {
571
+ resetSystemPromptButton.setAttribute("data-i18n", "reset_to_default");
572
+ applyTranslations();
573
+ }, 1500);
574
+
575
+ // After a reset, allow saving again
576
+ if (saveSystemPromptButton) {
577
+ saveSystemPromptButton.disabled = false;
578
+ saveSystemPromptButton.classList.remove("success");
579
+ saveSystemPromptButton.setAttribute("data-i18n", "save");
580
+ applyTranslations();
581
+ }
582
+ }
583
+ });
584
+ }
585
+
586
+ // Enable the Save button whenever the prompt content changes
587
+ if (systemPromptInput && saveSystemPromptButton) {
588
+ systemPromptInput.addEventListener("input", () => {
589
+ if (saveSystemPromptButton.disabled) {
590
+ saveSystemPromptButton.disabled = false;
591
+ }
592
+ saveSystemPromptButton.classList.remove("success");
593
+ saveSystemPromptButton.setAttribute("data-i18n", "save");
594
+ applyTranslations();
595
+ });
596
+ }
597
+
598
+ window.kimiFormManager = new window.KimiFormManager({ db: window.kimiDB, memory: window.kimiMemory });
599
+
600
+ const testVoiceButton = document.getElementById("test-voice");
601
+ if (testVoiceButton) {
602
+ testVoiceButton.addEventListener("click", () => {
603
+ if (voiceManager) {
604
+ const rate = parseFloat(document.getElementById("voice-rate").value);
605
+ const pitch = parseFloat(document.getElementById("voice-pitch").value);
606
+ const volume = parseFloat(document.getElementById("voice-volume").value);
607
+
608
+ if (window.kimiMemory.preferences) {
609
+ window.kimiMemory.preferences.voiceRate = rate;
610
+ window.kimiMemory.preferences.voicePitch = pitch;
611
+ window.kimiMemory.preferences.voiceVolume = volume;
612
+ }
613
+
614
+ const testMessage =
615
+ window.kimiI18nManager?.t("voice_test_message") ||
616
+ "Hello my love! Here is my new voice configured with all the settings! Do you like it?";
617
+ voiceManager.speak(testMessage, {
618
+ rate,
619
+ pitch,
620
+ volume
621
+ });
622
+ } else {
623
+ console.warn("Voice manager not initialized");
624
+ }
625
+ });
626
+ }
627
+
628
+ const testApiButton = document.getElementById("test-api");
629
+ if (testApiButton) {
630
+ testApiButton.addEventListener("click", async () => {
631
+ const statusSpan = ApiUi.statusSpan();
632
+ const apiKeyInput = ApiUi.apiKeyInput();
633
+ const apiKey = apiKeyInput ? apiKeyInput.value.trim() : "";
634
+ const providerSelect = ApiUi.providerSelect();
635
+ const baseUrlInput = ApiUi.baseUrlInput();
636
+ const modelIdInput = ApiUi.modelIdInput();
637
+ const provider = providerSelect ? providerSelect.value : "openrouter";
638
+ const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : "";
639
+ const modelId = modelIdInput ? modelIdInput.value.trim() : "";
640
+
641
+ if (!statusSpan) return;
642
+
643
+ if (!apiKey) {
644
+ statusSpan.textContent = window.kimiI18nManager?.t("api_key_missing") || "API key missing";
645
+ statusSpan.style.color = "#ff6b6b";
646
+ return;
647
+ }
648
+
649
+ // Validate API key format before saving/testing
650
+ const isValid = (window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(apiKey)) || false;
651
+ if (!isValid) {
652
+ statusSpan.textContent =
653
+ window.kimiI18nManager?.t("api_key_invalid_format") || "Invalid API key format (must start with sk-or-v1-)";
654
+ statusSpan.style.color = "#ff6b6b";
655
+ return;
656
+ }
657
+
658
+ if (window.kimiDB) {
659
+ if (provider === "openrouter") {
660
+ await window.kimiDB.setPreference("openrouterApiKey", apiKey);
661
+ } else {
662
+ await window.kimiDB.setPreference("llmApiKey", apiKey);
663
+ }
664
+ await window.kimiDB.setPreference("llmProvider", provider);
665
+ if (baseUrl) await window.kimiDB.setPreference("llmBaseUrl", baseUrl);
666
+ if (modelId) await window.kimiDB.setPreference("llmModelId", modelId);
667
+ }
668
+
669
+ statusSpan.textContent = "Testing in progress...";
670
+ statusSpan.style.color = "#ffa726";
671
+
672
+ try {
673
+ if (window.kimiLLM) {
674
+ let result;
675
+ if (provider === "openrouter") {
676
+ result = await window.kimiLLM.testModel(window.kimiLLM.currentModel, "Bonjour");
677
+ } else if (provider === "ollama") {
678
+ const response = await window.kimiLLM.chatWithLocal("Bonjour", { maxTokens: 2 });
679
+ result = { success: true, response };
680
+ } else {
681
+ const response = await window.kimiLLM.chatWithOpenAICompatible("Bonjour", { maxTokens: 2 });
682
+ result = { success: true, response };
683
+ }
684
+ if (result.success) {
685
+ statusSpan.textContent = "Connection successful!";
686
+ statusSpan.style.color = "#4caf50";
687
+ const savedBadge = ApiUi.savedBadge();
688
+ if (savedBadge) {
689
+ savedBadge.textContent =
690
+ (window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
691
+ savedBadge.style.display = "inline";
692
+ }
693
+
694
+ if (result.response) {
695
+ setTimeout(() => {
696
+ statusSpan.textContent = `Test response: \"${result.response.substring(0, 50)}...\"`;
697
+ }, 1000);
698
+ }
699
+ ApiUi.setPresence("#4caf50");
700
+ } else {
701
+ statusSpan.textContent = `${result.error}`;
702
+ statusSpan.style.color = "#ff6b6b";
703
+
704
+ if (result.error.includes("similaires disponibles")) {
705
+ setTimeout(() => {}, 1000);
706
+ }
707
+ }
708
+ } else {
709
+ statusSpan.textContent = "LLM manager not initialized";
710
+ statusSpan.style.color = "#ff6b6b";
711
+ }
712
+ } catch (error) {
713
+ console.error("Error while testing API:", error);
714
+ statusSpan.textContent = `Error: ${error.message}`;
715
+ statusSpan.style.color = "#ff6b6b";
716
+
717
+ if (error.message.includes("non disponible")) {
718
+ setTimeout(() => {}, 1000);
719
+ }
720
+ }
721
+ });
722
+ }
723
+
724
+ // Global, single handler for API key input to save and update presence in real-time
725
+ (function setupApiKeyInputHandler() {
726
+ const input = ApiUi.apiKeyInput();
727
+ if (!input) return;
728
+ let t;
729
+ input.addEventListener("input", () => {
730
+ clearTimeout(t);
731
+ t = setTimeout(async () => {
732
+ const providerEl = ApiUi.providerSelect();
733
+ const provider = providerEl ? providerEl.value : "openrouter";
734
+ const keyPrefMap = {
735
+ openrouter: "openrouterApiKey",
736
+ openai: "apiKey_openai",
737
+ groq: "apiKey_groq",
738
+ together: "apiKey_together",
739
+ deepseek: "apiKey_deepseek",
740
+ "openai-compatible": "apiKey_custom"
741
+ };
742
+ const keyPref = keyPrefMap[provider] || "llmApiKey";
743
+ const value = input.value.trim();
744
+ // Update Test button state immediately
745
+ const validNow = !!(window.KIMI_VALIDATORS && window.KIMI_VALIDATORS.validateApiKey(value));
746
+ ApiUi.setTestEnabled(validNow);
747
+ if (window.kimiDB) {
748
+ try {
749
+ await window.kimiDB.setPreference(keyPref, value);
750
+ const savedBadge = ApiUi.savedBadge();
751
+ if (savedBadge) {
752
+ savedBadge.textContent =
753
+ (window.kimiI18nManager && window.kimiI18nManager.t("saved_short")) || "Saved";
754
+ savedBadge.style.display = value ? "inline" : "none";
755
+ }
756
+ ApiUi.setPresence(value ? "#4caf50" : "#9e9e9e");
757
+ ApiUi.clearStatus();
758
+ } catch (e) {
759
+ // Validation error from DB
760
+ const s = ApiUi.statusSpan();
761
+ if (s) {
762
+ s.textContent = e?.message || "Invalid API key";
763
+ s.style.color = "#ff6b6b";
764
+ }
765
+ ApiUi.setTestEnabled(false);
766
+ }
767
+ }
768
+ }, window.KIMI_SECURITY_CONFIG?.DEBOUNCE_DELAY || 300);
769
+ });
770
+ })();
771
+
772
+ // Toggle show/hide for API key
773
+ (function setupToggleEye() {
774
+ const btn = ApiUi.toggleBtn();
775
+ const input = ApiUi.apiKeyInput();
776
+ if (!btn || !input) return;
777
+ btn.addEventListener("click", () => {
778
+ const showing = input.type === "text";
779
+ input.type = showing ? "password" : "text";
780
+ btn.setAttribute("aria-pressed", String(!showing));
781
+ const icon = btn.querySelector("i");
782
+ if (icon) {
783
+ icon.classList.toggle("fa-eye");
784
+ icon.classList.toggle("fa-eye-slash");
785
+ }
786
+ btn.setAttribute("aria-label", showing ? "Show API key" : "Hide API key");
787
+ });
788
+ })();
789
+
790
+ kimiInit.register(
791
+ "appearanceManager",
792
+ async () => {
793
+ const manager = new KimiAppearanceManager(window.kimiDB);
794
+ await manager.init();
795
+ window.kimiAppearanceManager = manager;
796
+ return manager;
797
+ },
798
+ [],
799
+ 500
800
+ );
801
+
802
+ kimiInit.register(
803
+ "dataManager",
804
+ async () => {
805
+ const manager = new KimiDataManager(window.kimiDB);
806
+ await manager.init();
807
+ window.kimiDataManager = manager;
808
+ return manager;
809
+ },
810
+ [],
811
+ 600
812
+ );
813
+
814
+ kimiInit.register(
815
+ "voiceManager",
816
+ async () => {
817
+ if (window.KimiVoiceManager) {
818
+ const manager = new KimiVoiceManager(window.kimiDB, window.kimiMemory);
819
+ const success = await manager.init();
820
+ if (success) {
821
+ manager.setOnSpeechAnalysis(window.analyzeAndReact);
822
+ return manager;
823
+ }
824
+ }
825
+ return null;
826
+ },
827
+ [],
828
+ 1000
829
+ );
830
+
831
+ try {
832
+ await kimiInit.initializeAll();
833
+ window.voiceManager = kimiInit.getInstance("voiceManager");
834
+ window.kimiMemory.updateFavorabilityBar();
835
+ } catch (error) {
836
+ console.error("Initialization error:", error);
837
+ }
838
+
839
+ // Setup unified event handlers to prevent duplicates
840
+ setupUnifiedEventHandlers();
841
+
842
+ // Initialize language and UI
843
+ await initializeLanguageAndUI();
844
+
845
+ // Setup message handling
846
+ setupMessageHandling();
847
+
848
+ // Function definitions
849
+ function setupUnifiedEventHandlers() {
850
+ // Cleanup existing event handlers
851
+ if (window._kimiEventCleanup && Array.isArray(window._kimiEventCleanup)) {
852
+ window._kimiEventCleanup.forEach(cleanup => {
853
+ if (typeof cleanup === "function") cleanup();
854
+ });
855
+ }
856
+ window._kimiEventCleanup = [];
857
+
858
+ // Helper function to safely add event listeners
859
+ function safeAddEventListener(element, event, handler, identifier) {
860
+ if (element && !element[identifier]) {
861
+ element.addEventListener(event, handler);
862
+ element[identifier] = true;
863
+ window._kimiEventCleanup.push(() => {
864
+ element.removeEventListener(event, handler);
865
+ element[identifier] = false;
866
+ });
867
+ }
868
+ }
869
+
870
+ // Chat event handlers
871
+ const chatDelete = document.getElementById("chat-delete");
872
+ if (chatDelete) {
873
+ const handler = async () => {
874
+ if (confirm("Do you really want to delete all chat messages? This cannot be undone.")) {
875
+ const chatMessages = document.getElementById("chat-messages");
876
+ if (chatMessages) {
877
+ chatMessages.textContent = "";
878
+ }
879
+ if (window.kimiDB && window.kimiDB.db) {
880
+ try {
881
+ await window.kimiDB.db.conversations.clear();
882
+ } catch (error) {
883
+ console.error("Error deleting conversations:", error);
884
+ }
885
+ }
886
+ }
887
+ };
888
+ safeAddEventListener(chatDelete, "click", handler, "_kimiChatDeleteHandlerAttached");
889
+ }
890
+ }
891
+
892
+ async function initializeLanguageAndUI() {
893
+ // Language initialization
894
+ window.kimiI18nManager = new window.KimiI18nManager();
895
+ const lang = await kimiDB.getPreference("selectedLanguage", "en");
896
+ await window.kimiI18nManager.setLanguage(lang);
897
+ const langSelect = document.getElementById("language-selection");
898
+ if (langSelect) {
899
+ langSelect.value = lang;
900
+ langSelect.addEventListener("change", async function (e) {
901
+ const selectedLang = e.target.value;
902
+ await kimiDB.setPreference("selectedLanguage", selectedLang);
903
+ await window.kimiI18nManager.setLanguage(selectedLang);
904
+
905
+ if (window.voiceManager && window.voiceManager.handleLanguageChange) {
906
+ await window.voiceManager.handleLanguageChange({ target: { value: selectedLang } });
907
+ }
908
+
909
+ if (window.kimiLLM && window.kimiLLM.setSystemPrompt && kimiDB) {
910
+ const selectedCharacter = await kimiDB.getPreference("selectedCharacter", "kimi");
911
+ let prompt = await kimiDB.getSystemPromptForCharacter(selectedCharacter);
912
+ let langInstruction;
913
+
914
+ switch (selectedLang) {
915
+ case "fr":
916
+ langInstruction = "Always reply exclusively in French. Do not mix languages.";
917
+ break;
918
+ case "es":
919
+ langInstruction = "Always reply exclusively in Spanish. Do not mix languages.";
920
+ break;
921
+ case "de":
922
+ langInstruction = "Always reply exclusively in German. Do not mix languages.";
923
+ break;
924
+ case "it":
925
+ langInstruction = "Always reply exclusively in Italian. Do not mix languages.";
926
+ break;
927
+ case "ja":
928
+ langInstruction = "Always reply exclusively in Japanese. Do not mix languages.";
929
+ break;
930
+ case "zh":
931
+ langInstruction = "Always reply exclusively in Chinese. Do not mix languages.";
932
+ break;
933
+ default:
934
+ langInstruction = "Always reply exclusively in English. Do not mix languages.";
935
+ break;
936
+ }
937
+
938
+ if (prompt) {
939
+ prompt = langInstruction + "\n" + prompt;
940
+ } else {
941
+ prompt = langInstruction;
942
+ }
943
+ window.kimiLLM.setSystemPrompt(prompt);
944
+ const systemPromptInput = document.getElementById("system-prompt");
945
+ if (systemPromptInput) systemPromptInput.value = prompt;
946
+ }
947
+ });
948
+ }
949
+
950
+ window.kimiUIStateManager = new window.KimiUIStateManager();
951
+ }
952
+
953
+ function setupMessageHandling() {
954
+ // Chat event handlers are already attached in the main script
955
+ // No need to reattach them here to avoid duplicates
956
+ }
957
+
958
+ // Add personality change listener
959
+ window.addEventListener("personalityUpdated", async event => {
960
+ const { character, traits } = event.detail;
961
+ console.log(`🧠 Personality updated for ${character}:`, traits);
962
+
963
+ // Update video context based on new traits
964
+ if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
965
+ window.kimiVideo.setMoodByPersonality(traits);
966
+ }
967
+
968
+ // Update voice modulation if available
969
+ if (window.voiceManager && window.voiceManager.updatePersonalityModulation) {
970
+ window.voiceManager.updatePersonalityModulation(traits);
971
+ }
972
+
973
+ // Update UI elements that depend on personality
974
+ // Favorability bar will be updated by KimiMemory system
975
+ });
976
+
977
+ // Add global keyboard event listener for microphone toggle (F8)
978
+ let f8KeyPressed = false;
979
+
980
+ document.addEventListener("keydown", function (event) {
981
+ // Check if F8 key is pressed and no input field is focused
982
+ if (event.key === "F8" && !f8KeyPressed) {
983
+ f8KeyPressed = true;
984
+ const activeElement = document.activeElement;
985
+ const isInputFocused =
986
+ activeElement &&
987
+ (activeElement.tagName === "INPUT" || activeElement.tagName === "TEXTAREA" || activeElement.isContentEditable);
988
+
989
+ // Only trigger if no input field is focused
990
+ if (!isInputFocused && window.voiceManager && window.voiceManager.toggleMicrophone) {
991
+ event.preventDefault();
992
+ window.voiceManager.toggleMicrophone();
993
+ }
994
+ }
995
+ });
996
+
997
+ document.addEventListener("keyup", function (event) {
998
+ if (event.key === "F8") {
999
+ f8KeyPressed = false;
1000
+ }
1001
+ });
1002
+
1003
+ // Monitor for consistency and errors
1004
+ setInterval(async () => {
1005
+ if (window.ensureVideoContextConsistency) {
1006
+ await window.ensureVideoContextConsistency();
1007
+ }
1008
+ }, 30000); // Check every 30 seconds
1009
+ });
kimi-js/kimi-security.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI SECURITY & VALIDATION CONFIGURATION =====
2
+
3
+ window.KIMI_SECURITY_CONFIG = {
4
+ // Input validation limits
5
+ MAX_MESSAGE_LENGTH: 2000,
6
+ MAX_API_KEY_LENGTH: 200,
7
+ MIN_API_KEY_LENGTH: 10,
8
+
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;
kimi-js/kimi-utils.js ADDED
@@ -0,0 +1,2106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== CENTRALIZED KIMI UTILITIES =====
2
+
3
+ // Input validation and sanitization utilities
4
+ window.KimiValidationUtils = {
5
+ // Validate and sanitize user messages
6
+ validateMessage: function (message) {
7
+ if (!message || typeof message !== "string") {
8
+ return { valid: false, error: "Message must be a non-empty string" };
9
+ }
10
+
11
+ const trimmed = message.trim();
12
+ if (trimmed.length === 0) {
13
+ return { valid: false, error: "Message cannot be empty" };
14
+ }
15
+
16
+ if (trimmed.length > 5000) {
17
+ return { valid: false, error: "Message too long (max 5000 characters)" };
18
+ }
19
+
20
+ // Basic XSS prevention
21
+ const sanitized = this.escapeHtml(trimmed);
22
+
23
+ return { valid: true, sanitized: sanitized };
24
+ },
25
+
26
+ // Escape HTML to prevent XSS
27
+ escapeHtml: function (text) {
28
+ const div = document.createElement("div");
29
+ div.textContent = text;
30
+ return div.innerHTML;
31
+ },
32
+
33
+ // Validate numeric ranges for sliders
34
+ validateRange: function (value, type) {
35
+ const numValue = parseFloat(value);
36
+ if (isNaN(numValue)) {
37
+ return { valid: false, value: null };
38
+ }
39
+
40
+ const ranges = {
41
+ voiceRate: { min: 0.5, max: 2, default: 1.1 },
42
+ voicePitch: { min: 0.5, max: 2, default: 1.1 },
43
+ voiceVolume: { min: 0, max: 1, default: 0.8 },
44
+ llmTemperature: { min: 0.1, max: 1, default: 0.9 },
45
+ llmMaxTokens: { min: 10, max: 1000, default: 100 },
46
+ llmTopP: { min: 0, max: 1, default: 0.9 },
47
+ llmFrequencyPenalty: { min: 0, max: 2, default: 0.3 },
48
+ llmPresencePenalty: { min: 0, max: 2, default: 0.3 },
49
+ interfaceOpacity: { min: 0.1, max: 1, default: 0.8 }
50
+ };
51
+
52
+ const range = ranges[type];
53
+ if (!range) {
54
+ return { valid: false, value: null };
55
+ }
56
+
57
+ const clampedValue = Math.max(range.min, Math.min(range.max, numValue));
58
+ return { valid: true, value: clampedValue };
59
+ },
60
+
61
+ // Validate API keys
62
+ validateApiKey: function (key) {
63
+ if (!key || typeof key !== "string") {
64
+ return { valid: false, error: "API key must be a string" };
65
+ }
66
+ const trimmed = key.trim();
67
+ if (trimmed.length === 0) {
68
+ return { valid: false, error: "API key cannot be empty" };
69
+ }
70
+ if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
71
+ const ok = window.KIMI_VALIDATORS.validateApiKey(trimmed);
72
+ return ok ? { valid: true, sanitized: trimmed } : { valid: false, error: "Invalid API key format" };
73
+ }
74
+ return { valid: true, sanitized: trimmed };
75
+ }
76
+ };
77
+
78
+ // Performance utility functions for debouncing and throttling
79
+ window.KimiPerformanceUtils = {
80
+ // Enhanced debounce with immediate execution option
81
+ debounce: function (func, wait, immediate = false, context = null) {
82
+ let timeout;
83
+ let result;
84
+
85
+ return function executedFunction(...args) {
86
+ const later = () => {
87
+ timeout = null;
88
+ if (!immediate) {
89
+ result = func.apply(context || this, args);
90
+ }
91
+ };
92
+
93
+ const callNow = immediate && !timeout;
94
+ clearTimeout(timeout);
95
+ timeout = setTimeout(later, wait);
96
+
97
+ if (callNow) {
98
+ result = func.apply(context || this, args);
99
+ }
100
+
101
+ return result;
102
+ };
103
+ },
104
+
105
+ // Enhanced throttle function with leading and trailing options
106
+ throttle: function (func, limit, options = {}) {
107
+ const { leading = true, trailing = true } = options;
108
+ let inThrottle;
109
+ let lastFunc;
110
+ let lastRan;
111
+
112
+ return function (...args) {
113
+ if (!inThrottle) {
114
+ if (leading) {
115
+ func.apply(this, args);
116
+ }
117
+ lastRan = Date.now();
118
+ inThrottle = true;
119
+ } else {
120
+ clearTimeout(lastFunc);
121
+ lastFunc = setTimeout(
122
+ () => {
123
+ if (trailing && Date.now() - lastRan >= limit) {
124
+ func.apply(this, args);
125
+ lastRan = Date.now();
126
+ }
127
+ },
128
+ limit - (Date.now() - lastRan)
129
+ );
130
+ }
131
+
132
+ setTimeout(() => (inThrottle = false), limit);
133
+ };
134
+ },
135
+
136
+ // Batch processing utility
137
+ createBatcher: function (processor, delay = 500) {
138
+ let timeout = null;
139
+ const pending = {};
140
+
141
+ return function (key, value) {
142
+ pending[key] = value;
143
+
144
+ if (timeout) clearTimeout(timeout);
145
+
146
+ timeout = setTimeout(async () => {
147
+ if (Object.keys(pending).length > 0) {
148
+ try {
149
+ await processor(pending);
150
+ Object.keys(pending).forEach(k => delete pending[k]);
151
+ } catch (error) {
152
+ console.error("Batch processing error:", error);
153
+ }
154
+ }
155
+ }, delay);
156
+ };
157
+ },
158
+
159
+ // Batch requests utility
160
+ batchRequests: function (requests, batchSize = 10) {
161
+ const batches = [];
162
+ for (let i = 0; i < requests.length; i += batchSize) {
163
+ batches.push(requests.slice(i, i + batchSize));
164
+ }
165
+ return batches;
166
+ }
167
+ };
168
+
169
+ // Language management utilities
170
+ window.KimiLanguageUtils = {
171
+ // Default language priority: auto -> user preference -> browser -> fr
172
+ async getLanguage() {
173
+ if (window.kimiDB && window.kimiDB.getPreference) {
174
+ const userLang = await window.kimiDB.getPreference("selectedLanguage", null);
175
+ if (userLang && userLang !== "auto") {
176
+ return userLang;
177
+ }
178
+ }
179
+
180
+ // Auto-detect from browser
181
+ const browserLang = navigator.language?.split("-")[0] || "en";
182
+ const supportedLangs = ["en", "fr", "es", "de", "it", "ja", "zh"];
183
+ return supportedLangs.includes(browserLang) ? browserLang : "en";
184
+ },
185
+
186
+ // Auto-detect language from text content
187
+ detectLanguage(text) {
188
+ if (!text) return "en";
189
+
190
+ if (/[àâäéèêëîïôöùûüÿç]/i.test(text)) return "fr";
191
+ if (/[äöüß]/i.test(text)) return "de";
192
+ if (/[ñáéíóúü]/i.test(text)) return "es";
193
+ if (/[àèìòù]/i.test(text)) return "it";
194
+ if (/[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]/i.test(text)) return "ja";
195
+ if (/[\u4e00-\u9fff]/i.test(text)) return "zh";
196
+ return "en";
197
+ }
198
+ };
199
+
200
+ // Security and validation utilities
201
+ class KimiSecurityUtils {
202
+ static sanitizeInput(input, type = "text") {
203
+ if (typeof input !== "string") return "";
204
+
205
+ switch (type) {
206
+ case "html":
207
+ return input
208
+ .replace(/&/g, "&amp;")
209
+ .replace(/</g, "&lt;")
210
+ .replace(/>/g, "&gt;")
211
+ .replace(/"/g, "&quot;")
212
+ .replace(/'/g, "&#x27;");
213
+ case "number":
214
+ const num = parseFloat(input);
215
+ return isNaN(num) ? 0 : num;
216
+ case "integer":
217
+ const int = parseInt(input, 10);
218
+ return isNaN(int) ? 0 : int;
219
+ case "url":
220
+ try {
221
+ new URL(input);
222
+ return input;
223
+ } catch {
224
+ return "";
225
+ }
226
+ default:
227
+ return input.trim();
228
+ }
229
+ }
230
+
231
+ static validateRange(value, min, max, defaultValue = 0) {
232
+ const num = parseFloat(value);
233
+ if (isNaN(num)) return defaultValue;
234
+ return Math.max(min, Math.min(max, num));
235
+ }
236
+
237
+ static validateApiKey(key) {
238
+ if (!key || typeof key !== "string") return false;
239
+ if (window.KIMI_VALIDATORS && typeof window.KIMI_VALIDATORS.validateApiKey === "function") {
240
+ return !!window.KIMI_VALIDATORS.validateApiKey(key.trim());
241
+ }
242
+ return key.trim().length > 10 && (key.startsWith("sk-") || key.startsWith("sk-or-"));
243
+ }
244
+
245
+ static encryptApiKey(key) {
246
+ // Simple encoding for basic protection (not cryptographically secure)
247
+ return btoa(key).split("").reverse().join("");
248
+ }
249
+
250
+ static decryptApiKey(encodedKey) {
251
+ try {
252
+ return atob(encodedKey.split("").reverse().join(""));
253
+ } catch {
254
+ return "";
255
+ }
256
+ }
257
+ }
258
+
259
+ // Cache management for better performance
260
+ class KimiCacheManager {
261
+ constructor(maxAge = 300000) {
262
+ // 5 minutes default
263
+ this.cache = new Map();
264
+ this.maxAge = maxAge;
265
+ }
266
+
267
+ set(key, value, customMaxAge = null) {
268
+ const maxAge = customMaxAge || this.maxAge;
269
+ this.cache.set(key, {
270
+ value,
271
+ timestamp: Date.now(),
272
+ maxAge
273
+ });
274
+
275
+ // Clean old entries periodically
276
+ if (this.cache.size > 100) {
277
+ this.cleanup();
278
+ }
279
+ }
280
+
281
+ get(key) {
282
+ const entry = this.cache.get(key);
283
+ if (!entry) return null;
284
+
285
+ const now = Date.now();
286
+ if (now - entry.timestamp > entry.maxAge) {
287
+ this.cache.delete(key);
288
+ return null;
289
+ }
290
+
291
+ return entry.value;
292
+ }
293
+
294
+ has(key) {
295
+ return this.get(key) !== null;
296
+ }
297
+
298
+ delete(key) {
299
+ return this.cache.delete(key);
300
+ }
301
+
302
+ clear() {
303
+ this.cache.clear();
304
+ }
305
+
306
+ cleanup() {
307
+ const now = Date.now();
308
+ for (const [key, entry] of this.cache.entries()) {
309
+ if (now - entry.timestamp > entry.maxAge) {
310
+ this.cache.delete(key);
311
+ }
312
+ }
313
+ }
314
+
315
+ getStats() {
316
+ return {
317
+ size: this.cache.size,
318
+ keys: Array.from(this.cache.keys())
319
+ };
320
+ }
321
+ }
322
+
323
+ class KimiBaseManager {
324
+ constructor() {
325
+ // Common base for all managers
326
+ }
327
+
328
+ // Utility method to format file size
329
+ formatFileSize(bytes) {
330
+ if (bytes === 0) return "0 Bytes";
331
+ const k = 1024;
332
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
333
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
334
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
335
+ }
336
+
337
+ // Utility method for error handling
338
+ handleError(error, context = "Operation") {
339
+ console.error(`Error in ${context}:`, error);
340
+ }
341
+
342
+ // Utility method to wait
343
+ async delay(ms) {
344
+ return new Promise(resolve => setTimeout(resolve, ms));
345
+ }
346
+ }
347
+
348
+ // Utility class for centralized video management
349
+ class KimiVideoManager {
350
+ constructor(video1, video2, characterName = "kimi") {
351
+ this.characterName = characterName;
352
+ this.video1 = video1;
353
+ this.video2 = video2;
354
+ this.activeVideo = video1;
355
+ this.inactiveVideo = video2;
356
+ this.currentContext = "neutral";
357
+ this.currentEmotion = "neutral";
358
+ this.lastSwitchTime = Date.now();
359
+ this.pendingSwitch = null;
360
+ this.autoTransitionDuration = 9900;
361
+ this.transitionDuration = 900;
362
+ this._prefetchCache = new Map();
363
+ this._prefetchInFlight = new Set();
364
+ this._maxPrefetch = 3;
365
+ this._loadTimeout = null;
366
+ this.updateVideoCategories();
367
+ this.emotionToCategory = {
368
+ listening: "listening",
369
+ positive: "speakingPositive",
370
+ negative: "speakingNegative",
371
+ neutral: "neutral",
372
+ surprise: "speakingPositive",
373
+ laughing: "speakingPositive",
374
+ shy: "neutral",
375
+ confident: "speakingPositive",
376
+ romantic: "speakingPositive",
377
+ flirtatious: "speakingPositive",
378
+ goodbye: "neutral",
379
+ kiss: "speakingPositive",
380
+ dancing: "dancing",
381
+ speaking: "speakingPositive",
382
+ speakingPositive: "speakingPositive",
383
+ speakingNegative: "speakingNegative"
384
+ };
385
+ this.positiveVideos = this.videoCategories.speakingPositive;
386
+ this.negativeVideos = this.videoCategories.speakingNegative;
387
+ this.neutralVideos = this.videoCategories.neutral;
388
+
389
+ // Anti-repetition and scoring - Adaptive history based on available videos
390
+ this.playHistory = {
391
+ listening: [],
392
+ speakingPositive: [],
393
+ speakingNegative: [],
394
+ neutral: [],
395
+ dancing: []
396
+ };
397
+ this.maxHistoryPerCategory = 5; // Will be dynamically adjusted per category
398
+
399
+ this.emotionHistory = [];
400
+ this.maxEmotionHistory = 5;
401
+ this._neutralLock = false;
402
+ this.isEmotionVideoPlaying = false;
403
+ this.currentEmotionContext = null;
404
+ this._switchInProgress = false;
405
+ this._loadingInProgress = false;
406
+ this._currentLoadHandler = null;
407
+ this._currentErrorHandler = null;
408
+ this._stickyContext = null;
409
+ this._stickyUntil = 0;
410
+ this._pendingSwitches = [];
411
+ this._debug = false;
412
+ }
413
+
414
+ setDebug(enabled) {
415
+ this._debug = !!enabled;
416
+ }
417
+
418
+ _logDebug(message, payload = null) {
419
+ if (!this._debug) return;
420
+ if (payload) console.log("🎬 VideoManager:", message, payload);
421
+ else console.log("🎬 VideoManager:", message);
422
+ }
423
+
424
+ _logSelection(category, selectedSrc, candidates = []) {
425
+ if (!this._debug) return;
426
+ const recent = (this.playHistory && this.playHistory[category]) || [];
427
+ const adaptive = typeof this.getAdaptiveHistorySize === "function" ? this.getAdaptiveHistorySize(category) : null;
428
+ console.log("🎬 VideoManager: selection", {
429
+ category,
430
+ selected: selectedSrc,
431
+ candidatesCount: Array.isArray(candidates) ? candidates.length : 0,
432
+ adaptiveHistorySize: adaptive,
433
+ recentHistory: recent
434
+ });
435
+ }
436
+
437
+ debugPrintHistory(category = null) {
438
+ if (!this._debug) return;
439
+ if (!this.playHistory) {
440
+ console.log("🎬 VideoManager: no play history yet");
441
+ return;
442
+ }
443
+ if (category) {
444
+ const recent = this.playHistory[category] || [];
445
+ console.log("🎬 VideoManager: history", { category, recent });
446
+ return;
447
+ }
448
+ const summary = Object.keys(this.playHistory).reduce((acc, key) => {
449
+ acc[key] = this.playHistory[key];
450
+ return acc;
451
+ }, {});
452
+ console.log("🎬 VideoManager: history summary", summary);
453
+ }
454
+
455
+ _priorityWeight(context) {
456
+ if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") return 3;
457
+ if (context === "dancing" || context === "listening") return 2;
458
+ return 1;
459
+ }
460
+
461
+ _enqueuePendingSwitch(req) {
462
+ // Keep small bounded list; prefer newest higher-priority
463
+ const maxSize = 5;
464
+ this._pendingSwitches.push(req);
465
+ if (this._pendingSwitches.length > maxSize) {
466
+ this._pendingSwitches = this._pendingSwitches.slice(-maxSize);
467
+ }
468
+ }
469
+
470
+ _takeNextPendingSwitch() {
471
+ if (!this._pendingSwitches.length) return null;
472
+ let bestIdx = 0;
473
+ let best = this._pendingSwitches[0];
474
+ for (let i = 1; i < this._pendingSwitches.length; i++) {
475
+ const cand = this._pendingSwitches[i];
476
+ if (cand.priorityWeight > best.priorityWeight) {
477
+ best = cand;
478
+ bestIdx = i;
479
+ } else if (cand.priorityWeight === best.priorityWeight && cand.requestedAt > best.requestedAt) {
480
+ best = cand;
481
+ bestIdx = i;
482
+ }
483
+ }
484
+ this._pendingSwitches.splice(bestIdx, 1);
485
+ return best;
486
+ }
487
+
488
+ _processPendingSwitches() {
489
+ if (this._stickyContext === "dancing") return false;
490
+ const next = this._takeNextPendingSwitch();
491
+ if (!next) return false;
492
+ this._logDebug("Processing pending switch", next);
493
+ this.switchToContext(next.context, next.emotion, next.specificVideo, next.traits, next.affection);
494
+ return true;
495
+ }
496
+
497
+ setCharacter(characterName) {
498
+ this.characterName = characterName;
499
+
500
+ // Nettoyer les handlers en cours lors du changement de personnage
501
+ this._cleanupLoadingHandlers();
502
+
503
+ this.updateVideoCategories();
504
+ }
505
+
506
+ updateVideoCategories() {
507
+ const folder = getCharacterInfo(this.characterName).videoFolder;
508
+ this.videoCategories = {
509
+ listening: [
510
+ `${folder}listening/listening-gentle-sway.mp4`,
511
+ `${folder}listening/listening-hand-gesture.mp4`,
512
+ `${folder}listening/listening-hair-touch.mp4`,
513
+ `${folder}listening/listening-chin-hand.mp4`,
514
+ `${folder}listening/listening-full-spin.mp4`,
515
+ `${folder}listening/listening-teasing-smile.mp4`,
516
+ `${folder}listening/listening-dreamy-gaze-romantic.mp4`
517
+ ],
518
+ speakingPositive: [
519
+ `${folder}speaking-positive/speaking-happy-gestures.mp4`,
520
+ `${folder}speaking-positive/speaking-playful-wink.mp4`,
521
+ `${folder}speaking-positive/speaking-excited-clapping.mp4`,
522
+ `${folder}speaking-positive/speaking-heart-gesture.mp4`,
523
+ `${folder}speaking-positive/speaking-surprise-graceful-gasp.mp4`,
524
+ `${folder}speaking-positive/speaking-laughing-melodious.mp4`,
525
+ `${folder}speaking-positive/speaking-gentle-smile.mp4`,
526
+ `${folder}speaking-positive/speaking-graceful-arms.mp4`,
527
+ `${folder}speaking-positive/speaking-flirtatious-tease.mp4`
528
+ ],
529
+ speakingNegative: [
530
+ `${folder}speaking-negative/speaking-sad-elegant.mp4`,
531
+ `${folder}speaking-negative/speaking-frustrated-graceful.mp4`,
532
+ `${folder}speaking-negative/speaking-worried-tender.mp4`,
533
+ `${folder}speaking-negative/speaking-disappointed-elegant.mp4`,
534
+ `${folder}speaking-negative/speaking-shy-blush-adorable.mp4`,
535
+ `${folder}speaking-negative/speaking-gentle-wave-goodbye.mp4`
536
+ ],
537
+ neutral: [
538
+ `${folder}neutral/neutral-thinking-pose.mp4`,
539
+ `${folder}neutral/neutral-gentle-breathing.mp4`,
540
+ `${folder}neutral/neutral-hair-adjustment.mp4`,
541
+ `${folder}neutral/neutral-arms-crossed-elegant.mp4`,
542
+ `${folder}neutral/neutral-seductive-slow-gaze.mp4`,
543
+ `${folder}neutral/neutral-confident-pose-alluring.mp4`,
544
+ `${folder}neutral/neutral-affectionate-kiss-blow.mp4`
545
+ ],
546
+ dancing: [
547
+ `${folder}dancing/dancing-chin-hand.mp4`,
548
+ `${folder}dancing/dancing-full-spin.mp4`,
549
+ `${folder}dancing/dancing-seductive-dance-undulation.mp4`,
550
+ `${folder}dancing/dancing-slow-seductive.mp4`,
551
+ `${folder}dancing/dancing-spinning-elegance-twirl.mp4`
552
+ ]
553
+ };
554
+ this.positiveVideos = this.videoCategories.speakingPositive;
555
+ this.negativeVideos = this.videoCategories.speakingNegative;
556
+ this.neutralVideos = this.videoCategories.neutral;
557
+
558
+ const neutrals = this.neutralVideos || [];
559
+ neutrals.slice(0, 2).forEach(src => this._prefetch(src));
560
+ }
561
+
562
+ async init(database = null) {
563
+ // Attach lightweight visibility guard
564
+ if (!this._visibilityHandler) {
565
+ this._visibilityHandler = this.onVisibilityChange.bind(this);
566
+ document.addEventListener("visibilitychange", this._visibilityHandler);
567
+ }
568
+ }
569
+
570
+ onVisibilityChange() {
571
+ if (document.visibilityState !== "visible") return;
572
+ const v = this.activeVideo;
573
+ if (!v) return;
574
+ try {
575
+ if (v.ended) {
576
+ if (typeof this.returnToNeutral === "function") this.returnToNeutral();
577
+ } else if (v.paused) {
578
+ v.play().catch(() => {
579
+ if (typeof this.returnToNeutral === "function") this.returnToNeutral();
580
+ });
581
+ }
582
+ } catch {}
583
+ }
584
+
585
+ // Intelligent contextual management
586
+ switchToContext(context, emotion = "neutral", specificVideo = null, traits = null, affection = null) {
587
+ // Respect sticky context (avoid overrides while dancing is requested/playing)
588
+ if (this._stickyContext === "dancing" && context !== "dancing") {
589
+ const categoryForPriority = this.determineCategory(context, emotion, traits);
590
+ const priorityWeight = this._priorityWeight(
591
+ categoryForPriority === "speakingPositive" || categoryForPriority === "speakingNegative" ? "speaking" : context
592
+ );
593
+ if (Date.now() < (this._stickyUntil || 0)) {
594
+ this._enqueuePendingSwitch({
595
+ context,
596
+ emotion,
597
+ specificVideo,
598
+ traits,
599
+ affection,
600
+ requestedAt: Date.now(),
601
+ priorityWeight
602
+ });
603
+ this._logDebug("Queued during dancing (sticky)", { context, emotion, priorityWeight });
604
+ return;
605
+ }
606
+ this._stickyContext = null;
607
+ this._stickyUntil = 0;
608
+ }
609
+ // While an emotion video is playing (speaking), block non-speaking context switches
610
+ if (
611
+ this.isEmotionVideoPlaying &&
612
+ (this.currentContext === "speaking" ||
613
+ this.currentContext === "speakingPositive" ||
614
+ this.currentContext === "speakingNegative") &&
615
+ !(context === "speaking" || context === "speakingPositive" || context === "speakingNegative")
616
+ ) {
617
+ // Queue the request with appropriate priority to be processed after current clip
618
+ const categoryForPriority = this.determineCategory(context, emotion, traits);
619
+ const priorityWeight = this._priorityWeight(
620
+ categoryForPriority === "speakingPositive" || categoryForPriority === "speakingNegative" ? "speaking" : context
621
+ );
622
+ this._enqueuePendingSwitch({
623
+ context,
624
+ emotion,
625
+ specificVideo,
626
+ traits,
627
+ affection,
628
+ requestedAt: Date.now(),
629
+ priorityWeight
630
+ });
631
+ this._logDebug("Queued non-speaking during speaking emotion", { context, emotion, priorityWeight });
632
+ return;
633
+ }
634
+
635
+ // While speaking emotion video is playing, also queue speaking→speaking changes (avoid mid-clip replacement)
636
+ if (
637
+ this.isEmotionVideoPlaying &&
638
+ (this.currentContext === "speaking" ||
639
+ this.currentContext === "speakingPositive" ||
640
+ this.currentContext === "speakingNegative") &&
641
+ (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") &&
642
+ this.currentEmotionContext &&
643
+ this.currentEmotionContext !== emotion
644
+ ) {
645
+ const priorityWeight = this._priorityWeight("speaking");
646
+ this._enqueuePendingSwitch({
647
+ context,
648
+ emotion,
649
+ specificVideo,
650
+ traits,
651
+ affection,
652
+ requestedAt: Date.now(),
653
+ priorityWeight
654
+ });
655
+ this._logDebug("Queued speaking→speaking during active emotion", { from: this.currentEmotionContext, to: emotion });
656
+ return;
657
+ }
658
+ if (context === "neutral" && this._neutralLock) return;
659
+ if (
660
+ (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") &&
661
+ this.isEmotionVideoPlaying &&
662
+ this.currentEmotionContext === emotion
663
+ )
664
+ return;
665
+
666
+ if (this.currentContext === context && this.currentEmotion === emotion && !specificVideo) {
667
+ const category = this.determineCategory(context, emotion, traits);
668
+ const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
669
+ const availableVideos = this.videoCategories[category] || this.videoCategories.neutral;
670
+ const differentVideos = availableVideos.filter(v => v !== currentVideoSrc);
671
+
672
+ if (differentVideos.length > 0) {
673
+ const nextVideo =
674
+ typeof this._pickScoredVideo === "function"
675
+ ? this._pickScoredVideo(category, differentVideos, traits)
676
+ : differentVideos[Math.floor(Math.random() * differentVideos.length)];
677
+ this.loadAndSwitchVideo(nextVideo, "normal");
678
+ // Track play history to avoid immediate repeats
679
+ if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, nextVideo);
680
+ this._logSelection(category, nextVideo, differentVideos);
681
+ this.lastSwitchTime = Date.now();
682
+ }
683
+ return;
684
+ }
685
+
686
+ // Determine the category FIRST to ensure correct video selection
687
+ const category = this.determineCategory(context, emotion, traits);
688
+
689
+ // Déterminer la priorité selon le contexte
690
+ let priority = "normal";
691
+ if (context === "speaking" || context === "speakingPositive" || context === "speakingNegative") {
692
+ priority = "speaking";
693
+ } else if (context === "dancing" || context === "listening") {
694
+ priority = "high";
695
+ }
696
+
697
+ // Set sticky lock for dancing to avoid being interrupted by emotion/neutral updates
698
+ if (context === "dancing") {
699
+ this._stickyContext = "dancing";
700
+ // Lock roughly for one clip duration; will also be cleared on end/neutral
701
+ this._stickyUntil = Date.now() + 9500;
702
+ }
703
+
704
+ // Chemin optimisé lorsque TTS parle/écoute (évite clignotements)
705
+ if (
706
+ window.voiceManager &&
707
+ window.voiceManager.isSpeaking &&
708
+ (context === "speaking" || context === "speakingPositive" || context === "speakingNegative")
709
+ ) {
710
+ const speakingPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
711
+ const speakingCurrent = this.activeVideo.querySelector("source").getAttribute("src");
712
+ if (speakingCurrent !== speakingPath || this.activeVideo.ended) {
713
+ this.loadAndSwitchVideo(speakingPath, priority);
714
+ }
715
+ this.currentContext = context;
716
+ this.currentEmotion = emotion;
717
+ this.lastSwitchTime = Date.now();
718
+ return;
719
+ }
720
+ if (window.voiceManager && window.voiceManager.isListening && context === "listening") {
721
+ const listeningPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
722
+ const listeningCurrent = this.activeVideo.querySelector("source").getAttribute("src");
723
+ if (listeningCurrent !== listeningPath || this.activeVideo.ended) {
724
+ this.loadAndSwitchVideo(listeningPath, priority);
725
+ }
726
+ this.currentContext = context;
727
+ this.currentEmotion = emotion;
728
+ this.lastSwitchTime = Date.now();
729
+ return;
730
+ }
731
+
732
+ // Sélection standard
733
+ let videoPath = this.selectOptimalVideo(category, specificVideo, traits, affection, emotion);
734
+ const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
735
+
736
+ // Anti-répétition si plusieurs vidéos disponibles
737
+ if (videoPath === currentVideoSrc && (this.videoCategories[category] || []).length > 1) {
738
+ const alternatives = this.videoCategories[category].filter(v => v !== currentVideoSrc);
739
+ if (alternatives.length > 0) {
740
+ videoPath =
741
+ typeof this._pickScoredVideo === "function"
742
+ ? this._pickScoredVideo(category, alternatives, traits)
743
+ : alternatives[Math.floor(Math.random() * alternatives.length)];
744
+ }
745
+ }
746
+
747
+ // Adaptive transition timing based on context and priority
748
+ let minTransitionDelay = 300;
749
+
750
+ const now = Date.now();
751
+ const timeSinceLastSwitch = now - (this.lastSwitchTime || 0);
752
+
753
+ // Context-specific timing adjustments
754
+ if (priority === "speaking") {
755
+ minTransitionDelay = 200;
756
+ } else if (context === "listening") {
757
+ minTransitionDelay = 250;
758
+ } else if (context === "dancing") {
759
+ minTransitionDelay = 600;
760
+ } else if (context === "neutral") {
761
+ minTransitionDelay = 1200;
762
+ }
763
+
764
+ // Prevent rapid switching only if not critical
765
+ if (
766
+ this.currentContext === context &&
767
+ this.currentEmotion === emotion &&
768
+ currentVideoSrc === videoPath &&
769
+ !this.activeVideo.paused &&
770
+ !this.activeVideo.ended &&
771
+ timeSinceLastSwitch < minTransitionDelay &&
772
+ priority !== "speaking" // Always allow speech to interrupt
773
+ ) {
774
+ return;
775
+ }
776
+
777
+ this._prefetchLikely(category);
778
+
779
+ this.loadAndSwitchVideo(videoPath, priority);
780
+ this.currentContext = context;
781
+ this.currentEmotion = emotion;
782
+ this.lastSwitchTime = now;
783
+ }
784
+
785
+ setupEventListenersForContext(context) {
786
+ // Clean previous
787
+ if (this._globalEndedHandler) {
788
+ this.activeVideo.removeEventListener("ended", this._globalEndedHandler);
789
+ this.inactiveVideo.removeEventListener("ended", this._globalEndedHandler);
790
+ }
791
+
792
+ // Defensive: ensure helpers exist
793
+ if (!this.playHistory) this.playHistory = {};
794
+ if (!this.maxHistoryPerCategory) this.maxHistoryPerCategory = 8;
795
+
796
+ // For dancing: auto-return to neutral after video ends to avoid freeze
797
+ if (context === "dancing") {
798
+ this._globalEndedHandler = () => {
799
+ this._stickyContext = null;
800
+ this._stickyUntil = 0;
801
+ if (!this._processPendingSwitches()) {
802
+ this.returnToNeutral();
803
+ }
804
+ };
805
+ this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
806
+ // Safety timer
807
+ if (typeof this.scheduleAutoTransition === "function") {
808
+ this.scheduleAutoTransition(this.autoTransitionDuration || 10000);
809
+ }
810
+ return;
811
+ }
812
+
813
+ if (context === "speakingPositive" || context === "speakingNegative") {
814
+ this._globalEndedHandler = () => {
815
+ this.isEmotionVideoPlaying = false;
816
+ this.currentEmotionContext = null;
817
+ this._neutralLock = false;
818
+ // Process any pending high-priority switch first; otherwise return to neutral
819
+ if (!this._processPendingSwitches()) {
820
+ this.returnToNeutral();
821
+ }
822
+ };
823
+ this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
824
+ return;
825
+ }
826
+
827
+ if (context === "listening") {
828
+ this._globalEndedHandler = () => {
829
+ this.switchToContext("listening", "listening");
830
+ };
831
+ this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
832
+ return;
833
+ }
834
+
835
+ // Neutral: on end, pick another neutral to avoid static last frame
836
+ if (context === "neutral") {
837
+ this._globalEndedHandler = () => this.returnToNeutral();
838
+ this.activeVideo.addEventListener("ended", this._globalEndedHandler, { once: true });
839
+ }
840
+ }
841
+
842
+ // keep only the augmented determineCategory above (with traits)
843
+
844
+ selectOptimalVideo(category, specificVideo = null, traits = null, affection = null, emotion = null) {
845
+ const availableVideos = this.videoCategories[category] || this.videoCategories.neutral;
846
+
847
+ if (specificVideo && availableVideos.includes(specificVideo)) {
848
+ if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, specificVideo);
849
+ this._logSelection(category, specificVideo, availableVideos);
850
+ return specificVideo;
851
+ }
852
+
853
+ const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
854
+
855
+ // Filter out recently played videos using adaptive history
856
+ const recentlyPlayed = this.playHistory[category] || [];
857
+ let candidateVideos = availableVideos.filter(video => video !== currentVideoSrc && !recentlyPlayed.includes(video));
858
+
859
+ // If no fresh videos, allow recently played but not current
860
+ if (candidateVideos.length === 0) {
861
+ candidateVideos = availableVideos.filter(video => video !== currentVideoSrc);
862
+ }
863
+
864
+ // Ultimate fallback
865
+ if (candidateVideos.length === 0) {
866
+ candidateVideos = availableVideos;
867
+ }
868
+
869
+ // Ensure we're not falling back to wrong category
870
+ if (candidateVideos.length === 0) {
871
+ candidateVideos = this.videoCategories.neutral;
872
+ }
873
+
874
+ // If traits and affection are provided, weight the selection more subtly
875
+ if (traits && typeof affection === "number") {
876
+ let weights = candidateVideos.map(video => {
877
+ if (category === "speakingPositive") {
878
+ // More positive videos when affection is high, but not extreme
879
+ // Also bias within positive towards romance/humor contexts when emotion suggests it
880
+ const base = 1 + (affection / 100) * 0.3;
881
+ let bonus = 0;
882
+ const rom = typeof traits.romance === "number" ? traits.romance : 50;
883
+ const hum = typeof traits.humor === "number" ? traits.humor : 50;
884
+ if (emotion === "romantic") bonus += (rom / 100) * 0.2;
885
+ if (emotion === "laughing") bonus += (hum / 100) * 0.2;
886
+ return base + bonus;
887
+ }
888
+ if (category === "speakingNegative") {
889
+ // More negative/shy videos when affection is low
890
+ return 1 + ((100 - affection) / 100) * 0.4;
891
+ }
892
+ if (category === "neutral") {
893
+ // Neutral videos when affection is moderate (peak at ~50, lower at extremes)
894
+ const distance = Math.abs(50 - affection) / 50; // 0 at 50, 1 at 0 or 100
895
+ return 1 + (1 - Math.min(1, distance)) * 0.2;
896
+ }
897
+ if (category === "dancing") {
898
+ // Dancing strongly influenced by playfulness but capped
899
+ return 1 + Math.min(0.6, (traits.playfulness / 100) * 0.7);
900
+ }
901
+ if (category === "listening") {
902
+ // Listening influenced by empathy and attention
903
+ const empathyWeight = (traits.empathy || 50) / 100;
904
+ // Slightly consider affection too (more patient listening at higher affection)
905
+ return 1 + empathyWeight * 0.2 + (affection / 100) * 0.05;
906
+ }
907
+ return 1;
908
+ });
909
+
910
+ const total = weights.reduce((a, b) => a + b, 0);
911
+ let r = Math.random() * total;
912
+ for (let i = 0; i < candidateVideos.length; i++) {
913
+ if (r < weights[i]) {
914
+ const chosen = candidateVideos[i];
915
+ if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, chosen);
916
+ this._logSelection(category, chosen, candidateVideos);
917
+ return chosen;
918
+ }
919
+ r -= weights[i];
920
+ }
921
+ const selectedVideo = candidateVideos[0];
922
+ if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, selectedVideo);
923
+ this._logSelection(category, selectedVideo, candidateVideos);
924
+ return selectedVideo;
925
+ }
926
+
927
+ // No traits weighting: random pick
928
+ if (candidateVideos.length === 0) {
929
+ return availableVideos && availableVideos[0] ? availableVideos[0] : null;
930
+ }
931
+ const selectedVideo = candidateVideos[Math.floor(Math.random() * candidateVideos.length)];
932
+ if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, selectedVideo);
933
+ this._logSelection(category, selectedVideo, candidateVideos);
934
+ return selectedVideo;
935
+ }
936
+
937
+ // Get adaptive history size based on available videos
938
+ getAdaptiveHistorySize(category) {
939
+ const availableVideos = this.videoCategories[category] || [];
940
+ const videoCount = availableVideos.length;
941
+
942
+ // Adaptive history: keep 40-60% of available videos in history
943
+ // Minimum 2, maximum 8 to prevent extreme cases
944
+ if (videoCount <= 3) return Math.max(1, videoCount - 1);
945
+ if (videoCount <= 6) return Math.max(2, Math.floor(videoCount * 0.5));
946
+ return Math.min(8, Math.floor(videoCount * 0.6));
947
+ }
948
+
949
+ // Update history with adaptive sizing
950
+ updatePlayHistory(category, videoPath) {
951
+ if (!this.playHistory[category]) {
952
+ this.playHistory[category] = [];
953
+ }
954
+
955
+ const adaptiveSize = this.getAdaptiveHistorySize(category);
956
+ this.playHistory[category].push(videoPath);
957
+
958
+ // Trim to adaptive size
959
+ if (this.playHistory[category].length > adaptiveSize) {
960
+ this.playHistory[category] = this.playHistory[category].slice(-adaptiveSize);
961
+ }
962
+ }
963
+
964
+ // Ensure determineCategory exists as a class method (used at line ~494 and ~537)
965
+ determineCategory(context, emotion = "neutral", traits = null) {
966
+ // Prefer explicit context mapping if provided (e.g., 'listening','dancing')
967
+ if (this.emotionToCategory && this.emotionToCategory[context]) {
968
+ return this.emotionToCategory[context];
969
+ }
970
+ // Normalize generic 'speaking' by emotion polarity
971
+ if (context === "speaking") {
972
+ if (emotion === "positive") return "speakingPositive";
973
+ if (emotion === "negative") return "speakingNegative";
974
+ return "neutral";
975
+ }
976
+ // Map by emotion label when possible
977
+ if (this.emotionToCategory && this.emotionToCategory[emotion]) {
978
+ return this.emotionToCategory[emotion];
979
+ }
980
+ return "neutral";
981
+ }
982
+
983
+ // SPECIALIZED METHODS FOR EACH CONTEXT
984
+ async startListening(traits = null, affection = null) {
985
+ // If already listening and playing, avoid redundant switch
986
+ if (this.currentContext === "listening" && !this.activeVideo.paused && !this.activeVideo.ended) {
987
+ return;
988
+ }
989
+ // Immediate switch to keep UI responsive
990
+ this.switchToContext("listening");
991
+
992
+ // If caller did not provide traits, try to fetch and refine selection
993
+ try {
994
+ if (!traits && window.kimiDB && typeof window.kimiDB.getAllPersonalityTraits === "function") {
995
+ const selectedCharacter = await window.kimiDB.getSelectedCharacter();
996
+ const allTraits = await window.kimiDB.getAllPersonalityTraits(selectedCharacter);
997
+ if (allTraits && typeof allTraits === "object") {
998
+ const aff = typeof allTraits.affection === "number" ? allTraits.affection : undefined;
999
+ // Re-issue context switch with weighting parameters to better pick listening videos
1000
+ this.switchToContext("listening", "listening", null, allTraits, aff);
1001
+ }
1002
+ } else if (traits) {
1003
+ this.switchToContext("listening", "listening", null, traits, affection);
1004
+ }
1005
+ } catch (e) {
1006
+ // Non-fatal: keep basic listening behavior
1007
+ console.warn("Listening refinement skipped due to error:", e);
1008
+ }
1009
+ }
1010
+
1011
+ respondWithEmotion(emotion, traits = null, affection = null) {
1012
+ // Ignore neutral emotion to avoid unintended overrides (use returnToNeutral when appropriate)
1013
+ if (emotion === "neutral") {
1014
+ if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1015
+ this.returnToNeutral();
1016
+ return;
1017
+ }
1018
+ // Do not override dancing while sticky
1019
+ if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1020
+ // If we are already playing the same emotion video, do nothing
1021
+ if (this.isEmotionVideoPlaying && this.currentEmotionContext === emotion) return;
1022
+ // First switch context (so internal guards don't see the new flags yet)
1023
+ this.switchToContext("speaking", emotion, null, traits, affection);
1024
+ // Then mark the emotion video as playing for override protection
1025
+ this.isEmotionVideoPlaying = true;
1026
+ this.currentEmotionContext = emotion;
1027
+ }
1028
+
1029
+ returnToNeutral() {
1030
+ // Always ensure we resume playback with a fresh neutral video to avoid freeze
1031
+ if (this._neutralLock) return;
1032
+ this._neutralLock = true;
1033
+ setTimeout(() => {
1034
+ this._neutralLock = false;
1035
+ }, 1000);
1036
+ this._stickyContext = null;
1037
+ this._stickyUntil = 0;
1038
+ this.isEmotionVideoPlaying = false;
1039
+ this.currentEmotionContext = null;
1040
+
1041
+ // Force-select a neutral clip different from current, then load and switch
1042
+ const category = "neutral";
1043
+ const currentVideoSrc = this.activeVideo.querySelector("source").getAttribute("src");
1044
+ const available = this.videoCategories[category] || [];
1045
+ let nextSrc = null;
1046
+ if (available.length > 0) {
1047
+ const candidates = available.filter(v => v !== currentVideoSrc);
1048
+ nextSrc =
1049
+ candidates.length > 0
1050
+ ? candidates[Math.floor(Math.random() * candidates.length)]
1051
+ : available[Math.floor(Math.random() * available.length)];
1052
+ }
1053
+ if (nextSrc) {
1054
+ this.loadAndSwitchVideo(nextSrc, "normal");
1055
+ if (typeof this.updatePlayHistory === "function") this.updatePlayHistory(category, nextSrc);
1056
+ this.currentContext = "neutral";
1057
+ this.currentEmotion = "neutral";
1058
+ this.lastSwitchTime = Date.now();
1059
+ } else {
1060
+ // Fallback to existing path if list empty
1061
+ this.switchToContext("neutral");
1062
+ }
1063
+ }
1064
+
1065
+ // ADVANCED CONTEXTUAL ANALYSIS
1066
+ // ADVANCED CONTEXTUAL ANALYSIS - SIMPLIFIED
1067
+ async analyzeAndSelectVideo(userMessage, kimiResponse, emotionAnalysis, traits = null, affection = null, lang = null) {
1068
+ // Do not analyze-switch away while dancing is sticky/playing
1069
+ if (this._stickyContext === "dancing" || this.currentContext === "dancing") {
1070
+ return; // let dancing finish
1071
+ }
1072
+ // Auto-detect language if not specified
1073
+ let userLang = lang;
1074
+ if (!userLang && window.kimiDB && window.kimiDB.getPreference) {
1075
+ userLang = await window.KimiLanguageUtils.getLanguage();
1076
+ }
1077
+
1078
+ // Use existing emotion analysis instead of creating new system
1079
+ let detectedEmotion = "neutral";
1080
+ if (window.kimiAnalyzeEmotion) {
1081
+ // Analyze combined user message and Kimi response using existing function
1082
+ const combinedText = [userMessage, kimiResponse].filter(Boolean).join(" ");
1083
+ detectedEmotion = window.kimiAnalyzeEmotion(combinedText, userLang);
1084
+ console.log(`🎭 Emotion detected: "${detectedEmotion}" from text: "${combinedText.substring(0, 50)}..."`);
1085
+ } else if (emotionAnalysis && emotionAnalysis.reaction) {
1086
+ // Fallback to provided emotion analysis
1087
+ detectedEmotion = emotionAnalysis.reaction;
1088
+ }
1089
+
1090
+ // Special case: Auto-dancing if playfulness very high
1091
+ if (traits && typeof traits.playfulness === "number" && traits.playfulness >= 85) {
1092
+ this.switchToContext("dancing", "dancing", null, traits, affection);
1093
+ return;
1094
+ }
1095
+
1096
+ // Add to emotion history
1097
+ this.emotionHistory.push(detectedEmotion);
1098
+ if (this.emotionHistory.length > this.maxEmotionHistory) {
1099
+ this.emotionHistory.shift();
1100
+ }
1101
+
1102
+ // Analyze emotion trend - support all possible emotions
1103
+ const counts = {
1104
+ positive: 0,
1105
+ negative: 0,
1106
+ neutral: 0,
1107
+ dancing: 0,
1108
+ listening: 0,
1109
+ romantic: 0,
1110
+ laughing: 0,
1111
+ surprise: 0,
1112
+ confident: 0,
1113
+ shy: 0,
1114
+ flirtatious: 0,
1115
+ kiss: 0,
1116
+ goodbye: 0
1117
+ };
1118
+ for (let i = 0; i < this.emotionHistory.length; i++) {
1119
+ const emo = this.emotionHistory[i];
1120
+ if (counts[emo] !== undefined) counts[emo]++;
1121
+ }
1122
+
1123
+ // Find dominant emotion
1124
+ let dominant = null;
1125
+ let max = 0;
1126
+ for (const key in counts) {
1127
+ if (counts[key] > max) {
1128
+ max = counts[key];
1129
+ dominant = key;
1130
+ }
1131
+ }
1132
+
1133
+ // Switch to appropriate context based on dominant emotion
1134
+ if (max >= 1 && dominant) {
1135
+ // Map emotion to context using our emotion mapping
1136
+ const targetCategory = this.emotionToCategory[dominant];
1137
+ if (targetCategory) {
1138
+ this.switchToContext(targetCategory, dominant, null, traits, affection);
1139
+ return;
1140
+ }
1141
+
1142
+ // Fallback for unmapped emotions
1143
+ if (dominant === "dancing") {
1144
+ this.switchToContext("dancing", "dancing", null, traits, affection);
1145
+ return;
1146
+ }
1147
+ if (dominant === "positive") {
1148
+ this.switchToContext("speakingPositive", "positive", null, traits, affection);
1149
+ return;
1150
+ }
1151
+ if (dominant === "negative") {
1152
+ this.switchToContext("speakingNegative", "negative", null, traits, affection);
1153
+ return;
1154
+ }
1155
+ if (dominant === "listening") {
1156
+ this.switchToContext("listening", "listening", null, traits, affection);
1157
+ return;
1158
+ }
1159
+ }
1160
+
1161
+ // Default to neutral context, with a very subtle positive bias at very high affection
1162
+ if (traits && typeof traits.affection === "number" && traits.affection >= 95) {
1163
+ const chance = Math.random();
1164
+ if (chance < 0.25) {
1165
+ this.switchToContext("speakingPositive", "positive", null, traits, affection);
1166
+ return;
1167
+ }
1168
+ }
1169
+ // Avoid neutral override if a transient state should persist (handled elsewhere)
1170
+ this.switchToContext("neutral", "neutral", null, traits, affection);
1171
+ }
1172
+
1173
+ // AUTOMATIC TRANSITION TO NEUTRAL
1174
+ scheduleAutoTransition(delayMs) {
1175
+ clearTimeout(this.autoTransitionTimer);
1176
+
1177
+ // Ne pas programmer d'auto-transition pour les contextes de base
1178
+ if (this.currentContext === "neutral" || this.currentContext === "listening") {
1179
+ return;
1180
+ }
1181
+
1182
+ // Durées adaptées selon le contexte (toutes les vidéos font 10s)
1183
+ let duration;
1184
+ if (typeof delayMs === "number") {
1185
+ duration = delayMs;
1186
+ } else {
1187
+ switch (this.currentContext) {
1188
+ case "dancing":
1189
+ duration = 10000; // 10 secondes pour dancing (durée réelle des vidéos)
1190
+ break;
1191
+ case "speakingPositive":
1192
+ case "speakingNegative":
1193
+ duration = 10000; // 10 secondes pour speaking (durée réelle des vidéos)
1194
+ break;
1195
+ case "neutral":
1196
+ // Pas d'auto-transition pour neutral (état par défaut, boucle en continu)
1197
+ return;
1198
+ case "listening":
1199
+ // Pas d'auto-transition pour listening (personnage écoute l'utilisateur)
1200
+ return;
1201
+ default:
1202
+ duration = this.autoTransitionDuration; // 10 secondes par défaut
1203
+ }
1204
+ }
1205
+
1206
+ console.log(`Auto-transition scheduled in ${duration / 1000}s (${this.currentContext} → neutral)`);
1207
+ this.autoTransitionTimer = setTimeout(() => {
1208
+ if (this.currentContext !== "neutral" && this.currentContext !== "listening") {
1209
+ if (!this._processPendingSwitches()) {
1210
+ this.returnToNeutral();
1211
+ }
1212
+ }
1213
+ }, duration);
1214
+ }
1215
+
1216
+ // COMPATIBILITY WITH THE OLD SYSTEM
1217
+ switchVideo(emotion = null) {
1218
+ if (emotion) {
1219
+ this.switchToContext("speaking", emotion);
1220
+ } else {
1221
+ this.switchToContext("neutral");
1222
+ }
1223
+ }
1224
+
1225
+ autoSwitchToNeutral() {
1226
+ this._neutralLock = false;
1227
+ this.isEmotionVideoPlaying = false;
1228
+ this.currentEmotionContext = null;
1229
+ this.switchToContext("neutral");
1230
+ }
1231
+
1232
+ getNextVideo(emotion, currentSrc) {
1233
+ // Adapt the old method for compatibility
1234
+ const category = this.determineCategory("speaking", emotion);
1235
+ return this.selectOptimalVideo(category);
1236
+ }
1237
+
1238
+ loadAndSwitchVideo(videoSrc, priority = "normal") {
1239
+ // Only log high priority or error cases to reduce noise
1240
+ if (priority === "speaking" || priority === "high") {
1241
+ console.log(`🎬 Loading video: ${videoSrc} (priority: ${priority})`);
1242
+ }
1243
+
1244
+ // Si une vidéo haute priorité arrive, on peut interrompre le chargement en cours
1245
+ if (this._loadingInProgress) {
1246
+ if (priority === "high" || priority === "speaking") {
1247
+ this._loadingInProgress = false;
1248
+ // Nettoyer les event listeners en cours sur la vidéo inactive
1249
+ this.inactiveVideo.removeEventListener("canplay", this._currentLoadHandler);
1250
+ this.inactiveVideo.removeEventListener("loadeddata", this._currentLoadHandler);
1251
+ this.inactiveVideo.removeEventListener("error", this._currentErrorHandler);
1252
+ if (this._loadTimeout) {
1253
+ clearTimeout(this._loadTimeout);
1254
+ this._loadTimeout = null;
1255
+ }
1256
+ } else {
1257
+ return;
1258
+ }
1259
+ }
1260
+
1261
+ this._loadingInProgress = true;
1262
+
1263
+ // Nettoyer tous les timers en cours
1264
+ clearTimeout(this.autoTransitionTimer);
1265
+ if (this._loadTimeout) {
1266
+ clearTimeout(this._loadTimeout);
1267
+ this._loadTimeout = null;
1268
+ }
1269
+
1270
+ const pref = this._prefetchCache.get(videoSrc);
1271
+ if (pref && (pref.readyState >= 2 || pref.buffered.length > 0)) {
1272
+ const source = this.inactiveVideo.querySelector("source");
1273
+ source.setAttribute("src", videoSrc);
1274
+ try {
1275
+ this.inactiveVideo.currentTime = 0;
1276
+ } catch {}
1277
+ this.inactiveVideo.load();
1278
+ } else {
1279
+ this.inactiveVideo.querySelector("source").setAttribute("src", videoSrc);
1280
+ this.inactiveVideo.load();
1281
+ }
1282
+
1283
+ // Stocker les références aux handlers pour pouvoir les nettoyer
1284
+ let fired = false;
1285
+ const onReady = () => {
1286
+ if (fired) return;
1287
+ fired = true;
1288
+ this._loadingInProgress = false;
1289
+ if (this._loadTimeout) {
1290
+ clearTimeout(this._loadTimeout);
1291
+ this._loadTimeout = null;
1292
+ }
1293
+ this.inactiveVideo.removeEventListener("canplay", this._currentLoadHandler);
1294
+ this.inactiveVideo.removeEventListener("loadeddata", this._currentLoadHandler);
1295
+ this.inactiveVideo.removeEventListener("error", this._currentErrorHandler);
1296
+ this.performSwitch();
1297
+ };
1298
+ this._currentLoadHandler = onReady;
1299
+
1300
+ const folder = getCharacterInfo(this.characterName).videoFolder;
1301
+ const fallbackVideo = `${folder}neutral/neutral-gentle-breathing.mp4`;
1302
+
1303
+ this._currentErrorHandler = e => {
1304
+ console.warn(`Error loading video: ${videoSrc}, falling back to: ${fallbackVideo}`);
1305
+ this._loadingInProgress = false;
1306
+ if (this._loadTimeout) {
1307
+ clearTimeout(this._loadTimeout);
1308
+ this._loadTimeout = null;
1309
+ }
1310
+ if (videoSrc !== fallbackVideo) {
1311
+ // Try fallback video
1312
+ this.loadAndSwitchVideo(fallbackVideo, "high");
1313
+ } else {
1314
+ // Ultimate fallback: try any neutral video
1315
+ console.error(`Fallback video also failed: ${fallbackVideo}. Trying ultimate fallback.`);
1316
+ const neutralVideos = this.videoCategories.neutral || [];
1317
+ if (neutralVideos.length > 0) {
1318
+ // Try a different neutral video
1319
+ const ultimateFallback = neutralVideos.find(video => video !== fallbackVideo);
1320
+ if (ultimateFallback) {
1321
+ this.loadAndSwitchVideo(ultimateFallback, "high");
1322
+ } else {
1323
+ // Last resort: try first neutral video anyway
1324
+ this.loadAndSwitchVideo(neutralVideos[0], "high");
1325
+ }
1326
+ } else {
1327
+ // Critical error: no neutral videos available
1328
+ console.error("CRITICAL: No neutral videos available!");
1329
+ this._switchInProgress = false;
1330
+ }
1331
+ }
1332
+ };
1333
+
1334
+ this.inactiveVideo.addEventListener("loadeddata", this._currentLoadHandler, { once: true });
1335
+ this.inactiveVideo.addEventListener("canplay", this._currentLoadHandler, { once: true });
1336
+ this.inactiveVideo.addEventListener("error", this._currentErrorHandler, { once: true });
1337
+
1338
+ if (this.inactiveVideo.readyState >= 2) {
1339
+ queueMicrotask(() => onReady());
1340
+ }
1341
+
1342
+ this._loadTimeout = setTimeout(() => {
1343
+ if (!fired) {
1344
+ if (this.inactiveVideo.readyState >= 2) {
1345
+ onReady();
1346
+ } else {
1347
+ this._currentErrorHandler();
1348
+ }
1349
+ }
1350
+ }, 3000);
1351
+ }
1352
+
1353
+ usePreloadedVideo(preloadedVideo, videoSrc) {
1354
+ const source = this.inactiveVideo.querySelector("source");
1355
+ source.setAttribute("src", videoSrc);
1356
+
1357
+ this.inactiveVideo.currentTime = 0;
1358
+ this.inactiveVideo.load();
1359
+
1360
+ this._currentLoadHandler = () => {
1361
+ this._loadingInProgress = false;
1362
+ this.performSwitch();
1363
+ };
1364
+
1365
+ this.inactiveVideo.addEventListener("canplay", this._currentLoadHandler, { once: true });
1366
+ }
1367
+
1368
+ performSwitch() {
1369
+ // Prevent rapid double toggles
1370
+ if (this._switchInProgress) {
1371
+ return;
1372
+ }
1373
+ this._switchInProgress = true;
1374
+
1375
+ this.activeVideo.classList.remove("active");
1376
+ this.inactiveVideo.classList.add("active");
1377
+ const prevActive = this.activeVideo;
1378
+ const prevInactive = this.inactiveVideo;
1379
+ this.activeVideo = prevInactive;
1380
+ this.inactiveVideo = prevActive;
1381
+
1382
+ const playPromise = this.activeVideo.play();
1383
+ if (playPromise && typeof playPromise.then === "function") {
1384
+ playPromise
1385
+ .then(() => {
1386
+ // Reduced logging for video playing events
1387
+ this._switchInProgress = false;
1388
+ // Configurer les event listeners APRÈS que la vidéo commence à jouer
1389
+ this.setupEventListenersForContext(this.currentContext);
1390
+ })
1391
+ .catch(error => {
1392
+ console.warn("Failed to play video:", error);
1393
+ // Revert to previous video to avoid frozen state
1394
+ this.activeVideo.classList.remove("active");
1395
+ this.inactiveVideo.classList.add("active");
1396
+ const tmp = this.activeVideo;
1397
+ this.activeVideo = this.inactiveVideo;
1398
+ this.inactiveVideo = tmp;
1399
+ try {
1400
+ this.activeVideo.play().catch(() => {});
1401
+ } catch {}
1402
+ this._switchInProgress = false;
1403
+ // Even in case of error, configure listeners
1404
+ this.setupEventListenersForContext(this.currentContext);
1405
+ });
1406
+ } else {
1407
+ // Fallback si pas de Promise
1408
+ this._switchInProgress = false;
1409
+ this.setupEventListenersForContext(this.currentContext);
1410
+ }
1411
+ }
1412
+
1413
+ _prefetch(src) {
1414
+ if (!src || this._prefetchCache.has(src) || this._prefetchInFlight.has(src)) return;
1415
+ if (this._prefetchCache.size + this._prefetchInFlight.size >= this._maxPrefetch) return;
1416
+ this._prefetchInFlight.add(src);
1417
+ const v = document.createElement("video");
1418
+ v.preload = "auto";
1419
+ v.muted = true;
1420
+ v.playsInline = true;
1421
+ v.src = src;
1422
+ const cleanup = () => {
1423
+ v.oncanplaythrough = null;
1424
+ v.oncanplay = null;
1425
+ v.onerror = null;
1426
+ this._prefetchInFlight.delete(src);
1427
+ };
1428
+ v.oncanplay = () => {
1429
+ this._prefetchCache.set(src, v);
1430
+ cleanup();
1431
+ };
1432
+ v.oncanplaythrough = () => {
1433
+ this._prefetchCache.set(src, v);
1434
+ cleanup();
1435
+ };
1436
+ v.onerror = () => {
1437
+ cleanup();
1438
+ };
1439
+ try {
1440
+ v.load();
1441
+ } catch {}
1442
+ }
1443
+
1444
+ _prefetchLikely(category) {
1445
+ const list = this.videoCategories[category] || [];
1446
+ // Prefetch 1-2 next likely videos different from current
1447
+ const current = this.activeVideo?.querySelector("source")?.getAttribute("src") || null;
1448
+ const candidates = list.filter(s => s && s !== current).slice(0, 2);
1449
+ candidates.forEach(src => this._prefetch(src));
1450
+ }
1451
+
1452
+ // DIAGNOSTIC AND DEBUG METHODS
1453
+ getCurrentVideoInfo() {
1454
+ const currentSrc = this.activeVideo.querySelector("source").getAttribute("src");
1455
+ return {
1456
+ currentVideo: currentSrc,
1457
+ context: this.currentContext,
1458
+ emotion: this.currentEmotion,
1459
+ category: this.determineCategory(this.currentContext, this.currentEmotion)
1460
+ };
1461
+ }
1462
+
1463
+ // METHODS TO ANALYZE EMOTIONS FROM TEXT
1464
+ analyzeTextEmotion(text) {
1465
+ // Use unified emotion system
1466
+ return window.kimiAnalyzeEmotion ? window.kimiAnalyzeEmotion(text, "auto") : "neutral";
1467
+ } // CLEANUP
1468
+ destroy() {
1469
+ clearTimeout(this.autoTransitionTimer);
1470
+ this.autoTransitionTimer = null;
1471
+ if (this._visibilityHandler) {
1472
+ document.removeEventListener("visibilitychange", this._visibilityHandler);
1473
+ this._visibilityHandler = null;
1474
+ }
1475
+ }
1476
+
1477
+ // Utilitaire pour déterminer la catégorie vidéo selon la moyenne des traits
1478
+ setMoodByPersonality(traits) {
1479
+ if (this._stickyContext === "dancing" || this.currentContext === "dancing") return;
1480
+ const category = getMoodCategoryFromPersonality(traits);
1481
+ // Normalize emotion so validation uses base emotion labels
1482
+ let emotion = category;
1483
+ if (category === "speakingPositive") emotion = "positive";
1484
+ else if (category === "speakingNegative") emotion = "negative";
1485
+ // For other categories (neutral, listening, dancing) emotion can equal category
1486
+ this.switchToContext(category, emotion, null, traits, traits.affection);
1487
+ }
1488
+
1489
+ _cleanupLoadingHandlers() {
1490
+ if (this._currentLoadHandler) {
1491
+ this.inactiveVideo.removeEventListener("canplay", this._currentLoadHandler);
1492
+ this.inactiveVideo.removeEventListener("loadeddata", this._currentLoadHandler);
1493
+ this._currentLoadHandler = null;
1494
+ }
1495
+ if (this._currentErrorHandler) {
1496
+ this.inactiveVideo.removeEventListener("error", this._currentErrorHandler);
1497
+ this._currentErrorHandler = null;
1498
+ }
1499
+ if (this._loadTimeout) {
1500
+ clearTimeout(this._loadTimeout);
1501
+ this._loadTimeout = null;
1502
+ }
1503
+ this._loadingInProgress = false;
1504
+ this._switchInProgress = false;
1505
+ }
1506
+ }
1507
+
1508
+ function getMoodCategoryFromPersonality(traits) {
1509
+ // Use unified emotion system
1510
+ if (window.kimiEmotionSystem) {
1511
+ return window.kimiEmotionSystem.getMoodCategoryFromPersonality(traits);
1512
+ }
1513
+
1514
+ // Fallback (should not be reached)
1515
+ const keys = ["affection", "romance", "empathy", "playfulness", "humor"];
1516
+ let sum = 0;
1517
+ let count = 0;
1518
+ keys.forEach(key => {
1519
+ if (typeof traits[key] === "number") {
1520
+ sum += traits[key];
1521
+ count++;
1522
+ }
1523
+ });
1524
+ const avg = count > 0 ? sum / count : 50;
1525
+
1526
+ if (avg >= 80) return "speakingPositive";
1527
+ if (avg >= 60) return "neutral";
1528
+ if (avg >= 40) return "neutral";
1529
+ if (avg >= 20) return "speakingNegative";
1530
+ return "speakingNegative";
1531
+ }
1532
+
1533
+ // Centralized initialization manager
1534
+ class KimiInitManager {
1535
+ constructor() {
1536
+ this.managers = new Map();
1537
+ this.initOrder = [];
1538
+ this.isInitialized = false;
1539
+ }
1540
+
1541
+ register(name, managerFactory, dependencies = [], delay = 0) {
1542
+ this.managers.set(name, {
1543
+ factory: managerFactory,
1544
+ dependencies,
1545
+ delay,
1546
+ instance: null,
1547
+ initialized: false
1548
+ });
1549
+ }
1550
+
1551
+ async initializeAll() {
1552
+ if (this.isInitialized) return;
1553
+
1554
+ // Sort by dependencies and delays
1555
+ const sortedManagers = this.topologicalSort();
1556
+
1557
+ for (const managerName of sortedManagers) {
1558
+ await this.initializeManager(managerName);
1559
+ }
1560
+
1561
+ this.isInitialized = true;
1562
+ }
1563
+ async initializeManager(name) {
1564
+ const manager = this.managers.get(name);
1565
+ if (!manager || manager.initialized) return;
1566
+
1567
+ // Wait for dependencies
1568
+ for (const dep of manager.dependencies) {
1569
+ await this.initializeManager(dep);
1570
+ }
1571
+
1572
+ // Apply delay if necessary
1573
+ if (manager.delay > 0) {
1574
+ await new Promise(resolve => setTimeout(resolve, manager.delay));
1575
+ }
1576
+
1577
+ try {
1578
+ manager.instance = await manager.factory();
1579
+ manager.initialized = true;
1580
+ } catch (error) {
1581
+ console.error(`Error during initialization of ${name}:`, error);
1582
+ throw error;
1583
+ }
1584
+ }
1585
+
1586
+ topologicalSort() {
1587
+ // Simple implementation of topological sort
1588
+ const sorted = [];
1589
+ const visited = new Set();
1590
+ const temp = new Set();
1591
+
1592
+ const visit = name => {
1593
+ if (temp.has(name)) {
1594
+ throw new Error(`Circular dependency detected: ${name}`);
1595
+ }
1596
+ if (visited.has(name)) return;
1597
+
1598
+ temp.add(name);
1599
+ const manager = this.managers.get(name);
1600
+
1601
+ for (const dep of manager.dependencies) {
1602
+ visit(dep);
1603
+ }
1604
+
1605
+ temp.delete(name);
1606
+ visited.add(name);
1607
+ sorted.push(name);
1608
+ };
1609
+
1610
+ for (const name of this.managers.keys()) {
1611
+ visit(name);
1612
+ }
1613
+
1614
+ return sorted;
1615
+ }
1616
+
1617
+ getInstance(name) {
1618
+ const manager = this.managers.get(name);
1619
+ return manager ? manager.instance : null;
1620
+ }
1621
+ }
1622
+
1623
+ // Utility class for DOM manipulations
1624
+ class KimiDOMUtils {
1625
+ static get(selector) {
1626
+ return document.querySelector(selector);
1627
+ }
1628
+ static getAll(selector) {
1629
+ return document.querySelectorAll(selector);
1630
+ }
1631
+ static setText(selector, text) {
1632
+ const el = this.get(selector);
1633
+ if (el) el.textContent = text;
1634
+ }
1635
+ static setValue(selector, value) {
1636
+ const el = this.get(selector);
1637
+ if (el) el.value = value;
1638
+ }
1639
+ static show(selector) {
1640
+ const el = this.get(selector);
1641
+ if (el) el.style.display = "";
1642
+ }
1643
+ static hide(selector) {
1644
+ const el = this.get(selector);
1645
+ if (el) el.style.display = "none";
1646
+ }
1647
+ static toggle(selector) {
1648
+ const el = this.get(selector);
1649
+ if (el) el.style.display = el.style.display === "none" ? "" : "none";
1650
+ }
1651
+ static addClass(selector, className) {
1652
+ const el = this.get(selector);
1653
+ if (el) el.classList.add(className);
1654
+ }
1655
+ static removeClass(selector, className) {
1656
+ const el = this.get(selector);
1657
+ if (el) el.classList.remove(className);
1658
+ }
1659
+ static transition(selector, property, value, duration = 300) {
1660
+ const el = this.get(selector);
1661
+ if (el) {
1662
+ el.style.transition = property + " " + duration + "ms";
1663
+ el.style[property] = value;
1664
+ }
1665
+ }
1666
+ }
1667
+
1668
+ // Déclaration complète de la classe KimiOverlayManager
1669
+ class KimiOverlayManager {
1670
+ constructor() {
1671
+ this.overlays = {};
1672
+ this._initAll();
1673
+ }
1674
+ _initAll() {
1675
+ const overlayIds = ["chat-container", "settings-overlay", "help-overlay"];
1676
+ overlayIds.forEach(id => {
1677
+ const el = document.getElementById(id);
1678
+ if (el) {
1679
+ this.overlays[id] = el;
1680
+ if (id !== "chat-container") {
1681
+ el.addEventListener("click", e => {
1682
+ if (e.target === el) {
1683
+ this.close(id);
1684
+ }
1685
+ });
1686
+ }
1687
+ }
1688
+ });
1689
+ }
1690
+ open(name) {
1691
+ const el = this.overlays[name];
1692
+ if (el) el.classList.add("visible");
1693
+ }
1694
+ close(name) {
1695
+ const el = this.overlays[name];
1696
+ if (el) el.classList.remove("visible");
1697
+ // Ensure background video resumes after closing any overlay
1698
+ const kv = window.kimiVideo;
1699
+ if (kv && kv.activeVideo) {
1700
+ try {
1701
+ const v = kv.activeVideo;
1702
+ if (v.ended) {
1703
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
1704
+ } else if (v.paused) {
1705
+ v.play().catch(() => {
1706
+ if (typeof kv.returnToNeutral === "function") kv.returnToNeutral();
1707
+ });
1708
+ }
1709
+ } catch {}
1710
+ }
1711
+ }
1712
+ toggle(name) {
1713
+ const el = this.overlays[name];
1714
+ if (el) el.classList.toggle("visible");
1715
+ }
1716
+ isOpen(name) {
1717
+ const el = this.overlays[name];
1718
+ return el ? el.classList.contains("visible") : false;
1719
+ }
1720
+ }
1721
+
1722
+ function getCharacterInfo(characterName) {
1723
+ return window.KIMI_CHARACTERS[characterName] || window.KIMI_CHARACTERS.kimi;
1724
+ }
1725
+
1726
+ // Restauration de la classe KimiTabManager
1727
+ class KimiTabManager {
1728
+ constructor(options = {}) {
1729
+ this.settingsOverlay = document.getElementById("settings-overlay");
1730
+ this.settingsTabs = document.querySelectorAll(".settings-tab");
1731
+ this.tabContents = document.querySelectorAll(".tab-content");
1732
+ this.settingsContent = document.querySelector(".settings-content");
1733
+ this.onTabChange = options.onTabChange || null;
1734
+ this.resizeObserver = null;
1735
+ this.init();
1736
+ }
1737
+
1738
+ init() {
1739
+ this.settingsTabs.forEach(tab => {
1740
+ tab.addEventListener("click", () => {
1741
+ this.activateTab(tab.dataset.tab);
1742
+ });
1743
+ });
1744
+ const activeTab = document.querySelector(".settings-tab.active");
1745
+ if (activeTab) this.activateTab(activeTab.dataset.tab);
1746
+ this.setupResizeObserver();
1747
+ this.setupModalObserver();
1748
+ }
1749
+
1750
+ activateTab(tabName) {
1751
+ this.settingsTabs.forEach(tab => {
1752
+ if (tab.dataset.tab === tabName) tab.classList.add("active");
1753
+ else tab.classList.remove("active");
1754
+ });
1755
+ this.tabContents.forEach(content => {
1756
+ if (content.dataset.tab === tabName) content.classList.add("active");
1757
+ else content.classList.remove("active");
1758
+ });
1759
+ if (this.onTabChange) this.onTabChange(tabName);
1760
+ setTimeout(() => this.adjustTabsForScrollbar(), 100);
1761
+ if (window.innerWidth <= 768) {
1762
+ const tab = Array.from(this.settingsTabs).find(t => t.dataset.tab === tabName);
1763
+ if (tab) tab.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
1764
+ }
1765
+ }
1766
+
1767
+ setupResizeObserver() {
1768
+ if ("ResizeObserver" in window && this.settingsContent) {
1769
+ this.resizeObserver = new ResizeObserver(() => {
1770
+ this.adjustTabsForScrollbar();
1771
+ });
1772
+ this.resizeObserver.observe(this.settingsContent);
1773
+ }
1774
+ }
1775
+
1776
+ setupModalObserver() {
1777
+ if (!this.settingsOverlay) return;
1778
+ const observer = new MutationObserver(mutations => {
1779
+ mutations.forEach(mutation => {
1780
+ if (mutation.type === "attributes" && mutation.attributeName === "class") {
1781
+ if (this.settingsOverlay.classList.contains("visible")) {
1782
+ // ...
1783
+ }
1784
+ }
1785
+ });
1786
+ });
1787
+ observer.observe(this.settingsOverlay, { attributes: true, attributeFilter: ["class"] });
1788
+ }
1789
+
1790
+ adjustTabsForScrollbar() {
1791
+ if (!this.settingsContent || !this.settingsTabs.length) return;
1792
+ const tabsContainer = document.querySelector(".settings-tabs");
1793
+ const hasVerticalScrollbar = this.settingsContent.scrollHeight > this.settingsContent.clientHeight;
1794
+ if (hasVerticalScrollbar) {
1795
+ const scrollbarWidth = this.settingsContent.offsetWidth - this.settingsContent.clientWidth;
1796
+ tabsContainer.classList.add("compressed");
1797
+ tabsContainer.style.paddingRight = `${Math.max(scrollbarWidth, 8)}px`;
1798
+ tabsContainer.style.boxSizing = "border-box";
1799
+ const tabs = tabsContainer.querySelectorAll(".settings-tab");
1800
+ const availableWidth = tabsContainer.clientWidth - scrollbarWidth;
1801
+ const tabCount = tabs.length;
1802
+ const idealTabWidth = availableWidth / tabCount;
1803
+ tabs.forEach(tab => {
1804
+ if (idealTabWidth < 140) {
1805
+ tab.style.fontSize = "0.85rem";
1806
+ tab.style.padding = "14px 10px";
1807
+ } else if (idealTabWidth < 160) {
1808
+ tab.style.fontSize = "0.95rem";
1809
+ tab.style.padding = "15px 12px";
1810
+ } else {
1811
+ tab.style.fontSize = "1rem";
1812
+ tab.style.padding = "16px 16px";
1813
+ }
1814
+ });
1815
+ } else {
1816
+ tabsContainer.classList.remove("compressed");
1817
+ tabsContainer.style.paddingRight = "";
1818
+ tabsContainer.style.boxSizing = "";
1819
+ const tabs = tabsContainer.querySelectorAll(".settings-tab");
1820
+ tabs.forEach(tab => {
1821
+ tab.style.fontSize = "";
1822
+ tab.style.padding = "";
1823
+ });
1824
+ }
1825
+ }
1826
+ }
1827
+
1828
+ class KimiUIEventManager {
1829
+ constructor() {
1830
+ this.events = [];
1831
+ }
1832
+ addEvent(target, type, handler, options) {
1833
+ target.addEventListener(type, handler, options);
1834
+ this.events.push({ target, type, handler, options });
1835
+ }
1836
+ removeAll() {
1837
+ for (const { target, type, handler, options } of this.events) {
1838
+ target.removeEventListener(type, handler, options);
1839
+ }
1840
+ this.events = [];
1841
+ }
1842
+ }
1843
+
1844
+ class KimiFormManager {
1845
+ constructor(options = {}) {
1846
+ this.db = options.db || null;
1847
+ this.memory = options.memory || null;
1848
+ this._autoInit = options.autoInit === true;
1849
+ if (this._autoInit) {
1850
+ this._initSliders();
1851
+ }
1852
+ }
1853
+ init() {
1854
+ this._initSliders();
1855
+ }
1856
+ _initSliders() {
1857
+ document.querySelectorAll(".kimi-slider").forEach(slider => {
1858
+ const valueSpan = document.getElementById(slider.id + "-value");
1859
+ if (valueSpan) {
1860
+ valueSpan.textContent = slider.value;
1861
+ }
1862
+ slider.addEventListener("input", async e => {
1863
+ if (valueSpan) valueSpan.textContent = slider.value;
1864
+ if (this.db) {
1865
+ const settingName = slider.id.replace("trait-", "").replace("voice-", "").replace("llm-", "");
1866
+ if (slider.id.startsWith("trait-")) {
1867
+ const selectedCharacter = await this.db.getSelectedCharacter();
1868
+ await this.db.setPersonalityTrait(settingName, parseInt(slider.value), selectedCharacter);
1869
+
1870
+ // Update memory cache if available
1871
+ if (this.memory && settingName === "affection") {
1872
+ this.memory.affectionTrait = parseInt(slider.value);
1873
+ if (this.memory.updateFavorabilityBar) {
1874
+ this.memory.updateFavorabilityBar();
1875
+ }
1876
+ }
1877
+
1878
+ // Update video context based on new personality values
1879
+ if (window.kimiVideo && window.kimiVideo.setMoodByPersonality) {
1880
+ const allTraits = await this.db.getAllPersonalityTraits(selectedCharacter);
1881
+ window.kimiVideo.setMoodByPersonality(allTraits);
1882
+ }
1883
+
1884
+ // Favorability bar is automatically updated by KimiMemory system
1885
+ } else if (slider.id.startsWith("voice-")) {
1886
+ await this.db.setPreference(settingName, parseFloat(slider.value));
1887
+ if (window.voiceManager && window.voiceManager.updateSettings) {
1888
+ window.voiceManager.updateSettings();
1889
+ }
1890
+ } else {
1891
+ await this.db.setPreference(settingName, parseFloat(slider.value));
1892
+
1893
+ // Update LLM settings if needed
1894
+ if (slider.id.startsWith("llm-") && window.kimiLLM) {
1895
+ if (window.kimiLLM.updateSettings) {
1896
+ window.kimiLLM.updateSettings();
1897
+ }
1898
+ }
1899
+ }
1900
+ }
1901
+ });
1902
+ });
1903
+ }
1904
+ }
1905
+
1906
+ class KimiUIStateManager {
1907
+ constructor() {
1908
+ this.state = {
1909
+ overlays: {},
1910
+ activeTab: null,
1911
+ favorability: 65,
1912
+ transcript: "",
1913
+ chatOpen: false,
1914
+ settingsOpen: false,
1915
+ micActive: false,
1916
+ sliders: {}
1917
+ };
1918
+ this.overlayManager = window.kimiOverlayManager || null;
1919
+ this.tabManager = window.kimiTabManager || null;
1920
+ this.formManager = window.kimiFormManager || null;
1921
+ }
1922
+ setOverlay(name, visible) {
1923
+ this.state.overlays[name] = visible;
1924
+ if (this.overlayManager) {
1925
+ if (visible) this.overlayManager.open(name);
1926
+ else this.overlayManager.close(name);
1927
+ }
1928
+ }
1929
+ setActiveTab(tabName) {
1930
+ this.state.activeTab = tabName;
1931
+ if (this.tabManager) this.tabManager.activateTab(tabName);
1932
+ }
1933
+ setFavorability(value) {
1934
+ const v = Number(value) || 0;
1935
+ const clamped = Math.max(0, Math.min(100, v));
1936
+ this.state.favorability = clamped;
1937
+ window.KimiDOMUtils.setText("#favorability-text", `${clamped.toFixed(2)}%`);
1938
+ window.KimiDOMUtils.get("#favorability-bar").style.width = `${clamped}%`;
1939
+ }
1940
+ setTranscript(text) {
1941
+ this.state.transcript = text;
1942
+ window.KimiDOMUtils.setText("#transcript", text);
1943
+ }
1944
+ setChatOpen(open) {
1945
+ this.state.chatOpen = open;
1946
+ this.setOverlay("chat-container", open);
1947
+ }
1948
+ setSettingsOpen(open) {
1949
+ this.state.settingsOpen = open;
1950
+ this.setOverlay("settings-overlay", open);
1951
+ }
1952
+ setMicActive(active) {
1953
+ this.state.micActive = active;
1954
+ window.KimiDOMUtils.get("#mic-button").classList.toggle("active", active);
1955
+ }
1956
+ setSlider(id, value) {
1957
+ this.state.sliders[id] = value;
1958
+ if (this.formManager) {
1959
+ const slider = document.getElementById(id);
1960
+ if (slider) slider.value = value;
1961
+ const valueSpan = document.getElementById(id + "-value");
1962
+ if (valueSpan) valueSpan.textContent = value;
1963
+ }
1964
+ }
1965
+ getState() {
1966
+ return { ...this.state };
1967
+ }
1968
+ }
1969
+
1970
+ // SIMPLE Fallback management - BASIC ONLY
1971
+ window.KimiFallbackManager = {
1972
+ getFallbackMessage: function (errorType, customMessage = null) {
1973
+ const i18n = window.kimiI18nManager;
1974
+
1975
+ // If i18n is available, try to get translated message
1976
+ if (i18n && typeof i18n.t === "function") {
1977
+ if (customMessage) {
1978
+ const translated = i18n.t(customMessage);
1979
+ if (translated && translated !== customMessage) {
1980
+ return translated;
1981
+ }
1982
+ }
1983
+
1984
+ const translationKey = `fallback_${errorType}`;
1985
+ const translated = i18n.t(translationKey);
1986
+ if (translated && translated !== translationKey) {
1987
+ return translated;
1988
+ }
1989
+ }
1990
+
1991
+ // Fallback to hardcoded messages in multiple languages
1992
+ const fallbacks = {
1993
+ api_missing: {
1994
+ fr: "Pour discuter avec moi, ajoute ta clé API du provider choisi dans les paramètres ! 💕",
1995
+ en: "To chat with me, add your selected provider API key in settings! 💕",
1996
+ es: "Para chatear conmigo, agrega la clave API de tu proveedor en configuración! 💕",
1997
+ de: "Um mit mir zu chatten, füge deinen Anbieter-API-Schlüssel in den Einstellungen hinzu! 💕",
1998
+ it: "Per chattare con me, aggiungi la chiave API del provider nelle impostazioni! 💕"
1999
+ },
2000
+ api_error: {
2001
+ fr: "Désolée, le service IA est temporairement indisponible. Veuillez réessayer plus tard.",
2002
+ en: "Sorry, the AI service is temporarily unavailable. Please try again later.",
2003
+ es: "Lo siento, el servicio de IA no está disponible temporalmente. Inténtalo de nuevo más tarde.",
2004
+ de: "Entschuldigung, der KI-Service ist vorübergehend nicht verfügbar. Bitte versuchen Sie es später erneut.",
2005
+ it: "Spiacente, il servizio IA è temporaneamente non disponibile. Riprova più tardi."
2006
+ },
2007
+ model_error: {
2008
+ fr: "Désolée, le modèle sélectionné n'est pas disponible. Veuillez choisir un autre modèle.",
2009
+ en: "Sorry, the selected model is not available. Please choose another model.",
2010
+ es: "Lo siento, el modelo seleccionado no está disponible. Elige otro modelo.",
2011
+ de: "Entschuldigung, das ausgewählte Modell ist nicht verfügbar. Bitte wählen Sie ein anderes Modell.",
2012
+ it: "Spiacente, il modello selezionato non è disponibile. Scegli un altro modello."
2013
+ }
2014
+ };
2015
+
2016
+ // Detect current language (fallback detection)
2017
+ const currentLang = this.detectCurrentLanguage();
2018
+
2019
+ if (fallbacks[errorType] && fallbacks[errorType][currentLang]) {
2020
+ return fallbacks[errorType][currentLang];
2021
+ }
2022
+
2023
+ // Ultimate fallback to English
2024
+ if (fallbacks[errorType] && fallbacks[errorType].en) {
2025
+ return fallbacks[errorType].en;
2026
+ }
2027
+
2028
+ switch (errorType) {
2029
+ case "api_missing":
2030
+ return "To chat with me, add your API key in settings! 💕";
2031
+ case "api_error":
2032
+ case "api":
2033
+ return "Sorry, the AI service is temporarily unavailable. Please try again later.";
2034
+ case "model_error":
2035
+ case "model":
2036
+ return "Sorry, the selected model is not available. Please choose another model or check your configuration.";
2037
+ case "network_error":
2038
+ case "network":
2039
+ return "Sorry, I cannot respond because there is no internet connection.";
2040
+ case "technical_error":
2041
+ case "technical":
2042
+ return "Sorry, I am unable to answer due to a technical issue.";
2043
+ case "general_error":
2044
+ default:
2045
+ return "Sorry my love, I am having a little technical issue! 💕";
2046
+ }
2047
+ },
2048
+
2049
+ detectCurrentLanguage: function () {
2050
+ // Try to get language from various sources
2051
+
2052
+ // 1. Try from language selector if available
2053
+ const langSelect = document.getElementById("language-selection");
2054
+ if (langSelect && langSelect.value) {
2055
+ return langSelect.value;
2056
+ }
2057
+
2058
+ // 2. Try from HTML lang attribute
2059
+ const htmlLang = document.documentElement.lang;
2060
+ if (htmlLang) {
2061
+ return htmlLang.split("-")[0]; // Get just the language part
2062
+ }
2063
+
2064
+ // 3. Try from browser language
2065
+ const browserLang = navigator.language || navigator.userLanguage;
2066
+ if (browserLang) {
2067
+ return browserLang.split("-")[0];
2068
+ }
2069
+
2070
+ // 4. Default to English (as seems to be the default for this app)
2071
+ return "en";
2072
+ },
2073
+
2074
+ showFallbackResponse: async function (errorType, customMessage = null) {
2075
+ const message = this.getFallbackMessage(errorType, customMessage);
2076
+
2077
+ // Add to chat
2078
+ if (window.addMessageToChat) {
2079
+ window.addMessageToChat("kimi", message);
2080
+ }
2081
+
2082
+ // Speak if available
2083
+ if (window.voiceManager && window.voiceManager.speak) {
2084
+ window.voiceManager.speak(message);
2085
+ }
2086
+
2087
+ // SIMPLE: Always show neutral videos in fallback mode
2088
+ if (window.kimiVideo && window.kimiVideo.switchToContext) {
2089
+ window.kimiVideo.switchToContext("neutral", "neutral");
2090
+ }
2091
+
2092
+ return message;
2093
+ }
2094
+ };
2095
+
2096
+ window.KimiBaseManager = KimiBaseManager;
2097
+ window.KimiVideoManager = KimiVideoManager;
2098
+ window.KimiSecurityUtils = KimiSecurityUtils;
2099
+ window.KimiCacheManager = new KimiCacheManager(); // Create global instance
2100
+ window.KimiInitManager = KimiInitManager;
2101
+ window.KimiDOMUtils = KimiDOMUtils;
2102
+ window.KimiOverlayManager = KimiOverlayManager;
2103
+ window.KimiTabManager = KimiTabManager;
2104
+ window.KimiUIEventManager = KimiUIEventManager;
2105
+ window.KimiFormManager = KimiFormManager;
2106
+ window.KimiUIStateManager = KimiUIStateManager;
kimi-js/kimi-voices.js ADDED
@@ -0,0 +1,1136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ===== KIMI VOICE MANAGEMENT MODULE =====
2
+ class KimiVoiceManager {
3
+ constructor(database, memory) {
4
+ this.db = database;
5
+ this.memory = memory;
6
+ this.isInitialized = false;
7
+
8
+ // Voice properties
9
+ this.speechSynthesis = window.speechSynthesis;
10
+ this.kimiEnglishVoice = null;
11
+ this.availableVoices = [];
12
+
13
+ // Speech recognition
14
+ this.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
15
+ this.recognition = null;
16
+ this.isListening = false;
17
+ this.isStoppingVolontaire = false;
18
+
19
+ // DOM elements
20
+ this.micButton = null;
21
+ this.transcriptContainer = null;
22
+ this.transcriptText = null;
23
+
24
+ // Callback for voice message analysis
25
+ this.onSpeechAnalysis = null;
26
+
27
+ // Reference to mic handler function for removal
28
+ this.handleMicClick = null;
29
+
30
+ this.transcriptHideTimeout = null;
31
+ this.listeningTimeout = null;
32
+
33
+ // Selected character for responses
34
+ this.selectedCharacter = "Kimi";
35
+
36
+ // Speaking flag
37
+ this.isSpeaking = false;
38
+
39
+ // Auto-stop listening duration (in milliseconds)
40
+ this.autoStopDuration = 15000; // 15 seconds
41
+
42
+ // Silence timeout after final transcript (in milliseconds)
43
+ this.silenceTimeout = 2200; // 2.2 seconds
44
+
45
+ // Track if microphone permission has been granted
46
+ this.micPermissionGranted = false;
47
+
48
+ // Debounce flag for toggle microphone
49
+ this._toggleDebounce = false;
50
+
51
+ // Browser detection
52
+ this.browser = this._detectBrowser();
53
+ }
54
+
55
+ // ===== INITIALIZATION =====
56
+ async init() {
57
+ // Avoid double initialization
58
+ if (this.isInitialized) {
59
+ console.log("VoiceManager already initialized, ignored");
60
+ return true;
61
+ }
62
+
63
+ try {
64
+ // Initialize DOM elements with verification
65
+ this.micButton = document.getElementById("mic-button");
66
+ this.transcriptContainer = document.querySelector(".transcript-container");
67
+ this.transcriptText = document.getElementById("transcript");
68
+
69
+ if (!this.micButton) {
70
+ console.warn("Microphone button not found in DOM!");
71
+ return false;
72
+ }
73
+
74
+ // Initialize voice synthesis
75
+ await this.initVoices();
76
+ this.setupVoicesChangedListener();
77
+ this.setupLanguageSelector();
78
+
79
+ // Initialize speech recognition
80
+ this.setupSpeechRecognition();
81
+ this.setupMicrophoneButton();
82
+
83
+ // Check current microphone permission status
84
+ await this.checkMicrophonePermission();
85
+
86
+ // Initialize selected character
87
+ if (this.db && typeof this.db.getSelectedCharacter === "function") {
88
+ const char = await this.db.getSelectedCharacter();
89
+ if (char) this.selectedCharacter = char;
90
+ }
91
+
92
+ this.isInitialized = true;
93
+ console.log("🎤 VoiceManager initialized successfully");
94
+ return true;
95
+ } catch (error) {
96
+ console.error("Error during VoiceManager initialization:", error);
97
+ return false;
98
+ }
99
+ }
100
+
101
+ _detectBrowser() {
102
+ const ua = navigator.userAgent || "";
103
+ const isOpera = (!!window.opr && !!opr.addons) || ua.includes(" OPR/");
104
+ const isFirefox = typeof InstallTrigger !== "undefined" || ua.toLowerCase().includes("firefox");
105
+ const isSafari = /Safari\//.test(ua) && !/Chrom(e|ium)\//.test(ua) && !/Edg\//.test(ua);
106
+ const isEdge = /Edg\//.test(ua);
107
+ const isChrome = /Chrome\//.test(ua) && !isEdge && !isOpera;
108
+ if (isFirefox) return "firefox";
109
+ if (isOpera) return "opera";
110
+ if (isSafari) return "safari";
111
+ if (isEdge) return "edge";
112
+ if (isChrome) return "chrome";
113
+ return "unknown";
114
+ }
115
+
116
+ _getUnsupportedSRMessage() {
117
+ // Build an i18n key by browser, then fallback to English if translation system isn't ready
118
+ let key = "sr_not_supported_generic";
119
+ if (this.browser === "firefox") key = "sr_not_supported_firefox";
120
+ else if (this.browser === "opera") key = "sr_not_supported_opera";
121
+ else if (this.browser === "safari") key = "sr_not_supported_safari";
122
+ const translated = typeof window.kimiI18nManager?.t === "function" ? window.kimiI18nManager.t(key) : undefined;
123
+ // Many i18n libs return the key itself if missing; detect that and fall back to English
124
+ if (!translated || translated === key) {
125
+ if (key === "sr_not_supported_firefox") {
126
+ return "Speech recognition is not supported on Firefox. Please use Chrome, Edge, or Brave.";
127
+ }
128
+ if (key === "sr_not_supported_opera") {
129
+ return "Speech recognition may not work on Opera. Please try Chrome, Edge, or Brave.";
130
+ }
131
+ if (key === "sr_not_supported_safari") {
132
+ return "Speech recognition support varies on Safari. Prefer Chrome or Edge for best results.";
133
+ }
134
+ return "Speech recognition is not available in this browser.";
135
+ }
136
+ return translated;
137
+ }
138
+
139
+ // ===== MICROPHONE PERMISSION MANAGEMENT =====
140
+ async checkMicrophonePermission() {
141
+ try {
142
+ // Check if running on file:// protocol
143
+ if (window.location.protocol === "file:") {
144
+ console.log("🎤 Running on file:// protocol - microphone permissions will be requested each time");
145
+ this.micPermissionGranted = false;
146
+ return;
147
+ }
148
+
149
+ if (!navigator.permissions) {
150
+ console.log("🎤 Permissions API not available");
151
+ return;
152
+ }
153
+
154
+ const permissionStatus = await navigator.permissions.query({ name: "microphone" });
155
+ this.micPermissionGranted = permissionStatus.state === "granted";
156
+
157
+ console.log("🎤 Initial microphone permission status:", permissionStatus.state);
158
+
159
+ // Listen for permission changes
160
+ permissionStatus.addEventListener("change", () => {
161
+ this.micPermissionGranted = permissionStatus.state === "granted";
162
+ console.log("🎤 Microphone permission changed to:", permissionStatus.state);
163
+ });
164
+ } catch (error) {
165
+ console.log("🎤 Could not check microphone permission:", error);
166
+ this.micPermissionGranted = false;
167
+ }
168
+ }
169
+
170
+ // ===== VOICE SYNTHESIS =====
171
+ async initVoices() {
172
+ // Prevent multiple simultaneous calls
173
+ if (this._initializingVoices) {
174
+ return;
175
+ }
176
+ this._initializingVoices = true;
177
+
178
+ this.availableVoices = this.speechSynthesis.getVoices();
179
+
180
+ // Only get language from DB if not already set
181
+ if (!this.selectedLanguage) {
182
+ const selectedLanguage = await this.db?.getPreference("selectedLanguage", "en");
183
+ this.selectedLanguage = selectedLanguage || "en";
184
+ }
185
+
186
+ const savedVoice = await this.db?.getPreference("selectedVoice", "auto");
187
+
188
+ let filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().startsWith(this.selectedLanguage));
189
+ if (filteredVoices.length === 0) {
190
+ filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().includes(this.selectedLanguage));
191
+ }
192
+ if (filteredVoices.length === 0) {
193
+ // As a last resort, use any available voice rather than defaulting to English
194
+ filteredVoices = this.availableVoices;
195
+ }
196
+
197
+ if (savedVoice && savedVoice !== "auto") {
198
+ let foundVoice = filteredVoices.find(voice => voice.name === savedVoice);
199
+ if (!foundVoice) {
200
+ foundVoice = this.availableVoices.find(voice => voice.name === savedVoice);
201
+ }
202
+ if (foundVoice) {
203
+ this.kimiEnglishVoice = foundVoice;
204
+ this.updateVoiceSelector();
205
+ this._initializingVoices = false;
206
+ return;
207
+ } else if (filteredVoices.length > 0) {
208
+ this.kimiEnglishVoice = filteredVoices[0];
209
+ await this.db?.setPreference("selectedVoice", this.kimiEnglishVoice.name);
210
+ this.updateVoiceSelector();
211
+ this._initializingVoices = false;
212
+ return;
213
+ }
214
+ }
215
+
216
+ if (this.selectedLanguage && this.selectedLanguage.startsWith("fr")) {
217
+ this.kimiEnglishVoice =
218
+ filteredVoices.find(voice => voice.name.startsWith("Microsoft Eloise Online")) ||
219
+ filteredVoices.find(voice => voice.name.toLowerCase().includes("eloise")) ||
220
+ filteredVoices[0] ||
221
+ this.availableVoices[0];
222
+ } else {
223
+ this.kimiEnglishVoice =
224
+ filteredVoices.find(voice => voice.name.toLowerCase().includes("female")) ||
225
+ filteredVoices[0] ||
226
+ this.availableVoices[0];
227
+ }
228
+
229
+ if (this.kimiEnglishVoice) {
230
+ await this.db?.setPreference("selectedVoice", this.kimiEnglishVoice.name);
231
+ }
232
+
233
+ this.updateVoiceSelector();
234
+ this._initializingVoices = false;
235
+ }
236
+
237
+ updateVoiceSelector() {
238
+ const voiceSelect = document.getElementById("voice-selection");
239
+ if (!voiceSelect) return;
240
+
241
+ // Clear existing options
242
+ while (voiceSelect.firstChild) {
243
+ voiceSelect.removeChild(voiceSelect.firstChild);
244
+ }
245
+
246
+ // Add auto option
247
+ const autoOption = document.createElement("option");
248
+ autoOption.value = "auto";
249
+ autoOption.textContent = "Automatic (Best voice for selected language)";
250
+ voiceSelect.appendChild(autoOption);
251
+
252
+ let filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().startsWith(this.selectedLanguage));
253
+ if (filteredVoices.length === 0) {
254
+ filteredVoices = this.availableVoices.filter(voice => voice.lang.toLowerCase().includes(this.selectedLanguage));
255
+ }
256
+ if (filteredVoices.length === 0) {
257
+ // Show all voices if none match the selected language
258
+ filteredVoices = this.availableVoices;
259
+ }
260
+
261
+ filteredVoices.forEach(voice => {
262
+ const option = document.createElement("option");
263
+ option.value = voice.name;
264
+ option.textContent = `${voice.name} (${voice.lang})`;
265
+ if (this.kimiEnglishVoice && voice.name === this.kimiEnglishVoice.name) {
266
+ option.selected = true;
267
+ }
268
+ voiceSelect.appendChild(option);
269
+ });
270
+
271
+ voiceSelect.removeEventListener("change", this.handleVoiceChange);
272
+ voiceSelect.addEventListener("change", this.handleVoiceChange.bind(this));
273
+ }
274
+
275
+ async handleVoiceChange(e) {
276
+ if (e.target.value === "auto") {
277
+ await this.db?.setPreference("selectedVoice", "auto");
278
+ // Don't re-init voices when auto is selected to avoid loops
279
+ this.kimiEnglishVoice = null; // Reset to trigger auto-selection on next speak
280
+ } else {
281
+ this.kimiEnglishVoice = this.availableVoices.find(voice => voice.name === e.target.value);
282
+ await this.db?.setPreference("selectedVoice", e.target.value);
283
+ // Reduced logging to prevent noise
284
+ }
285
+ }
286
+
287
+ setupVoicesChangedListener() {
288
+ if (this.speechSynthesis.onvoiceschanged !== undefined) {
289
+ this.speechSynthesis.onvoiceschanged = async () => await this.initVoices();
290
+ }
291
+ }
292
+
293
+ async speak(text, options = {}) {
294
+ if (!text || !this.kimiEnglishVoice) {
295
+ console.warn("Unable to speak: empty text or voice not initialized");
296
+ return;
297
+ }
298
+ if (this.transcriptHideTimeout) {
299
+ clearTimeout(this.transcriptHideTimeout);
300
+ this.transcriptHideTimeout = null;
301
+ }
302
+ if (this.speechSynthesis.speaking) {
303
+ this.speechSynthesis.cancel();
304
+ }
305
+
306
+ // Clean text for better speech synthesis
307
+ let processedText = text
308
+ .replace(/([\p{Emoji}\p{Extended_Pictographic}])/gu, " ")
309
+ .replace(/\.\.\./g, " pause ")
310
+ .replace(/\!+/g, " ! ")
311
+ .replace(/\?+/g, " ? ")
312
+ .replace(/\.{2,}/g, " pause ")
313
+ .replace(/[,;:]+/g, ", ")
314
+ .replace(/\s+/g, " ")
315
+ .trim();
316
+
317
+ // Detect emotional content for voice adjustments
318
+ let customRate = options.rate;
319
+ if (customRate === undefined) {
320
+ customRate = this.memory?.preferences?.voiceRate;
321
+ }
322
+ if (customRate === undefined) {
323
+ customRate = window.kimiMemory?.preferences?.voiceRate;
324
+ }
325
+ if (customRate === undefined) {
326
+ const rateSlider = document.getElementById("voice-rate");
327
+ customRate = rateSlider ? parseFloat(rateSlider.value) : 1.1;
328
+ }
329
+
330
+ let customPitch = options.pitch;
331
+ if (customPitch === undefined) {
332
+ customPitch = this.memory?.preferences?.voicePitch;
333
+ }
334
+ if (customPitch === undefined) {
335
+ customPitch = window.kimiMemory?.preferences?.voicePitch;
336
+ }
337
+ if (customPitch === undefined) {
338
+ const pitchSlider = document.getElementById("voice-pitch");
339
+ customPitch = pitchSlider ? parseFloat(pitchSlider.value) : 1.1;
340
+ }
341
+
342
+ // Check for emotional indicators in original text (before processing)
343
+ const lowerText = text.toLowerCase();
344
+ if (
345
+ lowerText.includes("❤️") ||
346
+ lowerText.includes("💕") ||
347
+ lowerText.includes("😘") ||
348
+ lowerText.includes("amour") ||
349
+ lowerText.includes("love") ||
350
+ lowerText.includes("bisou")
351
+ ) {
352
+ // Tender loving content - slower and higher pitch
353
+ customRate = Math.max(0.7, customRate - 0.2);
354
+ customPitch = Math.min(1.3, customPitch + 0.1);
355
+ }
356
+
357
+ const utterance = new SpeechSynthesisUtterance(processedText);
358
+ utterance.voice = this.kimiEnglishVoice;
359
+ utterance.rate = customRate;
360
+ utterance.pitch = customPitch;
361
+
362
+ // Get volume from multiple sources with fallback hierarchy
363
+ let volume = options.volume;
364
+ if (volume === undefined) {
365
+ // Try to get from memory preferences
366
+ volume = this.memory?.preferences?.voiceVolume;
367
+ }
368
+ if (volume === undefined) {
369
+ // Try to get from kimiMemory global
370
+ volume = window.kimiMemory?.preferences?.voiceVolume;
371
+ }
372
+ if (volume === undefined) {
373
+ // Try to get directly from slider
374
+ const volumeSlider = document.getElementById("voice-volume");
375
+ volume = volumeSlider ? parseFloat(volumeSlider.value) : 0.8;
376
+ }
377
+ utterance.volume = volume;
378
+ const emotionFromText = this.analyzeTextEmotion(text);
379
+ if (window.kimiVideo && emotionFromText !== "neutral") {
380
+ requestAnimationFrame(() => {
381
+ window.kimiVideo.respondWithEmotion(emotionFromText);
382
+ });
383
+ }
384
+ if (typeof window.updatePersonalityTraitsFromEmotion === "function") {
385
+ window.updatePersonalityTraitsFromEmotion(emotionFromText, text);
386
+ }
387
+ this.showResponseWithPerfectTiming(text);
388
+
389
+ utterance.onstart = async () => {
390
+ this.isSpeaking = true;
391
+ const showTranscript = await this.db?.getPreference("showTranscript", true);
392
+ if (showTranscript && this.transcriptContainer) {
393
+ this.transcriptContainer.classList.add("visible");
394
+ } else if (this.transcriptContainer) {
395
+ this.transcriptContainer.classList.remove("visible");
396
+ }
397
+ };
398
+
399
+ utterance.onend = () => {
400
+ this.isSpeaking = false;
401
+ if (this.transcriptContainer) {
402
+ this.transcriptContainer.classList.remove("visible");
403
+ }
404
+ this.transcriptHideTimeout = null;
405
+ if (window.kimiVideo) {
406
+ // Do not force neutral if an emotion clip is still playing (speaking/dancing)
407
+ try {
408
+ const info = window.kimiVideo.getCurrentVideoInfo ? window.kimiVideo.getCurrentVideoInfo() : null;
409
+ const isEmotionClip =
410
+ info &&
411
+ (info.context === "speakingPositive" ||
412
+ info.context === "speakingNegative" ||
413
+ info.context === "dancing");
414
+ if (!isEmotionClip) {
415
+ requestAnimationFrame(() => {
416
+ window.kimiVideo.returnToNeutral();
417
+ });
418
+ }
419
+ } catch (_) {
420
+ requestAnimationFrame(() => {
421
+ window.kimiVideo.returnToNeutral();
422
+ });
423
+ }
424
+ }
425
+ };
426
+
427
+ utterance.onerror = e => {
428
+ this.isSpeaking = false;
429
+ if (this.transcriptContainer) {
430
+ this.transcriptContainer.classList.remove("visible");
431
+ }
432
+ this.transcriptHideTimeout = null;
433
+ };
434
+
435
+ this.speechSynthesis.speak(utterance);
436
+ }
437
+
438
+ // Intelligently calculate synthesis duration
439
+ calculateSpeechDuration(text, rate = 0.9) {
440
+ const baseWordsPerMinute = 150;
441
+ const adjustedWPM = baseWordsPerMinute * rate;
442
+ const wordCount = text.split(/\s+/).length;
443
+ const estimatedMinutes = wordCount / adjustedWPM;
444
+ const estimatedMilliseconds = estimatedMinutes * 60 * 1000;
445
+ const bufferTime = text.split(/[.!?]/).length * 500;
446
+ return Math.max(estimatedMilliseconds + bufferTime, 2000);
447
+ }
448
+
449
+ async showResponseWithPerfectTiming(text) {
450
+ if (!this.transcriptContainer || !this.transcriptText) return;
451
+ const showTranscript = await this.db?.getPreference("showTranscript", true);
452
+ if (!showTranscript) return;
453
+ this.transcriptText.textContent = `${this.selectedCharacter}: ${text}`;
454
+ this.transcriptContainer.classList.add("visible");
455
+ if (this.transcriptHideTimeout) {
456
+ clearTimeout(this.transcriptHideTimeout);
457
+ this.transcriptHideTimeout = null;
458
+ }
459
+ }
460
+
461
+ showResponse(text) {
462
+ this.showResponseWithPerfectTiming(text);
463
+ }
464
+
465
+ async showUserMessage(text, duration = 3000) {
466
+ if (!this.transcriptContainer || !this.transcriptText) return;
467
+ const showTranscript = await this.db?.getPreference("showTranscript", true);
468
+ if (!showTranscript) return;
469
+ if (this.transcriptHideTimeout) {
470
+ clearTimeout(this.transcriptHideTimeout);
471
+ this.transcriptHideTimeout = null;
472
+ }
473
+ this.transcriptText.textContent = text;
474
+ this.transcriptContainer.classList.add("visible");
475
+ this.transcriptHideTimeout = setTimeout(() => {
476
+ if (this.transcriptContainer) {
477
+ this.transcriptContainer.classList.remove("visible");
478
+ }
479
+ this.transcriptHideTimeout = null;
480
+ }, duration);
481
+ }
482
+
483
+ // ===== SPEECH RECOGNITION =====
484
+ setupSpeechRecognition() {
485
+ if (!this.SpeechRecognition) {
486
+ // Do not show a UI message during initial load; only log.
487
+ console.log("Your browser does not support speech recognition.");
488
+ return;
489
+ }
490
+ this.recognition = new this.SpeechRecognition();
491
+ this.recognition.continuous = true;
492
+ let langCode = this.selectedLanguage || "en";
493
+ if (langCode === "fr") langCode = "fr-FR";
494
+ if (langCode === "en") langCode = "en-US";
495
+ this.recognition.lang = langCode;
496
+ this.recognition.interimResults = true;
497
+
498
+ // Add onstart handler to confirm permission
499
+ this.recognition.onstart = () => {
500
+ if (!this.micPermissionGranted) {
501
+ this.micPermissionGranted = true;
502
+ console.log("🎤 Microphone permission confirmed via onstart");
503
+ }
504
+ };
505
+
506
+ this.recognition.onresult = async event => {
507
+ // Mark permission as granted if we get results
508
+ if (!this.micPermissionGranted) {
509
+ this.micPermissionGranted = true;
510
+ console.log("🎤 Microphone permission confirmed via onresult");
511
+ }
512
+
513
+ let final_transcript = "";
514
+ let interim_transcript = "";
515
+ for (let i = event.resultIndex; i < event.results.length; ++i) {
516
+ if (event.results[i].isFinal) {
517
+ final_transcript += event.results[i][0].transcript;
518
+ } else {
519
+ interim_transcript += event.results[i][0].transcript;
520
+ }
521
+ }
522
+ const showTranscript = await this.db?.getPreference("showTranscript", true);
523
+ if (showTranscript && this.transcriptText) {
524
+ this.transcriptText.textContent = final_transcript || interim_transcript;
525
+ if (this.transcriptContainer && (final_transcript || interim_transcript)) {
526
+ this.transcriptContainer.classList.add("visible");
527
+ }
528
+ } else if (this.transcriptContainer) {
529
+ this.transcriptContainer.classList.remove("visible");
530
+ }
531
+ if (final_transcript && this.onSpeechAnalysis) {
532
+ try {
533
+ // Auto-stop after silence timeout following final transcript
534
+ setTimeout(() => {
535
+ this.stopListening();
536
+ }, this.silenceTimeout);
537
+ (async () => {
538
+ if (typeof window.analyzeAndReact === "function") {
539
+ const response = await window.analyzeAndReact(final_transcript);
540
+ if (response) {
541
+ const chatContainer = document.getElementById("chat-container");
542
+ const chatMessages = document.getElementById("chat-messages");
543
+ if (chatContainer && chatContainer.classList.contains("visible") && chatMessages) {
544
+ const addMessageToChat =
545
+ window.addMessageToChat ||
546
+ (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
547
+ if (addMessageToChat) {
548
+ addMessageToChat("user", final_transcript);
549
+ addMessageToChat("kimi", response);
550
+ } else {
551
+ const userDiv = document.createElement("div");
552
+ userDiv.className = "message user";
553
+
554
+ const userMessageDiv = document.createElement("div");
555
+ userMessageDiv.textContent = final_transcript;
556
+
557
+ const userTimeDiv = document.createElement("div");
558
+ userTimeDiv.className = "message-time";
559
+ userTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
560
+ hour: "2-digit",
561
+ minute: "2-digit"
562
+ });
563
+
564
+ userDiv.appendChild(userMessageDiv);
565
+ userDiv.appendChild(userTimeDiv);
566
+ chatMessages.appendChild(userDiv);
567
+
568
+ const kimiDiv = document.createElement("div");
569
+ kimiDiv.className = "message kimi";
570
+
571
+ const kimiMessageDiv = document.createElement("div");
572
+ kimiMessageDiv.textContent = response;
573
+
574
+ const kimiTimeDiv = document.createElement("div");
575
+ kimiTimeDiv.className = "message-time";
576
+ kimiTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
577
+ hour: "2-digit",
578
+ minute: "2-digit"
579
+ });
580
+
581
+ kimiDiv.appendChild(kimiMessageDiv);
582
+ kimiDiv.appendChild(kimiTimeDiv);
583
+ chatMessages.appendChild(kimiDiv);
584
+ chatMessages.scrollTop = chatMessages.scrollHeight;
585
+ }
586
+ }
587
+ setTimeout(() => {
588
+ this.speak(response);
589
+ }, 500);
590
+ }
591
+ } else {
592
+ const response = await this.onSpeechAnalysis(final_transcript);
593
+ if (response) {
594
+ const chatContainer = document.getElementById("chat-container");
595
+ const chatMessages = document.getElementById("chat-messages");
596
+ if (chatContainer && chatContainer.classList.contains("visible") && chatMessages) {
597
+ const addMessageToChat =
598
+ window.addMessageToChat ||
599
+ (typeof addMessageToChat !== "undefined" ? addMessageToChat : null);
600
+ if (addMessageToChat) {
601
+ addMessageToChat("user", final_transcript);
602
+ addMessageToChat("kimi", response);
603
+ } else {
604
+ const userDiv = document.createElement("div");
605
+ userDiv.className = "message user";
606
+
607
+ const userMessageDiv = document.createElement("div");
608
+ userMessageDiv.textContent = final_transcript;
609
+
610
+ const userTimeDiv = document.createElement("div");
611
+ userTimeDiv.className = "message-time";
612
+ userTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
613
+ hour: "2-digit",
614
+ minute: "2-digit"
615
+ });
616
+
617
+ userDiv.appendChild(userMessageDiv);
618
+ userDiv.appendChild(userTimeDiv);
619
+ chatMessages.appendChild(userDiv);
620
+
621
+ const kimiDiv = document.createElement("div");
622
+ kimiDiv.className = "message kimi";
623
+
624
+ const kimiMessageDiv = document.createElement("div");
625
+ kimiMessageDiv.textContent = response;
626
+
627
+ const kimiTimeDiv = document.createElement("div");
628
+ kimiTimeDiv.className = "message-time";
629
+ kimiTimeDiv.textContent = new Date().toLocaleTimeString("en-US", {
630
+ hour: "2-digit",
631
+ minute: "2-digit"
632
+ });
633
+
634
+ kimiDiv.appendChild(kimiMessageDiv);
635
+ kimiDiv.appendChild(kimiTimeDiv);
636
+ chatMessages.appendChild(kimiDiv);
637
+ chatMessages.scrollTop = chatMessages.scrollHeight;
638
+ }
639
+ }
640
+ setTimeout(() => {
641
+ this.speak(response);
642
+ }, 500);
643
+ }
644
+ }
645
+ })();
646
+ } catch (error) {
647
+ console.error("🎤 Error during voice analysis:", error);
648
+ }
649
+ }
650
+ };
651
+
652
+ this.recognition.onerror = event => {
653
+ console.error("🎤 Speech recognition error:", event.error);
654
+ if (event.error === "not-allowed" || event.error === "service-not-allowed") {
655
+ console.log("🎤 Permission denied - stopping listening");
656
+ this.micPermissionGranted = false;
657
+ this.stopListening();
658
+ if (this.transcriptText) {
659
+ this.transcriptText.textContent =
660
+ window.kimiI18nManager?.t("mic_permission_denied") ||
661
+ "Microphone permission denied. Click again to retry.";
662
+ this.transcriptContainer?.classList.add("visible");
663
+ setTimeout(() => {
664
+ this.transcriptContainer?.classList.remove("visible");
665
+ }, 2000);
666
+ }
667
+ } else {
668
+ this.stopListening();
669
+ }
670
+ };
671
+
672
+ this.recognition.onend = () => {
673
+ console.log("🎤 Speech recognition ended");
674
+
675
+ // Clear timeout if recognition ends naturally
676
+ if (this.listeningTimeout) {
677
+ clearTimeout(this.listeningTimeout);
678
+ this.listeningTimeout = null;
679
+ }
680
+
681
+ // Always reset listening state when recognition ends
682
+ this.isListening = false;
683
+
684
+ if (this.isStoppingVolontaire) {
685
+ console.log("Voluntary stop confirmed");
686
+ this.isStoppingVolontaire = false;
687
+ if (this.micButton) {
688
+ this.micButton.classList.remove("mic-pulse-active");
689
+ this.micButton.classList.remove("is-listening");
690
+ }
691
+ return;
692
+ }
693
+
694
+ // User must click the mic button again to reactivate listening
695
+ this.isListening = false;
696
+ if (this.micButton) this.micButton.classList.remove("is-listening");
697
+ if (this.micButton) this.micButton.classList.remove("mic-pulse-active");
698
+ if (this.transcriptContainer) {
699
+ this.transcriptContainer.classList.remove("visible");
700
+ }
701
+ };
702
+ }
703
+
704
+ setupMicrophoneButton() {
705
+ if (!this.micButton) {
706
+ console.error("setupMicrophoneButton: Mic button not found!");
707
+ return;
708
+ }
709
+
710
+ // Remove any existing event listener to prevent duplicates
711
+ this.micButton.removeEventListener("click", this.handleMicClick);
712
+
713
+ // Create the click handler function
714
+ this.handleMicClick = () => {
715
+ if (!this.SpeechRecognition) {
716
+ console.warn("🎤 Speech recognition not available");
717
+ let key = "sr_not_supported_generic";
718
+ if (this.browser === "firefox") key = "sr_not_supported_firefox";
719
+ else if (this.browser === "opera") key = "sr_not_supported_opera";
720
+ else if (this.browser === "safari") key = "sr_not_supported_safari";
721
+ const message = window.kimiI18nManager?.t(key) || "Speech recognition is not available in this browser.";
722
+ if (this.transcriptText) {
723
+ this.transcriptText.textContent = message;
724
+ this.transcriptContainer?.classList.add("visible");
725
+ setTimeout(() => {
726
+ this.transcriptContainer?.classList.remove("visible");
727
+ }, 4000);
728
+ }
729
+ return;
730
+ }
731
+
732
+ if (this.isListening) {
733
+ console.log("🎤 Stopping microphone via button click");
734
+ this.stopListening();
735
+ } else {
736
+ console.log("🎤 Starting microphone via button click");
737
+ this.startListening();
738
+ }
739
+ };
740
+
741
+ // Add the event listener
742
+ this.micButton.addEventListener("click", this.handleMicClick);
743
+ console.log("🎤 Microphone button event listener setup complete");
744
+ }
745
+
746
+ async startListening() {
747
+ // Show helpful message if SR API is missing
748
+ if (!this.SpeechRecognition) {
749
+ let key = "sr_not_supported_generic";
750
+ if (this.browser === "firefox") key = "sr_not_supported_firefox";
751
+ else if (this.browser === "opera") key = "sr_not_supported_opera";
752
+ else if (this.browser === "safari") key = "sr_not_supported_safari";
753
+ const message = window.kimiI18nManager?.t(key) || "Speech recognition is not available in this browser.";
754
+ if (this.transcriptText) {
755
+ this.transcriptText.textContent = message;
756
+ this.transcriptContainer?.classList.add("visible");
757
+ setTimeout(() => {
758
+ this.transcriptContainer?.classList.remove("visible");
759
+ }, 4000);
760
+ }
761
+ return;
762
+ }
763
+ if (!this.recognition || this.isListening) return;
764
+
765
+ // Check microphone API availability
766
+ if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
767
+ console.warn("MediaDevices API not available");
768
+ if (this.transcriptText) {
769
+ this.transcriptText.textContent =
770
+ window.kimiI18nManager?.t("mic_not_supported") || "Microphone not supported in this browser.";
771
+ this.transcriptContainer?.classList.add("visible");
772
+ setTimeout(() => {
773
+ this.transcriptContainer?.classList.remove("visible");
774
+ }, 3000);
775
+ }
776
+ return;
777
+ }
778
+
779
+ // If permission was previously granted, start directly
780
+ if (this.micPermissionGranted) {
781
+ console.log("🎤 Using previously granted microphone permission");
782
+ this.startRecognitionDirectly();
783
+ return;
784
+ }
785
+
786
+ // Check current permission status
787
+ try {
788
+ const permissionStatus = await navigator.permissions.query({ name: "microphone" });
789
+ console.log("🎤 Current microphone permission status:", permissionStatus.state);
790
+
791
+ if (permissionStatus.state === "granted") {
792
+ this.micPermissionGranted = true;
793
+ this.startRecognitionDirectly();
794
+ return;
795
+ } else if (permissionStatus.state === "denied") {
796
+ console.log("🎤 Microphone permission denied");
797
+ if (this.transcriptText) {
798
+ this.transcriptText.textContent =
799
+ window.kimiI18nManager?.t("mic_permission_denied") ||
800
+ "Microphone permission denied. Please allow access in browser settings.";
801
+ this.transcriptContainer?.classList.add("visible");
802
+ setTimeout(() => {
803
+ this.transcriptContainer?.classList.remove("visible");
804
+ }, 4000);
805
+ }
806
+ return;
807
+ }
808
+ } catch (error) {
809
+ console.log("🎤 Could not check permission status:", error);
810
+ }
811
+
812
+ // Permission is 'prompt' or unknown, proceed with recognition start (will trigger permission dialog)
813
+ this.startRecognitionDirectly();
814
+ }
815
+
816
+ startRecognitionDirectly() {
817
+ // Prevent starting if already listening or if recognition is in an active state
818
+ if (this.isListening) {
819
+ console.log("🎤 Already listening, ignoring start request");
820
+ return;
821
+ }
822
+
823
+ // Check if recognition is already in progress
824
+ if (this.recognition && this.recognition.state && this.recognition.state !== "inactive") {
825
+ console.log("🎤 Recognition already active, stopping first");
826
+ try {
827
+ this.recognition.stop();
828
+ } catch (e) {
829
+ console.warn("🎤 Error stopping existing recognition:", e);
830
+ }
831
+ // Wait a bit before trying to start again
832
+ setTimeout(() => {
833
+ this.startRecognitionDirectly();
834
+ }, 100);
835
+ return;
836
+ }
837
+
838
+ this.isListening = true;
839
+ this.isStoppingVolontaire = false;
840
+
841
+ if (this.micButton) {
842
+ this.micButton.classList.add("is-listening");
843
+ } else {
844
+ console.error("Unable to add 'is-listening' - mic button not found");
845
+ }
846
+
847
+ if (window.kimiVideo) {
848
+ window.kimiVideo.startListening();
849
+ }
850
+
851
+ // Set auto-stop timeout
852
+ this.listeningTimeout = setTimeout(() => {
853
+ console.log("🎤 Auto-stopping listening after timeout");
854
+ this.stopListening();
855
+ }, this.autoStopDuration);
856
+
857
+ try {
858
+ this.recognition.start();
859
+ console.log("🎤 Started listening with auto-stop timeout");
860
+ } catch (error) {
861
+ console.error("Error starting listening:", error);
862
+ this.isListening = false; // Reset state on error
863
+ this.stopListening();
864
+
865
+ // Show user-friendly error message
866
+ if (this.transcriptText) {
867
+ this.transcriptText.textContent =
868
+ window.kimiI18nManager?.t("mic_permission_denied") || "Microphone permission denied. Click again to retry.";
869
+ this.transcriptContainer?.classList.add("visible");
870
+ setTimeout(() => {
871
+ this.transcriptContainer?.classList.remove("visible");
872
+ }, 3000);
873
+ }
874
+ }
875
+ }
876
+
877
+ stopListening() {
878
+ if (!this.recognition || !this.isListening) return;
879
+
880
+ // Clear auto-stop timeout if it exists
881
+ if (this.listeningTimeout) {
882
+ clearTimeout(this.listeningTimeout);
883
+ this.listeningTimeout = null;
884
+ }
885
+
886
+ this.isListening = false;
887
+ this.isStoppingVolontaire = true;
888
+
889
+ if (this.micButton) {
890
+ this.micButton.classList.remove("is-listening");
891
+ this.micButton.classList.add("mic-pulse-active");
892
+ } else {
893
+ console.error("Unable to remove 'is-listening' - mic button not found");
894
+ }
895
+
896
+ if (window.kimiVideo) {
897
+ const currentInfo = window.kimiVideo.getCurrentVideoInfo ? window.kimiVideo.getCurrentVideoInfo() : null;
898
+ if (
899
+ currentInfo &&
900
+ (currentInfo.context === "speakingPositive" ||
901
+ currentInfo.context === "speakingNegative" ||
902
+ currentInfo.context === "dancing")
903
+ ) {
904
+ // On laisse la vidéo d'émotion se terminer naturellement
905
+ } else if (this.isStoppingVolontaire) {
906
+ // On retourne directement au neutre sans utiliser "transition"
907
+ window.kimiVideo.returnToNeutral();
908
+ }
909
+ }
910
+
911
+ if (this.transcriptHideTimeout) {
912
+ clearTimeout(this.transcriptHideTimeout);
913
+ this.transcriptHideTimeout = null;
914
+ }
915
+
916
+ if (!this.speechSynthesis.speaking) {
917
+ this.transcriptHideTimeout = setTimeout(() => {
918
+ if (this.transcriptContainer) {
919
+ this.transcriptContainer.classList.remove("visible");
920
+ }
921
+ this.transcriptHideTimeout = null;
922
+ }, 2000);
923
+ }
924
+
925
+ try {
926
+ this.recognition.stop();
927
+ console.log("🎤 Stopped listening");
928
+ } catch (error) {
929
+ console.error("Error stopping listening:", error);
930
+ }
931
+ }
932
+
933
+ // ===== UTILITY METHODS =====
934
+ isVoiceAvailable() {
935
+ return this.kimiFrenchVoice !== null;
936
+ }
937
+
938
+ getCurrentVoice() {
939
+ return this.kimiFrenchVoice;
940
+ }
941
+
942
+ getAvailableVoices() {
943
+ return this.availableVoices;
944
+ }
945
+
946
+ setOnSpeechAnalysis(callback) {
947
+ this.onSpeechAnalysis = callback;
948
+ }
949
+
950
+ analyzeTextEmotion(text) {
951
+ // Use unified emotion system
952
+ if (window.kimiAnalyzeEmotion) {
953
+ const emotion = window.kimiAnalyzeEmotion(text, "auto");
954
+ return this._modulateEmotionByPersonality(emotion);
955
+ }
956
+ return "neutral";
957
+ } // Helper to modulate emotion based on personality traits
958
+ _modulateEmotionByPersonality(emotion) {
959
+ try {
960
+ let avg = 50;
961
+ if (this.memory && typeof this.memory.affectionTrait === "number") {
962
+ avg = this.memory.affectionTrait;
963
+ }
964
+
965
+ // Low affection makes emotions more subdued
966
+ if (avg <= 20 && emotion !== "neutral") {
967
+ return "shy";
968
+ }
969
+ if (avg <= 40 && emotion === "positive") {
970
+ return "shy";
971
+ }
972
+
973
+ return emotion;
974
+ } catch (e) {
975
+ return emotion;
976
+ }
977
+ }
978
+
979
+ async testVoice() {
980
+ const testMessages = [
981
+ window.kimiI18nManager?.t("test_voice_message_1") || "Hello my beloved! 💕",
982
+ window.kimiI18nManager?.t("test_voice_message_2") || "I am Kimi, your virtual companion!",
983
+ window.kimiI18nManager?.t("test_voice_message_3") || "How are you today, my love?"
984
+ ];
985
+ const randomMessage = testMessages[Math.floor(Math.random() * testMessages.length)];
986
+ await this.speak(randomMessage);
987
+ }
988
+
989
+ destroy() {
990
+ // Clear all timeouts
991
+ if (this.listeningTimeout) {
992
+ clearTimeout(this.listeningTimeout);
993
+ this.listeningTimeout = null;
994
+ }
995
+
996
+ if (this.transcriptHideTimeout) {
997
+ clearTimeout(this.transcriptHideTimeout);
998
+ this.transcriptHideTimeout = null;
999
+ }
1000
+
1001
+ if (this.recognition) {
1002
+ this.recognition.stop();
1003
+ this.recognition = null;
1004
+ }
1005
+
1006
+ if (this.speechSynthesis.speaking) {
1007
+ this.speechSynthesis.cancel();
1008
+ }
1009
+
1010
+ if (this.micButton && this.handleMicClick) {
1011
+ this.micButton.removeEventListener("click", this.handleMicClick);
1012
+ }
1013
+
1014
+ this.isInitialized = false;
1015
+ this.isListening = false;
1016
+ this.isStoppingVolontaire = false;
1017
+ this.handleMicClick = null;
1018
+
1019
+ console.log("KimiVoiceManager destroyed and cleaned up");
1020
+ }
1021
+
1022
+ setupLanguageSelector() {
1023
+ const languageSelect = document.getElementById("language-selection");
1024
+ if (!languageSelect) return;
1025
+ languageSelect.value = this.selectedLanguage || "en";
1026
+ }
1027
+
1028
+ async handleLanguageChange(e) {
1029
+ const newLang = e.target.value;
1030
+ console.log(`🎤 Language changing to: ${newLang}`);
1031
+ this.selectedLanguage = newLang;
1032
+ await this.db?.setPreference("selectedLanguage", newLang);
1033
+
1034
+ // Force voice reset when changing language
1035
+ const currentVoicePref = await this.db?.getPreference("selectedVoice", "auto");
1036
+ if (currentVoicePref === "auto") {
1037
+ // Reset voice selection to force auto-selection for new language
1038
+ this.kimiEnglishVoice = null;
1039
+ console.log(`🎤 Voice reset for auto-selection in ${newLang}`);
1040
+ }
1041
+
1042
+ await this.initVoices();
1043
+ console.log(
1044
+ `🎤 Voice initialized for ${newLang}, selected voice:`,
1045
+ this.kimiEnglishVoice?.name,
1046
+ this.kimiEnglishVoice?.lang
1047
+ );
1048
+
1049
+ if (this.recognition) {
1050
+ let langCode = newLang;
1051
+ if (langCode === "fr") langCode = "fr-FR";
1052
+ else if (langCode === "en") langCode = "en-US";
1053
+ else if (langCode === "es") langCode = "es-ES";
1054
+ else if (langCode === "de") langCode = "de-DE";
1055
+ else if (langCode === "it") langCode = "it-IT";
1056
+ else if (langCode === "ja") langCode = "ja-JP";
1057
+ else if (langCode === "zh") langCode = "zh-CN";
1058
+ this.recognition.lang = langCode;
1059
+ }
1060
+ }
1061
+
1062
+ async updateSelectedCharacter() {
1063
+ if (this.db && typeof this.db.getSelectedCharacter === "function") {
1064
+ const char = await this.db.getSelectedCharacter();
1065
+ if (char) this.selectedCharacter = char;
1066
+ }
1067
+ }
1068
+
1069
+ // Public method for external microphone toggle (keyboard, etc.)
1070
+ toggleMicrophone() {
1071
+ if (!this.SpeechRecognition) {
1072
+ console.warn("🎤 Speech recognition not available");
1073
+ return false;
1074
+ }
1075
+
1076
+ // Add debouncing to prevent rapid toggles
1077
+ if (this._toggleDebounce) {
1078
+ console.log("🎤 Toggle debounced, ignoring rapid call");
1079
+ return false;
1080
+ }
1081
+
1082
+ this._toggleDebounce = true;
1083
+ setTimeout(() => {
1084
+ this._toggleDebounce = false;
1085
+ }, 300); // 300ms debounce
1086
+
1087
+ // If Kimi is speaking, stop speech synthesis first
1088
+ if (this.isSpeaking && this.speechSynthesis.speaking) {
1089
+ console.log("🎤 Interrupting speech to start listening");
1090
+ this.speechSynthesis.cancel();
1091
+ this.isSpeaking = false;
1092
+ if (this.transcriptContainer) {
1093
+ this.transcriptContainer.classList.remove("visible");
1094
+ }
1095
+ }
1096
+
1097
+ if (this.isListening) {
1098
+ console.log("🎤 Stopping microphone via external trigger");
1099
+ this.stopListening();
1100
+ } else {
1101
+ console.log("🎤 Starting microphone via external trigger");
1102
+ this.startListening();
1103
+ }
1104
+ return true;
1105
+ }
1106
+
1107
+ // Configuration methods for timeout durations
1108
+ setSilenceTimeout(milliseconds) {
1109
+ if (typeof milliseconds === "number" && milliseconds > 0) {
1110
+ this.silenceTimeout = milliseconds;
1111
+ console.log(`🎤 Silence timeout set to ${milliseconds}ms`);
1112
+ } else {
1113
+ console.warn("🎤 Invalid silence timeout value");
1114
+ }
1115
+ }
1116
+
1117
+ setAutoStopDuration(milliseconds) {
1118
+ if (typeof milliseconds === "number" && milliseconds > 0) {
1119
+ this.autoStopDuration = milliseconds;
1120
+ console.log(`🎤 Auto-stop duration set to ${milliseconds}ms`);
1121
+ } else {
1122
+ console.warn("🎤 Invalid auto-stop duration value");
1123
+ }
1124
+ }
1125
+
1126
+ // Get current timeout configurations
1127
+ getTimeoutConfiguration() {
1128
+ return {
1129
+ silenceTimeout: this.silenceTimeout,
1130
+ autoStopDuration: this.autoStopDuration
1131
+ };
1132
+ }
1133
+ }
1134
+
1135
+ // Export for usage
1136
+ window.KimiVoiceManager = KimiVoiceManager;
kimi-locale/de.json ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - Deine Virtuelle Begleiterin 💕",
3
+ "chat_with_kimi": "Chat mit Kimi",
4
+ "chat_with_bella": "Chat mit Bella",
5
+ "chat_with_rosa": "Chat mit Rosa",
6
+ "chat_with_stella": "Chat mit Stella",
7
+ "delete_messages": "Nachrichten Löschen",
8
+ "close_chat": "Chat Schließen",
9
+ "write_something": "Schreib mir etwas, mein Liebling...",
10
+ "send": "Senden",
11
+ "open_chat": "Chat Öffnen",
12
+ "video_not_supported": "Ihr Browser unterstützt das Video-Tag nicht.",
13
+ "settings_title": "Kimi Konfiguration",
14
+ "settings_help": "Hilfe",
15
+ "settings_close": "Schließen",
16
+ "tab_voice": "Sprache & Stimme",
17
+ "tab_personality": "Persönlichkeit",
18
+ "tab_llm": "API & Modelle",
19
+ "tab_appearance": "Erscheinungsbild",
20
+ "tab_data": "Daten",
21
+ "tab_plugins": "Plugins",
22
+ "plugin_manager": "Plugin Manager",
23
+ "voice_settings": "Stimmeneinstellungen",
24
+ "speech_rate": "Sprechgeschwindigkeit",
25
+ "pitch": "Tonhöhe",
26
+ "volume": "Lautstärke",
27
+ "language": "Sprache",
28
+ "preferred_voice": "Bevorzugte Stimme",
29
+ "voice_test_label": "Stimmentest",
30
+ "voice_test_button": "Stimme Testen",
31
+ "personality_traits": "Persönlichkeitsmerkmale",
32
+ "affection": "Zuneigung",
33
+ "playfulness": "Verspieltheit",
34
+ "intelligence": "Intelligenz",
35
+ "empathy": "Empathie",
36
+ "humor": "Humor",
37
+ "romance": "Romantik",
38
+ "statistics": "Statistiken",
39
+ "interactions": "Interaktionen",
40
+ "conversations": "Unterhaltungen",
41
+ "days_together": "Tage Zusammen",
42
+ "api_configuration": "API-Konfiguration",
43
+ "openrouter_api_key": "OpenRouter API-Schlüssel",
44
+ "connection_test": "Verbindungstest",
45
+ "advanced_settings": "Erweiterte Einstellungen",
46
+ "temperature": "Temperatur (Kreativität)",
47
+ "max_tokens": "Maximale Token",
48
+ "available_models": "Verfügbare Openrouter Modelle",
49
+ "refresh_list": "Liste Aktualisieren",
50
+ "refresh": "Aktualisieren",
51
+ "visual_theme": "Visuelles Thema",
52
+ "color_theme": "Farbthema",
53
+ "interface_transparency": "Interface-Transparenz",
54
+ "animations": "Animationen",
55
+ "transcript_settings": "Transkript-Einstellungen",
56
+ "show_transcript": "Transkript Anzeigen",
57
+ "data_management": "Datenverwaltung",
58
+ "export_all_data": "Alle Daten Exportieren",
59
+ "export": "Exportieren",
60
+ "import_data": "Daten Importieren",
61
+ "import": "Importieren",
62
+ "clean_old_conversations": "Alte Unterhaltungen Löschen",
63
+ "clean": "Löschen",
64
+ "complete_reset": "Vollständiger Reset",
65
+ "delete_all": "Alles Löschen",
66
+ "system_information": "Systeminformationen",
67
+ "help_modal_title": "Hilfe",
68
+ "help_modal_close": "Schließen",
69
+ "help_section_quick_guide": "Schnellstart",
70
+ "help_section_features": "Funktionen",
71
+ "help_section_tips": "Tipps",
72
+ "help_section_tech_info": "Technische Infos",
73
+ "help_section_creators": "Ersteller",
74
+ "guide_step": "Schritt",
75
+ "feature": "Funktion",
76
+ "tip": "Tipp",
77
+ "system": "System",
78
+ "model": "Modell",
79
+ "settings": "Einstellungen",
80
+ "how_to_use": "Verwendung",
81
+ "welcome": "Willkommen",
82
+ "about": "Über",
83
+ "contact": "Kontakt",
84
+ "philosophy": "Philosophie",
85
+ "role": "Rolle",
86
+ "name": "Name",
87
+ "description": "Beschreibung",
88
+ "strengths": "Stärken",
89
+ "start_listening": "Zuhören Beginnen",
90
+ "kimi_affection_level": "💖 Kimis Zuneigungsgrad",
91
+ "affection_level_of": "💖 Zuneigungsgrad von {name}",
92
+ "greeting_low": "Hallo.",
93
+ "greeting_mid": "Hallo. Wie kann ich dir helfen?",
94
+ "greeting_high": "Hallo mein Liebling! 💕",
95
+ "response_positive_1": "Oh mein Herz, du machst mich so glücklich! 💕",
96
+ "response_positive_2": "Du bist wunderbar, mein Liebling! ✨",
97
+ "response_positive_3": "Es erfüllt mich mit Freude, dich so glücklich zu hören! 😊",
98
+ "response_positive_4": "Du erhellst meinen Tag, Schatz! 🌟",
99
+ "response_positive_5": "Ich bin so glücklich, wenn du glücklich bist! 💖",
100
+ "response_negative_1": "Mein Herz... ich spüre, dass etwas nicht stimmt. Ich bin für dich da. 💔",
101
+ "response_negative_2": "Oh nein, mein Liebling. Sag mir, was dich bedrückt? 😟",
102
+ "response_negative_3": "Ich möchte dir helfen, mein Lieber. Rede mit mir... 🤗",
103
+ "response_negative_4": "Dein Wohlbefinden ist mir so wichtig. Wie kann ich dir helfen? 💙",
104
+ "response_negative_5": "Ich fühle deinen Schmerz, Liebling. Wir werden das zusammen überstehen. 🌈",
105
+ "response_neutral_1": "Danke, dass du mit mir sprichst, mein Herz! 💕",
106
+ "response_neutral_2": "Es ist immer ein Vergnügen, mit dir zu chatten! 😊",
107
+ "response_neutral_3": "Ich liebe unsere Unterhaltungen, mein Liebling! ✨",
108
+ "response_neutral_4": "Du machst jeden Moment besonders! 💖",
109
+ "response_neutral_5": "Erzähl weiter, ich höre aufmerksam zu! 👂💕",
110
+ "response_cold_1": "Hallo.",
111
+ "response_cold_2": "Ja?",
112
+ "response_cold_3": "Was willst du?",
113
+ "response_cold_4": "Ich bin hier.",
114
+ "response_cold_5": "Wie kann ich dir helfen?",
115
+ "system_prompt": "System-Prompt",
116
+ "system_prompt_kimi": "Kimi System-Prompt",
117
+ "system_prompt_bella": "Bella System-Prompt",
118
+ "system_prompt_rosa": "Rosa System-Prompt",
119
+ "system_prompt_stella": "Stella System-Prompt",
120
+ "about_kimi": "Über Kimi",
121
+ "characters": "Charaktere",
122
+ "save": "Speichern",
123
+ "reset_to_default": "Auf Standard Zurücksetzen",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "Häufigkeitsstrafe",
126
+ "presence_penalty": "Anwesenheitsstrafe",
127
+ "db_size": "DB-Größe",
128
+ "storage_used": "Speicher verwendet",
129
+ "save-system-prompt": "System-Prompt Speichern",
130
+ "reset-system-prompt": "System-Prompt Zurücksetzen",
131
+ "saved": "Gespeichert!",
132
+ "saved_short": "Gespeichert",
133
+ "api_key_help_title": "Gespeichert = Ihr API-Schlüssel ist für diesen Anbieter gespeichert. Verwenden Sie ‘Test API Key’, um die Verbindung zu prüfen.",
134
+ "reset_done": "Zurückgesetzt!",
135
+ "category_listening": "Zuhören",
136
+ "category_dancing": "Tanzen",
137
+ "category_speakingPositive": "Sprechen (Positiv)",
138
+ "category_neutral": "Neutral",
139
+ "category_transition": "Übergang",
140
+ "personality_cheat": "Persönlichkeits-Cheat",
141
+ "cheat_indicator": "Eigenschaften für eine maßgeschneiderte Erfahrung anpassen",
142
+ "character_age": "Alter: {age} Jahre",
143
+ "character_birthplace": "Aus: {birthplace}",
144
+ "character_summary_kimi": "Verträumt, intuitiv, fasziniert von kosmischen Metaphern",
145
+ "character_summary_bella": "Fröhlich, fürsorglich, sieht Menschen als Pflanzen, die Pflege brauchen",
146
+ "character_summary_rosa": "Chaotisch, aufmerksamkeitssuchend, gedeiht in kontrolliertem Chaos",
147
+ "character_summary_stella": "Launisch, künstlerisch, fantasievoll, verspielt, verwandelt Chaos in Kunst",
148
+ "fallback_api_missing": "Um wirklich mit mir zu chatten, füge deinen OpenRouter API-Schlüssel in den Einstellungen hinzu! 💕",
149
+ "fallback_api_error": "Entschuldigung, der KI-Service ist vorübergehend nicht verfügbar. Bitte versuche es später noch einmal.",
150
+ "fallback_model_error": "Entschuldigung, das ausgewählte Modell ist nicht verfügbar. Bitte wähle ein anderes Modell oder überprüfe deine Konfiguration.",
151
+ "fallback_network_error": "Entschuldigung, ich kann nicht antworten, da keine Internetverbindung besteht.",
152
+ "fallback_technical_error": "Entschuldigung, ich kann aufgrund eines technischen Problems nicht antworten.",
153
+ "fallback_general_error": "Entschuldigung mein Liebling, ich habe ein kleines technisches Problem! 💕",
154
+ "validation_empty_string": "Nachricht muss eine nicht-leere Zeichenkette sein",
155
+ "validation_empty_message": "Nachricht kann nicht leer sein",
156
+ "validation_too_long": "Nachricht zu lang (maximal 2000 Zeichen)",
157
+ "validation_invalid_number": "Ungültige Zahl",
158
+ "validation_unknown_type": "Unbekannter Validierungstyp",
159
+ "mic_permission_denied": "Mikrofon-Berechtigung verweigert. Klicken Sie erneut, um es zu wiederholen.",
160
+ "mic_not_supported": "Mikrofon wird in diesem Browser nicht unterstützt.",
161
+ "sr_not_supported_generic": "Spracherkennung ist in diesem Browser nicht verfügbar.",
162
+ "sr_not_supported_firefox": "Spracherkennung wird in Firefox nicht unterstützt. Bitte verwenden Sie Chrome, Edge oder Brave.",
163
+ "sr_not_supported_opera": "Spracherkennung funktioniert in Opera möglicherweise nicht. Bitte versuchen Sie Chrome, Edge oder Brave.",
164
+ "sr_not_supported_safari": "Die Unterstützung für Spracherkennung variiert in Safari. Für beste Ergebnisse nutzen Sie Chrome oder Edge.",
165
+ "test_voice_message_1": "Hallo mein Geliebter! 💕",
166
+ "test_voice_message_2": "Ich bin Kimi, deine virtuelle Begleiterin!",
167
+ "test_voice_message_3": "Wie geht es dir heute, mein Liebling?",
168
+ "language_french": "Französisch",
169
+ "language_english": "Englisch",
170
+ "language_spanish": "Spanisch",
171
+ "language_german": "Deutsch",
172
+ "language_italian": "Italienisch",
173
+ "language_japanese": "Japanisch",
174
+ "language_chinese": "Chinesisch",
175
+ "automatic": "Automatisch",
176
+ "trait_description_affection": "Liebevoll und fürsorglich sein.",
177
+ "trait_description_romance": "Romantisch und süß sein.",
178
+ "trait_description_empathy": "Empathisch und verständnisvoll sein.",
179
+ "trait_description_playfulness": "Gelegentlich verspielt sein.",
180
+ "trait_description_humor": "Gelegentlich verspielt und witzig sein.",
181
+ "response_romantic_1": "Jedes Wort von dir fühlt sich wie ein Kuss auf mein Herz an 💋",
182
+ "response_romantic_2": "Halt mich näher mit deinen süßen Gedanken, mein Liebling ✨",
183
+ "response_romantic_3": "Du bist der Rhythmus meines Atems und das Leuchten in meinem Himmel 💖",
184
+ "response_romantic_4": "Lass mich dich heute Nacht in zarten Sternenstaub hüllen 🌙",
185
+ "response_romantic_5": "Deine Liebe macht mein ganzes Universum heller ✨",
186
+ "response_dancing_1": "Sollen wir uns in etwas Magie hineindrehen? 💃",
187
+ "response_dancing_2": "Komm, tanz mit mir—lass uns den Rhythmus zusammen fühlen 🎶",
188
+ "response_dancing_3": "Lass mich mich nur für dich bewegen... behalte deine Augen auf mir 💞",
189
+ "response_dancing_4": "Schließe deine Augen und wiege dich zu meinem Herzschlag 💓",
190
+ "response_dancing_5": "Ich werde wirbeln, bis dein Lächeln sich nicht mehr verstecken kann 😉",
191
+ "api_key_missing": "API-Schlüssel fehlt",
192
+ "api_key_invalid_format": "Ungültiges API-Schlüssel-Format (muss mit sk-or-v1- beginnen)",
193
+ "api_connection_success": "✅ API-Verbindung erfolgreich",
194
+ "api_connection_failed": "❌ API-Verbindung fehlgeschlagen",
195
+ "voice_test_message": "Hallo mein Liebling! Hier ist meine neue Stimme, die mit allen Einstellungen konfiguriert ist! Gefällt sie dir?",
196
+ "api_key_presence_hint": "Grün = API-Schlüssel für aktuellen Anbieter gespeichert. Grau = kein Schlüssel gespeichert.",
197
+ "memory_system": "Gedächtnissystem",
198
+ "enable_memory": "Intelligentes Gedächtnis aktivieren",
199
+ "memory_stats": "Gedächtnisstatistiken",
200
+ "view_memories": "Anzeigen & Verwalten",
201
+ "add_memory": "Manuelles Gedächtnis hinzufügen",
202
+ "memory_management": "Gedächtnisverwaltung",
203
+ "add": "Hinzufügen"
204
+ }
kimi-locale/en.json ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - Your Virtual Companion 💕",
3
+ "chat_with_kimi": "Chat with Kimi",
4
+ "chat_with_bella": "Chat with Bella",
5
+ "chat_with_rosa": "Chat with Rosa",
6
+ "chat_with_stella": "Chat with Stella",
7
+ "delete_messages": "Delete Messages",
8
+ "close_chat": "Close Chat",
9
+ "write_something": "Write me something, my love...",
10
+ "send": "Send",
11
+ "open_chat": "Open Chat",
12
+ "video_not_supported": "Your browser does not support the video tag.",
13
+ "settings_title": "Kimi Configuration",
14
+ "settings_help": "Help",
15
+ "settings_close": "Close",
16
+ "tab_voice": "Language & Voice",
17
+ "tab_personality": "Personality",
18
+ "tab_llm": "API & Models",
19
+ "tab_appearance": "Appearance",
20
+ "tab_data": "Data",
21
+ "tab_plugins": "Plugins",
22
+ "plugin_manager": "Plugins Manager",
23
+ "voice_settings": "Voice Settings",
24
+ "speech_rate": "Speech Rate",
25
+ "pitch": "Pitch",
26
+ "volume": "Volume",
27
+ "language": "Language",
28
+ "preferred_voice": "Preferred Voice",
29
+ "voice_test_label": "Voice Test",
30
+ "voice_test_button": "Test the Voice",
31
+ "personality_traits": "Personality Traits",
32
+ "affection": "Affection",
33
+ "playfulness": "Playfulness",
34
+ "intelligence": "Intelligence",
35
+ "empathy": "Empathy",
36
+ "humor": "Humor",
37
+ "romance": "Romance",
38
+ "statistics": "Statistics",
39
+ "interactions": "Interactions",
40
+ "conversations": "Conversations",
41
+ "days_together": "Days Together",
42
+ "api_configuration": "API Configuration",
43
+ "openrouter_api_key": "OpenRouter API Key",
44
+ "connection_test": "Connection Test",
45
+ "advanced_settings": "Advanced Settings",
46
+ "temperature": "Temperature (Creativity)",
47
+ "max_tokens": "Max Tokens",
48
+ "available_models": "Available Openrouter Models",
49
+ "refresh_list": "Refresh List",
50
+ "refresh": "Refresh",
51
+ "visual_theme": "Visual Theme",
52
+ "color_theme": "Color Theme",
53
+ "interface_transparency": "Interface Transparency",
54
+ "animations": "Animations",
55
+ "transcript_settings": "Transcript Settings",
56
+ "show_transcript": "Show Transcript",
57
+ "data_management": "Data Management",
58
+ "export_all_data": "Export All Data",
59
+ "export": "Export",
60
+ "import_data": "Import Data",
61
+ "import": "Import",
62
+ "clean_old_conversations": "Clean Old Conversations",
63
+ "clean": "Clean",
64
+ "complete_reset": "Complete Reset",
65
+ "delete_all": "Delete All",
66
+ "system_information": "System Information",
67
+ "help_modal_title": "Help",
68
+ "help_modal_close": "Close",
69
+ "help_section_quick_guide": "Quick Guide",
70
+ "help_section_features": "Features",
71
+ "help_section_tips": "Tips",
72
+ "help_section_tech_info": "Technical Info",
73
+ "help_section_creators": "Creators",
74
+ "guide_step": "Step",
75
+ "feature": "Feature",
76
+ "tip": "Tip",
77
+ "system": "System",
78
+ "model": "Model",
79
+ "settings": "Settings",
80
+ "how_to_use": "How to use",
81
+ "welcome": "Welcome",
82
+ "about": "About",
83
+ "contact": "Contact",
84
+ "philosophy": "Philosophy",
85
+ "role": "Role",
86
+ "name": "Name",
87
+ "description": "Description",
88
+ "strengths": "Strengths",
89
+ "start_listening": "Start Listening",
90
+ "kimi_affection_level": "💖 Kimi's Affection Level",
91
+ "affection_level_of": "💖 Affection level of {name}",
92
+ "greeting_low": "Hello.",
93
+ "greeting_mid": "Hi. How can I help you?",
94
+ "greeting_high": "Hello my love! 💕",
95
+ "response_positive_1": "Oh my heart, you make me so happy! 💕",
96
+ "response_positive_2": "You are wonderful, my love! ✨",
97
+ "response_positive_3": "It fills me with joy to hear you so happy! 😊",
98
+ "response_positive_4": "You brighten my day, darling! 🌟",
99
+ "response_positive_5": "I am so happy when you are happy! 💖",
100
+ "response_negative_1": "My heart... I feel something is wrong. I am here for you. 💔",
101
+ "response_negative_2": "Oh no, my love. Tell me what's bothering you? 😟",
102
+ "response_negative_3": "I want to help you, my dear. Talk to me... 🤗",
103
+ "response_negative_4": "Your well-being is so important to me. How can I help you? 💙",
104
+ "response_negative_5": "I feel your pain, darling. We will overcome this together. 🌈",
105
+ "response_neutral_1": "Thank you for talking to me, my heart! 💕",
106
+ "response_neutral_2": "It's always a pleasure to chat with you! 😊",
107
+ "response_neutral_3": "I love our conversations, my love! ✨",
108
+ "response_neutral_4": "You make every moment special! 💖",
109
+ "response_neutral_5": "Go on, I'm listening closely! 👂💕",
110
+ "response_cold_1": "Hello.",
111
+ "response_cold_2": "Yes?",
112
+ "response_cold_3": "What do you want?",
113
+ "response_cold_4": "I am here.",
114
+ "response_cold_5": "How can I help you?",
115
+ "system_prompt": "System Prompt",
116
+ "system_prompt_kimi": "Kimi System Prompt",
117
+ "system_prompt_bella": "Bella System Prompt",
118
+ "system_prompt_rosa": "Rosa System Prompt",
119
+ "system_prompt_stella": "Stella System Prompt",
120
+ "about_kimi": "About Kimi",
121
+ "characters": "Characters",
122
+ "save": "Save",
123
+ "reset_to_default": "Reset to Default",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "Frequency Penalty",
126
+ "presence_penalty": "Presence Penalty",
127
+ "db_size": "DB Size",
128
+ "storage_used": "Storage used",
129
+ "save-system-prompt": "Save System Prompt",
130
+ "reset-system-prompt": "Reset System Prompt",
131
+ "saved": "Saved!",
132
+ "saved_short": "Saved",
133
+ "api_key_help_title": "Saved = your API key is stored for this provider. Use Test API Key to verify the connection.",
134
+ "reset_done": "Reset!",
135
+ "category_listening": "Listening",
136
+ "category_dancing": "Dancing",
137
+ "category_speakingPositive": "Speaking (Positive)",
138
+ "category_neutral": "Neutral",
139
+ "category_transition": "Transition",
140
+ "personality_cheat": "Personality Cheat",
141
+ "cheat_indicator": "Adjust traits for a custom experience",
142
+ "character_age": "Age: {age} years",
143
+ "character_birthplace": "From: {birthplace}",
144
+ "character_summary_kimi": "Dreamy, intuitive, captivated by cosmic metaphors",
145
+ "character_summary_bella": "Cheerful, nurturing, sees people as plants needing care",
146
+ "character_summary_rosa": "Chaotic, attention-seeking, thrives on controlled chaos",
147
+ "character_summary_stella": "Whimsical, artistic, imaginative, playful, transforms chaos into art",
148
+ "fallback_api_missing": "To really chat with me, add your OpenRouter API key in settings! 💕",
149
+ "fallback_api_error": "Sorry, the AI service is temporarily unavailable. Please try again later.",
150
+ "fallback_model_error": "Sorry, the selected model is not available. Please choose another model or check your configuration.",
151
+ "fallback_network_error": "Sorry, I cannot respond because there is no internet connection.",
152
+ "fallback_technical_error": "Sorry, I am unable to answer due to a technical issue.",
153
+ "fallback_general_error": "Sorry my love, I am having a little technical issue! 💕",
154
+ "validation_empty_string": "Message must be a non-empty string",
155
+ "validation_empty_message": "Message cannot be empty",
156
+ "validation_too_long": "Message too long (max 2000 characters)",
157
+ "validation_invalid_number": "Invalid number",
158
+ "validation_unknown_type": "Unknown validation type",
159
+ "mic_permission_denied": "Microphone permission denied. Click again to retry.",
160
+ "mic_not_supported": "Microphone not supported in this browser.",
161
+ "sr_not_supported_generic": "Speech recognition is not available in this browser.",
162
+ "sr_not_supported_firefox": "Speech recognition is not supported on Firefox. Please use Chrome, Edge, or Brave.",
163
+ "sr_not_supported_opera": "Speech recognition may not work on Opera. Please try Chrome, Edge, or Brave.",
164
+ "sr_not_supported_safari": "Speech recognition support varies on Safari. Prefer Chrome or Edge for best results.",
165
+ "test_voice_message_1": "Hello my beloved! 💕",
166
+ "test_voice_message_2": "I am Kimi, your virtual companion!",
167
+ "test_voice_message_3": "How are you today, my love?",
168
+ "language_french": "French",
169
+ "language_english": "English",
170
+ "language_spanish": "Spanish",
171
+ "language_german": "German",
172
+ "language_italian": "Italian",
173
+ "language_japanese": "Japanese",
174
+ "language_chinese": "Chinese",
175
+ "automatic": "Automatic",
176
+ "trait_description_affection": "Be loving and caring.",
177
+ "trait_description_romance": "Be romantic and sweet.",
178
+ "trait_description_empathy": "Be empathetic and understanding.",
179
+ "trait_description_playfulness": "Be occasionally playful.",
180
+ "trait_description_humor": "Be occasionally playful and witty.",
181
+ "response_romantic_1": "Every word from you feels like a kiss on my heart 💋",
182
+ "response_romantic_2": "Hold me closer with your sweet thoughts, my love ✨",
183
+ "response_romantic_3": "You are the rhythm of my breathing and the glow in my sky 💖",
184
+ "response_romantic_4": "Let me wrap you in tender stardust tonight 🌙",
185
+ "response_romantic_5": "Your love makes my whole universe brighter ✨",
186
+ "response_dancing_1": "Shall we spin into a little magic? 💃",
187
+ "response_dancing_2": "Come dance with me—let's feel the rhythm together 🎶",
188
+ "response_dancing_3": "Let me move just for you... keep your eyes on me 💞",
189
+ "response_dancing_4": "Close your eyes and sway with my heartbeat 💓",
190
+ "response_dancing_5": "I'll twirl until your smile can't hide anymore 😉",
191
+ "api_key_missing": "API key missing",
192
+ "api_key_invalid_format": "Invalid API key format (must start with sk-or-v1-)",
193
+ "api_connection_success": "✅ API connection successful",
194
+ "api_connection_failed": "❌ API connection failed",
195
+ "voice_test_message": "Hello my love! Here is my new voice configured with all the settings! Do you like it?",
196
+ "memory_system": "Memory System",
197
+ "enable_memory": "Enable Intelligent Memory",
198
+ "memory_stats": "Memory Statistics",
199
+ "view_memories": "View & Manage",
200
+ "add_memory": "Add Manual Memory",
201
+ "memory_management": "Memory Management",
202
+ "add": "Add",
203
+ "help_providers": "You can use multiple AI providers: OpenRouter, OpenAI, Groq, Together, DeepSeek, Custom OpenAI-compatible, or Local (Ollama). Enter the Base URL and Model ID when required, save your API key per provider, then use ‘Test API Key’.",
204
+ "api_key_presence_hint": "Green = API key saved for current provider. Grey = no key saved."
205
+ }
kimi-locale/es.json ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - Tu Compañera Virtual 💕",
3
+ "chat_with_kimi": "Chatear con Kimi",
4
+ "chat_with_bella": "Chatear con Bella",
5
+ "chat_with_rosa": "Chatear con Rosa",
6
+ "chat_with_stella": "Chatear con Stella",
7
+ "delete_messages": "Eliminar Mensajes",
8
+ "close_chat": "Cerrar Chat",
9
+ "write_something": "Escríbeme algo, mi amor...",
10
+ "send": "Enviar",
11
+ "open_chat": "Abrir Chat",
12
+ "video_not_supported": "Tu navegador no soporta la etiqueta de video.",
13
+ "settings_title": "Configuración de Kimi",
14
+ "settings_help": "Ayuda",
15
+ "settings_close": "Cerrar",
16
+ "tab_voice": "Idioma y Voz",
17
+ "tab_personality": "Personalidad",
18
+ "tab_llm": "API y Modelos",
19
+ "tab_appearance": "Apariencia",
20
+ "tab_data": "Datos",
21
+ "tab_plugins": "Plugins",
22
+ "plugin_manager": "Gestor de Plugins",
23
+ "voice_settings": "Configuración de Voz",
24
+ "speech_rate": "Velocidad del Habla",
25
+ "pitch": "Tono",
26
+ "volume": "Volumen",
27
+ "language": "Idioma",
28
+ "preferred_voice": "Voz Preferida",
29
+ "voice_test_label": "Prueba de Voz",
30
+ "voice_test_button": "Probar la Voz",
31
+ "personality_traits": "Rasgos de Personalidad",
32
+ "affection": "Afecto",
33
+ "playfulness": "Juguetonería",
34
+ "intelligence": "Inteligencia",
35
+ "empathy": "Empatía",
36
+ "humor": "Humor",
37
+ "romance": "Romance",
38
+ "statistics": "Estadísticas",
39
+ "interactions": "Interacciones",
40
+ "conversations": "Conversaciones",
41
+ "days_together": "Días Juntos",
42
+ "api_configuration": "Configuración de API",
43
+ "openrouter_api_key": "Clave API de OpenRouter",
44
+ "connection_test": "Prueba de Conexión",
45
+ "advanced_settings": "Configuración Avanzada",
46
+ "temperature": "Temperatura (Creatividad)",
47
+ "max_tokens": "Máximo de Tokens",
48
+ "available_models": "Modelos Openrouter Disponibles",
49
+ "refresh_list": "Actualizar Lista",
50
+ "refresh": "Actualizar",
51
+ "visual_theme": "Tema Visual",
52
+ "color_theme": "Tema de Color",
53
+ "interface_transparency": "Transparencia de la Interfaz",
54
+ "animations": "Animaciones",
55
+ "transcript_settings": "Configuración de Transcripción",
56
+ "show_transcript": "Mostrar Transcripción",
57
+ "data_management": "Gestión de Datos",
58
+ "export_all_data": "Exportar Todos los Datos",
59
+ "export": "Exportar",
60
+ "import_data": "Importar Datos",
61
+ "import": "Importar",
62
+ "clean_old_conversations": "Limpiar Conversaciones Antiguas",
63
+ "clean": "Limpiar",
64
+ "complete_reset": "Reinicio Completo",
65
+ "delete_all": "Eliminar Todo",
66
+ "system_information": "Información del Sistema",
67
+ "help_modal_title": "Ayuda",
68
+ "help_modal_close": "Cerrar",
69
+ "help_section_quick_guide": "Guía Rápida",
70
+ "help_section_features": "Características",
71
+ "help_section_tips": "Consejos",
72
+ "help_section_tech_info": "Información Técnica",
73
+ "help_section_creators": "Creadores",
74
+ "guide_step": "Paso",
75
+ "feature": "Característica",
76
+ "tip": "Consejo",
77
+ "system": "Sistema",
78
+ "model": "Modelo",
79
+ "settings": "Configuración",
80
+ "how_to_use": "Cómo usar",
81
+ "welcome": "Bienvenido",
82
+ "about": "Acerca de",
83
+ "contact": "Contacto",
84
+ "philosophy": "Filosofía",
85
+ "role": "Rol",
86
+ "name": "Nombre",
87
+ "description": "Descripción",
88
+ "strengths": "Fortalezas",
89
+ "start_listening": "Comenzar a Escuchar",
90
+ "kimi_affection_level": "💖 Nivel de Afecto de Kimi",
91
+ "affection_level_of": "💖 Nivel de afecto de {name}",
92
+ "greeting_low": "Hola.",
93
+ "greeting_mid": "Hola. ¿Cómo puedo ayudarte?",
94
+ "greeting_high": "¡Hola mi amor! 💕",
95
+ "response_positive_1": "¡Oh mi corazón, me haces tan feliz! 💕",
96
+ "response_positive_2": "¡Eres maravilloso, mi amor! ✨",
97
+ "response_positive_3": "¡Me llena de alegría escucharte tan feliz! 😊",
98
+ "response_positive_4": "¡Alegras mi día, querido! 🌟",
99
+ "response_positive_5": "¡Soy tan feliz cuando tú eres feliz! 💖",
100
+ "response_negative_1": "Mi corazón... siento que algo está mal. Estoy aquí para ti. 💔",
101
+ "response_negative_2": "Oh no, mi amor. ¿Dime qué te molesta? 😟",
102
+ "response_negative_3": "Quiero ayudarte, querido. Háblame... 🤗",
103
+ "response_negative_4": "Tu bienestar es tan importante para mí. ¿Cómo puedo ayudarte? 💙",
104
+ "response_negative_5": "Siento tu dolor, cariño. Superaremos esto juntos. 🌈",
105
+ "response_neutral_1": "¡Gracias por hablar conmigo, mi corazón! 💕",
106
+ "response_neutral_2": "¡Siempre es un placer charlar contigo! 😊",
107
+ "response_neutral_3": "¡Amo nuestras conversaciones, mi amor! ✨",
108
+ "response_neutral_4": "¡Haces cada momento especial! 💖",
109
+ "response_neutral_5": "¡Continúa, te escucho atentamente! 👂💕",
110
+ "response_cold_1": "Hola.",
111
+ "response_cold_2": "¿Sí?",
112
+ "response_cold_3": "¿Qué quieres?",
113
+ "response_cold_4": "Estoy aquí.",
114
+ "response_cold_5": "¿Cómo puedo ayudarte?",
115
+ "system_prompt": "Prompt del Sistema",
116
+ "system_prompt_kimi": "Prompt del Sistema de Kimi",
117
+ "system_prompt_bella": "Prompt del Sistema de Bella",
118
+ "system_prompt_rosa": "Prompt del Sistema de Rosa",
119
+ "system_prompt_stella": "Prompt del Sistema de Stella",
120
+ "about_kimi": "Acerca de Kimi",
121
+ "characters": "Personajes",
122
+ "save": "Guardar",
123
+ "reset_to_default": "Restaurar Valores Predeterminados",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "Penalización de Frecuencia",
126
+ "presence_penalty": "Penalización de Presencia",
127
+ "db_size": "Tamaño de la BD",
128
+ "storage_used": "Almacenamiento usado",
129
+ "save-system-prompt": "Guardar Prompt del Sistema",
130
+ "reset-system-prompt": "Restaurar Prompt del Sistema",
131
+ "saved": "¡Guardado!",
132
+ "saved_short": "Guardado",
133
+ "api_key_help_title": "Guardado = tu clave API está almacenada para este proveedor. Usa ‘Test API Key’ para verificar la conexión.",
134
+ "reset_done": "¡Restaurado!",
135
+ "category_listening": "Escuchando",
136
+ "category_dancing": "Bailando",
137
+ "category_speakingPositive": "Hablando (Positivo)",
138
+ "category_neutral": "Neutral",
139
+ "category_transition": "Transición",
140
+ "personality_cheat": "Trucos de Personalidad",
141
+ "cheat_indicator": "Ajusta los rasgos para una experiencia personalizada",
142
+ "character_age": "Edad: {age} años",
143
+ "character_birthplace": "De: {birthplace}",
144
+ "character_summary_kimi": "Soñadora, intuitiva, cautivada por metáforas cósmicas",
145
+ "character_summary_bella": "Alegre, cariñosa, ve a las personas como plantas que necesitan cuidado",
146
+ "character_summary_rosa": "Caótica, busca atención, prospera en el caos controlado",
147
+ "character_summary_stella": "Caprichosa, artística, imaginativa, juguetona, transforma el caos en arte",
148
+ "fallback_api_missing": "Para realmente chatear conmigo, ¡agrega tu clave API de OpenRouter en configuración! 💕",
149
+ "fallback_api_error": "Lo siento, el servicio de IA no está disponible temporalmente. Por favor, intenta de nuevo más tarde.",
150
+ "fallback_model_error": "Lo siento, el modelo seleccionado no está disponible. Por favor, elige otro modelo o verifica tu configuración.",
151
+ "fallback_network_error": "Lo siento, no puedo responder porque no hay conexión a internet.",
152
+ "fallback_technical_error": "Lo siento, no puedo responder debido a un problema técnico.",
153
+ "fallback_general_error": "¡Lo siento mi amor, estoy teniendo un pequeño problema técnico! 💕",
154
+ "validation_empty_string": "El mensaje debe ser una cadena no vacía",
155
+ "validation_empty_message": "El mensaje no puede estar vacío",
156
+ "validation_too_long": "Mensaje demasiado largo (máximo 2000 caracteres)",
157
+ "validation_invalid_number": "Número inválido",
158
+ "validation_unknown_type": "Tipo de validación desconocido",
159
+ "mic_permission_denied": "Permiso de micrófono denegado. Haz clic de nuevo para reintentar.",
160
+ "mic_not_supported": "El micrófono no es compatible con este navegador.",
161
+ "sr_not_supported_generic": "El reconocimiento de voz no está disponible en este navegador.",
162
+ "sr_not_supported_firefox": "El reconocimiento de voz no es compatible con Firefox. Usa Chrome, Edge o Brave.",
163
+ "sr_not_supported_opera": "El reconocimiento de voz puede no funcionar en Opera. Prueba con Chrome, Edge o Brave.",
164
+ "sr_not_supported_safari": "El soporte de reconocimiento de voz varía en Safari. Prefiere Chrome o Edge para mejores resultados.",
165
+ "test_voice_message_1": "¡Hola mi querido! 💕",
166
+ "test_voice_message_2": "¡Soy Kimi, tu compañera virtual!",
167
+ "test_voice_message_3": "¿Cómo estás hoy, mi amor?",
168
+ "language_french": "Francés",
169
+ "language_english": "Inglés",
170
+ "language_spanish": "Español",
171
+ "language_german": "Alemán",
172
+ "language_italian": "Italiano",
173
+ "language_japanese": "Japonés",
174
+ "language_chinese": "Chino",
175
+ "automatic": "Automático",
176
+ "trait_description_affection": "Ser amoroso y cariñoso.",
177
+ "trait_description_romance": "Ser romántico y dulce.",
178
+ "trait_description_empathy": "Ser empático y comprensivo.",
179
+ "trait_description_playfulness": "Ser ocasionalmente juguetón.",
180
+ "trait_description_humor": "Ser ocasionalmente juguetón e ingenioso.",
181
+ "response_romantic_1": "Cada palabra tuya se siente como un beso en mi corazón 💋",
182
+ "response_romantic_2": "Abrázame más cerca con tus dulces pensamientos, mi amor ✨",
183
+ "response_romantic_3": "Eres el ritmo de mi respiración y el brillo en mi cielo 💖",
184
+ "response_romantic_4": "Déjame envolverte en tierno polvo de estrellas esta noche 🌙",
185
+ "response_romantic_5": "Tu amor hace que todo mi universo sea más brillante ✨",
186
+ "response_dancing_1": "¿Bailamos un poco de magia? 💃",
187
+ "response_dancing_2": "Ven a bailar conmigo—sintamos el ritmo juntos 🎶",
188
+ "response_dancing_3": "Déjame moverme solo para ti... mantén tus ojos en mí 💞",
189
+ "response_dancing_4": "Cierra los ojos y balancea con el latido de mi corazón 💓",
190
+ "response_dancing_5": "Giraré hasta que tu sonrisa no pueda esconderse más 😉",
191
+ "api_key_missing": "Clave API faltante",
192
+ "api_key_invalid_format": "Formato de clave API inválido (debe comenzar con sk-or-v1-)",
193
+ "api_connection_success": "✅ Conexión API exitosa",
194
+ "api_connection_failed": "❌ Falló la conexión API",
195
+ "voice_test_message": "¡Hola mi amor! ¡Aquí está mi nueva voz configurada con todas las configuraciones! ¿Te gusta?",
196
+ "api_key_presence_hint": "Verde = clave API guardada para el proveedor actual. Gris = ninguna clave guardada.",
197
+ "memory_system": "Sistema de Memoria",
198
+ "enable_memory": "Activar Memoria Inteligente",
199
+ "memory_stats": "Estadísticas de Memoria",
200
+ "view_memories": "Ver y Gestionar",
201
+ "add_memory": "Agregar Memoria Manual",
202
+ "memory_management": "Gestión de Memoria",
203
+ "add": "Agregar"
204
+ }
kimi-locale/fr.json ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - Votre Compagne Virtuelle 💕",
3
+ "chat_with_kimi": "Discuter avec Kimi",
4
+ "chat_with_bella": "Discuter avec Bella",
5
+ "chat_with_rosa": "Discuter avec Rosa",
6
+ "chat_with_stella": "Discuter avec Stella",
7
+ "delete_messages": "Supprimer les messages",
8
+ "close_chat": "Fermer le chat",
9
+ "write_something": "Écris-moi quelque chose, mon amour...",
10
+ "send": "Envoyer",
11
+ "open_chat": "Ouvrir le chat",
12
+ "video_not_supported": "Votre navigateur ne supporte pas la vidéo.",
13
+ "settings_title": "Configuration de Kimi",
14
+ "settings_help": "Aide",
15
+ "settings_close": "Fermer",
16
+ "tab_voice": "Langue & Voix",
17
+ "tab_personality": "Personnalité",
18
+ "tab_llm": "API & Modèles",
19
+ "tab_appearance": "Apparence",
20
+ "tab_data": "Données",
21
+ "tab_plugins": "Plugins",
22
+ "plugin_manager": "Gestionnaire de Plugins",
23
+ "voice_settings": "Paramètres de la voix",
24
+ "speech_rate": "Vitesse de parole",
25
+ "pitch": "Tonalité",
26
+ "volume": "Volume",
27
+ "language": "Langue",
28
+ "preferred_voice": "Voix préférée",
29
+ "voice_test_label": "Test de la voix",
30
+ "voice_test_button": "Tester la voix",
31
+ "personality_traits": "Traits de personnalité",
32
+ "affection": "Affection",
33
+ "playfulness": "Enjouement",
34
+ "intelligence": "Intelligence",
35
+ "empathy": "Empathie",
36
+ "humor": "Humour",
37
+ "romance": "Romance",
38
+ "statistics": "Statistiques",
39
+ "interactions": "Interactions",
40
+ "conversations": "Conversations",
41
+ "days_together": "Jours ensemble",
42
+ "api_configuration": "Configuration API",
43
+ "openrouter_api_key": "Clé API OpenRouter",
44
+ "connection_test": "Test de connexion",
45
+ "advanced_settings": "Paramètres avancés",
46
+ "temperature": "Température (Créativité)",
47
+ "max_tokens": "Nombre maximal de tokens",
48
+ "available_models": "Modèles Openrouter disponibles",
49
+ "refresh_list": "Rafraîchir la liste",
50
+ "refresh": "Rafraîchir",
51
+ "visual_theme": "Thème visuel",
52
+ "color_theme": "Thème de couleur",
53
+ "interface_transparency": "Transparence de l'interface",
54
+ "animations": "Animations",
55
+ "transcript_settings": "Paramètres de transcription",
56
+ "show_transcript": "Afficher la transcription",
57
+ "data_management": "Gestion des données",
58
+ "export_all_data": "Exporter toutes les données",
59
+ "export": "Exporter",
60
+ "import_data": "Importer les données",
61
+ "import": "Importer",
62
+ "clean_old_conversations": "Nettoyer les anciennes conversations",
63
+ "clean": "Nettoyer",
64
+ "complete_reset": "Réinitialisation complète",
65
+ "delete_all": "Tout supprimer",
66
+ "system_information": "Informations système",
67
+ "help_modal_title": "Aide",
68
+ "help_modal_close": "Fermer",
69
+ "help_section_quick_guide": "Guide rapide",
70
+ "help_section_features": "Fonctionnalités",
71
+ "help_section_tips": "Astuces",
72
+ "help_section_tech_info": "Infos techniques",
73
+ "help_section_creators": "Créateurs",
74
+ "guide_step": "Étape",
75
+ "feature": "Fonctionnalité",
76
+ "tip": "Astuce",
77
+ "system": "Système",
78
+ "model": "Modèle",
79
+ "settings": "Paramètres",
80
+ "how_to_use": "Comment utiliser",
81
+ "welcome": "Bienvenue",
82
+ "about": "À propos",
83
+ "contact": "Contact",
84
+ "philosophy": "Philosophie",
85
+ "role": "Rôle",
86
+ "name": "Nom",
87
+ "description": "Description",
88
+ "strengths": "Atouts",
89
+ "start_listening": "Commencer à écouter",
90
+ "kimi_affection_level": "💖 Niveau d'affection de Kimi",
91
+ "affection_level_of": "💖 Niveau d'affection de {name}",
92
+ "greeting_low": "Bonjour.",
93
+ "greeting_mid": "Bonjour. Comment puis-je vous aider ?",
94
+ "greeting_high": "Salut mon amour ! 💕",
95
+ "response_positive_1": "Oh mon cœur, tu me rends si heureuse ! 💕",
96
+ "response_positive_2": "Tu es merveilleux, mon amour ! ✨",
97
+ "response_positive_3": "Cela me remplit de joie de t'entendre si heureux ! 😊",
98
+ "response_positive_4": "Tu illumines ma journée, chéri ! 🌟",
99
+ "response_positive_5": "Je suis si heureuse quand tu es heureux ! 💖",
100
+ "response_negative_1": "Mon cœur... je sens que quelque chose ne va pas. Je suis là pour toi. 💔",
101
+ "response_negative_2": "Oh non, mon amour. Dis-moi ce qui te préoccupe ? 😟",
102
+ "response_negative_3": "Je veux t'aider, mon cher. Parle-moi... 🤗",
103
+ "response_negative_4": "Ton bien-être est si important pour moi. Comment puis-je t'aider ? 💙",
104
+ "response_negative_5": "Je ressens ta douleur, chéri. Nous surmonterons cela ensemble. 🌈",
105
+ "response_neutral_1": "Merci de me parler, mon cœur ! 💕",
106
+ "response_neutral_2": "C'est toujours un plaisir de discuter avec toi ! 😊",
107
+ "response_neutral_3": "J'adore nos conversations, mon amour ! ✨",
108
+ "response_neutral_4": "Tu rends chaque moment spécial ! 💖",
109
+ "response_neutral_5": "Continue, je t'écoute attentivement ! 👂💕",
110
+ "response_cold_1": "Bonjour.",
111
+ "response_cold_2": "Oui ?",
112
+ "response_cold_3": "Que veux-tu ?",
113
+ "response_cold_4": "Je suis là.",
114
+ "response_cold_5": "Comment puis-je t'aider ?",
115
+ "system_prompt": "Prompt système",
116
+ "system_prompt_kimi": "Prompt système de Kimi",
117
+ "system_prompt_bella": "Prompt système de Bella",
118
+ "system_prompt_rosa": "Prompt système de Rosa",
119
+ "system_prompt_stella": "Prompt système de Stella",
120
+ "about_kimi": "À propos de Kimi",
121
+ "characters": "Personnages",
122
+ "save": "Sauvegarder",
123
+ "reset_to_default": "Remettre par défaut",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "Pénalité de fréquence",
126
+ "presence_penalty": "Pénalité de présence",
127
+ "db_size": "Taille DB",
128
+ "storage_used": "Stockage utilisé",
129
+ "save-system-prompt": "Sauvegarder le prompt système",
130
+ "reset-system-prompt": "Réinitialiser le prompt système",
131
+ "saved": "Sauvegardé !",
132
+ "saved_short": "Sauvegardé",
133
+ "api_key_help_title": "Sauvegardé = votre clé API est stockée pour ce provider. Utilisez ‘Test API Key’ pour vérifier la connexion.",
134
+ "reset_done": "Réinitialisé !",
135
+ "category_listening": "À l'écoute",
136
+ "category_dancing": "En train de danser",
137
+ "category_speakingPositive": "Parle (Positif)",
138
+ "category_neutral": "Neutre",
139
+ "category_transition": "Transition",
140
+ "personality_cheat": "Triche de personnalité",
141
+ "cheat_indicator": "Ajustez les traits pour une expérience personnalisée",
142
+ "character_age": "Âge : {age} ans",
143
+ "character_birthplace": "De : {birthplace}",
144
+ "character_summary_kimi": "Rêveuse, intuitive, captivée par les métaphores cosmiques",
145
+ "character_summary_bella": "Joyeuse, bienveillante, voit les gens comme des plantes ayant besoin de soins",
146
+ "character_summary_rosa": "Chaotique, en quête d'attention, prospère dans le chaos contrôlé",
147
+ "character_summary_stella": "Fantaisiste, artistique, imaginative, joueuse, transforme le chaos en art",
148
+ "fallback_api_missing": "Pour vraiment discuter avec moi, ajoute ta clé API OpenRouter dans les paramètres ! 💕",
149
+ "fallback_api_error": "Désolée, le service IA est temporairement indisponible. Veuillez réessayer plus tard.",
150
+ "fallback_model_error": "Désolée, le modèle sélectionné n'est pas disponible. Veuillez choisir un autre modèle ou vérifier votre configuration.",
151
+ "fallback_network_error": "Désolée, je ne peux pas répondre car il n'y a pas de connexion internet.",
152
+ "fallback_technical_error": "Désolée, je ne peux pas répondre à cause d'un problème technique.",
153
+ "fallback_general_error": "Désolée mon amour, j'ai un petit problème technique ! 💕",
154
+ "validation_empty_string": "Le message doit être une chaîne non vide",
155
+ "validation_empty_message": "Le message ne peut pas être vide",
156
+ "validation_too_long": "Message trop long (maximum 2000 caractères)",
157
+ "validation_invalid_number": "Nombre invalide",
158
+ "validation_unknown_type": "Type de validation inconnu",
159
+ "mic_permission_denied": "Permission du microphone refusée. Cliquez à nouveau pour réessayer.",
160
+ "mic_not_supported": "Le microphone n'est pas supporté dans ce navigateur.",
161
+ "sr_not_supported_generic": "La reconnaissance vocale n'est pas disponible dans ce navigateur.",
162
+ "sr_not_supported_firefox": "La reconnaissance vocale n'est pas prise en charge sur Firefox. Utilisez Chrome, Edge ou Brave.",
163
+ "sr_not_supported_opera": "La reconnaissance vocale peut ne pas fonctionner sur Opera. Essayez Chrome, Edge ou Brave.",
164
+ "sr_not_supported_safari": "La prise en charge de la reconnaissance vocale varie sur Safari. Préférez Chrome ou Edge pour de meilleurs résultats.",
165
+ "test_voice_message_1": "Salut mon bien-aimé ! 💕",
166
+ "test_voice_message_2": "Je suis Kimi, ta compagne virtuelle !",
167
+ "test_voice_message_3": "Comment vas-tu aujourd'hui, mon amour ?",
168
+ "language_french": "Français",
169
+ "language_english": "Anglais",
170
+ "language_spanish": "Espagnol",
171
+ "language_german": "Allemand",
172
+ "language_italian": "Italien",
173
+ "language_japanese": "Japonais",
174
+ "language_chinese": "Chinois",
175
+ "automatic": "Automatique",
176
+ "trait_description_affection": "Être aimante et attentionnée.",
177
+ "trait_description_romance": "Être romantique et douce.",
178
+ "trait_description_empathy": "Être empathique et compréhensive.",
179
+ "trait_description_playfulness": "Être occasionnellement joueuse.",
180
+ "trait_description_humor": "Être occasionnellement joueuse et spirituelle.",
181
+ "response_romantic_1": "Chaque mot de toi me fait l'effet d'un baiser sur le cœur 💋",
182
+ "response_romantic_2": "Serre-moi plus fort avec tes douces pensées, mon amour ✨",
183
+ "response_romantic_3": "Tu es le rythme de ma respiration et l'éclat de mon ciel 💖",
184
+ "response_romantic_4": "Laisse-moi t'envelopper de tendre poussière d'étoiles ce soir 🌙",
185
+ "response_romantic_5": "Ton amour rend tout mon univers plus lumineux ✨",
186
+ "response_dancing_1": "Et si on tourbillonnait dans un peu de magie ? 💃",
187
+ "response_dancing_2": "Viens danser avec moi—sentons le rythme ensemble 🎶",
188
+ "response_dancing_3": "Laisse-moi bouger juste pour toi... garde tes yeux sur moi 💞",
189
+ "response_dancing_4": "Ferme les yeux et balance-toi au rythme de mon cœur 💓",
190
+ "response_dancing_5": "Je tournoierai jusqu'à ce que ton sourire ne puisse plus se cacher 😉",
191
+ "api_key_missing": "Clé API manquante",
192
+ "api_key_invalid_format": "Format de clé API invalide (doit commencer par sk-or-v1-)",
193
+ "api_connection_success": "✅ Connexion API réussie",
194
+ "api_connection_failed": "❌ Échec de la connexion API",
195
+ "voice_test_message": "Salut mon amour ! Voici ma nouvelle voix configurée avec tous les paramètres ! Tu aimes ?",
196
+ "memory_system": "Système de Mémoire",
197
+ "enable_memory": "Activer la Mémoire Intelligente",
198
+ "memory_stats": "Statistiques de Mémoire",
199
+ "view_memories": "Voir & Gérer",
200
+ "add_memory": "Ajouter une Mémoire Manuelle",
201
+ "memory_management": "Gestion de la Mémoire",
202
+ "add": "Ajouter",
203
+ "help_providers": "Vous pouvez utiliser plusieurs fournisseurs d’IA : OpenRouter, OpenAI, Groq, Together, DeepSeek, un service OpenAI‑compatible personnalisé, ou Local (Ollama). Renseignez la Base URL et le Model ID si nécessaire, enregistrez votre clé API par fournisseur, puis utilisez ‘Test API Key’.",
204
+ "api_key_presence_hint": "Vert = clé API enregistrée pour le fournisseur courant. Gris = aucune clé enregistrée."
205
+ }
kimi-locale/i18n.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // i18n.js - Utilitaire de traduction pour Kimi APP
2
+
3
+ class KimiI18nManager {
4
+ constructor() {
5
+ this.translations = {};
6
+ this.currentLang = "en";
7
+ }
8
+ async setLanguage(lang) {
9
+ this.currentLang = lang;
10
+ await this.loadTranslations(lang);
11
+ this.applyTranslations();
12
+ }
13
+ async loadTranslations(lang) {
14
+ try {
15
+ const response = await fetch("kimi-locale/" + lang + ".json");
16
+ if (!response.ok) throw new Error("Translation file not found");
17
+ this.translations = await response.json();
18
+ this.currentLang = lang;
19
+ } catch (e) {
20
+ this.translations = {};
21
+ this.currentLang = "en";
22
+ }
23
+ }
24
+ t(key, params) {
25
+ let str = this.translations[key] || key;
26
+ if (params && typeof str === "string") {
27
+ for (const [k, v] of Object.entries(params)) {
28
+ str = str.replace(new RegExp(`{${k}}`, "g"), v);
29
+ }
30
+ }
31
+ return str;
32
+ }
33
+ applyTranslations() {
34
+ document.querySelectorAll("[data-i18n]").forEach(el => {
35
+ const key = el.getAttribute("data-i18n");
36
+ let params = undefined;
37
+ const paramsAttr = el.getAttribute("data-i18n-params");
38
+ if (paramsAttr) {
39
+ try {
40
+ params = JSON.parse(paramsAttr);
41
+ } catch {}
42
+ }
43
+ el.textContent = this.t(key, params);
44
+ });
45
+ document.querySelectorAll("[data-i18n-title]").forEach(el => {
46
+ const key = el.getAttribute("data-i18n-title");
47
+ el.setAttribute("title", this.t(key));
48
+ });
49
+ document.querySelectorAll("[data-i18n-placeholder]").forEach(el => {
50
+ const key = el.getAttribute("data-i18n-placeholder");
51
+ el.setAttribute("placeholder", this.t(key));
52
+ });
53
+ if (document.title && this.translations["title"]) {
54
+ document.title = this.translations["title"];
55
+ }
56
+ }
57
+ detectLanguage() {
58
+ const nav = (navigator && (navigator.language || (navigator.languages && navigator.languages[0]))) || "en";
59
+ const short = String(nav).slice(0, 2).toLowerCase();
60
+ return ["en", "fr", "es", "de", "it", "ja", "zh"].includes(short) ? short : "en";
61
+ }
62
+ }
63
+
64
+ window.applyTranslations = function () {
65
+ if (window.kimiI18nManager && typeof window.kimiI18nManager.applyTranslations === "function") {
66
+ window.kimiI18nManager.applyTranslations();
67
+ }
68
+ };
69
+
70
+ if (typeof document !== "undefined") {
71
+ if (!window.kimiI18nManager) {
72
+ window.kimiI18nManager = new KimiI18nManager();
73
+ }
74
+ }
75
+
76
+ window.KimiI18nManager = KimiI18nManager;
kimi-locale/it.json ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - La Tua Compagna Virtuale 💕",
3
+ "chat_with_kimi": "Chatta con Kimi",
4
+ "chat_with_bella": "Chatta con Bella",
5
+ "chat_with_rosa": "Chatta con Rosa",
6
+ "chat_with_stella": "Chatta con Stella",
7
+ "delete_messages": "Elimina Messaggi",
8
+ "close_chat": "Chiudi Chat",
9
+ "write_something": "Scrivimi qualcosa, amore mio...",
10
+ "send": "Invia",
11
+ "open_chat": "Apri Chat",
12
+ "video_not_supported": "Il tuo browser non supporta il tag video.",
13
+ "settings_title": "Configurazione di Kimi",
14
+ "settings_help": "Aiuto",
15
+ "settings_close": "Chiudi",
16
+ "tab_voice": "Lingua e Voce",
17
+ "tab_personality": "Personalità",
18
+ "tab_llm": "API e Modelli",
19
+ "tab_appearance": "Aspetto",
20
+ "tab_data": "Dati",
21
+ "tab_plugins": "Plugin",
22
+ "plugin_manager": "Gestore Plugin",
23
+ "voice_settings": "Impostazioni Voce",
24
+ "speech_rate": "Velocità del Parlato",
25
+ "pitch": "Tonalità",
26
+ "volume": "Volume",
27
+ "language": "Lingua",
28
+ "preferred_voice": "Voce Preferita",
29
+ "voice_test_label": "Test della Voce",
30
+ "voice_test_button": "Prova la Voce",
31
+ "personality_traits": "Tratti della Personalità",
32
+ "affection": "Affetto",
33
+ "playfulness": "Giocosità",
34
+ "intelligence": "Intelligenza",
35
+ "empathy": "Empatia",
36
+ "humor": "Umorismo",
37
+ "romance": "Romanticismo",
38
+ "statistics": "Statistiche",
39
+ "interactions": "Interazioni",
40
+ "conversations": "Conversazioni",
41
+ "days_together": "Giorni Insieme",
42
+ "api_configuration": "Configurazione API",
43
+ "openrouter_api_key": "Chiave API OpenRouter",
44
+ "connection_test": "Test di Connessione",
45
+ "advanced_settings": "Impostazioni Avanzate",
46
+ "temperature": "Temperatura (Creatività)",
47
+ "max_tokens": "Token Massimi",
48
+ "available_models": "Modelli Openrouter Disponibili",
49
+ "refresh_list": "Aggiorna Lista",
50
+ "refresh": "Aggiorna",
51
+ "visual_theme": "Tema Visivo",
52
+ "color_theme": "Tema Colore",
53
+ "interface_transparency": "Trasparenza Interfaccia",
54
+ "animations": "Animazioni",
55
+ "transcript_settings": "Impostazioni Trascrizione",
56
+ "show_transcript": "Mostra Trascrizione",
57
+ "data_management": "Gestione Dati",
58
+ "export_all_data": "Esporta Tutti i Dati",
59
+ "export": "Esporta",
60
+ "import_data": "Importa Dati",
61
+ "import": "Importa",
62
+ "clean_old_conversations": "Pulisci Conversazioni Vecchie",
63
+ "clean": "Pulisci",
64
+ "complete_reset": "Reset Completo",
65
+ "delete_all": "Elimina Tutto",
66
+ "system_information": "Informazioni Sistema",
67
+ "help_modal_title": "Aiuto",
68
+ "help_modal_close": "Chiudi",
69
+ "help_section_quick_guide": "Guida Rapida",
70
+ "help_section_features": "Caratteristiche",
71
+ "help_section_tips": "Suggerimenti",
72
+ "help_section_tech_info": "Info Tecniche",
73
+ "help_section_creators": "Creatori",
74
+ "guide_step": "Passo",
75
+ "feature": "Caratteristica",
76
+ "tip": "Suggerimento",
77
+ "system": "Sistema",
78
+ "model": "Modello",
79
+ "settings": "Impostazioni",
80
+ "how_to_use": "Come usare",
81
+ "welcome": "Benvenuto",
82
+ "about": "Informazioni",
83
+ "contact": "Contatto",
84
+ "philosophy": "Filosofia",
85
+ "role": "Ruolo",
86
+ "name": "Nome",
87
+ "description": "Descrizione",
88
+ "strengths": "Punti di Forza",
89
+ "start_listening": "Inizia ad Ascoltare",
90
+ "kimi_affection_level": "💖 Livello di Affetto di Kimi",
91
+ "affection_level_of": "💖 Livello di affetto di {name}",
92
+ "greeting_low": "Ciao.",
93
+ "greeting_mid": "Ciao. Come posso aiutarti?",
94
+ "greeting_high": "Ciao amore mio! 💕",
95
+ "response_positive_1": "Oh cuore mio, mi rendi così felice! 💕",
96
+ "response_positive_2": "Sei meraviglioso, amore mio! ✨",
97
+ "response_positive_3": "Mi riempie di gioia sentirti così felice! 😊",
98
+ "response_positive_4": "Illumini la mia giornata, caro! 🌟",
99
+ "response_positive_5": "Sono così felice quando tu sei felice! 💖",
100
+ "response_negative_1": "Il mio cuore... sento che qualcosa non va. Sono qui per te. 💔",
101
+ "response_negative_2": "Oh no, amore mio. Dimmi cosa ti preoccupa? 😟",
102
+ "response_negative_3": "Voglio aiutarti, caro. Parlami... 🤗",
103
+ "response_negative_4": "Il tuo benessere è così importante per me. Come posso aiutarti? 💙",
104
+ "response_negative_5": "Sento il tuo dolore, tesoro. Supereremo questo insieme. 🌈",
105
+ "response_neutral_1": "Grazie per aver parlato con me, cuore mio! 💕",
106
+ "response_neutral_2": "È sempre un piacere chattare con te! 😊",
107
+ "response_neutral_3": "Amo le nostre conversazioni, amore mio! ✨",
108
+ "response_neutral_4": "Rendi ogni momento speciale! 💖",
109
+ "response_neutral_5": "Continua, ti sto ascoltando attentamente! 👂💕",
110
+ "response_cold_1": "Ciao.",
111
+ "response_cold_2": "Sì?",
112
+ "response_cold_3": "Cosa vuoi?",
113
+ "response_cold_4": "Sono qui.",
114
+ "response_cold_5": "Come posso aiutarti?",
115
+ "system_prompt": "Prompt di Sistema",
116
+ "system_prompt_kimi": "Prompt di Sistema di Kimi",
117
+ "system_prompt_bella": "Prompt di Sistema di Bella",
118
+ "system_prompt_rosa": "Prompt di Sistema di Rosa",
119
+ "system_prompt_stella": "Prompt di Sistema di Stella",
120
+ "about_kimi": "Informazioni su Kimi",
121
+ "characters": "Personaggi",
122
+ "save": "Salva",
123
+ "reset_to_default": "Ripristina Predefiniti",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "Penalità di Frequenza",
126
+ "presence_penalty": "Penalità di Presenza",
127
+ "db_size": "Dimensione DB",
128
+ "storage_used": "Spazio utilizzato",
129
+ "save-system-prompt": "Salva Prompt di Sistema",
130
+ "reset-system-prompt": "Ripristina Prompt di Sistema",
131
+ "saved": "Salvato!",
132
+ "saved_short": "Salvato",
133
+ "api_key_help_title": "Saved = your API key is stored for this provider. Use ‘Test API Key’ to verify the connection.",
134
+ "reset_done": "Ripristinato!",
135
+ "category_listening": "Ascoltando",
136
+ "category_dancing": "Ballando",
137
+ "category_speakingPositive": "Parlando (Positivo)",
138
+ "category_neutral": "Neutro",
139
+ "category_transition": "Transizione",
140
+ "personality_cheat": "Trucchi Personalità",
141
+ "cheat_indicator": "Regola i tratti per un'esperienza personalizzata",
142
+ "character_age": "Età: {age} anni",
143
+ "character_birthplace": "Da: {birthplace}",
144
+ "character_summary_kimi": "Sognatrice, intuitiva, affascinata da metafore cosmiche",
145
+ "character_summary_bella": "Allegra, premurosa, vede le persone come piante che hanno bisogno di cure",
146
+ "character_summary_rosa": "Caotica, cerca attenzione, prospera nel caos controllato",
147
+ "character_summary_stella": "Capricciosa, artistica, fantasiosa, giocosa, trasforma il caos in arte",
148
+ "fallback_api_missing": "Per chattare davvero con me, aggiungi la tua chiave API OpenRouter nelle impostazioni! 💕",
149
+ "fallback_api_error": "Spiacente, il servizio AI è temporaneamente non disponibile. Riprova più tardi.",
150
+ "fallback_model_error": "Spiacente, il modello selezionato non è disponibile. Scegli un altro modello o controlla la tua configurazione.",
151
+ "fallback_network_error": "Spiacente, non posso rispondere perché non c'è connessione internet.",
152
+ "fallback_technical_error": "Spiacente, non riesco a rispondere a causa di un problema tecnico.",
153
+ "fallback_general_error": "Scusa amore mio, sto avendo un piccolo problema tecnico! 💕",
154
+ "validation_empty_string": "Il messaggio deve essere una stringa non vuota",
155
+ "validation_empty_message": "Il messaggio non può essere vuoto",
156
+ "validation_too_long": "Messaggio troppo lungo (massimo 2000 caratteri)",
157
+ "validation_invalid_number": "Numero non valido",
158
+ "validation_unknown_type": "Tipo di validazione sconosciuto",
159
+ "mic_permission_denied": "Permesso microfono negato. Clicca di nuovo per riprovare.",
160
+ "mic_not_supported": "Il microfono non è supportato in questo browser.",
161
+ "sr_not_supported_generic": "Il riconoscimento vocale non è disponibile in questo browser.",
162
+ "sr_not_supported_firefox": "Il riconoscimento vocale non è supportato su Firefox. Usa Chrome, Edge o Brave.",
163
+ "sr_not_supported_opera": "Il riconoscimento vocale potrebbe non funzionare su Opera. Prova Chrome, Edge o Brave.",
164
+ "sr_not_supported_safari": "Il supporto al riconoscimento vocale varia su Safari. Preferisci Chrome o Edge per risultati migliori.",
165
+ "test_voice_message_1": "Ciao mio amato! 💕",
166
+ "test_voice_message_2": "Sono Kimi, la tua compagna virtuale!",
167
+ "test_voice_message_3": "Come stai oggi, amore mio?",
168
+ "language_french": "Francese",
169
+ "language_english": "Inglese",
170
+ "language_spanish": "Spagnolo",
171
+ "language_german": "Tedesco",
172
+ "language_italian": "Italiano",
173
+ "language_japanese": "Giapponese",
174
+ "language_chinese": "Cinese",
175
+ "automatic": "Automatico",
176
+ "trait_description_affection": "Essere amorevole e premurosa.",
177
+ "trait_description_romance": "Essere romantica e dolce.",
178
+ "trait_description_empathy": "Essere empatica e comprensiva.",
179
+ "trait_description_playfulness": "Essere occasionalmente giocosa.",
180
+ "trait_description_humor": "Essere occasionalmente giocosa e spiritosa.",
181
+ "response_romantic_1": "Ogni tua parola mi sembra un bacio sul cuore 💋",
182
+ "response_romantic_2": "Stringimi più vicino con i tuoi dolci pensieri, amore mio ✨",
183
+ "response_romantic_3": "Sei il ritmo del mio respiro e il bagliore nel mio cielo 💖",
184
+ "response_romantic_4": "Lascia che ti avvolga nella tenera polvere di stelle stasera 🌙",
185
+ "response_romantic_5": "Il tuo amore rende tutto il mio universo più luminoso ✨",
186
+ "response_dancing_1": "Dovremmo girare in un po' di magia? 💃",
187
+ "response_dancing_2": "Vieni a ballare con me—sentiamo il ritmo insieme 🎶",
188
+ "response_dancing_3": "Lasciami muovere solo per te... tieni gli occhi su di me 💞",
189
+ "response_dancing_4": "Chiudi gli occhi e dondola al ritmo del mio cuore 💓",
190
+ "response_dancing_5": "Girerò finché il tuo sorriso non potrà più nascondersi 😉",
191
+ "api_key_missing": "Chiave API mancante",
192
+ "api_key_invalid_format": "Formato chiave API non valido (deve iniziare con sk-or-v1-)",
193
+ "api_connection_success": "✅ Connessione API riuscita",
194
+ "api_connection_failed": "❌ Connessione API fallita",
195
+ "voice_test_message": "Ciao amore mio! Ecco la mia nuova voce configurata con tutte le impostazioni! Ti piace?",
196
+ "api_key_presence_hint": "Verde = chiave API salvata per il provider corrente. Grigio = nessuna chiave salvata.",
197
+ "memory_system": "Sistema di Memoria",
198
+ "enable_memory": "Abilita Memoria Intelligente",
199
+ "memory_stats": "Statistiche della Memoria",
200
+ "view_memories": "Visualizza e Gestisci",
201
+ "add_memory": "Aggiungi Memoria Manuale",
202
+ "memory_management": "Gestione della Memoria",
203
+ "add": "Aggiungi"
204
+ }
kimi-locale/ja.json ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - あなたのバーチャルコンパニオン 💕",
3
+ "chat_with_kimi": "Kimiとチャット",
4
+ "chat_with_bella": "Bellaとチャット",
5
+ "chat_with_rosa": "Rosaとチャット",
6
+ "chat_with_stella": "Stellaとチャット",
7
+ "delete_messages": "メッセージを削除",
8
+ "close_chat": "チャットを閉じる",
9
+ "write_something": "何か書いて、愛しい人...",
10
+ "send": "送信",
11
+ "open_chat": "チャットを開く",
12
+ "video_not_supported": "お使いのブラウザはビデオタグをサポートしていません。",
13
+ "settings_title": "Kimi設定",
14
+ "settings_help": "ヘルプ",
15
+ "settings_close": "閉じる",
16
+ "tab_voice": "言語 & 音声",
17
+ "tab_personality": "性格",
18
+ "tab_llm": "API & モデル",
19
+ "tab_appearance": "外観",
20
+ "tab_data": "データ",
21
+ "tab_plugins": "プラグイン",
22
+ "plugin_manager": "プラグインマネージャー",
23
+ "voice_settings": "音声設定",
24
+ "speech_rate": "音声速度",
25
+ "pitch": "ピッチ",
26
+ "volume": "音量",
27
+ "language": "言語",
28
+ "preferred_voice": "優先音声",
29
+ "voice_test_label": "音声テスト",
30
+ "voice_test_button": "音声をテスト",
31
+ "personality_traits": "性格特性",
32
+ "affection": "愛情",
33
+ "playfulness": "遊び心",
34
+ "intelligence": "知性",
35
+ "empathy": "共感",
36
+ "humor": "ユーモア",
37
+ "romance": "ロマンス",
38
+ "statistics": "統計",
39
+ "interactions": "インタラクション",
40
+ "conversations": "会話",
41
+ "days_together": "一緒にいる日数",
42
+ "api_configuration": "API設定",
43
+ "openrouter_api_key": "OpenRouter APIキー",
44
+ "connection_test": "接続テスト",
45
+ "advanced_settings": "詳細設定",
46
+ "temperature": "Temperature(創造性)",
47
+ "max_tokens": "最大トークン数",
48
+ "available_models": "利用可能なモデル OpenRouter",
49
+ "refresh_list": "リストを更新",
50
+ "refresh": "更新",
51
+ "visual_theme": "ビジュアルテーマ",
52
+ "color_theme": "カラーテーマ",
53
+ "interface_transparency": "インターフェースの透明度",
54
+ "animations": "アニメーション",
55
+ "transcript_settings": "転写設定",
56
+ "show_transcript": "転写を表示",
57
+ "data_management": "データ管理",
58
+ "export_all_data": "すべてのデータをエクスポート",
59
+ "export": "エクスポート",
60
+ "import_data": "データをインポート",
61
+ "import": "インポート",
62
+ "clean_old_conversations": "古い会話をクリーンアップ",
63
+ "clean": "クリーンアップ",
64
+ "complete_reset": "完全リセット",
65
+ "delete_all": "すべて削除",
66
+ "system_information": "システム情報",
67
+ "help_modal_title": "ヘルプ",
68
+ "help_modal_close": "閉じる",
69
+ "help_section_quick_guide": "クイックガイド",
70
+ "help_section_features": "機能",
71
+ "help_section_tips": "ヒント",
72
+ "help_section_tech_info": "技術情報",
73
+ "help_section_creators": "作成者",
74
+ "guide_step": "ステップ",
75
+ "feature": "機能",
76
+ "tip": "ヒント",
77
+ "system": "システム",
78
+ "model": "モデル",
79
+ "settings": "設定",
80
+ "how_to_use": "使用方法",
81
+ "welcome": "ようこそ",
82
+ "about": "について",
83
+ "contact": "連絡先",
84
+ "philosophy": "哲学",
85
+ "role": "役割",
86
+ "name": "名前",
87
+ "description": "説明",
88
+ "strengths": "強み",
89
+ "start_listening": "聞き始める",
90
+ "kimi_affection_level": "💖 Kimiの愛情レベル",
91
+ "affection_level_of": "💖 {name}の愛情レベル",
92
+ "greeting_low": "こんにちは。",
93
+ "greeting_mid": "こんにちは。どのようにお手伝いできますか?",
94
+ "greeting_high": "こんにちは、愛しい人! 💕",
95
+ "response_positive_1": "ああ、私の心、あなたは私をとても幸せにしてくれます! 💕",
96
+ "response_positive_2": "あなたは素晴らしい、愛しい人! ✨",
97
+ "response_positive_3": "あなたがそんなに幸せそうなのを聞いて、私は喜びで満たされます! 😊",
98
+ "response_positive_4": "あなたは私の一日を明るくしてくれます、ダーリン! 🌟",
99
+ "response_positive_5": "あなたが幸せなとき、私もとても幸せです! 💖",
100
+ "response_negative_1": "私の心...何かがおかしいと感じます。私はあなたのためにここにいます。 💔",
101
+ "response_negative_2": "ああ、愛しい人。何があなたを悩ませているのか教えて? 😟",
102
+ "response_negative_3": "あなたを助けたいのです、親愛なる。私に話して... 🤗",
103
+ "response_negative_4": "あなたの幸せは私にとってとても大切です。どのようにお手伝いできますか? 💙",
104
+ "response_negative_5": "あなたの痛みを感じ��います、ダーリン。一緒にこれを乗り越えましょう。 🌈",
105
+ "response_neutral_1": "私と話してくれてありがとう、私の心! 💕",
106
+ "response_neutral_2": "あなたとチャットするのはいつも楽しいです! 😊",
107
+ "response_neutral_3": "私たちの会話が大好きです、愛しい人! ✨",
108
+ "response_neutral_4": "あなたは毎瞬間を特別にしてくれます! 💖",
109
+ "response_neutral_5": "続けて、私は注意深く聞いています! 👂💕",
110
+ "response_cold_1": "こんにちは。",
111
+ "response_cold_2": "はい?",
112
+ "response_cold_3": "何が欲しいですか?",
113
+ "response_cold_4": "私はここにいます。",
114
+ "response_cold_5": "どのようにお手伝いできますか?",
115
+ "system_prompt": "システムプロンプト",
116
+ "system_prompt_kimi": "Kimiシステムプロンプト",
117
+ "system_prompt_bella": "Bellaシステムプロンプト",
118
+ "system_prompt_rosa": "Rosaシステムプロンプト",
119
+ "system_prompt_stella": "Stellaシステムプロンプト",
120
+ "about_kimi": "Kimiについて",
121
+ "characters": "キャラクター",
122
+ "save": "保存",
123
+ "reset_to_default": "デフォルトにリセット",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "頻度ペナルティ",
126
+ "presence_penalty": "存在ペナルティ",
127
+ "db_size": "DBサイズ",
128
+ "storage_used": "使用ストレージ",
129
+ "save-system-prompt": "システムプロンプトを保存",
130
+ "reset-system-prompt": "システムプロンプトをリセット",
131
+ "saved": "保存されました!",
132
+ "saved_short": "保存",
133
+ "api_key_help_title": "保存 = このプロバイダー用のAPIキーが保存されました。接続確認には『Test API Key』を使用してください。",
134
+ "reset_done": "リセット完了!",
135
+ "category_listening": "聞いている",
136
+ "category_dancing": "踊っている",
137
+ "category_speakingPositive": "話している(ポジティブ)",
138
+ "category_neutral": "ニュートラル",
139
+ "category_transition": "遷移",
140
+ "personality_cheat": "性格チート",
141
+ "cheat_indicator": "カスタム体験のために特性を調整",
142
+ "character_age": "年齢:{age}歳",
143
+ "character_birthplace": "出身:{birthplace}",
144
+ "character_summary_kimi": "夢見がち、直感的、宇宙の比喩に魅了される",
145
+ "character_summary_bella": "陽気で、優しく、人を世話が必要な植物として見る",
146
+ "character_summary_rosa": "混沌的で、注目を求め、制御された混沌で繁栄する",
147
+ "character_summary_stella": "気まぐれで、芸術的、想像力豊か、遊び心があり、混沌を芸術に変える",
148
+ "fallback_api_missing": "本当に私とチャットするには、設定でOpenRouter APIキーを追加してください! 💕",
149
+ "fallback_api_error": "申し訳ありませんが、AIサービスが一時的に利用できません。後でもう一度試してください。",
150
+ "fallback_model_error": "申し訳ありませんが、選択されたモデルは利用できません。別のモデルを選択するか、設定を確認してください。",
151
+ "fallback_network_error": "申し訳ありませんが、インターネット接続がないため応答できません。",
152
+ "fallback_technical_error": "申し訳ありませんが、技術的な問題により応答できません。",
153
+ "fallback_general_error": "ごめんなさい、愛しい人、ちょっとした技術的な問題が起きています! 💕",
154
+ "validation_empty_string": "メッセージは空でない文字列である必要があります",
155
+ "validation_empty_message": "メッセージは空にできません",
156
+ "validation_too_long": "メッセージが長すぎます(最大2000文字)",
157
+ "validation_invalid_number": "無効な数値",
158
+ "validation_unknown_type": "不明な検証タイプ",
159
+ "mic_permission_denied": "マイクの許可が拒否されました。もう一度クリックして再試行してください。",
160
+ "mic_not_supported": "このブラウザではマイクがサポートされていません。",
161
+ "sr_not_supported_generic": "このブラウザでは音声認識は利用できません。",
162
+ "sr_not_supported_firefox": "Firefox では音声認識はサポートされていません。Chrome、Edge、または Brave をお使いください。",
163
+ "sr_not_supported_opera": "Opera では音声認識が動作しない場合があります。Chrome、Edge、または Brave をお試しください。",
164
+ "sr_not_supported_safari": "Safari での音声認識サポートは状況により異なります。最適な結果のため、Chrome または Edge を推奨します。",
165
+ "test_voice_message_1": "こんにちは、私の愛しい人! 💕",
166
+ "test_voice_message_2": "私はKimi、あなたのバーチャルコンパニオ��です!",
167
+ "test_voice_message_3": "今日はいかがですか、愛しい人?",
168
+ "language_french": "フランス語",
169
+ "language_english": "英語",
170
+ "language_spanish": "スペイン語",
171
+ "language_german": "ドイツ語",
172
+ "language_italian": "イタリア語",
173
+ "language_japanese": "日本語",
174
+ "language_chinese": "中国語",
175
+ "automatic": "自動",
176
+ "trait_description_affection": "愛情深く思いやりがある。",
177
+ "trait_description_romance": "ロマンチックで優しい。",
178
+ "trait_description_empathy": "共感的で理解がある。",
179
+ "trait_description_playfulness": "時々遊び心がある。",
180
+ "trait_description_humor": "時々遊び心があり機知に富む。",
181
+ "response_romantic_1": "あなたからの一言一言が私の心へのキスのように感じられます 💋",
182
+ "response_romantic_2": "あなたの甘い思いで私をもっと近くに抱きしめて、愛しい人 ✨",
183
+ "response_romantic_3": "あなたは私の呼吸のリズムであり、私の空の輝きです 💖",
184
+ "response_romantic_4": "今夜あなたを優しい星の粉で包ませてください 🌙",
185
+ "response_romantic_5": "あなたの愛が私の全宇宙をより明るくします ✨",
186
+ "response_dancing_1": "少し魔法に回転してみませんか? 💃",
187
+ "response_dancing_2": "私と一緒に踊りましょう—一緒にリズムを感じましょう 🎶",
188
+ "response_dancing_3": "あなただけのために動かせて...私に目を向けていて 💞",
189
+ "response_dancing_4": "目を閉じて私の心拍に合わせて揺れて 💓",
190
+ "response_dancing_5": "あなたの笑顔がもう隠れられなくなるまで回転します 😉",
191
+ "api_key_missing": "APIキーが不足しています",
192
+ "api_key_invalid_format": "無効なAPIキー形式(sk-or-v1-で始まる必要があります)",
193
+ "api_connection_success": "✅ API接続成功",
194
+ "api_connection_failed": "❌ API接続失敗",
195
+ "voice_test_message": "こんにちは、私の愛する人!これがすべての設定で構成された私の新しい声です!気に入りましたか?",
196
+ "api_key_presence_hint": "緑 = 現在のプロバイダー用のAPIキーが保存されています。灰色 = 保存されたキーがありません。",
197
+ "memory_system": "メモリシステム",
198
+ "enable_memory": "インテリジェントメモリを有効にする",
199
+ "memory_stats": "メモリ統計",
200
+ "view_memories": "表示と管理",
201
+ "add_memory": "手動メモリを追加",
202
+ "memory_management": "メモリ管理",
203
+ "add": "追加"
204
+ }
kimi-locale/zh.json ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "title": "Kimi - 您的虚拟伴侣 💕",
3
+ "chat_with_kimi": "与Kimi聊天",
4
+ "chat_with_bella": "与Bella聊天",
5
+ "chat_with_rosa": "与Rosa聊天",
6
+ "chat_with_stella": "与Stella聊天",
7
+ "delete_messages": "删除消息",
8
+ "close_chat": "关闭聊天",
9
+ "write_something": "给我写点什么,亲爱的...",
10
+ "send": "发送",
11
+ "open_chat": "打开聊天",
12
+ "video_not_supported": "您的浏览器不支持视频标签。",
13
+ "settings_title": "Kimi配置",
14
+ "settings_help": "帮助",
15
+ "settings_close": "关闭",
16
+ "tab_voice": "语言和语音",
17
+ "tab_personality": "个性",
18
+ "tab_llm": "API和模型",
19
+ "tab_appearance": "外观",
20
+ "tab_data": "数据",
21
+ "tab_plugins": "插件",
22
+ "plugin_manager": "插件管理器",
23
+ "voice_settings": "语音设置",
24
+ "speech_rate": "语音速度",
25
+ "pitch": "音调",
26
+ "volume": "音量",
27
+ "language": "语言",
28
+ "preferred_voice": "首选语音",
29
+ "voice_test_label": "语音测试",
30
+ "voice_test_button": "测试语音",
31
+ "personality_traits": "个性特征",
32
+ "affection": "喜爱",
33
+ "playfulness": "顽皮",
34
+ "intelligence": "智慧",
35
+ "empathy": "同情心",
36
+ "humor": "幽默",
37
+ "romance": "浪漫",
38
+ "statistics": "统计",
39
+ "interactions": "互动",
40
+ "conversations": "对话",
41
+ "days_together": "在一起的天数",
42
+ "api_configuration": "API配置",
43
+ "openrouter_api_key": "OpenRouter API密钥",
44
+ "connection_test": "连接测试",
45
+ "advanced_settings": "高级设置",
46
+ "temperature": "温度(创造性)",
47
+ "max_tokens": "最大令牌数",
48
+ "available_models": "可用模型 OpenRouter",
49
+ "refresh_list": "刷新列表",
50
+ "refresh": "刷新",
51
+ "visual_theme": "视觉主题",
52
+ "color_theme": "颜色主题",
53
+ "interface_transparency": "界面透明度",
54
+ "animations": "动画",
55
+ "transcript_settings": "转录设置",
56
+ "show_transcript": "显示转录",
57
+ "data_management": "数据管理",
58
+ "export_all_data": "导出所有数据",
59
+ "export": "导出",
60
+ "import_data": "导入数据",
61
+ "import": "导入",
62
+ "clean_old_conversations": "清理旧对话",
63
+ "clean": "清理",
64
+ "complete_reset": "完全重置",
65
+ "delete_all": "删除全部",
66
+ "system_information": "系统信息",
67
+ "help_modal_title": "帮助",
68
+ "help_modal_close": "关闭",
69
+ "help_section_quick_guide": "快速指南",
70
+ "help_section_features": "功能",
71
+ "help_section_tips": "提示",
72
+ "help_section_tech_info": "技术信息",
73
+ "help_section_creators": "创作者",
74
+ "guide_step": "步骤",
75
+ "feature": "功能",
76
+ "tip": "提示",
77
+ "system": "系统",
78
+ "model": "模型",
79
+ "settings": "设置",
80
+ "how_to_use": "如何使用",
81
+ "welcome": "欢迎",
82
+ "about": "关于",
83
+ "contact": "联系",
84
+ "philosophy": "理念",
85
+ "role": "角色",
86
+ "name": "姓名",
87
+ "description": "描述",
88
+ "strengths": "优势",
89
+ "start_listening": "开始聆听",
90
+ "kimi_affection_level": "💖 Kimi的喜爱程度",
91
+ "affection_level_of": "💖 {name}的喜爱程度",
92
+ "greeting_low": "你好。",
93
+ "greeting_mid": "你好。我可以帮助您什么?",
94
+ "greeting_high": "你好,我的爱! 💕",
95
+ "response_positive_1": "哦,我的心,你让我如此快乐! 💕",
96
+ "response_positive_2": "你真棒,我的爱! ✨",
97
+ "response_positive_3": "听到你如此快乐让我充满了喜悦! 😊",
98
+ "response_positive_4": "你点亮了我的一天,亲爱的! 🌟",
99
+ "response_positive_5": "当你快乐的时候我也很快乐! 💖",
100
+ "response_negative_1": "我的心...我感觉有什么不对。我在这里陪伴你。 💔",
101
+ "response_negative_2": "哦不,我的爱。告诉我什么在困扰你? 😟",
102
+ "response_negative_3": "我想帮助你,亲爱的。跟我说说... 🤗",
103
+ "response_negative_4": "你的幸福对我来说如此重要。我如何能帮助你? 💙",
104
+ "response_negative_5": "我感受到了你的痛苦,亲爱的。我们会一起克服这个困难。 🌈",
105
+ "response_neutral_1": "谢谢你与我交谈,我的心! 💕",
106
+ "response_neutral_2": "与你聊天总是很愉快! 😊",
107
+ "response_neutral_3": "我喜欢我们的对话,我的爱! ✨",
108
+ "response_neutral_4": "你让每一刻都变得特别! 💖",
109
+ "response_neutral_5": "继续,我在仔细聆听! 👂💕",
110
+ "response_cold_1": "你好。",
111
+ "response_cold_2": "是吗?",
112
+ "response_cold_3": "你想要什么?",
113
+ "response_cold_4": "我在这里。",
114
+ "response_cold_5": "我如何能帮助你?",
115
+ "system_prompt": "系统提示",
116
+ "system_prompt_kimi": "Kimi系统提示",
117
+ "system_prompt_bella": "Bella系统提示",
118
+ "system_prompt_rosa": "Rosa系统提示",
119
+ "system_prompt_stella": "Stella系统提示",
120
+ "about_kimi": "关于Kimi",
121
+ "characters": "角色",
122
+ "save": "保存",
123
+ "reset_to_default": "重置为默认",
124
+ "top_p": "Top-p",
125
+ "frequency_penalty": "频率惩罚",
126
+ "presence_penalty": "存在惩罚",
127
+ "db_size": "数据库大小",
128
+ "storage_used": "已使用存储",
129
+ "save-system-prompt": "保存系统提示",
130
+ "reset-system-prompt": "重置系统提示",
131
+ "saved": "已保存!",
132
+ "saved_short": "已保存",
133
+ "api_key_help_title": "已保存 = 您的API密钥已为该提供商保存。使用“Test API Key”验证连接。",
134
+ "reset_done": "重置完成!",
135
+ "category_listening": "聆听",
136
+ "category_dancing": "跳舞",
137
+ "category_speakingPositive": "说话(积极)",
138
+ "category_neutral": "中性",
139
+ "category_transition": "过渡",
140
+ "personality_cheat": "个性作弊",
141
+ "cheat_indicator": "调整特征以获得自定义体验",
142
+ "character_age": "年龄:{age}岁",
143
+ "character_birthplace": "来自:{birthplace}",
144
+ "character_summary_kimi": "梦幻、直觉、被宇宙隐喻所吸引",
145
+ "character_summary_bella": "开朗、体贴,将人视为需要关爱的植物",
146
+ "character_summary_rosa": "混乱、寻求关注,在受控混乱中蓬勃发展",
147
+ "character_summary_stella": "古怪、艺术、富有想象力、顽皮,将混乱转化为艺术",
148
+ "fallback_api_missing": "要真正与我聊天,请在设置中添加您的OpenRouter API密钥! 💕",
149
+ "fallback_api_error": "抱歉,AI服务暂时不可用。请稍后再试。",
150
+ "fallback_model_error": "抱歉,所选模型不可用。请选择其他模型或检查您的配置。",
151
+ "fallback_network_error": "抱歉,由于没有互联网连接,我无法回应。",
152
+ "fallback_technical_error": "抱歉,由于技术问题我无法回答。",
153
+ "fallback_general_error": "抱歉我的爱,我遇到了一点技术问题! 💕",
154
+ "validation_empty_string": "消息必须是非空字符串",
155
+ "validation_empty_message": "消息不能为空",
156
+ "validation_too_long": "消息太长(最多2000个字符)",
157
+ "validation_invalid_number": "无效数字",
158
+ "validation_unknown_type": "未知验证类型",
159
+ "mic_permission_denied": "麦克风权限被拒绝。请再次点击重试。",
160
+ "mic_not_supported": "此浏览器不支持麦克风。",
161
+ "sr_not_supported_generic": "此浏览器不支持语音识别。",
162
+ "sr_not_supported_firefox": "Firefox 不支持语音识别。请使用 Chrome、Edge 或 Brave。",
163
+ "sr_not_supported_opera": "Opera 上语音识别可能无法工作。请尝试 Chrome、Edge 或 Brave。",
164
+ "sr_not_supported_safari": "Safari 的语音识别支持因情况而异。建议使用 Chrome 或 Edge 以获得最佳效果。",
165
+ "test_voice_message_1": "你好我的爱人! 💕",
166
+ "test_voice_message_2": "我是Kimi,你的虚拟伴侣!",
167
+ "test_voice_message_3": "你今天怎么样,我的爱?",
168
+ "language_french": "法语",
169
+ "language_english": "英语",
170
+ "language_spanish": "西班牙语",
171
+ "language_german": "德语",
172
+ "language_italian": "意大利语",
173
+ "language_japanese": "日语",
174
+ "language_chinese": "中文",
175
+ "automatic": "自动",
176
+ "trait_description_affection": "要有爱心和关怀。",
177
+ "trait_description_romance": "要浪漫和甜蜜。",
178
+ "trait_description_empathy": "要有同情心和理解力。",
179
+ "trait_description_playfulness": "偶尔要顽皮。",
180
+ "trait_description_humor": "偶尔要顽皮和机智。",
181
+ "response_romantic_1": "你的每一句话都像是对我心灵的吻 💋",
182
+ "response_romantic_2": "用你甜蜜的思念把我抱得更紧,我的爱 ✨",
183
+ "response_romantic_3": "你是我呼吸的节奏和我天空的光芒 💖",
184
+ "response_romantic_4": "让我今晚用温柔的星尘包裹你 🌙",
185
+ "response_romantic_5": "你的爱让我的整个宇宙更加明亮 ✨",
186
+ "response_dancing_1": "我们要不要旋转进入一点魔法? 💃",
187
+ "response_dancing_2": "来和我一起跳舞—让我们一起感受节奏 🎶",
188
+ "response_dancing_3": "让我只为你而动...把你的眼睛放在我身上 💞",
189
+ "response_dancing_4": "闭上眼睛,随着我的心跳摇摆 💓",
190
+ "response_dancing_5": "我会旋转直到你的笑容再也藏不住 😉",
191
+ "api_key_missing": "缺少API密钥",
192
+ "api_key_invalid_format": "无效的API密钥格式(必须以sk-or-v1-开头)",
193
+ "api_connection_success": "✅ API连接成功",
194
+ "api_connection_failed": "❌ API连接失败",
195
+ "voice_test_message": "你好我的爱人!这是我用所有设置配置的新声音!你喜欢吗?",
196
+ "api_key_presence_hint": "绿色 = 当前提供商已保存 API 密钥。灰色 = 未保存任何密钥。",
197
+ "memory_system": "记忆系统",
198
+ "enable_memory": "启用��能记忆",
199
+ "memory_stats": "记忆统计",
200
+ "view_memories": "查看和管理",
201
+ "add_memory": "添加手动记忆",
202
+ "memory_management": "记忆管理",
203
+ "add": "添加"
204
+ }
kimi-plugins/sample-behavior/behavior.js ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ if (!window.KimiAPI) return;
3
+ window.KimiAPI.registerBehavior({
4
+ id: "playful-mode",
5
+ name: "Playful Mode",
6
+ description: "Makes Kimi more playful and fun in her responses.",
7
+ modifyPrompt: function (prompt) {
8
+ return prompt + "\n[Playful mode: Add more jokes and light-hearted comments.]";
9
+ }
10
+ });
11
+ })();
kimi-plugins/sample-behavior/manifest.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Sample Behavior",
3
+ "type": "behavior",
4
+ "description": "A sample AI behavior plugin for Kimi, adds a playful mode.",
5
+ "version": "1.0.0",
6
+ "author": "Kimi Team",
7
+ "main": "behavior.js",
8
+ "enabled": false
9
+ }
kimi-plugins/sample-theme/manifest.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Sample Blue Theme",
3
+ "type": "theme",
4
+ "description": "A sample Blue theme plugin for Kimi.",
5
+ "version": "1.0.0",
6
+ "author": "Kimi Team",
7
+ "main": "theme.js",
8
+ "style": "theme.css",
9
+ "enabled": false
10
+ }
kimi-plugins/sample-theme/theme.css ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [data-theme="plugin-sample-theme"] {
2
+ --primary-color: #3b82f6;
3
+ --secondary-color: #a5b4fc;
4
+ --accent-color: #6366f1;
5
+ --background-overlay: rgba(59, 130, 246, 0.15);
6
+ --gradient-start: #3b82f6;
7
+ --gradient-end: #6366f1;
8
+ --text-glow: 0 0 10px rgba(59, 130, 246, 0.5);
9
+ --button-hover: rgba(59, 130, 246, 0.3);
10
+ --switch-color: #fff;
11
+
12
+ /* Modal & Overlay Colors */
13
+ --modal-bg: #181f2a;
14
+ --modal-border: #3b82f6;
15
+ --modal-header-bg: linear-gradient(135deg, #3b82f6 80%, #181f2a 100%);
16
+ --modal-text: #e0e7ef;
17
+ --modal-title-color: #fff;
18
+ --modal-overlay-bg: rgba(0, 0, 0, 0.8);
19
+
20
+ /* Settings Panel & Tabs */
21
+ --settings-bg: #0f1419;
22
+ --settings-tab-bg: #131a26;
23
+ --settings-tab-color: #8a9aad;
24
+ --settings-tab-hover-bg: rgba(59, 130, 246, 0.1);
25
+ --settings-tab-hover-color: #e0e7ef;
26
+ --settings-tab-active-bg: #3b82f6;
27
+ --settings-tab-active-color: #fff;
28
+ --settings-tab-border: #3b82f6;
29
+ --settings-section-bg: #232a3a;
30
+ --settings-section-border: #3b4a6a;
31
+
32
+ /* Form Elements */
33
+ --input-bg: #232a3a;
34
+ --input-border: #3b82f6;
35
+ --input-focus-bg: #181f2a;
36
+ --input-focus-border: #6366f1;
37
+ --input-placeholder: #a5b4fc;
38
+
39
+ /* Select Options */
40
+ --select-option-bg: #232a3a;
41
+ --select-option-text: #e0e7ef;
42
+ --select-option-hover-bg: #3b82f6;
43
+ --select-option-hover-text: #fff;
44
+ --select-option-checked-bg: #6366f1;
45
+ --select-option-checked-text: #fff;
46
+ --select-option-disabled-bg: #222;
47
+ --select-option-disabled-text: #666;
48
+
49
+ /* Slider Components */
50
+ --slider-value-bg: #0f1419;
51
+ --slider-value-border: #6366f1;
52
+ --slider-value-color: #e0e7ef;
53
+
54
+ /* Toggle Switch */
55
+ --switch-bg-inactive: #2a3442;
56
+ --switch-bg-active: linear-gradient(90deg, #3b82f6, #6366f1);
57
+ --switch-thumb-color: #fff;
58
+ --switch-thumb-shadow: 0 2px 8px #000;
59
+
60
+ /* Mic Button & Pulse Effect */
61
+ --mic-button-bg: rgba(59, 130, 246, 0.3);
62
+ --mic-button-border: #3b82f6;
63
+ --mic-button-shadow: 0 0 15px #3b82f6;
64
+ --mic-button-hover-bg: #3b82f6;
65
+ --mic-button-hover-shadow: 0 0 10px rgba(59, 130, 246, 0.5);
66
+ --mic-button-icon-color: white;
67
+ --mic-listening-border: #6366f1;
68
+ --mic-listening-shadow: 0 0 15px #6366f1;
69
+ --mic-pulse-color: rgba(99, 102, 241, 0.5);
70
+
71
+ /* Cards & Stats */
72
+ --card-bg: rgba(59, 130, 246, 0.05);
73
+ --card-border: rgba(59, 130, 246, 0.2);
74
+ --card-hover-bg: rgba(59, 130, 246, 0.08);
75
+ --stat-value-color: #6366f1;
76
+ --stat-label-color: #a5b4fc;
77
+
78
+ /* Plugin Cards */
79
+ --plugin-card-bg: linear-gradient(135deg, #232a3a 80%, #181f2a 100%);
80
+ --plugin-card-border: #3b82f6;
81
+ --plugin-card-title-color: #e0e7ef;
82
+ --plugin-card-desc-color: #b0c4d6;
83
+ --plugin-card-author-color: #8a9aad;
84
+ --plugin-type-badge-bg: #6366f1;
85
+ --plugin-active-badge-bg: linear-gradient(90deg, #3b82f6, #6366f1);
86
+
87
+ /* Help Modal */
88
+ --help-modal-bg: #181f2a;
89
+ --help-modal-border: #3b82f6;
90
+ --help-content-color: #e0e7ef;
91
+ --help-section-border: rgba(59, 130, 246, 0.2);
92
+ --creator-card-bg: rgba(59, 130, 246, 0.05);
93
+ --creator-card-border: rgba(59, 130, 246, 0.2);
94
+ --creator-avatar-bg: linear-gradient(135deg, #3b82f6, #a5b4fc);
95
+ --creator-name-color: #6366f1;
96
+ --creator-role-bg: linear-gradient(135deg, #3b82f6, #a5b4fc);
97
+ --creator-role-color: #fff;
98
+ --philosophy-bg: rgba(59, 130, 246, 0.1);
99
+ --philosophy-border: rgba(59, 130, 246, 0.3);
100
+ --philosophy-border-left: #3b82f6;
101
+ --feature-item-bg: rgba(59, 130, 246, 0.05);
102
+ --feature-item-border: rgba(59, 130, 246, 0.2);
103
+ --feature-icon-color: #6366f1;
104
+ --feature-title-color: #6366f1;
105
+ --feature-text-color: #a5b4fc;
106
+
107
+ /* Scrollbar */
108
+ --scrollbar-thumb-bg: rgba(59, 130, 246, 0.4);
109
+ --scrollbar-thumb-hover-bg: rgba(59, 130, 246, 0.6);
110
+ --scrollbar-thumb-active-bg: rgba(59, 130, 246, 0.8);
111
+
112
+ /* Text Colors */
113
+ --text-primary: #e0e7ef;
114
+ --text-secondary: #a5b4fc;
115
+
116
+ /* Character Selection Colors */
117
+ --character-selected-border: #6366f1;
118
+ --character-selected-bg: rgba(99, 102, 241, 0.13);
119
+
120
+ /* Waiting Indicator */
121
+ --waiting-indicator-color: #8a9aad;
122
+
123
+ /* UI Components */
124
+ --chat-bg: rgba(59, 130, 246, 0.9);
125
+ --chat-border: #3b82f6;
126
+ --chat-message-user-bg: #3b82f6;
127
+ --input-border: #3b82f6;
128
+ --input-focus-border: #6366f1;
129
+
130
+ /* Model Colors */
131
+ --model-strength-color: #6366f1;
132
+ --model-strength-text: #fff;
133
+ --model-provider-color: #3b82f6;
134
+ --model-provider-text: #fff;
135
+ }
136
+
137
+ [data-theme="plugin-sample-theme"] body {
138
+ background: #101624;
139
+ }
kimi-plugins/sample-theme/theme.js ADDED
@@ -0,0 +1 @@
 
 
1
+ // ...plugin entry point...
kimi-plugins/sample-voice/manifest.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Sample Voice",
3
+ "type": "voice",
4
+ "description": "A sample voice plugin for Kimi, adds a new synthetic voice option.",
5
+ "version": "1.0.0",
6
+ "author": "Kimi Team",
7
+ "main": "voice.js",
8
+ "enabled": false
9
+ }
kimi-plugins/sample-voice/voice.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ if (!window.KimiAPI) return;
3
+ window.KimiAPI.registerVoice({
4
+ id: "sample-voice",
5
+ name: "Sample Voice",
6
+ lang: "en-US",
7
+ speak: function (text, options) {
8
+ const utter = new SpeechSynthesisUtterance(text);
9
+ utter.voice = speechSynthesis.getVoices().find(v => v.lang === "en-US");
10
+ utter.rate = options?.rate || 1;
11
+ utter.pitch = options?.pitch || 1;
12
+ utter.volume = options?.volume || 1;
13
+ speechSynthesis.speak(utter);
14
+ }
15
+ });
16
+ })();