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