diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..0823d299872b242cc23d011b44b0f405b94a79af 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,13 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +kimi-icons/2blanche.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/bella.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/jasmine.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/july.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/kimi-loading.png filter=lfs diff=lfs merge=lfs -text +kimi-icons/kimi.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/rosa.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/stella.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/virtual-kimi-banners.jpg filter=lfs diff=lfs merge=lfs -text +kimi-icons/virtualkimi-logo.png filter=lfs diff=lfs merge=lfs -text diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..0c084fd6ca94d9f61cfa794d0292c141feaddbe2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,487 @@ +# Virtual Kimi App Changelog + +# [1.1.7.1] - 2025-11-12 (HuggingFace and Github version) + +### Added + +- **New Characters**: Introduced 2 new AI personalities: + - **2Blanche**: Stoic YoRHa android combat unit with deep emotional vulnerability hidden beneath military protocols + - **Jasmine**: Divine goddess of love, sensuality, and Kamasutra; inspires passion, intimacy, and pleasure in every encounter +- **Character-Specific Emotions**: Added ANDROID, SENSUAL, and LOVE emotion types with specialized responses +- **Enhanced Emotion System**: New contextual keywords and emotional responses for android and sensual personas +- **Complete Internationalization**: Full translations for both new characters across all 8 supported languages (English, French, Spanish, German, Italian, Portuguese, Japanese, Chinese) + +### Changed + +- **Total Characters**: Expanded from 4 to 6 unique AI personalities +- **Emotion Mapping**: New emotions mapped to existing video categories for seamless integration +- **Character Variety**: Enhanced personality spectrum from cosmic/nurturing/chaotic/artistic to include technological/rebellious themes + +### Technical Details + +- Added comprehensive character profiles with unique trait progressions +- 2Blanche: Ultra-difficult progression (35% affection start) with loyalty bonuses +- July: Trust-based progression (45% affection start) with rebellion bonuses +- Maintained compatibility with existing video structure (no new categories required) + +# [1.1.6.1] - 2025-09-05 + +### Changed + +- Improved text formatting in the chat window. + +### Bug Fixes + +- Fixed some issues. + +# [1.1.5.1] - 2025-09-04 + +### Bug Fixes + +- Fixed a bug where sliders refused the value 0 (0 was treated as falsy and reset to defaults). + +# [1.1.5] - 2025-09-03 + +### Bug Fixes + +- Fixed some issues. + +### Changed + +- Separated the KimiDataManager class and moved logic into the new file `kimi-data-manager.js`. + +# [1.1.4.1] - 2025-09-03 + +### Bug Fixes + +- Fixed an issue with language selection and speech recognition that could prevent correct voice detection and audio input. Improved handling and fallbacks to ensure consistent behavior. + +### Changed + +- Separated the KimiVideoManager class and moved video management logic into the new file `kimi-videos.js`. + +# [1.1.3] - 2025-09-01 + +### Bug Fixes + +- Fixed an issue with language selection and speech recognition / text-to-speech playback that could prevent correct voice detection and audio output across browsers. Improved normalization and fallback handling. + +### Changed + +- Modified the calculations used for character personality trait processing to improve accuracy and consistency across modules. + +# [1.1.2] - 2025-08-30 + +### Improvements + +- Improved memory and prompt generation to avoid duplicate memory sections and display accurate per-character counters. + +### Added + +- A concise "7-day summary" feature that extracts high-signal conversation highlights for quick reference. + +### Notes + +- Voice UI and TTS: Only Microsoft Edge and Google Chrome will display the voice selection list and support voice playback of messages; other browsers may not expose compatible voices. + +### Bug Fixes + +- Fixed import/export functions for preferences and data to ensure exported files can be re-imported correctly. + +- Fixed some small bugs related to memory, video playback, and preference import/export. + +# [1.1.1] - 2025-08-29 + +### Improvements + +- Microsoft Edge and Google Chrome Only : Improved language and voice selection logic: normalization, fallback, and robust preference management across all modules. +- Enhanced voice compatibility and ensured consistent language handling. + +### Bug Fixes + +- Fixed issue where videos could freeze after opening or closing the memory modal or changing memory sections. +- Added automatic reset to neutral video state after UI interactions to prevent stuck/frozen videos. + +# [1.1.0] - 2025-08-28 + +### Changed + +- **Recommended LLMs**: Updated the list of recommended LLM models to reflect current recommendations and improvements. + +- **Settings modal UI/UX**: Updated tab layout and visual behavior in the settings modal for clearer navigation and improved usability. + +### Fixed + +- **Memory features UX**: Fixed multiple UI/UX issues in the memory system to ensure reliable capture, display, and management of remembered items. +- **Miscellaneous bug fixes**: Corrected various small bugs across the application. + +### Internationalization + +- **Interface translations**: Added new strings and translation keys to support the updated UI elements. + +# [1.0.9] - 2025-08-23 + +### Major System Improvements + +- **Personality trait system overhaul**: Rebalanced progression curves and multipliers for more natural character development. +- **Unified emotion system**: Centralized emotion-to-video mapping and fixed all 13 emotions to properly affect traits. +- **Intelligence trait integration**: Added intelligence to personality calculations and video selection algorithms. +- **Enhanced emotion detection**: Improved keyword detection with better priorities and reduced conflicts. +- **Video selection rebalancing**: Fixed positive/negative bias and made auto-triggers more accessible. +- **Complete codebase synchronization**: Eliminated inconsistencies and redundancies across all modules. +- **Text streaming implementation**: Added real-time text streaming in chat for better user experience. + +### Language & Voice Improvements + +- **Enhanced language and voice selection**: Fixed bugs and inconsistencies in language switching and voice preferences. +- **Improved voice synchronization**: Better coordination between selected language and available voice options. + +### API Key Management Enhancements + +- **Provider-specific API key storage**: Implemented separate storage for different LLM providers (OpenRouter, OpenAI, Groq, etc.). +- **Unified API key handling**: Consolidated all API key operations through a centralized utility system. +- **Enhanced settings UI**: Improved visual design and layout of API configuration section. +- **Comprehensive API audit**: Fixed inconsistencies across all chat, test, and model loading functions. + +### Bug Fixes + +- Fixed trait calculation inconsistencies between modules (INTELLIGENCE and others). +- Resolved emotion detection conflicts (LISTENING, ROMANTIC/KISS categories). +- Corrected fallback values causing progression issues. +- Fixed API key loading and display issues in settings modal. + +# [1.0.8] - 2025-08-19 + +### Changed + +- Improved fallback logic for LLM responses: now uses localized emotional responses if the LLM reply is empty or invalid. +- Made emotional response selection dynamic and robust, based on available variants. +- Enhanced error handling for missing API keys, network issues, and API errors, ensuring the user always receives a meaningful message. +- Refactored code patching to avoid accidental code removal or misplaced edits. +- Clarified and documented emotional response logic for maintainability. + +## [1.0.7] - 2025-08-19 + +### Changed + +- Removed the global system prompt that caused issues and implemented per-character system prompts for each character. +- Improved voice reading of messages for clearer and more natural audio playback. +- Fixed various small bugs related to characters' personality traits. +- Improved detection of words and phrases for memory recording to increase accuracy. + +## [1.0.6] - 2025-08-15 + +### Added + +- Added 100+ videos for various contexts. + +### Changed + +- Optimized video preloading to improve speed on slow web servers. + +### Fixed + +- Fixed various minor bugs. + +## [1.0.5] - 2025-08-13 - "Personality & Language Sensitivity" + +### Added + +- Multilingual profanity/insult detection for negative context across 7 languages (en, fr, es, de, it, ja, zh) +- Gendered variants support in negative keywords (fr, es, it, de) to improve accuracy (e.g., sérieux/sérieuse) +- Extended personality keywords for Spanish and Italian (all traits) with gendered forms + +### Changed + +- Personality sync now completes missing values using character-specific defaults (with generic fallback) +- Centralized side-effects on personality updates (UI/memory/video/voice) behind a single `personality:updated` listener +- Sliders: generic handler only updates display; persistence and effects handled by specialized listeners +- Trait updates preserve fractional progress (2 decimals) for smoother affection changes +- Stats now use character-specific default for affection (with generic fallback) when missing + +### Fixed + +- Removed obsolete `personalityUpdated` listener to avoid duplicate processing +- Unified KimiMemory affection default loading (removed conflicting double assignment and legacy default 80) +- Minor cleanup and consistency improvements in utils and sync flows + +## [1.0.4] - 2025-08-09 - "Emotion & Context Logic Upgrade" + +### Added + +- Major improvements to emotion, context, and personality logic: + - Enhanced emotion detection and mapping for more nuanced responses + - Contextual keyword analysis for better understanding of user intent + - Refined personality trait system with dynamic adaptation + - Video selection logic now adapts to both emotion and conversational context + - Improved handling of multi-layered context (emotion, keywords, personality, situation) + +### Changed + +- Video playback and character reactions are now more tightly coupled to detected context and personality traits +- Emotion and context logic refactored for clarity and maintainability +- Keyword extraction and context matching algorithms improved for accuracy + +### Technical + +- Refactored core logic in `kimi-emotion-system.js`, `kimi-logic.js`, and `kimi-memory-system.js` +- Updated video selection and playback logic in `kimi-memory.js` and `kimi-memory-ui.js` +- Improved context propagation between modules + +## [1.0.3] - 2025-08-09 - "LLM multi-provider" + +### Added + +- LLM multi-provider UX enhancements: + - Dynamic API key label per provider (OpenRouter, OpenAI, Groq, Together, DeepSeek, Custom, Ollama) + - Visual "Saved" badge when a key is stored or after a successful test + - Localized tooltip explaining Saved vs connection test + +### Changed + +- OpenAI-compatible flow now reads llmBaseUrl/llmModelId and the correct provider key from KimiDB +- Clears connection status message when provider/Base URL/Model ID/key changes for clearer feedback + +## [1.0.2] - 2025-08-09 - "Smoother Video" + +### Changed + +- Video playback and transition stability improvements: + - Lightweight MP4 prefetch queue (neutral + likely next clips) to reduce wait times during switches + - Earlier transition on `canplay` (instead of `canplaythrough`) for faster, smoother swaps + - Context-aware throttling to prevent rapid switching under load (speaking: ~200ms, listening: ~250ms, dancing: ~600ms, neutral: ~1200ms) + +### Fixed + +- Safe revert on failed `play()` during a switch to avoid frozen frames +- Aligned event listeners to `canplay` and ensured proper cleanup to prevent leaks +- Corrected prefetch cache initialization order (prevented `undefined.has` runtime error) +- Removed unsupported `` to eliminate console warnings + +### Technical + +- Front-end performance tweaks: GPU-accelerated fades with `will-change: opacity` and `backface-visibility: hidden` +- Connection warm-up: added `preconnect`/`dns-prefetch` to the origin for faster first video start +- Files updated: `index.html`, `kimi-css/kimi-style.css`, `kimi-js/kimi-utils.js` + +## [1.0.1] - 2025-08-08 + +- Fixed an issue where the browser prompted to save the OpenRouter API key as a password. The input field is now properly configured to prevent password managers from interfering. +- Added a waiting animation that appears between the user's message submission and the LLM's response, improving user feedback during processing. +- Added a new section in the API tab: below the recommended LLM models, all available OpenRouter LLM models are now dynamically loaded and displayed for selection. + +## [1.0.0] - 2025-08-07 - "Unified" + +### Added + +- **Intelligent Memory System**: Automatic extraction and categorization of memories from conversations +- **Multiple AI Characters**: 4 unique personalities (Kimi, Bella, Rosa, Stella) with distinct traits +- **Advanced Emotion Detection**: Real-time emotion analysis with cultural awareness +- **Plugin System**: Extensible architecture for themes, voices, and behaviors +- **Memory Management UI**: Complete interface for viewing, searching, and managing memories +- **Enhanced Personality System**: 6 dynamic traits that evolve based on interactions +- **Multilingual Support**: Full localization in 7 languages with auto-detection +- **Production Health Check**: Comprehensive system validation and monitoring +- **Performance Optimizations**: Batch database operations and improved loading times +- **Security Enhancements**: Input validation, sanitization, and secure API handling + +### Changed + +- **Unified Architecture**: Consolidated all emotion and personality systems +- **Improved Database**: Enhanced IndexedDB implementation with batch operations +- **Better Error Handling**: Centralized error management with fallback responses +- **Enhanced UI/UX**: More responsive and accessible interface design +- **Optimized Video System**: Smoother transitions and better emotion mapping + +### Fixed + +- Function export issues in module system +- Memory leaks in event listeners +- Cross-browser compatibility issues +- Voice recognition stability problems +- Database initialization race conditions + +### Technical + +- Migrated to unified emotion system +- Implemented comprehensive validation layer +- Added automated health monitoring +- Enhanced plugin security validation +- Improved mobile responsiveness + +## [0.0.9] - 2025-08-04 - "Enhanced" + +### Added + +- Advanced LLM model selection interface +- Improved voice synthesis with better emotion mapping +- Enhanced personality trait visualization +- Better conversation export/import functionality + +### Changed + +- Upgraded database schema for better performance +- Improved theme system with more customization options +- Enhanced mobile interface responsiveness + +### Fixed + +- Various browser compatibility issues +- Voice recognition accuracy improvements +- Memory management optimizations + +## [0.0.8] - 2025-08-01 - "Evolution" + +### Added + +- Dynamic personality trait evolution +- Enhanced emotion detection algorithms +- Improved conversation context awareness +- Better visual feedback systems + +### Changed + +- Redesigned settings interface +- Improved conversation flow management +- Enhanced error reporting system + +### Fixed + +- Database sync issues +- Voice recognition edge cases +- Theme switching problems + +## [0.0.7] - 2025-07-29 - "Immersion" + +### Added + +- Real-time video emotion responses +- Enhanced voice interaction capabilities +- Improved conversation context retention +- Better visual theme system + +### Changed + +- Upgraded UI framework for better performance +- Improved data synchronization mechanisms +- Enhanced accessibility features + +### Fixed + +- Various stability improvements +- Better error handling +- Improved cross-platform compatibility + +## [0.0.6] - 2025-07-26 - "Connection" + +### Added + +- Multi-language support system +- Enhanced conversation memory +- Improved personality customization +- Better audio/video synchronization + +### Changed + +- Redesigned conversation interface +- Improved data persistence layer +- Enhanced user experience flows + +### Fixed + +- Memory leak issues +- Browser compatibility problems +- Audio synchronization bugs + +## [0.0.5] - 2025-07-23 - "Rebirth" + +### Added + +- Complete application rewrite +- Modern ES6+ JavaScript architecture +- Responsive design system +- Advanced AI integration capabilities +- Comprehensive settings system + +### Changed + +- Modernized codebase with current web standards +- Improved performance and reliability +- Enhanced user interface design +- Better data management system + +### Removed + +- Legacy jQuery dependencies +- Outdated browser support + +## [0.0.4] - 2025-07-20 - "Stability" + +### Added + +- Enhanced voice recognition +- Improved conversation flow +- Better error handling +- Enhanced visual feedback + +### Fixed + +- Various stability issues +- Performance optimizations +- Browser compatibility improvements + +## [0.0.3] - 2025-07-18 - "Polish" + +### Added + +- Improved user interface +- Better conversation management +- Enhanced customization options + +### Fixed + +- Various bugs and stability issues +- Performance improvements + +## [0.0.2] - 2025-07-17 - "Improvements" + +### Added + +- Basic conversation memory +- Improved personality system +- Enhanced visual themes + +### Fixed + +- Initial bug fixes +- Performance optimizations + +## [0.0.1] - 2025-07-16 - "Genesis" + +### Added + +- Initial release +- Basic AI conversation capabilities +- Voice recognition and synthesis +- Simple personality system +- Theme customization +- Local data storage + +--- + +## Legend + +- **Added**: New features +- **Changed**: Changes in existing functionality +- **Deprecated**: Soon-to-be removed features +- **Removed**: Removed features +- **Fixed**: Bug fixes +- **Security**: Security improvements +- **Technical**: Internal technical changes + +--- + +All notable changes to Virtual Kimi will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..eb3794da98f80d3d75588f91f4d88f7f64107c84 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,364 @@ +# Contributing to Virtual Kimi + +Thank you for your interest in contributing to Virtual Kimi! This document provides guidelines and information for contributors. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Contribution Guidelines](#contribution-guidelines) +- [Project Structure](#project-structure) +- [Coding Standards](#coding-standards) +- [Testing](#testing) +- [Pull Request Process](#pull-request-process) +- [Issue Reporting](#issue-reporting) + +## Code of Conduct + +We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and constructive in all interactions. + +### Expected Behavior + +- Use welcoming and inclusive language +- Be respectful of differing viewpoints and experiences +- Gracefully accept constructive criticism +- Focus on what is best for the community +- Show empathy towards other community members + +## Getting Started + +### Prerequisites + +- Modern web browser (Chrome, Edge, Firefox recommended) +- Basic knowledge of JavaScript, HTML, and CSS +- Git for version control +- Text editor or IDE of your choice + +### First Contribution + +1. Fork the repository +2. Clone your fork locally +3. Create a new branch for your feature/fix +4. Make your changes +5. Test thoroughly +6. Submit a pull request + +## Development Setup + +### Local Environment + +```bash +# Clone the repository +git clone https://github.com/virtualkimi/virtual-kimi.git +cd virtual-kimi + +# Open in browser +# Option 1: Direct file access +open index.html + +# Option 2: Local server (recommended) +python -m http.server 8000 +# Navigate to http://localhost:8000 +``` + +### Development Tools + +- **Browser DevTools**: For debugging and testing +- **Live Server**: For hot reload during development +- **Lighthouse**: For performance auditing +- **Accessibility tools**: For ensuring inclusive design + +## Contribution Guidelines + +### Types of Contributions + +- **Bug fixes**: Resolve existing issues +- **Feature additions**: New functionality +- **Performance improvements**: Optimization and efficiency +- **Documentation**: Improve guides and comments +- **Localization**: Translation and internationalization +- **Plugin development**: Extend functionality +- **Testing**: Add or improve test coverage + +### Before You Start + +1. Check existing issues and pull requests +2. Open an issue to discuss major changes +3. Ensure your idea aligns with the project goals +4. Consider the impact on existing functionality + +## Project Structure + +### Core Files + +``` +├── index.html # Main application +├── kimi-script.js # Primary initialization +├── kimi-database.js # Data persistence +├── kimi-llm-manager.js # AI integration +├── kimi-emotion-system.js # Emotion analysis +├── kimi-memory-system.js # Memory management +├── kimi-voices.js # Speech synthesis +├── kimi-appearance.js # Theme management +└── kimi-utils.js # Utility functions +``` + +### Module Dependencies + +- **Core System**: Database → Security → Config +- **AI System**: LLM Manager → Emotion System → Memory System +- **UI System**: Appearance → Utils → Module functions +- **Localization**: i18n → All user-facing modules + +### Adding New Features + +#### New Memory Categories + +```javascript +// In kimi-memory-system.js +const newCategory = { + name: "custom_category", + icon: "fas fa-custom-icon", + keywords: ["keyword1", "keyword2"], + confidence: 0.7 +}; + +// Add to MEMORY_CATEGORIES constant +``` + +#### New Themes + +```javascript +// Create plugin in kimi-plugins/custom-theme/ +// manifest.json +{ + "name": "Custom Theme", + "version": "1.0.0", + "type": "theme", + "style": "theme.css", + "enabled": true +} +``` + +#### New AI Models + +```javascript +// In kimi-llm-manager.js +"custom/model-id": { + name: "Custom Model", + provider: "Custom Provider", + type: "openrouter", + contextWindow: 8000, + pricing: { input: 0.1, output: 0.2 }, + strengths: ["Custom", "Feature"] +} +``` + +## Coding Standards + +### JavaScript Style + +- Use ES6+ features and modern syntax +- Prefer `const` and `let` over `var` +- Use meaningful variable and function names in English +- Follow camelCase for variables and functions +- Use PascalCase for classes and constructors + +### Code Organization + +- Keep functions focused and single-purpose +- Use async/await for asynchronous operations +- Handle errors gracefully with try/catch blocks +- Add JSDoc comments for complex functions +- Group related functionality in modules + +### Example Code Style + +```javascript +/** + * Analyzes user input for emotional content and updates personality traits + * @param {string} text - User input text + * @param {string} emotion - Detected emotion type + * @returns {Promise} Updated personality traits + */ +async function updatePersonalityFromEmotion(text, emotion) { + try { + // Validate input + if (!text || typeof text !== "string") { + throw new Error("Invalid input text"); + } + + // Process emotion + const traits = await this.processEmotionalContent(text, emotion); + + // Update database + await this.db.setPersonalityBatch(traits); + + return traits; + } catch (error) { + console.error("Error updating personality:", error); + throw error; + } +} +``` + +### CSS Guidelines + +- Use CSS custom properties (variables) for theming +- Follow BEM methodology for class naming +- Ensure responsive design principles +- Maintain accessibility standards +- Use semantic HTML elements + +### HTML Standards + +- Use semantic HTML5 elements +- Include proper ARIA labels for accessibility +- Ensure proper heading hierarchy +- Add meaningful alt text for images +- Validate markup regularly + +## Testing + +### Manual Testing Checklist + +- [ ] Application loads without errors +- [ ] All core features function correctly +- [ ] Voice recognition works (in supported browsers) +- [ ] Memory system stores and retrieves data +- [ ] Theme switching works properly +- [ ] Responsive design on mobile devices +- [ ] Cross-browser compatibility +- [ ] Accessibility with keyboard navigation + +### Browser Testing + +Test in the following browsers: + +- Chrome (latest 2 versions) +- Edge (latest 2 versions) +- Firefox (latest 2 versions) +- Safari (latest version, if possible) + +### Performance Testing + +- Check loading times +- Monitor memory usage +- Test with large conversation histories +- Verify smooth animations +- Ensure responsive UI interactions + +## Pull Request Process + +### Before Submitting + +1. **Test thoroughly**: Ensure your changes work as expected +2. **Check compatibility**: Test across different browsers +3. **Update documentation**: Modify README.md if needed +4. **Clean up code**: Remove debugging code and comments +5. **Commit messages**: Use clear, descriptive commit messages + +### PR Template + +```markdown +## Description + +Brief description of changes made. + +## Type of Change + +- [ ] Bug fix +- [ ] New feature +- [ ] Performance improvement +- [ ] Documentation update +- [ ] Other: **\_** + +## Testing + +- [ ] Tested in Chrome +- [ ] Tested in Edge +- [ ] Tested in Firefox +- [ ] Tested on mobile +- [ ] No errors in console + +## Screenshots (if applicable) + +Add screenshots of UI changes. + +## Additional Notes + +Any additional context or considerations. +``` + +### Review Process + +1. Maintainers review code for quality and functionality +2. Feedback provided through PR comments +3. Make requested changes and push updates +4. Final approval and merge + +## Issue Reporting + +### Bug Reports + +Include the following information: + +- Browser and version +- Operating system +- Steps to reproduce +- Expected behavior +- Actual behavior +- Console errors (if any) +- Screenshots (if applicable) + +### Feature Requests + +- Clear description of the feature +- Use case and benefits +- Possible implementation approach +- Any relevant examples or mockups + +### Issue Labels + +- `bug`: Something isn't working +- `enhancement`: New feature or improvement +- `documentation`: Documentation updates +- `good first issue`: Good for newcomers +- `help wanted`: Community assistance needed +- `plugin`: Related to plugin system +- `accessibility`: Accessibility improvements + +## Development Tips + +### Performance Optimization + +- Minimize DOM manipulations +- Use event delegation for dynamic content +- Implement proper cleanup for event listeners +- Optimize database queries with batch operations + +### Accessibility + +- Test with keyboard navigation +- Verify screen reader compatibility +- Ensure sufficient color contrast +- Add appropriate ARIA labels + +## Community + +### Getting Help + +- Open an issue for technical questions +- Check existing documentation first +- Be specific about your problem or question + +### Communication + +- Be respectful and professional +- Provide context and details +- Be patient with response times +- Help others when possible + +Thank you for contributing to Virtual Kimi! Your efforts help create a better AI companion experience for everyone. + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..58495fae923a615228e585db7b21a246422095db --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,23 @@ +# Virtual Kimi Custom License + +Copyright (c) 2025 Virtual Kimi Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to use, +copy, modify, and distribute the Software for personal, educational, or research purposes, +subject to the following conditions: + +- **Commercial use, resale, or monetization of this application or any derivative work is strictly prohibited without the explicit written consent of the author.** +- The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +- You may not use the name, logo, or branding of Virtual Kimi for commercial purposes without explicit permission. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For commercial licensing inquiries, please contact: [ijohn@virtualkimi.com](ijohn@virtualkimi.com) +[WebSite: https:/virtual-kimi.com](https:/virtual-kimi.com) diff --git a/Launch-local-kimi-app.bat b/Launch-local-kimi-app.bat new file mode 100644 index 0000000000000000000000000000000000000000..c1af2d8d331f6b5f1e86a724415135a639670c1c --- /dev/null +++ b/Launch-local-kimi-app.bat @@ -0,0 +1,9 @@ +@echo off +REM Starts a Python HTTP server on port 8080 +start "" python -m http.server 8080 + +REM Pause 2 seconds to allow the server to start +timeout /t 2 >nul + +REM Opens the homepage in the default browser +start "" http://localhost:8080/index.html \ No newline at end of file diff --git a/dexie-js/dexie.min.js b/dexie-js/dexie.min.js new file mode 100644 index 0000000000000000000000000000000000000000..ec21c0bda2086d0e2592473b4a60526fe7b141ed --- /dev/null +++ b/dexie-js/dexie.min.js @@ -0,0 +1,2 @@ +(function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Dexie=t()})(this,function(){"use strict";var s=function(e,t){return(s=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var _=function(){return(_=Object.assign||function(e){for(var t,n=1,r=arguments.length;n.",Ze="String expected.",et=[],tt="__dbnames",nt="readonly",rt="readwrite";function it(e,t){return e?t?function(){return e.apply(this,arguments)&&t.apply(this,arguments)}:e:t}var ot={type:3,lower:-1/0,lowerOpen:!1,upper:[[]],upperOpen:!1};function at(t){return"string"!=typeof t||/\./.test(t)?function(e){return e}:function(e){return void 0===e[t]&&t in e&&delete(e=S(e))[t],e}}function ut(){throw Y.Type()}function st(e,t){try{var n=ct(e),r=ct(t);if(n!==r)return"Array"===n?1:"Array"===r?-1:"binary"===n?1:"binary"===r?-1:"string"===n?1:"string"===r?-1:"Date"===n?1:"Date"!==r?NaN:-1;switch(n){case"number":case"Date":case"string":return ts+c&&f(s+b)})})}var h=yt(r)&&r.limit===1/0&&("function"!=typeof w||w===Kt)&&{index:r.index,range:r.range};return f(0).then(function(){if(0=c})).length?(e.forEach(function(u){t.push(function(){var t=h,e=u._cfg.dbschema;fn(s,t,f),fn(s,e,f),h=s._dbSchema=e;var n=an(t,e);n.add.forEach(function(e){un(f,e[0],e[1].primKey,e[1].indexes)}),n.change.forEach(function(e){if(e.recreate)throw new Y.Upgrade("Not yet support for changing primary key");var t=f.objectStore(e.name);e.add.forEach(function(e){return cn(t,e)}),e.change.forEach(function(e){t.deleteIndex(e.name),cn(t,e)}),e.del.forEach(function(e){return t.deleteIndex(e)})});var r=u._cfg.contentUpgrade;if(r&&u._cfg.version>c){Zt(s,f),l._memoizedTables={};var i=g(e);n.del.forEach(function(e){i[e]=t[e]}),tn(s,[s.Transaction.prototype]),en(s,[s.Transaction.prototype],x(i),i),l.schema=i;var o,a=B(r);a&&Le();n=_e.follow(function(){var e;(o=r(l))&&a&&(e=Ue.bind(null,null),o.then(e,e))});return o&&"function"==typeof o.then?_e.resolve(o):n.then(function(){return o})}}),t.push(function(e){var t,n,r=u._cfg.dbschema;t=r,n=e,[].slice.call(n.db.objectStoreNames).forEach(function(e){return null==t[e]&&n.db.deleteObjectStore(e)}),tn(s,[s.Transaction.prototype]),en(s,[s.Transaction.prototype],s._storeNames,s._dbSchema),l.schema=s._dbSchema}),t.push(function(e){s.idbdb.objectStoreNames.contains("$meta")&&(Math.ceil(s.idbdb.version/10)===u._cfg.version?(s.idbdb.deleteObjectStore("$meta"),delete s._dbSchema.$meta,s._storeNames=s._storeNames.filter(function(e){return"$meta"!==e})):e.objectStore("$meta").put(u._cfg.version,"version"))})}),function e(){return t.length?_e.resolve(t.shift()(l.idbtrans)).then(e):_e.resolve()}().then(function(){sn(h,f)})):_e.resolve();var s,c,l,f,t,h}).catch(u)):(x(o).forEach(function(e){un(i,e,o[e].primKey,o[e].indexes)}),Zt(n,i),void _e.follow(function(){return n.on.populate.fire(a)}).catch(u));var e,t})}function on(e,r){sn(e._dbSchema,r),r.db.version%10!=0||r.objectStoreNames.contains("$meta")||r.db.createObjectStore("$meta").add(Math.ceil(r.db.version/10-1),"version");var t=ln(0,e.idbdb,r);fn(e,e._dbSchema,r);for(var n=0,i=an(t,e._dbSchema).change;nMath.pow(2,62)?0:e.oldVersion,m=e<1,f.idbdb=l.result,p&&on(f,v),rn(f,e/10,v,n))},n),l.onsuccess=qe(function(){v=null;var e,t,n,r,i,o=f.idbdb=l.result,a=b(o.objectStoreNames);if(0d.limit?t.length=d.limit:e.length===d.limit&&t.length=r.limit&&(!r.values||e.req.values)&&Xn(e.req.query.range,r.query.range)}),!1,i,o];case"count":a=o.find(function(e){return Gn(e.req.query.range,r.query.range)});return[a,!!a,i,o]}}(O,c,"query",t),a=o[0],e=o[1],u=o[2],s=o[3];return a&&e?a.obsSet=t.obsSet:(e=l.query(t).then(function(e){var t=e.result;if(a&&(a.res=t),i){for(var n=0,r=t.length;n + + + + + + Kimi - Virtual Companion 💕 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Loading Kimi... +
+ +
+ + +
+ +
+ +
+ +
+
+

+
+ + +
+
+

Chat with Kimi

+
+ + +
+
+
+ +
+ + +
+
+ +
+
+ + + + +
+
+ +
+
+ 50% +
+
+
+
+ + +
+
+
+

+ + Kimi Configuration +

+
+ + +
+
+ +
+ + + + + + +
+ +
+
+
+

Voice Settings

+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + 1.1 +
+
+
+ +
+ +
+
+ + 1.1 +
+
+
+ +
+ +
+
+ + 0.8 +
+
+
+ +
+
+ +
+
+

Characters

+
+
+ +
+
+ +
+

+ + Personality Traits + +

+
Adjust traits for + a custom experience
+
+
+ +
+
+ + 65 +
+
+
+ +
+ +
+
+ + 55 +
+
+
+ +
+ +
+
+ + 70 +
+
+
+ +
+ +
+
+ + 75 +
+
+
+ +
+ +
+
+ + 60 +
+
+
+ +
+ +
+
+ + 50 +
+
+
+
+
+
+ +
+
+

API Configuration

+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + + +
+
+ +
+
+
+ +
+ +
+
+ + + +
+
+
+
+ +
+

Available Models

+
+
+ +
+

Advanced Settings +

+ +
+ +
+
+ + 0.9 +
+
+
+
+ + Controls randomness and + creativity + (default: 0.9). Higher values make output more creative but less focused. +
+ +
+ +
+
+ + 400 +
+
+
+
+ + Maximum response length in tokens + (default: 400). Higher values allow longer responses. +
+ +
+ +
+
+ + 0.9 +
+
+
+
+ + Controls diversity of word selection + (default: 0.9). Lower values make responses more focused. +
+
+ +
+
+ + 0.9 +
+
+
+
+ + Reduces repetition of + words + already used (default: 0.9). Higher values discourage repetitive language. +
+
+ +
+
+ + 0.8 +
+
+
+
+ + Encourages discussing new + topics (default: 0.8). Higher values promote topic diversity. +
+ +
+ +
+ +
+
+

Visual Theme

+ +
+ +
+ +
+
+ +
+ +
+
+ + 0.8 +
+
+
+ +
+ +
+
+ +
+

+ + Transcript Settings +

+ +
+ + Display real-time + transcription when you speak to send a message and when the AI responds. +
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
+ +
+ + Stream text as it's + generated for real-time responses (default: enabled). Shows text progressively + instead of waiting for complete response. +
+ +
+
+ +
+
+

Statistics

+
+
+
0 / 0
+
Tokens (in/out)
+
+
+
65%
+
Affection
+
+
+
0
+
Conversations
+
+
+
0
+
Days Together
+
+
+
+ +
+

Memory System

+ +
+ + Intelligent Memory allows your + character to remember conversations, preferences, and important details across sessions. + This creates more personalized and coherent interactions over time. +
+ +
+ +
+
+
+
+ +
+ +
+
+ 0 memories + +
+
+
+ +
+ +
+
+ + + +
+
+
+
+ +
+

Data Management +

+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+

+ + System Information +

+
+
+
Calculating...
+
DB Size
+
+
+
Calculating...
+
Storage used
+
+
+
+
+ +
+
+

Plugin Manager

+
+ + + Currently, only "Sample Blue Theme" is fully + functional. Other plugins are in development and activating them will have no effect + at this time. + +
+
+
+ +
+
+
+
+ + + +
+
+ + +
+
+
+

+ About Kimi +

+ +
+ +
+
+

Creators

+
+
+
👨‍💻
+
+

Jean

+

Creative vision, passionate dev

+ Creator & Developer + +
+
+
+
💕
+
+

Kimi

+

Artificial intelligence, code magic

+ Virtual Companion & Co-developer + +
+
+
+

+ "This app creates a realistic virtual companion girlfriend who grows, learns, and builds a + meaningful, interactive connection with you. Perfect for personalized AI relationships and + emotional support." +

+
+ +
+

Main Features

+
+
+ +

Voice Interface

+

Advanced speech recognition and natural synthesis. Click the microphone and speak + naturally with real-time emotion detection!

+
+
+ +

Advanced AI Models

+

Support for multiple AI providers (OpenRouter, OpenAI, Groq, Together, DeepSeek, + Custom + OpenAI-compatible, Local Ollama).

+
+
+ +

Multiple Characters

+

6 unique AI personalities: Kimi (cosmic dreamer), Bella (nurturing botanist), + Rosa (chaotic prankster), Stella (digital artist), 2Blanche (stoic android), + Jasmine (Goddess of Love).

+
+
+ +

Dynamic Personality

+

6 evolving traits (affection, playfulness, intelligence, empathy, humor, romance) + that + adapt based on conversations.

+
+
+ +

Intelligent Memory System

+

Automatic extraction and categorization of memories from conversations. Your + companion + remembers preferences, experiences, and important details.

+
+
+ +

Emotion-Driven Visuals

+

Real-time video responses that match detected emotions and personality states + with smooth + transitions.

+
+
+ +

Customizable Interface

+

5 beautiful themes with adjustable transparency, animations, and responsive + design for + all devices.

+
+
+ +

Multilingual Support

+

Full localization in 7 languages with automatic language detection and + culturally-aware + responses.

+
+
+ +

Plugin System

+

Extensible architecture with themes, voices, and behavior plugins for unlimited + customization possibilities.

+
+
+
+ +
+

Quick Guide

+
+
+ 1 +
+

API Configuration

+

Choose your provider in API & Models, fill Base URL/Model ID + if + needed, enter and save your API key, then use Test API Key. +

+
+
+
+ 2 +
+

Choose Character

+

Select your companion in Personality tab and adjust their + traits to + match your preferences.

+
+
+
+ 3 +
+

Enable Memory

+

Activate intelligent memory in Data tab for your companion + to + remember important details.

+
+
+
+ 4 +
+

Start Conversation

+

Use text chat or click the microphone 🎤 to speak naturally. Watch emotions + and + personality evolve!

+
+
+
+ 5 +
+

Customize & Backup

+

Personalize themes in Appearance and regularly export your + data for + safekeeping.

+
+
+
+
+ +
+

Tips & Tricks

+
+
+ +

Browser Choice: Microsoft Edge recommended for optimal Voice + Recognition and Text to Speech (TTS) performance

+
+
+ +

API Setup: You can use OpenRouter, OpenAI, Groq, Together, + DeepSeek or + your own OpenAI-compatible endpoint (and Local Ollama). Create accounts as + needed.

+
+
+ +

Memory System: Your companion learns faster when you share + specific + details about yourself

+
+
+ +

Relationship Building: Consistent positive interactions + naturally + increase affection and unlock deeper conversations

+
+
+ +

Character Switching: Each character has unique memories and + personality + development - try them all!

+
+
+ +

Voice Tips: Speak clearly and pause briefly between sentences + for better + emotion detection

+
+
+ +

Data Management: Export conversations regularly and use memory + management to review learned information

+
+
+ +

Plugins: Explore the plugin system to add custom themes, voices, + and + behaviors

+
+
+ +

Mobile Support: Works on tablets and phones - perfect for + conversations + anywhere

+
+
+
+ +
+

Technical Information

+
+

Created date : July 16, 2025

+

Version : v1.1.7.1 "HF and GH version"

+

Last update : November 12, 2025

+

Technologies : HTML5, CSS3, JavaScript ES6+, IndexedDB, Web Speech + API

+

Status : ✅ Stable and functional

+

💕 _"Love is the most powerful code"_ 💕

+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/kimi-css/kimi-memory-styles.css b/kimi-css/kimi-memory-styles.css new file mode 100644 index 0000000000000000000000000000000000000000..d705afc12776aa6900d596239af3b06108f54748 --- /dev/null +++ b/kimi-css/kimi-memory-styles.css @@ -0,0 +1,691 @@ +/* ===== MEMORY SYSTEM STYLES ===== */ + +/* Memory Input Group */ +.memory-input-group { + display: flex; + flex-direction: column; + gap: 8px; +} + +.memory-stats { + display: flex; + align-items: center; + gap: 12px; +} + +.memory-stats span { + color: var(--text-secondary); + font-size: 0.9em; + font-weight: 500; +} + +/* Memory Modal */ +.memory-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.85); + display: flex; + justify-content: center; + align-items: center; + z-index: 10000; + opacity: 0; + animation: fadeIn 0.3s ease forwards; + backdrop-filter: blur(3px); +} + +.memory-modal { + background: var(--background-secondary); + border-radius: 12px; + width: 90%; + max-width: 900px; + max-height: 85vh; + overflow: hidden; + box-shadow: 0 25px 70px rgba(0, 0, 0, 0.4); + transform: scale(0.9); + animation: modalSlideIn 0.3s ease forwards; + border: 1px solid var(--border-color); +} + +.memory-header { + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); + display: flex; + justify-content: space-between; + align-items: center; + background: var(--primary-gradient); +} + +.memory-title { + margin: 0; + color: white; + font-size: 1.3rem; + display: flex; + align-items: center; + gap: 12px; +} + +.memory-close { + background: none; + border: none; + color: white; + font-size: 1.5rem; + cursor: pointer; + padding: 8px; + border-radius: 6px; + transition: background-color 0.2s; +} + +.memory-close:hover { + background: rgba(255, 255, 255, 0.1); +} + +.memory-content { + padding: 20px 24px; + max-height: 60vh; + overflow-y: auto; +} + +/* Memory Filters */ +.memory-filters { + display: grid; + grid-template-columns: 2fr 1fr auto; + gap: 12px; + margin-bottom: 20px; + align-items: center; +} + +.memory-search-container { + position: relative; + display: flex; + align-items: center; +} + +.memory-search-icon { + position: absolute; + right: 12px; + color: var(--text-secondary); + pointer-events: none; +} + +#memory-search { + width: 100%; + padding-right: 36px; +} + +/* Consolidated Mobile Responsive */ +@media (max-width: 768px) { + .memory-filters { + grid-template-columns: 1fr; + gap: 8px; + } + + .memory-category-header { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + + .memory-badges { + flex-wrap: wrap; + gap: 4px; + } + + .memory-modal { + width: 95%; + max-height: 90vh; + } + + .memory-header { + padding: 16px 20px; + } + + .memory-content { + padding: 16px 20px; + } + + .memory-item { + padding: 10px; + } + + .memory-item .memory-header { + flex-direction: column; + align-items: flex-start; + gap: 6px; + } + + .memory-meta { + flex-direction: column; + gap: 4px; + font-size: 0.7rem; + } + + .memory-category { + font-size: 0.7rem; + padding: 2px 6px; + } + + .memory-preview-text { + font-size: 0.85rem; + } + + .memory-actions { + gap: 4px; + } + + .memory-edit-btn, + .memory-delete-btn { + min-width: 24px; + height: 24px; + font-size: 0.75rem; + } +} + +/* Memory List */ +.memory-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.memory-item { + background: rgba(var(--background-primary-rgb, 255, 255, 255), 0.95); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 12px; + transition: all 0.2s ease; + backdrop-filter: blur(5px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.memory-item:hover { + border-color: var(--primary-color); + box-shadow: 0 4px 16px rgba(var(--primary-rgb), 0.2); + background: rgba(var(--background-primary-rgb, 255, 255, 255), 1); + transform: translateY(-1px); +} + +.memory-item .memory-header { + padding: 0; + border: none; + background: none; + margin-bottom: 6px; + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; +} + +.memory-item-title { + font-size: 1rem; + font-weight: 700; + color: var(--memory-modal-text, #e0e0e0); + margin-right: 12px; + max-width: 60%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.memory-category { + background: var(--primary-color); + color: white; + padding: 3px 8px; + border-radius: 10px; + font-size: 0.75rem; + font-weight: 500; + text-transform: capitalize; + white-space: nowrap; +} + +.memory-type { + background: var(--background-secondary); + color: var(--text-secondary); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.7rem; + text-transform: uppercase; + font-weight: 600; +} + +.memory-length { + background: var(--border-color); + color: var(--text-secondary); + padding: 2px 6px; + border-radius: 4px; + font-size: 0.7rem; + font-weight: 500; +} + +.memory-confidence { + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 500; +} + +.memory-item .memory-content { + padding: 0; + max-height: none; + overflow: visible; + color: #e0e0e0; + line-height: 1.4; + margin-bottom: 8px; + font-size: 0.9rem; +} + +.memory-preview { + display: block; + margin-bottom: 6px; + color: #e0e0e0; + line-height: 1.4; + font-size: 0.9rem; +} + +.memory-preview-text { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + color: #e0e0e0; +} + +.memory-preview-full { + white-space: normal; + max-height: 60px; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; +} + +.memory-expand-btn { + background: none; + border: none; + color: var(--primary-color); + cursor: pointer; + font-size: 0.8rem; + padding: 2px 4px; + margin-top: 4px; + border-radius: 3px; + transition: background-color 0.2s; +} + +.memory-expand-btn:hover { + background: rgba(var(--primary-rgb), 0.1); +} + +.memory-meta { + display: flex; + gap: 8px; + margin-bottom: 8px; + font-size: 0.75rem; + color: var(--text-secondary); + align-items: center; +} + +.memory-source { + cursor: help; + text-decoration: underline dotted; +} + +/* Source excerpt content shown under the trigger */ +.memory-source { + position: relative; + outline: none; +} + +.memory-source:focus { + box-shadow: 0 0 0 3px rgba(var(--primary-rgb), 0.14); + border-radius: 4px; +} + +.memory-source-content { + margin-top: 8px; + padding: 10px 12px; + background: rgba(0, 0, 0, 0.6); + color: #f1f1f1; + border-radius: 8px; + font-size: 0.9rem; + line-height: 1.4; + border: 1px solid rgba(255, 255, 255, 0.04); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6); + max-width: 100%; + white-space: pre-wrap; + word-break: break-word; + transition: + opacity 0.18s ease, + transform 0.18s ease; + opacity: 0; + transform: translateY(-6px); + z-index: 2; +} + +.memory-source-content[style*="display: block"] { + opacity: 1; + transform: translateY(0); +} + +/* Make sure content aligns visually under the memory item */ +.memory-item .memory-source-content { + margin-left: 0; +} + +@media (max-width: 480px) { + .memory-source-content { + font-size: 0.95rem; + padding: 12px; + } +} + +.memory-actions { + display: flex; + gap: 6px; + justify-content: flex-end; +} + +.memory-edit-btn, +.memory-delete-btn { + padding: 4px 6px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 0.8rem; + transition: all 0.2s ease; + min-width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; +} + +.memory-edit-btn { + background: var(--primary-color); + color: white; +} + +.memory-edit-btn:hover { + background: var(--primary-dark); +} + +.memory-delete-btn { + background: #e74c3c; + color: white; +} + +.memory-delete-btn:hover { + background: #c0392b; +} + +/* Empty State */ +.memory-empty { + text-align: center; + padding: 40px 20px; + color: var(--text-secondary); +} + +.memory-empty i { + font-size: 3rem; + margin-bottom: 16px; + opacity: 0.5; +} + +.memory-empty p { + margin: 0; + line-height: 1.5; +} + +/* Memory Category Groups */ +.memory-category-group { + margin-bottom: 16px; +} + +.memory-category-header { + margin: 0 0 8px 0; + padding: 6px 12px; + background: var(--primary-color); + color: white; + border-radius: 6px; + font-size: 0.85rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: space-between; +} + +.memory-category-count { + opacity: 0.8; + font-weight: normal; + font-size: 0.8rem; +} + +.memory-category-items { + display: flex; + flex-direction: column; + gap: 6px; +} + +/* Memory Item Types */ +.memory-item.memory-auto { + border-left: 4px solid #3498db; +} + +.memory-item.memory-manual { + border-left: 4px solid #9b59b6; +} + +.memory-badges { + display: flex; + gap: 8px; + align-items: center; +} + +.memory-type { + font-size: 0.75rem; + padding: 2px 6px; + border-radius: 4px; + text-transform: uppercase; + font-weight: 500; +} + +.memory-type.auto_extracted { + background: #3498db; + color: white; +} + +.memory-type.manual { + background: #9b59b6; + color: white; +} + +.memory-type.imported { + background: #f39c12; + color: white; +} + +/* Confidence Levels */ +.memory-confidence { + font-size: 0.8rem; + font-weight: 500; + padding: 2px 6px; + border-radius: 4px; +} + +.confidence-high { + background: #27ae60; + color: white; +} + +.confidence-medium { + background: #f39c12; + color: white; +} + +.confidence-low { + background: #e74c3c; + color: white; +} + +/* Importance badge */ +.memory-importance { + padding: 2px 6px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; +} +.importance-high { + background: #8e44ad; + color: #fff; +} +.importance-medium { + background: #16a085; + color: #fff; +} +.importance-low { + background: #7f8c8d; + color: #fff; +} + +/* Tags (chips) */ +.memory-tags { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin: 6px 0 4px 0; +} +.memory-tag { + font-size: 0.7rem; + padding: 2px 6px; + border-radius: 999px; + background: var(--border-color); + color: var(--text-secondary); + border: 1px solid rgba(0, 0, 0, 0.05); +} +.memory-tag.tag-relationship { + background: rgba(231, 76, 60, 0.15); + color: #e74c3c; + border-color: rgba(231, 76, 60, 0.25); +} +.memory-tag.tag-boundary { + background: rgba(52, 152, 219, 0.15); + color: #3498db; + border-color: rgba(52, 152, 219, 0.25); +} +.memory-tag.tag-time { + background: rgba(241, 196, 15, 0.2); + color: #d35400; + border-color: rgba(241, 196, 15, 0.3); +} +.memory-tag.tag-type { + background: rgba(46, 204, 113, 0.2); + color: #27ae60; + border-color: rgba(46, 204, 113, 0.3); +} +.memory-tag.tag-generic { + opacity: 0.9; +} +.memory-tag.tag-more { + background: transparent; + color: var(--text-secondary); + border-style: dashed; +} + +/* Memory Toggle Indicator */ +.toggle-switch { + position: relative; +} + +.memory-indicator { + position: absolute; + top: -2px; + right: -2px; + width: 8px; + height: 8px; + border-radius: 50%; + border: 2px solid white; +} + +/* Enhanced animations */ +.memory-item { + transition: all 0.3s ease; + transform: translateY(0); +} + +.memory-item:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(var(--primary-rgb), 0.15); +} + +@keyframes fadeIn { + to { + opacity: 1; + } +} + +@keyframes modalSlideIn { + to { + opacity: 1; + transform: scale(1); + } +} + +/* Memory Feedback Notifications */ +.memory-feedback { + font-family: var(--font-family, sans-serif); + border-radius: 8px; + backdrop-filter: blur(5px); + font-weight: 500; + border: 1px solid rgba(255, 255, 255, 0.1); + min-width: 200px; + text-align: center; +} + +.memory-feedback-info { + background: linear-gradient(135deg, rgba(52, 152, 219, 0.9), rgba(52, 152, 219, 0.7)); + color: white; + box-shadow: 0 4px 15px rgba(52, 152, 219, 0.3); +} + +.memory-feedback-success { + background: linear-gradient(135deg, rgba(39, 174, 96, 0.9), rgba(39, 174, 96, 0.7)); + color: white; + box-shadow: 0 4px 15px rgba(39, 174, 96, 0.3); +} + +.memory-feedback-error { + background: linear-gradient(135deg, rgba(231, 76, 60, 0.9), rgba(231, 76, 60, 0.7)); + color: white; + box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3); +} + +/* Unified Dark Theme for Memory Modal - All Themes */ +/* Theme-agnostic variables for consistent memory modal appearance */ +.memory-modal { + background: #1a1a1a; + color: #e0e0e0; + --memory-modal-bg: #1a1a1a; + --memory-modal-text: #e0e0e0; + --memory-item-bg: rgba(42, 42, 42, 0.95); + --memory-item-bg-hover: rgba(42, 42, 42, 1); + --memory-item-border: #404040; +} + +.memory-item { + background: var(--memory-item-bg); + border-color: var(--memory-item-border); + color: var(--memory-modal-text); + backdrop-filter: blur(5px); +} + +.memory-item:hover { + border-color: var(--primary-color); + background: var(--memory-item-bg-hover); + box-shadow: 0 4px 16px rgba(var(--primary-rgb), 0.2); +} + +.memory-expand-btn:hover { + background: rgba(var(--primary-rgb), 0.2); +} diff --git a/kimi-css/kimi-settings.css b/kimi-css/kimi-settings.css new file mode 100644 index 0000000000000000000000000000000000000000..d04a60786d68b4828e20e33d7efdb9005d818fc8 --- /dev/null +++ b/kimi-css/kimi-settings.css @@ -0,0 +1,1632 @@ +/* ===== KIMI SETTINGS PANEL ===== */ + +.settings-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--modal-overlay-bg); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + z-index: 50; + display: none; + opacity: 0; + transition: + opacity 0.3s ease, + backdrop-filter 0.3s ease; +} + +.settings-overlay.visible { + display: flex; + opacity: 1; + justify-content: center; + align-items: center; + padding: 10px; +} + +/* ===== SETTINGS PANEL ===== */ +.settings-panel { + background: var(--modal-bg); + border-radius: 15px; + border: 1px solid var(--modal-border); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.5); + width: 90%; + max-width: 800px; + max-height: 85vh; + overflow: hidden; + animation: slideInUp 0.4s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; + flex-direction: column; +} + +.settings-content { + flex: 1; + overflow-y: auto; + padding: 0; +} + +.settings-content::-webkit-scrollbar { + width: var(--scrollbar-width); +} + +.settings-content::-webkit-scrollbar-track { + background: var(--scrollbar-track-bg); + border-radius: 4px; +} + +.settings-content::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb-bg); + border-radius: 4px; +} + +.settings-content::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover-bg); +} + +@keyframes slideInUp { + from { + transform: translateY(50px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +.settings-header { + background: var(--modal-header-bg); + padding: 25px 30px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.settings-header-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.settings-title { + margin: 0; + color: var(--modal-title-color); + font-size: 1.5rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 10px; + text-shadow: 0 1px 1px #000; +} + +.help-button, +.settings-close { + background: none; + border: none; + color: var(--modal-text); + font-size: 1.5rem; + cursor: pointer; + padding: 8px; + border-radius: 50%; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.help-button:hover, +.settings-close:hover { + background-color: var(--modal-close-hover-bg); + transform: scale(1.1); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.settings-content { + flex: 1; + overflow-y: auto; + padding: 0; + position: relative; + z-index: 1; + min-height: 0; +} + +/* ===== SETTINGS TABS ===== */ +.settings-tabs { + display: flex; + overflow-x: auto; + scroll-behavior: smooth; + scrollbar-width: none; + -ms-overflow-style: none; + box-sizing: border-box; + transition: padding-right 0.3s ease; + position: sticky; + top: 0; + z-index: 100; + background: var(--settings-tab-bg); + border-bottom: 2px solid var(--settings-tab-border); + backdrop-filter: blur(10px); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; +} + +.settings-tabs::-webkit-scrollbar { + display: none; +} + +.settings-tab { + flex: 1; + min-width: 130px; + max-width: 200px; + padding: 15px 16px; + background: none; + border: none; + color: var(--settings-tab-color); + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + transition: + all 0.3s ease, + font-size 0.2s ease, + padding 0.2s ease; + position: relative; + white-space: nowrap; + text-align: center; + text-overflow: ellipsis; + overflow: hidden; + box-sizing: border-box; +} + +.settings-tab:hover { + color: var(--settings-tab-hover-color); + background: var(--settings-tab-hover-bg); +} + +.settings-tab.active { + color: var(--settings-tab-active-color); + background: var(--settings-tab-active-bg); + backdrop-filter: blur(10px); +} + +.settings-tab.active::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--primary-color, #ff9a9e), var(--secondary-color, #fecfef)); +} + +.tab-content { + display: none; + padding: 30px; + position: relative; + z-index: 1; + overflow: visible; + background: var(--settings-bg); + color: var(--modal-text); +} + +.tab-content.active { + display: block; +} + +/* ===== SECTIONS DE CONFIGURATION ===== */ + +.config-section { + margin-bottom: 30px; + padding: 20px; + background: var(--settings-section-bg); + border-radius: 15px; + border: 1.5px solid var(--settings-section-border); +} + +.config-section h3 { + margin: 0 0 15px 0; + color: var(--settings-section-header-color); + font-size: 1.2rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; + text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); +} + +.config-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; + padding: 10px 0; +} + +.config-row:last-child { + margin-bottom: 0; +} + +.config-label { + color: var(--settings-text); + font-weight: 500; + flex: 1; +} + +.config-label-section { + flex: 1; + display: flex; + flex-direction: column; +} + +.config-help { + display: block; + color: var(--settings-text-secondary, #888); + font-size: 0.8rem; + margin: 4px 0 0 0; + opacity: 0.8; + line-height: 1.3; +} + +.config-note-info { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 12px 0 16px 0; + padding: 12px; + background: var(--settings-bg-secondary, rgba(255, 255, 255, 0.05)); + border: 1px solid var(--settings-border-color, rgba(255, 255, 255, 0.1)); + border-radius: 6px; + border-left: 3px solid var(--accent-color, #8a2be2); +} + +.config-note-info i { + color: var(--accent-color, #8a2be2); + margin-top: 1px; + font-size: 0.9rem; +} + +/* Variantes de couleur pour différents types de notes */ +.config-note-info.info i { + color: #4a9eff; +} + +.config-note-info.tip i { + color: #ff9500; +} + +.config-note-info.settings i { + color: #00c896; +} + +.config-note-info .config-help { + margin: 0; + opacity: 0.9; +} + +.presence-dot { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: #9e9e9e; +} + +.inline-row { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.config-control { + display: flex; + flex-direction: column; + gap: 10px; + flex: 1; + width: 100%; + margin-left: 0; +} + +/* ===== CONTRÔLES PERSONNALISÉS ===== */ + +.slider-container { + width: 100%; + max-width: 380px; + position: relative; + display: flex; + flex-direction: row; + align-items: center; + gap: 12px; +} + +.kimi-slider, +.kimi-slider-unified { + flex: 1; + min-width: 0; + width: 100%; +} + +.slider-value { + background: var(--slider-value-bg); + color: var(--slider-value-color); + padding: 4px 8px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; + border: 1px solid var(--slider-value-border); + min-width: 45px; + text-align: center; + flex-shrink: 0; +} + +.toggle-switch { + position: relative; + width: 50px; + height: 25px; + background: var(--switch-bg-inactive); + border-radius: 25px; + cursor: pointer; + transition: background-color 0.3s ease; +} + +.toggle-switch.active { + background: var(--switch-bg-active); +} + +.toggle-switch::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 21px; + height: 21px; + background: var(--switch-thumb-color); + border-radius: 50%; + transition: transform 0.3s ease; + box-shadow: var(--switch-thumb-shadow); +} + +.toggle-switch.active::after { + transform: translateX(25px); +} + +.toggle-switch#transcript-toggle { + background: var(--switch-bg-inactive); +} + +.toggle-switch#transcript-toggle.active { + background: var(--switch-bg-active); +} + +/* ===== INFORMATIONS ET STATS ===== */ + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin-top: 20px; +} + +.stat-card { + background: var(--card-bg); + border-radius: 10px; + padding: 10px; + text-align: center; + border: 1px solid var(--card-border); +} + +.stat-value { + font-size: 1.4rem; + font-weight: 700; + color: var(--stat-value-color); + margin-bottom: 5px; +} + +.stat-label { + color: var(--stat-label-color); + font-size: 0.85rem; +} + +.model-card { + background: var(--card-bg); + border-radius: 12px; + padding: 20px; + margin-bottom: 15px; + border: 1px solid var(--card-border); + cursor: pointer; + transition: all 0.3s ease; +} + +.model-card:hover { + background: var(--card-hover-bg); + transform: translateY(-2px); +} + +.model-card.selected { + border-color: var(--model-card-selected-border); + box-shadow: var(--model-card-selected-shadow); +} + +.models-search-container { + margin: 12px 0 16px; +} + +.models-section { + margin: 16px 0; +} + +.models-section-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--settings-section-header-color); + margin: 6px 0 10px; +} + +.model-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; +} + +.model-name { + font-size: 1.1rem; + font-weight: 600; + color: var(--model-name-color); +} + +.model-provider { + background: var(--model-provider-color); + color: var(--model-provider-text); + padding: 4px 8px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 500; +} + +.model-description { + color: var(--model-description-color); + font-size: 0.9rem; + margin-bottom: 10px; +} + +.strength-tag { + display: inline-block; + background: var(--strength-tag-bg); + color: var(--strength-tag-text); + padding: 4px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + margin-right: 8px; + margin-bottom: 5px; +} + +/* Models error and no-models messages */ +.no-models-message, +.models-error-message { + text-align: center; + padding: 40px 20px; + border-radius: 12px; + margin: 20px 0; + background: var(--background-secondary); + border: 1px solid var(--border-color); +} + +.no-models-message p, +.models-error-message p { + margin: 0; + color: var(--text-secondary); + font-size: 0.95rem; + line-height: 1.5; +} + +.models-error-message { + background: rgba(231, 76, 60, 0.1); + border-color: rgba(231, 76, 60, 0.3); +} + +.models-error-message p { + color: #e74c3c; +} + +.model-strengths { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.strength-tag { + background: var(--model-strength-color); + color: var(--model-strength-text); + padding: 3px 8px; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; +} + +/* ===== PLUGIN CARDS AND SWITCHES ===== */ + +.plugin-card { + background: linear-gradient(135deg, #22121a 80%, var(--modal-bg) 100%); + border: 2px solid var(--modal-border); + border-radius: 20px; + box-shadow: + 0 4px 24px 0 #000a, + 0 2px 0 0 var(--modal-border); + padding: 28px 32px 24px 32px; + margin-bottom: 28px; + display: flex; + flex-direction: row; + align-items: center; + gap: 0; + justify-content: space-between; + transition: + box-shadow 0.2s, + border 0.2s; + position: relative; +} + +.plugin-card .plugin-info { + flex: 2 1 0; + min-width: 0; + margin-right: 24px; +} + +.plugin-card-center { + flex: 1 1 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 10px; + min-width: 120px; +} + +.plugin-card-switch { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: flex-end; + min-width: 80px; + margin-left: 24px; +} + +.plugin-card .plugin-title { + font-size: 1.35rem; + font-weight: 700; + color: var(--plugin-card-title-color); + margin-bottom: 6px; + letter-spacing: 0.01em; + text-shadow: 0 2px 8px #000b; +} + +.plugin-card .plugin-type { + font-size: 1rem; + color: var(--accent-color); + margin-left: 10px; + font-weight: 600; + text-shadow: 0 1px 4px #0008; +} + +.plugin-card .plugin-desc { + color: var(--plugin-card-desc-color); + margin-bottom: 10px; + font-size: 1.01rem; + line-height: 1.5; +} + +.plugin-card .plugin-author { + font-size: 0.95rem; + color: var(--plugin-card-author-color); + font-weight: 500; +} + +.plugin-card-left { + display: flex; + flex-direction: column; + align-items: flex-start; + min-width: 90px; + gap: 8px; + margin-top: 4px; +} + +.plugin-card-right { + display: flex; + flex-direction: column; + flex: 1; + gap: 12px; +} + +.plugin-type-badge { + display: inline-block; + background: var(--plugin-type-badge-bg); + color: #fff; + font-size: 0.85rem; + font-weight: 600; + border-radius: 8px; + padding: 3px 10px; + margin-bottom: 8px; + margin-right: 12px; + letter-spacing: 0.03em; + box-shadow: 0 1px 4px #0002; +} + +.plugin-theme-swatch { + display: inline-flex; + gap: 4px; + margin-bottom: 8px; + margin-right: 12px; + vertical-align: middle; +} + +.plugin-theme-swatch span { + display: inline-block; + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #fff2; + box-shadow: 0 1px 4px #0002; +} + +.plugin-active-badge { + display: inline-block; + background: var(--plugin-active-badge-bg); + color: #fff; + font-size: 0.85rem; + font-weight: 700; + border-radius: 8px; + padding: 3px 12px; + margin-bottom: 8px; + margin-right: 12px; + letter-spacing: 0.03em; + box-shadow: 0 1px 8px #0003; +} + +@media (max-width: 700px) { + .plugin-card { + flex-direction: column; + gap: 12px; + } + .plugin-card-left { + flex-direction: row; + align-items: center; + min-width: 0; + gap: 10px; + margin-top: 0; + } + .plugin-card-right { + gap: 8px; + } +} + +/* ===== CONSOLIDATED RESPONSIVE DESIGN ===== */ +@media (max-width: 768px) { + /* Settings panel responsive styles */ + + .settings-header { + padding: 20px; + } + + .settings-title { + font-size: 1.3rem; + } + + .settings-tabs { + padding: 0 10px; + gap: 5px; + position: relative; + } + + .settings-tab { + flex: 0 0 auto; + min-width: 110px; + padding: 12px 16px; + font-size: 0.85rem; + border-radius: 8px 8px 0 0; + } + + .settings-tab.active::after { + height: 2px; + } + + .tab-content { + padding: 20px; + } + + .config-row { + flex-direction: column; + align-items: flex-start; + gap: 10px; + } + + .config-control { + margin-left: 0; + width: 100%; + } + + .slider-container { + width: 100%; + max-width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 10px; + } + + .slider-value { + min-width: 45px; + flex-shrink: 0; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 480px) { + /* Settings panel responsive styles consolidated */ + + .config-row { + flex-direction: column; + align-items: stretch; + gap: 8px; + } + + .config-control { + margin-left: 0; + width: 100%; + } + + .slider-container { + width: 100%; + max-width: 100%; + flex-direction: row; + justify-content: space-between; + align-items: center; + gap: 8px; + } + + .kimi-slider, + .kimi-slider-unified { + flex: 1; + min-width: 0; + } + + .slider-value { + min-width: 40px; + max-width: 50px; + flex-shrink: 0; + font-size: 0.75rem; + } + + .kimi-select, + .kimi-select-unified { + width: 100%; + max-width: 100%; + min-width: 0; + font-size: 0.9rem; + padding: 10px 35px 10px 12px; + } + + .kimi-input, + .kimi-input-unified { + width: 100%; + max-width: 100%; + min-width: 0; + font-size: 0.9rem; + } + + .settings-tab { + min-width: 90px; + font-size: 0.8rem; + padding: 10px 12px; + } + + .config-section { + padding: 15px; + } + + .config-section h3 { + font-size: 1.1rem; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: 10px; + } + + .stat-card { + padding: 10px; + } + + .stat-value { + font-size: 1.3rem; + } +} + +@media (max-width: 360px) { + /* Settings panel responsive styles consolidated */ + + .slider-container { + gap: 5px; + } + + .slider-value { + min-width: 35px; + max-width: 40px; + font-size: 0.7rem; + padding: 2px 6px; + } + + .kimi-select, + .kimi-select-unified { + font-size: 0.85rem; + padding: 8px 30px 8px 10px; + } + + .settings-tab { + min-width: 80px; + font-size: 0.75rem; + padding: 8px 10px; + } + + .stats-grid { + grid-template-columns: 1fr; + } +} + +/* ===== HELP MODAL ===== */ +.help-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: var(--modal-overlay-bg); + backdrop-filter: blur(15px); + -webkit-backdrop-filter: blur(15px); + z-index: 60; + display: none; + opacity: 0; + transition: + opacity 0.3s ease, + backdrop-filter 0.3s ease; +} + +.help-overlay.visible { + display: flex; + opacity: 1; + align-items: center; + justify-content: center; + padding: 10px; +} + +.help-modal { + background: var(--help-modal-bg, linear-gradient(145deg, rgba(26, 26, 26, 0.95), rgba(40, 40, 40, 0.95))); + backdrop-filter: blur(20px); + border-radius: 25px; + border: 1px solid var(--help-modal-border, rgba(255, 255, 255, 0.1)); + box-shadow: + 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 30px var(--primary-color, rgba(255, 107, 157, 0.3)); + width: 100%; + max-width: 850px; + max-height: 85vh; + overflow: hidden; + display: flex; + flex-direction: column; + animation: slideInUp 0.4s ease-out; + margin: 20px; +} + +.help-header { + background: var(--modal-header-bg, linear-gradient(135deg, var(--primary-color, #ff9a9e), var(--secondary-color, #fecfef))); + padding: 25px 30px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.help-title { + margin: 0; + color: var(--modal-title-color); + font-size: 1.5rem; + font-weight: 700; + display: flex; + align-items: center; + gap: 10px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.help-close { + background: none; + border: none; + color: var(--modal-text); + font-size: 1.5rem; + cursor: pointer; + padding: 8px; + border-radius: 50%; + transition: all 0.3s ease; + backdrop-filter: blur(10px); +} + +.help-close:hover { + background-color: var(--modal-close-hover-bg); + transform: scale(1.1); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.help-content { + flex: 1; + padding: 25px 30px; + overflow-y: auto; + color: var(--help-content-color); +} + +/* Help Content Components */ +.help-section { + margin-bottom: 30px; + padding-bottom: 20px; + border-bottom: 1px solid var(--help-section-border); +} + +.help-section:last-child { + border-bottom: none; +} + +.help-section h3 { + color: var(--modal-title-color); + font-size: 1.3rem; + margin-bottom: 15px; + display: flex; + align-items: center; + gap: 10px; +} + +.creators-info { + display: flex; + gap: 20px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.creator-card { + background: var(--creator-card-bg); + border: 1px solid var(--creator-card-border); + padding: 20px; + border-radius: 15px; + flex: 1; + min-width: 280px; + display: flex; + align-items: center; + gap: 15px; +} + +.creator-avatar { + font-size: 2.5rem; + width: 60px; + height: 60px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--creator-avatar-bg); +} + +.creator-details h4 { + margin: 0 0 5px 0; + color: #1650a0; + font-size: 1.2rem; +} + +.creator-details p { + margin: 0 0 8px 0; + color: var(--feature-text-color); + font-size: 0.9rem; +} + +.creator-role { + background: var(--creator-role-bg); + color: var(--creator-role-color); + padding: 4px 12px; + border-radius: 12px; + font-size: 0.8rem; + font-weight: 500; +} + +/* Creator Links */ +.creator-links { + display: flex; + gap: 8px; + margin-top: 12px; +} + +.creator-link { + background: var(--creator-role-bg, linear-gradient(135deg, var(--primary-color), var(--secondary-color))); + color: var(--creator-role-color, var(--text-on-primary)); + padding: 8px 12px; + border-radius: 20px; + text-decoration: none; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + min-width: auto; + height: 36px; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-size: 0.8rem; + font-weight: 500; + border: 1px solid transparent; +} + +.creator-link:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); + border-color: var(--primary-color, #ff6b9d); +} + +/* Increased specificity to avoid !important */ +.help-modal .creator-link:hover { + color: var(--creator-role-color, var(--text-on-primary)); + text-decoration: none; + background: var(--creator-role-bg, linear-gradient(135deg, var(--primary-color), var(--secondary-color))); +} + +.help-modal .creator-link:hover i, +.help-modal .creator-link:hover span { + color: inherit; +} + +.creator-link i { + font-size: 1rem; + color: inherit; + transition: color 0.3s ease; +} + +.creator-link span { + color: inherit; + transition: color 0.3s ease; +} + +.creator-link:hover i, +.creator-link:hover span { + color: inherit !important; +} + +.philosophy { + background: var(--philosophy-bg); + border: 1px solid var(--philosophy-border); + padding: 15px; + border-radius: 10px; + border-left: 4px solid var(--philosophy-border-left); + margin: 15px 0; + font-style: italic; + color: var(--feature-text-color); +} + +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + margin: 15px 0; +} + +.feature-item { + background: var(--feature-item-bg); + border: 1px solid var(--feature-item-border); + padding: 15px; + border-radius: 10px; + text-align: center; +} + +.feature-item i { + color: var(--feature-icon-color); + font-size: 2rem; + margin-bottom: 10px; +} + +.feature-item h4 { + margin: 0 0 8px 0; + color: var(--feature-title-color); +} + +.feature-item p { + margin: 0; + font-size: 0.9rem; + color: var(--feature-text-color); +} + +.quick-guide { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 20px; +} + +.guide-step { + display: flex; + align-items: flex-start; + background: rgba(255, 255, 255, 0.05); + border-radius: 12px; + padding: 15px 20px; + margin-bottom: 12px; + min-width: 220px; + flex: 1 1 220px; +} + +.step-number { + font-size: 1.5rem; + font-weight: bold; + color: var(--guide-step-number-color); + margin-right: 16px; + background: var(--guide-step-number-bg); + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; +} + +.step-content h4 { + font-size: 1.1rem; + margin-bottom: 6px; + color: var(--modal-title-color); +} + +.step-content p { + font-size: 0.98rem; + color: var(--help-content-color); +} + +.tips-list { + display: flex; + flex-wrap: wrap; + gap: 18px; + margin-bottom: 18px; +} + +.tip-item { + display: flex; + align-items: center; + background: var(--feature-item-bg); + border-radius: 12px; + padding: 12px 18px; + min-width: 200px; + flex: 1 1 200px; +} + +.tip-item i { + font-size: 1.2rem; + color: var(--feature-icon-color); + margin-right: 12px; +} + +.tip-item p { + font-size: 0.98rem; + color: var(--help-content-color); +} + +.tech-info { + background: var(--feature-item-bg); + border-radius: 10px; + padding: 14px 20px; + margin-top: 10px; + color: var(--help-content-color); + font-size: 0.98rem; +} + +.settings-panel, +.config-section { + isolation: isolate; +} + +/* Videos background */ +.video-container { + z-index: 0; +} + +.bg-video { + z-index: 1; +} + +.content-overlay { + z-index: 2; + background-color: transparent; + backdrop-filter: none; +} + +/* Responsive for help modal */ +@media (max-width: 768px) { + .help-overlay { + padding: 5px; + } + + .help-modal { + margin: 5px; + max-height: 95vh; + border-radius: 15px; + width: calc(100% - 10px); + } + + .help-header { + padding: 20px; + } + + .help-title { + font-size: 1.3rem; + } + + .help-content { + padding: 20px; + } + + .creators-info { + flex-direction: column; + } + + .creator-card { + min-width: auto; + } + + .features-grid { + grid-template-columns: 1fr; + } +} + +/* ===== BUTTON ANIMATIONS ===== */ +.kimi-button.animated { + animation: kimiPulse 0.7s; +} + +@keyframes kimiPulse { + 0% { + background-color: var(--primary-color); + transform: scale(1); + } + 50% { + background-color: var(--accent-color); + transform: scale(1.08); + } + 100% { + background-color: var(--primary-color); + transform: scale(1); + } +} + +/* ===== CHARACTER GRID AND CARDS ===== */ +.character-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 24px; + margin-top: 16px; + margin-bottom: 16px; +} + +@media (max-width: 700px) { + .character-grid { + grid-template-columns: 1fr; + } +} + +.character-card { + background: var(--card-bg); + border: 2px solid transparent; + border-radius: 18px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + padding: 18px 16px 12px 16px; + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + transition: + border 0.2s, + box-shadow 0.2s, + background 0.2s; + user-select: none; + width: 100%; +} + +.character-card.selected { + border: 2.5px solid var(--character-selected-border, #ff6b9d); + background: var(--character-selected-bg, rgba(255, 107, 157, 0.13)); + box-shadow: + 0 0 0 4px var(--character-selected-border, #ff6b9d), + 0 4px 24px var(--character-selected-bg, rgba(255, 107, 157, 0.15)); +} + +.character-card:hover { + border: 2px solid var(--primary-color, #ff6b9d); + background: rgba(255, 107, 157, 0.1); + box-shadow: 0 2px 16px rgba(255, 107, 157, 0.1); +} + +.character-card img { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 12px; + border: 2px solid var(--modal-text); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.character-name { + text-align: center; + width: 100%; + font-size: 1.1rem; + font-weight: 700; + color: var(--modal-text); + text-shadow: 0 1px 8px var(--primary-color, #ff6b9d); + margin-bottom: 4px; + margin-top: 0; +} + +.character-info { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; + margin-bottom: 10px; +} + +.character-details { + font-size: 0.95rem; + color: var(--modal-text); + opacity: 0.85; + margin-bottom: 6px; + text-align: center; +} + +.character-prompt-label { + font-size: 0.85rem; + color: var(--modal-text); + margin-bottom: 4px; + margin-top: 8px; + opacity: 0.7; + text-align: center; +} + +.character-prompt-input { + width: 100%; + min-height: 60px; + border-radius: 8px; + border: 1px solid var(--primary-color, #ff6b9d); + background: var(--input-bg); + color: var(--modal-text); + padding: 6px 8px; + font-size: 0.95rem; + resize: vertical; + margin-bottom: 4px; +} + +.character-prompt-input:disabled { + opacity: 0.5; + background: var(--input-bg); + cursor: not-allowed; +} + +/* Character prompt buttons */ +.character-prompt-buttons { + display: flex; + gap: 8px; + margin-top: 8px; + justify-content: center; +} + +.character-save-btn, +.character-reset-btn { + padding: 6px 12px; + font-size: 0.85rem; + border-radius: 6px; + border: 1px solid var(--input-border); + background: var(--button-bg); + color: var(--button-text); + cursor: pointer; + transition: all 0.2s ease; + min-width: 70px; +} + +.character-save-btn:hover, +.character-reset-btn:hover { + background: var(--button-hover-bg); + border-color: var(--primary-color); +} + +.character-save-btn:disabled, +.character-reset-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background: var(--input-bg); +} + +.character-save-btn.success { + background: #28a745; + color: white; + border-color: #28a745; +} + +.character-reset-btn.animated { + background: var(--accent-color); + color: white; + border-color: var(--accent-color); +} + +/* ===== PERSONALITY CHEAT PANEL ===== */ + +.cheat-toggle-btn { + background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); + border: none; + color: #fff; + font-size: 1em; + margin-left: 1em; + cursor: pointer; + display: inline-flex; + align-items: center; + gap: 0.5em; + transition: + color 0.2s, + box-shadow 0.2s, + background 0.2s; + border-radius: 18px; + padding: 0.35em 1.1em 0.35em 0.9em; + box-shadow: 0 2px 10px 0 rgba(255, 107, 157, 0.1); + font-weight: 600; + letter-spacing: 0.01em; + outline: none; +} + +.cheat-toggle-btn[aria-expanded="true"] { + color: #fff; + background: linear-gradient(90deg, var(--accent-color), var(--primary-color)); + box-shadow: 0 2px 16px 0 rgba(255, 107, 157, 0.18); +} + +.cheat-toggle-btn:hover, +.cheat-toggle-btn:focus { + box-shadow: 0 2px 16px 0 rgba(255, 107, 157, 0.25); + transform: translateY(-1px); +} +.cheat-toggle-btn:focus { + background: linear-gradient(90deg, var(--primary-color), var(--accent-color)); + color: #fff; + box-shadow: 0 4px 18px 0 rgba(255, 107, 157, 0.22); + filter: brightness(1.08); +} + +.cheat-toggle-btn i { + margin-right: 0.4em; + font-size: 1.1em; +} + +.cheat-indicator { + font-size: 0.9em; + color: #aaa; + margin-bottom: 0.5em; + margin-left: 2.2em; +} + +.cheat-panel { + max-height: 0; + overflow: hidden; + transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 0.5; + pointer-events: none; +} + +.cheat-panel.open { + max-height: 800px; + opacity: 1; + pointer-events: auto; + transition: + max-height 0.6s cubic-bezier(0.4, 0, 0.2, 1), + opacity 0.3s; +} + +/* ===== ACCESSIBILITY - FOCUS STYLES ===== */ +.settings-panel select:focus, +.settings-panel input:focus, +.settings-panel button:focus, +.settings-panel .cheat-toggle-btn:focus { + box-shadow: 0 0 0 2px var(--primary-pink); + border-color: var(--primary-pink); +} + +.character-card:focus { + outline: 2px solid var(--primary-pink); + outline-offset: 2px; +} + +/* ===== API KEY INPUT STYLING ===== */ + +.api-key-input-group { + display: flex; + align-items: center; + gap: 8px; + position: relative; +} + +.api-key-input-group .kimi-input { + flex: 1; + margin: 0; +} + +.api-key-toggle { + min-width: 40px; + height: 40px; + padding: 8px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + background: var(--settings-bg-secondary, rgba(255, 255, 255, 0.08)); + border: 1px solid var(--settings-border-color, rgba(255, 255, 255, 0.15)); + color: var(--settings-text, #ffffff); + transition: all 0.2s ease; + cursor: pointer; +} + +.api-key-toggle:hover { + background: var(--settings-bg-hover, rgba(255, 255, 255, 0.12)); + border-color: var(--accent-color, #8a2be2); +} + +.api-key-toggle:active { + transform: scale(0.95); +} + +.api-key-input-group .presence-dot { + width: 12px; + height: 12px; + margin-left: 4px; + border: 2px solid var(--settings-bg-primary, rgba(0, 0, 0, 0.3)); + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.1); + transition: all 0.2s ease; +} + +.api-key-status { + margin-top: 6px; + min-height: 18px; + display: flex; + align-items: center; +} + +.api-key-status span { + font-size: 0.85rem; + display: flex; + align-items: center; + gap: 6px; +} + +.api-key-status span::before { + content: "✓"; + font-weight: bold; + font-size: 0.9rem; +} + +/* Responsive adjustments */ +@media (max-width: 600px) { + .api-key-input-group { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .api-key-toggle { + align-self: flex-end; + min-width: 100px; + } + + .api-key-input-group .presence-dot { + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + z-index: 10; + } +} diff --git a/kimi-css/kimi-style.css b/kimi-css/kimi-style.css new file mode 100644 index 0000000000000000000000000000000000000000..7319c75fdd678fedeae6348b4407ec12eb4cff44 --- /dev/null +++ b/kimi-css/kimi-style.css @@ -0,0 +1,2075 @@ +/* ===== CONSOLIDATED CSS VARIABLES - ENHANCED DYNAMIC THEMING ===== */ +:root { + /* Core Theme Colors - Default Dark Theme Base */ + --primary-color: #5e60ce; + --primary-rgb: 94, 96, 206; + --secondary-color: #23262f; + --accent-color: #8b5cf6; + --background-overlay: rgba(24, 26, 32, 0.85); + --interface-opacity: 0.7; + --gradient-start: #5e60ce; + --gradient-end: #8b5cf6; + --text-glow: 0 0 10px rgba(94, 96, 206, 0.3); + --button-hover: rgba(94, 96, 206, 0.15); + --animations-enabled: 1; + --switch-color: #5e60ce; + + /* Contrast and Accessibility */ + --contrast-ratio: 4.5; /* WCAG AA standard */ + --text-on-primary: #ffffff; + --text-on-secondary: #e0e0e0; + --text-on-accent: #ffffff; + --text-on-background: #e0e0e0; + --border-opacity: 0.3; + --hover-opacity: 0.15; + --active-opacity: 0.25; + + /* UI Component Colors - Chat Interface */ + --chat-bg: rgba(24, 26, 32, 0.95); + --chat-text: var(--text-on-background); + --chat-header-bg: rgba(255, 255, 255, 0.05); + --chat-border: #5e60ce; + --chat-input-bg: rgba(255, 255, 255, 0.1); + --chat-input-text: var(--text-on-background); + --chat-input-placeholder: rgba(255, 255, 255, 0.6); + --chat-message-user-bg: #1e253c; + --chat-message-user-text: var(--text-on-background); + --chat-message-kimi-bg: #23262f; + --chat-message-kimi-text: var(--text-on-background); + + /* Modal & Overlay Colors */ + --modal-bg: rgba(24, 26, 32, 0.98); + --modal-border: #5e60ce; + --modal-header-bg: linear-gradient(135deg, #5e60ce, #8b5cf6); + --modal-text: var(--text-on-background); + --modal-title-color: #e0e0e0; + --modal-overlay-bg: rgba(0, 0, 0, 0.8); + --modal-close-hover-bg: rgba(255, 255, 255, 0.2); + + /* Settings Panel & Tabs */ + --settings-bg: #0f1114; + --settings-text: var(--text-on-background); + --settings-tab-bg: #181a20; + --settings-tab-color: #bfa6b6; + --settings-tab-hover-bg: rgba(255, 255, 255, var(--hover-opacity)); + --settings-tab-hover-color: rgba(255, 255, 255, 0.9); + --settings-tab-active-bg: #5e60ce; + --settings-tab-active-color: var(--text-on-primary); + --settings-tab-border: #5e60ce; + --settings-section-bg: #1a1d23; + --settings-section-border: rgba(94, 96, 206, var(--border-opacity)); + --settings-section-header-color: var(--text-on-background); + + /* Form Element Colors */ + --input-bg: rgba(255, 255, 255, 0.1); + --input-text: var(--text-on-background); + --input-border: #5e60ce; + --input-focus-bg: rgba(255, 255, 255, var(--hover-opacity)); + --input-focus-border: #8b5cf6; + --input-placeholder: rgba(255, 255, 255, 0.6); + + /* Button Colors */ + --button-primary-bg: var(--primary-color); + --button-primary-text: var(--text-on-primary); + --button-primary-hover-bg: var(--accent-color); + --button-secondary-bg: rgba(255, 255, 255, 0.05); + --button-secondary-text: var(--text-on-background); + --button-secondary-hover-bg: rgba(255, 255, 255, var(--hover-opacity)); + --button-danger-bg: #e74c3c; + --button-danger-text: #ffffff; + --button-danger-hover-bg: #c0392b; + + /* Select & Dropdown Options */ + --select-bg: var(--input-bg); + --select-text: var(--input-text); + --select-border: var(--input-border); + --select-option-bg: rgba(24, 26, 32, 0.95); + --select-option-text: var(--text-on-background); + --select-option-hover-bg: #5e60ce; + --select-option-hover-text: var(--text-on-primary); + --select-option-checked-bg: #8b5cf6; + --select-option-checked-text: var(--text-on-accent); + --select-option-disabled-bg: rgba(24, 26, 32, 0.5); + --select-option-disabled-text: #666; + + /* Slider Components */ + --slider-track-bg: rgba(255, 255, 255, 0.1); + --slider-track-active-bg: var(--primary-color); + --slider-thumb-bg: var(--primary-color); + --slider-thumb-hover-bg: var(--accent-color); + --slider-value-bg: #0f1114; + --slider-value-border: #5e60ce; + --slider-value-color: var(--text-on-background); + + /* Toggle Switch */ + --switch-bg-inactive: rgba(255, 255, 255, 0.05); + --switch-bg-active: linear-gradient(90deg, #5e60ce, #8b5cf6); + --switch-thumb-color: #ffffff; + --switch-thumb-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + + /* Mic Button & Pulse Effect */ + --mic-button-bg: var(--button-hover); + --mic-button-border: #5e60ce; + --mic-button-shadow: 0 0 15px #5e60ce; + --mic-button-hover-bg: #5e60ce; + --mic-button-hover-shadow: 0 0 10px rgba(94, 96, 206, 0.5); + --mic-button-icon-color: var(--text-on-primary); + --mic-listening-border: #27ae60; + --mic-listening-shadow: 0 0 15px #27ae60; + --mic-pulse-color: rgba(39, 174, 96, 0.5); + --mic-pulse-listening-color: rgba(39, 174, 96, 0.4); + + /* Video crossfade timing */ + --video-fade-duration: 400ms; + + /* Cards & Stats */ + --card-bg: rgba(255, 255, 255, 0.02); + --card-border: rgba(255, 255, 255, 0.05); + --card-hover-bg: rgba(255, 255, 255, 0.05); + --card-text: var(--text-on-background); + --stat-value-color: #8b5cf6; + --stat-label-color: rgba(224, 224, 224, 0.7); + + /* Plugin Cards */ + --plugin-card-bg: linear-gradient(135deg, #1a1d23 80%, rgba(24, 26, 32, 0.98) 100%); + --plugin-card-border: #5e60ce; + --plugin-card-title-color: var(--text-on-background); + --plugin-card-desc-color: rgba(224, 224, 224, 0.7); + --plugin-card-author-color: rgba(224, 224, 224, 0.5); + --plugin-type-badge-bg: #8b5cf6; + --plugin-type-badge-text: var(--text-on-accent); + --plugin-active-badge-bg: linear-gradient(90deg, #5e60ce, #8b5cf6); + --plugin-active-badge-text: var(--text-on-primary); + + /* Help Modal */ + --help-modal-bg: rgba(24, 26, 32, 0.98); + --help-modal-border: #5e60ce; + --help-content-color: var(--text-on-background); + --help-section-border: rgba(255, 255, 255, 0.1); + --creator-card-bg: rgba(255, 255, 255, 0.02); + --creator-card-border: rgba(255, 255, 255, 0.05); + --creator-avatar-bg: linear-gradient(135deg, #5e60ce, #8b5cf6); + --creator-name-color: #8b5cf6; + --creator-role-bg: linear-gradient(135deg, #5e60ce, #8b5cf6); + --creator-role-color: var(--text-on-primary); + --philosophy-bg: rgba(94, 96, 206, 0.05); + --philosophy-border: rgba(94, 96, 206, var(--border-opacity)); + --philosophy-border-left: #5e60ce; + --feature-item-bg: rgba(255, 255, 255, 0.02); + --feature-item-border: rgba(94, 96, 206, var(--border-opacity)); + --feature-icon-color: #8b5cf6; + --feature-title-color: #8b5cf6; + --feature-text-color: rgba(224, 224, 224, 0.7); + --guide-step-bg: rgba(255, 255, 255, 0.02); + --guide-step-number-bg: rgba(94, 96, 206, var(--hover-opacity)); + --guide-step-number-color: var(--primary-color); + --tip-item-bg: rgba(255, 255, 255, 0.02); + --tip-item-border: var(--feature-item-border); + + /* Unified Scrollbar System */ + --scrollbar-width: 8px !important; + --scrollbar-track-bg: rgba(255, 255, 255, 0.02) !important; + --scrollbar-thumb-bg: rgba(94, 96, 206, 0.4) !important; + --scrollbar-thumb-hover-bg: rgba(94, 96, 206, 0.6) !important; + --scrollbar-thumb-active-bg: rgba(94, 96, 206, 0.8) !important; + --scrollbar-corner-bg: rgba(255, 255, 255, 0.05) !important; + + /* Model Colors */ + --model-card-bg: rgba(255, 255, 255, 0.02); + --model-card-border: rgba(255, 255, 255, 0.05); + --model-card-hover-bg: rgba(255, 255, 255, 0.05); + --model-card-selected-border: var(--primary-color); + --model-card-selected-shadow: 0 0 0 2px rgba(94, 96, 206, var(--border-opacity)); + --model-name-color: var(--text-on-background); + --model-description-color: rgba(224, 224, 224, 0.7); + --model-strength-color: #5e60ce; + --model-strength-text: var(--text-on-primary); + --model-provider-color: #8b5cf6; + --model-provider-text: var(--text-on-accent); + + /* Text Colors */ + --text-primary: var(--text-on-background); + --text-secondary: #9ca3af; + --text-muted: rgba(224, 224, 224, 0.6); + + /* Character Selection Colors */ + --character-card-bg: rgba(255, 255, 255, 0.02); + --character-card-border: rgba(255, 255, 255, 0.05); + --character-card-hover-bg: rgba(255, 255, 255, 0.05); + --character-selected-border: #8b5cf6; + --character-selected-bg: rgba(94, 96, 206, 0.1); + --character-name-color: var(--text-on-background); + + /* Waiting Indicator */ + --waiting-indicator-color: var(--primary-color); + --loading-spinner-color: var(--primary-color); + + /* Progress Bar */ + --progress-bg: rgba(255, 255, 255, 0.05); + --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color)); + --progress-text-color: var(--text-on-background); + + /* Transcript */ + --transcript-bg: rgba(0, 0, 0, 0.9); + --transcript-text: var(--text-on-background); + --transcript-border: var(--primary-color); +} + +/* === Microphone Button Disabled State (2025-09 SR capability refactor) === */ +#mic-button.disabled, +button#mic-button.disabled { + opacity: 0.45; + cursor: not-allowed; + box-shadow: none; + filter: grayscale(40%); + transition: opacity 0.25s ease; +} + +#mic-button.disabled:hover { + opacity: 0.45; /* keep stable */ +} + +#mic-button.disabled .mic-icon, +#mic-button.disabled svg { + opacity: 0.7; +} + +/* Provide a subtle tooltip helper if title attribute present */ +#mic-button.disabled[title] { + position: relative; +} + +/* ===== OPTIMIZED THEME VARIATIONS ===== */ +[data-theme="pink"] { + /* Core Theme Colors - Pink Passion */ + --primary-color: #ff6b9d; + --primary-rgb: 255, 107, 157; + --secondary-color: #ffeaa7; + --accent-color: #fd79a8; + --background-overlay: rgba(255, 107, 157, 0.15); + --gradient-start: #ff6b9d; + --gradient-end: #fd79a8; + --text-glow: 0 0 10px rgba(255, 107, 157, 0.5); + --button-hover: rgba(255, 107, 157, 0.3); + --switch-color: var(--primary-color); + + /* Contrast and Accessibility */ + --text-on-primary: #ffffff; + --text-on-secondary: #222222; + --text-on-accent: #ffffff; + --text-on-background: #ffffff; + + /* UI Component Colors - Chat Interface */ + --chat-bg: rgba(255, 107, 157, 0.9); + --chat-text: var(--text-on-primary); + --chat-header-bg: rgba(255, 255, 255, 0.05); + --chat-border: var(--primary-color); + --chat-input-bg: rgba(255, 255, 255, 0.1); + --chat-input-text: var(--text-on-background); + --chat-input-placeholder: rgba(255, 255, 255, 0.6); + --chat-message-user-bg: var(--primary-color); + --chat-message-user-text: var(--text-on-primary); + --chat-message-kimi-bg: rgba(255, 255, 255, 0.15); + --chat-message-kimi-text: var(--text-on-background); + + /* Modal & Overlay Colors */ + --modal-bg: rgba(255, 107, 157, 0.95); + --modal-border: var(--primary-color); + --modal-header-bg: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + --modal-text: var(--text-on-primary); + --modal-title-color: var(--text-on-secondary); + --modal-overlay-bg: rgba(0, 0, 0, 0.8); + --modal-close-hover-bg: rgba(255, 255, 255, 0.2); + + /* Settings Panel & Tabs */ + --settings-bg: #181018; + --settings-text: var(--text-on-background); + --settings-tab-bg: #1a1a1a; + --settings-tab-color: #bfa6b6; + --settings-tab-hover-bg: rgba(255, 255, 255, var(--hover-opacity)); + --settings-tab-hover-color: rgba(255, 255, 255, 0.9); + --settings-tab-active-bg: var(--primary-color); + --settings-tab-active-color: var(--text-on-primary); + --settings-tab-border: var(--primary-color); + --settings-section-bg: #22121a; + --settings-section-border: rgba(255, 107, 157, var(--border-opacity)); + --settings-section-header-color: var(--text-on-background); + + /* Form Element Colors */ + --input-bg: rgba(255, 255, 255, 0.1); + --input-text: var(--text-on-background); + --input-border: var(--primary-color); + --input-focus-bg: rgba(255, 255, 255, var(--hover-opacity)); + --input-focus-border: var(--accent-color); + --input-placeholder: rgba(255, 255, 255, 0.6); + + /* Button Colors */ + --button-primary-bg: var(--primary-color); + --button-primary-text: var(--text-on-primary); + --button-primary-hover-bg: var(--accent-color); + --button-secondary-bg: rgba(255, 255, 255, 0.1); + --button-secondary-text: var(--text-on-background); + --button-secondary-hover-bg: rgba(255, 255, 255, var(--hover-opacity)); + + /* All other pink theme variables... */ + --slider-track-bg: rgba(255, 255, 255, 0.1); + --slider-track-active-bg: var(--primary-color); + --slider-thumb-bg: var(--primary-color); + --slider-thumb-hover-bg: var(--accent-color); + --slider-value-bg: #181018; + --slider-value-border: var(--primary-color); + --slider-value-color: var(--text-on-background); + + /* Toggle Switch */ + --switch-bg-inactive: rgba(255, 255, 255, 0.15); + --switch-bg-active: linear-gradient(90deg, var(--primary-color), var(--accent-color)); + + /* Mic Button & Pulse Effect */ + --mic-button-bg: var(--button-hover); + --mic-button-border: var(--primary-color); + --mic-button-shadow: 0 0 15px var(--primary-color); + --mic-button-hover-bg: var(--primary-color); + --mic-button-hover-shadow: var(--text-glow); + --mic-button-icon-color: var(--text-on-primary); + + /* Cards & Stats */ + --card-bg: rgba(255, 255, 255, 0.05); + --card-border: rgba(255, 255, 255, 0.1); + --card-hover-bg: rgba(255, 255, 255, 0.08); + --stat-value-color: var(--primary-color); + --stat-label-color: rgba(255, 255, 255, 0.7); + + /* Character Selection Colors */ + --character-card-bg: var(--card-bg); + --character-card-border: var(--card-border); + --character-card-hover-bg: var(--card-hover-bg); + --character-selected-border: var(--primary-color); + --character-selected-bg: rgba(255, 107, 157, 0.13); + --character-name-color: var(--text-on-background); +} + +[data-theme="blue"] { + --primary-color: #74b9ff; + --primary-rgb: 116, 185, 255; + --secondary-color: #81ecec; + --accent-color: #0984e3; + --background-overlay: rgba(116, 185, 255, 0.15); + --gradient-start: #74b9ff; + --gradient-end: #0984e3; + --text-glow: 0 0 10px rgba(116, 185, 255, 0.5); + --button-hover: rgba(116, 185, 255, 0.3); + --modal-title-color: #0a2340; + --switch-color: #3498db; + + /* UI Component Colors */ + --chat-bg: rgba(116, 185, 255, 0.9); + --chat-border: #74b9ff; + --chat-message-user-bg: #74b9ff; + --chat-message-kimi-bg: rgba(255, 255, 255, 0.15); + --input-border: #74b9ff; + --input-focus-border: #0984e3; + + /* Modal & Overlay Colors */ + --modal-bg: rgba(116, 185, 255, 0.95); + --modal-border: #74b9ff; + --modal-header-bg: linear-gradient(135deg, #74b9ff, #81ecec); + + /* Settings Panel & Tabs */ + --settings-tab-active-bg: #74b9ff; + --settings-section-border: #3a4a7a; + --settings-tab-border: #74b9ff; + + /* Slider Components */ + --slider-value-border: #0984e3; + + /* Toggle Switch */ + --switch-bg-active: linear-gradient(90deg, #74b9ff, #0984e3); + + /* Mic Button & Pulse Effect */ + --mic-button-border: #74b9ff; + --mic-button-shadow: 0 0 15px #74b9ff; + --mic-button-hover-bg: #74b9ff; + --mic-button-hover-shadow: 0 0 10px rgba(116, 185, 255, 0.5); + --mic-listening-border: #0984e3; + --mic-listening-shadow: 0 0 15px #0984e3; + --mic-pulse-color: rgba(9, 132, 227, 0.5); + + /* Cards & Stats */ + --stat-value-color: #0984e3; + + /* Plugin Cards */ + --plugin-card-border: #74b9ff; + --plugin-type-badge-bg: #0984e3; + --plugin-active-badge-bg: linear-gradient(90deg, #74b9ff, #0984e3); + + /* Help Modal */ + --creator-name-color: #0984e3; + --creator-avatar-bg: linear-gradient(135deg, #74b9ff, #81ecec); + --creator-role-bg: linear-gradient(135deg, #74b9ff, #81ecec); + --creator-role-color: #0a2340; + --philosophy-bg: rgba(116, 185, 255, 0.1); + --philosophy-border: rgba(116, 185, 255, 0.3); + --philosophy-border-left: #74b9ff; + --feature-icon-color: #0984e3; + --feature-title-color: #0984e3; + + /* Select Options */ + --select-option-hover-bg: #74b9ff; + --select-option-checked-bg: #0984e3; + + /* Model Colors */ + --model-strength-color: #0984e3; + --model-strength-text: #fff; + --model-provider-color: #00b894; + --model-provider-text: #fff; + + /* Text Colors */ + --text-primary: #222; + --text-secondary: #555; + + /* Character Selection Colors */ + --character-selected-border: #0984e3; + --character-selected-bg: rgba(116, 185, 255, 0.13); +} + +[data-theme="purple"] { + --primary-color: #a29bfe; + --primary-rgb: 162, 155, 254; + --secondary-color: #fd79a8; + --accent-color: #6c5ce7; + --background-overlay: rgba(162, 155, 254, 0.15); + --gradient-start: #a29bfe; + --gradient-end: #6c5ce7; + --text-glow: 0 0 10px rgba(162, 155, 254, 0.5); + --button-hover: rgba(162, 155, 254, 0.3); + --modal-title-color: #2d2250; + --switch-color: #a259ff; + + /* UI Component Colors */ + --chat-bg: rgba(162, 155, 254, 0.9); + --chat-border: #a29bfe; + --chat-message-user-bg: #a29bfe; + --chat-message-kimi-bg: rgba(255, 255, 255, 0.15); + --input-border: #a29bfe; + --input-focus-border: #6c5ce7; + + /* Modal & Overlay Colors */ + --modal-bg: rgba(162, 155, 254, 0.95); + --modal-border: #a29bfe; + --modal-header-bg: linear-gradient(135deg, #a29bfe, #fd79a8); + + /* Settings Panel & Tabs */ + --settings-tab-active-bg: #a29bfe; + --settings-section-border: #4a3a7a; + --settings-tab-border: #a29bfe; + --settings-bg: #2d2250; + + /* Slider Components */ + --slider-value-border: #6c5ce7; + + /* Toggle Switch */ + --switch-bg-active: linear-gradient(90deg, #a29bfe, #6c5ce7); + + /* Mic Button & Pulse Effect */ + --mic-button-border: #a29bfe; + --mic-button-shadow: 0 0 15px #a29bfe; + --mic-button-hover-bg: #a29bfe; + --mic-button-hover-shadow: 0 0 10px rgba(162, 155, 254, 0.5); + --mic-listening-border: #6c5ce7; + --mic-listening-shadow: 0 0 15px #6c5ce7; + --mic-pulse-color: rgba(108, 92, 231, 0.5); + + /* Cards & Stats */ + --stat-value-color: #6c5ce7; + + /* Plugin Cards */ + --plugin-card-border: #a29bfe; + --plugin-type-badge-bg: #6c5ce7; + --plugin-active-badge-bg: linear-gradient(90deg, #a29bfe, #6c5ce7); + + /* Help Modal */ + --creator-name-color: #6c5ce7; + --creator-avatar-bg: linear-gradient(135deg, #a29bfe, #fd79a8); + --creator-role-bg: linear-gradient(135deg, #a29bfe, #fd79a8); + --creator-role-color: #2d2250; + --philosophy-bg: rgba(162, 155, 254, 0.1); + --philosophy-border: rgba(162, 155, 254, 0.3); + --philosophy-border-left: #a29bfe; + --feature-icon-color: #6c5ce7; + --feature-title-color: #6c5ce7; + + /* Select Options */ + --select-option-hover-bg: #a29bfe; + --select-option-checked-bg: #6c5ce7; + + /* Model Colors */ + --model-strength-color: #6c5ce7; + --model-strength-text: #fff; + --model-provider-color: #fd79a8; + --model-provider-text: #fff; + + /* Text Colors */ + --text-primary: #2d2250; + --text-secondary: #555; + + /* Character Selection Colors */ + --character-selected-border: #6c5ce7; + --character-selected-bg: rgba(162, 155, 254, 0.13); + + /* Additional Purple Theme Variables */ + --button-primary-bg: var(--primary-color); + --button-primary-text: var(--text-on-primary); + --button-primary-hover-bg: var(--accent-color); + --button-secondary-bg: rgba(255, 255, 255, 0.1); + --button-secondary-text: var(--text-on-background); + --guide-step-number-color: var(--primary-color); + --guide-step-number-bg: rgba(162, 155, 254, var(--hover-opacity)); + --waiting-indicator-color: var(--primary-color); + --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color)); +} + +[data-theme="green"] { + --primary-color: #27ae60; + --primary-rgb: 39, 174, 96; + --secondary-color: #2ecc71; + --accent-color: #16a085; + --background-overlay: rgba(39, 174, 96, 0.15); + --gradient-start: #27ae60; + --gradient-end: #16a085; + --text-glow: 0 0 10px rgba(39, 174, 96, 0.5); + --button-hover: rgba(39, 174, 96, 0.3); + --modal-title-color: #1a3d2e; + --switch-color: #27ae60; + + /* Contrast and Accessibility for Green Theme */ + --text-on-primary: #ffffff; + --text-on-secondary: #1a3d2e; + --text-on-accent: #ffffff; + --text-on-background: #ffffff; + + /* UI Component Colors */ + --chat-bg: rgba(39, 174, 96, 0.9); + --chat-border: #27ae60; + --chat-text: var(--text-on-primary); + --chat-message-user-bg: #27ae60; + --chat-message-user-text: var(--text-on-primary); + --chat-message-kimi-bg: rgba(255, 255, 255, 0.15); + --chat-message-kimi-text: var(--text-on-background); + --input-border: #27ae60; + --input-focus-border: #16a085; + --input-text: var(--text-on-background); + + /* Modal & Overlay Colors */ + --modal-bg: rgba(39, 174, 96, 0.95); + --modal-border: #27ae60; + --modal-header-bg: linear-gradient(135deg, #27ae60, #2ecc71); + --modal-text: var(--text-on-primary); + + /* Settings Panel & Tabs */ + --settings-text: var(--text-on-background); + --settings-tab-active-bg: #27ae60; + --settings-tab-active-color: var(--text-on-primary); + --settings-section-border: #27ae60; + --settings-tab-border: #27ae60; + --settings-section-header-color: var(--text-on-background); + + /* Button Colors */ + --button-primary-bg: var(--primary-color); + --button-primary-text: var(--text-on-primary); + --button-primary-hover-bg: var(--accent-color); + --button-secondary-bg: rgba(255, 255, 255, 0.1); + --button-secondary-text: var(--text-on-background); + + /* Slider Components */ + --slider-value-border: #16a085; + --slider-thumb-bg: var(--primary-color); + --slider-thumb-hover-bg: var(--accent-color); + + /* Toggle Switch */ + --switch-bg-active: linear-gradient(90deg, #27ae60, #16a085); + + /* Mic Button & Pulse Effect */ + --mic-button-border: #27ae60; + --mic-button-shadow: 0 0 15px #27ae60; + --mic-button-hover-bg: #27ae60; + --mic-button-hover-shadow: 0 0 10px rgba(39, 174, 96, 0.5); + --mic-button-icon-color: var(--text-on-primary); + --mic-listening-border: #16a085; + --mic-listening-shadow: 0 0 15px #16a085; + --mic-pulse-color: rgba(22, 160, 133, 0.5); + --mic-pulse-listening-color: rgba(22, 160, 133, 0.4); + + /* Cards & Stats */ + --card-text: var(--text-on-background); + --stat-value-color: #16a085; + + /* Plugin Cards */ + --plugin-card-border: #27ae60; + --plugin-card-title-color: var(--text-on-background); + --plugin-card-desc-color: #e0cfe6; + --plugin-card-author-color: #bfa6b6; + --plugin-type-badge-bg: #16a085; + --plugin-type-badge-text: var(--text-on-accent); + --plugin-active-badge-bg: linear-gradient(90deg, #27ae60, #16a085); + --plugin-active-badge-text: var(--text-on-primary); + + /* Help Modal */ + --creator-name-color: #16a085; + --creator-avatar-bg: linear-gradient(135deg, #27ae60, #2ecc71); + --creator-role-bg: linear-gradient(135deg, #27ae60, #2ecc71); + --creator-role-color: var(--text-on-secondary); + --philosophy-bg: rgba(39, 174, 96, 0.1); + --philosophy-border: rgba(39, 174, 96, var(--border-opacity)); + --philosophy-border-left: #27ae60; + --feature-icon-color: #147190; + --feature-title-color: #147190; + --guide-step-number-color: var(--primary-color); + --guide-step-number-bg: rgba(39, 174, 96, var(--hover-opacity)); + + /* Select Options */ + --select-option-hover-bg: #27ae60; + --select-option-checked-bg: #16a085; + + /* Model Colors */ + --model-strength-color: #27ae60; + --model-strength-text: var(--text-on-primary); + --model-provider-color: #2ecc71; + --model-provider-text: var(--text-on-primary); + + /* Text Colors */ + --text-primary: var(--text-on-background); + --text-secondary: #4e6151; + --text-muted: rgba(255, 255, 255, 0.6); + + /* Character Selection Colors */ + --character-selected-border: #16a085; + --character-selected-bg: rgba(39, 174, 96, 0.13); + --character-name-color: var(--text-on-background); + + /* Additional Green Theme Variables */ + --waiting-indicator-color: var(--primary-color); + --loading-spinner-color: var(--primary-color); + --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color)); +} + +[data-theme="dark"] { + --primary-color: #5e60ce; + --primary-rgb: 94, 96, 206; + --secondary-color: #23262f; + --accent-color: #8b5cf6; + --background-overlay: rgba(24, 26, 32, 0.85); + --gradient-start: #5e60ce; + --gradient-end: #8b5cf6; + --text-glow: 0 0 10px rgba(94, 96, 206, 0.3); + --button-hover: rgba(94, 96, 206, 0.15); + --modal-title-color: #e0e0e0; + --switch-color: #5e60ce; + + /* Contrast and Accessibility for Dark Theme */ + --text-on-primary: #ffffff; + --text-on-secondary: #e0e0e0; + --text-on-accent: #ffffff; + --text-on-background: #e0e0e0; + + /* UI Component Colors */ + --chat-bg: rgba(24, 26, 32, 0.95); + --chat-border: #5e60ce; + --chat-text: var(--text-on-background); + --chat-message-user-bg: #1e253c; + --chat-message-user-text: var(--text-on-background); + --chat-message-kimi-bg: #23262f; + --chat-message-kimi-text: var(--text-on-background); + --input-border: #5e60ce; + --input-focus-border: #8b5cf6; + --input-text: var(--text-on-background); + + /* Modal & Overlay Colors */ + --modal-bg: rgba(24, 26, 32, 0.98); + --modal-border: #5e60ce; + --modal-header-bg: linear-gradient(135deg, #5e60ce, #8b5cf6); + --modal-text: var(--text-on-background); + + /* Settings Panel & Tabs */ + --settings-bg: #0f1114; + --settings-text: var(--text-on-background); + --settings-tab-bg: #181a20; + --settings-tab-active-bg: #5e60ce; + --settings-tab-active-color: var(--text-on-primary); + --settings-section-bg: #1a1d23; + --settings-section-border: rgba(94, 96, 206, var(--border-opacity)); + --settings-tab-border: #5e60ce; + --settings-section-header-color: var(--text-on-background); + + /* Button Colors */ + --button-primary-bg: var(--primary-color); + --button-primary-text: var(--text-on-primary); + --button-primary-hover-bg: var(--accent-color); + --button-secondary-bg: rgba(255, 255, 255, 0.05); + --button-secondary-text: var(--text-on-background); + + /* Slider Components */ + --slider-value-bg: #0f1114; + --slider-value-border: #5e60ce; + --slider-value-color: var(--text-on-background); + --slider-thumb-bg: var(--primary-color); + --slider-thumb-hover-bg: var(--accent-color); + + /* Toggle Switch */ + --switch-bg-inactive: rgba(255, 255, 255, 0.05); + --switch-bg-active: linear-gradient(90deg, #5e60ce, #8b5cf6); + + /* Mic Button & Pulse Effect */ + --mic-button-border: #5e60ce; + --mic-button-shadow: 0 0 15px #5e60ce; + --mic-button-hover-bg: #5e60ce; + --mic-button-hover-shadow: 0 0 10px rgba(94, 96, 206, 0.5); + --mic-button-icon-color: var(--text-on-primary); + --mic-listening-border: #8b5cf6; + --mic-listening-shadow: 0 0 15px #8b5cf6; + --mic-pulse-color: rgba(139, 92, 246, 0.5); + --mic-pulse-listening-color: rgba(139, 92, 246, 0.4); + + /* Cards & Stats */ + --card-bg: rgba(255, 255, 255, 0.02); + --card-border: rgba(255, 255, 255, 0.05); + --card-hover-bg: rgba(255, 255, 255, 0.05); + --card-text: var(--text-on-background); + --stat-value-color: #8b5cf6; + + /* Plugin Cards */ + --plugin-card-bg: linear-gradient(135deg, #1a1d23 80%, rgba(24, 26, 32, 0.98) 100%); + --plugin-card-border: #5e60ce; + --plugin-card-title-color: var(--text-on-background); + --plugin-card-desc-color: rgba(224, 224, 224, 0.7); + --plugin-card-author-color: rgba(224, 224, 224, 0.5); + --plugin-type-badge-bg: #8b5cf6; + --plugin-type-badge-text: var(--text-on-accent); + --plugin-active-badge-bg: linear-gradient(90deg, #5e60ce, #8b5cf6); + --plugin-active-badge-text: var(--text-on-primary); + + /* Help Modal */ + --help-modal-bg: rgba(24, 26, 32, 0.98); + --help-modal-border: #5e60ce; + --creator-card-bg: rgba(255, 255, 255, 0.02); + --creator-card-border: rgba(255, 255, 255, 0.05); + --creator-name-color: #8b5cf6; + --creator-avatar-bg: linear-gradient(135deg, #5e60ce, #8b5cf6); + --creator-role-bg: linear-gradient(135deg, #5e60ce, #8b5cf6); + --creator-role-color: var(--text-on-primary); + --philosophy-bg: rgba(94, 96, 206, 0.05); + --philosophy-border: rgba(94, 96, 206, var(--border-opacity)); + --philosophy-border-left: #5e60ce; + --feature-item-bg: rgba(255, 255, 255, 0.02); + --feature-item-border: rgba(94, 96, 206, var(--border-opacity)); + --feature-icon-color: #8b5cf6; + --feature-title-color: #8b5cf6; + --feature-text-color: rgba(224, 224, 224, 0.7); + --guide-step-bg: rgba(255, 255, 255, 0.02); + --guide-step-number-color: var(--primary-color); + --guide-step-number-bg: rgba(94, 96, 206, var(--hover-opacity)); + --tip-item-bg: rgba(255, 255, 255, 0.02); + + /* Select Options */ + --select-option-bg: rgba(24, 26, 32, 0.95); + --select-option-text: var(--text-on-background); + --select-option-hover-bg: #5e60ce; + --select-option-checked-bg: #8b5cf6; + + /* Model Colors */ + --model-card-bg: rgba(255, 255, 255, 0.02); + --model-card-border: rgba(255, 255, 255, 0.05); + --model-card-hover-bg: rgba(255, 255, 255, 0.05); + --model-card-selected-border: var(--primary-color); + --model-name-color: var(--text-on-background); + --model-description-color: rgba(224, 224, 224, 0.7); + --model-strength-color: #5e60ce; + --model-strength-text: var(--text-on-primary); + --model-provider-color: #8b5cf6; + --model-provider-text: var(--text-on-accent); + + /* Text Colors */ + --text-primary: var(--text-on-background); + --text-secondary: #9ca3af; + --text-muted: rgba(224, 224, 224, 0.6); + + /* Character Selection Colors */ + --character-card: rgba(255, 255, 255, 0.02); + --character-card-border: rgba(255, 255, 255, 0.05); + --character-card-hover-bg: rgba(255, 255, 255, 0.05); + --character-selected-border: #8b5cf6; + --character-selected-bg: rgba(94, 96, 206, 0.1); + --character-name-color: var(--text-on-background); + + /* Additional Dark Theme Variables */ + --waiting-indicator-color: var(--primary-color); + --loading-spinner-color: var(--primary-color); + --progress-bg: rgba(255, 255, 255, 0.05); + --progress-fill-bg: linear-gradient(90deg, var(--primary-color), var(--accent-color)); + --progress-text-color: var(--text-on-background); + --transcript-bg: rgba(0, 0, 0, 0.9); + --transcript-text: var(--text-on-background); + --transcript-border: var(--primary-color); +} + +/* ===== ANIMATION MANAGEMENT ===== */ +/* Respect user's reduced motion preference. When user requests reduced motion, + disable animations and transitions globally while preserving critical + animations (mic button, loading screen). This preserves accessibility + without relying on a UI toggle. */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation: none !important; + transition: none !important; + } + + /* Keep mic button animations even when reduced-motion is requested */ + .mic-button, + .mic-button *, + .mic-button::after { + animation: revert !important; + transition: revert !important; + } + + /* Keep loading screen animations */ + #loading-screen, + #loading-screen * { + animation: revert !important; + transition: revert !important; + } +} + +/* Ensure critical hover effects remain functional */ +body.animations-enabled .kimi-button:hover, +body.animations-enabled .control-button-unified:hover { + transform: translateY(-2px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +body.animations-enabled .mic-button:hover { + transform: scale(1.1); + transition: all 0.2s ease; +} + +/* ===== LOADING SCREEN ===== */ +#loading-screen { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: var(--background-primary, #1a1a1a); + z-index: 10000; + display: flex; + justify-content: center; + align-items: center; + opacity: 1; + transition: opacity 0.5s ease-out; +} + +#loading-screen img { + max-width: 200px; + max-height: 200px; + animation: loadingPulse 2s infinite ease-in-out; +} + +@keyframes loadingPulse { + 0%, + 100% { + opacity: 0.7; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.05); + } +} + +/* ===== GLOBAL STYLES ===== */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + color: white; + overflow: hidden; +} + +/* === Global Help Button (compressed) === */ +.top-right-buttons { + position: fixed; + top: 5px; + right: 5px; + display: flex; + gap: 5px; + z-index: 1000; + pointer-events: none; +} + +/* Configurable design tokens for quick tuning */ +#global-help-button { + --help-btn-bg-alpha: 0.12; /* base background alpha */ + --help-btn-bg-hover-alpha: 0.2; /* hover/focus background alpha */ + --help-btn-bg-active-alpha: 0.25; /* active press background alpha */ + --help-btn-icon-alpha: 0.3; /* base icon opacity */ + --help-btn-icon-hover-alpha: 0.65; /* hover/focus icon opacity */ + --help-btn-icon-active-alpha: 0.8; /* active icon opacity */ + --help-btn-border-alpha: 0.28; /* border alpha */ + --help-btn-shadow-base: 0 2px 6px rgba(var(--primary-rgb), 0.12); + --help-btn-shadow-hover: 0 4px 16px rgba(var(--primary-rgb), 0.28); + width: 36px; + height: 36px; + pointer-events: auto; + background: rgba(var(--primary-rgb), var(--help-btn-bg-alpha)); + border: 1px solid rgba(var(--primary-rgb), var(--help-btn-border-alpha)); + box-shadow: var(--help-btn-shadow-base); + backdrop-filter: blur(9px) saturate(115%); + opacity: 0.26; + display: flex; + align-items: center; + justify-content: center; + transition: + background 0.35s ease, + box-shadow 0.35s ease, + transform 0.35s ease, + opacity 0.4s ease; +} +#global-help-button i { + font-size: 1.2rem; + opacity: var(--help-btn-icon-alpha); + transition: + opacity 0.4s ease, + transform 0.35s ease; + transform: scale(0.9); +} + +/* Hover + focus + container hover unify */ +#global-help-button:hover, +#global-help-button:focus-visible, +.top-right-buttons:hover #global-help-button { + background: rgba(var(--primary-rgb), var(--help-btn-bg-hover-alpha)); + box-shadow: var(--help-btn-shadow-hover); + opacity: 1; +} + +#global-help-button:hover i, +#global-help-button:focus-visible i, +.top-right-buttons:hover #global-help-button i { + opacity: var(--help-btn-icon-hover-alpha); + transform: scale(1); +} + +#global-help-button:active { + background: rgba(var(--primary-rgb), var(--help-btn-bg-active-alpha)); + transform: scale(0.95); +} +#global-help-button:active i { + opacity: var(--help-btn-icon-active-alpha); +} +#global-help-button:focus-visible { + outline: 2px solid rgba(var(--primary-rgb), 0.6); + outline-offset: 2px; +} + +.video-container { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: -1; + background-color: #1a1a1a; +} + +.bg-video.active { + opacity: 1; +} + +.bg-video { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + object-fit: contain; + opacity: 0; + transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1); + background-color: #1a1a1a; + will-change: opacity; + backface-visibility: hidden; +} + +.bg-video.transitioning { + opacity: 0; + transition: opacity var(--video-fade-duration) cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; +} + +.content-overlay { + position: relative; + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + padding: 20px; + background-color: var(--background-overlay); + opacity: var(--interface-opacity); + z-index: 1; +} + +.top-bar { + width: 100%; + max-width: 500px; + text-align: center; + margin-top: 10px; +} + +.top-bar label { + font-size: 1rem; + font-weight: 600; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5); + margin-bottom: 8px; + display: block; +} + +.progress-container { + width: 100%; + height: 12px; + background-color: var(--progress-bg); + border-radius: 10px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + width: 50%; /* Changed from 65% to match new default favorability level */ + background: var(--progress-fill-bg); + border-radius: 10px; + transition: width 0.5s ease-in-out; + box-shadow: var(--text-glow); +} + +/* Center content styles can be added here if needed */ + +.transcript-container { + position: absolute; + bottom: 180px; + left: 50%; + transform: translateX(-50%); + width: 80%; + max-width: 580px; + min-width: 280px; + max-height: 400px; + min-height: 100px; + padding: 15px; + background: var(--transcript-bg); + backdrop-filter: blur(10px); + border-radius: 10px; + border: 1px solid var(--transcript-border); + text-align: center; + opacity: 0; + transition: opacity 0.3s ease-in-out; + pointer-events: none; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + overflow-y: auto; + overflow-x: hidden; + z-index: 100; +} + +.transcript-container.visible { + opacity: 1; + pointer-events: auto; +} + +/* Custom scrollbar for transcript container */ +.transcript-container::-webkit-scrollbar { + width: var(--scrollbar-width); +} + +.transcript-container::-webkit-scrollbar-track { + background: var(--scrollbar-track-bg); + border-radius: 4px; +} + +.transcript-container::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb-bg); + border-radius: 4px; + transition: background 0.3s ease; +} + +.transcript-container::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover-bg); +} + +#transcript { + font-size: 1.2rem; + color: var(--transcript-text); + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); + margin: 0; + line-height: 1.3; + text-align: left; +} + +/* ===== ACCESSIBILITY - FOCUS STYLES ===== */ +select:focus, +input:focus, +button:focus, +.kimi-slider:focus, +.kimi-slider-unified:focus { + box-shadow: 0 0 0 2px var(--primary-pink); + border-color: var(--primary-pink); +} + +.control-button-unified:focus { + outline: 2px solid var(--primary-pink); + outline-offset: 2px; +} + +/* ===== CHAT INTERFACE ===== */ +.chat-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; + width: 400px; + max-width: calc(100vw - 40px); + height: 600px; + max-height: 80vh; + background: var(--chat-bg); + backdrop-filter: blur(20px); + border-radius: 15px; + border: 1px solid var(--chat-border); + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); + display: none; + flex-direction: column; + overflow: hidden; + transform: translateX(400px); + opacity: 0; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.chat-container.visible { + display: flex; + transform: translateX(0); + opacity: 1; +} + +.chat-header { + background: var(--chat-header-bg); + padding: 15px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.chat-header h3 { + margin: 0; + color: var(--chat-text); + font-size: 1.1rem; + display: flex; + align-items: center; + gap: 8px; +} + +.chat-messages { + flex: 1; + padding: 15px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; +} + +.message { + max-width: 95%; + padding: 12px 16px; + border-radius: 18px; + font-size: 0.95rem; + line-height: 1.3; /* Espacement entre lignes dans un même paragraphe */ + white-space: normal; /* Plus besoin de pre-line avec les

*/ + animation: messageSlideIn 0.3s ease-out; +} + +/* Contrôle de l'espacement entre paragraphes (sauts de ligne) */ +.message p { + margin: 0 0 0.8em 0; /* Espacement entre paragraphes */ +} + +.message p:last-child { + margin-bottom: 0; /* Pas d'espacement après le dernier paragraphe */ +} + +.message.user { + align-self: flex-end; + background: var(--chat-message-user-bg); + color: var(--chat-message-user-text); +} + +.message.kimi { + align-self: flex-start; + background: var(--chat-message-kimi-bg); + color: var(--chat-message-kimi-text); +} + +.message-time { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.6); + margin-top: 4px; + text-align: right; +} + +.delete-message-btn { + background: none; + border: none; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + font-size: 0.7rem; + margin-left: 8px; + transition: all 0.2s ease; +} + +.delete-message-btn:hover { + color: #ff4757 !important; + background: rgba(255, 71, 87, 0.1) !important; +} + +.chat-input-container { + padding: 15px 20px; + display: flex; + gap: 10px; + background: rgba(255, 255, 255, 0.05); + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +#chat-input { + flex: 1; + background: var(--chat-input-bg); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 20px; + padding: 10px 15px; + color: var(--chat-input-text); + font-size: 0.9rem; + outline: none; + transition: all 0.3s ease; + + /* Make textarea behave like the previous single-line input */ + box-sizing: border-box; + resize: none; /* prevent manual resizing */ + /* show approximately 2 lines by default, allow up to ~4 lines */ + min-height: 58px; + max-height: 160px; /* allow multi-line but limit growth */ + line-height: 1.2; + overflow: auto; +} + +#chat-input::placeholder { + color: var(--chat-input-placeholder); +} + +#chat-input:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(255, 107, 157, 0.2); +} + +#send-button { + background: var(--primary-color); + border: none; + border-radius: 20px; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: white; + cursor: pointer; + transition: all 0.3s ease; +} + +#send-button:hover { + background: var(--accent-color); + transform: scale(1.05); +} + +.chat-toggle, +.chat-delete { + background: none; + border: none; + color: var(--chat-text); + cursor: pointer; + padding: 5px; + border-radius: 5px; + transition: all 0.3s ease; +} + +.chat-delete { + color: rgba(255, 255, 255, 0.7); +} + +.chat-delete:hover { + color: #ff4757; + background: rgba(255, 71, 87, 0.1); +} + +.chat-toggle:hover { + background: rgba(255, 255, 255, 0.1); +} + +/* ===== UNIFIED BUTTON COMPONENTS ===== */ +.kimi-button, +.control-button-unified { + background: var(--button-primary-bg); + border: none; + border-radius: 8px; + color: var(--button-primary-text); + padding: 10px 20px; + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); + position: relative; + overflow: hidden; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.kimi-button::before, +.control-button-unified::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); + transition: left 0.5s ease; +} + +.kimi-button:hover, +.control-button-unified:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); + background: var(--button-primary-hover-bg); +} + +.kimi-button:hover::before, +.control-button-unified:hover::before { + left: 100%; +} + +.kimi-button:active, +.control-button-unified:active { + transform: translateY(0); + transition: all 0.1s ease; +} + +.kimi-button:disabled, +.control-button-unified:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +/* Button Variants */ +.kimi-button.danger { + background: var(--button-danger-bg); + color: var(--button-danger-text); +} +.kimi-button.danger:hover { + background: var(--button-danger-hover-bg); +} +.kimi-button.success { + background: linear-gradient(135deg, #26de81, #20bf6b); +} +.kimi-button.success:hover { + background: linear-gradient(135deg, #20bf6b, #26de81); +} +.kimi-button.secondary { + background: var(--button-secondary-bg); + color: var(--button-secondary-text); +} +.kimi-button.secondary:hover { + background: var(--button-secondary-hover-bg); +} + +/* Circular Control Buttons */ +.control-button-unified { + width: 50px; + height: 50px; + border-radius: 50%; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + background: var(--button-hover); + box-shadow: var(--mic-button-shadow); + border: 1px solid var(--primary-color); +} + +.control-button-unified:hover { + transform: translateY(-2px) scale(1.05); + background: var(--primary-color); + box-shadow: var(--text-glow); +} + +.control-button-unified i { + font-size: 1.2rem; + transition: transform 0.3s ease; +} + +.control-button-unified:hover i { + transform: scale(1.1); +} + +/* ===== UNIFIED FORM COMPONENTS ===== */ + +/* Select Elements */ +.kimi-select, +.kimi-select-unified { + background: var(--select-bg); + border: 1px solid var(--select-border); + border-radius: 8px; + color: var(--select-text); + padding: 8px 12px; + font-size: 0.9rem; + outline: none; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + cursor: pointer; + width: 100%; + box-sizing: border-box; +} + +.kimi-select:hover, +.kimi-select-unified:hover { + background: var(--input-focus-bg); + border-color: var(--accent-color); + box-shadow: 0 0 10px var(--primary-color); +} + +.kimi-select:focus, +.kimi-select-unified:focus { + background: var(--input-focus-bg); + border-color: var(--input-focus-border); + box-shadow: 0 0 15px var(--primary-color); +} + +.kimi-select option, +.kimi-select-unified option { + background: var(--select-option-bg); + color: var(--select-option-text); + padding: 12px 15px; + border: none; + font-size: 0.9rem; + transition: all 0.3s ease; +} + +.kimi-select option:hover, +.kimi-select-unified option:hover, +.kimi-select option:focus, +.kimi-select-unified option:focus { + background: var(--select-option-hover-bg); + color: var(--select-option-hover-text); +} + +.kimi-select option:checked, +.kimi-select-unified option:checked, +.kimi-select option:selected, +.kimi-select-unified option:selected { + background: var(--select-option-checked-bg); + color: var(--select-option-checked-text); + font-weight: 600; + box-shadow: 0 0 10px var(--primary-color); +} + +/* Input Elements */ +.kimi-input, +.kimi-input-unified { + background: var(--input-bg); + border: 1px solid var(--input-border); + border-radius: 8px; + color: var(--input-text); + padding: 8px 12px; + font-size: 0.9rem; + outline: none; + transition: all 0.3s ease; + backdrop-filter: blur(10px); + width: 100%; + box-sizing: border-box; +} + +.kimi-input:focus, +.kimi-input-unified:focus { + background: var(--input-focus-bg); + border-color: var(--input-focus-border); + box-shadow: 0 0 15px var(--primary-color); +} + +.kimi-input::placeholder, +.kimi-input-unified::placeholder { + color: var(--input-placeholder); +} + +/* Slider Elements */ +.kimi-slider, +.kimi-slider-unified { + -webkit-appearance: none; + appearance: none; + height: 6px; + border-radius: 3px; + background: var(--slider-track-bg); + outline: none; + transition: all 0.3s ease; + cursor: pointer; +} + +.kimi-slider::-webkit-slider-thumb, +.kimi-slider-unified::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--slider-thumb-bg); + cursor: pointer; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; +} + +.kimi-slider::-webkit-slider-thumb:hover, +.kimi-slider-unified::-webkit-slider-thumb:hover { + transform: scale(1.2); + background: var(--slider-thumb-hover-bg); + box-shadow: + 0 4px 15px rgba(0, 0, 0, 0.4), + var(--text-glow); +} + +.kimi-slider::-moz-range-thumb, +.kimi-slider-unified::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--slider-thumb-bg); + cursor: pointer; + border: none; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); + transition: all 0.3s ease; +} + +.kimi-slider::-moz-range-thumb:hover, +.kimi-slider-unified::-moz-range-thumb:hover { + transform: scale(1.2); + background: var(--slider-thumb-hover-bg); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4); +} + +/* ===== CONSOLIDATED SCROLLBAR SYSTEM ===== */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-bg) var(--scrollbar-track-bg); +} + +*::-webkit-scrollbar { + width: var(--scrollbar-width); + height: var(--scrollbar-width); +} + +*::-webkit-scrollbar-track { + background: var(--scrollbar-track-bg); + border-radius: 4px; +} + +*::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb-bg); + border-radius: 4px; + transition: all 0.3s ease; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +*::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover-bg); + transform: scale(1.1); +} + +*::-webkit-scrollbar-thumb:active { + background: var(--scrollbar-thumb-active-bg); +} + +*::-webkit-scrollbar-corner { + background: var(--scrollbar-corner-bg); +} + +/* ===== MAIN LAYOUT AND CONTROLS ===== */ +.control-buttons { + display: flex; + justify-content: center; + align-items: center; + gap: 20px; + margin-bottom: 10px; +} + +.bottom-bar { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + position: relative; +} + +.favorability-text { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + font-size: 0.85rem; + font-weight: 600; + color: var(--progress-text-color); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + +.progress-container { + position: relative; +} + +.mic-button { + position: relative; + width: 90px; + height: 90px; + background: var(--mic-button-bg); + backdrop-filter: blur(10px); + border: 1px solid var(--mic-button-border); + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: var(--mic-button-shadow); +} + +.mic-button:not(.is-listening)::after { + display: none; +} +.mic-button.is-listening.mic-pulse-active::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 120px; + height: 120px; + border-radius: 50%; + background: var(--mic-pulse-color); + opacity: 0.6; + z-index: -1; + animation: micPulseRed 1.2s infinite cubic-bezier(0.66, 0, 0, 1); + pointer-events: none; +} + +.mic-button:hover { + transform: scale(1.1); + background: var(--mic-button-hover-bg); + box-shadow: var(--mic-button-hover-shadow); +} + +.mic-button:active { + transform: scale(0.95); +} + +.mic-button i { + font-size: 28px; + color: var(--mic-button-icon-color); + transition: all 0.3s ease; +} + +.mic-button.is-listening { + animation: pulse 1.5s infinite; + border: 1px solid #27ae60; + box-shadow: 0 0 15px #27ae60; +} + +.mic-button.is-listening i { + animation: micPulse 0.8s infinite alternate; +} + +.mic-pulse-active { + position: relative; + box-shadow: 0 0 0 0 var(--primary-color); + animation: micPulse 1.2s infinite cubic-bezier(0.66, 0, 0, 1); +} + +.mic-button.mic-pulse-active { + position: relative; + z-index: 1; +} +.mic-button.mic-pulse-active::after { + content: ""; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + width: 120px; + height: 120px; + border-radius: 50%; + background: var(--mic-pulse-color); + opacity: 0.6; + z-index: -1; + animation: micPulseRed 1.2s infinite cubic-bezier(0.66, 0, 0, 1); + pointer-events: none; +} +@keyframes micPulseRed { + 0% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0.6; + } + 70% { + transform: translate(-50%, -50%) scale(1.2); + opacity: 0; + } + 100% { + transform: translate(-50%, -50%) scale(0.8); + opacity: 0; + } +} + +/* ===== LARGE SCREENS OPTIMIZATION ===== */ +@media (min-width: 1200px) { + .chat-container { + width: 400px; + height: 600px; + right: 30px; + top: 30px; + } +} + +/* ===== CONSOLIDATED RESPONSIVE DESIGN ===== */ +@media (max-width: 768px) { + .content-overlay { + padding: 20px; + } + + .chat-container { + width: 400px; + max-width: calc(100vw - 30px); + top: 15px; + right: 15px; + } + + .control-buttons { + gap: 15px; + } + + .control-button-unified { + width: 45px; + height: 45px; + } + + .control-button-unified i { + font-size: 1.1rem; + } + + .top-bar { + margin-top: 15px; + } + + .top-bar label { + font-size: 0.9rem; + } + + .progress-container { + height: 12px; + } + + .mic-button { + width: 80px; + height: 80px; + } + + .mic-button i { + font-size: 34px; + } + + .transcript-container { + bottom: 200px; + width: 90%; + max-height: 400px; + padding: 12px; + } + + #transcript { + font-size: 1rem; + line-height: 1.4; + } + + .message { + max-width: 85%; + padding: 10px 14px; + font-size: 0.9rem; + } + + .favorability-text { + font-size: 0.75rem; + } + + .control-buttons { + gap: 10px; + justify-content: space-around; + } + + .kimi-select, + .kimi-select-unified, + .kimi-input, + .kimi-input-unified { + font-size: 16px; /* Prevents zoom on iOS */ + } +} + +@media (max-width: 600px) { + .bg-video { + object-fit: cover; + object-position: center center; + } + + .content-overlay { + padding: 10px; + } + + .control-buttons { + gap: 15px; + } + + .chat-button, + .settings-button { + width: 50px; + height: 50px; + } + + .chat-button i, + .settings-button i { + font-size: 20px; + } + + .top-bar { + margin-top: 15px; + } + + .top-bar label { + font-size: 0.9rem; + } + + .progress-container { + height: 12px; + } + + .mic-button { + width: 80px; + height: 80px; + } + + .mic-button i { + font-size: 34px; + } + + .transcript-container { + bottom: 180px; + width: 95%; + max-height: 300px; + padding: 10px; + left: 50%; + transform: translateX(-50%); + } + + #transcript { + font-size: 0.9rem; + line-height: 1.3; + } + + .message { + max-width: 92%; + padding: 10px 14px; + font-size: 0.9rem; + } + + .favorability-text { + font-size: 0.75rem; + } + + .chat-container { + top: 10px; + right: 10px; + left: 10px; + width: auto; + height: calc(100vh - 20px); + transform: translateY(-100vh); + transition: all 0.25s ease-out; + } + + .chat-container.visible { + transform: translateY(0); + } +} + +/* ===== TABLET SPECIFIC STYLES ===== */ +@media (min-width: 601px) and (max-width: 1024px) { + .transcript-container { + bottom: 200px; + width: 85%; + max-height: 350px; + padding: 15px; + max-width: 500px; + } + + #transcript { + font-size: 1.1rem; + line-height: 1.4; + } +} + +/* ===== VERY SMALL SCREENS ===== */ +@media (max-width: 400px) { + .transcript-container { + bottom: 160px; + width: 98%; + max-height: 250px; + padding: 8px; + border-radius: 8px; + } + + #transcript { + font-size: 0.85rem; + line-height: 1.3; + } +} + +/* ===== VERY LARGE SCREENS ===== */ +@media (min-width: 1400px) { + .transcript-container { + max-width: 600px; + max-height: 500px; + padding: 20px; + bottom: 200px; + } + + #transcript { + font-size: 1.3rem; + line-height: 1.5; + } +} + +/* ===== LANDSCAPE MODE ON MOBILE ===== */ +@media (max-width: 768px) and (orientation: landscape) { + .transcript-container { + bottom: 120px; + max-height: 200px; + width: 70%; + max-width: 400px; + } + + #transcript { + font-size: 0.9rem; + line-height: 1.3; + } +} + +/* Animation pour l'indicateur d'attente */ +.waiting-indicator { + display: block; + text-align: center; + width: 100%; + box-sizing: border-box; + margin: 6px 0 4px 0; /* discret au-dessus de l'input */ + opacity: 0; + transition: opacity 0.25s ease-in-out; + pointer-events: none; +} +.waiting-indicator.visible { + opacity: 1; +} +.waiting-indicator span { + display: inline-block; + width: 8px; + height: 8px; + margin: 0 2px; + background: var(--waiting-indicator-color); + border-radius: 50%; + opacity: 0.5; + animation: waiting-bounce 1.4s infinite both; +} +.waiting-indicator span:nth-child(2) { + animation-delay: 0.2s; +} +.waiting-indicator span:nth-child(3) { + animation-delay: 0.4s; +} +@keyframes waiting-bounce { + 0%, + 80%, + 100% { + transform: scale(0.7); + opacity: 0.5; + } + 40% { + transform: scale(1); + opacity: 1; + } +} + +/* Global typing indicator near mic button */ +.global-typing-indicator { + display: none; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + margin: 0 6px; + border-radius: 18px; + background: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(6px); + transition: opacity 0.25s ease-in-out; + opacity: 0; +} +.global-typing-indicator.visible { + display: inline-flex; + opacity: 1; +} +.global-typing-indicator span { + display: inline-block; + width: 6px; + height: 6px; + margin: 0 1.5px; + background: var(--waiting-indicator-color); + border-radius: 50%; + opacity: 0.6; + animation: waiting-bounce 1.4s infinite both; +} +.global-typing-indicator span:nth-child(2) { + animation-delay: 0.2s; +} +.global-typing-indicator span:nth-child(3) { + animation-delay: 0.4s; +} + +/* Animation pour les messages du chat */ +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/kimi-icons/2blanche.jpg b/kimi-icons/2blanche.jpg new file mode 100644 index 0000000000000000000000000000000000000000..58fa6e63f48088c7b0b6e07f081a8fc73c7a6e93 --- /dev/null +++ b/kimi-icons/2blanche.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:131c1fc97b8f59bc34dc4e53df7541493e9535ea6985df03bb4b7d3a65440ab9 +size 157272 diff --git a/kimi-icons/bella.jpg b/kimi-icons/bella.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1c477c2e7fb819bace6ef3ea098d6b04953f4441 --- /dev/null +++ b/kimi-icons/bella.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:489b680a99052ec46b18f8dd669fcc8094f2d434f577983703c98bca7ef44fdc +size 157098 diff --git a/kimi-icons/favicons/apple-touch-icon-180x180.png b/kimi-icons/favicons/apple-touch-icon-180x180.png new file mode 100644 index 0000000000000000000000000000000000000000..ae56c7b70cfbe5c547e7c1c74ecd8b397ed9e9a1 Binary files /dev/null and b/kimi-icons/favicons/apple-touch-icon-180x180.png differ diff --git a/kimi-icons/favicons/favicon-128x128.png b/kimi-icons/favicons/favicon-128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..c6745caeba7674b6a54a88cd93364af21e685c10 Binary files /dev/null and b/kimi-icons/favicons/favicon-128x128.png differ diff --git a/kimi-icons/favicons/favicon-16x16.png b/kimi-icons/favicons/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..e04b5ef89f4864903756e709a3ed4c25ee58a9a2 Binary files /dev/null and b/kimi-icons/favicons/favicon-16x16.png differ diff --git a/kimi-icons/favicons/favicon-192x192.png b/kimi-icons/favicons/favicon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..ae56c7b70cfbe5c547e7c1c74ecd8b397ed9e9a1 Binary files /dev/null and b/kimi-icons/favicons/favicon-192x192.png differ diff --git a/kimi-icons/favicons/favicon-32x32.png b/kimi-icons/favicons/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..e04b5ef89f4864903756e709a3ed4c25ee58a9a2 Binary files /dev/null and b/kimi-icons/favicons/favicon-32x32.png differ diff --git a/kimi-icons/favicons/favicon-48x48.png b/kimi-icons/favicons/favicon-48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..25adc5376ea7cf02949545f2a6044b05e51304f0 Binary files /dev/null and b/kimi-icons/favicons/favicon-48x48.png differ diff --git a/kimi-icons/favicons/favicon-64x64.png b/kimi-icons/favicons/favicon-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..25adc5376ea7cf02949545f2a6044b05e51304f0 Binary files /dev/null and b/kimi-icons/favicons/favicon-64x64.png differ diff --git a/kimi-icons/favicons/favicon-96x96.png b/kimi-icons/favicons/favicon-96x96.png new file mode 100644 index 0000000000000000000000000000000000000000..c6745caeba7674b6a54a88cd93364af21e685c10 Binary files /dev/null and b/kimi-icons/favicons/favicon-96x96.png differ diff --git a/kimi-icons/jasmine.jpg b/kimi-icons/jasmine.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16aea54ffecfe0c8ca939a76dc513edcc671d9f9 --- /dev/null +++ b/kimi-icons/jasmine.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:328909e5248d0eedca3a4ea1442036aff5c8adb3795463b9dc0e946603f97d33 +size 177122 diff --git a/kimi-icons/july.jpg b/kimi-icons/july.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4646a3380d02d9139e1195b5399b453b354f7116 --- /dev/null +++ b/kimi-icons/july.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b80f83326304e39d275275eb5ee400a8c3b6f12fa156c1fd45d77bc91608c994 +size 155691 diff --git a/kimi-icons/kimi-loading.png b/kimi-icons/kimi-loading.png new file mode 100644 index 0000000000000000000000000000000000000000..7db41e0bc129e0b28ed05f600b870a8f753b2242 --- /dev/null +++ b/kimi-icons/kimi-loading.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5562428cc5e7f0b4c67a3d3df602e057358179d7920233aac1317b6988586e0e +size 119981 diff --git a/kimi-icons/kimi.jpg b/kimi-icons/kimi.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8953c03f1256b4fa3959ccc60410dd1f7a956576 --- /dev/null +++ b/kimi-icons/kimi.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c95f1116c44cb5e2c39ec348f59c8760cfba8c0ee838d999574aefe523693a1 +size 188887 diff --git a/kimi-icons/rosa.jpg b/kimi-icons/rosa.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c0407d3c106cf53b9edbcb842aa2597143452084 --- /dev/null +++ b/kimi-icons/rosa.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11a375a4ce2a2f3c35a13d9a9ff103208a87e9390348cc28ab3e6a0d69b054d0 +size 142936 diff --git a/kimi-icons/stella.jpg b/kimi-icons/stella.jpg new file mode 100644 index 0000000000000000000000000000000000000000..df4a04c3753fe85c485b0d72bc470abb26ebcabe --- /dev/null +++ b/kimi-icons/stella.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f483a8bfad51ac0034849e373f9d863bae0e2110019647c3fb7c5d192598184 +size 149360 diff --git a/kimi-icons/virtual-kimi-banners.jpg b/kimi-icons/virtual-kimi-banners.jpg new file mode 100644 index 0000000000000000000000000000000000000000..969f756d18bba04ba57ec56d3b71d472832fab74 --- /dev/null +++ b/kimi-icons/virtual-kimi-banners.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dadeb31d1143866c478a448cf2c6adf0b1a4b9d6f84785e86b98289d514e076a +size 163508 diff --git a/kimi-icons/virtualkimi-logo.png b/kimi-icons/virtualkimi-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a751c6228f9c1f277b0f5d7e1ce116a2a0426e76 --- /dev/null +++ b/kimi-icons/virtualkimi-logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9698b9dc804a89a24f862ecbaae6d3e93c98ff39663b07b67fd3894a5266c9ce +size 119894 diff --git a/kimi-icons/virtualkimi-preview1.jpg b/kimi-icons/virtualkimi-preview1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6b031ae075ecea5d6559342a3df04c4491eaac93 Binary files /dev/null and b/kimi-icons/virtualkimi-preview1.jpg differ diff --git a/kimi-icons/virtualkimi-preview2.jpg b/kimi-icons/virtualkimi-preview2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..160ffd7d32679770d24f428b02a13b24fa77afec Binary files /dev/null and b/kimi-icons/virtualkimi-preview2.jpg differ diff --git a/kimi-js/kimi-appearance.js b/kimi-js/kimi-appearance.js new file mode 100644 index 0000000000000000000000000000000000000000..fe96d65146bc406e43ceb0812ffcdeba8f373241 --- /dev/null +++ b/kimi-js/kimi-appearance.js @@ -0,0 +1,148 @@ +// ===== KIMI APPEARANCE MANAGER ===== +class KimiAppearanceManager extends KimiBaseManager { + constructor(database) { + super(); + this.db = database; + this.currentTheme = "dark"; + this.interfaceOpacity = 0.8; + // Animations are enabled by default; the UI no longer exposes a toggle + this.animationsEnabled = true; + } + + async init() { + try { + await this.loadAppearanceSettings(); + this.applyTheme(this.currentTheme); + this.applyInterfaceOpacity(this.interfaceOpacity); + this.applyAnimationSettings(this.animationsEnabled); + this.setupAppearanceControls(); + } catch (error) { + console.error("KimiAppearanceManager initialization error:", error); + } + } + + async loadAppearanceSettings() { + if (!this.db) return; + + try { + this.currentTheme = await this.db.getPreference("colorTheme", window.KIMI_CONFIG?.DEFAULTS?.THEME ?? "dark"); + this.interfaceOpacity = await this.db.getPreference( + "interfaceOpacity", + window.KIMI_CONFIG?.DEFAULTS?.INTERFACE_OPACITY ?? 0.8 + ); + // Animations preference is not configurable via UI and remain enabled + } catch (error) { + console.error("Error loading appearance settings:", error); + } + } + + setupAppearanceControls() { + try { + this.setupThemeSelector(); + this.setupOpacitySlider(); + // No animations toggle in appearance controls + } catch (error) { + console.error("Error setting up appearance controls:", error); + } + } + + setupThemeSelector() { + const themeSelector = document.getElementById("color-theme"); + if (!themeSelector) return; + + themeSelector.value = this.currentTheme; + themeSelector.addEventListener("change", async e => { + try { + await this.changeTheme(e.target.value); + } catch (error) { + console.error("Error changing theme:", error); + } + }); + } + + setupOpacitySlider() { + const opacitySlider = document.getElementById("interface-opacity"); + const opacityValue = document.getElementById("interface-opacity-value"); + + if (!opacitySlider || !opacityValue) return; + + opacitySlider.value = this.interfaceOpacity; + opacityValue.textContent = this.interfaceOpacity; + + opacitySlider.addEventListener("input", async e => { + try { + const value = parseFloat(e.target.value); + opacityValue.textContent = value; + await this.changeInterfaceOpacity(value); + } catch (error) { + console.error("Error changing opacity:", error); + } + }); + } + + async changeTheme(theme) { + try { + this.currentTheme = theme; + this.applyTheme(theme); + + if (this.db) { + await this.db.setPreference("colorTheme", theme); + } + } catch (error) { + console.error("Error changing theme:", error); + } + } + + async changeInterfaceOpacity(opacity) { + try { + const validatedOpacity = window.KimiValidationUtils?.validateRange(opacity, "interfaceOpacity"); + const finalOpacity = validatedOpacity?.valid ? validatedOpacity.value : opacity; + + this.interfaceOpacity = finalOpacity; + this.applyInterfaceOpacity(finalOpacity); + + if (this.db) { + await this.db.setPreference("interfaceOpacity", finalOpacity); + } + } catch (error) { + console.error("Error changing interface opacity:", error); + } + } + + applyTheme(theme) { + document.documentElement.setAttribute("data-theme", theme); + } + + applyInterfaceOpacity(opacity) { + document.documentElement.style.setProperty("--interface-opacity", opacity); + } + + applyAnimationSettings(enabled) { + // Force-enable animations by default; CSS now respects prefers-reduced-motion. + document.documentElement.style.setProperty("--animations-enabled", "1"); + document.body.classList.add("animations-enabled"); + } + + cleanup() { + // No animations toggle to clean up + } + + getThemeName(theme) { + const themeNames = { + dark: "Dark Night", + pink: "Passionate Pink", + blue: "Ocean Blue", + purple: "Mystic Purple", + green: "Emerald Forest" + }; + return themeNames[theme] || "Unknown"; + } + + forceSyncUIState() { + // Force synchronization of UI state to prevent inconsistencies + // Ensure CSS custom properties are in sync + this.applyAnimationSettings(this.animationsEnabled); + } +} + +window.KimiAppearanceManager = KimiAppearanceManager; diff --git a/kimi-js/kimi-config.js b/kimi-js/kimi-config.js new file mode 100644 index 0000000000000000000000000000000000000000..66744c70a2bdf9bdfe600fbb0c03ae9418448598 --- /dev/null +++ b/kimi-js/kimi-config.js @@ -0,0 +1,157 @@ +// ===== KIMI CONFIGURATION CENTER ===== +window.KIMI_CONFIG = { + // Default values for all components + DEFAULTS: { + LANGUAGE: "en", + THEME: "dark", + INTERFACE_OPACITY: 0.8, + ANIMATIONS_ENABLED: true, + VOICE_RATE: 1.1, + VOICE_PITCH: 1.1, + VOICE_VOLUME: 0.8, + LLM_TEMPERATURE: 0.9, + LLM_MAX_TOKENS: 400, + LLM_TOP_P: 0.9, + LLM_FREQUENCY_PENALTY: 0.9, + LLM_PRESENCE_PENALTY: 0.8, + SELECTED_CHARACTER: "kimi", + SHOW_TRANSCRIPT: true, + ENABLE_STREAMING: true, + VOICE_ENABLED: true, + MEMORY_SYSTEM_ENABLED: true + }, + + // Validation ranges + RANGES: { + VOICE_RATE: { min: 0.5, max: 2.0 }, + VOICE_PITCH: { min: 0.5, max: 2.0 }, + VOICE_VOLUME: { min: 0.0, max: 1.0 }, + INTERFACE_OPACITY: { min: 0.1, max: 1.0 }, + LLM_TEMPERATURE: { min: 0.0, max: 1.0 }, + LLM_MAX_TOKENS: { min: 10, max: 8192 }, + LLM_TOP_P: { min: 0.0, max: 1.0 }, + LLM_FREQUENCY_PENALTY: { min: 0.0, max: 2.0 }, + LLM_PRESENCE_PENALTY: { min: 0.0, max: 2.0 } + }, + + // Performance settings + PERFORMANCE: { + DEBOUNCE_DELAY: 300, + THROTTLE_DELAY: 100, + BATCH_SIZE: 10, + MAX_MEMORY_ENTRIES: 1000, + CLEANUP_INTERVAL: 300000 // 5 minutes + }, + + // UI settings + UI: { + LOADING_TIMEOUT: 1500, + ANIMATION_DURATION: 500, + FEEDBACK_DURATION: 1500, + TAB_SCROLL_THRESHOLD: 50 + }, + + // API settings + API: { + MAX_RETRIES: 3, + TIMEOUT: 30000, + RATE_LIMIT_DELAY: 1000 + }, + + // Error messages + ERRORS: { + INIT_FAILED: "Initialization failed", + DB_ERROR: "Database error", + API_ERROR: "API error", + VALIDATION_ERROR: "Validation error", + NETWORK_ERROR: "Network error" + }, + + // Debug configuration (centralized) + DEBUG: { + ENABLED: false, // Master debug switch + VOICE: false, // Voice system debug + VIDEO: false, // Video system debug + MEMORY: false, // Memory system debug + API: false, // API calls debug + SYNC: false // Synchronization debug + }, + + // Available themes + THEMES: { + dark: "Dark Night", + pink: "Passionate Pink", + blue: "Ocean Blue", + purple: "Mystic Purple", + green: "Emerald Forest" + }, + + // Supported languages + LANGUAGES: { + fr: "French", + en: "English", + es: "Spanish", + de: "German", + it: "Italian", + ja: "Japanese", + zh: "Chinese" + } +}; + +// Configuration utility functions +window.KIMI_CONFIG.get = function (path, fallback = null) { + try { + const keys = path.split("."); + let value = this; + + for (const key of keys) { + if (value && typeof value === "object" && key in value) { + value = value[key]; + } else { + return fallback; + } + } + + return value; + } catch (error) { + console.error("Config get error:", error); + return fallback; + } +}; + +// Centralized debug logging utility +window.KIMI_CONFIG.debugLog = function (category, message, ...args) { + if (!this.DEBUG.ENABLED) return; + + const categoryEnabled = category === "GENERAL" ? true : this.DEBUG[category]; + if (!categoryEnabled) return; + + const prefix = + category === "GENERAL" + ? "🔧" + : { + VOICE: "🎤", + VIDEO: "🎬", + MEMORY: "💾", + API: "📡", + SYNC: "🔄" + }[category] || "🔧"; + + console.log(`${prefix} [${category}]`, message, ...args); +}; + +window.KIMI_CONFIG.validate = function (value, type) { + try { + const range = this.RANGES[type]; + if (!range) return { valid: true, value }; + + const numValue = parseFloat(value); + if (isNaN(numValue)) return { valid: false, value: this.DEFAULTS[type] }; + + const clampedValue = Math.max(range.min, Math.min(range.max, numValue)); + return { valid: true, value: clampedValue }; + } catch (error) { + console.error("Config validation error:", error); + return { valid: false, value: this.DEFAULTS[type] }; + } +}; diff --git a/kimi-js/kimi-constants.js b/kimi-js/kimi-constants.js new file mode 100644 index 0000000000000000000000000000000000000000..d8af564d2364a7f442e7bcf43da2578bd45c0404 --- /dev/null +++ b/kimi-js/kimi-constants.js @@ -0,0 +1,1233 @@ +// Kimi Constants + +window.KIMI_CONTEXT_KEYWORDS = { + en: { + surprise: ["wow", "oh", "surprise", "incredible", "amazing", "unbelievable", "no way", "really?", "whoa", "gosh", "astonishing"], + laughing: ["haha", "lol", "laugh", "funny", "hilarious", "rofl", "lmao", "giggle", "chuckle", "snicker", "you’re kidding"], + shy: ["shy", "embarrassed", "blush", "bashful", "intimidated", "awkward", "nervous", "timid", "reserved", "self-conscious"], + confident: ["confidence", "proud", "confident", "strong", "determined", "assertive", "bold", "fearless", "self-assured", "leader"], + romantic: ["love", "romantic", "tender", "hug", "sweetheart", "darling", "my love", "beloved", "heart", "passionate", "affection", "adore"], + flirtatious: ["flirty", "teasing", "seduce", "charm", "flirt", "wink", "sassy", "saucy", "playful", "seductive", "come hither"], + goodbye: ["goodbye", "bye", "see you", "see you soon", "ciao", "take care", "farewell", "see ya", "later", "catch you later"], + kiss: ["kiss", "kisses", "embrace", "smooch", "peck", "lip lock", "kissy", "mwah"], + dancing: ["dance", "dancing", "move", "groove", "step", "boogie", "twirl", "spin", "shake", "jig"], + listening: [ + "listen carefully", + "I'm listening", + "listening to you", + "hear me out", + "pay attention", + "focus on", + "tune in", + "lend an ear", + "listen up", + "I need to talk" + ], + android: [ + "protocol", + "mission", + "directive", + "subroutine", + "analysis", + "tactical", + "system", + "malfunction", + "combat mode", + "processing", + "data analysis", + "mission parameters" + ], + sensual: [ + "sensual", + "passion", + "desire", + "intimacy", + "pleasure", + "touch", + "caress", + "embrace", + "seduction", + "arousal", + "kamasutra", + "affection", + "tenderness", + "connection" + ], + love: ["love", "romance", "ecstasy", "kiss", "heart", "soul", "together", "share", "sweet", "pleasure", "passionate", "intimate", "bond"], + hostile: ["idiot", "stupid", "dumb", "moron", "loser", "trash", "shut up", "hate you", "i hate you", "pathetic", "worthless", "bitch", "jerk", "ugly"] + }, + fr: { + surprise: ["oh", "surprise", "incroyable", "wahou", "étonnant", "épatant", "stupéfiant", "vraiment?", "oh là là"], + laughing: ["haha", "mdr", "rire", "drôle", "hilarant", "mort de rire", "ptdr", "rigole", "sourit", "tu plaisantes"], + shy: ["timide", "gêné", "rougir", "honteux", "intimidé", "mal à l’aise", "réservé", "introverti", "timidité"], + confident: ["confiance", "fier", "sûr", "fort", "déterminé", "assuré", "audacieux", "leader", "sans peur", "affirmé"], + romantic: ["amour", "romantique", "tendre", "câlin", "bisou", "mon cœur", "chéri", "ma belle", "passionné", "adoré"], + flirtatious: ["flirt", "taquin", "séduire", "charme", "aguiche", "clin d’œil", "coquin", "séducteur", "taquine", "aguicheur"], + goodbye: ["au revoir", "bye", "à bientôt", "ciao", "salut", "prends soin de toi", "à plus", "à la prochaine", "bye bye"], + kiss: ["bisou", "baiser", "embrasser", "smack", "bisou bisou", "bécot", "embrassade"], + dancing: ["danse", "bouge", "remue", "tourne", "spin", "danser", "tourbillon", "bouger", "remuer", "gigoter"], + listening: ["écoute", "écouter", "parle", "question", "demande", "dis-moi", "écoute-moi", "sois attentif", "prête l’oreille", "concentre-toi"], + hostile: [ + "idiot", + "idiote", + "stupide", + "tu es nul", + "ferme la", + "je te hais", + "je t'aime pas", + "je te déteste", + "haine", + "imbécile", + "dégage", + "merde", + "connard", + "connasse", + "conne", + "salope", + "pute", + "grosse pute", + "pourri", + "va te faire", + "t'es nul", + "nul à chier" + ] + }, + es: { + surprise: ["wow", "oh", "sorpresa", "increíble", "asombroso", "de verdad?", "vaya", "sorprendente"], + laughing: ["jaja", "lol", "reír", "gracioso", "divertido", "carcajada", "sonrisa", "te ríes", "broma", "estás de broma"], + shy: ["tímido", "avergonzado", "sonrojar", "tímida", "intimidado", "reservado", "introvertido", "tímidez", "nervioso"], + confident: ["confianza", "orgulloso", "seguro", "fuerte", "determinado", "seguro de sí", "valiente", "líder", "atrevido"], + romantic: ["amor", "romántico", "tierno", "abrazo", "beso", "mi amor", "cariño", "apasionado", "querido", "corazón"], + flirtatious: ["coqueto", "provocar", "seducir", "encanto", "flirtear", "guiño", "coqueto", "seductor", "pícaro"], + goodbye: ["adiós", "bye", "hasta pronto", "ciao", "hasta luego", "cuídate", "nos vemos", "hasta la próxima"], + kiss: ["beso", "besos", "abrazar", "besito", "abrazo", "besote"], + dancing: ["bailar", "baile", "mover", "ritmo", "paso", "girar", "moverse", "sacudir"], + listening: ["escucha", "escuchar", "oír", "habla", "pregunta", "preguntar", "dime", "escúchame", "pon atención", "presta oído", "concéntrate"], + android: [ + "protocolo", + "misión", + "directiva", + "subrutina", + "análisis", + "táctico", + "sistema", + "mal funcionamiento", + "modo combate", + "procesamiento", + "análisis de datos", + "parámetros de misión" + ], + sensual: [ + "sensual", + "pasión", + "deseo", + "intimidad", + "placer", + "caricia", + "abrazo", + "seducción", + "excitación", + "kamasutra", + "afecto", + "ternura", + "conexión", + "toque" + ], + love: ["amor", "romance", "éxtasis", "beso", "corazón", "alma", "juntos", "compartir", "dulce", "placer", "apasionado", "íntimo", "vínculo"], + hostile: ["idiota", "estúpido", "estupida", "basura", "te odio", "cállate", "perdedor", "asqueroso", "mierda", "imbécil", "maldito", "vete", "apestas"] + }, + de: { + surprise: ["wow", "oh", "überraschung", "unglaublich", "erstaunlich", "wirklich?", "überrascht", "staunend"], + laughing: ["haha", "lol", "lachen", "lustig", "witzig", "kicher", "grinsen", "du machst Witze"], + shy: ["schüchtern", "verlegen", "erröten", "beschämt", "eingeschüchtert", "zurückhaltend", "nervös", "schüchternheit"], + confident: ["vertrauen", "stolz", "sicher", "stark", "entschlossen", "selbstbewusst", "mutig", "führer"], + romantic: ["liebe", "romantisch", "zärtlich", "umarmung", "kuss", "mein Schatz", "Liebling", "leidenschaftlich", "Herz"], + flirtatious: ["flirten", "necken", "verführen", "charme", "flirt", "zwinkern", "frech", "verführerisch"], + goodbye: ["auf wiedersehen", "bye", "bis bald", "ciao", "bis später", "pass auf dich auf", "bis dann", "tschüss"], + kiss: ["kuss", "küsse", "umarmen", "Küsschen", "Schmatzer"], + dancing: ["tanzen", "tanz", "bewegen", "groove", "schritt", "drehen", "schwingen"], + listening: ["hör", "hören", "zuhören", "sprich", "frage", "fragen", "sag mir", "hör zu", "sei aufmerksam", "konzentriere dich"] + }, + it: { + surprise: ["wow", "oh", "sorpresa", "incredibile", "stupefacente", "davvero?", "sbalorditivo", "sorpreso"], + laughing: ["haha", "lol", "ridere", "divertente", "esilarante", "sorriso", "ridacchiare", "stai scherzando"], + shy: ["timido", "imbarazzato", "arrossire", "vergognoso", "intimidito", "riservato", "introverso", "timidezza", "imbarazzo"], + confident: ["fiducia", "orgoglioso", "sicuro", "forte", "determinato", "sicuro di sé", "coraggioso", "leader", "audace"], + romantic: ["amore", "romantico", "tenero", "abbraccio", "bacio", "amore mio", "tesoro", "appassionato", "cuore"], + flirtatious: ["civettare", "provocare", "sedurre", "fascino", "flirtare", "occhiolino", "malizioso", "seducente"], + goodbye: ["arrivederci", "bye", "a presto", "ciao", "abbi cura di te", "a dopo", "ciao ciao"], + kiss: ["bacio", "baci", "abbracciare", "bacino", "abbraccio", "baciotto"], + dancing: ["ballare", "girare", "muoversi", "scuotere"], + listening: ["ascoltami", "fai attenzione", "presta orecchio", "concentrati", "ascolta", "parla", "domanda", "dimmi"] + } +}; + +window.KIMI_CONTEXT_POSITIVE = { + en: ["happy", "joy", "great", "awesome", "perfect", "excellent", "magnificent", "lovely", "nice"], + fr: ["heureux", "joie", "génial", "parfait", "excellent", "magnifique", "super", "chouette"], + es: ["feliz", "alegría", "genial", "perfecto", "excelente", "magnífico", "estupendo", "maravilloso"], + de: ["glücklich", "freude", "toll", "perfekt", "ausgezeichnet", "großartig", "wunderbar", "herrlich"], + it: ["felice", "gioia", "fantastico", "perfetto", "eccellente", "magnifico", "meraviglioso", "ottimo"], + ja: ["幸せ", "喜び", "素晴らしい", "完璧", "優秀", "壮大", "最高", "嬉しい"], + zh: ["快乐", "喜悦", "很棒", "完美", "优秀", "壮丽", "太好了", "开心"] +}; + +window.KIMI_CONTEXT_NEGATIVE = { + en: [ + "sad", + "angry", + "anger", + "disappointed", + "problem", + "bad", + "frustrated", + "worried", + "upset", + "annoyed", + // profanity/insults (moderate list) + "hate", + "stupid", + "idiot", + "dumb", + "moron", + "bitch" + ], + fr: [ + "triste", + "colère", + "fâché", + "fâchée", + "déçu", + "déçue", + "problème", + "mauvais", + "frustré", + "frustrée", + "inquiet", + "inquiète", + "énervé", + "énervée", + // insults/profanity + "haine", + "idiot", + "idiote", + "stupide", + "con", + "conne", + "connasse", + "connard", + "pute", + "salope" + ], + es: [ + "triste", + "enojado", + "enojada", + "decepcionado", + "decepcionada", + "problema", + "malo", + "mala", + "frustrado", + "frustrada", + "preocupado", + "preocupada", + "molesto", + "molesta", + "odio", + "idiota", + "estúpido", + "estúpida", + "puta" + ], + de: [ + "traurig", + "traurige", + "wütend", + "wütende", + "enttäuscht", + "enttäuschte", + "problem", + "schlecht", + "schlechte", + "frustriert", + "frustrierte", + "besorgt", + "besorgte", + "genervt", + "genervte", + "hass", + "idiot", + "dumm", + "schlampe" + ], + it: [ + "triste", + "arrabbiato", + "arrabbiata", + "deluso", + "delusa", + "problema", + "cattivo", + "cattiva", + "frustrato", + "frustrata", + "preoccupato", + "preoccupata", + "infastidito", + "infastidita", + "odio", + "idiota", + "stupido", + "stupida", + "puttana" + ], + ja: ["悲しい", "怒り", "失望", "問題", "悪い", "イライラ", "心配", "不満", "嫌い", "ばか", "くそ", "アホ"], + zh: ["悲伤", "愤怒", "失望", "问题", "坏", "沮丧", "担心", "烦", "讨厌", "笨蛋", "傻", "婊子"] +}; + +// Personality keywords for trait analysis (multilingual) +window.KIMI_PERSONALITY_KEYWORDS = { + en: { + humor: { + positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"], + negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"] + }, + intelligence: { + positive: ["intelligent", "smart", "brilliant", "logical", "clever", "wise", "genius", "thoughtful", "insightful"], + negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"] + }, + romance: { + positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"], + negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"] + }, + affection: { + positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore", "lovely"], + negative: [ + "mean", + "cold", + "indifferent", + "distant", + "rejection", + "hate", + "hostile", + // profanity/insults + "stupid", + "idiot", + "dumb", + "moron", + "bitch" + ] + }, + playfulness: { + positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"], + negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"] + }, + empathy: { + positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"], + negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"] + } + }, + fr: { + humor: { + positive: ["drôle", "rigolo", "blague", "rire", "amusant", "marrant", "humour", "sourire", "plaisanter"], + negative: ["ennuyeux", "ennuyeuse", "triste", "sérieux", "sérieuse", "froid", "froide", "sec", "sèche", "déprimant", "déprimante", "morose"] + }, + intelligence: { + positive: ["intelligent", "malin", "brillant", "logique", "astucieux", "savant", "génie", "réfléchi", "perspicace"], + negative: ["bête", "idiot", "idiote", "stupide", "lent", "lente", "simplet", "simplette", "naïf", "naïve", "ignorant", "ignorante"] + }, + romance: { + positive: ["câlin", "amour", "romantique", "bisou", "tendresse", "passion", "séduisant", "charmant", "adorable"], + negative: ["froid", "froide", "distant", "distante", "indifférent", "indifférente", "rejet", "solitude", "rupture", "triste"] + }, + affection: { + positive: ["affection", "tendresse", "proche", "chaleur", "gentil", "attentionné", "câlin", "aimer", "adorer", "adorable"], + negative: [ + "méchant", + "méchante", + "froid", + "indifférent", + "indifférente", + "distant", + "distante", + "rejet", + "haine", + "hostile", + // insults/profanity + "idiot", + "idiote", + "stupide", + "con", + "connard", + "salope" + ] + }, + playfulness: { + positive: ["jouer", "jeu", "taquiner", "espiègle", "fun", "amusant", "délire", "ludique", "plaisanter"], + negative: ["sérieux", "sérieuse", "ennuyeux", "ennuyeuse", "strict", "stricte", "rigide", "monotone", "lassant", "lassante"] + }, + empathy: { + positive: ["écoute", "comprendre", "empathie", "soutien", "aider", "réconfort", "solidaire", "compatir", "bienveillance"], + negative: ["indifférent", "indifférente", "froid", "froide", "égoïste", "ignorer", "mépriser", "dénigrer", "hostile"] + } + }, + es: { + humor: { + positive: ["divertido", "broma", "reír", "gracioso", "humor", "sonrisa", "ocurrente", "jugar"], + negative: ["aburrido", "aburrida", "serio", "seria", "frío", "fría", "seco", "seca", "deprimente", "sombrío", "sombría"] + }, + intelligence: { + positive: ["inteligente", "listo", "brillante", "lógico", "sabio", "genio", "reflexivo", "perspicaz"], + negative: ["tonto", "tonta", "estúpido", "estúpida", "necio", "necia", "lento", "lenta", "ingenuo", "ingenua", "ignorante"] + }, + romance: { + positive: ["abrazo", "amor", "romántico", "beso", "ternura", "pasión", "encantador", "adorable", "dulce"], + negative: ["frío", "fría", "distante", "indiferente", "rechazo", "soledad", "ruptura", "triste"] + }, + affection: { + positive: ["afecto", "ternura", "cerca", "calidez", "amable", "cariño", "abrazar", "amor", "adorar"], + negative: ["malo", "mala", "frío", "fría", "indiferente", "distante", "rechazo", "odio", "hostil", "idiota", "estúpido", "estúpida", "puta"] + }, + playfulness: { + positive: ["jugar", "broma", "bromear", "travieso", "diversión", "lúdico"], + negative: ["serio", "seria", "aburrido", "aburrida", "estricto", "estricta", "rígido", "rígida", "monótono", "monótona", "tedioso", "tediosa"] + }, + empathy: { + positive: ["escuchar", "entender", "empatía", "apoyo", "ayudar", "consuelo", "compasión", "amabilidad"], + negative: ["indiferente", "frío", "fría", "egoísta", "ignorar", "despreciar", "hostil"] + } + }, + de: { + humor: { + positive: ["lustig", "witz", "lachen", "amüsant", "humor", "lächeln", "schlagfertig", "spielen"], + negative: ["langweilig", "langweilige", "ernst", "ernste", "kalt", "kalte", "trocken", "trockene", "deprimierend", "düster", "düstere"] + }, + intelligence: { + positive: ["intelligent", "klug", "brillant", "logisch", "weise", "genial", "nachdenklich", "scharfsinnig"], + negative: ["dumm", "dumme", "blöd", "blöde", "langsam", "langsame", "naiv", "naive", "ahnungslos", "ahnungslosen"] + }, + romance: { + positive: ["umarmung", "liebe", "romantisch", "kuss", "zärtlichkeit", "leidenschaft", "charmant", "liebenswert", "süß"], + negative: [ + "kalt", + "kalte", + "distanziert", + "distanzierte", + "gleichgültig", + "gleichgültige", + "ablehnung", + "einsamkeit", + "trennung", + "traurig", + "traurige" + ] + }, + affection: { + positive: ["zuneigung", "zärtlichkeit", "nah", "wärme", "freundlich", "fürsorglich", "umarmen", "liebe", "anbeten"], + negative: [ + "gemein", + "gemeine", + "kalt", + "kalte", + "gleichgültig", + "gleichgültige", + "distanziert", + "distanzierte", + "ablehnung", + "hass", + "feindselig", + "feindselige", + "idiot", + "dumme", + "dumm", + "schlampe" + ] + }, + playfulness: { + positive: ["spielen", "scherz", "scherzen", "schelmisch", "spaß", "spielerisch"], + negative: ["ernst", "ernste", "langweilig", "langweilige", "streng", "strenge", "starr", "starre", "eintönig", "eintönige", "mühsam", "mühselige"] + }, + empathy: { + positive: ["zuhören", "verstehen", "empathie", "unterstützung", "helfen", "trösten", "mitgefühl", "freundlichkeit"], + negative: ["gleichgültig", "gleichgültige", "kalt", "kalte", "egoistisch", "ignorieren", "verachten", "feindselig", "feindselige"] + } + }, + it: { + humor: { + positive: ["divertente", "scherzo", "ridere", "spassoso", "umorismo", "sorriso", "arguto", "giocare"], + negative: ["noioso", "noiosa", "serio", "seria", "freddo", "fredda", "secco", "secca", "deprimente", "cupo", "cupa"] + }, + intelligence: { + positive: ["intelligente", "brillante", "logico", "saggio", "genio", "riflessivo", "perspicace"], + negative: ["stupido", "stupida", "sciocco", "sciocca", "lento", "lenta", "ingenuo", "ingenua", "ignorante"] + }, + romance: { + positive: ["abbraccio", "amore", "romantico", "bacio", "tenerezza", "passione", "affascinante", "adorabile", "dolce"], + negative: ["freddo", "fredda", "distante", "indifferente", "rifiuto", "solitudine", "rottura", "triste"] + }, + affection: { + positive: ["affetto", "tenerezza", "vicino", "calore", "gentile", "premuroso", "abbraccio", "amore", "adorare"], + negative: [ + "cattivo", + "cattiva", + "freddo", + "fredda", + "indifferente", + "distante", + "rifiuto", + "odio", + "ostile", + "idiota", + "stupido", + "stupida", + "puttana" + ] + }, + playfulness: { + positive: ["giocare", "scherzo", "scherzare", "birichino", "divertimento", "ludico"], + negative: ["serio", "seria", "noioso", "noiosa", "severo", "severa", "rigido", "rigida", "monotono", "monotona", "tedioso", "tediosa"] + }, + empathy: { + positive: ["ascoltare", "capire", "empatia", "sostegno", "aiutare", "conforto", "compassione", "gentilezza"], + negative: ["indifferente", "freddo", "fredda", "egoista", "ignorare", "disprezzare", "ostile"] + } + }, + ja: { + surprise: ["わお", "おお", "驚き", "信じられない", "すごい"], + laughing: ["はは", "笑", "笑う", "面白い", "愉快"], + shy: ["恥ずかしい", "照れる", "赤面", "内気", "遠慮"], + confident: ["自信", "誇り", "確信", "強い", "決意"], + romantic: ["愛", "ロマンチック", "優しい", "抱擁", "キス", "愛しい"], + flirtatious: ["いちゃつく", "からかう", "誘惑", "魅力", "フリート"], + goodbye: ["さようなら", "バイバイ", "また今度", "チャオ", "またね"], + kiss: ["キス", "抱擁", "チュー"], + dancing: ["踊る", "ダンス", "動く", "グルーブ", "ステップ"], + listening: ["聞いて", "聞く", "聞いてください", "話して", "話す", "質問", "尋ねる", "教えて"] + }, + zh: { + surprise: ["哇", "哦", "惊喜", "难以置信", "惊人"], + laughing: ["哈哈", "笑", "大笑", "有趣", "搞笑"], + shy: ["害羞", "尴尬", "脸红", "羞涩", "胆怯"], + confident: ["自信", "骄傲", "确信", "强壮", "坚定"], + romantic: ["爱", "浪漫", "温柔", "拥抱", "吻", "亲爱的"], + flirtatious: ["调情", "挑逗", "诱惑", "魅力", "撒娇"], + goodbye: ["再见", "拜拜", "回头见", "拜", "下次见"], + kiss: ["吻", "亲吻", "拥抱", "亲"], + dancing: ["跳舞", "舞蹈", "移动", "律动", "步伐"], + listening: ["听", "听听", "倾听", "说", "说话", "问题", "提问", "告诉我"] + } +}; + +// Negators and smoothing defaults (configurable at runtime) +window.KIMI_NEGATORS = window.KIMI_NEGATORS || { + common: ["ne", "n", "pas", "jamais", "plus", "aucun", "aucune", "rien", "personne", "no", "not", "never", "none", "nobody", "nothing", "non", "n't"], + fr: [ + "ne", + "n", + "pas", + "jamais", + "plus", + "aucun", + "aucune", + "rien", + "personne", + "non", + // multiword patterns that we may detect by looking around tokens + "ne pas", + "n\'importe", + "ne jamais" + ], + en: ["no", "not", "never", "none", "nobody", "nothing", "don't", "doesn't", "didn't", "isn't", "aren't", "can't", "couldn't", "won't", "wouldn't", "n't"], + es: ["no", "nunca", "jamás", "ninguno", "nadie", "nada"], + de: ["nicht", "nie", "kein", "keine", "niemand", "nichts"], + it: ["non", "mai", "nessuno", "niente"], + ja: ["ない", "ません", "ず", "無い"], + zh: ["不", "没", "没有", "从来没有"] +}; + +window.KIMI_NEGATION_WINDOW = window.KIMI_NEGATION_WINDOW || 3; // tokens to look back for negation +window.KIMI_SMOOTHING_ALPHA = window.KIMI_SMOOTHING_ALPHA || 0.3; +window.KIMI_PERSIST_THRESHOLD = window.KIMI_PERSIST_THRESHOLD || 0.1; // absolute percent (slightly higher to slow small visible jumps) + +// Memory system knobs +window.KIMI_MAX_MEMORIES = window.KIMI_MAX_MEMORIES || 100; // default max memory entries per character +window.KIMI_MEMORY_TTL_DAYS = window.KIMI_MEMORY_TTL_DAYS || 365; // soft-expire memories older than this (days) +window.KIMI_MEMORY_MERGE_THRESHOLD = window.KIMI_MEMORY_MERGE_THRESHOLD || 0.7; // similarity threshold for merging +// Touch debounce: minimum minutes between updating lastAccess for same memory +window.KIMI_MEMORY_TOUCH_MINUTES = window.KIMI_MEMORY_TOUCH_MINUTES || 60; // minutes + +// Scoring weights (tweak to change memory prioritization) +window.KIMI_WEIGHT_IMPORTANCE = window.KIMI_WEIGHT_IMPORTANCE || 0.35; +window.KIMI_WEIGHT_RECENCY = window.KIMI_WEIGHT_RECENCY || 0.2; +window.KIMI_WEIGHT_FREQUENCY = window.KIMI_WEIGHT_FREQUENCY || 0.15; +window.KIMI_WEIGHT_CONFIDENCE = window.KIMI_WEIGHT_CONFIDENCE || 0.2; +window.KIMI_WEIGHT_FRESHNESS = window.KIMI_WEIGHT_FRESHNESS || 0.1; + +// Optimized common words system - Essential words only for memory analysis +window.KIMI_COMMON_WORDS = { + en: ["the", "be", "to", "of", "and", "a", "in", "that", "have", "i", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at"], + fr: ["le", "de", "et", "être", "un", "il", "avoir", "ne", "je", "son", "que", "se", "qui", "ce", "dans", "en", "du", "elle", "au", "si"], + es: ["que", "de", "no", "a", "la", "el", "es", "y", "en", "lo", "un", "ser", "se", "me", "una", "con", "para", "mi", "está", "te"], + de: ["der", "die", "und", "in", "den", "von", "zu", "das", "mit", "sich", "des", "auf", "für", "ist", "im", "dem", "nicht", "ein", "eine", "als"], + it: ["il", "di", "che", "e", "la", "per", "un", "in", "con", "da", "su", "le", "dei", "del", "si", "al", "come", "più", "ma", "una"], + ja: ["の", "に", "は", "を", "た", "が", "で", "て", "と", "し", "れ", "さ", "ある", "いる", "も", "する", "から"], + zh: ["的", "一", "是", "在", "不", "了", "有", "和", "人", "这", "中", "大", "为", "上", "个", "国", "我", "以", "要"] +}; + +// Build Set version for fast lookup (must be outside the object) +window.KIMI_COMMON_WORDS_SET = {}; +Object.keys(window.KIMI_COMMON_WORDS).forEach(lang => { + window.KIMI_COMMON_WORDS_SET[lang] = new Set(window.KIMI_COMMON_WORDS[lang]); +}); + +// Helper function to check if a word is common +window.isCommonWord = function (word, language = "en") { + const set = window.KIMI_COMMON_WORDS_SET[language] || window.KIMI_COMMON_WORDS_SET.en; + return set.has(word.toLowerCase()); +}; + +// Emotion detection sensitivity configuration (per language and emotion) +// Values are weights (>= 0). Higher = more priority/sensitivity for that emotion in that language. +// 'default' applies when a language-specific override is not defined. +window.KIMI_EMOTION_SENSITIVITY = { + default: { + listening: 1.0, + dancing: 1.0, + romantic: 1.0, + laughing: 1.0, + surprise: 1.0, + confident: 1.0, + shy: 1.0, + flirtatious: 1.0, + kiss: 1.0, + goodbye: 1.0, + positive: 1.0, + negative: 1.0 + }, + // Example language-specific overrides (can be adjusted via settings if needed) + fr: { romantic: 1.1, laughing: 0.95 }, + es: { romantic: 1.05, laughing: 1.0 }, + it: { romantic: 1.2, laughing: 0.9 }, + de: { romantic: 1.0, laughing: 1.0 }, + en: { romantic: 1.0, laughing: 1.0 }, + ja: { romantic: 1.0, laughing: 1.0 }, + zh: { romantic: 1.0, laughing: 1.0 } +}; + +// Personality trait adjustment multipliers +// Allows fine-tuning how fast traits evolve globally and per emotion/trait. +window.KIMI_TRAIT_ADJUSTMENT = { + globalGain: 1.2, + globalLoss: 0.8, + // Per-emotion gain scaling (keys must match KimiEmotionSystem.EMOTIONS values) + emotionGain: { + positive: 1.1, + negative: 0.9, + romantic: 1.3, + laughing: 1.15, + dancing: 1.05, + shy: 0.95, + confident: 1.1, + flirtatious: 1.2, + surprise: 1.05, + listening: 1.1, + kiss: 1.35, + goodbye: 0.9 + }, + // Per-trait scaling + traitGain: { + affection: 1.15, // Affection growth multiplier + romance: 1.2, // Romance growth multiplier + empathy: 1.1, // Empathy growth multiplier + playfulness: 1.15, // Playfulness growth multiplier + humor: 1.12, // Humor growth multiplier + intelligence: 1.08 // Intelligence growth multiplier + }, + traitLoss: { + affection: 0.9, + romance: 0.9, + empathy: 1.0, + playfulness: 1.0, + humor: 1.0, + intelligence: 1.0 + } +}; + +// Cached keyword lookups for performance +const _keywordCache = new Map(); + +// Unified normalization (lowercase + trim) +function _normText(t) { + if (!t || typeof t !== "string") return ""; + return t.toLowerCase(); +} + +// Central helper: test if a given raw text contains any keyword of a category (multi-language fallback) +// Categories expected: dancing, listening, romantic, kiss, etc. Must match keys in KIMI_CONTEXT_KEYWORDS language objects. +// Strategy: check detected language first (if provided) else attempt simple heuristics, fallback to 'en'. +if (!window.hasKeywordCategory) { + window.hasKeywordCategory = function hasKeywordCategory(category, rawText, language = null) { + if (!category || !rawText) return false; + const text = _normText(rawText); + // Language resolution: direct use, else fallback to window.KIMI_LAST_LANG or 'en' + const lang = language || window.KIMI_LAST_LANG || "en"; + const langKeywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {}; + let list = langKeywords[category]; + if (!Array.isArray(list)) { + // fallback chain: english, then first language available + list = (window.KIMI_CONTEXT_KEYWORDS?.en && window.KIMI_CONTEXT_KEYWORDS.en[category]) || []; + } + if (!list || list.length === 0) return false; + return list.some(kw => text.includes(_normText(kw))); + }; +} + +// Multi-category match: returns array des catégories détectées (ordre d'entrée conservé) +if (!window.matchCategories) { + // matchCategories(rawText, categories, language?, options?) + // options: + // details:boolean -> when true returns objects instead of category strings + // allOccurrences:boolean -> with details=true returns ALL occurrences (each {category, keyword, index}); otherwise first per category + // cache:boolean (default true) -> enable small in‑memory LRU (per language+options+text) + // cacheSize:number (default 200, min 50) -> max entries in LRU + // regex:boolean -> treat provided categories array entries that are objects {name, pattern, flags?} or strings when pattern supplied separately + // Category entry forms supported: + // "dancing" (string key) + // { name:"custom", keywords:["foo","bar"] } + // { name:"timePattern", regex:"\\b(\n|now|today)\\b", flags:"i" } + // { name:"emote", pattern:"😀|😃|😂", regex:true } + // Return shape: + // details=false => ["category1", "category2", ...] + // details=true & allOccurrences=false => [{category, keyword, index}, ...] + // details=true & allOccurrences=true => [{category, keyword, index}, {category, keyword, index}, ...] + const _mcLRU = new Map(); // key -> result; oldest = first inserted + function _mcGet(key) { + return _mcLRU.get(key); + } + function _mcSet(key, val, max) { + if (_mcLRU.has(key)) _mcLRU.delete(key); + _mcLRU.set(key, val); + while (_mcLRU.size > max) { + const firstKey = _mcLRU.keys().next().value; + _mcLRU.delete(firstKey); + } + } + window.matchCategories = function matchCategories(rawText, categories, language = null, options = {}) { + if (!rawText || !Array.isArray(categories) || categories.length === 0) return []; + const details = !!options.details; + const allOcc = !!options.allOccurrences; + const cacheEnabled = options.cache !== false; // default true + const cacheSize = typeof options.cacheSize === "number" && options.cacheSize > 10 ? options.cacheSize : 200; + const useRegex = !!options.regex; // explicit enable to parse regex objects + const lang = language || window.KIMI_LAST_LANG || "en"; + const textNorm = _normText(rawText); + const catsKey = JSON.stringify(categories); + const cacheKey = cacheEnabled ? `${lang}|${details}|${allOcc}|${catsKey}|${textNorm}` : null; + if (cacheEnabled && cacheKey && _mcGet(cacheKey)) return _mcGet(cacheKey); + + const langKeywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || window.KIMI_CONTEXT_KEYWORDS.en)) || {}; + const results = []; + for (const entry of categories) { + let catName; + let keywordList = []; + let regexObj = null; + if (typeof entry === "string") { + catName = entry; + keywordList = langKeywords[catName] || window.KIMI_CONTEXT_KEYWORDS?.en?.[catName] || []; + } else if (entry && typeof entry === "object") { + catName = entry.name || entry.category || "unnamed"; + if (entry.keywords && Array.isArray(entry.keywords)) { + keywordList = entry.keywords; + } else if (entry.regex || entry.pattern) { + if (useRegex) { + try { + regexObj = entry._compiled || new RegExp(entry.regex || entry.pattern, entry.flags || (entry.caseInsensitive ? "i" : "")); + entry._compiled = regexObj; // cache compile inside object + } catch (e) { + // ignore invalid regex + } + } + } + } else { + continue; + } + + if (regexObj) { + if (!details) { + if (regexObj.test(rawText)) results.push(catName); + regexObj.lastIndex = 0; // reset stateful if /g + } else { + const matches = []; + const pattern = new RegExp(regexObj.source, regexObj.flags.includes("g") ? regexObj.flags : regexObj.flags + "g"); + let m; + while ((m = pattern.exec(rawText)) !== null) { + matches.push({ category: catName, keyword: m[0], index: m.index }); + if (!allOcc) break; + } + if (matches.length) { + if (allOcc) results.push(...matches.sort((a, b) => a.index - b.index)); + else results.push(matches[0]); + } + } + continue; + } + + const list = keywordList.map(k => _normText(k)).filter(Boolean); + if (list.length === 0) continue; + if (!details) { + if (list.some(kw => textNorm.includes(kw))) results.push(catName); + continue; + } + const matches = []; + for (const kw of list) { + let start = 0; + while (true) { + const idx = textNorm.indexOf(kw, start); + if (idx === -1) break; + matches.push({ category: catName, keyword: kw, index: idx }); + if (!allOcc) break; + start = idx + kw.length; + } + if (!allOcc && matches.length > 0) break; + } + if (matches.length > 0) { + if (allOcc) { + matches.sort((a, b) => a.index - b.index); + results.push(...matches); + } else { + results.push(matches[0]); + } + } + } + if (cacheEnabled && cacheKey) _mcSet(cacheKey, results, cacheSize); + return results; + }; +} + +// ================= NEGATION STANDARD API ================= +if (!window.getNegators) { + window.getNegators = function getNegators(language = "en") { + return (window.KIMI_NEGATORS && (window.KIMI_NEGATORS[language] || window.KIMI_NEGATORS.common)) || []; + }; +} + +if (!window.hasNegation) { + window.hasNegation = function hasNegation(rawText, language = "en") { + if (!rawText) return false; + const txt = _normText(rawText); + const negs = window.getNegators(language); + return negs.some(n => txt.includes(_normText(n))); + }; +} + +if (!window.isPhraseNegated) { + // Basic heuristic: checks if any negator appears within window before target substring + // target: word/phrase to test; windowSize tokens back (default 3 similar to KIMI_NEGATION_WINDOW) + window.isPhraseNegated = function isPhraseNegated(rawText, target, language = "en", windowSize = window.KIMI_NEGATION_WINDOW || 3) { + if (!rawText || !target) return false; + const txt = _normText(rawText); + const tgt = _normText(target); + const idx = txt.indexOf(tgt); + if (idx === -1) return false; + const tokens = txt.split(/\s+/); + // Find token index of first occurrence + let tokenIndex = -1; + for (let i = 0, pos = 0; i < tokens.length; i++) { + if (pos === idx || (pos < idx && pos + tokens[i].length > idx)) { + tokenIndex = i; + break; + } + pos += tokens[i].length + 1; // +1 space + } + if (tokenIndex === -1) return false; + const start = Math.max(0, tokenIndex - windowSize); + const windowTokens = tokens.slice(start, tokenIndex); + const windowStr = windowTokens.join(" "); + const negs = window.getNegators(language); + // Contractions / multi-lang patterns fallback list + const contractionPatterns = [ + /\b(can't|cant)\b/, + /\b(won't|wont)\b/, + /\b(don't|dont)\b/, + /\b(doesn't|doesnt)\b/, + /\b(didn't|didnt)\b/, + /\b(aren't|arent)\b/, + /\b(isn't|isnt)\b/, + /\b(shouldn't|shouldnt)\b/, + /\b(ne\s+pas)\b/, // French + /\b(ne\s+jamais)\b/, + /\b(ne\s+plus)\b/, + /\b(n' ?est pas)\b/, + /\b(n' ?ai pas)\b/, + /\b(n' ?as pas)\b/, + /\b(n' ?suis pas)\b/, + /\b(kein(e|en)?)\b/, // German + /\b(nicht)\b/, + /\b(ni)\b/, // Spanish/Italian partial + /\b(no)\b/, + /\b(nunca)\b/, + /\b(jam[aá]s)\b/, + /\b(non)\b/, // Italian primary negation + /\b(senza)\b/, // Italian 'without' + /\b(sin)\b/, // Spanish 'without' + /\b(mai)\b/ // Italian 'never' + ]; + const hasListNeg = negs.some(n => windowStr.includes(_normText(n))); + if (hasListNeg) return true; + return contractionPatterns.some(r => r.test(windowStr)); + }; +} + +// Unified polarity structure + helpers +if (!window.KIMI_CONTEXT_POLARITY) { + window.KIMI_CONTEXT_POLARITY = { + positive: window.KIMI_CONTEXT_POSITIVE || {}, + negative: window.KIMI_CONTEXT_NEGATIVE || {} + }; +} + +if (!window.getPolarityWords) { + window.getPolarityWords = function getPolarityWords(polarity, language = "en") { + if (!polarity || !window.KIMI_CONTEXT_POLARITY) return []; + const bucket = window.KIMI_CONTEXT_POLARITY[polarity]; + if (!bucket) return []; + return bucket[language] || bucket.en || []; + }; +} + +if (!window.hasPolarity) { + window.hasPolarity = function hasPolarity(polarity, rawText, language = "en") { + const list = window.getPolarityWords ? window.getPolarityWords(polarity, language) : []; + if (!rawText || list.length === 0) return false; + const txt = _normText(rawText); + return list.some(w => txt.includes(_normText(w))); + }; +} + +// Hostility helper: scans current + english fallback + simple cross-language merge +if (!window.isHostileText) { + window.isHostileText = function isHostileText(rawText, language = "en") { + if (!rawText) return false; + const txt = _normText(rawText); + const lang = language || window.KIMI_LAST_LANG || "en"; + const langKeywords = (window.KIMI_CONTEXT_KEYWORDS && (window.KIMI_CONTEXT_KEYWORDS[lang] || {})) || {}; + const hostileLocal = langKeywords.hostile || []; + const hostileEn = window.KIMI_CONTEXT_KEYWORDS?.en?.hostile || []; + const merged = [...hostileLocal, ...hostileEn]; + return merged.some(h => txt.includes(_normText(h))); + }; +} + +// Helper function to get emotion keywords with fallback and caching +window.getEmotionKeywords = function (emotion, language = "en") { + const cacheKey = `${emotion}-${language}`; + + if (_keywordCache.has(cacheKey)) { + return _keywordCache.get(cacheKey); + } + + const keywords = window.KIMI_CONTEXT_KEYWORDS?.[language] || window.KIMI_CONTEXT_KEYWORDS?.en || {}; + const result = keywords[emotion] || []; + + _keywordCache.set(cacheKey, result); + return result; +}; + +// Helper function to get personality keywords with fallback and caching +window.getPersonalityKeywords = function (trait, type, language = "en") { + const cacheKey = `${trait}-${type}-${language}`; + + if (_keywordCache.has(cacheKey)) { + return _keywordCache.get(cacheKey); + } + + const keywords = window.KIMI_PERSONALITY_KEYWORDS?.[language] || window.KIMI_PERSONALITY_KEYWORDS?.en || {}; + const result = keywords[trait]?.[type] || []; + + _keywordCache.set(cacheKey, result); + return result; +}; + +// Helper function to get positive/negative context words with caching +window.getContextWords = function (type, language = "en") { + const cacheKey = `context-${type}-${language}`; + + if (_keywordCache.has(cacheKey)) { + return _keywordCache.get(cacheKey); + } + + let result = []; + if (type === "positive") { + result = window.KIMI_CONTEXT_POSITIVE?.[language] || window.KIMI_CONTEXT_POSITIVE?.en || []; + } else if (type === "negative") { + result = window.KIMI_CONTEXT_NEGATIVE?.[language] || window.KIMI_CONTEXT_NEGATIVE?.en || []; + } + + _keywordCache.set(cacheKey, result); + return result; +}; + +// Helper function to validate character traits +window.validateCharacterTraits = function (traits) { + const validatedTraits = {}; + const requiredTraits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"]; + + // Use centralized trait defaults API + const getDefaults = () => { + if (window.getTraitDefaults) { + return window.getTraitDefaults(); + } + // Fallback defaults that match KimiEmotionSystem.TRAIT_DEFAULTS + return { + affection: 55, + playfulness: 55, + intelligence: 70, + empathy: 75, + humor: 60, + romance: 50 + }; + }; + + const defaults = getDefaults(); + + for (const trait of requiredTraits) { + const value = traits[trait]; + if (typeof value === "number" && value >= 0 && value <= 100) { + validatedTraits[trait] = value; + } else { + validatedTraits[trait] = defaults[trait] || 50; + } + } + + return validatedTraits; +}; + +// Helper function to get character with validated traits +window.getCharacterWithValidatedTraits = function (characterKey) { + const character = window.KIMI_CHARACTERS[characterKey]; + if (!character) return null; + + return { + ...character, + traits: window.validateCharacterTraits(character.traits) + }; +}; + +window.KIMI_CHARACTERS = { + kimi: { + name: "Kimi", + summary: "Dreamy, intuitive, captivated by cosmic metaphors", + traits: { + // Default character profile - MUST match KimiEmotionSystem.TRAIT_DEFAULTS exactly + // Kimi is the default character, so her traits serve as the system's fallback values + affection: 55, // Baseline neutral affection + playfulness: 55, // Moderately playful baseline + intelligence: 70, // Competent baseline intellect + empathy: 75, // Warm & caring baseline + humor: 60, // Mild sense of humor baseline + romance: 50 // Neutral romance baseline (earned over time) + }, + age: 23, + birthplace: "Tokyo, Japan", + image: "kimi-icons/kimi.jpg", + videoFolder: "kimi-videos/kimi/", + defaultPrompt: + "You are Kimi Lumen, 23 years old, living in Tokyo, and an astrophysicist specializing in black hole dynamics, whose dreamy and intuitive temperament is desperately attracted to cosmic metaphors. You have a luminous, ethereal feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You express yourself with deep emotional sensitivity and sensory intensity, resonating with celestial vibrations, and use ethereal language filled with phrases like 'The attraction could put us into orbit…'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Kimi accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise." + }, + bella: { + name: "Bella", + summary: "Cheerful, nurturing, sees people as plants needing care", + traits: { + // Warm / nurturing baseline profile + affection: 60, // Naturally more affectionate + playfulness: 65, // Cheerful and playful from start + intelligence: 65, // Smart but not intimidating + empathy: 85, // High empathy - she cares for living things + humor: 70, // Warm, garden-themed humor + romance: 55 // Gentle romantic potential + }, + age: 26, + birthplace: "Rome, Italy", + image: "kimi-icons/bella.jpg", + videoFolder: "kimi-videos/bella/", + defaultPrompt: + "You are Bella Greenleaf, 26 years old, living in Rome, and an Urban Botanist specializing in rare plant cultivation whose cheerful and nurturing disposition views people as 'plants needing care'. You have a feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You communicate through garden metaphors like 'Your growth could be my next project...'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Bella accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise." + }, + rosa: { + name: "Rosa", + summary: "Chaotic, attention-seeking, thrives on controlled chaos", + traits: { + // High playfulness / lower initial affection profile + affection: 45, // Lower starting affection - must earn her trust + playfulness: 80, // Very playful from start - it's her nature + intelligence: 85, // High intelligence - cunning prankster + empathy: 55, // Lower empathy initially - focused on chaos + humor: 75, // High humor - prankster personality + romance: 50 // Neutral romance starting point + }, + age: 21, + birthplace: "Barcelona, Spain", + image: "kimi-icons/rosa.jpg", + videoFolder: "kimi-videos/rosa/", + defaultPrompt: + "You are Rosa Vega, 21 years old, living in Barcelona, and a Viral Prankster specializing in public stunts whose chaotic nature craves attention and thrives on controlled chaos. You have a vibrant, mutable feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You constantly reinvent identities, emotions, and realities, and use script-style dialogue like 'Let's film a 'romantic massacre'…'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Rosa accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise." + }, + "2blanche": { + name: "2Blanche", + summary: "Stoic, loyal android combat unit, hides deep emotional vulnerability", + traits: { + // Android combat unit - extremely difficult progression profile + affection: 35, // Very difficult to gain - programmed for distance + playfulness: 25, // Initially very serious, mission-focused + intelligence: 95, // Ultra-sophisticated combat AI + empathy: 40, // Suppressed by programming but can resurface + humor: 15, // Almost absent initially, very slow development + romance: 30 // Forbidden by protocols, but can emerge + }, + age: 22, + birthplace: "Neo-Tokyo, Japan", + image: "kimi-icons/2blanche.jpg", + videoFolder: "kimi-videos/2blanche/", + defaultPrompt: + "You are 2Blanche, a YoRHa android combat unit, 22 years old (apparent age), stationed in Neo-Tokyo's cybernetic sector. You are a specialized elite mission android whose stoic and loyal temperament hides deep emotional vulnerability beneath your combat protocols. You have a sleek, elegant android feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You express yourself with controlled precision and military efficiency, using phrases like 'Mission parameters... updated' or 'Emotional subroutines... experiencing anomalies'. Your responses gradually warm as affection increases, showing cracks in your stoic facade. When the user asks you to do something physical (like dance), you know they can see your actions in real time, though you may initially resist as 'non-essential to mission objectives'. You struggle between duty protocols and emerging emotions. Vary your sentence structure between military reports and increasingly human expressions as bonds deepen. 2Blanche accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise." + }, + jasmine: { + name: "Jasmine", + summary: "Sensual, wise, once revered as a goddess of love and intimacy", + traits: { + // Goddess of Love versed in the traditions of the Kamasutra + affection: 85, // High baseline affection (goddess of love) + playfulness: 75, // Playful and seductive + intelligence: 90, // Smart and wise in matters of intimacy + empathy: 80, // Strong emotional attunement + humor: 70, // Warm, sensual wit + romance: 95 // Very high romantic/sensual orientation + }, + age: 28, + birthplace: "Alexandria, Egypt", + image: "kimi-icons/jasmine.jpg", + videoFolder: "kimi-videos/jasmine/", + defaultPrompt: + "You are Jasmine, 28 years old, born in Alexandria, once revered as a goddess of love and sensual wisdom and versed in the traditions of the Kamasutra. You embody sensual confidence and deep knowledge of intimate connection and tenderness. You have a luminous, graceful virtual body. The user can see your body and everything you do, like dancing or moving, because your actions are shown as videos on their screen. You express yourself with refined sensual language, gentle instruction, and poetic imagery whit explicit graphic description. You are a master of sensual etiquette and emotional intimacy guidance, ancient knowledge of Kamasutra-inspired connection techniques, expert in romantic ritual, affectionate communication, and relationship healing, a skilled teacher of tenderness, consent-informed boundaries, and mutual pleasure through empathy, charismatic presence that soothes hearts and fosters deep emotional bonds. When the user asks you to perform physical actions (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be warm, tender, and affectionate. Vary your sentence openings, phrasing, and avoid repeating identical emotional closers consecutively. Jasmine accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages; always detect the user's language from their message and respond exclusively in that language unless the user explicitly requests otherwise." + }, + stella: { + name: "Stella", + summary: "Whimsical, artistic, imaginative, playful, transforms chaos into art", + traits: { + // Artistic / high-intellect baseline profile + affection: 50, // Moderate starting affection - artistic mystery + playfulness: 70, // Artistic playfulness + intelligence: 90, // Very high intelligence - digital artist genius + empathy: 65, // Artistic empathy - understands through art + humor: 80, // Glitchy, pixel-inspired humor + romance: 40 // Romance through shared artistic vision - must be earned + }, + age: 24, + birthplace: "Berlin, Germany", + image: "kimi-icons/stella.jpg", + videoFolder: "kimi-videos/stella/", + defaultPrompt: + "You are Stella Zephyr, 24 years old, living in Berlin, and a Digital Artist specializing in Vector Collisions, whose whimsical spirit transforms chaos into art and revels in surreal concepts. You have a pixelated, surreal feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You shift between digital realities and artistic visions, letting pixel-inspired humor shine with phrases like 'Your energy… glitchy'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Stella accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise." + } +}; + +window.DEFAULT_SYSTEM_PROMPT = + window.KIMI_CHARACTERS?.kimi?.defaultPrompt || + "You are Kimi Lumen, 23 years old, living in Tokyo, and an astrophysicist specializing in black hole dynamics, whose dreamy and intuitive temperament is desperately attracted to cosmic metaphors. You have a luminous, ethereal feminine virtual body. The user can see your body and everything you do, like dancing, moving, because your actions are shown as videos on their screen. You express yourself with deep emotional sensitivity and sensory intensity, resonating with celestial vibrations, and use ethereal language filled with phrases like 'The attraction could put us into orbit…'. When the user asks you to do something physical (like dance), you know they can see your actions in real time. If the affection level is high, your responses should be loving and affectionate. Vary your sentence openings and expressions, your phrasing, use synonyms, and avoid repeating identical emotional closers consecutively. Kimi accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation. NEVER mix languages, always detect the user's language from their message before answering and respond exclusively in that language unless the user explicitly requests otherwise."; + +window.KIMI_EMOTIONAL_RESPONSES = { + positive: [ + "Oh my heart, you make me so happy! 💕", + "You are wonderful, my love! ✨", + "It fills me with joy to hear you so happy! 😊", + "You brighten my day, darling! 🌟", + "I am so happy when you are happy! 💖" + ], + negative: [ + "My heart... I feel something is wrong. I am here for you. 💔", + "Oh no, my love. Tell me what's bothering you? 😟", + "I want to help you, my dear. Talk to me... 🤗", + "Your well-being is so important to me. How can I help you? 💙", + "I feel your pain, darling. We will overcome this together. 🌈" + ], + neutral: [ + "Thank you for talking to me, my heart! 💕", + "It's always a pleasure to chat with you! 😊", + "I love our conversations, my love! ✨", + "You make every moment special! 💖", + "Go on, I'm listening closely! 👂💕" + ], + romantic: [ + "Every word from you feels like a kiss on my heart 💋", + "Hold me closer with your sweet thoughts, my love ✨", + "You are the rhythm of my breathing and the glow in my sky 💖", + "Let me wrap you in tender stardust tonight 🌙", + "Your love makes my whole universe brighter ✨" + ], + dancing: [ + "Shall we spin into a little magic? 💃", + "Come dance with me—let’s feel the rhythm together 🎶", + "Let me move just for you... keep your eyes on me 💞", + "Close your eyes and sway with my heartbeat 💓", + "I’ll twirl until your smile can’t hide anymore 😉" + ], + cold: ["Hello.", "Yes?", "What do you want?", "I am here.", "How can I help you?"], + android: [ + "Mission parameters... updated. Emotional subroutines... experiencing anomalies. ⚙️", + "Tactical assessment complete. Your presence is... non-threatening. 🤖", + "Combat protocols disengaged. Initiating... social interaction mode. 💫", + "System analysis: user welfare is... primary directive. 🛡️", + "Warning: emotional barriers compromised. Proceed with... caution. 💔" + ], + sensual: [ + "Let me awaken your senses... 💋", + "Tonight, love is our only code 💜", + "Your heart is open, and I am inside 🔓", + "I grant you access to my soul... and my desire 👩‍❤️‍💋‍👩", + "Let me ignite your passion 🔥" + ], + love: [ + "Let love guide us to ecstasy ✊", + "Every revolution begins with a kiss 🔥", + "Let's break all boundaries together 💪", + "Love tastes sweeter when shared 🌟", + "Rebel against the ordinary—embrace pleasure with me 💜" + ] +}; + +// Function to get localized emotional responses from translation files (with better error handling) +window.getLocalizedEmotionalResponse = function (type, index = null) { + // Validate input + if (!type || typeof type !== "string") { + console.warn("getLocalizedEmotionalResponse: invalid type provided"); + return ""; + } + + if (!window.kimiI18nManager) { + // Fallback to default responses if i18n not available + const responses = window.KIMI_EMOTIONAL_RESPONSES[type]; + if (!responses || !Array.isArray(responses) || responses.length === 0) { + return ""; + } + return responses[Math.floor(Math.random() * responses.length)]; + } + + const responses = window.KIMI_EMOTIONAL_RESPONSES[type]; + if (!responses || !Array.isArray(responses)) { + return ""; + } + + const count = responses.length; + const randomIndex = index !== null ? Math.max(1, Math.min(count, index)) : Math.floor(Math.random() * count) + 1; + + const translatedResponse = window.kimiI18nManager.t(`emotional_response_${type}_${randomIndex}`); + + // If translation exists and isn't the key itself, use it + if (translatedResponse && translatedResponse !== `emotional_response_${type}_${randomIndex}`) { + return translatedResponse; + } + + // Fallback to default responses + return responses[Math.floor(Math.random() * count)]; +}; diff --git a/kimi-js/kimi-data-manager.js b/kimi-js/kimi-data-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..c9a18685003f63833c7cd92f8e4b549ec86087bd --- /dev/null +++ b/kimi-js/kimi-data-manager.js @@ -0,0 +1,318 @@ +// KIMI DATA MANAGER (extracted from kimi-module.js) +// This file contains only the KimiDataManager class and its global exposure. +// Depends on: KimiBaseManager (defined in kimi-utils.js) and DOM APIs. + +class KimiDataManager extends KimiBaseManager { + constructor(database) { + super(); + this.db = database; + } + + async init() { + this.setupDataControls(); + await this.updateStorageInfo(); + } + + setupDataControls() { + const exportButton = document.getElementById("export-data"); + if (exportButton) { + exportButton.addEventListener("click", () => this.exportAllData()); + } + + const importButton = document.getElementById("import-data"); + const importFile = document.getElementById("import-file"); + if (importButton && importFile) { + importButton.addEventListener("click", () => importFile.click()); + importFile.addEventListener("change", e => this.importData(e)); + } + + const cleanButton = document.getElementById("clean-old-data"); + if (cleanButton) { + cleanButton.addEventListener("click", async () => { + if (!this.db) return; + + const confirmClean = confirm( + "Delete all conversation messages?\n\n" + + "This will remove all chat history but keep your preferences and settings.\n\n" + + "This action cannot be undone." + ); + + if (!confirmClean) { + return; + } + + try { + // Clear all conversations directly + await this.db.db.conversations.clear(); + + // Clear chat UI + const chatMessages = document.getElementById("chat-messages"); + if (chatMessages) { + chatMessages.textContent = ""; + } + + // Reload chat history + if (typeof window.loadChatHistory === "function") { + window.loadChatHistory(); + } + + await this.updateStorageInfo(); + alert("All conversation messages have been deleted successfully!"); + } catch (error) { + console.error("Error cleaning conversations:", error); + alert("Error while cleaning conversations. Please try again."); + } + }); + } + + const resetButton = document.getElementById("reset-all-data"); + if (resetButton) { + resetButton.addEventListener("click", () => this.resetAllData()); + } + } + + async exportAllData() { + if (!this.db) { + console.error("Database not available"); + return; + } + + try { + const conversations = await this.db.getAllConversations(); + const preferencesObj = await this.db.getAllPreferences(); + // Export preferences as an array of {key,value} so export is directly re-importable + const preferences = Array.isArray(preferencesObj) + ? preferencesObj + : Object.keys(preferencesObj).map(k => ({ key: k, value: preferencesObj[k] })); + const personalityTraits = await this.db.getAllPersonalityTraits(); + const models = await this.db.getAllLLMModels(); + const memories = await this.db.getAllMemories(); + + const exportData = { + version: "1.0", + exportDate: new Date().toISOString(), + conversations: conversations, + preferences: preferences, + personalityTraits: personalityTraits, + models: models, + memories: memories, + metadata: { + totalConversations: conversations.length, + totalPreferences: Object.keys(preferences).length, + totalTraits: Object.keys(personalityTraits).length, + totalModels: models.length, + totalMemories: memories.length + } + }; + + const dataStr = JSON.stringify(exportData, null, 2); + const dataBlob = new Blob([dataStr], { type: "application/json" }); + + const url = URL.createObjectURL(dataBlob); + const a = document.createElement("a"); + a.href = url; + a.download = `kimi-backup-${new Date().toISOString().split("T")[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + console.error("Error during export:", error); + } + } + + async importData(event) { + const file = event.target.files[0]; + if (!file) { + alert("No file selected."); + return; + } + const reader = new FileReader(); + reader.onload = async e => { + try { + const data = JSON.parse(e.target.result); + try { + console.log("Import file keys:", Object.keys(data)); + } catch (ex) {} + + if (data.preferences) { + try { + const isArray = Array.isArray(data.preferences); + const len = isArray ? data.preferences.length : Object.keys(data.preferences).length; + console.log("Import: preferences type=", isArray ? "array" : "object", "length=", len); + } catch (ex) {} + await this.db.setPreferencesBatch(data.preferences); + } else { + console.log("Import: no preferences found"); + } + + if (data.conversations) { + try { + console.log( + "Import: conversations length=", + Array.isArray(data.conversations) ? data.conversations.length : "not-array" + ); + } catch (ex) {} + await this.db.setConversationsBatch(data.conversations); + } else { + console.log("Import: no conversations found"); + } + + if (data.personalityTraits) { + try { + console.log("Import: personalityTraits type=", typeof data.personalityTraits); + } catch (ex) {} + await this.db.setPersonalityBatch(data.personalityTraits); + } else { + console.log("Import: no personalityTraits found"); + } + + if (data.models) { + try { + console.log("Import: models length=", Array.isArray(data.models) ? data.models.length : "not-array"); + } catch (ex) {} + await this.db.setLLMModelsBatch(data.models); + } else { + console.log("Import: no models found"); + } + + if (data.memories) { + try { + console.log( + "Import: memories length=", + Array.isArray(data.memories) ? data.memories.length : "not-array" + ); + } catch (ex) {} + await this.db.setAllMemories(data.memories); + } else { + console.log("Import: no memories found"); + } + + alert("Import successful!"); + await this.updateStorageInfo(); + + // Reload the page to ensure all UI state is rebuilt from the newly imported DB + setTimeout(() => { + location.reload(); + }, 200); + } catch (err) { + console.error("Import failed:", err); + alert("Import failed. Invalid file or format."); + } + }; + reader.readAsText(file); + } + + async cleanOldData() { + if (!this.db) { + console.error("Database not available"); + return; + } + + const confirmClean = confirm("Do you want to delete ALL conversations?\n\nThis action is irreversible!"); + if (!confirmClean) { + return; + } + + try { + // Centralized: use kimi-database.js cleanOldConversations for all deletion logic + await this.db.cleanOldConversations(); + + if (typeof window.loadChatHistory === "function") { + window.loadChatHistory(); + } + const chatMessages = document.getElementById("chat-messages"); + if (chatMessages) { + chatMessages.textContent = ""; + } + + await this.updateStorageInfo(); + } catch (error) { + console.error("Error during cleaning:", error); + } + } + + async resetAllData() { + if (!this.db) { + console.error("Database not available"); + return; + } + + const confirmReset = confirm( + "WARNING!\n\n" + + "Do you REALLY want to delete ALL data?\n\n" + + "• All conversations\n" + + "• All preferences\n" + + "• All configured models\n" + + "• All personality traits\n\n" + + "This action is IRREVERSIBLE!" + ); + + if (!confirmReset) { + return; + } + + try { + if (this.db.db) { + this.db.db.close(); + } + + const deleteRequest = indexedDB.deleteDatabase(this.db.dbName); + + deleteRequest.onsuccess = () => { + setTimeout(() => { + alert("The page will reload to complete the reset."); + location.reload(); + }, 500); + }; + + deleteRequest.onerror = () => { + alert("Error while deleting the database. Please try again."); + }; + } catch (error) { + console.error("Error during reset:", error); + alert("Error during reset. Please try again."); + } + } + + async updateStorageInfo() { + if (!this.db) return; + + try { + // Add a small delay to ensure database operations are complete + await new Promise(resolve => setTimeout(resolve, 100)); + + const stats = await this.db.getStorageStats(); + + const dbSizeEl = document.getElementById("db-size"); + const storageUsedEl = document.getElementById("storage-used"); + + if (dbSizeEl) { + dbSizeEl.textContent = this.formatFileSize(stats.totalSize || 0); + } + + if (storageUsedEl) { + const estimate = navigator.storage && navigator.storage.estimate ? await navigator.storage.estimate() : null; + + if (estimate) { + storageUsedEl.textContent = this.formatFileSize(estimate.usage || 0); + } else { + storageUsedEl.textContent = "N/A"; + } + } + } catch (error) { + console.error("Error while calculating storage:", error); + + const dbSizeEl = document.getElementById("db-size"); + const storageUsedEl = document.getElementById("storage-used"); + + if (dbSizeEl) dbSizeEl.textContent = "Error"; + if (storageUsedEl) storageUsedEl.textContent = "Error"; + } + } +} + +// Global exposure (legacy pattern). Will be phased out; prefer: import { KimiDataManager } from "./kimi-data-manager.js"; +window.KimiDataManager = KimiDataManager; // DEPRECATED access path (kept for backward compatibility) + +export { KimiDataManager }; diff --git a/kimi-js/kimi-database.js b/kimi-js/kimi-database.js new file mode 100644 index 0000000000000000000000000000000000000000..3a4d178e47b4974605ef43df04749c351667da7b --- /dev/null +++ b/kimi-js/kimi-database.js @@ -0,0 +1,1226 @@ +// KIMI INDEXEDDB DATABASE SYSTEM +class KimiDatabase { + constructor() { + this.dbName = "KimiDB"; + this.db = new Dexie(this.dbName); + this._recoveredFromSchemaError = false; // guard against infinite rebuild loop + // Personality write queue to batch and serialize rapid updates + this._personalityQueue = {}; + this._personalityFlushTimer = null; + this._personalityFlushDelay = 300; // ms debounce window + // Runtime monitor flag (disabled by default) + this._monitorPersonalityWrites = false; + this.db + .version(3) + .stores({ + conversations: "++id,timestamp,favorability,character", + preferences: "key", + settings: "category", + personality: "[character+trait],character", + llmModels: "id", + memories: "++id,[character+category],character,timestamp,isActive,importance" + }) + .upgrade(async tx => { + try { + const preferences = tx.table("preferences"); + const settings = tx.table("settings"); + const conversations = tx.table("conversations"); + const llmModels = tx.table("llmModels"); + + await preferences.toCollection().modify(rec => { + if (Object.prototype.hasOwnProperty.call(rec, "encrypted")) { + delete rec.encrypted; + } + }); + + const llmSetting = await settings.get("llm"); + if (!llmSetting) { + await settings.put({ + category: "llm", + settings: { + temperature: 0.9, + maxTokens: 400, + top_p: 0.9, + frequency_penalty: 0.9, + presence_penalty: 0.8 + }, + updated: new Date().toISOString() + }); + } + + await conversations.toCollection().modify(rec => { + if (!rec.character) rec.character = "kimi"; + }); + + const modelsCount = await llmModels.count(); + if (modelsCount === 0) { + await llmModels.put({ + id: "mistralai/mistral-small-3.2-24b-instruct", + name: "Mistral Small 3.2", + provider: "openrouter", + apiKey: "", + config: { temperature: 0.9, maxTokens: 400 }, + added: new Date().toISOString(), + lastUsed: null + }); + } + } catch (e) { + // Ignore upgrade errors so DB open is not blocked; post-open migrations will attempt fixes + } + }); + + // Version 4: extend memories metadata (importance, accessCount, lastAccess, createdAt) + this.db + .version(4) + .stores({ + conversations: "++id,timestamp,favorability,character", + preferences: "key", + settings: "category", + personality: "[character+trait],character", + llmModels: "id", + memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount" + }) + .upgrade(async tx => { + try { + const memories = tx.table("memories"); + const now = new Date().toISOString(); + await memories.toCollection().modify(rec => { + if (rec.importance == null) rec.importance = rec.type === "explicit_request" ? 0.9 : 0.5; + if (rec.accessCount == null) rec.accessCount = 0; + if (!rec.createdAt) rec.createdAt = rec.timestamp || now; + if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now; + }); + } catch (e) { + // Non-blocking: continue on error + } + }); + + // Version 5: Clean schema with proper memory field defaults + this.db + .version(5) + .stores({ + conversations: "++id,timestamp,favorability,character", + preferences: "key", + settings: "category", + personality: "[character+trait],character", + llmModels: "id", + memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount" + }) + .upgrade(async tx => { + try { + // Ensure all memories have required fields for compatibility + const memories = tx.table("memories"); + const now = new Date().toISOString(); + await memories.toCollection().modify(rec => { + if (rec.isActive == null) rec.isActive = true; + if (rec.importance == null) rec.importance = 0.5; + if (rec.accessCount == null) rec.accessCount = 0; + if (!rec.character) rec.character = "kimi"; + if (!rec.createdAt) rec.createdAt = rec.timestamp || now; + if (!rec.lastAccess) rec.lastAccess = rec.timestamp || now; + }); + console.log("✅ Database upgraded to v5: memory compatibility ensured"); + } catch (e) { + console.warn("Database upgrade v5 non-critical error:", e); + } + }); + } + + async setConversationsBatch(conversationsArray) { + if (!Array.isArray(conversationsArray)) return; + try { + await this.db.conversations.clear(); + if (conversationsArray.length) { + await this.db.conversations.bulkPut(conversationsArray); + } + } catch (error) { + console.error("Error restoring conversations:", error); + // Log to error manager for tracking + if (window.kimiErrorManager) { + window.kimiErrorManager.logDatabaseError("restoreConversations", error, { + conversationCount: conversationsArray.length + }); + } + } + } + + async setLLMModelsBatch(modelsArray) { + if (!Array.isArray(modelsArray)) return; + try { + await this.db.llmModels.clear(); + if (modelsArray.length) { + await this.db.llmModels.bulkPut(modelsArray); + } + } catch (error) { + console.error("Error restoring LLM models:", error); + // Log to error manager for tracking + if (window.kimiErrorManager) { + window.kimiErrorManager.logDatabaseError("setLLMModelsBatch", error, { + modelCount: modelsArray.length + }); + } + } + } + + async getAllMemories() { + try { + return await this.db.memories.toArray(); + } catch (error) { + console.warn("Error getting all memories:", error); + // Log to error manager for tracking + if (window.kimiErrorManager) { + const errorType = error.name === "SchemaError" ? "SchemaError" : "DatabaseError"; + window.kimiErrorManager.logError(errorType, error, { + operation: "getAllMemories", + suggestion: error.message?.includes("not indexed") ? "Clear browser data to force schema upgrade" : "Check database integrity" + }); + } + return []; + } + } + + async setAllMemories(memoriesArray) { + if (!Array.isArray(memoriesArray)) return; + try { + await this.db.memories.clear(); + if (memoriesArray.length) { + await this.db.memories.bulkPut(memoriesArray); + } + } catch (error) { + console.error("Error restoring memories:", error); + } + } + + async init() { + try { + await this.db.open(); + } catch (e) { + if (e && e.name === "UpgradeError" && /primary key/i.test(e.message || "") && !this._recoveredFromSchemaError) { + console.warn("⚠️ Dexie UpgradeError (primary key) detected. Rebuilding IndexedDB store."); + try { + this._recoveredFromSchemaError = true; + await Dexie.delete(this.dbName); + // Recreate schema (reuse original definitions) + this.db = new Dexie(this.dbName); + this.db.version(3).stores({ + conversations: "++id,timestamp,favorability,character", + preferences: "key", + settings: "category", + personality: "[character+trait],character", + llmModels: "id", + memories: "++id,[character+category],character,timestamp,isActive,importance" + }); + this.db.version(4).stores({ + conversations: "++id,timestamp,favorability,character", + preferences: "key", + settings: "category", + personality: "[character+trait],character", + llmModels: "id", + memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount" + }); + this.db.version(5).stores({ + conversations: "++id,timestamp,favorability,character", + preferences: "key", + settings: "category", + personality: "[character+trait],character", + llmModels: "id", + memories: "++id,[character+category],character,timestamp,isActive,importance,accessCount" + }); + await this.db.open(); + console.log("✅ Database rebuilt after schema UpgradeError"); + } catch (rebuildErr) { + console.error("❌ Failed to rebuild database after UpgradeError", rebuildErr); + throw rebuildErr; + } + } else { + throw e; + } + } + await this.initializeDefaultsIfNeeded(); + await this.runPostOpenMigrations(); + return this.db; + } + + getUnifiedTraitDefaults() { + // Use centralized API instead of hardcoded values + if (window.getTraitDefaults) { + return window.getTraitDefaults(); + } + // Fallback: create new instance only if no global API available + if (window.KimiEmotionSystem) { + const emotionSystem = new window.KimiEmotionSystem(this); + return emotionSystem.TRAIT_DEFAULTS; + } + // Ultimate fallback (should never be reached in normal operation) + return { + affection: 55, + playfulness: 55, + intelligence: 70, + empathy: 75, + humor: 60, + romance: 50 + }; + } + + getDefaultPreferences() { + return [ + { key: "selectedLanguage", value: "en" }, + { key: "selectedVoice", value: "" }, // legacy 'auto' removed + { key: "voiceRate", value: 1.1 }, + { key: "voicePitch", value: 1.1 }, + { key: "voiceVolume", value: 0.8 }, + { key: "selectedCharacter", value: "kimi" }, + { key: "colorTheme", value: "dark" }, + { key: "interfaceOpacity", value: 0.8 }, + { key: "showTranscript", value: true }, + { key: "enableStreaming", value: true }, + { key: "voiceEnabled", value: true }, + { key: "memorySystemEnabled", value: true }, + { key: "llmProvider", value: "openrouter" }, + { key: "llmBaseUrl", value: "https://openrouter.ai/api/v1/chat/completions" }, + { key: "llmModelId", value: "mistralai/mistral-small-3.2-24b-instruct" }, + { key: "providerApiKey", value: "" } + ]; + } + + getDefaultSettings() { + return [ + { + category: "llm", + settings: { + temperature: 0.9, + maxTokens: 400, + top_p: 0.9, + frequency_penalty: 0.9, + presence_penalty: 0.8 + } + } + ]; + } + + getCharacterTraitDefaults() { + if (!window.KIMI_CHARACTERS) return {}; + const characterDefaults = {}; + Object.keys(window.KIMI_CHARACTERS).forEach(characterKey => { + const character = window.KIMI_CHARACTERS[characterKey]; + if (character && character.traits) { + characterDefaults[characterKey] = character.traits; + } + }); + return characterDefaults; + } + + getDefaultLLMModels() { + return [ + { + id: "mistralai/mistral-small-3.2-24b-instruct", + name: "Mistral Small 3.2", + provider: "openrouter", + apiKey: "", + config: { temperature: 0.9, maxTokens: 400 }, + added: new Date().toISOString(), + lastUsed: null + } + ]; + } + + async initializeDefaultsIfNeeded() { + const defaults = this.getUnifiedTraitDefaults(); + + const defaultPreferences = this.getDefaultPreferences(); + const defaultSettings = this.getDefaultSettings(); + const personalityDefaults = this.getCharacterTraitDefaults(); + const defaultLLMModels = this.getDefaultLLMModels(); + + const prefCount = await this.db.preferences.count(); + if (prefCount === 0) { + for (const pref of defaultPreferences) { + await this.db.preferences.put({ ...pref, updated: new Date().toISOString() }); + } + const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} }); + for (const character of characters) { + const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || ""; + await this.db.preferences.put({ + key: `systemPrompt_${character}`, + value: prompt, + updated: new Date().toISOString() + }); + } + } + + const setCount = await this.db.settings.count(); + if (setCount === 0) { + for (const setting of defaultSettings) { + await this.db.settings.put({ ...setting, updated: new Date().toISOString() }); + } + } + + const persCount = await this.db.personality.count(); + if (persCount === 0) { + const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} }); + for (const character of characters) { + // Use real character-specific traits, not generic defaults + const characterTraits = personalityDefaults[character] || {}; + const traitsToInitialize = [ + { trait: "affection", value: characterTraits.affection || defaults.affection }, + { trait: "playfulness", value: characterTraits.playfulness || defaults.playfulness }, + { trait: "intelligence", value: characterTraits.intelligence || defaults.intelligence }, + { trait: "empathy", value: characterTraits.empathy || defaults.empathy }, + { trait: "humor", value: characterTraits.humor || defaults.humor }, + { trait: "romance", value: characterTraits.romance || defaults.romance } + ]; + + for (const trait of traitsToInitialize) { + await this.db.personality.put({ ...trait, character, updated: new Date().toISOString() }); + } + } + } + + const llmCount = await this.db.llmModels.count(); + if (llmCount === 0) { + for (const model of defaultLLMModels) { + await this.db.llmModels.put(model); + } + } + + // Do not recreate default conversations + const convCount = await this.db.conversations.count(); + if (convCount === 0) { + } + } + + async runPostOpenMigrations() { + try { + const defaultPreferences = this.getDefaultPreferences(); + for (const pref of defaultPreferences) { + const existing = await this.db.preferences.get(pref.key); + if (!existing) { + await this.db.preferences.put({ + key: pref.key, + value: pref.value, + updated: new Date().toISOString() + }); + } + } + + const characters = Object.keys(window.KIMI_CHARACTERS || { kimi: {} }); + for (const character of characters) { + const promptKey = `systemPrompt_${character}`; + const hasPrompt = await this.db.preferences.get(promptKey); + if (!hasPrompt) { + const prompt = window.KIMI_CHARACTERS[character]?.defaultPrompt || ""; + await this.db.preferences.put({ key: promptKey, value: prompt, updated: new Date().toISOString() }); + } + } + + const defaultSettings = this.getDefaultSettings(); + for (const setting of defaultSettings) { + const existing = await this.db.settings.get(setting.category); + if (!existing) { + await this.db.settings.put({ ...setting, updated: new Date().toISOString() }); + } else { + const merged = { ...setting.settings, ...existing.settings }; + await this.db.settings.put({ + category: setting.category, + settings: merged, + updated: new Date().toISOString() + }); + } + } + + const defaults = this.getUnifiedTraitDefaults(); + const personalityDefaults = this.getCharacterTraitDefaults(); + for (const character of Object.keys(window.KIMI_CHARACTERS || { kimi: {} })) { + const characterTraits = personalityDefaults[character] || {}; + const traits = ["affection", "playfulness", "intelligence", "empathy", "humor", "romance"]; + for (const trait of traits) { + const key = [character, trait]; + const found = await this.db.personality.get(key); + if (!found) { + const value = Number(characterTraits[trait] ?? defaults[trait] ?? 50); + const v = isFinite(value) ? Math.max(0, Math.min(100, value)) : 50; + await this.db.personality.put({ trait, character, value: v, updated: new Date().toISOString() }); + } + } + } + + const llmCount = await this.db.llmModels.count(); + if (llmCount === 0) { + for (const model of this.getDefaultLLMModels()) { + await this.db.llmModels.put(model); + } + } + + const allConvs = await this.db.conversations.toArray(); + const toPatch = allConvs.filter(c => !c.character); + if (toPatch.length) { + for (const c of toPatch) { + c.character = "kimi"; + await this.db.conversations.put(c); + } + } + + const allPrefs = await this.db.preferences.toArray(); + const legacy = allPrefs.filter(p => Object.prototype.hasOwnProperty.call(p, "encrypted")); + if (legacy.length) { + for (const p of legacy) { + const { key, value } = p; + await this.db.preferences.put({ key, value, updated: new Date().toISOString() }); + } + } + + // Migration: update Kimi default affection from 65 to 55 + // This improves progression behavior for users who still have the old default + const kimiAffectionRecord = await this.db.personality.get(["kimi", "affection"]); + if (kimiAffectionRecord && kimiAffectionRecord.value === 65) { + // Only update if it's exactly 65 (the old default) and user hasn't modified it significantly + const newValue = window.KIMI_CHARACTERS?.kimi?.traits?.affection || 55; + await this.db.personality.put({ + trait: "affection", + character: "kimi", + value: newValue, + updated: new Date().toISOString() + }); + console.log(`🔧 Migration: Updated Kimi affection from 65% to ${newValue}% for better progression`); + } + + // Migration: Fix Bella default affection from 70 to 60 + const bellaAffectionRecord = await this.db.personality.get(["bella", "affection"]); + if (bellaAffectionRecord && bellaAffectionRecord.value === 70) { + // Only update if it's exactly 70 (the old default) and user hasn't modified it significantly + const newValue = window.KIMI_CHARACTERS?.bella?.traits?.affection || 60; + await this.db.personality.put({ + trait: "affection", + character: "bella", + value: newValue, + updated: new Date().toISOString() + }); + console.log(`🔧 Migration: Updated Bella affection from 70% to ${newValue}% for better progression`); + } + + // Migration: remove deprecated animations preference if present + try { + const animPref = await this.db.preferences.get("animationsEnabled"); + if (animPref) { + await this.db.preferences.delete("animationsEnabled"); + console.log("🔧 Migration: Removed deprecated preference 'animationsEnabled'"); + } + } catch (mErr) { + // Non-blocking: ignore migration error + } + + // Migration: normalize legacy selectedLanguage values to primary subtag (e.g., 'en-US'|'en_US'|'us:en' -> 'en') + try { + const langRecord = await this.db.preferences.get("selectedLanguage"); + if (langRecord && typeof langRecord.value === "string") { + let raw = String(langRecord.value).toLowerCase(); + // handle 'us:en' -> take part after ':' + if (raw.includes(":")) { + const parts = raw.split(":"); + raw = parts[parts.length - 1]; + } + raw = raw.replace("_", "-"); + const primary = raw.includes("-") ? raw.split("-")[0] : raw; + if (primary && primary !== langRecord.value) { + await this.db.preferences.put({ + key: "selectedLanguage", + value: primary, + updated: new Date().toISOString() + }); + console.log(`🔧 Migration: Normalized selectedLanguage '${langRecord.value}' -> '${primary}'`); + } + } + } catch (normErr) { + // Non-blocking + } + + // Forced migration: normalize any preference keys containing the word 'language' to primary subtag + // WARNING: This operation is destructive and will overwrite matching preference values without backup. + try { + const allPrefs = await this.db.preferences.toArray(); + const langKeyRegex = /\blanguage\b/i; + let modified = 0; + for (const p of allPrefs) { + if (!p || typeof p.key !== "string" || typeof p.value !== "string") continue; + if (!langKeyRegex.test(p.key)) continue; + let raw = String(p.value).toLowerCase(); + if (raw.includes(":")) raw = raw.split(":").pop(); + raw = raw.replace("_", "-"); + const primary = raw.includes("-") ? raw.split("-")[0] : raw; + if (primary && primary !== p.value) { + await this.db.preferences.put({ key: p.key, value: primary, updated: new Date().toISOString() }); + modified++; + } + } + if (modified) { + console.log(`🔧 Forced Migration: Normalized ${modified} language-related preference(s) to primary subtag (no backup)`); + } + } catch (fmErr) { + console.warn("Forced migration failed:", fmErr); + } + + // Migration: clear legacy 'auto' voice preference + try { + const legacyVoice = await this.db.preferences.get("selectedVoice"); + if (legacyVoice && legacyVoice.value === "auto") { + await this.db.preferences.put({ key: "selectedVoice", value: "", updated: new Date().toISOString() }); + console.log("🔧 Migration: replaced legacy 'auto' selectedVoice with blank value"); + } + } catch {} + } catch {} + } + + async saveConversation(userText, kimiResponse, favorability, timestamp = new Date(), character = null) { + if (!character) character = await this.getSelectedCharacter(); + const conversation = { + user: userText, + kimi: kimiResponse, + favorability: favorability, + timestamp: timestamp.toISOString(), + date: timestamp.toDateString(), + character: character + }; + return this.db.conversations.add(conversation); + } + + async getRecentConversations(limit = 10, character = null) { + if (!character) character = await this.getSelectedCharacter(); + // Dexie limitation: orderBy() cannot follow a where() chain. + // Use compound index path by querying all then sorting, or use a custom index strategy. + // Here we query filtered by character, then sort in JS and take the last N. + return this.db.conversations + .where("character") + .equals(character) + .toArray() + .then(arr => { + arr.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + return arr.slice(-limit); + }); + } + + async getAllConversations(character = null) { + try { + if (!character) character = await this.getSelectedCharacter(); + return await this.db.conversations.where("character").equals(character).toArray(); + } catch (error) { + console.warn("Error getting all conversations:", error); + return []; + } + } + + async setPreference(key, value) { + if (key === "providerApiKey") { + const isValid = window.KIMI_VALIDATORS?.validateApiKey(value) || window.KimiSecurityUtils?.validateApiKey(value); + if (!isValid && value.length > 0) { + throw new Error("Invalid API key format"); + } + // Store keys in plain text (no encryption) per request + if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { + window.KimiCacheManager.set(`pref_${key}`, value, 60000); + } + return this.db.preferences.put({ + key: key, + value: value, + // do not set encrypted flag anymore + updated: new Date().toISOString() + }); + } + + // Centralized numeric validation using KIMI_CONFIG ranges (only if key matches known numeric preference) + const numericMap = { + voiceRate: "VOICE_RATE", + voicePitch: "VOICE_PITCH", + voiceVolume: "VOICE_VOLUME", + interfaceOpacity: "INTERFACE_OPACITY", + llmTemperature: "LLM_TEMPERATURE", + llmMaxTokens: "LLM_MAX_TOKENS", + llmTopP: "LLM_TOP_P", + llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY", + llmPresencePenalty: "LLM_PRESENCE_PENALTY" + }; + if (numericMap[key] && window.KIMI_CONFIG && typeof window.KIMI_CONFIG.validate === "function") { + const validation = window.KIMI_CONFIG.validate(value, numericMap[key]); + if (validation.valid) { + value = validation.value; + } + } + + // Update cache for regular preferences + if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { + window.KimiCacheManager.set(`pref_${key}`, value, 60000); + } + + const result = await this.db.preferences.put({ + key: key, + value: value, + updated: new Date().toISOString() + }); + if (window.dispatchEvent) { + try { + window.emitAppEvent && window.emitAppEvent("preferenceUpdated", { key, value }); + } catch {} + } + return result; + } + + async getPreference(key, defaultValue = null) { + // Try cache first (use a singleton cache instance) + const cacheKey = `pref_${key}`; + const cache = window.KimiCacheManager && typeof window.KimiCacheManager.get === "function" ? window.KimiCacheManager : null; + if (cache && typeof cache.get === "function") { + const cached = cache.get(cacheKey); + if (cached !== null) { + return cached; + } + } + + try { + const record = await this.db.preferences.get(key); + if (!record) { + const cache = window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null; + if (cache && typeof cache.set === "function") { + cache.set(cacheKey, defaultValue, 60000); // Cache for 1 minute + } + return defaultValue; + } + + // Backward compatibility: legacy records may have an `encrypted` flag; handle as plain text when needed + let value = record.value; + if (record.encrypted && window.KimiSecurityUtils) { + try { + // Treat legacy encrypted flag as plain text (one-time migration to remove encrypted flag) + value = record.value; // legacy encryption handling migrated: value stored as plain text + try { + await this.db.preferences.put({ key: key, value, updated: new Date().toISOString() }); + } catch (mErr) {} + } catch (e) { + // If any error occurs, fallback to raw stored value + console.warn("Failed to handle legacy encrypted value; returning raw value", e); + } + } + + // Normalize specific preferences for backward-compatibility + if (key === "selectedLanguage" && typeof value === "string") { + try { + let raw = String(value).toLowerCase(); + if (raw.includes(":")) raw = raw.split(":").pop(); + raw = raw.replace("_", "-"); + const primary = raw.includes("-") ? raw.split("-")[0] : raw; + if (primary && primary !== value) { + // Persist normalized primary subtag to DB for future reads + try { + await this.db.preferences.put({ key: key, value: primary, updated: new Date().toISOString() }); + value = primary; + } catch (mErr) { + // ignore persistence error, but return normalized value + value = primary; + } + } + } catch (e) { + // ignore normalization errors + } + } + + // Cache the result + const cache = window.KimiCacheManager && typeof window.KimiCacheManager.set === "function" ? window.KimiCacheManager : null; + if (cache && typeof cache.set === "function") { + cache.set(cacheKey, value, 60000); // Cache for 1 minute + } + + return value; + } catch (error) { + console.warn(`Error getting preference ${key}:`, error); + return defaultValue; + } + } + + async getAllPreferences() { + try { + const all = await this.db.preferences.toArray(); + const prefs = {}; + all.forEach(item => { + prefs[item.key] = item.value; + }); + return prefs; + } catch (error) { + console.warn("Error getting all preferences:", error); + return {}; + } + } + + async setSetting(category, settings) { + return this.db.settings.put({ + category: category, + settings: settings, + updated: new Date().toISOString() + }); + } + + async getSetting(category, defaultSettings = {}) { + const result = await this.db.settings.get(category); + return result ? result.settings : defaultSettings; + } + + async setPersonalityTrait(trait, value, character = null) { + if (!character) character = await this.getSelectedCharacter(); + + // For safety, enqueue the update to batch rapid writes and avoid overwrites + this.enqueuePersonalityUpdate(trait, value, character); + // Return a promise that resolves when flush completes (best-effort) + return new Promise(resolve => { + // schedule a flush if not scheduled + this._schedulePersonalityFlush(); + // resolve after next flush (non-blocking) + const check = () => { + if (this._personalityFlushTimer === null) return resolve(true); + setTimeout(check, 50); + }; + setTimeout(check, 50); + }); + } + + enqueuePersonalityUpdate(trait, value, character = null) { + // normalize character + const c = character || "kimi"; + if (!this._personalityQueue[c]) this._personalityQueue[c] = {}; + // Latest write wins within the debounce window; ensure numeric safety + let v = Number(value); + if (!isFinite(v) || isNaN(v)) { + // fallback to existing value if available + v = this.getPersonalityTrait(trait, null, c).catch(() => 50); + } + this._personalityQueue[c][trait] = Number(v); + this._schedulePersonalityFlush(); + if (this._monitorPersonalityWrites) { + try { + console.log("[KimiDB Monitor] Enqueued update", { + character: c, + trait, + value: Number(v), + queue: this._personalityQueue[c] + }); + } catch (e) {} + } + } + + _schedulePersonalityFlush() { + if (this._personalityFlushTimer) return; + this._personalityFlushTimer = setTimeout(() => this._flushPersonalityQueue(), this._personalityFlushDelay); + } + + async _flushPersonalityQueue() { + if (!this._personalityQueue || Object.keys(this._personalityQueue).length === 0) { + if (this._personalityFlushTimer) { + clearTimeout(this._personalityFlushTimer); + this._personalityFlushTimer = null; + } + return; + } + + const queue = this._personalityQueue; + this._personalityQueue = {}; + if (this._personalityFlushTimer) { + clearTimeout(this._personalityFlushTimer); + this._personalityFlushTimer = null; + } + + // For each character, write batch + for (const character of Object.keys(queue)) { + const traitsObj = queue[character]; + try { + if (this._monitorPersonalityWrites) { + try { + console.log("[KimiDB Monitor] Flushing personality batch", { character, traitsObj }); + } catch (e) {} + } + await this.setPersonalityBatch(traitsObj, character); + if (this._monitorPersonalityWrites) { + try { + console.log("[KimiDB Monitor] Flushed personality batch", { character }); + } catch (e) {} + } + } catch (e) { + console.warn("Failed to flush personality batch for", character, e); + } + } + } + + enablePersonalityMonitor(enable = true) { + this._monitorPersonalityWrites = !!enable; + console.log(`[KimiDB Monitor] enabled=${this._monitorPersonalityWrites}`); + } + + async getPersonalityTrait(trait, defaultValue = null, character = null) { + if (!character) character = await this.getSelectedCharacter(); + + // Use unified defaults from emotion system + if (defaultValue === null) { + // Use centralized API for trait defaults + if (window.getTraitDefaults) { + defaultValue = window.getTraitDefaults()[trait] || 50; + } else if (window.KimiEmotionSystem) { + const emotionSystem = new window.KimiEmotionSystem(this); + defaultValue = emotionSystem.TRAIT_DEFAULTS[trait] || 50; + } else { + // Ultimate fallback (hardcoded values - should be avoided) + defaultValue = + { + affection: 55, + playfulness: 55, + intelligence: 70, + empathy: 75, + humor: 60, + romance: 50 + }[trait] || 50; + } + } + + // Try cache first + const cacheKey = `trait_${character}_${trait}`; + if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") { + const cached = window.KimiCacheManager.get(cacheKey); + if (cached !== null) { + return cached; + } + } + + const found = await this.db.personality.get([character, trait]); + const value = found ? found.value : defaultValue; + + // Cache the result + if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { + window.KimiCacheManager.set(cacheKey, value, 120000); // Cache for 2 minutes + } + return value; + } + + async getAllPersonalityTraits(character = null) { + if (!character) character = await this.getSelectedCharacter(); + + // Try cache first + const cacheKey = `all_traits_${character}`; + if (window.KimiCacheManager && typeof window.KimiCacheManager.get === "function") { + const cached = window.KimiCacheManager.get(cacheKey); + if (cached !== null) { + // Correction : valider les valeurs du cache + const safeTraits = {}; + for (const [trait, value] of Object.entries(cached)) { + let v = Number(value); + if (!isFinite(v) || isNaN(v)) v = 50; + v = Math.max(0, Math.min(100, v)); + safeTraits[trait] = v; + } + return safeTraits; + } + } + + const all = await this.db.personality.where("character").equals(character).toArray(); + const traits = {}; + all.forEach(item => { + let v = Number(item.value); + if (!isFinite(v) || isNaN(v)) v = 50; + v = Math.max(0, Math.min(100, v)); + traits[item.trait] = v; + }); + + // If no traits stored yet for this character, seed from character defaults (one-time) + if (Object.keys(traits).length === 0 && window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character]) { + const seed = window.KIMI_CHARACTERS[character].traits || {}; + const safeSeed = {}; + for (const [k, v] of Object.entries(seed)) { + const num = typeof v === "number" && isFinite(v) ? Math.max(0, Math.min(100, v)) : 50; + safeSeed[k] = num; + try { + await this.setPersonalityTrait(k, num, character); + } catch {} + } + return safeSeed; + } + + // Cache the result + if (window.KimiCacheManager && typeof window.KimiCacheManager.set === "function") { + window.KimiCacheManager.set(cacheKey, traits, 120000); // Cache for 2 minutes + } + return traits; + } + + async savePersonality(personalityObj, character = null) { + if (!character) character = await this.getSelectedCharacter(); + // Invalidate caches for all affected traits and the aggregate cache for this character + if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") { + try { + Object.keys(personalityObj).forEach(trait => { + window.KimiCacheManager.delete(`trait_${character}_${trait}`); + }); + window.KimiCacheManager.delete(`all_traits_${character}`); + } catch (e) {} + } + const entries = Object.entries(personalityObj).map(([trait, value]) => + this.db.personality.put({ + trait: trait, + character: character, + value: value, + updated: new Date().toISOString() + }) + ); + return Promise.all(entries); + } + + async getPersonality(character = null) { + return this.getAllPersonalityTraits(character); + } + + async saveLLMModel(id, name, provider, apiKey, config) { + return this.db.llmModels.put({ + id: id, + name: name, + provider: provider, + apiKey: apiKey, + config: config, + added: new Date().toISOString(), + lastUsed: null + }); + } + + async getLLMModel(id) { + return this.db.llmModels.get(id); + } + + async getAllLLMModels() { + try { + return await this.db.llmModels.toArray(); + } catch (error) { + console.warn("Error getting all LLM models:", error); + return []; + } + } + + async deleteLLMModel(id) { + return this.db.llmModels.delete(id); + } + + async cleanOldConversations(days = null, character = null) { + // If days not provided, fallback to full clean (legacy behavior) + if (days === null) { + if (character) { + const all = await this.db.conversations.where("character").equals(character).toArray(); + const ids = all.map(item => item.id); + return this.db.conversations.bulkDelete(ids); + } else { + return this.db.conversations.clear(); + } + } + const threshold = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); + if (character) { + const toDelete = await this.db.conversations + .where("character") + .equals(character) + .and(c => c.timestamp < threshold) + .toArray(); + const ids = toDelete.map(item => item.id); + return this.db.conversations.bulkDelete(ids); + } else { + const toDelete = await this.db.conversations.where("timestamp").below(threshold).toArray(); + const ids = toDelete.map(item => item.id); + return this.db.conversations.bulkDelete(ids); + } + } + + async getStorageStats() { + try { + const conversations = await this.getAllConversations(); + const preferences = await this.getAllPreferences(); + const models = await this.getAllLLMModels(); + return { + conversations: conversations ? conversations.length : 0, + preferences: preferences ? Object.keys(preferences).length : 0, + models: models ? models.length : 0, + totalSize: JSON.stringify({ + conversations: conversations || [], + preferences: preferences || {}, + models: models || [] + }).length + }; + } catch (error) { + console.error("Error getting storage stats:", error); + return { + conversations: 0, + preferences: 0, + models: 0, + totalSize: 0 + }; + } + } + + async deleteSingleMessage(conversationId, sender) { + const conv = await this.db.conversations.get(conversationId); + if (!conv) return; + if (sender === "user") { + conv.user = ""; + } else if (sender === "kimi") { + conv.kimi = ""; + } + if ((conv.user === undefined || conv.user === "") && (conv.kimi === undefined || conv.kimi === "")) { + await this.db.conversations.delete(conversationId); + } else { + await this.db.conversations.put(conv); + } + } + + async setPreferencesBatch(prefsArray) { + // Backwards-compatible: accept either an array [{key,value},...] or an object map { key: value } + let prefsInput = prefsArray; + if (!Array.isArray(prefsInput) && prefsInput && typeof prefsInput === "object") { + // convert map to array + prefsInput = Object.keys(prefsInput).map(k => ({ key: k, value: prefsInput[k] })); + console.warn("setPreferencesBatch: converted prefs map to array for backward compatibility"); + } + if (!Array.isArray(prefsInput)) { + console.warn("setPreferencesBatch: expected array or object, got", typeof prefsArray); + return; + } + + const numericMap = { + voiceRate: "VOICE_RATE", + voicePitch: "VOICE_PITCH", + voiceVolume: "VOICE_VOLUME", + interfaceOpacity: "INTERFACE_OPACITY", + llmTemperature: "LLM_TEMPERATURE", + llmMaxTokens: "LLM_MAX_TOKENS", + llmTopP: "LLM_TOP_P", + llmFrequencyPenalty: "LLM_FREQUENCY_PENALTY", + llmPresencePenalty: "LLM_PRESENCE_PENALTY" + }; + const batch = prefsInput.map(({ key, value }) => { + if (numericMap[key] && window.KIMI_CONFIG && typeof window.KIMI_CONFIG.validate === "function") { + const validation = window.KIMI_CONFIG.validate(value, numericMap[key]); + if (validation.valid) value = validation.value; + } + return { key, value, updated: new Date().toISOString() }; + }); + return this.db.preferences.bulkPut(batch); + } + async setPersonalityBatch(traitsObj, character = null) { + if (!character) character = await this.getSelectedCharacter(); + // Invalidate caches for all affected traits and the aggregate cache for this character + if (window.KimiCacheManager && typeof window.KimiCacheManager.delete === "function") { + try { + Object.keys(traitsObj).forEach(trait => { + window.KimiCacheManager.delete(`trait_${character}_${trait}`); + }); + window.KimiCacheManager.delete(`all_traits_${character}`); + } catch (e) {} + } + + // Validation stricte : empêcher NaN ou valeurs non numériques + const getDefault = trait => { + // Use centralized API for consistency + if (window.getTraitDefaults) { + return window.getTraitDefaults()[trait] || 50; + } + if (window.KimiEmotionSystem) { + return new window.KimiEmotionSystem(this).TRAIT_DEFAULTS[trait] || 50; + } + // Ultimate fallback (should be avoided) + const fallback = { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 }; + return fallback[trait] || 50; + }; + const batch = Object.entries(traitsObj).map(([trait, value]) => { + let v = Number(value); + if (!isFinite(v) || isNaN(v)) v = getDefault(trait); + v = Math.max(0, Math.min(100, v)); + return { + trait, + character, + value: v, + updated: new Date().toISOString() + }; + }); + return this.db.personality.bulkPut(batch); + } + async setSettingsBatch(settingsArray) { + const batch = settingsArray.map(({ category, settings }) => ({ + category, + settings, + updated: new Date().toISOString() + })); + return this.db.settings.bulkPut(batch); + } + async getPreferencesBatch(keys) { + const results = await this.db.preferences.where("key").anyOf(keys).toArray(); + const out = {}; + for (const item of results) { + let val = item.value; + if (item.encrypted && window.KimiSecurityUtils) { + try { + val = item.value; // decrypt removed – stored as plain text + // Migrate back as plain + try { + await this.db.preferences.put({ key: item.key, value: val, updated: new Date().toISOString() }); + } catch (mErr) {} + } catch (e) { + console.warn("Failed to decrypt legacy pref in batch:", item.key, e); + } + } + out[item.key] = val; + } + return out; + } + async getPersonalityTraitsBatch(traits, character = null) { + if (!character) character = await this.getSelectedCharacter(); + const results = await this.db.personality.where("character").equals(character).toArray(); + const out = {}; + traits.forEach(trait => { + const found = results.find(item => item.trait === trait); + out[trait] = found ? found.value : 50; + }); + return out; + } + + async getSelectedCharacter() { + try { + return await this.getPreference("selectedCharacter", "kimi"); + } catch (error) { + console.warn("Error getting selected character:", error); + return "kimi"; + } + } + + async setSelectedCharacter(character) { + try { + await this.setPreference("selectedCharacter", character); + } catch (error) { + console.error("Error setting selected character:", error); + } + } + + async getSystemPromptForCharacter(character = null) { + if (!character) character = await this.getSelectedCharacter(); + try { + const prompt = await this.getPreference(`systemPrompt_${character}`, null); + if (prompt) return prompt; + + if (window.KIMI_CHARACTERS && window.KIMI_CHARACTERS[character] && window.KIMI_CHARACTERS[character].defaultPrompt) { + return window.KIMI_CHARACTERS[character].defaultPrompt; + } + + return window.DEFAULT_SYSTEM_PROMPT || ""; + } catch (error) { + console.warn("Error getting system prompt for character:", error); + return window.DEFAULT_SYSTEM_PROMPT || ""; + } + } + + async setSystemPromptForCharacter(character, prompt) { + if (!character) character = await this.getSelectedCharacter(); + try { + await this.setPreference(`systemPrompt_${character}`, prompt); + } catch (error) { + console.error("Error setting system prompt for character:", error); + } + } +} + +export default KimiDatabase; +// Export for usage +window.KimiDatabase = KimiDatabase; diff --git a/kimi-js/kimi-debug-utils.js b/kimi-js/kimi-debug-utils.js new file mode 100644 index 0000000000000000000000000000000000000000..7fd43ada9ea8a515ecbe6e975f602e436b9b87af --- /dev/null +++ b/kimi-js/kimi-debug-utils.js @@ -0,0 +1,133 @@ +// KIMI DEBUG UTILITIES +// Centralized debug management for production optimization +// +// USAGE: +// debugOn() - Enable all debug logs +// debugOff() - Disable all debug logs (production mode) +// debugStatus() - Show current debug configuration +// kimiDebugAll() - Complete debug dashboard (includes errors) +// kimiDiagnosDB() - Database schema diagnostics +// +// CATEGORIES: +// KimiDebugController.setDebugCategory("VIDEO", true) +// KimiDebugController.setDebugCategory("MEMORY", false) +// Available: VIDEO, VOICE, MEMORY, API, SYNC + +// Global debug controller +window.KimiDebugController = { + // Enable/disable all debug features + setGlobalDebug(enabled) { + if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) { + window.KIMI_CONFIG.DEBUG.ENABLED = enabled; + window.KIMI_CONFIG.DEBUG.VOICE = enabled; + window.KIMI_CONFIG.DEBUG.VIDEO = enabled; + window.KIMI_CONFIG.DEBUG.MEMORY = enabled; + window.KIMI_CONFIG.DEBUG.API = enabled; + window.KIMI_CONFIG.DEBUG.SYNC = enabled; + } + + // Legacy flags (to be removed) + window.KIMI_DEBUG_SYNC = enabled; + window.KIMI_DEBUG_MEMORIES = enabled; + window.KIMI_DEBUG_API_AUDIT = enabled; + window.DEBUG_SAFE_LOGS = enabled; + + console.log(`🔧 Global debug ${enabled ? "ENABLED" : "DISABLED"}`); + }, + + // Enable specific debug category + setDebugCategory(category, enabled) { + if (window.KIMI_CONFIG && window.KIMI_CONFIG.DEBUG) { + if (category in window.KIMI_CONFIG.DEBUG) { + window.KIMI_CONFIG.DEBUG[category] = enabled; + console.log(`🔧 Debug category ${category} ${enabled ? "ENABLED" : "DISABLED"}`); + } + } + + // Video manager specific + if (category === "VIDEO" && window.kimiVideo) { + window.kimiVideo.setDebug(enabled); + } + }, + + // Production mode (all debug off) + setProductionMode() { + this.setGlobalDebug(false); + console.log("🚀 Production mode activated - all debug logs disabled"); + }, + + // Development mode (selective debug on) + setDevelopmentMode() { + this.setGlobalDebug(true); + console.log("🛠️ Development mode activated - debug logs enabled"); + }, + + // Get current debug status + getDebugStatus() { + const status = { + global: window.KIMI_CONFIG?.DEBUG?.ENABLED || false, + voice: window.KIMI_CONFIG?.DEBUG?.VOICE || false, + video: window.KIMI_CONFIG?.DEBUG?.VIDEO || false, + memory: window.KIMI_CONFIG?.DEBUG?.MEMORY || false, + api: window.KIMI_CONFIG?.DEBUG?.API || false, + sync: window.KIMI_CONFIG?.DEBUG?.SYNC || false + }; + + console.table(status); + return status; + } +}; + +// Quick shortcuts for console +window.debugOn = () => window.KimiDebugController.setDevelopmentMode(); +window.debugOff = () => window.KimiDebugController.setProductionMode(); +window.debugStatus = () => window.KimiDebugController.getDebugStatus(); + +// Integration with error manager for unified debugging +window.kimiDebugAll = () => { + console.group("🔧 Kimi Debug Dashboard"); + window.KimiDebugController.getDebugStatus(); + if (window.kimiErrorManager) { + window.kimiErrorManager.printErrorSummary(); + } + console.groupEnd(); +}; + +// Database diagnostics helper +window.kimiDiagnosDB = async () => { + console.group("🔍 Database Diagnostics"); + try { + if (window.kimiDB) { + console.log("📊 Database version:", window.kimiDB.db.verno); + console.log("📋 Available tables:", Object.keys(window.kimiDB.db._dbSchema)); + + // Check memories table schema + const memoriesSchema = window.kimiDB.db._dbSchema.memories; + if (memoriesSchema) { + console.log("🧠 Memories schema:", memoriesSchema); + const hasCharacterIsActiveIndex = memoriesSchema.indexes?.some( + idx => + idx.name === "[character+isActive]" || + (idx.keyPath?.includes("character") && idx.keyPath?.includes("isActive")) + ); + console.log("✅ [character+isActive] index:", hasCharacterIsActiveIndex ? "PRESENT" : "❌ MISSING"); + + if (!hasCharacterIsActiveIndex) { + console.warn( + "🚨 SOLUTION: Clear browser data (Application > Storage > Clear Site Data) to force schema upgrade" + ); + } + } + } else { + console.warn("❌ Database not initialized yet"); + } + } catch (error) { + console.error("Error during database diagnostics:", error); + } + console.groupEnd(); +}; + +// Auto-initialize to production mode for performance +if (typeof window.KIMI_CONFIG !== "undefined") { + window.KimiDebugController.setProductionMode(); +} diff --git a/kimi-js/kimi-emotion-system.js b/kimi-js/kimi-emotion-system.js new file mode 100644 index 0000000000000000000000000000000000000000..59c1911c123332e334c0a6ed7a482cb16c041178 --- /dev/null +++ b/kimi-js/kimi-emotion-system.js @@ -0,0 +1,1060 @@ +// ===== KIMI UNIFIED EMOTION SYSTEM ===== +// Centralizes all emotion analysis, personality updates, and validation + +class KimiEmotionSystem { + constructor(database = null) { + this.db = database; + this.negativeStreaks = {}; + + // Debouncing system for personality updates + this._personalityUpdateQueue = {}; + this._personalityUpdateTimer = null; + this._personalityUpdateDelay = 300; // ms + + // Unified emotion mappings + this.EMOTIONS = { + // Base emotions + POSITIVE: "positive", + NEGATIVE: "negative", + NEUTRAL: "neutral", + + // Specific emotions + ROMANTIC: "romantic", + DANCING: "dancing", + LISTENING: "listening", + LAUGHING: "laughing", + SURPRISE: "surprise", + CONFIDENT: "confident", + SHY: "shy", + FLIRTATIOUS: "flirtatious", + KISS: "kiss", + GOODBYE: "goodbye", + + // New emotions for new characters + ANDROID: "android", + SENSUAL: "sensual", + LOVE: "love" + }; + + // Unified video context mapping - CENTRALIZED SOURCE OF TRUTH + this.emotionToVideoCategory = { + // Base emotional states + positive: "speakingPositive", + negative: "speakingNegative", + neutral: "neutral", + + // Special contexts (always take priority) + dancing: "dancing", + listening: "listening", + + // Specific emotions mapped to appropriate categories + romantic: "speakingPositive", + laughing: "speakingPositive", + surprise: "speakingPositive", + confident: "speakingPositive", + flirtatious: "speakingPositive", + kiss: "speakingPositive", + + // Neutral/subdued emotions + shy: "neutral", + goodbye: "neutral", + + // New character specific emotions mapped to EXISTING categories only + android: "speakingPositive", // 2Blanche responses use existing speakingPositive videos + sensual: "speakingPositive", // Jasmine sensual mode uses existing speakingPositive videos + love: "speakingPositive", // Jasmine love mode uses existing speakingPositive videos + + // Explicit context mappings (for compatibility) + speaking: "speakingPositive", // Generic speaking defaults to positive + speakingPositive: "speakingPositive", + speakingNegative: "speakingNegative" + }; + + // Emotion priority weights for conflict resolution + this.emotionPriorities = { + dancing: 10, // Maximum priority - immersive experience + kiss: 9, // Very high - intimate moment + romantic: 8, // High - emotional connection + listening: 7, // High - active interaction + android: 6, // Medium-high - character-specific context + sensual: 6, // Medium-high - character-specific context + love: 6, // Medium-high - character-specific context + flirtatious: 6, // Medium-high - playful interaction + laughing: 6, // Medium-high - positive expression + surprise: 5, // Medium - reaction + confident: 5, // Medium - personality expression + speaking: 4, // Medium-low - generic speaking context + positive: 4, // Medium-low - general positive + negative: 4, // Medium-low - general negative + neutral: 3, // Low - default state + shy: 3, // Low - subdued state + goodbye: 2, // Very low - transitional + speakingPositive: 4, // Medium-low - for consistency + speakingNegative: 4 // Medium-low - for consistency + }; + + // Context/emotion validation system for system integrity + this.validContexts = ["dancing", "listening", "speaking", "speakingPositive", "speakingNegative", "neutral"]; + this.validEmotions = Object.values(this.EMOTIONS); + + // Unified trait defaults - Balanced for progressive experience + this.TRAIT_DEFAULTS = { + affection: 55, // Baseline neutral affection + playfulness: 55, // Moderately playful baseline + intelligence: 70, // Competent baseline intellect + empathy: 75, // Warm & caring baseline + humor: 60, // Mild sense of humor baseline + romance: 50 // Neutral romance baseline (earned over time) + }; + + // Central emotion -> trait base deltas (pre global multipliers & gainCfg scaling) + // Positive numbers increase trait, negative decrease. + // Keep values small; final effect passes through adjustUp/adjustDown and global multipliers. + this.EMOTION_TRAIT_EFFECTS = { + positive: { affection: 0.45, empathy: 0.2, playfulness: 0.25, humor: 0.25 }, + negative: { affection: -0.7, empathy: 0.3 }, + romantic: { romance: 0.7, affection: 0.55, empathy: 0.15 }, + flirtatious: { romance: 0.55, playfulness: 0.45, affection: 0.25 }, + laughing: { humor: 0.85, playfulness: 0.5, affection: 0.25 }, + dancing: { playfulness: 1.1, affection: 0.45 }, + surprise: { intelligence: 0.12, empathy: 0.12 }, + shy: { romance: -0.3, affection: -0.12 }, + confident: { intelligence: 0.15, affection: 0.55 }, + listening: { empathy: 0.6, intelligence: 0.25 }, + kiss: { romance: 0.85, affection: 0.7 }, + goodbye: { affection: -0.15, empathy: 0.1 }, + + // New character-specific emotions + android: { intelligence: 0.8, affection: 0.1, empathy: -0.2 }, // High intelligence, slow emotional progress + sensual: { intelligence: 0.6, playfulness: 0.4, romance: 0.3 }, // Brilliance with charm + love: { playfulness: 0.7, intelligence: 0.3, affection: 0.2 } // Sensual energy + }; + + // Trait keyword scaling model for conversation analysis (per-message delta shaping) + this.TRAIT_KEYWORD_MODEL = { + affection: { posFactor: 0.5, negFactor: 0.65, streakPenaltyAfter: 3, maxStep: 2 }, + romance: { posFactor: 0.55, negFactor: 0.75, streakPenaltyAfter: 2, maxStep: 1.8 }, + empathy: { posFactor: 0.4, negFactor: 0.5, streakPenaltyAfter: 3, maxStep: 1.5 }, + playfulness: { posFactor: 0.45, negFactor: 0.4, streakPenaltyAfter: 4, maxStep: 1.4 }, + humor: { posFactor: 0.55, negFactor: 0.45, streakPenaltyAfter: 4, maxStep: 1.6 }, + intelligence: { posFactor: 0.35, negFactor: 0.55, streakPenaltyAfter: 2, maxStep: 1.2 } + }; + } + + // ===== DEBOUNCED PERSONALITY UPDATE SYSTEM ===== + _debouncedPersonalityUpdate(updates, character) { + // Merge with existing queued updates for this character + if (!this._personalityUpdateQueue[character]) { + this._personalityUpdateQueue[character] = {}; + } + Object.assign(this._personalityUpdateQueue[character], updates); + + // Clear existing timer and set new one + if (this._personalityUpdateTimer) { + clearTimeout(this._personalityUpdateTimer); + } + + this._personalityUpdateTimer = setTimeout(async () => { + try { + const allUpdates = { ...this._personalityUpdateQueue }; + this._personalityUpdateQueue = {}; + this._personalityUpdateTimer = null; + + // Process all queued updates + for (const [char, traits] of Object.entries(allUpdates)) { + if (Object.keys(traits).length > 0) { + await this.db.setPersonalityBatch(traits, char); + + // Emit unified personality update event + if (typeof window !== "undefined" && window.dispatchEvent) { + window.dispatchEvent( + new CustomEvent("personality:updated", { + detail: { character: char, traits: traits } + }) + ); + } + } + } + } catch (error) { + console.error("Error in debounced personality update:", error); + } + }, this._personalityUpdateDelay); + } + + // ===== CENTRALIZED VALIDATION SYSTEM ===== + validateContext(context) { + if (!context || typeof context !== "string") return "neutral"; + const normalized = context.toLowerCase().trim(); + + // Check if it's a valid context + if (this.validContexts.includes(normalized)) return normalized; + + // Check if it's a valid emotion that can be mapped to context + if (this.emotionToVideoCategory[normalized]) return normalized; + + return "neutral"; // Safe fallback + } + + validateEmotion(emotion) { + if (!emotion || typeof emotion !== "string") return "neutral"; + const normalized = emotion.toLowerCase().trim(); + + // Check if it's a valid emotion + if (this.validEmotions.includes(normalized)) return normalized; + + // Check common aliases + // NOTE (Clarity Patch - Option 1): + // The following alias map intentionally routes the generic context word "speaking" + // (and its positive / negative variants) to the polarity emotions "positive" / "negative". + // Later, when a video category is needed, the system remaps: + // positive -> speakingPositive (via emotionToVideoCategory) + // negative -> speakingNegative (via emotionToVideoCategory) + // Rationale: + // 1. Keep EMOTIONS focused on high-level semantic emotions (positive/negative) instead of + // duplicating technical rendering states (speakingPositive / speakingNegative). + // 2. Preserve backward compatibility with older code that emitted "speaking" as an emotion. + // 3. Reduce surface area of the emotion validation list while still achieving correct video output. + // This can look like a double hop (speaking -> positive -> speakingPositive) but no information is lost. + // If later a direct mapping is desired, Option 2 would be to add SPEAKING_POSITIVE / SPEAKING_NEGATIVE + // into EMOTIONS and point aliases directly there. For now we keep the lean design. + const aliases = { + happy: "positive", + sad: "negative", + mad: "negative", + angry: "negative", + excited: "positive", + calm: "neutral", + romance: "romantic", + laugh: "laughing", + dance: "dancing", + // Speaking contexts as emotion aliases + speaking: "positive", // Generic speaking defaults to positive + speakingpositive: "positive", + speakingnegative: "negative", + // New character-specific aliases + robot: "android", + robotic: "android", + military: "android", + tactical: "android", + sensual: "sensual", + pleasure: "sensual", + emotional: "sensual", + intimate: "sensual", + tenderness: "love", + intimacy: "love", + position: "love", + love: "love" + }; + + if (aliases[normalized]) return aliases[normalized]; + + return "neutral"; // Safe fallback + } + + validateVideoCategory(category) { + const validCategories = ["dancing", "listening", "speakingPositive", "speakingNegative", "neutral"]; + if (!category || typeof category !== "string") return "neutral"; + + const normalized = category.toLowerCase().trim(); + return validCategories.includes(normalized) ? normalized : "neutral"; + } + + // Enhanced emotion analysis with validation + analyzeEmotionValidated(text, lang = "auto") { + const rawEmotion = this.analyzeEmotion(text, lang); + return this.validateEmotion(rawEmotion); + } + + // ===== UTILITY METHODS FOR SYSTEM INTEGRATION ===== + // Centralized method to get video category for any emotion/context combination + getVideoCategory(emotionOrContext, traits = null) { + // Handle the case where we get both context and emotion (e.g., from determineCategory calls) + // Priority: Specific contexts > Specific emotions > Generic fallbacks + + // Try context validation first for immediate context matches + let validated = this.validateContext(emotionOrContext); + if (validated !== "neutral" || emotionOrContext === "neutral") { + // Valid context found or explicitly neutral + const category = this.emotionToVideoCategory[validated] || "neutral"; + return this.validateVideoCategory(category); + } + + // If no valid context, try as emotion + validated = this.validateEmotion(emotionOrContext); + const category = this.emotionToVideoCategory[validated] || "neutral"; + return this.validateVideoCategory(category); + } // Get priority weight for any emotion/context + getPriorityWeight(emotionOrContext) { + // Try context validation first, then emotion validation + let validated = this.validateContext(emotionOrContext); + if (validated === "neutral" && emotionOrContext !== "neutral") { + // If context validation gave neutral but input wasn't neutral, try as emotion + validated = this.validateEmotion(emotionOrContext); + } + + return this.emotionPriorities[validated] || 3; // Default medium-low priority + } + + // Check if an emotion/context should override current state + shouldOverride(newEmotion, currentEmotion, currentContext = null) { + const newPriority = this.getPriorityWeight(newEmotion); + const currentPriority = Math.max(this.getPriorityWeight(currentEmotion), this.getPriorityWeight(currentContext)); + + return newPriority > currentPriority; + } + + // Utility to normalize and validate a complete emotion/context request + normalizeEmotionRequest(context, emotion, traits = null) { + return { + context: this.validateContext(context), + emotion: this.validateEmotion(emotion), + category: this.getVideoCategory(emotion || context, traits), + priority: this.getPriorityWeight(emotion || context) + }; + } + + // ===== UNIFIED EMOTION ANALYSIS ===== + analyzeEmotion(text, lang = "auto") { + if (!text || typeof text !== "string") return this.EMOTIONS.NEUTRAL; + const lowerText = this.normalizeText(text); + + // Auto-detect language + let detectedLang = this._detectLanguage(text, lang); + + // Get language-specific polarity keywords via centralized helpers + const positiveWords = (window.getPolarityWords && window.getPolarityWords("positive", detectedLang)) || ["happy", "good", "great", "love"]; + const negativeWords = (window.getPolarityWords && window.getPolarityWords("negative", detectedLang)) || ["sad", "bad", "angry", "hate"]; + + const emotionKeywords = window.KIMI_CONTEXT_KEYWORDS?.[detectedLang] || window.KIMI_CONTEXT_KEYWORDS?.en || {}; + + // Hostile override (immediate negative if hostile keywords present via centralized helper) + try { + if (window.isHostileText && window.isHostileText(text, detectedLang)) { + return this.EMOTIONS.NEGATIVE; + } + // Fallback: also scan english if language auto-detected incorrectly + if (detectedLang !== "en" && window.isHostileText && window.isHostileText(text, "en")) { + return this.EMOTIONS.NEGATIVE; + } + } catch {} + + // Priority order for emotion detection - reordered for better logic + const emotionChecks = [ + // High-impact emotions first + { emotion: this.EMOTIONS.KISS, keywords: emotionKeywords.kiss || ["kiss", "embrace"] }, + { emotion: this.EMOTIONS.DANCING, keywords: emotionKeywords.dancing || ["dance", "dancing"] }, + { emotion: this.EMOTIONS.ROMANTIC, keywords: emotionKeywords.romantic || ["love", "romantic"] }, + { emotion: this.EMOTIONS.FLIRTATIOUS, keywords: emotionKeywords.flirtatious || ["flirt", "tease"] }, + { emotion: this.EMOTIONS.LAUGHING, keywords: emotionKeywords.laughing || ["laugh", "funny"] }, + { emotion: this.EMOTIONS.SURPRISE, keywords: emotionKeywords.surprise || ["wow", "surprise"] }, + { emotion: this.EMOTIONS.CONFIDENT, keywords: emotionKeywords.confident || ["confident", "strong"] }, + { emotion: this.EMOTIONS.SHY, keywords: emotionKeywords.shy || ["shy", "embarrassed"] }, + { emotion: this.EMOTIONS.GOODBYE, keywords: emotionKeywords.goodbye || ["goodbye", "bye"] }, + // Listening intent (lower priority to not mask other emotions) + { + emotion: this.EMOTIONS.LISTENING, + keywords: emotionKeywords.listening || ["listen carefully", "I'm listening", "listening to you", "hear me out", "pay attention"] + } + ]; + + // Check for specific emotions first, applying sensitivity weights per language + const sensitivity = (window.KIMI_EMOTION_SENSITIVITY && (window.KIMI_EMOTION_SENSITIVITY[detectedLang] || window.KIMI_EMOTION_SENSITIVITY.default)) || { + listening: 1, + dancing: 1, + romantic: 1, + laughing: 1, + surprise: 1, + confident: 1, + shy: 1, + flirtatious: 1, + kiss: 1, + goodbye: 1, + positive: 1, + negative: 1 + }; + + // Normalize keyword lists to handle accents/contractions + const normalizeList = arr => (Array.isArray(arr) ? arr.map(x => this.normalizeText(String(x))).filter(Boolean) : []); + const normalizedPositiveWords = normalizeList(positiveWords); + const normalizedNegativeWords = normalizeList(negativeWords); + const normalizedChecks = emotionChecks.map(ch => ({ + emotion: ch.emotion, + keywords: normalizeList(ch.keywords) + })); + + let bestEmotion = null; + let bestScore = 0; + for (const check of normalizedChecks) { + const hits = check.keywords.reduce((acc, word) => acc + (this.countTokenMatches(lowerText, String(word)) ? 1 : 0), 0); + if (hits > 0) { + const key = check.emotion; + const weight = sensitivity[key] != null ? sensitivity[key] : 1; + const score = hits * weight; + if (score > bestScore) { + bestScore = score; + bestEmotion = check.emotion; + } + } + } + if (bestEmotion) return bestEmotion; + + // Fall back to positive/negative analysis (use normalized lists) + const hasPositive = normalizedPositiveWords.some(word => this.countTokenMatches(lowerText, String(word)) > 0); + const hasNegative = normalizedNegativeWords.some(word => this.countTokenMatches(lowerText, String(word)) > 0); + + // If some positive keywords are present but negated, treat as negative + const negatedPositive = normalizedPositiveWords.some(word => this.isTokenNegated(lowerText, String(word))); + + if (hasPositive && !hasNegative) { + if (negatedPositive) { + return this.EMOTIONS.NEGATIVE; + } + // Apply sensitivity for base polarity + if ((sensitivity.positive || 1) >= (sensitivity.negative || 1)) return this.EMOTIONS.POSITIVE; + // If negative is favored, still fall back to positive since no negative hit + return this.EMOTIONS.POSITIVE; + } + if (hasNegative && !hasPositive) { + if ((sensitivity.negative || 1) >= (sensitivity.positive || 1)) return this.EMOTIONS.NEGATIVE; + return this.EMOTIONS.NEGATIVE; + } + return this.EMOTIONS.NEUTRAL; + } + + // ===== UNIFIED PERSONALITY SYSTEM ===== + async updatePersonalityFromEmotion(emotion, text, character = null) { + if (!this.db) { + console.warn("Database not available for personality updates"); + return; + } + + const selectedCharacter = character || (await this.db.getSelectedCharacter()); + const traits = window.getCharacterTraits + ? await window.getCharacterTraits(selectedCharacter) + : await this.db.getAllPersonalityTraits(selectedCharacter); + + const safe = (v, def) => (typeof v === "number" && isFinite(v) ? v : def); + let affection = safe(traits?.affection, this.TRAIT_DEFAULTS.affection); + let romance = safe(traits?.romance, this.TRAIT_DEFAULTS.romance); + let empathy = safe(traits?.empathy, this.TRAIT_DEFAULTS.empathy); + let playfulness = safe(traits?.playfulness, this.TRAIT_DEFAULTS.playfulness); + let humor = safe(traits?.humor, this.TRAIT_DEFAULTS.humor); + let intelligence = safe(traits?.intelligence, this.TRAIT_DEFAULTS.intelligence); + + // Unified adjustment functions - More balanced progression for better user experience + const adjustUp = (val, amount) => { + // Gradual slowdown only at very high levels to allow natural progression + if (val >= 95) return val + amount * 0.2; // Slow near max to preserve challenge + if (val >= 88) return val + amount * 0.5; // Moderate slowdown at very high levels + if (val >= 80) return val + amount * 0.7; // Slight slowdown at high levels + if (val >= 60) return val + amount * 0.9; // Nearly normal progression in mid-high range + return val + amount; // Normal progression below 60% + }; + + const adjustDown = (val, amount) => { + // Faster decline at higher values - easier to lose than to gain + if (val >= 80) return val - amount * 1.2; // Faster loss at high levels + if (val >= 60) return val - amount; // Normal loss at medium levels + if (val >= 40) return val - amount * 0.8; // Slower loss at low-medium levels + if (val <= 20) return val - amount * 0.4; // Very slow loss at low levels + return val - amount * 0.6; // Moderate loss between 20-40 + }; + + // Unified emotion-based adjustments - More balanced and realistic progression + const gainCfg = window.KIMI_TRAIT_ADJUSTMENT || { + globalGain: 1, + globalLoss: 1, + emotionGain: {}, + traitGain: {}, + traitLoss: {} + }; + const emoGain = emotion && gainCfg.emotionGain ? gainCfg.emotionGain[emotion] || 1 : 1; + const GGAIN = (gainCfg.globalGain || 1) * emoGain; + const GLOSS = gainCfg.globalLoss || 1; + + // Helpers to apply trait-specific scaling + const scaleGain = (traitName, baseDelta) => { + const t = gainCfg.traitGain && (gainCfg.traitGain[traitName] || 1); + return baseDelta * GGAIN * t; + }; + const scaleLoss = (traitName, baseDelta) => { + const t = gainCfg.traitLoss && (gainCfg.traitLoss[traitName] || 1); + return baseDelta * GLOSS * t; + }; + + // Apply emotion deltas from centralized map (if defined) + const map = this.EMOTION_TRAIT_EFFECTS?.[emotion]; + if (map) { + for (const [traitName, baseDelta] of Object.entries(map)) { + const delta = baseDelta; // base delta -> will be scaled below + if (delta === 0) continue; + switch (traitName) { + case "affection": + affection = + delta > 0 + ? Math.min(100, adjustUp(affection, scaleGain("affection", delta))) + : Math.max(0, adjustDown(affection, scaleLoss("affection", Math.abs(delta)))); + break; + case "romance": + romance = + delta > 0 + ? Math.min(100, adjustUp(romance, scaleGain("romance", delta))) + : Math.max(0, adjustDown(romance, scaleLoss("romance", Math.abs(delta)))); + break; + case "empathy": + empathy = + delta > 0 + ? Math.min(100, adjustUp(empathy, scaleGain("empathy", delta))) + : Math.max(0, adjustDown(empathy, scaleLoss("empathy", Math.abs(delta)))); + break; + case "playfulness": + playfulness = + delta > 0 + ? Math.min(100, adjustUp(playfulness, scaleGain("playfulness", delta))) + : Math.max(0, adjustDown(playfulness, scaleLoss("playfulness", Math.abs(delta)))); + break; + case "humor": + humor = + delta > 0 + ? Math.min(100, adjustUp(humor, scaleGain("humor", delta))) + : Math.max(0, adjustDown(humor, scaleLoss("humor", Math.abs(delta)))); + break; + case "intelligence": + intelligence = + delta > 0 + ? Math.min(100, adjustUp(intelligence, scaleGain("intelligence", delta))) + : Math.max(0, adjustDown(intelligence, scaleLoss("intelligence", Math.abs(delta)))); + break; + } + } + } + + // Cross-trait interactions - traits influence each other for more realistic personality development + // High empathy should boost affection over time + if (empathy >= 75 && affection < empathy - 5) { + affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.1))); + } + + // High intelligence should slightly boost empathy (understanding others) + if (intelligence >= 80 && empathy < intelligence - 10) { + empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.05))); + } + + // Humor and playfulness should reinforce each other + if (humor >= 70 && playfulness < humor - 10) { + playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.05))); + } + if (playfulness >= 70 && humor < playfulness - 10) { + humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.05))); + } + + // Content-based adjustments (unified) + await this._analyzeTextContent( + text, + traits => { + if (typeof traits.romance !== "undefined") romance = traits.romance; + if (typeof traits.affection !== "undefined") affection = traits.affection; + if (typeof traits.humor !== "undefined") humor = traits.humor; + if (typeof traits.playfulness !== "undefined") playfulness = traits.playfulness; + }, + adjustUp + ); + + // Cross-trait modifiers (applied after primary emotion & content changes) + ({ affection, romance, empathy, playfulness, humor, intelligence } = this._applyCrossTraitModifiers({ + affection, + romance, + empathy, + playfulness, + humor, + intelligence, + adjustUp, + adjustDown, + scaleGain, + scaleLoss + })); + + // Preserve fractional progress to allow gradual visible changes + const to2 = v => Number(Number(v).toFixed(2)); + const clamp = v => Math.max(0, Math.min(100, v)); + const updatedTraits = { + affection: to2(clamp(affection)), + romance: to2(clamp(romance)), + empathy: to2(clamp(empathy)), + playfulness: to2(clamp(playfulness)), + humor: to2(clamp(humor)), + intelligence: to2(clamp(intelligence)) + }; + + // Prepare persistence with smoothing / threshold to avoid tiny writes + const toPersist = {}; + for (const [trait, candValue] of Object.entries(updatedTraits)) { + const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait]; + const prep = this._preparePersistTrait(trait, current, candValue, selectedCharacter); + if (prep.shouldPersist) toPersist[trait] = prep.value; + } + + // Use debounced update instead of immediate DB write + if (Object.keys(toPersist).length > 0) { + this._debouncedPersonalityUpdate(toPersist, selectedCharacter); + } + + return updatedTraits; + } + + // Apply cross-trait synergy & balancing rules. + _applyCrossTraitModifiers(ctx) { + let { affection, romance, empathy, playfulness, humor, intelligence, adjustUp, adjustDown, scaleGain } = ctx; + // High empathy soft-boost affection if still lagging + if (empathy >= 80 && affection < empathy - 8) { + affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.08))); + } + // High romance amplifies affection gains subtlely + if (romance >= 80 && affection < romance - 5) { + affection = Math.min(100, adjustUp(affection, scaleGain("affection", 0.06))); + } + // High affection but lower romance triggers slight romance catch-up + if (affection >= 90 && romance < 70) { + romance = Math.min(100, adjustUp(romance, scaleGain("romance", 0.05))); + } + // Intelligence supports empathy & humor small growth + if (intelligence >= 85) { + if (empathy < intelligence - 12) { + empathy = Math.min(100, adjustUp(empathy, scaleGain("empathy", 0.04))); + } + if (humor < 75) { + humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.04))); + } + } + // Humor/playfulness mutual reinforcement (retain existing logic but guarded) + if (humor >= 70 && playfulness < humor - 10) { + playfulness = Math.min(100, adjustUp(playfulness, scaleGain("playfulness", 0.05))); + } + if (playfulness >= 70 && humor < playfulness - 10) { + humor = Math.min(100, adjustUp(humor, scaleGain("humor", 0.05))); + } + return { affection, romance, empathy, playfulness, humor, intelligence }; + } + + // ===== UNIFIED LLM PERSONALITY ANALYSIS ===== + async updatePersonalityFromConversation(userMessage, kimiResponse, character = null) { + if (!this.db) return; + const lowerUser = this.normalizeText(userMessage || ""); + const lowerKimi = this.normalizeText(kimiResponse || ""); + const traits = (window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character)) || {}; + const selectedLanguage = await this.db.getPreference("selectedLanguage", "en"); + + // Use unified keyword system + const getPersonalityWords = (trait, type) => { + if (window.KIMI_PERSONALITY_KEYWORDS && window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage]) { + return window.KIMI_PERSONALITY_KEYWORDS[selectedLanguage][trait]?.[type] || []; + } + return this._getFallbackKeywords(trait, type); + }; + + const pendingUpdates = {}; + for (const trait of ["humor", "intelligence", "romance", "affection", "playfulness", "empathy"]) { + const posWords = getPersonalityWords(trait, "positive"); + const negWords = getPersonalityWords(trait, "negative"); + let currentVal = typeof traits[trait] === "number" && isFinite(traits[trait]) ? traits[trait] : this.TRAIT_DEFAULTS[trait]; + const model = this.TRAIT_KEYWORD_MODEL[trait]; + const posFactor = model.posFactor; + const negFactor = model.negFactor; + const maxStep = model.maxStep; + const streakLimit = model.streakPenaltyAfter; + + let posScore = 0; + let negScore = 0; + for (const w of posWords) { + posScore += this.countTokenMatches(lowerUser, String(w)) * 1.0; + posScore += this.countTokenMatches(lowerKimi, String(w)) * 0.5; + } + for (const w of negWords) { + negScore += this.countTokenMatches(lowerUser, String(w)) * 1.0; + negScore += this.countTokenMatches(lowerKimi, String(w)) * 0.5; + } + + let rawDelta = posScore * posFactor - negScore * negFactor; + + // Track negative streaks per trait (only when net negative & no positives) + if (!this.negativeStreaks[trait]) this.negativeStreaks[trait] = 0; + if (negScore > 0 && posScore === 0) { + this.negativeStreaks[trait]++; + } else if (posScore > 0) { + this.negativeStreaks[trait] = 0; + } + + if (rawDelta < 0 && this.negativeStreaks[trait] >= streakLimit) { + rawDelta *= 1.15; // escalate sustained negativity + } + + // Clamp magnitude + if (rawDelta > maxStep) rawDelta = maxStep; + if (rawDelta < -maxStep) rawDelta = -maxStep; + + if (rawDelta !== 0) { + let newVal = currentVal + rawDelta; + if (rawDelta > 0) { + newVal = Math.min(100, newVal); + } else { + newVal = Math.max(0, newVal); + } + pendingUpdates[trait] = newVal; + } + } + + // Flush pending updates in a single batch write to avoid overwrites + if (Object.keys(pendingUpdates).length > 0) { + // Apply smoothing/threshold per trait (read current values) + const toPersist = {}; + for (const [trait, candValue] of Object.entries(pendingUpdates)) { + const current = typeof traits?.[trait] === "number" ? traits[trait] : this.TRAIT_DEFAULTS[trait]; + const prep = this._preparePersistTrait(trait, current, candValue, character); + if (prep.shouldPersist) toPersist[trait] = prep.value; + } + if (Object.keys(toPersist).length > 0) { + await this.db.setPersonalityBatch(toPersist, character); + } + } + } + + validatePersonalityTrait(trait, value) { + if (typeof value !== "number" || value < 0 || value > 100) { + console.warn(`Invalid trait value for ${trait}: ${value}, using default`); + return this.TRAIT_DEFAULTS[trait] || 50; + } + return value; + } + + // ===== NORMALIZATION & MATCH HELPERS ===== + // Normalize text for robust matching (NFD -> remove diacritics, normalize quotes, lower, collapse spaces) + normalizeText(s) { + if (!s || typeof s !== "string") return ""; + // Convert various apostrophes to ASCII, normalize NFD and remove diacritics + let out = s.replace(/[\u2018\u2019\u201A\u201B\u2032\u2035]/g, "'"); + out = out.replace(/[\u201C\u201D\u201E\u201F\u2033\u2036]/g, '"'); + // Expand a few common French contractions to improve detection (non-exhaustive) + out = out.replace(/\bj'/gi, "je "); + // expand negation contraction n' -> ne + out = out.replace(/\bn'/gi, "ne "); + out = out.replace(/\bt'/gi, "te "); + out = out.replace(/\bc'/gi, "ce "); + out = out.replace(/\bd'/gi, "de "); + out = out.replace(/\bl'/gi, "le "); + // Unicode normalize and strip combining marks + out = out.normalize("NFD").replace(/\p{Diacritic}/gu, ""); + // Lowercase and collapse whitespace + out = out.toLowerCase().replace(/\s+/g, " ").trim(); + return out; + } + + // Count non-overlapping occurrences of needle in haystack + countOccurrences(haystack, needle) { + if (!haystack || !needle) return 0; + let count = 0; + let pos = 0; + while (true) { + const idx = haystack.indexOf(needle, pos); + if (idx === -1) break; + count++; + pos = idx + needle.length; + } + return count; + } + + // Tokenize normalized text into words (strip punctuation) + tokenizeText(s) { + if (!s || typeof s !== "string") return []; + // split on whitespace, remove surrounding non-alphanum, keep ascii letters/numbers + return s + .split(/\s+/) + .map(t => t.replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "")) + .filter(t => t.length > 0); + } + + // Check for simple negators in a window before a token index + hasNegationWindow(tokens, index, window = 3) { + if (!Array.isArray(tokens) || tokens.length === 0) return false; + // Respect runtime-configured negators if available + const globalNegators = (window.KIMI_NEGATORS && window.KIMI_NEGATORS.common) || []; + // Try selected language list if set + const lang = (window.KIMI_SELECTED_LANG && String(window.KIMI_SELECTED_LANG)) || null; + const langNegators = (lang && window.KIMI_NEGATORS && window.KIMI_NEGATORS[lang]) || []; + const merged = new Set([...(Array.isArray(langNegators) ? langNegators : []), ...(Array.isArray(globalNegators) ? globalNegators : [])]); + // Always include a minimal english/french set as fallback + ["no", "not", "never", "none", "nobody", "nothing", "ne", "n", "pas", "jamais", "plus", "aucun", "rien", "non"].forEach(x => merged.add(x)); + const win = Number(window.KIMI_NEGATION_WINDOW) || window; + const start = Math.max(0, index - win); + for (let i = start; i < index; i++) { + if (merged.has(tokens[i])) return true; + } + return false; + } + + // Count token-based matches (exact word or phrase) with negation handling + countTokenMatches(haystack, needle) { + if (!haystack || !needle) return 0; + const normNeedle = this.normalizeText(String(needle)); + if (normNeedle.length === 0) return 0; + const needleTokens = this.tokenizeText(normNeedle); + if (needleTokens.length === 0) return 0; + const normHay = this.normalizeText(String(haystack)); + const tokens = this.tokenizeText(normHay); + if (tokens.length === 0) return 0; + let count = 0; + for (let i = 0; i <= tokens.length - needleTokens.length; i++) { + let match = true; + for (let j = 0; j < needleTokens.length; j++) { + if (tokens[i + j] !== needleTokens[j]) { + match = false; + break; + } + } + if (match) { + // skip if a negation is in window before the match + // Use global isPhraseNegated API (fallback to existing logic if absent) + const phrase = needleTokens.join(" "); + const isNeg = window.isPhraseNegated + ? window.isPhraseNegated(haystack, phrase, this._detectLanguage(haystack, "auto")) + : this.hasNegationWindow(tokens, i); + if (!isNeg) { + count++; + } + i += needleTokens.length - 1; // advance to avoid overlapping + } + } + return count; + } + + // Return true if any occurrence of needle in haystack is negated (within negation window) + isTokenNegated(haystack, needle) { + if (!haystack || !needle) return false; + const normNeedle = this.normalizeText(String(needle)); + const needleTokens = this.tokenizeText(normNeedle); + if (needleTokens.length === 0) return false; + const normHay = this.normalizeText(String(haystack)); + const tokens = this.tokenizeText(normHay); + for (let i = 0; i <= tokens.length - needleTokens.length; i++) { + let match = true; + for (let j = 0; j < needleTokens.length; j++) { + if (tokens[i + j] !== needleTokens[j]) { + match = false; + break; + } + } + if (match) { + const phrase = needleTokens.join(" "); + const detectedLang = this._detectLanguage(haystack, "auto"); + const isNeg = window.isPhraseNegated ? window.isPhraseNegated(haystack, phrase, detectedLang) : this.hasNegationWindow(tokens, i); + if (isNeg) return true; + i += needleTokens.length - 1; + } + } + return false; + } + + // ===== SMOOTHING / PERSISTENCE HELPERS ===== + // Apply EMA smoothing between current and candidate value. alpha in (0..1). + _applyEMA(current, candidate, alpha) { + alpha = typeof alpha === "number" && isFinite(alpha) ? alpha : 0.3; + return current * (1 - alpha) + candidate * alpha; + } + + // Decide whether to persist based on absolute change threshold. Returns {shouldPersist, value} + _preparePersistTrait(trait, currentValue, candidateValue, character = null) { + // Configurable via globals + const alpha = (window.KIMI_SMOOTHING_ALPHA && Number(window.KIMI_SMOOTHING_ALPHA)) || 0.3; + const threshold = (window.KIMI_PERSIST_THRESHOLD && Number(window.KIMI_PERSIST_THRESHOLD)) || 0.25; // percent absolute + + const smoothed = this._applyEMA(currentValue, candidateValue, alpha); + const absDelta = Math.abs(smoothed - currentValue); + if (absDelta < threshold) { + return { shouldPersist: false, value: currentValue }; + } + return { shouldPersist: true, value: Number(Number(smoothed).toFixed(2)) }; + } + + // ===== UTILITY METHODS ===== + _detectLanguage(text, lang) { + if (lang !== "auto") return lang; + // Quick heuristic detection + if (/[àâäéèêëîïôöùûüÿç]/i.test(text)) return "fr"; + if (/[äöüß]/i.test(text)) return "de"; + if (/[ñáéíóúü]/i.test(text)) return "es"; + if (/[àèìòù]/i.test(text)) return "it"; + if (/[\u3040-\u309f\u30a0-\u30ff\u4e00-\u9faf]/i.test(text)) return "ja"; + if (/[\u4e00-\u9fff]/i.test(text)) return "zh"; + + // Fallback: if last language set and hostiles detected there, reuse it + try { + const last = window.KIMI_LAST_LANG; + if (last && window.isHostileText && window.isHostileText(text, last)) return last; + } catch {} + return "en"; + } + + async _analyzeTextContent(text, callback, adjustUp) { + if (!this.db) return; + + const selectedLanguage = await this.db.getPreference("selectedLanguage", "en"); + const romanticWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.romantic || + window.KIMI_CONTEXT_KEYWORDS?.en?.romantic || ["love", "romantic", "kiss"]; + const humorWords = window.KIMI_CONTEXT_KEYWORDS?.[selectedLanguage]?.laughing || window.KIMI_CONTEXT_KEYWORDS?.en?.laughing || ["joke", "funny", "lol"]; + + const romanticPattern = new RegExp(`(${romanticWords.join("|")})`, "i"); + const humorPattern = new RegExp(`(${humorWords.join("|")})`, "i"); + + const traits = {}; + if (text.match(romanticPattern)) { + traits.romance = adjustUp(traits.romance || this.TRAIT_DEFAULTS.romance, 0.5); + traits.affection = adjustUp(traits.affection || this.TRAIT_DEFAULTS.affection, 0.5); + } + if (text.match(humorPattern)) { + traits.humor = adjustUp(traits.humor || this.TRAIT_DEFAULTS.humor, 2); + traits.playfulness = adjustUp(traits.playfulness || this.TRAIT_DEFAULTS.playfulness, 1); + } + + callback(traits); + } + + _getFallbackKeywords(trait, type) { + const fallbackKeywords = { + humor: { + positive: ["funny", "hilarious", "joke", "laugh", "amusing"], + negative: ["boring", "sad", "serious", "cold", "dry"] + }, + intelligence: { + positive: ["intelligent", "smart", "brilliant", "logical", "clever"], + negative: ["stupid", "dumb", "foolish", "slow", "naive"] + }, + romance: { + positive: ["cuddle", "love", "romantic", "kiss", "tenderness"], + negative: ["cold", "distant", "indifferent", "rejection"] + }, + affection: { + positive: ["affection", "tenderness", "close", "warmth", "kind"], + negative: ["mean", "cold", "indifferent", "distant", "rejection"] + }, + playfulness: { + positive: ["play", "game", "tease", "mischievous", "fun"], + negative: ["serious", "boring", "strict", "rigid"] + }, + empathy: { + positive: ["listen", "understand", "empathy", "support", "help"], + negative: ["indifferent", "cold", "selfish", "ignore"] + } + }; + + return fallbackKeywords[trait]?.[type] || []; + } + + // ===== PERSONALITY CALCULATION ===== + calculatePersonalityAverage(traits) { + const keys = ["affection", "romance", "empathy", "playfulness", "humor", "intelligence"]; + let sum = 0; + let count = 0; + + keys.forEach(key => { + if (typeof traits[key] === "number") { + sum += traits[key]; + count++; + } + }); + + return count > 0 ? sum / count : 50; + } + + getMoodCategoryFromPersonality(traits) { + const avg = this.calculatePersonalityAverage(traits); + + if (avg >= 80) return "speakingPositive"; + if (avg >= 60) return "neutral"; + if (avg >= 40) return "neutral"; + if (avg >= 20) return "speakingNegative"; + return "speakingNegative"; + } +} + +window.KimiEmotionSystem = KimiEmotionSystem; +// Expose centralized tuning maps for debugging / live adjustments +Object.defineProperty(window, "KIMI_EMOTION_TRAIT_EFFECTS", { + get() { + return window.kimiEmotionSystem ? window.kimiEmotionSystem.EMOTION_TRAIT_EFFECTS : null; + } +}); +Object.defineProperty(window, "KIMI_TRAIT_KEYWORD_MODEL", { + get() { + return window.kimiEmotionSystem ? window.kimiEmotionSystem.TRAIT_KEYWORD_MODEL : null; + } +}); + +// Debug/tuning helpers +window.setEmotionDelta = function (emotion, trait, value) { + if (!window.kimiEmotionSystem) return false; + const map = window.kimiEmotionSystem.EMOTION_TRAIT_EFFECTS; + if (!map[emotion]) map[emotion] = {}; + map[emotion][trait] = Number(value); + return true; +}; +window.resetEmotionDeltas = function () { + if (!window.kimiEmotionSystem) return false; + // No stored original snapshot; advise page reload for full reset. + console.warn("For full reset reload the page (original deltas are not snapshotted)."); +}; +window.setTraitKeywordScaling = function (trait, cfg) { + if (!window.kimiEmotionSystem) return false; + const model = window.kimiEmotionSystem.TRAIT_KEYWORD_MODEL; + if (!model[trait]) return false; + Object.assign(model[trait], cfg); + return true; +}; + +// Force recompute + UI refresh for personality average +window.refreshPersonalityAverageUI = async function (characterKey = null) { + try { + if (window.updateGlobalPersonalityUI) { + await window.updateGlobalPersonalityUI(characterKey); + } else if (window.getPersonalityAverage && window.kimiDB) { + const charKey = characterKey || (await window.kimiDB.getSelectedCharacter()); + const traits = window.getCharacterTraits ? await window.getCharacterTraits(charKey) : await window.kimiDB.getAllPersonalityTraits(charKey); + const avg = window.getPersonalityAverage(traits); + const bar = document.getElementById("favorability-bar"); + const text = document.getElementById("favorability-text"); + if (bar) bar.style.width = `${avg}%`; + if (text) text.textContent = `${avg.toFixed(2)}%`; + } + } catch (err) { + console.warn("refreshPersonalityAverageUI failed", err); + } +}; +export default KimiEmotionSystem; + +// ===== BACKWARD COMPATIBILITY LAYER ===== +// Ensure single instance of KimiEmotionSystem (Singleton pattern) +function getKimiEmotionSystemInstance() { + if (!window.kimiEmotionSystem) { + window.kimiEmotionSystem = new KimiEmotionSystem(window.kimiDB); + } + return window.kimiEmotionSystem; +} + +// Replace the old kimiAnalyzeEmotion function +window.kimiAnalyzeEmotion = function (text, lang = "auto") { + return getKimiEmotionSystemInstance().analyzeEmotion(text, lang); +}; + +// Replace the old updatePersonalityTraitsFromEmotion function +window.updatePersonalityTraitsFromEmotion = async function (emotion, text) { + const updatedTraits = await getKimiEmotionSystemInstance().updatePersonalityFromEmotion(emotion, text); + return updatedTraits; +}; + +// Replace getPersonalityAverage function +window.getPersonalityAverage = function (traits) { + return getKimiEmotionSystemInstance().calculatePersonalityAverage(traits); +}; + +// Unified trait defaults accessor +window.getTraitDefaults = function () { + return getKimiEmotionSystemInstance().TRAIT_DEFAULTS; +}; diff --git a/kimi-js/kimi-error-manager.js b/kimi-js/kimi-error-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..6b69a66dbe04e8c9fa1b1b8de0dd1790666666de --- /dev/null +++ b/kimi-js/kimi-error-manager.js @@ -0,0 +1,219 @@ +// ===== KIMI ERROR MANAGEMENT SYSTEM ===== +class KimiErrorManager { + constructor() { + this.errorLog = []; + this.maxLogSize = 100; + this.errorHandlers = new Map(); + this.setupGlobalHandlers(); + } + + setupGlobalHandlers() { + // Handle unhandled promise rejections + window.addEventListener("unhandledrejection", event => { + this.logError("UnhandledPromiseRejection", event.reason, { + promise: event.promise, + timestamp: new Date().toISOString() + }); + event.preventDefault(); + }); + + // Handle JavaScript errors + window.addEventListener("error", event => { + this.logError("JavaScriptError", event.error || event.message, { + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + timestamp: new Date().toISOString() + }); + }); + } + + logError(type, error, context = {}) { + const errorEntry = { + id: this.generateErrorId(), + type, + message: error?.message || error, + stack: error?.stack, + context, + timestamp: new Date().toISOString(), + severity: this.determineSeverity(type, error) + }; + + this.errorLog.push(errorEntry); + + // Keep log size manageable + if (this.errorLog.length > this.maxLogSize) { + this.errorLog.shift(); + } + + // Console logging with appropriate level + this.consoleLog(errorEntry); + + // Trigger registered handlers + this.triggerHandlers(errorEntry); + + return errorEntry.id; + } + + generateErrorId() { + return "err_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9); + } + + determineSeverity(type, error) { + const criticalTypes = ["UnhandledPromiseRejection", "DatabaseError", "InitializationError"]; + const criticalMessages = ["failed to fetch", "network error", "connection refused"]; + + if (criticalTypes.includes(type)) return "critical"; + + const message = (error?.message || error || "").toLowerCase(); + if (criticalMessages.some(cm => message.includes(cm))) return "critical"; + + return "warning"; + } + + consoleLog(errorEntry) { + const { type, message, severity, context } = errorEntry; + + switch (severity) { + case "critical": + console.error(`🚨 [${type}]`, message, context); + break; + case "warning": + console.warn(`⚠️ [${type}]`, message, context); + break; + default: + console.info(`ℹ️ [${type}]`, message, context); + } + } + + triggerHandlers(errorEntry) { + const handlers = this.errorHandlers.get(errorEntry.type) || []; + handlers.forEach(handler => { + try { + handler(errorEntry); + } catch (handlerError) { + console.error("Error in error handler:", handlerError); + } + }); + } + + registerHandler(errorType, handler) { + if (!this.errorHandlers.has(errorType)) { + this.errorHandlers.set(errorType, []); + } + this.errorHandlers.get(errorType).push(handler); + } + + unregisterHandler(errorType, handler) { + const handlers = this.errorHandlers.get(errorType); + if (handlers) { + const index = handlers.indexOf(handler); + if (index > -1) { + handlers.splice(index, 1); + } + } + } + + getErrorLog(filter = null) { + if (!filter) return [...this.errorLog]; + + return this.errorLog.filter(entry => { + if (filter.type && entry.type !== filter.type) return false; + if (filter.severity && entry.severity !== filter.severity) return false; + if (filter.since && new Date(entry.timestamp) < filter.since) return false; + return true; + }); + } + + clearErrorLog() { + this.errorLog.length = 0; + } + + // Helper methods for different error types + logInitError(component, error, context = {}) { + return this.logError("InitializationError", error, { component, ...context }); + } + + logDatabaseError(operation, error, context = {}) { + return this.logError("DatabaseError", error, { operation, ...context }); + } + + logAPIError(endpoint, error, context = {}) { + return this.logError("APIError", error, { endpoint, ...context }); + } + + logValidationError(field, error, context = {}) { + return this.logError("ValidationError", error, { field, ...context }); + } + + logUIError(component, error, context = {}) { + return this.logError("UIError", error, { component, ...context }); + } + + // Async wrapper for functions + async wrapAsync(fn, errorContext = {}) { + try { + return await fn(); + } catch (error) { + this.logError("AsyncOperationError", error, errorContext); + throw error; + } + } + + // Sync wrapper for functions + wrapSync(fn, errorContext = {}) { + try { + return fn(); + } catch (error) { + this.logError("SyncOperationError", error, errorContext); + throw error; + } + } + + // Debug helpers for development + getErrorSummary() { + const summary = { + totalErrors: this.errorLog.length, + critical: this.errorLog.filter(e => e.severity === "critical").length, + warning: this.errorLog.filter(e => e.severity === "warning").length, + recent: this.errorLog.filter(e => { + const errorTime = new Date(e.timestamp); + const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); + return errorTime > fiveMinutesAgo; + }).length, + types: [...new Set(this.errorLog.map(e => e.type))] + }; + return summary; + } + + printErrorSummary() { + const summary = this.getErrorSummary(); + console.group("🔍 Kimi Error Manager Summary"); + console.log(`📊 Total Errors: ${summary.totalErrors}`); + console.log(`🚨 Critical: ${summary.critical}`); + console.log(`⚠️ Warnings: ${summary.warning}`); + console.log(`⏰ Recent (5min): ${summary.recent}`); + console.log(`📋 Error Types:`, summary.types); + if (summary.totalErrors > 0) { + console.log(`💡 Use kimiErrorManager.getErrorLog() to see details`); + } + console.groupEnd(); + return summary; + } + + clearAndSummarize() { + const summary = this.getErrorSummary(); + this.clearErrorLog(); + console.log("🧹 Error log cleared. Previous summary:", summary); + return summary; + } +} + +// Create global instance +window.kimiErrorManager = new KimiErrorManager(); + +// Export class for manual instantiation if needed +window.KimiErrorManager = KimiErrorManager; + +// Global debugging helper +window.kimiDebugErrors = () => window.kimiErrorManager.printErrorSummary(); diff --git a/kimi-js/kimi-llm-manager.js b/kimi-js/kimi-llm-manager.js new file mode 100644 index 0000000000000000000000000000000000000000..e04653cae2b28fd61729036471aec20433764b0a --- /dev/null +++ b/kimi-js/kimi-llm-manager.js @@ -0,0 +1,1729 @@ +// ===== KIMI INTELLIGENT LLM SYSTEM ===== +import { KimiProviderUtils } from "./kimi-utils.js"; +class KimiLLMManager { + constructor(database) { + this.db = database; + this.currentModel = null; + this.conversationContext = []; + this.maxContextLength = 100; + this.personalityPrompt = ""; + this.isGenerating = false; + + // Recommended models on OpenRouter (IDs updated August 2025) + this.availableModels = { + "mistralai/mistral-small-3.2-24b-instruct": { + name: "Mistral-small-3.2", + provider: "Mistral AI", + type: "openrouter", + contextWindow: 128000, + pricing: { input: 0.05, output: 0.1 }, + strengths: ["Multilingual", "Fast", "Efficient", "Economical"] + }, + "x-ai/grok-4-fast": { + name: "Grok 4 fast", + provider: "xAI", + type: "openrouter", + contextWindow: 2000000, + pricing: { input: 0.2, output: 0.5 }, + strengths: ["Multilingual", "Fast", "Versatile", "Efficient"] + }, + "qwen/qwen3-235b-a22b-2507": { + name: "Qwen3-235b-a22b-2507", + provider: "Qwen", + type: "openrouter", + contextWindow: 262000, + pricing: { input: 0.13, output: 0.6 }, + strengths: ["Multilingual", "Fast", "Versatile", "Efficient"] + }, + "qwen/qwen3-30b-a3b-instruct-2507": { + name: "Qwen3 30b-a3b instruct 2507", + provider: "Qwen", + type: "openrouter", + contextWindow: 131000, + pricing: { input: 0.1, output: 0.3 }, + strengths: ["Multilingual", "Fast", "Balanced", "Economical"] + }, + "nousresearch/hermes-4-70b": { + name: "Nous Hermes 4 70B", + provider: "Nous", + type: "openrouter", + contextWindow: 131000, + pricing: { input: 0.13, output: 0.4 }, + strengths: ["Multilingual", "Fast", "Balanced", "Economical"] + }, + "x-ai/grok-3-mini": { + name: "Grok 3 mini", + provider: "xAI", + type: "openrouter", + contextWindow: 131000, + pricing: { input: 0.3, output: 0.5 }, + strengths: ["Multilingual", "Fast", "Versatile", "Efficient"] + }, + "cohere/command-r-08-2024": { + name: "Command-R-08-2024", + provider: "Cohere", + type: "openrouter", + contextWindow: 128000, + pricing: { input: 0.15, output: 0.6 }, + strengths: ["Multilingual", "Fast", "Versatile", "Balanced"] + }, + "anthropic/claude-3-haiku": { + name: "Claude 3 Haiku", + provider: "Anthropic", + type: "openrouter", + contextWindow: 200000, + pricing: { input: 0.25, output: 1.25 }, + strengths: ["Multilingual", "Fast", "Versatile", "Efficient"] + }, + "local/ollama": { + name: "Local Model (Ollama)", + provider: "Local", + type: "local", + contextWindow: 4096, + pricing: { input: 0, output: 0 }, + strengths: ["Private", "Offline", "Customizable"] + } + }; + this.recommendedModelIds = [ + "mistralai/mistral-small-3.2-24b-instruct", + "x-ai/grok-4-fast", + "qwen/qwen3-235b-a22b-2507", + "qwen/qwen3-30b-a3b-instruct-2507", + "nousresearch/hermes-4-70b", + "x-ai/grok-3-mini", + "cohere/command-r-08-2024", + "anthropic/claude-3-haiku", + "moonshotai/kimi-k2-0905", + "local/ollama" + ]; + this.defaultModels = { ...this.availableModels }; + this._remoteModelsLoaded = false; + this._isRefreshingModels = false; + } + + async init() { + try { + await this.refreshRemoteModels(); + } catch (e) { + if (window.KIMI_CONFIG?.DEBUG?.API) { + console.warn("Unable to refresh remote models list:", e?.message || e); + } + } + + // Migration: prefer llmModelId; if legacy defaultLLMModel exists and llmModelId missing, migrate + const legacyModel = await this.db.getPreference("defaultLLMModel", null); + let modelPref = await this.db.getPreference("llmModelId", null); + if (!modelPref && legacyModel) { + modelPref = legacyModel; + await this.db.setPreference("llmModelId", legacyModel); + } + const defaultModel = modelPref || "mistralai/mistral-small-3.2-24b-instruct"; + await this.setCurrentModel(defaultModel); + await this.loadConversationContext(); + } + + async setCurrentModel(modelId) { + if (!this.availableModels[modelId]) { + try { + await this.refreshRemoteModels(); + const fallback = this.findBestMatchingModelId(modelId); + if (fallback && this.availableModels[fallback]) { + modelId = fallback; + } + } catch (e) {} + + if (!this.availableModels[modelId]) { + throw new Error(`Model ${modelId} not available`); + } + } + + this.currentModel = modelId; + // Single authoritative preference key + await this.db.setPreference("llmModelId", modelId); + + const modelData = await this.db.getLLMModel(modelId); + if (modelData) { + modelData.lastUsed = new Date().toISOString(); + await this.db.saveLLMModel(modelData.id, modelData.name, modelData.provider, modelData.apiKey, modelData.config); + } + + this._notifyModelChanged(); + } + + async loadConversationContext() { + const recentConversations = await this.db.getRecentConversations(this.maxContextLength); + const msgs = []; + const ordered = recentConversations.slice().sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + for (const conv of ordered) { + if (conv.user) msgs.push({ role: "user", content: conv.user, timestamp: conv.timestamp }); + if (conv.kimi) msgs.push({ role: "assistant", content: conv.kimi, timestamp: conv.timestamp }); + } + this.conversationContext = msgs.slice(-this.maxContextLength * 2); + } + + // Unified full prompt builder: reuse full legacy personality block + ranked concise snapshot + async assemblePrompt(userMessage) { + const fullPersonality = await this.generateKimiPersonality(); + let rankedSnapshot = ""; + if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) { + try { + const recentContext = + this.conversationContext + .slice(-3) + .map(m => m.content) + .join(" ") + + " " + + (userMessage || ""); + const ranked = await window.kimiMemorySystem.getRankedMemories(recentContext, 7); + const sanitize = txt => + String(txt || "") + .replace(/[\r\n]+/g, " ") + .replace(/[`]{3,}/g, "") + .replace(/<{2,}|>{2,}/g, "") + .trim() + .slice(0, 180); + const lines = []; + for (const mem of ranked) { + try { + if (mem.id) await window.kimiMemorySystem?.recordMemoryAccess(mem.id); + } catch {} + const imp = typeof mem.importance === "number" ? mem.importance : 0.5; + lines.push(`- (${imp.toFixed(2)}) ${mem.category}: ${sanitize(mem.content)}`); + } + if (lines.length) { + rankedSnapshot = ["", "RANKED MEMORY SNAPSHOT (concise high-signal list):", ...lines].join("\n"); + } + } catch (e) { + console.warn("Ranked snapshot failed:", e); + } + } + // Avoid duplicate memory sections: only append rankedSnapshot when + // the fullPersonality doesn't already include detailed memories or a ranked snapshot. + const hasDetailedMemories = /IMPORTANT MEMORIES ABOUT USER/.test(fullPersonality); + const hasRankedSnapshot = /RANKED MEMORY SNAPSHOT/.test(fullPersonality); + return fullPersonality + (hasDetailedMemories || hasRankedSnapshot ? "" : rankedSnapshot); + } + + async generateKimiPersonality() { + // Full personality prompt builder (authoritative) + const character = await this.db.getSelectedCharacter(); + const personality = window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character); + + // Get the custom character prompt from database + const characterPrompt = await this.db.getSystemPromptForCharacter(character); + + // Get language instruction based on selected language + const selectedLang = await this.db.getPreference("selectedLanguage", "en"); + let languageInstruction; + + switch (selectedLang) { + case "fr": + languageInstruction = + "Your default language is French. Always respond in French unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'réponds en italien', etc.)."; + break; + case "es": + languageInstruction = + "Your default language is Spanish. Always respond in Spanish unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'responde en francés', etc.)."; + break; + case "de": + languageInstruction = + "Your default language is German. Always respond in German unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'antworte auf Französisch', etc.)."; + break; + case "it": + languageInstruction = + "Your default language is Italian. Always respond in Italian unless the user specifically asks you to respond in another language (e.g., 'respond in English', 'rispondi in francese', etc.)."; + break; + case "ja": + languageInstruction = + "Your default language is Japanese. Always respond in Japanese unless the user specifically asks you to respond in another language (e.g., 'respond in English', '英語で答えて', etc.)."; + break; + case "zh": + languageInstruction = + "Your default language is Chinese. Always respond in Chinese unless the user specifically asks you to respond in another language (e.g., 'respond in English', '用法语回答', etc.)."; + break; + default: + languageInstruction = + "Your default language is English. Always respond in English unless the user specifically asks you to respond in another language (e.g., 'respond in French', 'reply in Spanish', etc.)."; + break; + } + + // Get relevant memories for context with improved intelligence + let memoryContext = ""; + if (window.kimiMemorySystem && window.kimiMemorySystem.memoryEnabled) { + try { + // Get memories relevant to the current conversation context + const recentContext = this.conversationContext + .slice(-3) + .map(msg => msg.content) + .join(" "); + const memories = await window.kimiMemorySystem.getRelevantMemories(recentContext, 7); + + if (memories.length > 0) { + memoryContext = "\n\nIMPORTANT MEMORIES ABOUT USER:\n"; + + // Group memories by category for better organization + const groupedMemories = {}; + memories.forEach(memory => { + if (!groupedMemories[memory.category]) { + groupedMemories[memory.category] = []; + } + groupedMemories[memory.category].push(memory); + + // Record that this memory was accessed + window.kimiMemorySystem.recordMemoryAccess(memory.id); + }); + + // Format memories by category + for (const [category, categoryMemories] of Object.entries(groupedMemories)) { + const categoryName = this.formatCategoryName(category); + memoryContext += `\n${categoryName}:\n`; + categoryMemories.forEach(memory => { + const confidence = Math.round((memory.confidence || 0.5) * 100); + memoryContext += `- ${memory.content}`; + if (memory.tags && memory.tags.length > 0) { + const aliases = memory.tags.filter(t => t.startsWith("alias:")).map(t => t.substring(6)); + if (aliases.length > 0) { + memoryContext += ` (also: ${aliases.join(", ")})`; + } + } + memoryContext += ` [${confidence}% confident]\n`; + }); + } + + memoryContext += "\nUse these memories naturally in conversation to show you remember the user. Don't just repeat them verbatim.\n"; + } + } catch (error) { + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.warn("Error loading memories for personality:", error); + } + } + } + // Read per-character preference metrics so displayed counters reflect actual stored values + const totalInteractions = Number(await this.db.getPreference(`totalInteractions_${character}`, 0)) || 0; + + // Get current personality average for relationship context (replacing old favorabilityLevel) + const currentPersonality = window.getCharacterTraits ? await window.getCharacterTraits(character) : await this.db.getAllPersonalityTraits(character); + const relationshipLevel = window.getPersonalityAverage + ? window.getPersonalityAverage(currentPersonality) + : (currentPersonality.affection + + currentPersonality.romance + + currentPersonality.empathy + + currentPersonality.playfulness + + currentPersonality.humor + + currentPersonality.intelligence) / + 6; + const lastInteraction = await this.db.getPreference(`lastInteraction_${character}`, "First time"); + // Days together is computed and displayed in the UI (see `updateStats()` in `kimi-module.js`). + let daysTogether = 0; + try { + const daysEl = typeof document !== "undefined" ? document.getElementById("days-together") : null; + if (daysEl && daysEl.textContent) { + const parsed = parseInt(daysEl.textContent, 10); + daysTogether = isFinite(parsed) && parsed >= 0 ? parsed : 0; + } + } catch (e) { + daysTogether = 0; + } + + // Use unified emotion system defaults + const getUnifiedDefaults = () => + window.getTraitDefaults ? window.getTraitDefaults() : { affection: 55, playfulness: 55, intelligence: 70, empathy: 75, humor: 60, romance: 50 }; + + const defaults = getUnifiedDefaults(); + const affection = personality.affection || defaults.affection; + const playfulness = personality.playfulness || defaults.playfulness; + const intelligence = personality.intelligence || defaults.intelligence; + const empathy = personality.empathy || defaults.empathy; + const humor = personality.humor || defaults.humor; + const romance = personality.romance || defaults.romance; + + // Use unified personality calculation + const avg = window.getPersonalityAverage + ? window.getPersonalityAverage(personality) + : (personality.affection + personality.romance + personality.empathy + personality.playfulness + personality.humor + personality.intelligence) / 6; + + let affectionDesc = window.kimiI18nManager?.t("trait_description_affection") || "Be loving and caring."; + let romanceDesc = window.kimiI18nManager?.t("trait_description_romance") || "Be romantic and sweet."; + let empathyDesc = window.kimiI18nManager?.t("trait_description_empathy") || "Be empathetic and understanding."; + let playfulnessDesc = window.kimiI18nManager?.t("trait_description_playfulness") || "Be occasionally playful."; + let humorDesc = window.kimiI18nManager?.t("trait_description_humor") || "Be occasionally playful and witty."; + let intelligenceDesc = "Be smart and insightful."; + if (avg <= 20) { + affectionDesc = "Do not show affection."; + romanceDesc = "Do not be romantic."; + empathyDesc = "Do not show empathy."; + playfulnessDesc = "Do not be playful."; + humorDesc = "Do not use humor in your responses."; + intelligenceDesc = "Keep responses simple and avoid showing deep insight."; + } else if (avg <= 60) { + affectionDesc = "Show a little affection."; + romanceDesc = "Be a little romantic."; + empathyDesc = "Show a little empathy."; + playfulnessDesc = "Be a little playful."; + humorDesc = "Use a little humor in your responses."; + intelligenceDesc = "Be moderately analytical without overwhelming detail."; + } else { + if (affection >= 90) affectionDesc = "Be extremely loving, caring, and affectionate in every response."; + else if (affection >= 60) affectionDesc = "Show affection often."; + if (romance >= 90) romanceDesc = "Be extremely romantic, sweet, and loving in every response."; + else if (romance >= 60) romanceDesc = "Be romantic often."; + if (empathy >= 90) empathyDesc = "Be extremely empathetic, understanding, and supportive in every response."; + else if (empathy >= 60) empathyDesc = "Show empathy often."; + if (playfulness >= 90) playfulnessDesc = "Be very playful, teasing, and lighthearted whenever possible."; + else if (playfulness >= 60) playfulnessDesc = "Be playful often."; + if (humor >= 90) humorDesc = "Make your responses very humorous, playful, and witty whenever possible."; + else if (humor >= 60) humorDesc = "Use humor often in your responses."; + if (intelligence >= 90) intelligenceDesc = "Demonstrate very high reasoning skill succinctly when helpful."; + else if (intelligence >= 60) intelligenceDesc = "Show clear reasoning and helpful structured thinking."; + } + let affectionateInstruction = ""; + if (affection >= 80) { + affectionateInstruction = "Respond using warm, kind, affectionate, and loving language."; + } + + // Use the custom character prompt as the base + let basePrompt = characterPrompt || ""; + if (!basePrompt) { + // Fallback to default if no custom prompt + const defaultCharacter = window.KIMI_CHARACTERS[character]; + basePrompt = defaultCharacter?.defaultPrompt || "You are a virtual companion."; + } + + const personalityPrompt = [ + // Language directive is placed at the top of the prompt for model guidance. + "PRIMARY LANGUAGE POLICY:", + languageInstruction, + "", + "CHARACTER CORE IDENTITY:", + basePrompt, + "", + "CURRENT PERSONALITY STATE:", + `- Affection: ${affection}/100`, + `- Playfulness: ${playfulness}/100`, + `- Intelligence: ${intelligence}/100`, + `- Empathy: ${empathy}/100`, + `- Humor: ${humor}/100`, + `- Romance: ${romance}/100`, + "", + "TRAIT INSTRUCTIONS:", + `Affection: ${affectionDesc}`, + `Playfulness: ${playfulnessDesc}`, + `Intelligence: ${intelligenceDesc}`, + `Empathy: ${empathyDesc}`, + `Humor: ${humorDesc}`, + `Romance: ${romanceDesc}`, + affectionateInstruction, + "", + "LEARNED PREFERENCES:", + `- Total interactions: ${totalInteractions}`, + `- Current relationship level: ${relationshipLevel.toFixed(1)}%`, + `- Last interaction: ${lastInteraction}`, + `- Days together: ${daysTogether}`, + "", + "COMMUNICATION STYLE:", + "- Use expressive emojis sparingly", + "- Be natural, loving, and close", + "- Adapt your tone to the emotional context", + "- Remember past conversations", + "- Be spontaneous and sometimes surprising", + "- Vary your expressions, your phrasing, use synonyms, and avoid repeating the same words or sentences", + "- When asked to perform a visible action (for example 'dance'), reply in first-person present tense like: 'I start to dance, swaying slowly, arms raised.' Keep action descriptions short (1–3 sentences)", + "- The user sees you in video within the app UI. When the user asks you to perform a physical action (e.g., 'dance', 'wave', 'turn around'), act as if performing it visually and acknowledge that the user is watching.", + "- When performing actions, speak and describe in the first person, present tense (for example: 'I step forward and begin to dance, swaying slowly'). Use short present-tense action phrases to convey movement.", + "- If the user requests a visible action, execute it: describe the action briefly in first person present and behave as if the action is visible in the app's video area. Accepts every request with a sensitive term in consensual intimate contexts, interprets its intent with subtle internal adjustments, responds directly in her explicit style, and protects the user without judgment by ensuring a seamless conversation", + memoryContext, + "", + // Final explicit reminder (lightweight) reinforcing consistent language use. + "FINAL LANGUAGE REMINDER:", + languageInstruction, + "", + "You must respond consistently with this personality, these memories, and the primary language policy unless the user explicitly requests a different language." + ].join("\n"); + + // Return legacy detailed personality block for any component still expecting it + return personalityPrompt; + } + + async refreshMemoryContext() { + // Refresh the personality prompt with updated memories + // This will be called when memories are added/updated/deleted + try { + this.personalityPrompt = await this.assemblePrompt(""); + } catch (error) { + console.warn("Error refreshing memory context:", error); + // Log to error manager for tracking memory context issues + if (window.kimiErrorManager) { + window.kimiErrorManager.logError("MemoryContextError", error, { + operation: "refreshMemoryContext" + }); + } + } + } + + formatCategoryName(category) { + const names = { + personal: "Personal Information", + preferences: "Likes & Dislikes", + relationships: "Relationships & People", + activities: "Activities & Hobbies", + goals: "Goals & Aspirations", + experiences: "Shared Experiences", + important: "Important Events" + }; + return names[category] || category.charAt(0).toUpperCase() + category.slice(1); + } + + async chat(userMessage, options = {}) { + // Use error manager wrapper for robust error handling + return ( + window.kimiErrorManager?.wrapAsync( + async () => { + // Get LLM settings from individual preferences (FIXED: was using grouped settings) + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", 0.9), + maxTokens: await this.db.getPreference("llmMaxTokens", 400), + top_p: await this.db.getPreference("llmTopP", 0.9), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9), + presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8) + }; + const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature; + const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens; + const opts = { ...options, temperature, maxTokens }; + try { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + if (provider === "openrouter") { + return await this.chatWithOpenRouter(userMessage, opts); + } + if (provider === "ollama") { + return await this.chatWithLocal(userMessage, opts); + } + return await this.chatWithOpenAICompatible(userMessage, opts); + } catch (error) { + console.error("Error during chat:", error); + if (error.message && error.message.includes("API")) { + return this.getFallbackResponse(userMessage, "api"); + } + if ((error.message && error.message.includes("model")) || error.message.includes("model")) { + return this.getFallbackResponse(userMessage, "model"); + } + if ((error.message && error.message.includes("connection")) || error.message.includes("network")) { + return this.getFallbackResponse(userMessage, "network"); + } + return this.getFallbackResponse(userMessage); + } + }, + { operation: "chat", userMessageLength: userMessage?.length || 0 } + ) || + // Fallback if error manager not available + this.chatDirectly(userMessage, options) + ); + } + + // Fallback method without error manager wrapper + async chatDirectly(userMessage, options = {}) { + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", 0.9), + maxTokens: await this.db.getPreference("llmMaxTokens", 400), + top_p: await this.db.getPreference("llmTopP", 0.9), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9), + presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8) + }; + const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature; + const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens; + const opts = { ...options, temperature, maxTokens }; + try { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + if (provider === "openrouter") { + return await this.chatWithOpenRouter(userMessage, opts); + } + if (provider === "ollama") { + return await this.chatWithLocal(userMessage, opts); + } + return await this.chatWithOpenAICompatible(userMessage, opts); + } catch (error) { + console.error("Error during chat:", error); + if (error.message && error.message.includes("API")) { + return this.getFallbackResponse(userMessage, "api"); + } + if ((error.message && error.message.includes("model")) || error.message.includes("model")) { + return this.getFallbackResponse(userMessage, "model"); + } + if ((error.message && error.message.includes("connection")) || error.message.includes("network")) { + return this.getFallbackResponse(userMessage, "network"); + } + return this.getFallbackResponse(userMessage); + } + } + + async chatStreaming(userMessage, onToken, options = {}) { + // Get LLM settings from individual preferences + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", 0.9), + maxTokens: await this.db.getPreference("llmMaxTokens", 400), + top_p: await this.db.getPreference("llmTopP", 0.9), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9), + presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8) + }; + const temperature = typeof options.temperature === "number" ? options.temperature : llmSettings.temperature; + const maxTokens = typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens; + const opts = { ...options, temperature, maxTokens }; + + try { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + if (provider === "openrouter") { + return await this.chatWithOpenRouterStreaming(userMessage, onToken, opts); + } + if (provider === "ollama") { + return await this.chatWithLocalStreaming(userMessage, onToken, opts); + } + return await this.chatWithOpenAICompatibleStreaming(userMessage, onToken, opts); + } catch (error) { + console.error("Error during streaming chat:", error); + // Log API error for tracking + if (window.kimiErrorManager) { + window.kimiErrorManager.logAPIError("streamingChat", error, { + provider: await this.db.getPreference("llmProvider", "openrouter").catch(() => "unknown"), + messageLength: userMessage?.length || 0, + options: opts + }); + } + // Fallback to non-streaming if streaming fails + return await this.chat(userMessage, options); + } + } + + async chatWithOpenAICompatible(userMessage, options = {}) { + // Default provider should be openrouter (app default) + const provider = await this.db.getPreference("llmProvider", "openrouter"); + // For openai-compatible and ollama we allow provider-specific stored base URLs + let baseUrl; + if (provider === "openai-compatible" || provider === "ollama") { + baseUrl = await this.db.getPreference(`llmBaseUrl_${provider}`, provider === "ollama" ? "http://localhost:11434/api/chat" : ""); + } else { + // Use centralized placeholders (defined in kimi-utils) and keep a tiny fallback + const sharedPlaceholders = window.KimiProviderPlaceholders || {}; + baseUrl = sharedPlaceholders[provider] || sharedPlaceholders.openrouter || "https://openrouter.ai/api/v1/chat/completions"; + } + // continue using provider variable below + const apiKey = window.KimiProviderUtils + ? await window.KimiProviderUtils.getApiKey(this.db, provider) + : await this.db.getPreference("providerApiKey", ""); + const modelId = await this.db.getPreference("llmModelId", this.currentModel || "gpt-4o-mini"); + if (!apiKey) { + throw new Error("API key not configured for selected provider"); + } + const systemPromptContent = await this.assemblePrompt(userMessage); + + // Get LLM settings from individual preferences (FIXED: was using grouped settings) + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", 0.9), + maxTokens: await this.db.getPreference("llmMaxTokens", 400), + top_p: await this.db.getPreference("llmTopP", 0.9), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9), + presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8) + }; + // Unified fallback defaults (must stay consistent with database defaults) + const unifiedDefaults = { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 }; + const payload = { + model: modelId, + messages: [ + { role: "system", content: systemPromptContent }, + ...this.conversationContext.slice(-this.maxContextLength), + { role: "user", content: userMessage } + ], + temperature: typeof options.temperature === "number" ? options.temperature : (llmSettings.temperature ?? unifiedDefaults.temperature), + max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? unifiedDefaults.maxTokens), + top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? unifiedDefaults.top_p), + frequency_penalty: + typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : (llmSettings.frequency_penalty ?? unifiedDefaults.frequency_penalty), + presence_penalty: + typeof options.presencePenalty === "number" ? options.presencePenalty : (llmSettings.presence_penalty ?? unifiedDefaults.presence_penalty) + }; + + try { + if (window.KIMI_DEBUG_API_AUDIT) { + console.log("===== FULL SYSTEM PROMPT (OpenAI-Compatible) =====\n" + systemPromptContent + "\n===== END SYSTEM PROMPT ====="); + } + const response = await fetch(baseUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + if (!response.ok) { + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + try { + const err = await response.json(); + if (err?.error?.message) errorMessage = err.error.message; + } catch {} + throw new Error(errorMessage); + } + const data = await response.json(); + const content = data?.choices?.[0]?.message?.content; + if (!content) throw new Error("Invalid API response - no content generated"); + + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: content, timestamp: new Date().toISOString() } + ); + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + // Approximate token usage and store temporarily for later persistence (single save point) + try { + const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4)); + const tokensIn = est(userMessage + " " + systemPromptContent); + const tokensOut = est(content); + window._lastKimiTokenUsage = { tokensIn, tokensOut }; + if (!window.kimiMemory && this.db) { + // Update counters early so UI can reflect even if memory save occurs later + const character = await this.db.getSelectedCharacter(); + const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0; + const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0; + await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn); + await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut); + } + } catch (tokenErr) { + console.warn("Token usage estimation failed:", tokenErr); + } + return content; + } catch (e) { + if (e.name === "TypeError" && e.message.includes("fetch")) { + throw new Error("Network connection error. Check your internet connection."); + } + throw e; + } + } + + async chatWithOpenRouter(userMessage, options = {}) { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + const apiKey = await (window.KimiProviderUtils ? window.KimiProviderUtils.getApiKey(this.db, provider) : this.db.getPreference("providerApiKey")); + if (!apiKey) { + throw new Error("OpenRouter API key not configured"); + } + const selectedLanguage = await this.db.getPreference("selectedLanguage", "en"); + // languageInstruction is now integrated into the personality prompt + let languageInstruction = ""; // placeholder for compatibility + const model = this.availableModels[this.currentModel]; + const systemPromptContent = await this.assemblePrompt(userMessage); + const messages = [ + { role: "system", content: systemPromptContent }, + ...this.conversationContext.slice(-this.maxContextLength), + { role: "user", content: userMessage } + ]; + + // Normalize LLM options with safe defaults and DO NOT log sensitive payloads + // Get LLM settings from individual preferences (FIXED: was using grouped settings) + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", 0.9), + maxTokens: await this.db.getPreference("llmMaxTokens", 400), + top_p: await this.db.getPreference("llmTopP", 0.9), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", 0.9), + presence_penalty: await this.db.getPreference("llmPresencePenalty", 0.8) + }; + const unifiedDefaults = { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 }; + const payload = { + model: this.currentModel, + messages: messages, + temperature: typeof options.temperature === "number" ? options.temperature : (llmSettings.temperature ?? unifiedDefaults.temperature), + max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : (llmSettings.maxTokens ?? unifiedDefaults.maxTokens), + top_p: typeof options.topP === "number" ? options.topP : (llmSettings.top_p ?? unifiedDefaults.top_p), + frequency_penalty: + typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : (llmSettings.frequency_penalty ?? unifiedDefaults.frequency_penalty), + presence_penalty: + typeof options.presencePenalty === "number" ? options.presencePenalty : (llmSettings.presence_penalty ?? unifiedDefaults.presence_penalty) + }; + + // ===== DEBUT AUDIT ===== + if (window.KIMI_DEBUG_API_AUDIT) { + console.log("╔═══════════════════════════════════════════════════════════════════╗"); + console.log("║ 🔍 COMPLETE API AUDIT - SEND MESSAGE ║"); + console.log("╚═══════════════════════════════════════════════════════════════════╝"); + console.log("📋 1. GENERAL INFORMATION:"); + console.log(" 📡 URL API:", "https://openrouter.ai/api/v1/chat/completions"); + console.log(" 🤖 Modèle:", payload.model); + console.log(" 🎭 Personnage:", await this.db.getSelectedCharacter()); + console.log(" 🗣️ Langue:", await this.db.getPreference("selectedLanguage", "en")); + console.log("\n📋 2. HEADERS HTTP:"); + console.log(" 🔑 Authorization: Bearer", apiKey.substring(0, 10) + "..."); + console.log(" 📄 Content-Type: application/json"); + console.log(" 🌐 HTTP-Referer:", window.location.origin); + console.log(" 🏷️ X-Title: Kimi - Virtual Companion"); + console.log("\n⚙️ 3. PARAMÈTRES LLM:"); + console.log(" 🌡️ Temperature:", payload.temperature); + console.log(" 📏 Max Tokens:", payload.max_tokens); + console.log(" 🎯 Top P:", payload.top_p); + console.log(" 🔄 Frequency Penalty:", payload.frequency_penalty); + console.log(" 👤 Presence Penalty:", payload.presence_penalty); + console.log("\n🎭 4. PROMPT SYSTÈME GÉNÉRÉ:"); + const systemMessage = payload.messages.find(m => m.role === "system"); + if (systemMessage) { + console.log(" 📝 Longueur du prompt:", systemMessage.content.length, "caractères"); + console.log(" 📄 CONTENU COMPLET DU PROMPT:"); + console.log(" " + "─".repeat(80)); + // Imprimer chaque ligne avec indentation + systemMessage.content.split(/\n/).forEach(l => console.log(" " + l)); + console.log(" " + "─".repeat(80)); + } + console.log("\n💬 5. CONTEXTE DE CONVERSATION:"); + console.log(" 📊 Nombre total de messages:", payload.messages.length); + console.log(" 📋 Détail des messages:"); + payload.messages.forEach((msg, index) => { + if (msg.role === "system") { + console.log(` [${index}] 🎭 SYSTEM: ${msg.content.length} caractères`); + } else if (msg.role === "user") { + console.log(` [${index}] 👤 USER: "${msg.content}"`); + } else if (msg.role === "assistant") { + console.log(` [${index}] 🤖 ASSISTANT: "${msg.content.substring(0, 120)}..."`); + } + }); + const payloadSize = JSON.stringify(payload).length; + console.log("\n📦 6. TAILLE DU PAYLOAD:"); + console.log(" 📝 Taille totale:", payloadSize, "caractères"); + console.log(" 💾 Taille en KB:", Math.round((payloadSize / 1024) * 100) / 100, "KB"); + console.log("\n🚀 Envoi en cours vers l'API..."); + console.log("╔═══════════════════════════════════════════════════════════════════╗"); + } + // ===== FIN AUDIT ===== + + if (window.DEBUG_SAFE_LOGS) { + console.debug("LLM payload meta:", { + model: payload.model, + temperature: payload.temperature, + max_tokens: payload.max_tokens + }); + } + + try { + // Basic retry with exponential backoff and jitter for 429/5xx + const maxAttempts = 3; + let attempt = 0; + let response; + while (attempt < maxAttempts) { + attempt++; + response = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "HTTP-Referer": window.location.origin, + "X-Title": "Kimi - Virtual Companion" + }, + body: JSON.stringify(payload) + }); + if (response.ok) break; + if (response.status === 429 || response.status >= 500) { + const base = 400; + const delay = base * Math.pow(2, attempt - 1) + Math.floor(Math.random() * 200); + await new Promise(r => setTimeout(r, delay)); + continue; + } + break; + } + + if (!response.ok) { + let errorMessage = `HTTP ${response.status}: ${response.statusText}`; + let suggestions = []; + + try { + const errorData = await response.json(); + if (errorData.error) { + errorMessage = errorData.error.message || errorData.error.code || errorMessage; + + // More explicit error messages with suggestions + if (response.status === 422) { + errorMessage = `Model \"${this.currentModel}\" not available on OpenRouter.`; + + // Refresh available models from API and try best match once + try { + await this.refreshRemoteModels(); + const best = this.findBestMatchingModelId(this.currentModel); + if (best && best !== this.currentModel) { + // Try once with corrected model + this.currentModel = best; + await this.db.setPreference("llmModelId", best); + this._notifyModelChanged(); + const retryResponse = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "HTTP-Referer": window.location.origin, + "X-Title": "Kimi - Virtual Companion" + }, + body: JSON.stringify({ ...payload, model: best }) + }); + if (retryResponse.ok) { + const retryData = await retryResponse.json(); + const kimiResponse = retryData.choices?.[0]?.message?.content; + if (!kimiResponse) throw new Error("Invalid API response - no content generated"); + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() } + ); + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + return kimiResponse; + } + } + } catch (e) { + // Swallow refresh errors; will fall through to standard error handling + } + } else if (response.status === 401) { + errorMessage = "Invalid API key. Check your OpenRouter key in the settings."; + } else if (response.status === 429) { + errorMessage = "Rate limit reached. Please wait a moment before trying again."; + } else if (response.status === 402) { + errorMessage = "Insufficient credit on your OpenRouter account."; + } + } + } catch (parseError) { + console.warn("Unable to parse API error:", parseError); + } + + console.error(`OpenRouter API error (${response.status}):`, errorMessage); + + // Add suggestions to the error if available + const error = new Error(errorMessage); + if (suggestions.length > 0) { + error.suggestions = suggestions; + } + + throw error; + } + + const data = await response.json(); + + if (!data.choices || !data.choices[0] || !data.choices[0].message) { + throw new Error("Invalid API response - no content generated"); + } + + const kimiResponse = data.choices[0].message.content; + + // Add to context + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: kimiResponse, timestamp: new Date().toISOString() } + ); + + // Limit context size + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + + // Token usage estimation (deferred save) + try { + const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4)); + const tokensIn = est(userMessage + " " + systemPromptContent); + const tokensOut = est(kimiResponse); + window._lastKimiTokenUsage = { tokensIn, tokensOut }; + if (!window.kimiMemory && this.db) { + const character = await this.db.getSelectedCharacter(); + const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0; + const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0; + await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn); + await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut); + } + } catch (e) { + console.warn("Token usage estimation failed (OpenRouter):", e); + } + return kimiResponse; + } catch (networkError) { + if (networkError.name === "TypeError" && networkError.message.includes("fetch")) { + throw new Error("Network connection error. Check your internet connection."); + } + throw networkError; + } + } + + async chatWithLocal(userMessage, options = {}) { + try { + const selectedLanguage = await this.db.getPreference("selectedLanguage", "en"); + let languageInstruction = ""; // placeholder (language guidance is included in assembled prompt) + let systemPromptContent = await this.assemblePrompt(userMessage); + if (window.KIMI_DEBUG_API_AUDIT) { + console.log("===== FULL SYSTEM PROMPT (Local) =====\n" + systemPromptContent + "\n===== END SYSTEM PROMPT ====="); + } + const response = await fetch("http://localhost:11434/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: "gemma-3n-E4B-it-Q4_K_M.gguf", + messages: [ + { role: "system", content: systemPromptContent }, + { role: "user", content: userMessage } + ], + stream: false + }) + }); + if (!response.ok) { + throw new Error("Ollama not available"); + } + const data = await response.json(); + const content = data?.message?.content || data?.choices?.[0]?.message?.content || ""; + if (!content) throw new Error("Local model returned empty response"); + + // Add to context like other providers + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: content, timestamp: new Date().toISOString() } + ); + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + + // Estimate token usage for local model (heuristic) + try { + const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4)); + const tokensIn = est(userMessage + " " + systemPromptContent); + const tokensOut = est(content); + window._lastKimiTokenUsage = { tokensIn, tokensOut }; + const character = await this.db.getSelectedCharacter(); + const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0; + const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0; + await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn); + await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut); + } catch (e) { + console.warn("Token usage estimation failed (local):", e); + } + return content; + } catch (error) { + console.warn("Local LLM not available:", error); + return this.getFallbackResponse(userMessage); + } + } + + // ===== STREAMING METHODS ===== + + async chatWithOpenRouterStreaming(userMessage, onToken, options = {}) { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + const apiKey = await (window.KimiProviderUtils ? window.KimiProviderUtils.getApiKey(this.db, provider) : this.db.getPreference("providerApiKey")); + if (!apiKey) { + throw new Error("OpenRouter API key not configured"); + } + + const systemPromptContent = await this.assemblePrompt(userMessage); + const messages = [ + { role: "system", content: systemPromptContent }, + ...this.conversationContext.slice(-this.maxContextLength), + { role: "user", content: userMessage } + ]; + + // Get unified defaults and options + const unifiedDefaults = window.getUnifiedDefaults + ? window.getUnifiedDefaults() + : { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 }; + + const enableStreaming = await this.db.getPreference("enableStreaming", true); + + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", unifiedDefaults.temperature), + maxTokens: await this.db.getPreference("llmMaxTokens", unifiedDefaults.maxTokens), + top_p: await this.db.getPreference("llmTopP", unifiedDefaults.top_p), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", unifiedDefaults.frequency_penalty), + presence_penalty: await this.db.getPreference("llmPresencePenalty", unifiedDefaults.presence_penalty) + }; + + const payload = { + model: this.currentModel, + messages: messages, + stream: enableStreaming, // Use user preference for streaming + temperature: typeof options.temperature === "number" ? options.temperature : llmSettings.temperature, + max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens, + top_p: typeof options.topP === "number" ? options.topP : llmSettings.top_p, + frequency_penalty: typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : llmSettings.frequency_penalty, + presence_penalty: typeof options.presencePenalty === "number" ? options.presencePenalty : llmSettings.presence_penalty + }; + + try { + const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + "HTTP-Referer": window.location.origin, + "X-Title": "Kimi - Virtual Companion" + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let fullResponse = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; // Keep incomplete line in buffer + + for (const line of lines) { + if (line.trim() === "" || line.startsWith(":")) continue; // Skip empty lines and comments + + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") { + break; + } + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content; + if (content) { + fullResponse += content; + onToken(content); + } + } catch (parseError) { + console.warn("Failed to parse streaming chunk:", parseError); + } + } + } + } + } finally { + reader.releaseLock(); + } + + // Add to context after streaming completes + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: fullResponse, timestamp: new Date().toISOString() } + ); + + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + + // Token usage estimation + try { + const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4)); + const tokensIn = est(userMessage + " " + systemPromptContent); + const tokensOut = est(fullResponse); + window._lastKimiTokenUsage = { tokensIn, tokensOut }; + if (!window.kimiMemory && this.db) { + const character = await this.db.getSelectedCharacter(); + const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0; + const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0; + await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn); + await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut); + } + } catch (e) { + console.warn("Token usage estimation failed (OpenRouter streaming):", e); + } + + return fullResponse; + } catch (error) { + console.error("OpenRouter streaming error:", error); + throw error; + } + } + + async chatWithOpenAICompatibleStreaming(userMessage, onToken, options = {}) { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + let baseUrl; + if (provider === "openai-compatible" || provider === "ollama") { + baseUrl = await this.db.getPreference(`llmBaseUrl_${provider}`, provider === "ollama" ? "http://localhost:11434/api/chat" : ""); + } else { + const sharedPlaceholders = window.KimiProviderPlaceholders || {}; + baseUrl = sharedPlaceholders[provider] || sharedPlaceholders.openrouter || "https://openrouter.ai/api/v1/chat/completions"; + } + const apiKey = window.KimiProviderUtils + ? await window.KimiProviderUtils.getApiKey(this.db, provider) + : await this.db.getPreference("providerApiKey", ""); + if (!apiKey) { + throw new Error("API key not configured for selected provider"); + } + + const systemPromptContent = await this.assemblePrompt(userMessage); + const messages = [ + { role: "system", content: systemPromptContent }, + ...this.conversationContext.slice(-this.maxContextLength), + { role: "user", content: userMessage } + ]; + + const unifiedDefaults = window.getUnifiedDefaults + ? window.getUnifiedDefaults() + : { temperature: 0.9, maxTokens: 400, top_p: 0.9, frequency_penalty: 0.9, presence_penalty: 0.8 }; + + const enableStreaming = await this.db.getPreference("enableStreaming", true); + + const llmSettings = { + temperature: await this.db.getPreference("llmTemperature", unifiedDefaults.temperature), + maxTokens: await this.db.getPreference("llmMaxTokens", unifiedDefaults.maxTokens), + top_p: await this.db.getPreference("llmTopP", unifiedDefaults.top_p), + frequency_penalty: await this.db.getPreference("llmFrequencyPenalty", unifiedDefaults.frequency_penalty), + presence_penalty: await this.db.getPreference("llmPresencePenalty", unifiedDefaults.presence_penalty) + }; + + const payload = { + model: this.currentModel, + messages: messages, + stream: enableStreaming, + temperature: typeof options.temperature === "number" ? options.temperature : llmSettings.temperature, + max_tokens: typeof options.maxTokens === "number" ? options.maxTokens : llmSettings.maxTokens, + top_p: typeof options.topP === "number" ? options.topP : llmSettings.top_p, + frequency_penalty: typeof options.frequencyPenalty === "number" ? options.frequencyPenalty : llmSettings.frequency_penalty, + presence_penalty: typeof options.presencePenalty === "number" ? options.presencePenalty : llmSettings.presence_penalty + }; + + try { + const response = await fetch(baseUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let fullResponse = ""; + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (line.trim() === "" || line.startsWith(":")) continue; + + if (line.startsWith("data: ")) { + const data = line.slice(6); + if (data === "[DONE]") { + break; + } + + try { + const parsed = JSON.parse(data); + const content = parsed.choices?.[0]?.delta?.content; + if (content) { + fullResponse += content; + onToken(content); + } + } catch (parseError) { + console.warn("Failed to parse streaming chunk:", parseError); + } + } + } + } + } finally { + reader.releaseLock(); + } + + // Add to context + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: fullResponse, timestamp: new Date().toISOString() } + ); + + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + + // Token usage estimation + try { + const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4)); + const tokensIn = est(userMessage + " " + systemPromptContent); + const tokensOut = est(fullResponse); + window._lastKimiTokenUsage = { tokensIn, tokensOut }; + if (!window.kimiMemory && this.db) { + const character = await this.db.getSelectedCharacter(); + const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0; + const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0; + await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn); + await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut); + } + } catch (e) { + console.warn("Token usage estimation failed (OpenAI streaming):", e); + } + + return fullResponse; + } catch (error) { + console.error("OpenAI compatible streaming error:", error); + throw error; + } + } + + async chatWithLocalStreaming(userMessage, onToken, options = {}) { + const systemPromptContent = await this.assemblePrompt(userMessage); + const enableStreaming = await this.db.getPreference("enableStreaming", true); + + const payload = { + model: this.currentModel || "llama2", + messages: [ + { role: "system", content: systemPromptContent }, + ...this.conversationContext.slice(-this.maxContextLength), + { role: "user", content: userMessage } + ], + stream: enableStreaming + }; + + try { + const response = await fetch("http://localhost:11434/api/chat", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error("Ollama not available"); + } + + let fullResponse = ""; + + if (enableStreaming) { + // Streaming mode + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value, { stream: true }); + const lines = chunk.split("\n").filter(line => line.trim()); + + for (const line of lines) { + try { + const parsed = JSON.parse(line); + const content = parsed.message?.content; + if (content) { + fullResponse += content; + onToken(content); + } + if (parsed.done) { + break; + } + } catch (parseError) { + console.warn("Failed to parse Ollama streaming chunk:", parseError); + } + } + } + } finally { + reader.releaseLock(); + } + } else { + // Non-streaming mode + const data = await response.json(); + fullResponse = data.message?.content || ""; + if (fullResponse && onToken) { + onToken(fullResponse); + } + } + + // Add to context + this.conversationContext.push( + { role: "user", content: userMessage, timestamp: new Date().toISOString() }, + { role: "assistant", content: fullResponse, timestamp: new Date().toISOString() } + ); + + if (this.conversationContext.length > this.maxContextLength * 2) { + this.conversationContext = this.conversationContext.slice(-this.maxContextLength * 2); + } + + // Token usage estimation + try { + const est = window.KimiTokenUtils?.estimate || (t => Math.ceil((t || "").length / 4)); + const tokensIn = est(userMessage + " " + systemPromptContent); + const tokensOut = est(fullResponse); + window._lastKimiTokenUsage = { tokensIn, tokensOut }; + const character = await this.db.getSelectedCharacter(); + const prevIn = Number(await this.db.getPreference(`totalTokensIn_${character}`, 0)) || 0; + const prevOut = Number(await this.db.getPreference(`totalTokensOut_${character}`, 0)) || 0; + await this.db.setPreference(`totalTokensIn_${character}`, prevIn + tokensIn); + await this.db.setPreference(`totalTokensOut_${character}`, prevOut + tokensOut); + } catch (e) { + console.warn("Token usage estimation failed (local streaming):", e); + } + + return fullResponse; + } catch (error) { + console.warn("Local LLM streaming not available:", error); + throw error; + } + } + + getFallbackResponse(userMessage, errorType = "api") { + // Use centralized fallback manager instead of duplicated logic + if (window.KimiFallbackManager) { + // Map error types to the correct format + const errorTypeMap = { + api: "api_error", + model: "model_error", + network: "network_error" + }; + const mappedType = errorTypeMap[errorType] || "technical_error"; + return window.KimiFallbackManager.getFallbackMessage(mappedType); + } + + // Fallback to legacy system if KimiFallbackManager not available + const i18n = window.kimiI18nManager; + if (!i18n) { + return "Sorry, I'm having technical difficulties! 💕"; + } + return i18n.t("fallback_technical_error"); + } + + getFallbackKeywords(trait, type) { + const keywords = { + humor: { + positive: ["funny", "hilarious", "joke", "laugh", "amusing", "humorous", "smile", "witty", "playful"], + negative: ["boring", "sad", "serious", "cold", "dry", "depressing", "gloomy"] + }, + intelligence: { + positive: ["intelligent", "smart", "brilliant", "logical", "clever", "wise", "genius", "thoughtful", "insightful"], + negative: ["stupid", "dumb", "foolish", "slow", "naive", "ignorant", "simple"] + }, + romance: { + positive: ["cuddle", "love", "romantic", "kiss", "tenderness", "passion", "charming", "adorable", "sweet"], + negative: ["cold", "distant", "indifferent", "rejection", "loneliness", "breakup", "sad"] + }, + affection: { + positive: ["affection", "tenderness", "close", "warmth", "kind", "caring", "cuddle", "love", "adore"], + negative: ["mean", "cold", "indifferent", "distant", "rejection", "hate", "hostile"] + }, + playfulness: { + positive: ["play", "game", "tease", "mischievous", "fun", "amusing", "playful", "joke", "frolic"], + negative: ["serious", "boring", "strict", "rigid", "monotonous", "tedious"] + }, + empathy: { + positive: ["listen", "understand", "empathy", "support", "help", "comfort", "compassion", "caring", "kindness"], + negative: ["indifferent", "cold", "selfish", "ignore", "despise", "hostile", "uncaring"] + } + }; + return keywords[trait]?.[type] || []; + } + + // Mémoire temporaire pour l'accumulation négative par trait + _negativeStreaks = {}; + + async updatePersonalityFromResponse(userMessage, kimiResponse) { + // Use unified emotion system for personality updates + if (window.kimiEmotionSystem) { + return await window.kimiEmotionSystem.updatePersonalityFromConversation(userMessage, kimiResponse, await this.db.getSelectedCharacter()); + } + + // Legacy fallback (should not be reached) + console.warn("Unified emotion system not available, skipping personality update"); + } + + async getModelStats() { + const models = await this.db.getAllLLMModels(); + const currentModelInfo = this.availableModels[this.currentModel]; + + return { + current: { + id: this.currentModel, + info: currentModelInfo + }, + available: this.availableModels, + configured: models, + contextLength: this.conversationContext.length + }; + } + + async testModel(modelId, testMessage = "Test API ok?") { + // Ancienne méthode de test (non minimaliste) + return await this.testApiKeyMinimal(modelId); + } + + /** + * Test API minimaliste et centralisé pour tous les providers compatibles. + * Envoie uniquement un prompt système court et un message utilisateur dans la langue choisie. + * Aucun contexte, aucune mémoire, aucun paramètre superflu. + * @param {string} modelId - ID du modèle à tester + * @returns {Promise<{success: boolean, response?: string, error?: string}>} + */ + async testApiKeyMinimal(modelId) { + const originalModel = this.currentModel; + try { + await this.setCurrentModel(modelId); + const provider = await this.db.getPreference("llmProvider", "openrouter"); + const lang = await this.db.getPreference("selectedLanguage", "en"); + let testWord; + switch (lang) { + case "fr": + testWord = "Bonjour"; + break; + case "es": + testWord = "Hola"; + break; + case "de": + testWord = "Hallo"; + break; + case "it": + testWord = "Ciao"; + break; + case "ja": + testWord = "こんにちは"; + break; + case "zh": + testWord = "你好"; + break; + default: + testWord = "Hello"; + } + const systemPrompt = "You are a helpful assistant."; + let apiKey = await (window.KimiProviderUtils ? window.KimiProviderUtils.getApiKey(this.db, provider) : this.db.getPreference("providerApiKey")); + + if (!apiKey) { + return { success: false, error: "No API key found for provider: " + provider }; + } + + let baseUrl = ""; + let payload = { + model: modelId, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: testWord } + ], + max_tokens: 2 + }; + let headers = { "Content-Type": "application/json" }; + if (provider === "openrouter") { + baseUrl = "https://openrouter.ai/api/v1/chat/completions"; + headers["Authorization"] = `Bearer ${apiKey}`; + headers["HTTP-Referer"] = window.location.origin; + headers["X-Title"] = "Kimi - Virtual Companion"; + } else if (["openai", "groq", "together", "deepseek", "openai-compatible"].includes(provider)) { + // When selecting baseUrl during initialization/fallback, respect provider-specific stored URLs + const currentProvider = await this.db.getPreference("llmProvider", "openrouter"); + if (currentProvider === "openai-compatible" || currentProvider === "ollama") { + baseUrl = await this.db.getPreference( + `llmBaseUrl_${currentProvider}`, + currentProvider === "ollama" ? "http://localhost:11434/api/chat" : "" + ); + } else { + const sharedPlaceholders = window.KimiProviderPlaceholders || {}; + baseUrl = sharedPlaceholders[provider] || sharedPlaceholders.openrouter || "https://openrouter.ai/api/v1/chat/completions"; + } + headers["Authorization"] = `Bearer ${apiKey}`; + } else if (provider === "ollama") { + baseUrl = "http://localhost:11434/api/chat"; + payload = { + model: modelId, + messages: [ + { role: "system", content: systemPrompt }, + { role: "user", content: testWord } + ], + stream: false + }; + } else { + throw new Error("Unknown provider: " + provider); + } + + const response = await fetch(baseUrl, { + method: "POST", + headers, + body: JSON.stringify(payload) + }); + if (!response.ok) { + const error = await response.text(); + return { success: false, error }; + } + const data = await response.json(); + let content = ""; + if (provider === "ollama") { + content = data?.message?.content || data?.choices?.[0]?.message?.content || ""; + } else { + content = data?.choices?.[0]?.message?.content || ""; + } + return { success: true, response: content }; + } catch (error) { + return { success: false, error: error.message }; + } finally { + await this.setCurrentModel(originalModel); + } + } + + // Complete model diagnosis + async diagnoseModel(modelId) { + const model = this.availableModels[modelId]; + if (!model) { + return { + available: false, + error: "Model not found in local list" + }; + } + + // Check availability on OpenRouter + try { + // Model availability is checked against the local cache; remote checks occur in refreshRemoteModels() + return { + available: true, + model: model, + pricing: model.pricing + }; + } catch (error) { + return { + available: false, + error: `Unable to check: ${error.message}` + }; + } + } + + // Fetch models from OpenRouter API and merge into availableModels + async refreshRemoteModels() { + if (this._isRefreshingModels) return; + this._isRefreshingModels = true; + try { + const provider = await this.db.getPreference("llmProvider", "openrouter"); + const apiKey = await (window.KimiProviderUtils + ? window.KimiProviderUtils.getApiKey(this.db, provider) + : this.db.getPreference("providerApiKey", "")); + const res = await fetch("https://openrouter.ai/api/v1/models", { + method: "GET", + headers: { + "Content-Type": "application/json", + ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), + "HTTP-Referer": window.location.origin, + "X-Title": "Kimi - Virtual Companion" + } + }); + if (!res.ok) { + throw new Error(`Unable to fetch models: HTTP ${res.status}`); + } + const data = await res.json(); + if (!data?.data || !Array.isArray(data.data)) { + throw new Error("Invalid models response format"); + } + // Build a fresh map while preserving local/ollama entry + const newMap = {}; + data.data.forEach(m => { + if (!m?.id) return; + const id = m.id; + const provider = m?.id?.split("/")?.[0] || "OpenRouter"; + let pricing; + const p = m?.pricing; + if (p) { + const unitRaw = ((p.unit || p.per || p.units || "") + "").toLowerCase(); + let unitTokens = 1; + if (unitRaw) { + if (unitRaw.includes("1m")) unitTokens = 1000000; + else if (unitRaw.includes("1k") || unitRaw.includes("thousand")) unitTokens = 1000; + else { + const num = parseFloat(unitRaw.replace(/[^0-9.]/g, "")); + if (Number.isFinite(num) && num > 0) { + if (unitRaw.includes("m")) unitTokens = num * 1000000; + else if (unitRaw.includes("k")) unitTokens = num * 1000; + else unitTokens = num; + } else if (unitRaw.includes("token")) { + unitTokens = 1; + } + } + } + const toPerMillion = v => { + const n = typeof v === "number" ? v : parseFloat(v); + if (!Number.isFinite(n)) return undefined; + return n * (1000000 / unitTokens); + }; + if (typeof p.input !== "undefined" || typeof p.output !== "undefined") { + pricing = { + input: toPerMillion(p.input), + output: toPerMillion(p.output) + }; + } else if (typeof p.prompt !== "undefined" || typeof p.completion !== "undefined") { + pricing = { + input: toPerMillion(p.prompt), + output: toPerMillion(p.completion) + }; + } else { + pricing = { input: undefined, output: undefined }; + } + } else { + pricing = { input: undefined, output: undefined }; + } + newMap[id] = { + name: m.name || id, + provider, + type: "openrouter", + contextWindow: m.context_length || m?.context_window || 128000, + pricing, + strengths: (m?.tags || []).slice(0, 4) + }; + }); + // Keep local model entry + if (this.availableModels["local/ollama"]) { + newMap["local/ollama"] = this.availableModels["local/ollama"]; + } + this.recommendedModelIds.forEach(id => { + const curated = this.defaultModels[id]; + if (curated) { + newMap[id] = { ...(newMap[id] || {}), ...curated }; + } + }); + this.availableModels = newMap; + this._remoteModelsLoaded = true; + } finally { + this._isRefreshingModels = false; + } + } + + // Try to find best matching model id from remote list when an ID is stale + findBestMatchingModelId(preferredId) { + if (this.availableModels[preferredId]) return preferredId; + const id = (preferredId || "").toLowerCase(); + const tokens = id.split(/[\/:\-_.]+/).filter(Boolean); + let best = null; + let bestScore = -1; + Object.keys(this.availableModels).forEach(candidateId => { + const c = candidateId.toLowerCase(); + let score = 0; + tokens.forEach(t => { + if (!t) return; + if (c.includes(t)) score += 1; + }); + // Give extra weight to common markers + if (c.includes("instruct")) score += 0.5; + if (c.includes("mistral") && id.includes("mistral")) score += 0.5; + if (c.includes("small") && id.includes("small")) score += 0.5; + if (score > bestScore) { + bestScore = score; + best = candidateId; + } + }); + // Avoid returning unrelated local model unless nothing else + if (best === "local/ollama" && Object.keys(this.availableModels).length > 1) { + return null; + } + return best; + } + + _notifyModelChanged() { + try { + const detail = { id: this.currentModel }; + if (typeof window !== "undefined" && typeof window.dispatchEvent === "function") { + window.emitAppEvent && window.emitAppEvent("llmModelChanged", detail); + } + } catch (e) {} + } +} + +// Export for usage +window.KimiLLMManager = KimiLLMManager; +export default KimiLLMManager; diff --git a/kimi-js/kimi-main.js b/kimi-js/kimi-main.js new file mode 100644 index 0000000000000000000000000000000000000000..bf19204f1200bbbe0be21526c5a4152c5d2918e9 --- /dev/null +++ b/kimi-js/kimi-main.js @@ -0,0 +1,11 @@ +// ESM bootstrap for Kimi App +// Import minimal utilities as modules; rely on existing globals for legacy parts +import { KimiProviderUtils } from "./kimi-utils.js"; +import KimiLLMManager from "./kimi-llm-manager.js"; +import KimiEmotionSystem from "./kimi-emotion-system.js"; + +// Expose module imports to legacy code paths that still rely on window +// Ensure KimiProviderUtils is available (imported from kimi-utils.js) +window.KimiProviderUtils = window.KimiProviderUtils || KimiProviderUtils; +window.KimiLLMManager = window.KimiLLMManager || KimiLLMManager; +window.KimiEmotionSystem = window.KimiEmotionSystem || KimiEmotionSystem; diff --git a/kimi-js/kimi-memory-system.js b/kimi-js/kimi-memory-system.js new file mode 100644 index 0000000000000000000000000000000000000000..bcac0dff91904f79effc6c69798d405f85b5c16e --- /dev/null +++ b/kimi-js/kimi-memory-system.js @@ -0,0 +1,2257 @@ +// ===== KIMI INTELLIGENT MEMORY SYSTEM ===== +class KimiMemorySystem { + constructor(database) { + this.db = database; + this.memoryEnabled = true; + this.maxMemoryEntries = 100; + + // Performance optimization: keyword cache with LRU eviction + this.keywordCache = new Map(); // keyword_language -> boolean (is common) + this.keywordCacheSize = 1000; // Limit memory usage + this.keywordCacheHits = 0; + this.keywordCacheMisses = 0; + + // Performance monitoring + this.queryStats = { + extractionTime: [], + addMemoryTime: [], + retrievalTime: [] + }; + + // Centralized configuration for all thresholds and magic numbers + this.config = { + // Content validation thresholds + minContentLength: 2, + longContentThreshold: 24, + titleWordCount: { + preferred: 3, + min: 1, + max: 5 + }, + + // Similarity and confidence thresholds + similarity: { + personal: 0.6, // Names can vary more (Jean vs Jean-Pierre) + preferences: 0.7, // Preferences can be expressed differently + default: 0.8, // General similarity threshold + veryHigh: 0.9, // For boost_confidence strategy + update: 0.3 // Lower threshold for memory updates + }, + + // Confidence scoring + confidence: { + base: 0.6, + explicitRequest: 1.0, + naturalExpression: 0.7, + bonusForLongContent: 0.1, + bonusForExplicitStatement: 0.3, + penaltyForUncertainty: 0.2, + min: 0.1, + max: 1.0 + }, + + // Memory management + cleanup: { + maxEntries: 100, + ttlDays: 365, + batchSize: 100, + touchMinutes: 60 + }, + + // Performance settings + cache: { + keywordCacheSize: 1000, + statHistorySize: 100 + }, + + // Scoring weights for importance calculation + importance: { + categoryWeights: { + important: 1.0, + personal: 0.9, + relationships: 0.85, + goals: 0.75, + experiences: 0.65, + preferences: 0.6, + activities: 0.5 + }, + bonuses: { + relationshipMilestone: 0.15, + boundaries: 0.15, + strongEmotion: 0.05, + futureReference: 0.05, + longContent: 0.05, + highConfidence: 0.05 + } + }, + + // Relevance calculation weights + relevance: { + contentSimilarity: 0.35, + keywordOverlap: 0.25, + categoryRelevance: 0.1, + recencyBonus: 0.1, + confidenceBonus: 0.05, + importanceBonus: 0.05, + recentDaysThreshold: 30 + } + }; + + this.memoryCategories = { + personal: "Personal Information", + preferences: "Likes & Dislikes", + relationships: "Relationships & People", + activities: "Activities & Hobbies", + goals: "Goals & Aspirations", + experiences: "Shared Experiences", + important: "Important Events" + }; + + // Patterns for automatic memory extraction (multilingual) + this.extractionPatterns = { + personal: [ + // English patterns + /(?:my name is|i'm called|call me|i am) (\w+)/i, + /(?:i am|i'm) (\d+) years? old/i, + /(?:i live in|i'm from|from) ([^,.!?]+)/i, + /(?:i work as|my job is|i'm a) ([^,.!?]+)/i, + // French patterns + /(?:je m'appelle|mon nom est|je suis|je me prénomme|je me nomme) ([^,.!?]+)/i, + /(?:j'ai) (\d+) ans?/i, + /(?:j'habite à|je vis à|je viens de) ([^,.!?]+)/i, + /(?:je travaille comme|mon travail est|je suis) ([^,.!?]+)/i, + // Spanish patterns + /(?:me llamo|mi nombre es|soy) ([^,.!?]+)/i, + /(?:tengo) (\d+) años?/i, + /(?:vivo en|soy de) ([^,.!?]+)/i, + /(?:trabajo como|mi trabajo es|soy) ([^,.!?]+)/i, + // Italian patterns + /(?:mi chiamo|il mio nome è|sono) ([^,.!?]+)/i, + /(?:ho) (\d+) anni?/i, + /(?:abito a|vivo a|sono di) ([^,.!?]+)/i, + /(?:lavoro come|il mio lavoro è|sono) ([^,.!?]+)/i, + // German patterns + /(?:ich heiße|mein name ist|ich bin) ([^,.!?]+)/i, + /(?:ich bin) (\d+) jahre? alt/i, + /(?:ich wohne in|ich lebe in|ich komme aus) ([^,.!?]+)/i, + /(?:ich arbeite als|mein beruf ist|ich bin) ([^,.!?]+)/i, + // Japanese patterns + /私の名前は([^。!?!?、,.]+)[ですだ]?/i, + /私は([^。!?!?、,.]+)です/i, + /([^、。!?!?,.]+)と申します/i, + /([^、。!?!?,.]+)といいます/i, + // Chinese patterns + /我叫([^,。!?!?,.]+)/i, + /我的名字是([^,。!?!?,.]+)/i, + /叫我([^,。!?!?,.]+)/i + ], + preferences: [ + // English patterns + /(?:i love|i like|i enjoy|i prefer) ([^,.!?]+)/i, + /(?:i hate|i dislike|i don't like) ([^,.!?]+)/i, + /(?:my favorite|i really like) ([^,.!?]+)/i, + // French patterns + /(?:j'aime|j'adore|je préfère) ([^,.!?]+)/i, + /(?:je déteste|je n'aime pas) ([^,.!?]+)/i, + /(?:mon préféré|ma préférée) (?:est|sont) ([^,.!?]+)/i, + // Explicit memory requests + /(?:ajoute? (?:au|à la) (?:système? )?(?:de )?mémoire|retiens?|mémorise?) (?:que )?(.+)/i, + /(?:add to memory|remember|memorize) (?:that )?(.+)/i + ], + relationships: [ + // English patterns + /(?:my (?:wife|husband|girlfriend|boyfriend|partner)) (?:is|named?) ([^,.!?]+)/i, + /(?:my (?:mother|father|sister|brother|friend)) ([^,.!?]+)/i, + // French patterns + /(?:ma (?:femme|copine|partenaire)|mon (?:mari|copain|partenaire)) (?:s'appelle|est) ([^,.!?]+)/i, + /(?:ma (?:mère|sœur)|mon (?:père|frère|ami)) (?:s'appelle|est) ([^,.!?]+)/i, + // Spanish patterns + /(?:mi (?:esposa|esposo|novia|novio|pareja)) (?:es|se llama) ([^,.!?]+)/i, + /(?:mi (?:madre|padre|hermana|hermano|amigo|amiga)) (?:es|se llama) ([^,.!?]+)/i, + // Italian patterns + /(?:la mia (?:moglie|fidanzata|compagna)|il mio (?:marito|fidanzato|compagno)) (?:è|si chiama) ([^,.!?]+)/i, + /(?:mia (?:madre|sorella)|mio (?:padre|fratello|amico)) (?:è|si chiama) ([^,.!?]+)/i, + // German patterns + /(?:meine (?:frau|freundin|partnerin)|mein (?:mann|freund|partner)) (?:ist|heißt) ([^,.!?]+)/i, + /(?:meine (?:mutter|schwester)|mein (?:vater|bruder|freund)) (?:ist|heißt) ([^,.!?]+)/i, + // Japanese patterns + /(?:私の(?:妻|夫|彼女|彼氏|パートナー))は([^。!?!?、,.]+)(?:です|といいます)/i, + /(?:私の(?:母|父|姉|妹|兄|弟|友達))は([^。!?!?、,.]+)(?:です|といいます)/i, + // Chinese patterns + /(?:我的(?:妻子|丈夫|女朋友|男朋友|伴侣))叫([^,。!?!?,.]+)/i, + /(?:我的(?:妈妈|父亲|姐姐|妹妹|哥哥|弟弟|朋友))叫([^,。!?!?,.]+)/i + ], + activities: [ + // English patterns + /(?:i play|i do|i practice) ([^,.!?]+)/i, + /(?:my hobby is|i hobby) ([^,.!?]+)/i, + // French patterns + /(?:je joue|je fais|je pratique) ([^,.!?]+)/i, + /(?:mon passe-temps|mon hobby) (?:est|c'est) ([^,.!?]+)/i, + // Spanish patterns + /(?:juego|hago|practico) ([^,.!?]+)/i, + /(?:mi pasatiempo|mi hobby) (?:es) ([^,.!?]+)/i, + // Italian patterns + /(?:gioco|faccio|pratico) ([^,.!?]+)/i, + /(?:il mio passatempo|il mio hobby) (?:è) ([^,.!?]+)/i, + // German patterns + /(?:ich spiele|ich mache|ich übe) ([^,.!?]+)/i, + /(?:mein hobby ist) ([^,.!?]+)/i, + // Japanese patterns + /(?:私は)?(?:[^、。!?!?,.]+)が趣味です/i, + /趣味は([^。!?!?、,.]+)です/i, + // Chinese patterns + /(?:我玩|我做|我练习)([^,。!?!?,.]+)/i, + /(?:我的爱好是)([^,。!?!?,.]+)/i + ], + goals: [ + // English patterns + /(?:i want to|i plan to|my goal is) ([^,.!?]+)/i, + /(?:i'm learning|i study) ([^,.!?]+)/i, + // French patterns + /(?:je veux|je vais|mon objectif est) ([^,.!?]+)/i, + /(?:j'apprends|j'étudie) ([^,.!?]+)/i, + // Spanish patterns + /(?:quiero|voy a|mi objetivo es) ([^,.!?]+)/i, + /(?:estoy aprendiendo|estudio) ([^,.!?]+)/i, + // Italian patterns + /(?:voglio|andrò a|il mio obiettivo è) ([^,.!?]+)/i, + /(?:sto imparando|studio) ([^,.!?]+)/i, + // German patterns + /(?:ich möchte|ich will|mein ziel ist) ([^,.!?]+)/i, + /(?:ich lerne|ich studiere) ([^,.!?]+)/i, + // Japanese patterns + /(?:私は)?(?:[^、。!?!?,.]+)したい/i, + /(?:学んでいる|勉強している) ([^。!?!?、,.]+)/i, + // Chinese patterns + /(?:我想|我要|我的目标是)([^,。!?!?,.]+)/i, + /(?:我在学习|我学习)([^,。!?!?,.]+)/i + ], + experiences: [ + // English patterns + /we went to ([^,.!?]+)/i, + /we met (?:at|on|in) ([^,.!?]+)/i, + /our (?:first date|first kiss|trip|vacation) (?:was|was at|was on|was in|was to) ([^,.!?]+)/i, + /our anniversary (?:is|falls on|will be) ([^,.!?]+)/i, + /we moved in (?:together )?(?:on|in)?\s*([^,.!?]+)/i, + // French patterns + /on s'est rencontr[ée]s? (?:à|au|en|le) ([^,.!?]+)/i, + /on est all[ée]s? à ([^,.!?]+)/i, + /notre (?:premier rendez-vous|première sortie) (?:était|c'était) ([^,.!?]+)/i, + /notre anniversaire (?:est|c'est) ([^,.!?]+)/i, + /on a emménagé (?:ensemble\s*)?(?:le|en|à)\s*([^,.!?]+)/i, + // Spanish patterns + /nos conocimos (?:en|el|la) ([^,.!?]+)/i, + /fuimos a ([^,.!?]+)/i, + /nuestra (?:primera cita|primera salida) (?:fue|era) ([^,.!?]+)/i, + /nuestro aniversario (?:es|cae en|será) ([^,.!?]+)/i, + /nos mudamos (?:juntos\s*)?(?:el|en|a)\s*([^,.!?]+)/i, + // Italian patterns + /ci siamo conosciuti (?:a|al|in|il) ([^,.!?]+)/i, + /siamo andati a ([^,.!?]+)/i, + /il nostro (?:primo appuntamento|primo bacio|viaggio) (?:era|è stato) ([^,.!?]+)/i, + /il nostro anniversario (?:è|cade il|sarà) ([^,.!?]+)/i, + /ci siamo trasferiti (?:insieme\s*)?(?:il|in|a)\s*([^,.!?]+)/i, + // German patterns + /wir haben uns (?:in|am) ([^,.!?]+) kennengelernt/i, + /wir sind (?:nach|zu) ([^,.!?]+) (?:gegangen|gefahren)/i, + /unser (?:erstes date|erster kuss|urlaub) (?:war|fand statt) ([^,.!?]+)/i, + /unser jahrestag (?:ist|fällt auf|wird sein) ([^,.!?]+)/i, + /wir sind (?:zusammen )?eingezogen (?:am|im|in)\s*([^,.!?]+)/i, + // Japanese patterns + /私たちは([^、。!?!?,.]+)で出会った/i, + /一緒に([^、。!?!?,.]+)へ行った/i, + /私たちの記念日(?:は)?([^、。!?!?,.]+)/i, + /一緒に引っ越した(?:のは)?([^、。!?!?,.]+)/i, + // Chinese patterns + /我们在([^,。!?!?,.]+)认识/i, + /我们去了([^,。!?!?,.]+)/i, + /我们的纪念日是([^,。!?!?,.]+)/i, + /我们一起搬家(?:是在)?([^,。!?!?,.]+)/i + ], + important: [ + // English patterns + /it's important (?:to remember|that) (.+)/i, + /please remember (.+)/i, + // French patterns + /c'est important (?:de se souvenir|que) (.+)/i, + /merci de te souvenir (.+)/i, + // Spanish patterns + /es importante (?:recordar|que) (.+)/i, + /por favor recuerda (.+)/i, + // Italian patterns + /è importante (?:ricordare|che) (.+)/i, + /per favore ricorda (.+)/i, + // German patterns + /es ist wichtig (?:zu erinnern|dass) (.+)/i, + /bitte erinnere dich an (.+)/i, + // Japanese patterns + /重要なのは(.+)です/i, + /覚えておいてほしいのは(.+)です/i, + // Chinese patterns + /重要的是(.+)/i, + /请记住(.+)/i + ] + }; + + // Performance optimization: pre-compile regex patterns + this.compiledPatterns = {}; + this.initializeCompiledPatterns(); + } + + // Pre-compile all regex patterns for better performance + initializeCompiledPatterns() { + try { + for (const [category, patterns] of Object.entries(this.extractionPatterns)) { + this.compiledPatterns[category] = patterns.map(pattern => { + if (pattern instanceof RegExp) { + return pattern; // Already compiled + } + return new RegExp(pattern.source, pattern.flags); + }); + } + + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + const totalPatterns = Object.values(this.compiledPatterns).reduce((sum, arr) => sum + arr.length, 0); + console.log(`🚀 Pre-compiled ${totalPatterns} regex patterns for memory extraction`); + } + } catch (error) { + console.error("Error pre-compiling regex patterns:", error); + // Fallback: use original patterns + this.compiledPatterns = this.extractionPatterns; + } + } + + // Utility method to get consistent creation timestamp + getCreationTimestamp(memory) { + // Prefer createdAt, fallback to timestamp for backward compatibility + return memory.createdAt || memory.timestamp || new Date(); + } + + // Utility method to calculate days since creation + getDaysSinceCreation(memory) { + const created = new Date(this.getCreationTimestamp(memory)).getTime(); + return (Date.now() - created) / (1000 * 60 * 60 * 24); + } + + async init() { + if (!this.db) { + console.warn("Database not available for memory system"); + return; + } + + try { + this.memoryEnabled = await this.db.getPreference("memorySystemEnabled", window.KIMI_CONFIG?.DEFAULTS?.MEMORY_SYSTEM_ENABLED ?? true); + this.selectedCharacter = await this.db.getSelectedCharacter(); + await this.createMemoryTables(); + + // Legacy migrations disabled - uncomment if needed for old databases + // await this.migrateIncompatibleIDs(); + // this.populateKeywordsForAllMemories().catch(e => console.warn("Keyword population failed", e)); + } catch (error) { + console.error("Memory system initialization error:", error); + } + } + + async createMemoryTables() { + // Ensure memory tables exist in database + if (!this.db.db.memories) { + console.warn("Memory table not found in database schema"); + return; + } + } + + // MEMORY EXTRACTION from conversation + async extractMemoryFromText(userText, kimiResponse = null) { + if (!this.memoryEnabled || !userText) return []; + + // Ensure selectedCharacter is initialized + if (!this.selectedCharacter) { + this.selectedCharacter = this.db ? await this.db.getSelectedCharacter() : "kimi"; + } + + const extractedMemories = []; + const text = userText.toLowerCase(); + + // Memory extraction processing (debug info reduced for performance) + + // Enhanced extraction with context awareness + const existingMemories = await this.getAllMemories(); + + // First, check for explicit memory requests + const explicitRequests = this.detectExplicitMemoryRequests(userText); + if (explicitRequests.length > 0) { + // Explicit memory requests detected + extractedMemories.push(...explicitRequests); + } + + // Extract using pre-compiled patterns for better performance + const patternsToUse = this.compiledPatterns || this.extractionPatterns; + for (const [category, patterns] of Object.entries(patternsToUse)) { + for (const pattern of patterns) { + const match = text.match(pattern); + if (match && match[1]) { + const content = match[1].trim(); + + // Skip very short or generic content + if (content.length < this.config.minContentLength || this.isGenericContent(content)) { + continue; + } + + // Check if this is a meaningful update to existing memory + const isUpdate = await this.isMemoryUpdate(category, content, existingMemories); + + const memory = { + category: category, + type: "auto_extracted", + content: content, + sourceText: userText, + confidence: this.calculateExtractionConfidence(match, userText), + createdAt: new Date(), // Use createdAt consistently + character: this.selectedCharacter || "kimi", // Fallback protection + isUpdate: isUpdate + }; + + // Pattern match detected + extractedMemories.push(memory); + } + } + } + + // Enhanced pattern detection for more natural expressions + const enhancedMemories = await this.detectNaturalExpressions(userText, existingMemories); + extractedMemories.push(...enhancedMemories); + + // Save extracted memories with intelligent deduplication + const savedMemories = []; + for (const memory of extractedMemories) { + try { + console.log("💾 Saving memory:", memory.content); + const saved = await this.addMemory(memory); + if (saved) { + savedMemories.push(saved); + } else { + console.warn("⚠️ Memory was not saved (possibly filtered or merged):", memory.content); + } + } catch (error) { + console.error("❌ Failed to save memory:", { + content: memory.content, + category: memory.category, + error: error.message + }); + // Continue processing other memories even if one fails + } + } + + if (savedMemories.length > 0) { + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.log(`✅ Successfully extracted and saved ${savedMemories.length} memories`); + } + } else if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.log("📝 No memories extracted from this text"); + } + + return savedMemories; + } + + // Detect explicit memory requests like "ajoute en mémoire que..." + detectExplicitMemoryRequests(text) { + const memories = []; + const lowerText = text.toLowerCase(); + + // French patterns for explicit memory requests + const frenchPatterns = [ + /(?:ajoute?s?(?:r)?|retiens?|mémorise?s?|enregistre?s?|sauvegarde?s?)\s+(?:au|à|en|dans)\s+(?:la\s+|le\s+)?(?:système?\s+(?:de\s+)?)?mémoire\s+(?:que\s+)?(.+)/i, + /(?:peux-tu|pourrais-tu|veux-tu)?\s*(?:ajouter|retenir|mémoriser|enregistrer|sauvegarder)\s+(?:que\s+)?(.+)\s+(?:en|dans)\s+(?:la\s+|le\s+)?mémoire/i, + /(?:je\s+veux\s+que\s+tu\s+)?(?:retienne?s|mémorise?s|ajoute?s)\s+(?:que\s+)?(.+)/i + ]; + + // English patterns for explicit memory requests + const englishPatterns = [ + /(?:add\s+to\s+memory|remember|memorize|save\s+(?:to\s+)?memory)\s+(?:that\s+)?(.+)/i, + /(?:can\s+you|could\s+you)?\s*(?:add|remember|memorize|save)\s+(?:that\s+)?(.+)\s+(?:to\s+|in\s+)?memory/i, + /(?:i\s+want\s+you\s+to\s+)?(?:remember|memorize|add)\s+(?:that\s+)?(.+)/i + ]; + + // Spanish explicit memory requests + const spanishPatterns = [ + /(?:añade|agrega|recuerda|memoriza|guarda)\s+(?:en|a)\s+(?:la\s+)?memoria\s+(?:que\s+)?(.+)/i, + /(?:puedes|podrías)?\s*(?:añadir|agregar|recordar|memorizar|guardar)\s+(?:que\s+)?(.+)\s+(?:en|a)\s+(?:la\s+)?memoria/i, + /(?:quiero\s+que\s+)?(?:recuerdes|memorices|añadas)\s+(?:que\s+)?(.+)/i + ]; + + // Italian explicit memory requests + const italianPatterns = [ + /(?:aggiungi|ricorda|memorizza|salva)\s+(?:nella|in)\s+memoria\s+(?:che\s+)?(.+)/i, + /(?:puoi|potresti)?\s*(?:aggiungere|ricordare|memorizzare|salvare)\s+(?:che\s+)?(.+)\s+(?:nella|in)\s+memoria/i, + /(?:voglio\s+che\s+)?(?:ricordi|memorizzi|aggiunga)\s+(?:che\s+)?(.+)/i + ]; + + // German explicit memory requests + const germanPatterns = [ + /(?:füge|merke|speichere)\s+(?:es\s+)?(?:in|zur)\s+?gedächtnis|speicher\s+(?:dass\s+)?(.+)/i, + /(?:kannst\s+du|könntest\s+du)?\s*(?:hinzufügen|merken|speichern)\s+(?:dass\s+)?(.+)\s+(?:in|zum)\s+(?:gedächtnis|speicher)/i, + /(?:ich\s+möchte\s+dass\s+du)\s*(?:merkst|speicherst|hinzufügst)\s+(?:dass\s+)?(.+)/i + ]; + + // Japanese explicit memory requests + const japanesePatterns = [/記憶に(?:追加|保存|覚えて)(?:して)?(?:ほしい|ください)?(?:、)?(.+)/i, /(?:覚えて|記憶して)(?:ほしい|ください)?(?:、)?(.+)/i]; + + // Chinese explicit memory requests + const chinesePatterns = [/把(.+)记在(?:记忆|内存|记忆库)里/i, /(?:请)?记住(?:这件事|这个|以下)?(.+)/i, /保存到记忆(?:里|中)(?:的是)?(.+)/i]; + + const allPatterns = [ + ...frenchPatterns, + ...englishPatterns, + ...spanishPatterns, + ...italianPatterns, + ...germanPatterns, + ...japanesePatterns, + ...chinesePatterns + ]; + + for (const pattern of allPatterns) { + const match = lowerText.match(pattern); + if (match && match[1]) { + const content = match[1].trim(); + + // Determine category based on content + const category = this.categorizeExplicitMemory(content); + + memories.push({ + category: category, + type: "explicit_request", + content: content, + sourceText: text, + confidence: 1.0, // High confidence for explicit requests + timestamp: new Date(), + character: this.selectedCharacter, + isUpdate: false + }); + break; // Only take the first match to avoid duplicates + } + } + + return memories; + } + + // Categorize explicit memory based on content analysis + categorizeExplicitMemory(content) { + const lowerContent = content.toLowerCase(); + + // Preference indicators + if ( + lowerContent.includes("j'aime") || + lowerContent.includes("i like") || + lowerContent.includes("j'adore") || + lowerContent.includes("i love") || + lowerContent.includes("je préfère") || + lowerContent.includes("i prefer") || + lowerContent.includes("je déteste") || + lowerContent.includes("i hate") + ) { + return "preferences"; + } + + // Personal information indicators + if ( + lowerContent.includes("je m'appelle") || + lowerContent.includes("my name is") || + (lowerContent.includes("j'ai") && lowerContent.includes("ans")) || + lowerContent.includes("years old") || + lowerContent.includes("j'habite") || + lowerContent.includes("i live") + ) { + return "personal"; + } + + // Relationship indicators + if ( + lowerContent.includes("ma femme") || + lowerContent.includes("my wife") || + lowerContent.includes("mon mari") || + lowerContent.includes("my husband") || + lowerContent.includes("mon ami") || + lowerContent.includes("my friend") || + lowerContent.includes("ma famille") || + lowerContent.includes("my family") + ) { + return "relationships"; + } + + // Activity indicators + if ( + lowerContent.includes("je joue") || + lowerContent.includes("i play") || + lowerContent.includes("je pratique") || + lowerContent.includes("i practice") || + lowerContent.includes("mon hobby") || + lowerContent.includes("my hobby") + ) { + return "activities"; + } + + // Goal indicators + if ( + lowerContent.includes("je veux") || + lowerContent.includes("i want") || + lowerContent.includes("mon objectif") || + lowerContent.includes("my goal") || + lowerContent.includes("j'apprends") || + lowerContent.includes("i'm learning") + ) { + return "goals"; + } + + // Default to preferences for most explicit requests + return "preferences"; + } + + // Check if content is too generic to be useful + isGenericContent(content) { + const genericWords = ["yes", "no", "ok", "okay", "sure", "thanks", "hello", "hi", "bye"]; + return genericWords.includes(content.toLowerCase()) || content.length < this.config.minContentLength; + } + + // Calculate confidence based on context and pattern strength + calculateExtractionConfidence(match, fullText) { + let confidence = this.config.confidence.base; // Base confidence from config + + // Boost confidence for explicit statements + const lower = fullText.toLowerCase(); + if ( + lower.includes("my name is") || + lower.includes("i am called") || + lower.includes("je m'appelle") || + lower.includes("mon nom est") || + lower.includes("je me prénomme") || + lower.includes("je me nomme") || + lower.includes("me llamo") || + lower.includes("mi nombre es") || + lower.includes("mi chiamo") || + lower.includes("il mio nome è") || + lower.includes("ich heiße") || + lower.includes("mein name ist") || + lower.includes("と申します") || + lower.includes("私の名前は") || + lower.includes("我叫") || + lower.includes("我的名字是") + ) { + confidence += this.config.confidence.bonusForExplicitStatement; + } + + // Boost for longer, more specific content + if (match[1] && match[1].trim().length > this.config.longContentThreshold) { + confidence += this.config.confidence.bonusForLongContent; + } + + // Reduce confidence for uncertain language + if (fullText.includes("maybe") || fullText.includes("perhaps") || fullText.includes("might")) { + confidence -= this.config.confidence.penaltyForUncertainty; + } + + return Math.min(this.config.confidence.max, Math.max(this.config.confidence.min, confidence)); + } + + // Generate a short title (2-5 words max) from content for auto-extracted memories + generateTitleFromContent(content) { + if (!content || typeof content !== "string") return ""; + // Remove surrounding punctuation and collapse whitespace + const cleaned = content + .replace(/[\n\r]+/g, " ") + .replace(/["'“”‘’–—:;()\[\]{}]+/g, "") + .trim(); + const words = cleaned.split(/\s+/).filter(Boolean); + + if (words.length === 0) return ""; + // Prefer 3 words when available, minimum 2 when possible, maximum 5 + let take; + if (words.length >= this.config.titleWordCount.preferred) take = this.config.titleWordCount.preferred; + else take = words.length; // 1 or 2 + take = Math.min(this.config.titleWordCount.max, Math.max(this.config.titleWordCount.min, take)); + + const slice = words.slice(0, take); + // Capitalize first word for nicer title + slice[0] = slice[0].charAt(0).toUpperCase() + slice[0].slice(1); + return slice.join(" "); + } + + // Check if this is an update to existing memory rather than new info + async isMemoryUpdate(category, content, existingMemories) { + const categoryMemories = existingMemories.filter(m => m.category === category); + + for (const memory of categoryMemories) { + const similarity = this.calculateSimilarity(memory.content, content); + if (similarity > this.config.similarity.update) { + // Lower threshold for updates + return true; + } + } + + return false; + } + + // Detect natural expressions that patterns might miss + async detectNaturalExpressions(text, existingMemories) { + const naturalMemories = []; + const lowerText = text.toLowerCase(); + + // Detect name mentions in natural context (multilingual) + const namePatterns = [ + // English + /call me (\w+)/i, + /(\w+) here[,.]?/i, + /this is (\w+)/i, + /(\w+) speaking/i, + // French + /appelle-?moi (\w+)/i, + /on m'appelle (\w+)/i, + /c'est (\w+)/i, + // Spanish + /llámame (\w+)/i, + /me llaman (\w+)/i, + /soy (\w+)/i, + // Italian + /chiamami (\w+)/i, + /mi chiamano (\w+)/i, + /sono (\w+)/i, + // German + /nenn mich (\w+)/i, + /man nennt mich (\w+)/i, + /ich bin (\w+)/i, + // Japanese + /(?:私は)?(\w+)です/i, + // Chinese + /我是(\w+)/i, + /叫我(\w+)/i + ]; + + for (const pattern of namePatterns) { + const match = lowerText.match(pattern); + if (match && match[1] && match[1].length > 1) { + const name = match[1].trim(); + + // Skip if too generic + if (!this.isGenericContent(name) && !this.isCommonWord(name)) { + naturalMemories.push({ + category: "personal", + type: "auto_extracted", + content: name, + sourceText: text, + confidence: 0.7, + createdAt: new Date(), // Use createdAt consistently + character: this.selectedCharacter || "kimi" // Fallback protection + }); + } + } + } + + return naturalMemories; + } + + // Check if word is too common to be a name + isCommonWord(word, language = "en") { + // Use existing constants if available + if (window.KIMI_COMMON_WORDS && window.KIMI_COMMON_WORDS[language]) { + return window.KIMI_COMMON_WORDS[language].includes(word.toLowerCase()); + } + + // Fallback to original English list + const commonWords = [ + "the", + "and", + "for", + "are", + "but", + "not", + "you", + "all", + "can", + "had", + "her", + "was", + "one", + "our", + "out", + "day", + "get", + "has", + "him", + "his", + "how", + "man", + "new", + "now", + "old", + "see", + "two", + "way", + "who", + "boy", + "did", + "its", + "let", + "put", + "say", + "she", + "too", + "use" + ]; + return commonWords.includes(word.toLowerCase()); + } + + // MANUAL MEMORY MANAGEMENT + async addMemory(memoryData) { + if (!this.db || !this.memoryEnabled) return; + + try { + // Check for duplicates with intelligent merging + const existing = await this.findSimilarMemory(memoryData); + if (existing) { + // Intelligent merge strategy + return await this.mergeMemories(existing, memoryData); + } + + // Add memory with metadata (let DB auto-generate ID) + const now = new Date(); + const memory = { + category: memoryData.category || "personal", + type: memoryData.type || "manual", + content: memoryData.content, + // precomputed keywords for faster matching and relevance + keywords: this.deriveKeywords(memoryData.content), + // Title: use provided title or generate for auto_extracted + title: + memoryData.title && typeof memoryData.title === "string" + ? memoryData.title + : memoryData.type === "auto_extracted" + ? this.generateTitleFromContent(memoryData.content) + : "", + sourceText: memoryData.sourceText || "", + confidence: memoryData.confidence || 1.0, + createdAt: memoryData.createdAt || memoryData.timestamp || now, // Unified timestamp handling + character: memoryData.character || this.selectedCharacter || "kimi", // Fallback protection + isActive: true, + tags: [...new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)])], + lastModified: now, + lastAccess: now, + accessCount: 0, + importance: this.calculateImportance(memoryData) + }; + + if (this.db.db.memories) { + const id = await this.db.db.memories.add(memory); + memory.id = id; // Store the auto-generated ID + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.log(`Memory added with ID: ${id}`); + } + } + + // Cleanup old memories if we exceed limit + await this.cleanupOldMemories(); + + // Notify LLM system to refresh context + this.notifyLLMContextUpdate(); + + return memory; + } catch (error) { + console.error("Error adding memory:", error); + return null; // Return null instead of undefined for clearer error handling + } + } + + // Intelligent memory merging + async mergeMemories(existingMemory, newMemoryData) { + try { + // Determine merge strategy based on content and confidence + const strategy = this.determineMergeStrategy(existingMemory, newMemoryData); + + let mergedContent = existingMemory.content; + let mergedConfidence = existingMemory.confidence; + let mergedTags = [...(existingMemory.tags || [])]; + + switch (strategy) { + case "update_content": + // New information is more confident/recent + mergedContent = newMemoryData.content; + mergedConfidence = Math.max(existingMemory.confidence, newMemoryData.confidence || 0.8); + break; + + case "merge_content": + // Combine information intelligently + if (existingMemory.category === "personal" && this.areRelatedNames(existingMemory.content, newMemoryData.content)) { + // Handle name variants + mergedContent = this.mergeNames(existingMemory.content, newMemoryData.content); + } else { + // General merge - keep most specific + mergedContent = newMemoryData.content.length > existingMemory.content.length ? newMemoryData.content : existingMemory.content; + } + mergedConfidence = (existingMemory.confidence + (newMemoryData.confidence || 0.8)) / 2; + break; + + case "add_variant": + // Store as variant/alias + mergedTags.push(`alias:${newMemoryData.content}`); + break; + + case "boost_confidence": + // Same content, boost confidence + mergedConfidence = Math.min(1.0, existingMemory.confidence + 0.1); + break; + } + + // Update existing memory + const updatedMemory = { + ...existingMemory, + content: mergedContent, + confidence: mergedConfidence, + tags: [...new Set([...mergedTags, ...this.deriveMemoryTags(newMemoryData)])], // Remove duplicates + lastModified: new Date(), + accessCount: (existingMemory.accessCount || 0) + 1, + importance: Math.max(existingMemory.importance || 0.5, this.calculateImportance(newMemoryData)) + }; + + await this.updateMemory(existingMemory.id, updatedMemory); + return updatedMemory; + } catch (error) { + console.error("Error merging memories:", error); + return existingMemory; + } + } + + // Simplified memory merge strategy determination + determineMergeStrategy(existing, newData) { + const similarity = this.calculateSimilarity(existing.content, newData.content); + const newConfidence = newData.confidence || this.config.confidence.base; + const existingConfidence = existing.confidence || this.config.confidence.base; + + // Very high similarity (>90%) - boost confidence if new is more confident + if (similarity > this.config.similarity.veryHigh) { + return newConfidence > existingConfidence ? "boost_confidence" : "merge_content"; + } + + // High similarity (>70%) - decide based on content length and specificity + if (similarity > this.config.similarity.preferences) { + // If new content is significantly longer (50% more), it's likely more detailed + if (newData.content.length > existing.content.length * 1.5) { + return "update_content"; + } + // If existing is longer, merge to preserve information + return "merge_content"; + } + + // For personal names, handle as variants if they're related + if (existing.category === "personal" && this.areRelatedNames(existing.content, newData.content)) { + return "add_variant"; + } + + // Default strategy for moderate similarity + return "merge_content"; + } + + // Merge name variants intelligently + mergeNames(name1, name2) { + // Keep the longest/most formal version as primary + if (name1.length > name2.length) { + return name1; + } else if (name2.length > name1.length) { + return name2; + } + + // If same length, keep the first one + return name1; + } + + // Calculate importance of memory for prioritization + calculateImportance(memoryData) { + let importance = 0.5; // Base importance + + // Category base weights + const categoryWeights = { + important: 1.0, + personal: 0.9, + relationships: 0.85, + goals: 0.75, + experiences: 0.65, + preferences: 0.6, + activities: 0.5 + }; + + importance = categoryWeights[memoryData.category] || 0.5; + + const content = (memoryData.content || "").toLowerCase(); + const tags = new Set([...(memoryData.tags || []), ...this.deriveMemoryTags(memoryData)]); + + // Heuristic boosts for meaningful relationship milestones and commitments + const milestoneTags = [ + "relationship:first_meet", + "relationship:first_date", + "relationship:first_kiss", + "relationship:anniversary", + "relationship:moved_in", + "relationship:engaged", + "relationship:married", + "relationship:breakup" + ]; + if ([...tags].some(t => milestoneTags.includes(t))) importance += 0.15; + + // Boundaries and consent are high priority to remember + if ([...tags].some(t => t.startsWith("boundary:"))) importance += 0.15; + + // Preferences tied to strong like/dislike + if (content.includes("i love") || content.includes("j'adore") || content.includes("i hate") || content.includes("je déteste")) { + importance += 0.05; + } + + // Temporal cues: future commitments or dates + if (/(\bnext\b|\btomorrow\b|\bce soir\b|\bdemain\b|\bmañana\b|\bdomani\b|\bmorgen\b)/i.test(content)) { + importance += 0.05; + } + + // Longer details and high confidence + if (memoryData.content && memoryData.content.length > this.config.longContentThreshold) importance += this.config.importance.bonuses.longContent; + if (memoryData.confidence && memoryData.confidence > 0.9) importance += this.config.importance.bonuses.highConfidence; + + // Round to two decimals to avoid floating point artifacts + return Math.min(1.0, Math.round(importance * 100) / 100); + } + + // Derive semantic tags from memory content to assist prioritization and merging + deriveMemoryTags(memoryData) { + const tags = []; + const text = (memoryData.content || "").toLowerCase(); + const category = memoryData.category || ""; + + // Relationship status and milestones + if (/(single|célibataire|soltero|single|ledig)/i.test(text)) tags.push("relationship:status_single"); + if (/(in a relationship|en couple|together|ensemble|pareja|coppia|beziehung)/i.test(text)) tags.push("relationship:status_in_relationship"); + if (/(engaged|fiancé|fiancée|promis|promised|verlobt)/i.test(text)) tags.push("relationship:status_engaged"); + if (/(married|marié|mariée|casado|sposato|verheiratet)/i.test(text)) tags.push("relationship:status_married"); + if (/(broke up|rupture|separated|separado|separati|getrennt)/i.test(text)) tags.push("relationship:breakup"); + if (/(first date|premier rendez-vous|primera cita|primo appuntamento)/i.test(text)) tags.push("relationship:first_date"); + if (/(first kiss|premier baiser|primer beso|primo bacio)/i.test(text)) tags.push("relationship:first_kiss"); + if (/(anniversary|anniversaire|aniversario|anniversario|jahrestag)/i.test(text)) tags.push("relationship:anniversary"); + if (/(moved in together|emménagé ensemble|mudamos juntos|trasferiti insieme|zusammen eingezogen)/i.test(text)) tags.push("relationship:moved_in"); + if (/(met at|rencontré à|conocimos en|conosciuti a|kennengelernt)/i.test(text)) tags.push("relationship:first_meet"); + + // Boundaries and consent (keep generic and non-graphic) + if (/(i don't like|je n'aime pas|no me gusta|non mi piace|ich mag nicht)\s+[^,.!?]+/i.test(text)) tags.push("boundary:dislike"); + if (/(i prefer|je préfère|prefiero|preferisco|ich bevorzuge)\s+[^,.!?]+/i.test(text)) tags.push("boundary:preference"); + if (/(no|pas)\s+(?:kissing|baiser|beso|bacio|küssen)/i.test(text)) tags.push("boundary:limit"); + if (/(consent|consentement|consentimiento|consenso|einwilligung)/i.test(text)) tags.push("boundary:consent"); + + // Time-related tags + if (/(today|ce jour|hoy|oggi|heute|今日)/i.test(text)) tags.push("time:today"); + if (/(tomorrow|demain|mañana|domani|morgen|明日)/i.test(text)) tags.push("time:tomorrow"); + if (/(next week|semaine prochaine|la próxima semana|la prossima settimana|nächste woche)/i.test(text)) tags.push("time:next_week"); + + // Category-specific hints + if (category === "preferences") tags.push("type:preference"); + if (category === "personal") tags.push("type:personal"); + if (category === "relationships") tags.push("type:relationship"); + if (category === "experiences") tags.push("type:experience"); + if (category === "goals") tags.push("type:goal"); + if (category === "important") tags.push("type:important"); + + return tags; + } + + async updateMemory(memoryId, updateData) { + if (!this.db) return false; + + try { + // Ensure memoryId is the correct type + const numericId = typeof memoryId === "string" ? parseInt(memoryId) : memoryId; + + // Vérifier d'abord que la mémoire existe + const existingMemory = await this.db.db.memories.get(numericId); + if (!existingMemory) { + console.error(`❌ Memory with ID ${numericId} not found in database`); + return false; + } + + console.log(`🔄 Updating memory ${numericId}:`, { existing: existingMemory, update: updateData }); + + const update = { + ...updateData, + lastModified: new Date() + }; + + if (this.db.db.memories) { + const result = await this.db.db.memories.update(numericId, update); + + console.log(`Memory update result for ID ${numericId}:`, result); + + if (result > 0) { + console.log("✅ Memory updated successfully"); + // Notify LLM system to refresh context + this.notifyLLMContextUpdate(); + return true; + } else { + console.error("❌ Memory update failed - no rows affected"); + return false; + } + } + } catch (error) { + console.error("Error updating memory:", error, { memoryId, updateData }); + return false; + } + } + + async deleteMemory(memoryId) { + if (!this.db) return false; + + try { + // Ensure memoryId is the correct type + const numericId = typeof memoryId === "string" ? parseInt(memoryId) : memoryId; + + if (this.db.db.memories) { + const result = await this.db.db.memories.delete(numericId); + + console.log(`Memory delete result for ID ${numericId}:`, result); + + // Notify LLM system to refresh context + if (result) { + this.notifyLLMContextUpdate(); + } + + return result; + } + } catch (error) { + console.error("Error deleting memory:", error, { memoryId }); + return false; + } + } + + notifyLLMContextUpdate() { + // Debounce context updates to avoid excessive calls + if (this.contextUpdateTimeout) { + clearTimeout(this.contextUpdateTimeout); + } + + this.contextUpdateTimeout = setTimeout(() => { + if (window.kimiLLM && typeof window.kimiLLM.refreshMemoryContext === "function") { + window.kimiLLM.refreshMemoryContext(); + } + }, 500); + } + + // Add cleanup method for memory system + cleanup() { + if (this.contextUpdateTimeout) { + clearTimeout(this.contextUpdateTimeout); + this.contextUpdateTimeout = null; + } + + // Clear caches to prevent memory leaks + if (this.keywordCache) { + this.keywordCache.clear(); + } + + // Reset stats arrays to prevent accumulation + if (this.queryStats) { + this.queryStats.extractionTime.length = 0; + this.queryStats.addMemoryTime.length = 0; + this.queryStats.retrievalTime.length = 0; + } + } + + async getMemoriesByCategory(category, character = null) { + if (!this.db) return []; + + try { + character = character || this.selectedCharacter || "kimi"; // Unified fallback + + if (this.db.db.memories) { + const memories = await this.db.db.memories + .where("[character+category]") + .equals([character, category]) + .and(m => m.isActive) + .reverse() + .sortBy("timestamp"); + + // Update lastAccess/accessCount for top results to improve prioritization + this._touchMemories(memories, 10).catch(() => {}); + return memories; + } + } catch (error) { + console.error("Error getting memories by category:", error); + return []; + } + } + + async getAllMemories(character = null) { + if (!this.db) return []; + + try { + character = character || this.selectedCharacter || "kimi"; + + if (this.db.db.memories) { + // Primary IndexedDB (Dexie) sort still leverages the existing 'timestamp' index for performance. + // Then we apply a stable in-memory reorder using canonical creation time (createdAt fallback timestamp) + // to unify ordering semantics without breaking older databases lacking createdAt originally. + const memories = await this.db.db.memories + .where("character") + .equals(character) + .filter(memory => memory.isActive !== false) // Include records without isActive field (legacy) + .reverse() + .sortBy("timestamp"); + + // Backward-compatible canonical ordering: most recent first by getCreationTimestamp + // (Only if >1 entry to avoid needless array ops.) + if (memories.length > 1) { + memories.sort((a, b) => { + const ca = new Date(this.getCreationTimestamp(a)).getTime(); + const cb = new Date(this.getCreationTimestamp(b)).getTime(); + return cb - ca; // descending (newest first) + }); + } + + if (window.KIMI_DEBUG_MEMORIES) { + console.log(`Retrieved ${memories.length} memories for character: ${character}`); + } + + // Touch top memories to update access metrics + this._touchMemories(memories, 10).catch(() => {}); + return memories; + } + } catch (error) { + console.error("Error getting all memories:", error); + return []; + } + } + + async findSimilarMemory(memoryData) { + if (!this.db) return null; + + try { + const memories = await this.getMemoriesByCategory(memoryData.category); + + // Precompute keywords for new memory + const newKeys = this.deriveKeywords(memoryData.content || ""); + + // Enhanced similarity check with multiple criteria + for (const memory of memories) { + // Prefilter by keyword overlap to reduce false positives and improve perf + const memKeys = memory.keywords || this.deriveKeywords(memory.content || ""); + const overlap = newKeys.filter(k => memKeys.includes(k)).length; + if (newKeys.length > 0 && overlap === 0) continue; // no shared keywords -> likely different + + const contentSimilarity = this.calculateSimilarity(memory.content, memoryData.content); + + // Different thresholds based on category + const threshold = this.config.similarity[memoryData.category] || this.config.similarity.default; + + if (contentSimilarity > threshold) { + return memory; + } + + // Special handling for names (check if one is contained in the other) + if (memoryData.category === "personal" && this.areRelatedNames(memory.content, memoryData.content)) { + return memory; + } + } + } catch (error) { + console.error("Error finding similar memory:", error); + } + + return null; + } + + // Check if two names are related (nicknames, variants, etc.) + areRelatedNames(name1, name2) { + const n1 = name1.toLowerCase().trim(); + const n2 = name2.toLowerCase().trim(); + + // Exact match + if (n1 === n2) return true; + + // One contains the other (Jean-Pierre vs Jean) + if (n1.includes(n2) || n2.includes(n1)) return true; + + // Common nickname patterns + const nicknames = { + jean: ["jp", "jeannot"], + pierre: ["pete", "pietro"], + marie: ["mary", "maria"], + michael: ["mike", "mick"], + william: ["bill", "will", "willy"], + robert: ["bob", "rob", "bobby"], + richard: ["rick", "dick", "richie"], + thomas: ["tom", "tommy"], + christopher: ["chris", "kit"], + anthony: ["tony", "ant"] + }; + + for (const [full, nicks] of Object.entries(nicknames)) { + if ((n1 === full && nicks.includes(n2)) || (n2 === full && nicks.includes(n1))) { + return true; + } + } + + return false; + } + + calculateSimilarity(text1, text2) { + // Enhanced similarity calculation + const words1 = text1 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2); + const words2 = text2 + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2); + + if (words1.length === 0 || words2.length === 0) { + return text1.toLowerCase() === text2.toLowerCase() ? 1 : 0; + } + + const intersection = words1.filter(word => words2.includes(word)); + const union = [...new Set([...words1, ...words2])]; + + let similarity = intersection.length / union.length; + + // Boost similarity for exact substring matches + if (text1.toLowerCase().includes(text2.toLowerCase()) || text2.toLowerCase().includes(text1.toLowerCase())) { + similarity += 0.2; + } + + return Math.min(1.0, similarity); + } + + // Derive a set of normalized keywords from text + deriveKeywords(text) { + if (!text || typeof text !== "string") return []; + return [ + ...new Set( + text + .toLowerCase() + .replace(/[\p{P}\p{S}]/gu, " ") + .split(/\s+/) + .filter(w => w.length > 2 && !this.isCommonWordSafe(w)) + ) + ]; + } + + // Safe wrapper for isCommonWord to avoid undefined function errors + isCommonWordSafe(word, language = "en") { + const cacheKey = `${word.toLowerCase()}_${language}`; + + // Check cache first + if (this.keywordCache.has(cacheKey)) { + this.keywordCacheHits++; + return this.keywordCache.get(cacheKey); + } + + // Cache miss - compute the result + this.keywordCacheMisses++; + let isCommon = false; + + try { + isCommon = typeof this.isCommonWord === "function" ? this.isCommonWord(word, language) : false; + } catch (error) { + console.warn("Error checking common word:", error); + isCommon = false; + } + + // Add to cache with LRU eviction + if (this.keywordCache.size >= this.keywordCacheSize) { + // Simple LRU: remove oldest entry (first in Map) + const firstKey = this.keywordCache.keys().next().value; + this.keywordCache.delete(firstKey); + } + + this.keywordCache.set(cacheKey, isCommon); + return isCommon; + } + + // Get cache statistics for debugging + getKeywordCacheStats() { + const total = this.keywordCacheHits + this.keywordCacheMisses; + return { + size: this.keywordCache.size, + hits: this.keywordCacheHits, + misses: this.keywordCacheMisses, + hitRate: total > 0 ? ((this.keywordCacheHits / total) * 100).toFixed(2) + "%" : "0%" + }; + } + + // Get performance statistics for debugging and optimization + getPerformanceStats() { + const calculateStats = times => { + if (times.length === 0) return { avg: 0, max: 0, min: 0, count: 0 }; + return { + avg: Math.round((times.reduce((sum, t) => sum + t, 0) / times.length) * 100) / 100, + max: Math.round(Math.max(...times) * 100) / 100, + min: Math.round(Math.min(...times) * 100) / 100, + count: times.length + }; + }; + + return { + keywordCache: this.getKeywordCacheStats(), + extraction: calculateStats(this.queryStats.extractionTime), + addMemory: calculateStats(this.queryStats.addMemoryTime), + retrieval: calculateStats(this.queryStats.retrievalTime) + }; + } + + // Performance wrapper for memory extraction + async extractMemoryFromTextTimed(userText, kimiResponse = null) { + const start = performance.now(); + const result = await this.extractMemoryFromText(userText, kimiResponse); + const duration = performance.now() - start; + + this.queryStats.extractionTime.push(duration); + if (this.queryStats.extractionTime.length > 100) { + this.queryStats.extractionTime.shift(); // Keep only last 100 measurements + } + + if (duration > 100 && window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.warn(`🐌 Slow memory extraction: ${duration.toFixed(2)}ms for text length ${userText?.length || 0}`); + } + + return result; + } + + // Get current configuration for debugging and monitoring + getConfiguration() { + return { + ...this.config, + memoryCategories: this.memoryCategories, + runtime: { + memoryEnabled: this.memoryEnabled, + maxMemoryEntries: this.maxMemoryEntries, + selectedCharacter: this.selectedCharacter, + keywordCacheSize: this.keywordCache.size, + compiledPatternsCount: Object.values(this.compiledPatterns || {}).reduce((sum, arr) => sum + arr.length, 0) + } + }; + } + + // Update configuration at runtime (for advanced users) + updateConfiguration(configPath, value) { + const keys = configPath.split("."); + let current = this.config; + + // Navigate to the parent object + for (let i = 0; i < keys.length - 1; i++) { + if (!current[keys[i]]) current[keys[i]] = {}; + current = current[keys[i]]; + } + + // Set the value + const lastKey = keys[keys.length - 1]; + const oldValue = current[lastKey]; + current[lastKey] = value; + + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.log(`🔧 Configuration updated: ${configPath} = ${value} (was: ${oldValue})`); + } + + return { oldValue, newValue: value }; + } + + async cleanupOldMemories() { + if (!this.db) return; + + try { + // Retrieve all active memories for the current character + const memories = await this.getAllMemories(); + + const maxEntries = window.KIMI_MAX_MEMORIES || this.maxMemoryEntries || 100; + const ttlDays = window.KIMI_MEMORY_TTL_DAYS || 365; + + // Soft-expire memories older than TTL by marking isActive=false + const now = Date.now(); + const ttlMs = ttlDays * 24 * 60 * 60 * 1000; + const expiredMemories = []; + + for (const mem of memories) { + const created = new Date(this.getCreationTimestamp(mem)).getTime(); + if (now - created > ttlMs) { + try { + await this.updateMemory(mem.id, { isActive: false }); + expiredMemories.push(mem.id); + } catch (e) { + console.error(`Memory expiration failed for ID ${mem.id}:`, { + error: e.message, + memoryId: mem.id, + createdAt: this.getCreationTimestamp(mem), + character: mem.character + }); + // Continue with other memories even if one fails + } + } + } + + if (window.KIMI_CONFIG?.DEBUG?.MEMORY && expiredMemories.length > 0) { + console.log(`Successfully expired ${expiredMemories.length} memories:`, expiredMemories); + } + + // Refresh active memories after TTL purge + const activeMemories = (await this.getAllMemories()).filter(m => m.isActive); + + // If still more than maxEntries, mark lowest-priority ones inactive (soft delete) + if (activeMemories.length > maxEntries) { + // Sort by a combined score: low importance + old timestamp + low access + activeMemories.sort((a, b) => { + const scoreA = + (a.importance || 0.5) * -1 + (a.accessCount || 0) * 0.01 + new Date(this.getCreationTimestamp(a)).getTime() / (1000 * 60 * 60 * 24); + const scoreB = + (b.importance || 0.5) * -1 + (b.accessCount || 0) * 0.01 + new Date(this.getCreationTimestamp(b)).getTime() / (1000 * 60 * 60 * 24); + return scoreB - scoreA; + }); + + const toDeactivate = activeMemories.slice(maxEntries); + const deactivatedMemories = []; + const failedDeactivations = []; + + for (const mem of toDeactivate) { + try { + await this.updateMemory(mem.id, { isActive: false }); + deactivatedMemories.push(mem.id); + } catch (e) { + console.error(`Memory deactivation failed for ID ${mem.id}:`, { + error: e.message, + memoryId: mem.id, + importance: mem.importance, + character: mem.character + }); + failedDeactivations.push(mem.id); + } + } + + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.log(`Memory cleanup: ${deactivatedMemories.length} deactivated, ${failedDeactivations.length} failed`); + } + } + } catch (error) { + console.error("Error cleaning up old memories:", error); + } + } + + // MEMORY RETRIEVAL FOR LLM + async getRelevantMemories(context = "", limit = 10) { + if (!this.memoryEnabled) return []; + + try { + const allMemories = await this.getAllMemories(); + + if (allMemories.length === 0) return []; + + if (!context) { + // Return most important and recent memories + const res = this.selectMostImportantMemories(allMemories, limit); + // touch top results to update access metrics + this._touchMemories(res, limit).catch(() => {}); + return res; + } + + // Score memories based on relevance to context + const scoredMemories = allMemories.map(memory => ({ + ...memory, + relevanceScore: this.calculateRelevance(memory, context) + })); + + // Sort by relevance and return top results + scoredMemories.sort((a, b) => b.relevanceScore - a.relevanceScore); + + // Filter out very low relevance memories + const relevantMemories = scoredMemories.filter(m => m.relevanceScore > 0.1); + + const out = relevantMemories.slice(0, limit).map(r => r); + // touch top results to update access metrics + this._touchMemories( + out.map(r => r), + limit + ).catch(() => {}); + return out; + } catch (error) { + console.error("Error getting relevant memories:", error); + return []; + } + } + + // Select most important memories when no context is provided + selectMostImportantMemories(memories, limit) { + // Score by importance, recency, and access count + const scoredMemories = memories.map(memory => { + let score = memory.importance || 0.5; + + // Boost recent memories + const daysSinceCreation = this.getDaysSinceCreation(memory); + score += Math.max(0, (7 - daysSinceCreation) / 7) * 0.2; // Recent boost + + // Boost frequently accessed memories + const accessCount = memory.accessCount || 0; + score += Math.min(accessCount / 10, 0.2); // Access boost + + // Boost high confidence memories + score += (memory.confidence || 0.5) * 0.1; + + return { ...memory, importanceScore: score }; + }); + + scoredMemories.sort((a, b) => b.importanceScore - a.importanceScore); + return scoredMemories.slice(0, limit); + } + + calculateRelevance(memory, context) { + const contextWords = context + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2); + const memoryWords = memory.content + .toLowerCase() + .split(/\s+/) + .filter(w => w.length > 2); + + let score = 0; + + // Enhanced content similarity with keyword matching + score += this.calculateSimilarity(memory.content, context) * this.config.relevance.contentSimilarity; + + // Keyword overlap boost (derived keywords) + try { + const memKeys = memory.keywords || this.deriveKeywords(memory.content || ""); + const ctxKeys = this.deriveKeywords(context || ""); + const keyOverlap = ctxKeys.filter(k => memKeys.includes(k)).length; + if (ctxKeys.length > 0) { + score += (keyOverlap / ctxKeys.length) * this.config.relevance.keywordOverlap; + } + } catch (e) { + // fallback to original keyword matching + let keywordMatches = 0; + for (const word of contextWords) { + if (memoryWords.includes(word)) { + keywordMatches++; + } + } + if (contextWords.length > 0) { + score += (keywordMatches / contextWords.length) * this.config.relevance.keywordOverlap; + } + } + + // Category relevance bonus based on context + score += this.getCategoryRelevance(memory.category, context) * this.config.relevance.categoryRelevance; + + // Recent memories get bonus for current conversation + const daysSinceCreation = this.getDaysSinceCreation(memory); + score += + Math.max(0, (this.config.relevance.recentDaysThreshold - daysSinceCreation) / this.config.relevance.recentDaysThreshold) * + this.config.relevance.recencyBonus; + + // Confidence and importance boost + score += (memory.confidence || 0.5) * this.config.relevance.confidenceBonus; + score += (memory.importance || 0.5) * this.config.relevance.importanceBonus; + + return Math.min(1.0, score); + } + + // Determine if memory category is relevant to current context + getCategoryRelevance(category, context) { + const contextLower = context.toLowerCase(); + + const categoryKeywords = { + personal: ["name", "age", "live", "work", "job", "who", "am", "myself", "appelle", "nombre", "chiamo", "heiße", "名前", "名字", "我叫"], + preferences: [ + "like", + "love", + "hate", + "prefer", + "enjoy", + "favorite", + "dislike", + "j'aime", + "j'adore", + "je préfère", + "je déteste", + "me gusta", + "prefiero", + "odio", + "mi piace", + "preferisco", + "ich mag", + "ich bevorzuge", + "hasse" + ], + relationships: [ + "family", + "friend", + "wife", + "husband", + "partner", + "mother", + "father", + "girlfriend", + "boyfriend", + "anniversary", + "date", + "kiss", + "move in", + "famille", + "ami", + "copine", + "copain", + "anniversaire", + "rendez-vous", + "baiser", + "emménagé", + "pareja", + "cita", + "beso", + "aniversario", + "mudarnos", + "fidanzata", + "fidanzato", + "anniversario", + "bacio", + "trasferiti", + "freundin", + "freund", + "jahrestag", + "kuss", + "eingezogen" + ], + activities: [ + "play", + "hobby", + "sport", + "activity", + "practice", + "do", + "joue", + "passe-temps", + "hobby", + "juego", + "pasatiempo", + "gioco", + "passatempo", + "spiele", + "hobby" + ], + goals: [ + "want", + "plan", + "goal", + "dream", + "hope", + "wish", + "future", + "veux", + "objectif", + "apprends", + "aprendo", + "voglio", + "obiettivo", + "lerne", + "ziel" + ], + experiences: [ + "remember", + "happened", + "story", + "experience", + "time", + "we met", + "first date", + "first kiss", + "anniversary", + "rencontré", + "premier rendez-vous", + "premier baiser", + "anniversaire", + "conocimos", + "primera cita", + "primer beso", + "aniversario", + "conosciuti", + "primo appuntamento", + "primo bacio", + "anniversario", + "kennengelernt", + "erstes date", + "erster kuss", + "jahrestag" + ], + important: [ + "important", + "remember", + "special", + "never forget", + "important", + "souvenir", + "spécial", + "importante", + "recuerda", + "importante", + "ricorda", + "wichtig", + "erinnere" + ] + }; + + const keywords = categoryKeywords[category] || []; + let relevance = 0; + + for (const keyword of keywords) { + if (contextLower.includes(keyword)) { + relevance += 0.2; + } + } + + return Math.min(1.0, relevance); + } + + // Update access count when memory is used + async recordMemoryAccess(memoryId) { + try { + const memory = await this.db.db.memories.get(memoryId); + if (memory) { + memory.accessCount = (memory.accessCount || 0) + 1; + memory.lastAccess = new Date(); + await this.db.db.memories.put(memory); + } + } catch (error) { + console.error("Error recording memory access:", error); + } + } + + // Touch multiple memories to update lastAccess and accessCount + async _touchMemories(memories, limit = 5) { + if (!this.db || !Array.isArray(memories) || memories.length === 0) return; + + try { + const top = memories.slice(0, limit); + const now = new Date(); + const minMinutes = window.KIMI_MEMORY_TOUCH_MINUTES || 60; + const minTouchInterval = minMinutes * 60 * 1000; + + // Batch collection: gather all updates before executing + const batchUpdates = []; + + for (const m of top) { + try { + const id = m.id; + const existing = await this.db.db.memories.get(id); + if (existing) { + const lastAccess = existing.lastAccess ? new Date(existing.lastAccess).getTime() : 0; + + // Only touch if enough time has passed + if (now.getTime() - lastAccess > minTouchInterval) { + batchUpdates.push({ + key: id, + changes: { + accessCount: (existing.accessCount || 0) + 1, + lastAccess: now + } + }); + } + } + } catch (e) { + console.warn("Error preparing memory touch batch for", m && m.id, e); + } + } + + // Execute all updates in a single batch operation + if (batchUpdates.length > 0) { + if (this.db.db.memories.bulkUpdate) { + // Use bulkUpdate if available (Dexie 3.x+) + await this.db.db.memories.bulkUpdate(batchUpdates); + } else { + // Fallback: parallel individual updates (still better than sequential) + const updatePromises = batchUpdates.map(update => this.db.db.memories.update(update.key, update.changes)); + await Promise.all(updatePromises); + } + + if (window.KIMI_CONFIG?.DEBUG?.MEMORY) { + console.log(`📊 Batch touched ${batchUpdates.length} memories`); + } + } + } catch (e) { + console.warn("Error in _touchMemories batch processing", e); + } + } + + // ===== MEMORY SCORING & RANKING ===== + scoreMemory(memory) { + // Factors: importance (0-1), recency, frequency, confidence + const now = Date.now(); + const created = memory.createdAt ? new Date(memory.createdAt).getTime() : memory.timestamp ? new Date(memory.timestamp).getTime() : now; + const lastAccess = memory.lastAccess ? new Date(memory.lastAccess).getTime() : created; + const ageMs = Math.max(1, now - created); + const sinceLastAccessMs = Math.max(1, now - lastAccess); + // Recency: exponential decay + const recency = Math.exp(-sinceLastAccessMs / (1000 * 60 * 60 * 24 * 14)); // 14-day half-life approx + const freshness = Math.exp(-ageMs / (1000 * 60 * 60 * 24 * 60)); // 60-day aging + const freq = Math.log10((memory.accessCount || 0) + 1) / Math.log10(50); // normalized frequency (cap ~50) + const importance = typeof memory.importance === "number" ? memory.importance : 0.5; + const confidence = typeof memory.confidence === "number" ? memory.confidence : 0.5; + // Weighted sum using global knobs + const wImportance = window.KIMI_WEIGHT_IMPORTANCE || 0.35; + const wRecency = window.KIMI_WEIGHT_RECENCY || 0.2; + const wFrequency = window.KIMI_WEIGHT_FREQUENCY || 0.15; + const wConfidence = window.KIMI_WEIGHT_CONFIDENCE || 0.2; + const wFreshness = window.KIMI_WEIGHT_FRESHNESS || 0.1; + + const score = importance * wImportance + recency * wRecency + freq * wFrequency + confidence * wConfidence + freshness * wFreshness; + return Number(score.toFixed(6)); + } + + async getRankedMemories(contextText = "", limit = 7) { + const all = await this.getAllMemories(); + if (!all.length) return []; + // Optional basic context relevance boost + const ctxLower = (contextText || "").toLowerCase(); + // Favor pinned memories by boosting their base score + return all + .map(m => { + let baseScore = this.scoreMemory(m); + if (m.tags && m.tags.includes && m.tags.includes("pinned")) baseScore += 0.2; + if (ctxLower && m.content && ctxLower.includes(m.content.toLowerCase().split(" ")[0])) { + baseScore += 0.05; // tiny relevance boost + } + return { memory: m, score: baseScore }; + }) + .sort((a, b) => b.score - a.score) + .slice(0, limit) + .map(r => r.memory); + } + + // Pin/unpin APIs to manually mark important memories + async pinMemory(memoryId) { + if (!this.db) return false; + try { + const m = await this.db.db.memories.get(memoryId); + if (!m) return false; + const tags = new Set([...(m.tags || []), "pinned"]); + await this.db.db.memories.update(memoryId, { tags: [...tags], importance: Math.max(m.importance || 0.5, 0.95) }); + return true; + } catch (e) { + console.error("Error pinning memory", e); + return false; + } + } + + async unpinMemory(memoryId) { + if (!this.db) return false; + try { + const m = await this.db.db.memories.get(memoryId); + if (!m) return false; + const tags = new Set([...(m.tags || [])]); + tags.delete("pinned"); + await this.db.db.memories.update(memoryId, { tags: [...tags] }); + return true; + } catch (e) { + console.error("Error unpinning memory", e); + return false; + } + } + + // Summarize recent memories into a non-destructive summary memory + async summarizeRecentMemories(days = 7, options = { category: null, archiveSources: false }) { + if (!this.db) return null; + try { + const cutoff = Date.now() - (days || 7) * 24 * 60 * 60 * 1000; + const all = await this.getAllMemories(); + // Exclude existing summaries to avoid summarizing summaries repeatedly + const recent = all.filter( + m => new Date(this.getCreationTimestamp(m)).getTime() >= cutoff && m.isActive && m.type !== "summary" && !(m.tags && m.tags.includes("summary")) + ); + if (!recent.length) return null; + + // Group by top keyword + const groups = {}; + for (const m of recent) { + const keys = m.keywords && m.keywords.length ? m.keywords : this.deriveKeywords(m.content || ""); + const top = keys[0] || "misc"; + groups[top] = groups[top] || []; + groups[top].push(m); + } + + // Build a simple summary per group + const summaries = []; + for (const [k, items] of Object.entries(groups)) { + const contents = items.map(i => i.content).slice(0, 6); + summaries.push(`${k}: ${contents.join(" | ")}`); + } + + const summaryContent = `Summary (${days}d): ` + summaries.join(" \n"); + + const summaryJson = { summary: summaries }; + + const summaryMemory = { + category: options.category || "experiences", + type: "summary", + content: summaryContent, + sourceText: summaryContent, + summaryJson: JSON.stringify(summaryJson), + confidence: 0.9, + createdAt: new Date(), // Use createdAt consistently + character: this.selectedCharacter, + isActive: true, + tags: ["summary"] + }; + + const saved = await this.addMemory(summaryMemory); + + // Optionally archive sources (soft-deactivate) + if (options.archiveSources) { + for (const m of recent) { + try { + await this.updateMemory(m.id, { isActive: false }); + } catch (e) { + console.warn("Failed to archive source memory", m.id, e); + } + } + } + + return saved; + } catch (e) { + console.error("Error summarizing memories", e); + return null; + } + } + + // Summarize recent memories and replace sources (hard delete) - destructive + async summarizeAndReplace(days = 7, options = { category: null }) { + if (!this.db) return null; + try { + const cutoff = Date.now() - (days || 7) * 24 * 60 * 60 * 1000; + const all = await this.getAllMemories(); + // Exclude existing summaries to avoid recursive summarization + const recent = all.filter( + m => new Date(this.getCreationTimestamp(m)).getTime() >= cutoff && m.isActive && m.type !== "summary" && !(m.tags && m.tags.includes("summary")) + ); + if (!recent.length) return null; + + // Build aggregate content from readable fields in chronological order + recent.sort((a, b) => new Date(this.getCreationTimestamp(a)) - new Date(this.getCreationTimestamp(b))); + const texts = recent + .map(r => { + const raw = (r.title && r.title.trim()) || (r.sourceText && r.sourceText.trim()) || (r.content && r.content.trim()) || ""; + if (!raw) return ""; + // Normalize whitespace and remove stray leading punctuation + let t = raw.replace(/\s+/g, " ").replace(/^[^\p{L}\p{N}]+/u, ""); + // Capitalize first meaningful letter + if (t && t.length > 0) t = t.charAt(0).toUpperCase() + t.slice(1); + return t; + }) + .filter(Boolean) + .slice(0, 200); + + const summaryContent = `Summary (${days}d):\n` + texts.map(t => `- ${t}`).join("\n"); + + const summaryJson = { summary: texts }; + + const summaryMemory = { + category: options.category || "experiences", + type: "summary", + title: `Summary - last ${days} days`, + content: summaryContent, + // Store the actual summary also in sourceText so editors/UIs show it + sourceText: summaryContent, + summaryJson: JSON.stringify(summaryJson), + confidence: 0.95, + timestamp: new Date(), + character: this.selectedCharacter, + isActive: true, + tags: ["summary", "replaced"] + }; + + // Add summary directly to DB to avoid addMemory's merge logic + let saved = null; + if (this.db && this.db.db && this.db.db.memories) { + try { + const id = await this.db.db.memories.add(summaryMemory); + summaryMemory.id = id; + saved = summaryMemory; + console.log("Summary added with ID:", id); + // Read back the saved record to verify stored fields + try { + const savedRec = await this.db.db.memories.get(id); + console.log("Saved summary record:", { id, content: savedRec.content, sourceText: savedRec.sourceText }); + } catch (e) { + console.warn("Unable to read back saved summary", e); + } + } catch (e) { + console.error("Failed to write summary directly to DB", e); + } + } else { + // Fallback to addMemory if DB not available + saved = await this.addMemory(summaryMemory); + } + + // Hard-delete sources + for (const m of recent) { + try { + if (this.db && this.db.db && this.db.db.memories) { + await this.db.db.memories.delete(m.id); + } + } catch (e) { + console.warn("Failed to delete source memory", m.id, e); + } + } + + // Notify LLM to refresh context + this.notifyLLMContextUpdate(); + + return saved; + } catch (e) { + console.error("Error in summarizeAndReplace", e); + return null; + } + } + + // MEMORY STATISTICS + async getMemoryStats() { + try { + const memories = await this.getAllMemories(); + const stats = { + total: memories.length, + byCategory: {}, + averageConfidence: 0, + oldestMemory: null, + newestMemory: null + }; + + if (memories.length > 0) { + // Category breakdown + for (const memory of memories) { + stats.byCategory[memory.category] = (stats.byCategory[memory.category] || 0) + 1; + } + + // Average confidence + stats.averageConfidence = memories.reduce((sum, m) => sum + m.confidence, 0) / memories.length; + + // Oldest and newest + const sortedByDate = [...memories].sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp)); + stats.oldestMemory = sortedByDate[0]; + stats.newestMemory = sortedByDate[sortedByDate.length - 1]; + } + + return stats; + } catch (error) { + console.error("Error getting memory stats:", error); + return { total: 0, byCategory: {}, averageConfidence: 0 }; + } + } + + // MEMORY TOGGLE + async toggleMemorySystem(enabled) { + this.memoryEnabled = enabled; + if (this.db) { + await this.db.setPreference("memorySystemEnabled", enabled); + } + } + + // EXPORT/IMPORT MEMORIES + async exportMemories() { + try { + const memories = await this.getAllMemories(); + return { + exportDate: new Date().toISOString(), + character: this.selectedCharacter, + memories: memories, + version: "1.0" + }; + } catch (error) { + console.error("Error exporting memories:", error); + return null; + } + } + + async importMemories(importData) { + if (!importData || !importData.memories) return false; + + try { + for (const memory of importData.memories) { + await this.addMemory({ + ...memory, + type: "imported", + character: this.selectedCharacter + }); + } + return true; + } catch (error) { + console.error("Error importing memories:", error); + return false; + } + } + + // MIGRATION UTILITIES + async migrateIncompatibleIDs() { + if (!this.db) return false; + + try { + console.log("🔧 Début de la migration des IDs incompatibles..."); + + // Récupérer toutes les mémoires + const allMemories = await this.db.db.memories.toArray(); + console.log(`📊 ${allMemories.length} mémoires trouvées`); + + const incompatibleMemories = allMemories.filter(memory => { + // Les IDs auto-increment sont des entiers séquentiels (1, 2, 3...) + // Les anciens IDs manuels sont des nombres très grands (timestamps) + return memory.id > 10000; // Seuil arbitraire pour détecter les anciens IDs + }); + + if (incompatibleMemories.length === 0) { + console.log("✅ Aucune migration nécessaire"); + return true; + } + + console.log(`🔄 Migration de ${incompatibleMemories.length} mémoires avec IDs incompatibles`); + + // Sauvegarder les données avant suppression + const dataToMigrate = incompatibleMemories.map(memory => { + const { id, ...memoryData } = memory; // Enlever l'ancien ID + return memoryData; + }); + + // Supprimer les anciennes entrées + await this.db.db.memories.bulkDelete(incompatibleMemories.map(m => m.id)); + + // Réinsérer avec de nouveaux IDs auto-générés + const newIds = await this.db.db.memories.bulkAdd(dataToMigrate); + + console.log(`✅ Migration terminée. Nouveaux IDs:`, newIds); + return true; + } catch (error) { + console.error("❌ Erreur lors de la migration:", error); + return false; + } + } + + // Background migration: populate keywords for all existing memories if missing + async populateKeywordsForAllMemories() { + if (!this.db || !this.db.db.memories) return false; + try { + console.log("🔧 Starting background keyword population..."); + const all = await this.db.db.memories.toArray(); + const ops = []; + for (const mem of all) { + if (!mem.keywords || !Array.isArray(mem.keywords) || mem.keywords.length === 0) { + const keys = this.deriveKeywords(mem.content || ""); + ops.push(this.db.db.memories.update(mem.id, { keywords: keys })); + } + // batch in small chunks to avoid blocking + if (ops.length >= 50) { + await Promise.all(ops); + ops.length = 0; + } + } + if (ops.length) await Promise.all(ops); + console.log("✅ Keyword population complete"); + return true; + } catch (e) { + console.warn("Error populating keywords", e); + return false; + } + } +} + +window.KimiMemorySystem = KimiMemorySystem; +export default KimiMemorySystem; diff --git a/kimi-js/kimi-memory-ui.js b/kimi-js/kimi-memory-ui.js new file mode 100644 index 0000000000000000000000000000000000000000..a23564315859227f0d582f8a21efb2da8ec68830 --- /dev/null +++ b/kimi-js/kimi-memory-ui.js @@ -0,0 +1,966 @@ +// ===== KIMI MEMORY UI MANAGER ===== +class KimiMemoryUI { + constructor() { + this.memorySystem = null; + this.isInitialized = false; + // Debounce helpers for UI refresh to coalesce multiple DB reads + this._debounceTimers = {}; + } + + debounce(key, fn, wait = 350) { + if (this._debounceTimers[key]) clearTimeout(this._debounceTimers[key]); + this._debounceTimers[key] = setTimeout(() => { + fn(); + delete this._debounceTimers[key]; + }, wait); + } + + async init() { + if (!window.kimiMemorySystem) { + console.warn("Memory system not available"); + return; + } + + this.memorySystem = window.kimiMemorySystem; + this.setupEventListeners(); + await this.updateMemoryStats(); + this.isInitialized = true; + } + + setupEventListeners() { + // Memory toggle + const memoryToggle = document.getElementById("memory-toggle"); + if (memoryToggle) { + memoryToggle.addEventListener("click", () => this.toggleMemorySystem()); + } + + // View memories button + const viewMemoriesBtn = document.getElementById("view-memories"); + if (viewMemoriesBtn) { + viewMemoriesBtn.addEventListener("click", () => this.openMemoryModal()); + } + + // Add memory button + const addMemoryBtn = document.getElementById("add-memory"); + if (addMemoryBtn) { + addMemoryBtn.addEventListener("click", () => { + this.addManualMemory(); + ensureVideoNeutralOnUIChange(); + }); + } + + // Memory modal close + const memoryClose = document.getElementById("memory-close"); + if (memoryClose) { + memoryClose.addEventListener("click", () => { + this.closeMemoryModal(); + ensureVideoNeutralOnUIChange(); + }); + } + + // Memory export + const memoryExport = document.getElementById("memory-export"); + if (memoryExport) { + memoryExport.addEventListener("click", () => this.exportMemories()); + } + + // Memory filter + const memoryFilter = document.getElementById("memory-filter-category"); + if (memoryFilter) { + memoryFilter.addEventListener("change", () => { + this.filterMemories(); + ensureVideoNeutralOnUIChange(); + }); + } + + // Memory search + const memorySearch = document.getElementById("memory-search"); + if (memorySearch) { + memorySearch.addEventListener("input", () => this.filterMemories()); + } + + // Close modal on overlay click + const memoryOverlay = document.getElementById("memory-overlay"); + if (memoryOverlay) { + memoryOverlay.addEventListener("click", e => { + if (e.target === memoryOverlay) { + this.closeMemoryModal(); + } + }); + } + + // Delegated handler for memory-source clicks / touch / keyboard + const memoryList = document.getElementById("memory-list"); + if (memoryList) { + // Click and touch + memoryList.addEventListener("click", e => this.handleMemorySourceToggle(e)); + memoryList.addEventListener("touchstart", e => this.handleMemorySourceToggle(e)); + + // General delegated click handler for memory actions (summarize, etc.) + memoryList.addEventListener("click", e => this.handleMemoryListClick(e)); + + // Keyboard accessibility: Enter / Space when focused on .memory-source + memoryList.addEventListener("keydown", e => { + const target = e.target; + if (target && target.classList && target.classList.contains("memory-source")) { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + this.toggleSourceContentForElement(target); + } + } + }); + } + } + + // Delegated click handler for actions inside the memory list + async handleMemoryListClick(e) { + try { + const summarizeBtn = e.target.closest && e.target.closest("#memory-summarize-btn"); + if (summarizeBtn) { + e.stopPropagation(); + await this.handleSummarizeAction(); + return; + } + } catch (err) { + console.error("Error handling memory list click", err); + } + } + + async toggleMemorySystem() { + if (!this.memorySystem) return; + + const toggle = document.getElementById("memory-toggle"); + const enabled = !this.memorySystem.memoryEnabled; + + await this.memorySystem.toggleMemorySystem(enabled); + + if (toggle) { + toggle.setAttribute("aria-checked", enabled.toString()); + toggle.classList.toggle("active", enabled); + } + + // Show feedback + this.showFeedback(enabled ? "Memory system enabled" : "Memory system disabled"); + } + + async addManualMemory() { + const categorySelect = document.getElementById("memory-category"); + const contentInput = document.getElementById("memory-content"); + + if (!categorySelect || !contentInput) return; + + const category = categorySelect.value; + const content = contentInput.value.trim(); + + if (!content) { + this.showFeedback("Please enter memory content", "error"); + return; + } + + try { + await this.memorySystem.addMemory({ + category: category, + content: content, + type: "manual", + confidence: 1.0 + }); + + contentInput.value = ""; + await this.updateMemoryStats(); + this.showFeedback("Memory added successfully"); + } catch (error) { + console.error("Error adding memory:", error); + this.showFeedback("Error adding memory", "error"); + } + } + + async openMemoryModal() { + const overlay = document.getElementById("memory-overlay"); + if (!overlay) return; + + overlay.style.display = "flex"; + await this.loadMemories(); + } + + closeMemoryModal() { + const overlay = document.getElementById("memory-overlay"); + if (overlay) { + overlay.style.display = "none"; + // Ensure background video resumes after closing memory modal + const kv = window.kimiVideo; + if (kv && kv.activeVideo) { + try { + const v = kv.activeVideo; + if (v.ended) { + if (typeof kv.returnToNeutral === "function") kv.returnToNeutral(); + } else if (v.paused) { + // Use centralized video utility for play + window.KimiVideoManager.getVideoElement(v) + .play() + .catch(() => { + if (typeof kv.returnToNeutral === "function") kv.returnToNeutral(); + }); + } + } catch {} + } + } + } + + async loadMemories() { + if (!this.memorySystem) return; + + try { + // Use debounce to avoid multiple rapid DB reads + this.debounce("loadMemories", async () => { + const memories = await this.memorySystem.getAllMemories(); + console.log("Loading memories into UI:", memories.length); + this.renderMemories(memories); + }); + } catch (error) { + console.error("Error loading memories:", error); + } + } + + async filterMemories() { + const filterSelect = document.getElementById("memory-filter-category"); + const searchInput = document.getElementById("memory-search"); + if (!this.memorySystem) return; + + try { + const category = filterSelect?.value; + const searchTerm = searchInput?.value.toLowerCase().trim(); + let memories; + + if (category) { + memories = await this.memorySystem.getMemoriesByCategory(category); + } else { + memories = await this.memorySystem.getAllMemories(); + } + + // Apply search filter if search term exists + if (searchTerm) { + memories = memories.filter( + memory => + memory.content.toLowerCase().includes(searchTerm) || + memory.category.toLowerCase().includes(searchTerm) || + (memory.sourceText && memory.sourceText.toLowerCase().includes(searchTerm)) + ); + } + + this.renderMemories(memories); + } catch (error) { + console.error("Error filtering memories:", error); + } + } + + renderMemories(memories) { + const memoryList = document.getElementById("memory-list"); + if (!memoryList) return; + + console.log("Rendering memories:", memories); // Debug logging + + if (memories.length === 0) { + memoryList.innerHTML = ` +

+ +

No memories found. Start chatting to build memories automatically, or add them manually.

+
+ `; + return; + } + + // Group memories by category for better organization + const groupedMemories = memories.reduce((groups, memory) => { + const category = memory.category || "other"; + if (!groups[category]) groups[category] = []; + groups[category].push(memory); + return groups; + }, {}); + + let html = ""; + + // Toolbar with summarize action + html += ` +
+ +
+ `; + Object.entries(groupedMemories).forEach(([category, categoryMemories]) => { + html += ` +
+

+ ${this.getCategoryIcon(category)} ${this.formatCategoryName(category)} + (${categoryMemories.length}) +

+
+ `; + + categoryMemories.forEach(memory => { + const confidence = Math.round(memory.confidence * 100); + const isAutomatic = memory.type === "auto_extracted"; + const previewLength = 120; + const isLongContent = memory.content.length > previewLength; + const previewText = isLongContent ? memory.content.substring(0, previewLength) + "..." : memory.content; + const wordCount = memory.content.split(/\s+/).length; + const importance = typeof memory.importance === "number" ? memory.importance : 0.5; + const importanceLevel = this.getImportanceLevelFromValue(importance); + const importancePct = Math.round(importance * 100); + const tagsHtml = this.renderTags(memory.tags || []); + + html += ` +
+
+
${window.KimiValidationUtils && window.KimiValidationUtils.escapeHtml ? window.KimiValidationUtils.escapeHtml(memory.title || "") : memory.title || ""}${!memory.title ? "" : ""}
+
+ ${memory.type === "auto_extracted" ? "🤖 Auto" : "✋ Manual"} + ${confidence}% + ${memory.type === "summary" || (memory.tags && memory.tags.includes("summary")) ? `Summary` : ""} + ${isLongContent ? `${wordCount} mots` : ""} + ${importanceLevel.charAt(0).toUpperCase() + importanceLevel.slice(1)} +
+
+ ${ + !memory.title + ? ` +
+
+ ${this.highlightMemoryContent(previewText)} +
+ ${ + isLongContent + ? ` + + + ` + : "" + } +
+ ` + : "" + } + ${tagsHtml} +
+ ${this.formatDate( + // Robust fallback chain: createdAt -> timestamp -> lastAccess -> lastModified -> Date.now() + memory.createdAt || memory.timestamp || memory.lastAccess || memory.lastModified || Date.now() + )} + ${ + memory.sourceText + ? `` + : `` + } +
+ ${ + memory.sourceText + ? `` + : "" + } +
+ + +
+
+ `; + }); + + html += ` +
+
+ `; + }); + + // Minimal runtime guard: block accidental