├── LICENSE ├── README.md ├── about.js ├── app.js ├── character-continuation-modal.js ├── confirmation-modal-fix.js ├── css ├── character-gallery.css ├── character-modal.css ├── code-blocks.css ├── continue-with-character.css ├── modern-ui.css ├── performance-optimizations.css ├── settings-modal.css ├── system-prompt-overlay.css └── theme-variables.css ├── delete-all-chats.js ├── empty-message-modal.js ├── external-site-confirmation-modal.js ├── file.png ├── help.js ├── icon.png ├── icon22.png ├── index.html ├── js ├── api-service.js ├── character-gallery-touch-handler.js ├── character-gallery.js ├── character-manager.js ├── character-modal-touch-handler.js ├── chat-history-touch-handler.js ├── chat-service.js ├── dom-elements.js ├── event-handlers.js ├── export-import.js ├── file-upload.js ├── main.js ├── marked-bundle.js ├── marked-nomodule.js ├── model-manager.js ├── monaco-performance.js ├── performance-utils.js ├── reset-app.js ├── settings-manager.js ├── settings-modal-manager.js ├── settings-modal-touch-handler.js ├── shared-state.js ├── sidebar-touch-handler.js ├── touch-handlers.js ├── ui-manager.js ├── utils.js └── whats-new.js ├── reasoning.js ├── splash2.png └── styles.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 techcow2 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📱 LMSA - LM Studio Android 2 | 3 | ![Status: Open Source](https://img.shields.io/badge/Status-Open%20Source-brightgreen) 4 | ![Version: Current](https://img.shields.io/badge/Version-Current-green) 5 | ![Release: Community](https://img.shields.io/badge/Release-Community%20Driven-blue) 6 | 7 | ## 📸 Screenshots 8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 |

16 | 17 | ## 📋 What is LMSA? 18 | 19 | LMSA (LM Studio Assistant) is an Android front-end application for LM Studio that provides a clean, user-friendly interface to interact with language models on your Android device. It's designed with privacy in mind, offering a tracking-free and ad-free experience for users who want to leverage the power of large language models on mobile. 20 | 21 | ### Key Functionality: 22 | - Connect to LM Studio running on your computer 23 | - Chat with AI models through a mobile-optimized interface 24 | - Upload and analyze documents using AI 25 | - Customize AI behavior with system prompts and temperature settings 26 | 27 | ## 🔓 Project Status 28 | 29 | This project has transitioned to **open source** development. 30 | 31 | ### Development Information 32 | 33 | The project is now community-driven and open source. Development efforts are focused on collaborative community contributions, with regular updates and improvements available through the Google Play Store. 34 | 35 | ## 📱 Version Comparison 36 | 37 | ### Community Release 38 | 39 | The current version is available through Google Play Store: 40 | 41 | | Get it on Google Play | **Google Play Store**
- Official distribution
- Automatic updates
- Easy installation
- Verified security
- Community-driven updates | 42 | |:---|:---| 43 | 44 | ## 🌟 Features Overview 45 | 46 | ### Privacy & Security 47 | - **Community updates** improving app security and stability 48 | - **Privacy-focused design** with no unnecessary tracking 49 | - **Secure connections** to your LM Studio instance 50 | - **Privacy First** - Your conversations never leave your personal network as models run locally 51 | 52 | ### AI Capabilities 53 | - **Document analysis** - Chat with your files 54 | - **Custom instructions** - Personalize AI responses 55 | - **Precision tuning** - Adjust response creativity and accuracy 56 | - **System prompt** - Set context for more relevant responses 57 | - **AI Reasoning Visibility** - Toggle the ability to see the model's "thinking" process before generating responses 58 | - **File Processing** - Upload and process files to include in your prompts for more context-aware responses 59 | 60 | ### User Experience 61 | - **Clean interface** - Intuitive design for effortless interaction 62 | - **Model information** - View details about the loaded AI model 63 | - **Tablet optimization** - Enhanced layout for larger screens 64 | - **Sidebar navigation** - Quick access to conversations and settings 65 | - **Dark Mode Support** - Chat comfortably day or night with full UI theme support 66 | - **Mobile-Responsive Design** - Optimized for both phones and tablets of various screen sizes 67 | - **Quick Navigation** - "Scroll to Bottom" button for faster movement in long chats 68 | 69 | ### Conversation Management 70 | - **Multiple Conversations** - Create and manage separate chat threads for different topics 71 | - **Comprehensive History** - Save, browse, and continue previous conversations with organized chat management 72 | - **Automatic Titles** - Saved chats are automatically titled for easy identification 73 | - **Import/Export** - Easily backup and transfer your saved conversations between devices 74 | - **Response Management** - Copy, regenerate, or modify AI responses with ease 75 | 76 | ### Connection & Configuration 77 | - **Simple Connection** - Connect to your LM Studio server with just an IP address and port 78 | - **Advanced Customization** - Adjust temperature, system prompts, and other model parameters directly from your phone 79 | 80 | ## 💻 How It Works 81 | 82 | 1. Start LM Studio on your computer and load your favorite language model 83 | 2. Activate the server feature in LM Studio (usually on port 1234) 84 | 3. Connect the Android app to your computer using your local network 85 | 4. Start chatting with your AI models from anywhere in your home 86 | 87 | LMSA connects to LM Studio running on your computer, allowing you to: 88 | - Access powerful AI language models from your mobile device 89 | - Chat with AI models using a simple, intuitive interface 90 | - Upload documents for the AI to analyze and discuss 91 | - Customize AI behavior through temperature and system prompt settings 92 | 93 | ## 👥 Perfect For 94 | 95 | - AI enthusiasts who want to access their models on the go 96 | - Privacy-conscious users who prefer keeping their data local 97 | - Developers testing different prompts and model responses 98 | - Researchers who need to include file content in their AI interactions 99 | - Anyone who wants the convenience of mobile access to powerful AI 100 | 101 | ## 🔧 Technical Requirements 102 | 103 | - Android 8.0 or higher 104 | - LM Studio installed and running on a computer with a suitable language model 105 | - Both devices connected to the same network 106 | 107 | ## 🚀 Get Started Today 108 | 109 | 1. Download and install the app from Google Play Store 110 | 2. Start LM Studio and load a model on your computer 111 | 3. Start the LM Studio server 112 | 4. Open the app and enter the server IP and port in Settings 113 | 5. Begin chatting immediately! 114 | 115 | ## 📥 Download Information 116 | 117 | ### Available on Google Play Store 118 | 119 |

120 | 121 | Get it on Google Play 122 | 123 |

124 | 125 | **LMSA - LM Studio Android**
126 | **Publisher:** LMSA Community
127 | **Updates:** Regular community-driven feature and maintenance updates
128 | [**Download Now →**](https://play.google.com/store/apps/details?id=com.lmsa.app) 129 | 130 | ## ⚠️ Disclaimer 131 | 132 | LMSA is a third-party application and is not affiliated with LM Studio or its developers. This app is independently developed to provide an Android front-end interface for interacting with LM Studio. Use of this app is at your own discretion, and the developers of LMSA are not responsible for any issues arising from its use. 133 | 134 | --- 135 | 136 | © 2025 TechRay Apps LLC. All rights reserved 137 | -------------------------------------------------------------------------------- /about.js: -------------------------------------------------------------------------------- 1 | // Import the checkAndShowWelcomeMessage function 2 | import { checkAndShowWelcomeMessage } from './js/ui-manager.js'; 3 | import { showExternalSiteModal } from './external-site-confirmation-modal.js'; 4 | 5 | // Get DOM elements 6 | const aboutButtonElement = document.getElementById('about-btn'); 7 | const aboutModal = document.getElementById('about-modal'); 8 | const closeAboutButton = document.getElementById('close-about'); 9 | const sidebarElement = document.getElementById('sidebar'); 10 | const modalContent = aboutModal ? aboutModal.querySelector('.modal-content') : null; 11 | const openHelpLink = document.getElementById('open-help-link'); 12 | const helpModal = document.getElementById('help-modal'); 13 | const rateAppLink = document.getElementById('rate-app-link'); 14 | const officialWebsiteLink = document.getElementById('official-website-link'); 15 | 16 | // Function to close sidebar 17 | function closeSidebar() { 18 | if (sidebarElement) { 19 | sidebarElement.classList.add('hidden'); 20 | sidebarElement.classList.remove('active'); 21 | document.body.classList.remove('sidebar-open'); 22 | 23 | // Also close the options container 24 | const optionsContainer = document.getElementById('options-container'); 25 | if (optionsContainer) { 26 | optionsContainer.classList.add('hidden'); 27 | optionsContainer.classList.remove('animate-fade-in'); 28 | } 29 | 30 | // Remove the sidebar overlay 31 | const sidebarOverlay = document.getElementById('sidebar-overlay'); 32 | if (sidebarOverlay) { 33 | sidebarOverlay.classList.remove('active'); 34 | sidebarOverlay.classList.add('hidden'); 35 | } 36 | 37 | // Collapse all sections when sidebar is closed 38 | const sectionHeaders = sidebarElement.querySelectorAll('.section-header'); 39 | const chatHistorySection = sidebarElement.querySelector('.sidebar-section:last-child'); 40 | sectionHeaders.forEach(header => { 41 | header.classList.remove('active'); 42 | const content = header.nextElementSibling; 43 | if (content && content.classList.contains('collapsible-content')) { 44 | content.classList.remove('show'); 45 | } 46 | }); 47 | 48 | // Ensure chat history is visible when sidebar is closed 49 | if (chatHistorySection) { 50 | chatHistorySection.classList.remove('chat-history-hidden'); 51 | } 52 | } 53 | } 54 | 55 | // About button click handler is now managed in js/event-handlers.js 56 | // This prevents duplicate event handlers and conflicts 57 | 58 | // Close About modal button handler 59 | if (closeAboutButton) { 60 | closeAboutButton.addEventListener('click', () => { 61 | if (aboutModal) { 62 | modalContent.classList.add('animate-modal-out'); 63 | setTimeout(() => { 64 | aboutModal.classList.add('hidden'); 65 | modalContent.classList.remove('animate-modal-out'); 66 | 67 | // Check if welcome message should be shown 68 | checkAndShowWelcomeMessage(); 69 | }, 300); 70 | } 71 | }); 72 | } 73 | 74 | // Open help link click handler 75 | if (openHelpLink && helpModal) { 76 | openHelpLink.addEventListener('click', (e) => { 77 | e.preventDefault(); 78 | 79 | // Close the about modal first 80 | if (aboutModal) { 81 | modalContent.classList.add('animate-modal-out'); 82 | setTimeout(() => { 83 | aboutModal.classList.add('hidden'); 84 | modalContent.classList.remove('animate-modal-out'); 85 | 86 | // Then open the help modal 87 | helpModal.classList.remove('hidden'); 88 | const helpModalContent = helpModal.querySelector('.modal-content'); 89 | if (helpModalContent) { 90 | helpModalContent.classList.add('animate-modal-in'); 91 | 92 | // Reset scroll position to top 93 | const scrollableContent = helpModal.querySelector('.overflow-y-auto'); 94 | if (scrollableContent) { 95 | scrollableContent.scrollTop = 0; 96 | } 97 | 98 | setTimeout(() => { 99 | helpModalContent.classList.remove('animate-modal-in'); 100 | }, 300); 101 | } 102 | }, 300); 103 | } 104 | }); 105 | } 106 | 107 | // Rate app link click handler 108 | if (rateAppLink) { 109 | rateAppLink.addEventListener('click', (e) => { 110 | e.preventDefault(); 111 | 112 | // Add a visual feedback effect when clicked 113 | rateAppLink.classList.add('active-scale'); 114 | 115 | // Short delay before showing confirmation modal for visual feedback 116 | setTimeout(() => { 117 | // Show confirmation modal before opening Google Play Store 118 | const googlePlayUrl = 'https://play.google.com/store/apps/details?id=com.lmsa.app&pcampaignid=web_share'; 119 | showExternalSiteModal(googlePlayUrl); 120 | 121 | // Remove the active scale class 122 | rateAppLink.classList.remove('active-scale'); 123 | }, 200); 124 | }); 125 | } 126 | 127 | // Official website link click handler 128 | if (officialWebsiteLink) { 129 | officialWebsiteLink.addEventListener('click', (e) => { 130 | e.preventDefault(); 131 | 132 | // Add a visual feedback effect when clicked 133 | officialWebsiteLink.classList.add('active-scale'); 134 | 135 | // Short delay before showing confirmation modal for visual feedback 136 | setTimeout(() => { 137 | // Show confirmation modal before opening official website 138 | const officialWebsiteUrl = 'https://lmsa.app'; 139 | showExternalSiteModal(officialWebsiteUrl); 140 | 141 | // Remove the active scale class 142 | officialWebsiteLink.classList.remove('active-scale'); 143 | }, 200); 144 | }); 145 | } -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Main application entry point 2 | // This file has been refactored into a modular structure 3 | // All functionality is now imported from the js/ directory 4 | 5 | // Import the main module which initializes everything 6 | import './js/main.js'; 7 | 8 | // Add event listeners for the sidebar overlay 9 | document.addEventListener('DOMContentLoaded', function() { 10 | const sidebarOverlay = document.getElementById('sidebar-overlay'); 11 | if (sidebarOverlay) { 12 | // Click event for desktop 13 | sidebarOverlay.addEventListener('click', function() { 14 | // Import the toggleSidebar function dynamically 15 | import('./js/ui-manager.js').then(module => { 16 | module.toggleSidebar(); 17 | }); 18 | }); 19 | 20 | // Touch event for tablets and mobile 21 | sidebarOverlay.addEventListener('touchend', function(e) { 22 | e.preventDefault(); // Prevent any default behavior 23 | // Import the toggleSidebar function dynamically 24 | import('./js/ui-manager.js').then(module => { 25 | module.toggleSidebar(); 26 | }); 27 | }, { passive: false }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /character-continuation-modal.js: -------------------------------------------------------------------------------- 1 | // Character Continuation Modal 2 | // This module handles the character continuation modal functionality 3 | 4 | import { getActiveCharacter } from './js/character-manager.js'; 5 | import { createNewChat } from './js/chat-service.js'; 6 | import { checkAndShowWelcomeMessage } from './js/ui-manager.js'; 7 | import { handleContinueWithCharacter as continueWithCharacterHandler } from './js/event-handlers.js'; 8 | 9 | // DOM elements 10 | let characterContinuationModal; 11 | let closeCharacterContinuationModalButton; 12 | let newChatWithoutCharacterButton; 13 | let continueWithCharacterButton; 14 | let characterContinuationName; 15 | let characterContinuationDescription; 16 | let characterContinuationAvatar; 17 | 18 | /** 19 | * Initializes the character continuation modal 20 | */ 21 | function initializeCharacterContinuationModal() { 22 | // Get DOM elements 23 | characterContinuationModal = document.getElementById('character-continuation-modal'); 24 | closeCharacterContinuationModalButton = document.getElementById('close-character-continuation-modal'); 25 | newChatWithoutCharacterButton = document.getElementById('new-chat-without-character'); 26 | continueWithCharacterButton = document.getElementById('continue-with-character-btn'); 27 | characterContinuationName = document.getElementById('character-continuation-name'); 28 | characterContinuationDescription = document.getElementById('character-continuation-description'); 29 | characterContinuationAvatar = document.getElementById('character-continuation-avatar'); 30 | 31 | // Add event listeners 32 | if (closeCharacterContinuationModalButton) { 33 | closeCharacterContinuationModalButton.addEventListener('click', hideCharacterContinuationModal); 34 | } 35 | 36 | if (newChatWithoutCharacterButton) { 37 | newChatWithoutCharacterButton.addEventListener('click', handleNewChatWithoutCharacter); 38 | } 39 | 40 | if (continueWithCharacterButton) { 41 | continueWithCharacterButton.addEventListener('click', handleContinueWithCharacter); 42 | } 43 | 44 | // Close modal when clicking outside 45 | if (characterContinuationModal) { 46 | characterContinuationModal.addEventListener('click', (e) => { 47 | if (e.target === characterContinuationModal) { 48 | hideCharacterContinuationModal(); 49 | } 50 | }); 51 | } 52 | 53 | // Override the new chat button click event 54 | const newChatButton = document.getElementById('new-chat'); 55 | if (newChatButton) { 56 | // Remove any existing event listeners by cloning and replacing the element 57 | const newChatButtonClone = newChatButton.cloneNode(true); 58 | newChatButton.parentNode.replaceChild(newChatButtonClone, newChatButton); 59 | 60 | // Keep a reference to the original background 61 | const originalBackground = 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)'; 62 | const activeBackground = 'linear-gradient(135deg, #4f46e5 0%, #4338ca 100%)'; 63 | 64 | // Track whether we're currently touching the button 65 | let isTouching = false; 66 | 67 | // Regular click event - handled last to ensure it doesn't interfere with touch 68 | newChatButtonClone.addEventListener('click', (e) => { 69 | if (!isTouching) { // Only handle click if not from a touch event 70 | handleNewChatButtonClick(); 71 | } 72 | }); 73 | 74 | // Touch start - change to active state 75 | newChatButtonClone.addEventListener('touchstart', (e) => { 76 | isTouching = true; 77 | newChatButtonClone.style.background = activeBackground; 78 | newChatButtonClone.style.transform = 'translateY(1px)'; 79 | newChatButtonClone.style.boxShadow = '0 2px 8px rgba(79, 70, 229, 0.3)'; 80 | }, { passive: true }); 81 | 82 | // Touch end - restore original state and handle the action 83 | newChatButtonClone.addEventListener('touchend', (e) => { 84 | // Prevent default browser behavior that might cause stuck states 85 | e.preventDefault(); 86 | 87 | // Reset visual state immediately 88 | newChatButtonClone.style.background = originalBackground; 89 | newChatButtonClone.style.transform = 'none'; 90 | newChatButtonClone.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)'; 91 | 92 | // Force blur 93 | newChatButtonClone.blur(); 94 | 95 | // Handle the action 96 | handleNewChatButtonClick(); 97 | 98 | // Reset touch state after a short delay 99 | setTimeout(() => { 100 | isTouching = false; 101 | }, 300); 102 | }, { passive: false }); 103 | 104 | // Touch cancel - just restore original state 105 | newChatButtonClone.addEventListener('touchcancel', (e) => { 106 | // Reset visual state immediately 107 | newChatButtonClone.style.background = originalBackground; 108 | newChatButtonClone.style.transform = 'none'; 109 | newChatButtonClone.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)'; 110 | 111 | // Force blur 112 | newChatButtonClone.blur(); 113 | 114 | // Reset touch state 115 | isTouching = false; 116 | }, { passive: true }); 117 | 118 | // Handle leaving the button area during touch 119 | newChatButtonClone.addEventListener('touchmove', (e) => { 120 | // Check if touch has moved out of the button 121 | const touch = e.touches[0]; 122 | const buttonRect = newChatButtonClone.getBoundingClientRect(); 123 | 124 | if (touch.clientX < buttonRect.left || touch.clientX > buttonRect.right || 125 | touch.clientY < buttonRect.top || touch.clientY > buttonRect.bottom) { 126 | // Touch moved outside button - reset visual state 127 | newChatButtonClone.style.background = originalBackground; 128 | newChatButtonClone.style.transform = 'none'; 129 | newChatButtonClone.style.boxShadow = '0 4px 12px rgba(37, 99, 235, 0.3)'; 130 | } 131 | }, { passive: true }); 132 | } 133 | } 134 | 135 | /** 136 | * Handles the new chat button click 137 | * If a character is active, shows the character continuation modal 138 | * Otherwise, creates a new chat 139 | */ 140 | export function handleNewChatButtonClick() { 141 | // Force blur on active element to prevent any stuck focus states 142 | if (document.activeElement) { 143 | document.activeElement.blur(); 144 | } 145 | 146 | // Get the button again to ensure we're working with the current DOM element 147 | const button = document.getElementById('new-chat'); 148 | if (button) { 149 | // Reset any CSS classes that might be keeping the button in a highlighted state 150 | button.classList.remove('active', 'touch-active', 'focus', 'focus-visible'); 151 | } 152 | 153 | // Check if the modal is already visible 154 | if (characterContinuationModal && !characterContinuationModal.classList.contains('hidden')) { 155 | // Modal is already open, close it and create a new chat without character 156 | hideCharacterContinuationModal(); 157 | 158 | // Import character-manager to explicitly clear the active character 159 | import('./js/character-manager.js').then(module => { 160 | // First clear the active character 161 | module.clearActiveCharacter(); 162 | 163 | // Then create a new chat without character 164 | createNewChat(false); 165 | }); 166 | return; 167 | } 168 | 169 | const activeCharacter = getActiveCharacter(); 170 | 171 | if (activeCharacter) { 172 | // Show the character continuation modal 173 | showCharacterContinuationModal(activeCharacter); 174 | } else { 175 | // No active character, just create a new chat 176 | createNewChat(); 177 | } 178 | } 179 | 180 | /** 181 | * Shows the character continuation modal 182 | * @param {Object} character - The active character data 183 | */ 184 | export function showCharacterContinuationModal(character) { 185 | if (!characterContinuationModal) return; 186 | 187 | // Update the modal with character information 188 | if (characterContinuationName) { 189 | characterContinuationName.textContent = character.name; 190 | } 191 | 192 | if (characterContinuationDescription) { 193 | characterContinuationDescription.textContent = character.description || 'No description'; 194 | } 195 | 196 | // Update the avatar 197 | if (characterContinuationAvatar) { 198 | characterContinuationAvatar.innerHTML = ''; 199 | if (character.image) { 200 | const img = document.createElement('img'); 201 | img.src = character.image; 202 | img.className = 'w-full h-full object-cover'; 203 | characterContinuationAvatar.appendChild(img); 204 | } else { 205 | const icon = document.createElement('i'); 206 | icon.className = 'fas fa-user text-white'; 207 | characterContinuationAvatar.appendChild(icon); 208 | } 209 | } 210 | 211 | // Show the modal with animation 212 | characterContinuationModal.classList.remove('hidden'); 213 | 214 | // Add animation class 215 | const modalContent = characterContinuationModal.querySelector('.modal-content'); 216 | if (modalContent) { 217 | modalContent.classList.add('animate-modal-in'); 218 | setTimeout(() => { 219 | modalContent.classList.remove('animate-modal-in'); 220 | }, 300); 221 | } 222 | } 223 | 224 | /** 225 | * Hides the character continuation modal 226 | */ 227 | function hideCharacterContinuationModal() { 228 | if (!characterContinuationModal) return; 229 | 230 | // Add animation class 231 | const modalContent = characterContinuationModal.querySelector('.modal-content'); 232 | if (modalContent) { 233 | modalContent.classList.add('animate-modal-out'); 234 | setTimeout(() => { 235 | characterContinuationModal.classList.add('hidden'); 236 | modalContent.classList.remove('animate-modal-out'); 237 | 238 | // Check if welcome message should be shown 239 | checkAndShowWelcomeMessage(); 240 | }, 300); 241 | } else { 242 | characterContinuationModal.classList.add('hidden'); 243 | 244 | // Check if welcome message should be shown 245 | checkAndShowWelcomeMessage(); 246 | } 247 | } 248 | 249 | /** 250 | * Handles the "New Chat without Character" button click 251 | * Creates a new chat without the character 252 | */ 253 | function handleNewChatWithoutCharacter() { 254 | // Hide the modal first 255 | hideCharacterContinuationModal(); 256 | 257 | // Import character-manager to explicitly clear the active character 258 | import('./js/character-manager.js').then(module => { 259 | // First clear the active character 260 | module.clearActiveCharacter(); 261 | 262 | // Then create a new chat without character 263 | createNewChat(false); 264 | }); 265 | } 266 | 267 | /** 268 | * Handles the "Continue with Character" button click 269 | * Creates a new chat while keeping the current character active 270 | */ 271 | function handleContinueWithCharacter() { 272 | // Hide the modal first 273 | hideCharacterContinuationModal(); 274 | 275 | // Use the imported handler function 276 | continueWithCharacterHandler(); 277 | } 278 | 279 | // Initialize the modal when the DOM is loaded 280 | document.addEventListener('DOMContentLoaded', initializeCharacterContinuationModal); 281 | -------------------------------------------------------------------------------- /confirmation-modal-fix.js: -------------------------------------------------------------------------------- 1 | // Import the checkAndShowWelcomeMessage function 2 | import { checkAndShowWelcomeMessage } from './js/ui-manager.js'; 3 | import { getLightThemeEnabled } from './js/settings-manager.js'; 4 | 5 | // Script to ensure the confirmation modal is properly hidden on page load 6 | document.addEventListener('DOMContentLoaded', function() { 7 | // Get the confirmation modal 8 | const confirmationModal = document.getElementById('confirmation-modal'); 9 | 10 | // Ensure it's properly hidden 11 | if (confirmationModal) { 12 | confirmationModal.classList.add('hidden'); 13 | confirmationModal.style.display = 'none'; 14 | 15 | // Set the z-index to ensure it appears on top of other modals when shown 16 | confirmationModal.style.zIndex = '1060'; // Higher than settings modal (1050) 17 | 18 | // Find the modal content and ensure it's also on top 19 | const modalContent = confirmationModal.querySelector('div:first-child'); 20 | if (modalContent) { 21 | modalContent.style.zIndex = '1061'; // Higher than the modal background 22 | } 23 | 24 | // Add event listener to the cancel button 25 | const cancelButton = document.getElementById('cancel-action'); 26 | if (cancelButton) { 27 | cancelButton.addEventListener('click', function() { 28 | confirmationModal.classList.add('hidden'); 29 | confirmationModal.style.display = 'none'; 30 | 31 | // Check if welcome message should be shown 32 | checkAndShowWelcomeMessage(); 33 | }); 34 | } 35 | 36 | // Add event listener to the modal background for closing when clicked outside 37 | confirmationModal.addEventListener('click', function(event) { 38 | if (event.target === confirmationModal) { 39 | confirmationModal.classList.add('hidden'); 40 | confirmationModal.style.display = 'none'; 41 | 42 | // Check if welcome message should be shown 43 | checkAndShowWelcomeMessage(); 44 | } 45 | }); 46 | } 47 | }); 48 | 49 | // Function to update the confirmation modal theme when the theme changes 50 | export function updateConfirmationModalTheme() { 51 | const confirmationModal = document.getElementById('confirmation-modal'); 52 | if (confirmationModal) { 53 | const modalContent = confirmationModal.querySelector('div:first-child'); 54 | const cancelButton = document.getElementById('cancel-action'); 55 | 56 | if (modalContent) { 57 | // Update modal background color based on current theme 58 | modalContent.style.backgroundColor = 'var(--bg-secondary)'; 59 | modalContent.style.color = 'var(--text-primary)'; 60 | } 61 | 62 | if (cancelButton) { 63 | // Update cancel button color based on current theme 64 | cancelButton.style.backgroundColor = 'var(--bg-tertiary)'; 65 | cancelButton.style.color = 'var(--text-primary)'; 66 | } 67 | } 68 | } 69 | 70 | // Function to update the export/import modals theme when the theme changes 71 | export function updateExportImportModalsTheme() { 72 | // Export Confirmation Modal 73 | const exportConfirmationModal = document.getElementById('export-confirmation-modal'); 74 | if (exportConfirmationModal) { 75 | const modalContent = exportConfirmationModal.querySelector('.modal-content'); 76 | const cancelButton = document.getElementById('cancel-export'); 77 | 78 | if (modalContent) { 79 | // Ensure the modal content uses theme variables 80 | modalContent.style.backgroundColor = 'var(--bg-secondary)'; 81 | modalContent.style.color = 'var(--text-primary)'; 82 | } 83 | 84 | if (cancelButton) { 85 | // Ensure the cancel button uses theme variables 86 | cancelButton.style.backgroundColor = 'var(--bg-tertiary)'; 87 | cancelButton.style.color = 'var(--text-primary)'; 88 | } 89 | } 90 | 91 | // Export Success Modal 92 | const exportSuccessModal = document.getElementById('export-success-modal'); 93 | if (exportSuccessModal) { 94 | const modalContent = exportSuccessModal.querySelector('.modal-content'); 95 | const successMessage = document.getElementById('export-success-message'); 96 | 97 | if (modalContent) { 98 | modalContent.style.backgroundColor = 'var(--bg-secondary)'; 99 | modalContent.style.color = 'var(--text-primary)'; 100 | } 101 | 102 | if (successMessage) { 103 | successMessage.style.color = 'var(--text-primary)'; 104 | } 105 | } 106 | 107 | // Import Modal 108 | const importModal = document.getElementById('import-modal'); 109 | if (importModal) { 110 | const modalContent = importModal.querySelector('.modal-content'); 111 | const cancelButton = document.getElementById('cancel-import'); 112 | const radioLabels = importModal.querySelectorAll('label span'); 113 | 114 | if (modalContent) { 115 | modalContent.style.backgroundColor = 'var(--bg-secondary)'; 116 | modalContent.style.color = 'var(--text-primary)'; 117 | } 118 | 119 | if (cancelButton) { 120 | cancelButton.style.backgroundColor = 'var(--bg-tertiary)'; 121 | cancelButton.style.color = 'var(--text-primary)'; 122 | } 123 | 124 | // Update text color for radio button labels 125 | radioLabels.forEach(span => { 126 | span.style.color = 'var(--text-primary)'; 127 | }); 128 | } 129 | 130 | // Import Success Modal 131 | const importSuccessModal = document.getElementById('import-success-modal'); 132 | if (importSuccessModal) { 133 | const modalContent = importSuccessModal.querySelector('.modal-content'); 134 | const successMessage = document.getElementById('import-success-message'); 135 | 136 | if (modalContent) { 137 | modalContent.style.backgroundColor = 'var(--bg-secondary)'; 138 | modalContent.style.color = 'var(--text-primary)'; 139 | } 140 | 141 | if (successMessage) { 142 | successMessage.style.color = 'var(--text-primary)'; 143 | } 144 | } 145 | } 146 | 147 | // Listen for theme changes 148 | document.addEventListener('themeChanged', () => { 149 | updateConfirmationModalTheme(); 150 | updateExportImportModalsTheme(); 151 | }); 152 | 153 | // Initialize the themes when the DOM is loaded 154 | document.addEventListener('DOMContentLoaded', () => { 155 | updateExportImportModalsTheme(); 156 | }); 157 | -------------------------------------------------------------------------------- /css/character-modal.css: -------------------------------------------------------------------------------- 1 | /* Character Modal Styles */ 2 | .character-modal { 3 | background: linear-gradient(135deg, var(--modern-bg-secondary), var(--modern-bg-primary)); 4 | border-radius: 1.25rem; 5 | border: 1px solid rgba(255, 255, 255, 0.12); 6 | box-shadow: var(--modern-shadow-lg), 0 0 20px rgba(0, 0, 0, 0.2); 7 | overflow: hidden; 8 | transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1), 9 | opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1), 10 | transform 0.4s cubic-bezier(0.16, 1, 0.3, 1); 11 | max-width: 550px; 12 | width: 95%; 13 | max-height: 90vh; 14 | display: flex; 15 | flex-direction: column; 16 | will-change: transform, opacity; /* Optimize for animations */ 17 | animation: slide-up 0.4s cubic-bezier(0.16, 1, 0.3, 1); 18 | } 19 | 20 | body.light-theme .character-modal { 21 | background: linear-gradient(135deg, var(--modern-light-bg-secondary), var(--modern-light-bg-primary)); 22 | border: 1px solid rgba(0, 0, 0, 0.1); 23 | box-shadow: var(--modern-shadow-lg), 0 0 20px rgba(0, 0, 0, 0.1); 24 | } 25 | 26 | .character-modal-header { 27 | background: linear-gradient(90deg, rgba(59, 130, 246, 0.12), transparent); 28 | border-bottom: 1px solid rgba(255, 255, 255, 0.12); 29 | padding: 1.15rem 1.5rem; 30 | display: flex; 31 | justify-content: space-between; 32 | align-items: center; 33 | } 34 | 35 | body.light-theme .character-modal-header { 36 | background: linear-gradient(90deg, rgba(59, 130, 246, 0.12), transparent); 37 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 38 | } 39 | 40 | .character-modal-title { 41 | font-weight: 700; 42 | font-size: 1.35rem; 43 | color: var(--modal-title); 44 | text-shadow: var(--title-shadow); 45 | letter-spacing: -0.02em; 46 | position: relative; 47 | display: inline-block; 48 | padding-bottom: 2px; 49 | } 50 | 51 | .character-modal-title::after { 52 | content: ''; 53 | position: absolute; 54 | bottom: -6px; 55 | left: 0; 56 | width: 40px; 57 | height: 3px; 58 | border-radius: 1.5px; 59 | background: linear-gradient(90deg, var(--modern-accent-primary), rgba(59, 130, 246, 0.5)); 60 | } 61 | 62 | .character-modal-close { 63 | background: transparent; 64 | border: none; 65 | color: var(--text-muted); 66 | font-size: 1.25rem; 67 | cursor: pointer; 68 | width: 2.25rem; 69 | height: 2.25rem; 70 | display: flex; 71 | align-items: center; 72 | justify-content: center; 73 | border-radius: 50%; 74 | transition: all 0.2s ease; 75 | } 76 | 77 | .character-modal-close:hover { 78 | color: var(--text-primary); 79 | transform: rotate(90deg); 80 | } 81 | 82 | .character-modal-body { 83 | padding: 1.5rem; 84 | overflow-y: auto; 85 | flex-grow: 1; 86 | -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ 87 | touch-action: pan-y; /* Allow vertical scrolling */ 88 | overscroll-behavior: contain; /* Prevent scroll chaining */ 89 | } 90 | 91 | .character-form-group { 92 | margin-bottom: 1.75rem; 93 | } 94 | 95 | .character-form-label { 96 | display: block; 97 | margin-bottom: 0.5rem; 98 | font-weight: 600; 99 | color: var(--text-secondary); 100 | font-size: 0.95rem; 101 | letter-spacing: 0.01em; 102 | } 103 | 104 | .character-form-input { 105 | width: 100%; 106 | padding: 0.85rem 1.15rem; 107 | border-radius: 0.75rem; 108 | background-color: rgba(0, 0, 0, 0.25); 109 | border: 1px solid rgba(255, 255, 255, 0.12); 110 | color: var(--text-primary); 111 | transition: all 0.3s ease; 112 | font-size: 0.95rem; 113 | } 114 | 115 | body.light-theme .character-form-input { 116 | background-color: rgba(0, 0, 0, 0.05); 117 | border: 1px solid rgba(0, 0, 0, 0.1); 118 | color: var(--modern-light-text-primary); 119 | } 120 | 121 | .character-form-input:hover { 122 | border-color: rgba(59, 130, 246, 0.3); 123 | background-color: rgba(0, 0, 0, 0.28); 124 | } 125 | 126 | body.light-theme .character-form-input:hover { 127 | background-color: rgba(0, 0, 0, 0.06); 128 | border-color: rgba(59, 130, 246, 0.2); 129 | } 130 | 131 | .character-form-input:focus { 132 | outline: none; 133 | border-color: var(--modern-accent-primary); 134 | box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25); 135 | background-color: rgba(0, 0, 0, 0.3); 136 | } 137 | 138 | body.light-theme .character-form-input:focus { 139 | background-color: rgba(0, 0, 0, 0.07); 140 | } 141 | 142 | .character-form-textarea { 143 | min-height: 120px; 144 | resize: vertical; 145 | line-height: 1.5; 146 | } 147 | 148 | .character-preview { 149 | width: 100%; 150 | max-height: 250px; 151 | object-fit: contain; 152 | border-radius: 0.75rem; 153 | margin-top: 0.75rem; 154 | background-color: rgba(0, 0, 0, 0.25); 155 | border: 1px solid rgba(255, 255, 255, 0.12); 156 | box-shadow: var(--modern-shadow-sm); 157 | transition: all 0.3s ease; 158 | } 159 | 160 | .character-preview:not(.hidden) { 161 | padding: 0.5rem; 162 | } 163 | 164 | body.light-theme .character-preview { 165 | background-color: rgba(0, 0, 0, 0.05); 166 | border: 1px solid rgba(0, 0, 0, 0.1); 167 | } 168 | 169 | .character-upload-btn { 170 | display: inline-flex; 171 | align-items: center; 172 | justify-content: center; 173 | padding: 0.65rem 1.25rem; 174 | background-color: rgba(59, 130, 246, 0.12); 175 | color: var(--modern-accent-primary); 176 | border: 1px solid rgba(59, 130, 246, 0.3); 177 | border-radius: 0.75rem; 178 | cursor: pointer; 179 | transition: all 0.3s ease; 180 | font-weight: 600; 181 | margin-top: 0.75rem; 182 | box-shadow: var(--modern-shadow-sm); 183 | } 184 | 185 | .character-upload-btn:hover { 186 | background-color: rgba(59, 130, 246, 0.2); 187 | border-color: rgba(59, 130, 246, 0.5); 188 | transform: translateY(-2px); 189 | box-shadow: var(--modern-shadow-md); 190 | } 191 | 192 | .character-upload-btn:active { 193 | transform: translateY(0); 194 | } 195 | 196 | .character-upload-btn i { 197 | margin-right: 0.65rem; 198 | font-size: 1.1em; 199 | } 200 | 201 | .character-modal-footer { 202 | padding: 1.25rem 1.5rem; 203 | border-top: 1px solid rgba(255, 255, 255, 0.12); 204 | display: flex; 205 | justify-content: space-between; 206 | align-items: center; 207 | background-color: rgba(0, 0, 0, 0.1); 208 | } 209 | 210 | body.light-theme .character-modal-footer { 211 | border-top: 1px solid rgba(0, 0, 0, 0.1); 212 | background-color: rgba(0, 0, 0, 0.03); 213 | } 214 | 215 | .character-save-btn { 216 | background: var(--modern-gradient-primary); 217 | color: white; 218 | font-weight: 600; 219 | padding: 0.75rem 1.5rem; 220 | border-radius: 0.75rem; 221 | border: none; 222 | box-shadow: var(--modern-shadow), 0 0 0 1px rgba(59, 130, 246, 0.3); 223 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 224 | cursor: pointer; 225 | font-size: 0.95rem; 226 | letter-spacing: 0.01em; 227 | position: relative; 228 | overflow: hidden; 229 | } 230 | 231 | .character-save-btn::before { 232 | content: ''; 233 | position: absolute; 234 | top: 0; 235 | left: 0; 236 | width: 100%; 237 | height: 100%; 238 | background: linear-gradient(rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0)); 239 | transform: translateX(-100%); 240 | transition: transform 0.8s ease; 241 | } 242 | 243 | .character-save-btn:hover { 244 | transform: translateY(-2px); 245 | box-shadow: var(--modern-shadow-md), 0 0 0 1px rgba(59, 130, 246, 0.5); 246 | } 247 | 248 | .character-save-btn:hover::before { 249 | transform: translateX(100%); 250 | } 251 | 252 | .character-save-btn:active { 253 | transform: translateY(1px); 254 | box-shadow: var(--modern-shadow-sm), 0 0 0 1px rgba(59, 130, 246, 0.4); 255 | } 256 | 257 | .character-delete-btn { 258 | background-color: rgba(239, 68, 68, 0.1); 259 | color: #ef4444; 260 | border: 1px solid rgba(239, 68, 68, 0.3); 261 | padding: 0.75rem 1.5rem; 262 | border-radius: 0.75rem; 263 | font-weight: 600; 264 | transition: all 0.3s ease; 265 | cursor: pointer; 266 | font-size: 0.95rem; 267 | letter-spacing: 0.01em; 268 | box-shadow: var(--modern-shadow-sm); 269 | } 270 | 271 | .character-delete-btn:hover { 272 | background-color: rgba(239, 68, 68, 0.2); 273 | border-color: rgba(239, 68, 68, 0.5); 274 | transform: translateY(-2px); 275 | box-shadow: var(--modern-shadow-md); 276 | } 277 | 278 | .character-delete-btn:active { 279 | transform: translateY(0); 280 | } 281 | 282 | /* Character List Styles */ 283 | .characters-section { 284 | margin-bottom: 1rem; 285 | padding-bottom: 1rem; 286 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 287 | } 288 | 289 | body.light-theme .characters-section { 290 | border-bottom: 1px solid rgba(0, 0, 0, 0.1); 291 | } 292 | 293 | .characters-header { 294 | display: flex; 295 | justify-content: space-between; 296 | align-items: center; 297 | padding: 0.75rem 1rem; 298 | margin-bottom: 0.5rem; 299 | } 300 | 301 | .characters-title { 302 | font-weight: 600; 303 | color: var(--text-secondary); 304 | cursor: pointer; 305 | display: flex; 306 | align-items: center; 307 | transition: color 0.2s ease; 308 | text-shadow: var(--title-shadow); 309 | } 310 | 311 | .characters-title:hover { 312 | color: var(--title-accent); 313 | } 314 | 315 | .characters-actions { 316 | display: flex; 317 | gap: 0.5rem; 318 | } 319 | 320 | .create-character-btn, 321 | .character-gallery-btn { 322 | background-color: transparent; 323 | color: var(--modern-accent-primary); 324 | border: none; 325 | width: 2rem; 326 | height: 2rem; 327 | border-radius: 50%; 328 | display: flex; 329 | align-items: center; 330 | justify-content: center; 331 | cursor: pointer; 332 | transition: all 0.2s ease; 333 | } 334 | 335 | .create-character-btn:hover, 336 | .character-gallery-btn:hover { 337 | background-color: rgba(59, 130, 246, 0.1); 338 | transform: scale(1.05); 339 | } 340 | 341 | .character-item { 342 | background-color: rgba(255, 255, 255, 0.05); 343 | border: 1px solid rgba(255, 255, 255, 0.1); 344 | } 345 | 346 | body.light-theme .character-item { 347 | background-color: rgba(0, 0, 0, 0.05); 348 | border: 1px solid rgba(0, 0, 0, 0.1); 349 | } 350 | 351 | .character-item:hover { 352 | background-color: rgba(59, 130, 246, 0.1); 353 | transform: translateX(4px); 354 | } 355 | 356 | .active-character { 357 | background-color: rgba(59, 130, 246, 0.2); 358 | border-color: rgba(59, 130, 246, 0.3); 359 | } 360 | 361 | .character-avatar { 362 | background: linear-gradient(135deg, #2563eb, #3b82f6); 363 | } 364 | 365 | /* Character Badge for Chat */ 366 | .character-badge { 367 | display: inline-flex; 368 | align-items: center; 369 | padding: 0.25rem 0.75rem 0.25rem 0.25rem; 370 | background-color: rgba(59, 130, 246, 0.1); 371 | border: 1px solid rgba(59, 130, 246, 0.3); 372 | border-radius: 1rem; 373 | transition: all 0.2s ease; 374 | width: auto; /* Set width to auto */ 375 | max-width: fit-content; /* Only as wide as content needs */ 376 | min-width: 0; /* Allow badge to shrink below content size */ 377 | overflow: visible; /* Allow content to determine size */ 378 | flex-shrink: 0; /* Prevent badge from shrinking */ 379 | flex-grow: 0; /* Prevent badge from growing */ 380 | margin: 0.5rem 0; /* Add margin for better visibility */ 381 | } 382 | 383 | /* Active character display container */ 384 | #active-character-display { 385 | display: block; 386 | padding: 0.25rem 0.5rem; 387 | margin-bottom: 0; /* Remove margin to prevent gap */ 388 | width: 100%; 389 | box-sizing: border-box; 390 | z-index: 5; 391 | background-color: transparent; /* Make it transparent to match the background above it */ 392 | border-bottom: none; /* Remove border to avoid visible line */ 393 | } 394 | 395 | /* Ensure hidden state is respected */ 396 | #active-character-display.hidden { 397 | display: none !important; 398 | } 399 | 400 | .character-badge:hover { 401 | background-color: rgba(59, 130, 246, 0.2); 402 | } 403 | 404 | .character-badge-avatar { 405 | width: 1.5rem; 406 | height: 1.5rem; 407 | border-radius: 50%; 408 | margin-right: 0.5rem; 409 | background-color: rgba(59, 130, 246, 0.3); 410 | display: flex; 411 | align-items: center; 412 | justify-content: center; 413 | overflow: hidden; 414 | } 415 | 416 | .character-badge-avatar img { 417 | width: 100%; 418 | height: 100%; 419 | object-fit: cover; 420 | } 421 | 422 | .character-badge-name { 423 | overflow: hidden; 424 | text-overflow: ellipsis; 425 | white-space: nowrap; 426 | max-width: max-content; /* Only as wide as text needs */ 427 | font-size: 0.875rem; 428 | font-weight: 500; 429 | color: var(--modern-accent-primary); 430 | min-width: 0; /* Allow text to shrink */ 431 | flex: 0 1 auto; /* Don't grow, allow shrink, auto basis */ 432 | display: inline-block; /* Ensure proper text handling */ 433 | } 434 | 435 | /* Delete Character Modal Styles */ 436 | #delete-character-modal { 437 | z-index: 100; /* Ensure it's above the character gallery (z-index: 50) */ 438 | position: fixed; 439 | inset: 0; 440 | display: flex; 441 | align-items: center; /* Center vertically */ 442 | justify-content: center; /* Center horizontally */ 443 | padding: 0 !important; /* Remove any padding that might affect centering */ 444 | } 445 | 446 | #delete-character-modal.hidden { 447 | display: none !important; 448 | } 449 | 450 | #delete-character-modal .modal-content { 451 | position: relative; 452 | z-index: 101; /* Higher than the modal container */ 453 | margin: auto !important; /* Ensure the modal is centered */ 454 | transform: translateY(0) !important; /* Prevent any unwanted transforms */ 455 | } 456 | 457 | /* Delete Character Modal Animations */ 458 | @keyframes modal-in { 459 | 0% { 460 | opacity: 0; 461 | transform: scale(0.95) translateY(10px); 462 | } 463 | 100% { 464 | opacity: 1; 465 | transform: scale(1) translateY(0); 466 | } 467 | } 468 | 469 | @keyframes modal-out { 470 | 0% { 471 | opacity: 1; 472 | transform: scale(1) translateY(0); 473 | } 474 | 100% { 475 | opacity: 0; 476 | transform: scale(0.95) translateY(10px); 477 | } 478 | } 479 | 480 | .animate-modal-in { 481 | animation: modal-in 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 482 | } 483 | 484 | .animate-modal-out { 485 | animation: modal-out 0.3s cubic-bezier(0.4, 0, 0.2, 1) forwards; 486 | } 487 | 488 | /* Mobile Optimizations */ 489 | @media (max-width: 640px) { 490 | .character-modal { 491 | width: 100%; 492 | max-width: 100%; 493 | border-radius: 0; 494 | height: 100vh; 495 | max-height: 100vh; 496 | position: fixed; 497 | top: 0; 498 | left: 0; 499 | right: 0; 500 | bottom: 0; 501 | } 502 | 503 | .character-modal-header { 504 | padding: 1rem; 505 | } 506 | 507 | .character-modal-title::after { 508 | bottom: -4px; 509 | width: 30px; 510 | height: 2px; 511 | } 512 | 513 | .character-modal-body { 514 | padding: 1.25rem 1rem; 515 | -webkit-overflow-scrolling: touch; 516 | touch-action: pan-y; 517 | overscroll-behavior: contain; 518 | scroll-behavior: smooth; 519 | } 520 | 521 | .character-form-group { 522 | margin-bottom: 1.25rem; 523 | } 524 | 525 | .character-form-input, 526 | .character-upload-btn { 527 | min-height: 48px; 528 | font-size: 16px; /* Prevent iOS zoom on focus */ 529 | } 530 | 531 | .character-save-btn, 532 | .character-delete-btn { 533 | min-height: 48px; 534 | padding: 0.65rem 1.25rem; 535 | font-size: 16px; 536 | width: 45%; /* Make buttons more balanced on mobile */ 537 | justify-content: center; 538 | } 539 | 540 | .character-modal-footer { 541 | padding: 1rem; 542 | } 543 | } 544 | 545 | /* Character Modal Animations */ 546 | @keyframes fade-in { 547 | from { opacity: 0; } 548 | to { opacity: 1; } 549 | } 550 | 551 | @keyframes fade-out { 552 | from { opacity: 1; } 553 | to { opacity: 0; } 554 | } 555 | 556 | @keyframes slide-up { 557 | from { transform: translateY(20px); opacity: 0; } 558 | to { transform: translateY(0); opacity: 1; } 559 | } 560 | 561 | @keyframes slide-down { 562 | from { transform: translateY(0); opacity: 1; } 563 | to { transform: translateY(20px); opacity: 0; } 564 | } 565 | 566 | #character-modal { 567 | backdrop-filter: blur(8px); 568 | animation: fade-in 0.3s ease-out; 569 | } 570 | 571 | #character-modal.hiding { 572 | animation: fade-out 0.3s ease-out forwards; 573 | } 574 | 575 | .character-modal.hiding { 576 | animation: slide-down 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards; 577 | } 578 | 579 | /* Character image container hover effect */ 580 | .character-image-container { 581 | transition: all 0.3s ease; 582 | position: relative; 583 | } 584 | 585 | .character-image-container:hover { 586 | border-color: rgba(59, 130, 246, 0.3); 587 | box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1); 588 | } 589 | 590 | .character-image-placeholder { 591 | transition: all 0.3s ease; 592 | } 593 | 594 | .character-image-container:hover .character-image-placeholder i { 595 | transform: scale(1.1); 596 | color: rgba(59, 130, 246, 0.7); 597 | } 598 | 599 | /* Ripple effect for buttons */ 600 | .character-save-btn, 601 | .character-upload-btn { 602 | position: relative; 603 | overflow: hidden; 604 | } 605 | 606 | .character-save-btn::after, 607 | .character-upload-btn::after { 608 | content: ''; 609 | position: absolute; 610 | top: 50%; 611 | left: 50%; 612 | width: 5px; 613 | height: 5px; 614 | background: rgba(255, 255, 255, 0.7); 615 | opacity: 0; 616 | border-radius: 100%; 617 | transform: scale(1, 1) translate(-50%, -50%); 618 | transform-origin: 50% 50%; 619 | } 620 | 621 | .character-save-btn:active::after, 622 | .character-upload-btn:active::after { 623 | animation: ripple 0.4s ease-out; 624 | } 625 | 626 | @keyframes ripple { 627 | 0% { 628 | opacity: 1; 629 | transform: scale(0, 0) translate(-50%, -50%); 630 | } 631 | 100% { 632 | opacity: 0; 633 | transform: scale(25, 25) translate(-50%, -50%); 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /css/continue-with-character.css: -------------------------------------------------------------------------------- 1 | /* Continue with Character Feature Styles */ 2 | 3 | /* Make the active character display more flexible */ 4 | #active-character-display { 5 | /* Only apply flex display when not hidden */ 6 | display: flex; 7 | align-items: center; 8 | padding: 0.75rem 1rem; 9 | background-color: transparent; /* Make it transparent to match the background above it */ 10 | border-bottom: none; /* Remove border to avoid visible line */ 11 | width: 100%; /* Ensure it doesn't stretch beyond its container */ 12 | box-sizing: border-box; /* Include padding in width calculation */ 13 | max-width: 100%; /* Prevent overflow */ 14 | justify-content: flex-start; /* Align to the left */ 15 | } 16 | 17 | /* Ensure hidden state is respected */ 18 | #active-character-display.hidden { 19 | display: none !important; 20 | } 21 | 22 | /* Style the container for badge */ 23 | #active-character-display > div { 24 | width: 100%; 25 | display: flex; /* Removed !important to respect parent's hidden state */ 26 | justify-content: flex-start; 27 | align-items: center; 28 | max-width: 100%; /* Prevent overflow */ 29 | } 30 | 31 | /* Style the character badge */ 32 | .character-badge { 33 | display: flex; 34 | align-items: center; 35 | flex: 0 0 auto; /* Don't grow, don't shrink, auto basis */ 36 | } 37 | 38 | /* Fix for Character Continuation Modal text in light theme */ 39 | body.light-theme #character-continuation-modal p.text-gray-300 { 40 | color: #1e293b !important; /* Dark text for light theme */ 41 | } 42 | -------------------------------------------------------------------------------- /css/performance-optimizations.css: -------------------------------------------------------------------------------- 1 | /* Enhanced performance optimizations for the app with adaptive settings */ 2 | 3 | /* Base hardware acceleration for all devices */ 4 | .hardware-accelerated, 5 | #messages, 6 | #chat-history, 7 | #sidebar, 8 | .character-gallery-container, 9 | .monaco-container, 10 | .modal-content, 11 | .welcome-container, 12 | .character-card { 13 | transform: translateZ(0); 14 | backface-visibility: hidden; 15 | perspective: 1000px; 16 | } 17 | 18 | /* Adaptive hardware acceleration based on device capabilities */ 19 | /* High-end devices get full optimization */ 20 | @media (min-width: 1024px) and (min-height: 768px) { 21 | .hardware-accelerated, 22 | #messages, 23 | #chat-history, 24 | #sidebar, 25 | .character-gallery-container, 26 | .monaco-container, 27 | .modal-content, 28 | .welcome-container, 29 | .character-card { 30 | will-change: transform, opacity; 31 | } 32 | } 33 | 34 | /* Medium devices get moderate optimization */ 35 | @media (max-width: 1023px) and (min-width: 768px) { 36 | .hardware-accelerated, 37 | #messages, 38 | #chat-history, 39 | #sidebar, 40 | .character-gallery-container, 41 | .monaco-container, 42 | .modal-content, 43 | .welcome-container, 44 | .character-card { 45 | will-change: transform; 46 | } 47 | } 48 | 49 | /* Low-end devices get minimal optimization to avoid performance issues */ 50 | @media (max-width: 767px) { 51 | .hardware-accelerated, 52 | #messages, 53 | #chat-history, 54 | #sidebar, 55 | .character-gallery-container, 56 | .monaco-container, 57 | .modal-content, 58 | .welcome-container, 59 | .character-card { 60 | will-change: auto; 61 | } 62 | 63 | /* Reduce complex effects on low-end devices */ 64 | .character-card { 65 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; 66 | } 67 | 68 | .modal-content { 69 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important; 70 | } 71 | } 72 | 73 | /* Adaptive animations based on device performance */ 74 | /* Base animation settings */ 75 | .animate-fade-in { 76 | animation-timing-function: cubic-bezier(0.2, 0, 0.2, 1); 77 | } 78 | 79 | .transition-optimized, 80 | .character-card, 81 | .character-gallery-container, 82 | .modal-content, 83 | #scroll-to-bottom, 84 | .menu-item, 85 | .chat-item { 86 | transition-timing-function: cubic-bezier(0.2, 0, 0.2, 1) !important; 87 | } 88 | 89 | /* High-end devices - full animation quality */ 90 | @media (min-width: 1024px) and (min-height: 768px) { 91 | .animate-fade-in { 92 | animation-duration: 0.3s; 93 | } 94 | 95 | .transition-optimized, 96 | .character-card, 97 | .character-gallery-container, 98 | .modal-content, 99 | #scroll-to-bottom, 100 | .menu-item, 101 | .chat-item { 102 | transition-duration: 0.3s !important; 103 | } 104 | } 105 | 106 | /* Medium devices - balanced animations */ 107 | @media (max-width: 1023px) and (min-width: 768px) { 108 | .animate-fade-in { 109 | animation-duration: 0.25s; 110 | } 111 | 112 | .transition-optimized, 113 | .character-card, 114 | .character-gallery-container, 115 | .modal-content, 116 | #scroll-to-bottom, 117 | .menu-item, 118 | .chat-item { 119 | transition-duration: 0.25s !important; 120 | } 121 | } 122 | 123 | /* Low-end devices - faster, simpler animations */ 124 | @media (max-width: 767px) { 125 | .animate-fade-in { 126 | animation-duration: 0.2s; 127 | } 128 | 129 | .transition-optimized, 130 | .character-card, 131 | .character-gallery-container, 132 | .modal-content, 133 | #scroll-to-bottom, 134 | .menu-item, 135 | .chat-item { 136 | transition-duration: 0.2s !important; 137 | } 138 | 139 | /* Disable complex animations on very slow devices */ 140 | @media (max-width: 480px) { 141 | .character-card:hover { 142 | transform: none !important; 143 | } 144 | 145 | .character-card-image img { 146 | transition: none !important; 147 | } 148 | 149 | .character-card:hover .character-card-image img { 150 | transform: none !important; 151 | } 152 | 153 | /* Simplify shimmer effects */ 154 | .img-placeholder { 155 | animation: none !important; 156 | background: #ececec !important; 157 | } 158 | } 159 | } 160 | 161 | /* Adaptive scrolling optimizations based on device performance */ 162 | .scroll-optimized, 163 | #chat-history, 164 | #sidebar, 165 | .character-gallery-content, 166 | .settings-content-wrapper, 167 | .modal-content, 168 | #messages { 169 | -webkit-overflow-scrolling: touch !important; 170 | touch-action: pan-y !important; 171 | overscroll-behavior: contain !important; 172 | } 173 | 174 | /* High-end devices - allow smooth scrolling */ 175 | @media (min-width: 1024px) and (min-height: 768px) { 176 | .scroll-optimized, 177 | #chat-history, 178 | #sidebar, 179 | .character-gallery-content, 180 | .settings-content-wrapper, 181 | .modal-content, 182 | #messages { 183 | scroll-behavior: smooth; 184 | } 185 | } 186 | 187 | /* Medium and low-end devices - use auto scrolling for better performance */ 188 | @media (max-width: 1023px) { 189 | .scroll-optimized, 190 | #chat-history, 191 | #sidebar, 192 | .character-gallery-content, 193 | .settings-content-wrapper, 194 | .modal-content, 195 | #messages { 196 | scroll-behavior: auto !important; /* Use JS for smooth scrolling instead */ 197 | } 198 | } 199 | 200 | /* Very low-end devices - minimal scroll optimizations */ 201 | @media (max-width: 480px) { 202 | .scroll-optimized, 203 | #chat-history, 204 | #sidebar, 205 | .character-gallery-content, 206 | .settings-content-wrapper, 207 | .modal-content, 208 | #messages { 209 | /* Reduce GPU usage for scrolling */ 210 | will-change: auto !important; 211 | /* Simplify scroll behavior */ 212 | scroll-behavior: auto !important; 213 | /* Optimize for memory usage */ 214 | contain: layout !important; 215 | } 216 | } 217 | 218 | /* Optimize touch targets for mobile */ 219 | @media (max-width: 768px) { 220 | .button, 221 | .menu-item, 222 | .chat-item, 223 | .character-card-btn, 224 | #send-btn, 225 | #stop-btn, 226 | #scroll-to-bottom { 227 | min-width: 44px !important; 228 | min-height: 44px !important; 229 | padding: 10px !important; 230 | } 231 | 232 | /* Increase tap target size without affecting layout */ 233 | .menu-item::after, 234 | .chat-item::after, 235 | .button::after { 236 | content: ''; 237 | position: absolute; 238 | top: -5px; 239 | left: -5px; 240 | right: -5px; 241 | bottom: -5px; 242 | z-index: -1; 243 | } 244 | } 245 | 246 | /* Optimize image loading */ 247 | img[data-src] { 248 | opacity: 0; 249 | transition: opacity 0.3s ease-in; 250 | } 251 | 252 | img[data-src][src]:not([src^="data:"]) { 253 | opacity: 1; 254 | } 255 | 256 | /* Placeholder for images while loading */ 257 | .img-placeholder { 258 | background: linear-gradient(110deg, #ececec 8%, #f5f5f5 18%, #ececec 33%); 259 | background-size: 200% 100%; 260 | animation: shimmer 1.5s linear infinite; 261 | } 262 | 263 | @keyframes shimmer { 264 | 0% { background-position: 100% 0; } 265 | 100% { background-position: -100% 0; } 266 | } 267 | 268 | /* Optimize Monaco editor containers */ 269 | .monaco-container { 270 | contain: content; /* Use CSS containment for better performance */ 271 | } 272 | 273 | /* Optimize scroll-to-bottom button */ 274 | #scroll-to-bottom { 275 | transition: opacity 0.2s cubic-bezier(0.2, 0, 0.2, 1), 276 | transform 0.2s cubic-bezier(0.2, 0, 0.2, 1), 277 | visibility 0.2s cubic-bezier(0.2, 0, 0.2, 1) !important; 278 | } 279 | 280 | /* Optimize modals */ 281 | .modal-content { 282 | contain: content; /* Use CSS containment for better performance */ 283 | } 284 | 285 | /* Optimize welcome message */ 286 | .welcome-container { 287 | contain: layout; /* Use CSS containment for better performance */ 288 | } 289 | 290 | /* Optimize character cards */ 291 | .character-card { 292 | contain: content; /* Use CSS containment for better performance */ 293 | } 294 | 295 | /* Optimize for reduced motion if user prefers it */ 296 | @media (prefers-reduced-motion: reduce) { 297 | * { 298 | animation-duration: 0.001ms !important; 299 | animation-iteration-count: 1 !important; 300 | transition-duration: 0.001ms !important; 301 | scroll-behavior: auto !important; 302 | } 303 | } 304 | 305 | /* Optimize for mobile devices */ 306 | @media (max-width: 768px) { 307 | /* Reduce animation complexity on mobile */ 308 | .animate-fade-in { 309 | animation-duration: 0.2s; 310 | } 311 | 312 | /* Optimize touch scrolling on mobile */ 313 | body, html { 314 | touch-action: manipulation; 315 | -webkit-tap-highlight-color: transparent; 316 | } 317 | 318 | /* Ensure buttons have proper touch feedback */ 319 | button, .button, .menu-item, .chat-item { 320 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1); 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /css/system-prompt-overlay.css: -------------------------------------------------------------------------------- 1 | /* System Prompt Overlay Styles */ 2 | 3 | /* Main overlay container */ 4 | #system-prompt-overlay { 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | background-color: var(--modal-overlay); 9 | backdrop-filter: blur(4px); 10 | -webkit-backdrop-filter: blur(4px); 11 | transition: all 0.3s ease; 12 | padding: 1rem; 13 | overflow-y: auto; 14 | opacity: 0.95; 15 | } 16 | 17 | #system-prompt-overlay.active { 18 | opacity: 1; 19 | } 20 | 21 | /* Editor container with improved styling */ 22 | #system-prompt-overlay .bg-darkSecondary { 23 | background: var(--modal-bg); 24 | border: 1px solid var(--border-color); 25 | border-radius: 1rem; 26 | box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5); 27 | transition: all 0.3s ease; 28 | animation: modalFadeIn 0.25s ease-out forwards; 29 | max-height: 90vh; 30 | width: 100%; 31 | max-width: 700px; 32 | margin: auto; 33 | position: relative; 34 | } 35 | 36 | /* Header styling */ 37 | #system-prompt-overlay .flex.justify-between { 38 | border-bottom: 1px solid var(--border-color); 39 | padding-bottom: 0.75rem; 40 | margin-bottom: 1rem; 41 | transform: none; 42 | will-change: auto; 43 | } 44 | 45 | #system-prompt-overlay h2 { 46 | color: var(--modal-title); 47 | font-weight: 600; 48 | display: flex; 49 | align-items: center; 50 | transform: none; 51 | will-change: auto; 52 | } 53 | 54 | #system-prompt-overlay h2::before { 55 | content: '\f044'; /* Edit icon */ 56 | font-family: 'Font Awesome 5 Free'; 57 | font-weight: 900; 58 | margin-right: 0.5rem; 59 | color: var(--button-primary-bg); 60 | font-size: 0.9em; 61 | } 62 | 63 | 64 | 65 | /* Textarea styling */ 66 | #system-prompt-editor { 67 | background-color: var(--settings-input-bg); 68 | border: 1px solid var(--settings-input-border); 69 | border-radius: 0.75rem; 70 | color: var(--settings-input-text); 71 | font-size: 1rem; 72 | line-height: 1.5; 73 | padding: 1rem; 74 | transition: all 0.2s ease; 75 | resize: none; 76 | min-height: 200px; 77 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); 78 | } 79 | 80 | #system-prompt-editor:focus { 81 | outline: none; 82 | border-color: var(--button-primary-bg); 83 | box-shadow: 84 | 0 0 0 2px rgba(59, 130, 246, 0.3), 85 | inset 0 2px 4px rgba(0, 0, 0, 0.1); 86 | } 87 | 88 | /* Button container */ 89 | #system-prompt-overlay .flex.justify-end { 90 | margin-top: 0.5rem; 91 | } 92 | 93 | /* Button styling */ 94 | #system-prompt-overlay button { 95 | border-radius: 0.5rem; 96 | font-weight: 500; 97 | transition: all 0.2s ease; 98 | padding: 0.625rem 1.25rem; 99 | display: flex; 100 | align-items: center; 101 | justify-content: center; 102 | } 103 | 104 | #cancel-system-prompt-edit { 105 | background-color: var(--settings-label-bg); 106 | color: var(--modal-text); 107 | } 108 | 109 | #cancel-system-prompt-edit:hover { 110 | background-color: var(--sidebar-item-hover); 111 | } 112 | 113 | #save-system-prompt-edit { 114 | background-color: var(--button-primary-bg); 115 | color: white; 116 | } 117 | 118 | #save-system-prompt-edit:hover { 119 | background-color: var(--button-primary-hover); 120 | transform: translateY(-1px); 121 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 122 | } 123 | 124 | #save-system-prompt-edit:active { 125 | transform: translateY(0); 126 | box-shadow: none; 127 | } 128 | 129 | /* Add icons to buttons */ 130 | #cancel-system-prompt-edit::before { 131 | content: '\f00d'; /* Times icon */ 132 | font-family: 'Font Awesome 5 Free'; 133 | font-weight: 900; 134 | margin-right: 0.5rem; 135 | } 136 | 137 | #save-system-prompt-edit::before { 138 | content: '\f0c7'; /* Save icon */ 139 | font-family: 'Font Awesome 5 Free'; 140 | font-weight: 900; 141 | margin-right: 0.5rem; 142 | } 143 | 144 | /* Clear button styling in overlay */ 145 | #clear-system-prompt-btn { 146 | background-color: transparent; 147 | color: var(--text-danger, #ef4444); 148 | border: 1px solid var(--border-danger, rgba(239, 68, 68, 0.3)); 149 | font-size: 0.875rem; 150 | font-weight: 500; 151 | } 152 | 153 | #clear-system-prompt-btn:hover { 154 | background-color: var(--bg-danger-hover, rgba(239, 68, 68, 0.1)); 155 | border-color: var(--border-danger-hover, rgba(239, 68, 68, 0.5)); 156 | color: var(--text-danger-hover, #dc2626); 157 | transform: translateY(-1px); 158 | box-shadow: 0 2px 4px rgba(239, 68, 68, 0.2); 159 | } 160 | 161 | #clear-system-prompt-btn:active { 162 | transform: translateY(0); 163 | box-shadow: none; 164 | } 165 | 166 | /* Responsive adjustments for mobile and small tablets */ 167 | @media (max-width: 768px) { 168 | #system-prompt-overlay { 169 | padding: 0.5rem; 170 | } 171 | 172 | #system-prompt-overlay .bg-darkSecondary { 173 | padding: 1rem; 174 | max-width: 95%; 175 | } 176 | 177 | #system-prompt-editor { 178 | min-height: 150px; 179 | font-size: 0.95rem; 180 | padding: 0.75rem; 181 | } 182 | 183 | #system-prompt-overlay button { 184 | padding: 0.5rem 1rem; 185 | font-size: 0.95rem; 186 | } 187 | 188 | #clear-system-prompt-btn { 189 | font-size: 0.8rem; 190 | padding: 0.4rem 0.8rem; 191 | } 192 | 193 | #system-prompt-overlay h2 { 194 | font-size: 1.25rem; 195 | } 196 | } 197 | 198 | /* Tablet-specific enhancements (768px-1024px) */ 199 | @media (min-width: 768px) and (max-width: 1024px) { 200 | #system-prompt-overlay .bg-darkSecondary { 201 | max-width: 650px; 202 | width: 90%; 203 | padding: 1.5rem; 204 | border-radius: 1.25rem; 205 | } 206 | 207 | #system-prompt-editor { 208 | min-height: 180px; 209 | font-size: 1rem; 210 | } 211 | 212 | /* Improve button spacing for tablets */ 213 | #system-prompt-overlay .flex.justify-end { 214 | margin-top: 1rem; 215 | } 216 | 217 | #system-prompt-overlay button { 218 | min-width: 100px; 219 | min-height: 44px; 220 | } 221 | } 222 | 223 | /* Small mobile devices */ 224 | @media (max-width: 480px) { 225 | #system-prompt-overlay .bg-darkSecondary { 226 | padding: 0.75rem; 227 | max-width: 100%; 228 | margin: 0.5rem; 229 | } 230 | 231 | #system-prompt-overlay .flex.justify-between { 232 | padding-bottom: 0.5rem; 233 | margin-bottom: 0.75rem; 234 | } 235 | 236 | #system-prompt-editor { 237 | min-height: 120px; 238 | font-size: 0.9rem; 239 | padding: 0.5rem; 240 | } 241 | 242 | #system-prompt-overlay button { 243 | padding: 0.4rem 0.8rem; 244 | font-size: 0.9rem; 245 | min-width: 80px; /* Ensure buttons are wide enough for touch */ 246 | min-height: 40px; /* Ensure buttons are tall enough for touch */ 247 | } 248 | 249 | #system-prompt-overlay h2 { 250 | font-size: 1.1rem; 251 | } 252 | 253 | 254 | 255 | /* Improve spacing between buttons for touch */ 256 | #system-prompt-overlay .flex.justify-end { 257 | gap: 10px; 258 | } 259 | } 260 | 261 | /* Extra small devices */ 262 | @media (max-width: 360px) { 263 | #system-prompt-overlay { 264 | padding: 0.25rem; 265 | /* Use dynamic viewport height to account for mobile keyboards */ 266 | height: 100dvh; 267 | min-height: 100vh; 268 | } 269 | 270 | #system-prompt-overlay .bg-darkSecondary { 271 | padding: 0.5rem; 272 | max-height: calc(100dvh - 0.5rem); 273 | overflow-y: auto; 274 | -webkit-overflow-scrolling: touch; 275 | /* Ensure modal fits within safe areas */ 276 | margin: 0.25rem; 277 | margin-top: max(0.25rem, env(safe-area-inset-top)); 278 | margin-bottom: max(0.25rem, env(safe-area-inset-bottom)); 279 | } 280 | 281 | #system-prompt-editor { 282 | min-height: 80px; 283 | max-height: 40vh; 284 | resize: vertical; 285 | overflow-y: auto; 286 | -webkit-overflow-scrolling: touch; 287 | } 288 | 289 | /* Stack buttons on very small screens */ 290 | #system-prompt-overlay .flex.justify-end { 291 | flex-direction: column-reverse; 292 | gap: 6px; 293 | margin-top: 0.5rem; 294 | /* Ensure buttons are always visible */ 295 | position: sticky; 296 | bottom: 0; 297 | background: var(--modal-bg); 298 | padding-top: 0.5rem; 299 | margin-left: -0.5rem; 300 | margin-right: -0.5rem; 301 | padding-left: 0.5rem; 302 | padding-right: 0.5rem; 303 | } 304 | 305 | #system-prompt-overlay button { 306 | width: 100%; 307 | min-height: 44px; 308 | font-size: 0.9rem; 309 | padding: 0.75rem 1rem; 310 | border-radius: 0.5rem; 311 | } 312 | 313 | /* Optimize header for ultra-small screens */ 314 | #system-prompt-overlay .flex.justify-between { 315 | padding-bottom: 0.375rem; 316 | margin-bottom: 0.5rem; 317 | flex-wrap: nowrap; 318 | } 319 | 320 | #system-prompt-overlay h2 { 321 | font-size: 1rem; 322 | line-height: 1.2; 323 | flex-shrink: 1; 324 | min-width: 0; 325 | } 326 | 327 | #close-system-prompt-overlay { 328 | width: 32px; 329 | height: 32px; 330 | flex-shrink: 0; 331 | border-radius: 50%; 332 | display: flex; 333 | align-items: center; 334 | justify-content: center; 335 | } 336 | } 337 | 338 | /* iPhone SE specific optimizations */ 339 | @media (min-width: 361px) and (max-width: 375px) { 340 | #system-prompt-overlay { 341 | padding: 0.5rem; 342 | } 343 | 344 | #system-prompt-overlay .bg-darkSecondary { 345 | padding: 0.75rem; 346 | max-width: 100%; 347 | margin: 0.5rem; 348 | } 349 | 350 | #system-prompt-editor { 351 | min-height: 120px; 352 | font-size: 0.9rem; 353 | padding: 0.75rem; 354 | } 355 | 356 | /* Improve button layout for iPhone SE */ 357 | #system-prompt-overlay .flex.justify-end { 358 | gap: 12px; 359 | } 360 | 361 | #system-prompt-overlay button { 362 | min-width: 90px; 363 | min-height: 44px; /* Better touch target */ 364 | padding: 0.5rem 1rem; 365 | font-size: 0.9rem; 366 | } 367 | 368 | /* Optimize header for iPhone SE */ 369 | #system-prompt-overlay .flex.justify-between { 370 | padding-bottom: 0.5rem; 371 | margin-bottom: 0.75rem; 372 | } 373 | 374 | #system-prompt-overlay h2 { 375 | font-size: 1.1rem; 376 | } 377 | 378 | 379 | } 380 | 381 | /* Desktop enhancements (larger screens) */ 382 | @media (min-width: 1025px) { 383 | #system-prompt-overlay .bg-darkSecondary { 384 | max-width: 800px; 385 | padding: 2rem; 386 | border-radius: 1.5rem; 387 | box-shadow: 388 | 0 20px 25px -5px rgba(0, 0, 0, 0.5), 389 | 0 10px 10px -5px rgba(0, 0, 0, 0.2), 390 | 0 0 0 1px rgba(255, 255, 255, 0.05) inset; 391 | } 392 | 393 | #system-prompt-editor { 394 | min-height: 250px; 395 | font-size: 1.05rem; 396 | line-height: 1.6; 397 | padding: 1.25rem; 398 | border-radius: 1rem; 399 | } 400 | 401 | #system-prompt-overlay h2 { 402 | font-size: 1.5rem; 403 | } 404 | 405 | #system-prompt-overlay .flex.justify-between { 406 | margin-bottom: 1.5rem; 407 | } 408 | 409 | #system-prompt-overlay .flex.justify-end { 410 | margin-top: 1.5rem; 411 | } 412 | 413 | #system-prompt-overlay button { 414 | padding: 0.75rem 1.5rem; 415 | font-size: 1rem; 416 | min-width: 120px; 417 | border-radius: 0.75rem; 418 | } 419 | 420 | /* Add subtle hover effects for desktop */ 421 | #system-prompt-editor:hover { 422 | border-color: var(--border-color-hover); 423 | } 424 | } 425 | 426 | /* Ultra-small screens (smaller than 320px) */ 427 | @media (max-width: 320px) { 428 | #system-prompt-overlay { 429 | padding: 0.125rem; 430 | } 431 | 432 | #system-prompt-overlay .bg-darkSecondary { 433 | padding: 0.375rem; 434 | margin: 0.125rem; 435 | border-radius: 0.5rem; 436 | } 437 | 438 | #system-prompt-editor { 439 | min-height: 60px; 440 | max-height: 35vh; 441 | font-size: 0.85rem; 442 | padding: 0.375rem; 443 | line-height: 1.3; 444 | } 445 | 446 | #system-prompt-overlay h2 { 447 | font-size: 0.9rem; 448 | } 449 | 450 | #system-prompt-overlay button { 451 | min-height: 40px; 452 | font-size: 0.85rem; 453 | padding: 0.5rem 0.75rem; 454 | } 455 | 456 | 457 | } 458 | 459 | /* Landscape orientation for small devices */ 460 | @media (max-width: 768px) and (max-height: 500px) and (orientation: landscape) { 461 | #system-prompt-overlay { 462 | padding: 0.25rem; 463 | height: 100dvh; 464 | min-height: 100vh; 465 | } 466 | 467 | #system-prompt-overlay .bg-darkSecondary { 468 | max-height: calc(100dvh - 0.5rem); 469 | padding: 0.5rem; 470 | margin: 0.25rem; 471 | display: flex; 472 | flex-direction: column; 473 | } 474 | 475 | #system-prompt-editor { 476 | min-height: 60px; 477 | max-height: 30vh; 478 | flex: 1; 479 | resize: none; 480 | } 481 | 482 | /* Keep buttons horizontal in landscape */ 483 | #system-prompt-overlay .flex.justify-end { 484 | flex-direction: row; 485 | gap: 0.5rem; 486 | margin-top: 0.5rem; 487 | position: sticky; 488 | bottom: 0; 489 | background: var(--modal-bg); 490 | padding-top: 0.5rem; 491 | } 492 | 493 | #system-prompt-overlay button { 494 | flex: 1; 495 | min-height: 36px; 496 | font-size: 0.85rem; 497 | } 498 | 499 | /* Compact header for landscape */ 500 | #system-prompt-overlay .flex.justify-between { 501 | padding-bottom: 0.25rem; 502 | margin-bottom: 0.375rem; 503 | } 504 | 505 | #system-prompt-overlay h2 { 506 | font-size: 0.95rem; 507 | } 508 | 509 | 510 | } 511 | 512 | /* Keyboard visible state adjustments */ 513 | @media (max-width: 480px) { 514 | #system-prompt-overlay.keyboard-visible { 515 | height: 100vh; 516 | padding-bottom: 0; 517 | } 518 | 519 | #system-prompt-overlay.keyboard-visible .bg-darkSecondary { 520 | max-height: 50vh; 521 | padding-bottom: 0.25rem; 522 | } 523 | 524 | #system-prompt-overlay.keyboard-visible #system-prompt-editor { 525 | max-height: 25vh; 526 | min-height: 60px; 527 | } 528 | } 529 | 530 | /* High DPI / Retina display optimizations for small screens */ 531 | @media (max-width: 480px) and (-webkit-min-device-pixel-ratio: 2) { 532 | #system-prompt-overlay button { 533 | /* Slightly larger touch targets on high DPI small screens */ 534 | min-height: 46px; 535 | padding: 0.75rem 1rem; 536 | } 537 | 538 | 539 | } 540 | 541 | /* Accessibility improvements for small screens */ 542 | @media (max-width: 480px) { 543 | /* Ensure focus indicators are visible on small screens */ 544 | #system-prompt-editor:focus { 545 | outline: 2px solid var(--button-primary-bg); 546 | outline-offset: 2px; 547 | } 548 | 549 | /* Improve button focus indicators */ 550 | #system-prompt-overlay button:focus { 551 | outline: 2px solid var(--button-primary-bg); 552 | outline-offset: 2px; 553 | } 554 | 555 | 556 | 557 | /* Improve touch feedback */ 558 | #system-prompt-overlay button:active { 559 | transform: scale(0.98); 560 | transition: transform 0.1s ease; 561 | } 562 | 563 | /* Prevent text selection on buttons for better touch experience */ 564 | #system-prompt-overlay button { 565 | -webkit-user-select: none; 566 | -moz-user-select: none; 567 | -ms-user-select: none; 568 | user-select: none; 569 | -webkit-tap-highlight-color: transparent; 570 | } 571 | 572 | /* Ensure textarea is scrollable with touch */ 573 | #system-prompt-editor { 574 | -webkit-overflow-scrolling: touch; 575 | touch-action: manipulation; 576 | overscroll-behavior: contain; 577 | } 578 | } 579 | 580 | /* Prevent zoom on input focus for iOS */ 581 | @media (max-width: 480px) { 582 | #system-prompt-editor { 583 | font-size: max(16px, 0.9rem); /* Prevent iOS zoom */ 584 | } 585 | } 586 | 587 | /* Dark mode specific improvements for small screens */ 588 | @media (max-width: 480px) and (prefers-color-scheme: dark) { 589 | #system-prompt-overlay .bg-darkSecondary { 590 | /* Ensure good contrast in dark mode */ 591 | border: 1px solid rgba(255, 255, 255, 0.1); 592 | } 593 | 594 | #system-prompt-editor { 595 | /* Better visibility in dark mode */ 596 | background-color: rgba(0, 0, 0, 0.3); 597 | border-color: rgba(255, 255, 255, 0.2); 598 | } 599 | } 600 | 601 | /* Animation for modal appearance */ 602 | @keyframes modalFadeIn { 603 | from { 604 | opacity: 0; 605 | transform: translateY(5px); 606 | } 607 | to { 608 | opacity: 1; 609 | transform: translateY(0); 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /css/theme-variables.css: -------------------------------------------------------------------------------- 1 | /* Theme Variables for LMSA 2 | * This file contains CSS variables for both light and dark themes 3 | */ 4 | 5 | /* Ensure custom-dark-mode class uses the dark theme variables */ 6 | .custom-dark-mode { 7 | /* Background colors */ 8 | --bg-primary: #121212; 9 | --bg-secondary: #1e1e1e; 10 | --bg-tertiary: #2c2c2c; 11 | --bg-gradient: linear-gradient(135deg, #121212 0%, #1e1e1e 100%); 12 | --modal-bg: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); 13 | --sidebar-bg: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); 14 | 15 | /* Text colors */ 16 | --text-primary: #e0e0e0; 17 | --text-secondary: #bdc3c7; 18 | --text-muted: #a0a0a0; 19 | 20 | /* Border colors */ 21 | --border-color: rgba(148, 163, 184, 0.2); 22 | --border-color-hover: rgba(148, 163, 184, 0.4); 23 | 24 | /* UI element colors */ 25 | --user-message-bg: #3498db; 26 | --user-message-text: #ffffff; 27 | --ai-message-bg: #2c3e50; 28 | --ai-message-text: #e0e0e0; 29 | --ai-message-border: #34495e; 30 | --toggle-bg: rgba(15, 23, 42, 0.8); 31 | --toggle-border: rgba(148, 163, 184, 0.3); 32 | --toggle-dot: #f8fafc; 33 | --toggle-active-bg: #3b82f6; 34 | --toggle-active-border: rgba(59, 130, 246, 0.7); 35 | --model-display-bg: #2c3e50; 36 | --model-display-text: #00ff00; 37 | --loading-indicator: #3498db; 38 | --chat-item-bg: rgba(30, 41, 59, 0.5); 39 | --chat-item-border: rgba(255, 255, 255, 0.1); 40 | --chat-item-text: #e0e0e0; 41 | --chat-item-hover-bg: rgba(59, 130, 246, 0.2); 42 | --scrollbar-track: rgba(15, 23, 42, 0.3); 43 | --scrollbar-thumb: rgba(96, 165, 250, 0.3); 44 | --scrollbar-thumb-hover: rgba(96, 165, 250, 0.5); 45 | --header-bg: #0f172a; /* Original dark header color */ 46 | --header-text: #e0e0e0; 47 | --header-icon: #e0e0e0; 48 | --header-icon-hover: #3b82f6; 49 | --header-border: #1e293b; /* Original dark header border */ 50 | --input-bg: #2c2c2c; 51 | --input-border: #34495e; 52 | --input-text: #e0e0e0; 53 | --input-placeholder: #a0a0a0; 54 | --sidebar-text: #e0e0e0; 55 | --sidebar-item-hover: rgba(59, 130, 246, 0.2); 56 | --sidebar-border: rgba(255, 255, 255, 0.1); 57 | --modal-overlay: rgba(0, 0, 0, 0.75); 58 | --modal-text: #e0e0e0; 59 | --modal-title: #ffffff; 60 | --title-accent: #3b82f6; 61 | --title-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 5px rgba(59, 130, 246, 0.15); 62 | --title-border: rgba(59, 130, 246, 0.3); 63 | --title-gradient: linear-gradient(90deg, 64 | var(--header-text), 65 | color-mix(in srgb, var(--header-text) 80%, var(--title-accent) 20%) 50%, 66 | var(--header-text) 100%); 67 | --settings-title-border: rgba(59, 130, 246, 0.3); 68 | --settings-label-bg: rgba(30, 41, 59, 0.5); 69 | --settings-label-text: #e0e0e0; 70 | --settings-input-bg: #0a192f; /* Original dark input background */ 71 | --settings-input-border: #34495e; 72 | --settings-input-text: #e0e0e0; 73 | --settings-help-text: #a0a0a0; 74 | 75 | /* Button colors */ 76 | --button-primary-bg: #3b82f6; 77 | --button-primary-hover: #2563eb; 78 | --button-secondary-bg: #6b7280; 79 | --button-secondary-hover: #4b5563; 80 | --button-danger-bg: #ef4444; 81 | --button-danger-hover: #dc2626; 82 | --button-success-bg: #10b981; 83 | --button-success-hover: #059669; 84 | } 85 | 86 | :root { 87 | /* Default theme is dark */ 88 | /* Background colors */ 89 | --bg-primary: #121212; 90 | --bg-secondary: #1e1e1e; 91 | --bg-tertiary: #2c2c2c; 92 | --bg-gradient: linear-gradient(135deg, #121212 0%, #1e1e1e 100%); 93 | --modal-bg: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); 94 | --sidebar-bg: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); 95 | 96 | /* Text colors */ 97 | --text-primary: #e0e0e0; 98 | --text-secondary: #bdc3c7; 99 | --text-muted: #a0a0a0; 100 | 101 | /* Border colors */ 102 | --border-color: rgba(148, 163, 184, 0.2); 103 | --border-color-hover: rgba(148, 163, 184, 0.4); 104 | 105 | /* UI element colors */ 106 | --user-message-bg: #3498db; 107 | --user-message-text: #ffffff; 108 | --ai-message-bg: #2c3e50; 109 | --ai-message-text: #e0e0e0; 110 | --ai-message-border: #34495e; 111 | --toggle-bg: rgba(15, 23, 42, 0.8); 112 | --toggle-border: rgba(148, 163, 184, 0.3); 113 | --toggle-dot: #f8fafc; 114 | --toggle-active-bg: #3b82f6; 115 | --toggle-active-border: rgba(59, 130, 246, 0.7); 116 | --model-display-bg: #2c3e50; 117 | --model-display-text: #00ff00; 118 | --loading-indicator: #3498db; 119 | --chat-item-bg: rgba(30, 41, 59, 0.5); 120 | --chat-item-border: rgba(255, 255, 255, 0.1); 121 | --chat-item-text: #e0e0e0; 122 | --chat-item-hover-bg: rgba(59, 130, 246, 0.2); 123 | --scrollbar-track: rgba(15, 23, 42, 0.3); 124 | --scrollbar-thumb: rgba(96, 165, 250, 0.3); 125 | --scrollbar-thumb-hover: rgba(96, 165, 250, 0.5); 126 | --header-bg: #0f172a; /* Original dark header color */ 127 | --header-text: #e0e0e0; 128 | --header-icon: #e0e0e0; 129 | --header-icon-hover: #3b82f6; 130 | --header-border: #1e293b; /* Original dark header border */ 131 | --input-bg: #2c2c2c; 132 | --input-border: #34495e; 133 | --input-text: #e0e0e0; 134 | --input-placeholder: #a0a0a0; 135 | --sidebar-text: #e0e0e0; 136 | --sidebar-item-hover: rgba(59, 130, 246, 0.2); 137 | --sidebar-border: rgba(255, 255, 255, 0.1); 138 | --modal-overlay: rgba(0, 0, 0, 0.75); 139 | --modal-text: #e0e0e0; 140 | --modal-title: #ffffff; 141 | --title-accent: #3b82f6; 142 | --title-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); 143 | --title-border: rgba(59, 130, 246, 0.3); 144 | --settings-title-border: rgba(59, 130, 246, 0.3); 145 | --settings-label-bg: rgba(30, 41, 59, 0.5); 146 | --settings-label-text: #e0e0e0; 147 | --settings-input-bg: #0a192f; /* Original dark input background */ 148 | --settings-input-border: #34495e; 149 | --settings-input-text: #e0e0e0; 150 | --settings-help-text: #a0a0a0; 151 | --info-color: #3b82f6; /* Info color for informational elements */ 152 | 153 | /* Button colors */ 154 | --button-primary-bg: #3b82f6; 155 | --button-primary-hover: #2563eb; 156 | --button-secondary-bg: #6b7280; 157 | --button-secondary-hover: #4b5563; 158 | --button-danger-bg: #ef4444; 159 | --button-danger-hover: #dc2626; 160 | --button-success-bg: #10b981; 161 | --button-success-hover: #059669; 162 | } 163 | 164 | /* Light theme variables */ 165 | body.light-theme { 166 | /* Background colors */ 167 | --bg-primary: #f8fafc; 168 | --bg-secondary: #f1f5f9; 169 | --bg-tertiary: #e2e8f0; 170 | --bg-gradient: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); 171 | --modal-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); 172 | --sidebar-bg: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%); 173 | 174 | /* Text colors */ 175 | --text-primary: #1e293b; 176 | --text-secondary: #334155; 177 | --text-muted: #64748b; 178 | 179 | /* Border colors */ 180 | --border-color: rgba(100, 116, 139, 0.2); 181 | --border-color-hover: rgba(100, 116, 139, 0.4); 182 | 183 | /* UI element colors */ 184 | --user-message-bg: #3b82f6; 185 | --user-message-text: #ffffff; 186 | --ai-message-bg: #e2e8f0; 187 | --ai-message-text: #1e293b; 188 | --ai-message-border: #cbd5e1; 189 | --toggle-bg: rgba(203, 213, 225, 0.8); 190 | --toggle-border: rgba(100, 116, 139, 0.3); 191 | --toggle-dot: #ffffff; 192 | --toggle-active-bg: #3b82f6; 193 | --toggle-active-border: rgba(59, 130, 246, 0.7); 194 | --model-display-bg: #e2e8f0; 195 | --model-display-text: #047857; 196 | --loading-indicator: #3b82f6; 197 | --chat-item-bg: rgba(241, 245, 249, 0.8); 198 | --chat-item-border: rgba(100, 116, 139, 0.2); 199 | --chat-item-text: #1e293b; 200 | --chat-item-hover-bg: rgba(224, 231, 255, 0.6); 201 | --scrollbar-track: rgba(203, 213, 225, 0.5); 202 | --scrollbar-thumb: rgba(59, 130, 246, 0.3); 203 | --scrollbar-thumb-hover: rgba(59, 130, 246, 0.5); 204 | --header-bg: #f8fafc; 205 | --header-text: #1e293b; 206 | --header-icon: #000000; 207 | --header-icon-hover: #3b82f6; 208 | --header-border: #cbd5e1; 209 | --input-bg: #f8fafc; 210 | --input-border: #cbd5e1; 211 | --input-text: #1e293b; 212 | --input-placeholder: #94a3b8; 213 | --sidebar-text: #1e293b; 214 | --sidebar-item-hover: rgba(59, 130, 246, 0.1); 215 | --sidebar-border: #cbd5e1; 216 | --modal-overlay: rgba(15, 23, 42, 0.5); 217 | --modal-text: #1e293b; 218 | --modal-title: #1e293b; 219 | --title-accent: #2563eb; 220 | --title-shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 0 5px rgba(37, 99, 235, 0.1); 221 | --title-border: rgba(59, 130, 246, 0.3); 222 | --title-gradient: linear-gradient(90deg, 223 | var(--header-text), 224 | color-mix(in srgb, var(--header-text) 80%, var(--title-accent) 20%) 50%, 225 | var(--header-text) 100%); 226 | --settings-title-border: rgba(59, 130, 246, 0.3); 227 | --settings-label-bg: rgba(241, 245, 249, 0.7); 228 | --settings-label-text: #1e293b; 229 | --settings-input-bg: #f8fafc; 230 | --settings-input-border: #cbd5e1; 231 | --settings-input-text: #1e293b; 232 | --settings-help-text: #64748b; 233 | --info-color: #2563eb; /* Info color for informational elements */ 234 | 235 | /* Button colors remain mostly the same for consistency */ 236 | --button-primary-bg: #3b82f6; 237 | --button-primary-hover: #2563eb; 238 | --button-secondary-bg: #64748b; 239 | --button-secondary-hover: #475569; 240 | --button-danger-bg: #ef4444; 241 | --button-danger-hover: #dc2626; 242 | --button-success-bg: #10b981; 243 | --button-success-hover: #059669; 244 | } 245 | -------------------------------------------------------------------------------- /delete-all-chats.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | // This file is kept for backward compatibility but the functionality has been moved to event-handlers.js 3 | // The delete-all-confirmation-modal is no longer used to prevent duplicate confirmation dialogs 4 | console.log('delete-all-chats.js: Functionality moved to event-handlers.js'); 5 | }); 6 | -------------------------------------------------------------------------------- /empty-message-modal.js: -------------------------------------------------------------------------------- 1 | // Import the checkAndShowWelcomeMessage function 2 | import { checkAndShowWelcomeMessage } from './js/ui-manager.js'; 3 | import { getUploadedFiles } from './js/file-upload.js'; 4 | 5 | // Global variable to track if a long-press is in progress on the send button 6 | window.isSendButtonLongPressInProgress = false; 7 | 8 | document.addEventListener('DOMContentLoaded', () => { 9 | const emptyMessageModal = document.getElementById('empty-message-modal'); 10 | const closeModalButton = emptyMessageModal.querySelector('.close-modal'); 11 | const chatForm = document.getElementById('chat-form'); 12 | const userInput = document.getElementById('user-input'); 13 | 14 | // Function to show the modal 15 | function showEmptyMessageModal() { 16 | emptyMessageModal.classList.remove('hidden'); 17 | emptyMessageModal.classList.add('active'); 18 | } 19 | 20 | // Function to hide the modal 21 | function hideEmptyMessageModal() { 22 | emptyMessageModal.classList.remove('active'); 23 | setTimeout(() => { 24 | emptyMessageModal.classList.add('hidden'); 25 | 26 | // Check if welcome message should be shown 27 | checkAndShowWelcomeMessage(); 28 | }, 300); // Match the transition duration 29 | } 30 | 31 | // Handle empty message check before form submission 32 | userInput.addEventListener('keypress', (e) => { 33 | if (e.key === 'Enter' && !e.shiftKey) { 34 | const messageContent = userInput.value.trim(); 35 | // Check for empty message and no files 36 | const hasUploadedFiles = getUploadedFiles() && getUploadedFiles().length > 0; 37 | 38 | if (messageContent === '' && !hasUploadedFiles) { 39 | e.preventDefault(); 40 | showEmptyMessageModal(); 41 | userInput.focus(); 42 | } 43 | } 44 | }); 45 | 46 | // Handle empty message check for send button 47 | const sendButton = document.getElementById('send-button'); 48 | if (sendButton) { 49 | sendButton.addEventListener('click', (e) => { 50 | // Check if the context menu is visible 51 | const sendContextMenu = document.getElementById('send-context-menu'); 52 | const isContextMenuVisible = sendContextMenu && 53 | sendContextMenu.style.display === 'block'; 54 | 55 | // Skip empty message check if a long-press is in progress or context menu is visible 56 | if (window.isSendButtonLongPressInProgress || isContextMenuVisible) { 57 | console.log('Skipping empty message check - long press detected or context menu visible'); 58 | return; 59 | } 60 | 61 | const messageContent = userInput.value.trim(); 62 | // Check for uploaded files - if files are present, allow submission even with empty message 63 | const uploadedFiles = getUploadedFiles(); 64 | const hasUploadedFiles = uploadedFiles && uploadedFiles.length > 0; 65 | 66 | console.log('Send button clicked:', { 67 | message: messageContent, 68 | hasUploadedFiles: hasUploadedFiles, 69 | filesCount: hasUploadedFiles ? uploadedFiles.length : 0 70 | }); 71 | 72 | if (messageContent === '' && !hasUploadedFiles) { 73 | // Only prevent default if the message is empty and no files 74 | e.preventDefault(); 75 | e.stopPropagation(); // Stop event propagation 76 | showEmptyMessageModal(); 77 | userInput.focus(); 78 | } 79 | // If the message is not empty or files are attached, let the form submission proceed normally 80 | }); 81 | } 82 | 83 | // Close modal when clicking the close button 84 | closeModalButton.addEventListener('click', hideEmptyMessageModal); 85 | 86 | // Close modal when clicking outside 87 | emptyMessageModal.addEventListener('click', (e) => { 88 | if (e.target === emptyMessageModal) { 89 | hideEmptyMessageModal(); 90 | } 91 | }); 92 | 93 | // Close modal when pressing Escape key 94 | document.addEventListener('keydown', (e) => { 95 | if (e.key === 'Escape' && emptyMessageModal.classList.contains('active')) { 96 | hideEmptyMessageModal(); 97 | } 98 | }); 99 | }); -------------------------------------------------------------------------------- /file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techcow2/LMSA/490959b2154fdc7ed4bd0e8c252698aacdfe9a45/file.png -------------------------------------------------------------------------------- /help.js: -------------------------------------------------------------------------------- 1 | // Import required functions 2 | import { checkAndShowWelcomeMessage } from './js/ui-manager.js'; 3 | import { hideSettingsModal } from './js/settings-modal-manager.js'; 4 | import { showExternalSiteModal } from './external-site-confirmation-modal.js'; 5 | 6 | document.addEventListener('DOMContentLoaded', () => { 7 | const helpBtn = document.getElementById('help-btn'); 8 | const helpIconBtn = document.getElementById('help-icon-button'); 9 | const helpModal = document.getElementById('help-modal'); 10 | const closeHelpBtn = document.getElementById('close-help'); 11 | const tutorialVideoBtn = document.getElementById('tutorial-video-btn'); 12 | const supportArticlesBtn = document.getElementById('support-articles-btn'); 13 | const settingsHelpBtn = document.getElementById('settings-help-btn'); 14 | const sidebarElement = document.getElementById('sidebar'); 15 | const modalContent = helpModal ? helpModal.querySelector('.modal-content') : null; 16 | const openSettingsLink = document.getElementById('open-settings-link'); 17 | const settingsModal = document.getElementById('settings-modal'); 18 | 19 | // Contact support modal elements 20 | const needMoreHelpLink = document.getElementById('need-more-help-link'); 21 | const contactFormModal = document.getElementById('contact-form-modal'); 22 | const closeContactFormBtn = document.getElementById('close-contact-form'); 23 | const contactFormModalContent = contactFormModal ? contactFormModal.querySelector('.modal-content') : null; 24 | const openEmailSupportBtn = document.getElementById('open-email-support'); 25 | 26 | // Function to generate email support link 27 | function generateSupportEmail() { 28 | const supportEmail = 'help@techray.on.spiceworks.com'; 29 | const subject = 'LMSA App Technical Support'; 30 | 31 | // Create a user-friendly email template with simple questions 32 | const emailBody = `Hello LMSA Support Team, 33 | 34 | I need help with the LMSA app. Please see my information below: 35 | 36 | 1. What device are you using? (phone, tablet, etc.) 37 | Answer: 38 | 39 | 2. What problem are you having? 40 | Answer: 41 | 42 | 3. When did this problem start? 43 | Answer: 44 | 45 | 4. Does this happen every time you use the app? 46 | Answer: 47 | 48 | 5. Have you tried closing and reopening the app? 49 | Answer: 50 | 51 | 6. Any other details you think might help: 52 | Answer: 53 | 54 | 📷 SCREENSHOT: If possible, please attach a screenshot of the issue. This helps us understand the problem much faster! 55 | 56 | Thank you for your help! 57 | 58 | [Your Name]`; 59 | 60 | // Encode the email components for URL 61 | const encodedSubject = encodeURIComponent(subject); 62 | const encodedBody = encodeURIComponent(emailBody); 63 | 64 | // Create the mailto link 65 | const mailtoLink = `mailto:${supportEmail}?subject=${encodedSubject}&body=${encodedBody}`; 66 | 67 | return mailtoLink; 68 | } 69 | 70 | // Function to close sidebar 71 | function closeSidebar() { 72 | if (sidebarElement) { 73 | sidebarElement.classList.add('hidden'); 74 | sidebarElement.classList.remove('active'); 75 | document.body.classList.remove('sidebar-open'); 76 | 77 | // Also close the options container 78 | const optionsContainer = document.getElementById('options-container'); 79 | if (optionsContainer) { 80 | optionsContainer.classList.add('hidden'); 81 | optionsContainer.classList.remove('animate-fade-in'); 82 | } 83 | 84 | // Remove the sidebar overlay 85 | const sidebarOverlay = document.getElementById('sidebar-overlay'); 86 | if (sidebarOverlay) { 87 | sidebarOverlay.classList.remove('active'); 88 | sidebarOverlay.classList.add('hidden'); 89 | } 90 | 91 | // Collapse all sections when sidebar is closed 92 | const sectionHeaders = sidebarElement.querySelectorAll('.section-header'); 93 | const chatHistorySection = sidebarElement.querySelector('.sidebar-section:last-child'); 94 | sectionHeaders.forEach(header => { 95 | header.classList.remove('active'); 96 | const content = header.nextElementSibling; 97 | if (content && content.classList.contains('collapsible-content')) { 98 | content.classList.remove('show'); 99 | } 100 | }); 101 | 102 | // Ensure chat history is visible when sidebar is closed 103 | if (chatHistorySection) { 104 | chatHistorySection.classList.remove('chat-history-hidden'); 105 | } 106 | } 107 | } 108 | 109 | // Function to close help modal 110 | function closeHelpModal() { 111 | if (helpModal && modalContent) { 112 | modalContent.classList.add('animate-modal-out'); 113 | setTimeout(() => { 114 | helpModal.classList.add('hidden'); 115 | modalContent.classList.remove('animate-modal-out'); 116 | 117 | // Check if welcome message should be shown 118 | checkAndShowWelcomeMessage(); 119 | }, 300); 120 | } 121 | } 122 | 123 | // Function to open help modal 124 | function openHelpModal() { 125 | if (helpModal && modalContent) { 126 | // Close the sidebar first 127 | closeSidebar(); 128 | // Show the help modal 129 | helpModal.classList.remove('hidden'); 130 | modalContent.classList.add('animate-modal-in'); 131 | 132 | // Reset scroll position to top 133 | const scrollableContent = helpModal.querySelector('.overflow-y-auto'); 134 | if (scrollableContent) { 135 | scrollableContent.scrollTop = 0; 136 | } 137 | 138 | setTimeout(() => { 139 | modalContent.classList.remove('animate-modal-in'); 140 | }, 300); 141 | } 142 | } 143 | 144 | // Function to open contact support modal 145 | function openContactFormModal() { 146 | if (contactFormModal && contactFormModalContent) { 147 | // Close the help modal first 148 | closeHelpModal(); 149 | // Show the contact support modal 150 | contactFormModal.classList.remove('hidden'); 151 | contactFormModalContent.classList.add('animate-modal-in'); 152 | // Use a slightly longer duration for a smoother effect 153 | setTimeout(() => { 154 | contactFormModalContent.classList.remove('animate-modal-in'); 155 | }, 400); 156 | } 157 | } 158 | 159 | // Function to close contact support modal 160 | function closeContactFormModal() { 161 | if (contactFormModal && contactFormModalContent) { 162 | contactFormModalContent.classList.add('animate-modal-out'); 163 | setTimeout(() => { 164 | contactFormModal.classList.add('hidden'); 165 | contactFormModalContent.classList.remove('animate-modal-out'); 166 | 167 | // Check if welcome message should be shown 168 | checkAndShowWelcomeMessage(); 169 | }, 300); 170 | } 171 | } 172 | 173 | // Event listeners 174 | if (helpBtn) { 175 | helpBtn.addEventListener('click', openHelpModal); 176 | } 177 | 178 | // Help icon button in header - ensure this listener works regardless of other elements 179 | if (helpIconBtn) { 180 | helpIconBtn.addEventListener('click', openHelpModal); 181 | console.log('Help icon button event listener attached'); 182 | } 183 | 184 | // Settings help button - closes Settings modal and opens Help modal 185 | if (settingsHelpBtn) { 186 | settingsHelpBtn.addEventListener('click', () => { 187 | // First close the settings modal 188 | const settingsModal = document.getElementById('settings-modal'); 189 | if (settingsModal) { 190 | // Close the settings modal using the imported function 191 | hideSettingsModal(); 192 | 193 | // Wait a short time for the settings modal to close before opening help 194 | setTimeout(() => { 195 | openHelpModal(); 196 | }, 100); 197 | } else { 198 | // If settings modal isn't found, just open help modal 199 | openHelpModal(); 200 | } 201 | }); 202 | console.log('Settings help button event listener attached'); 203 | } 204 | 205 | // Only proceed with other modal functionality if required elements exist 206 | if (helpModal && closeHelpBtn) { 207 | if (closeHelpBtn) { 208 | closeHelpBtn.addEventListener('click', closeHelpModal); 209 | } 210 | 211 | // Tutorial video button event listener 212 | if (tutorialVideoBtn) { 213 | tutorialVideoBtn.addEventListener('click', () => { 214 | // Show confirmation modal before opening YouTube video 215 | const youtubeUrl = 'https://www.youtube.com/watch?v=qoXfa6In5BM&pp=ygUMbG1zYSBhbmRyb2lk'; 216 | showExternalSiteModal(youtubeUrl); 217 | }); 218 | } 219 | 220 | // Support Articles button event listener 221 | if (supportArticlesBtn) { 222 | supportArticlesBtn.addEventListener('click', () => { 223 | // Show confirmation modal before opening support articles 224 | const supportUrl = 'https://lmsa.app/support.html'; 225 | showExternalSiteModal(supportUrl); 226 | }); 227 | } 228 | 229 | // Settings link event listener 230 | if (openSettingsLink && settingsModal) { 231 | openSettingsLink.addEventListener('click', (e) => { 232 | e.preventDefault(); 233 | // Close help modal 234 | closeHelpModal(); 235 | 236 | // Show the settings modal using classes instead of inline styles 237 | settingsModal.classList.remove('hidden'); 238 | settingsModal.classList.add('show'); 239 | settingsModal.classList.remove('hide'); 240 | 241 | // Only set minimal inline styles that don't conflict with our CSS 242 | document.body.style.overflow = 'hidden'; // Prevent scrolling when modal is open 243 | 244 | const settingsModalContent = settingsModal.querySelector('.modal-content'); 245 | if (settingsModalContent) { 246 | settingsModalContent.classList.add('animate-modal-in'); 247 | setTimeout(() => { 248 | settingsModalContent.classList.remove('animate-modal-in'); 249 | }, 300); 250 | } 251 | }); 252 | } 253 | 254 | // Need more help link event listener 255 | if (needMoreHelpLink) { 256 | needMoreHelpLink.addEventListener('click', (e) => { 257 | e.preventDefault(); 258 | openContactFormModal(); 259 | }); 260 | } 261 | 262 | // Close modal when clicking outside - REMOVED 263 | // helpModal.addEventListener('click', (e) => { 264 | // if (e.target === helpModal) { 265 | // closeHelpModal(); 266 | // } 267 | // }); 268 | 269 | // Close modal with Escape key - REMOVED 270 | // document.addEventListener('keydown', (e) => { 271 | // if (e.key === 'Escape' && !helpModal.classList.contains('hidden')) { 272 | // closeHelpModal(); 273 | // } 274 | // }); 275 | } else { 276 | console.warn('Some help modal elements are missing in the DOM'); 277 | } 278 | 279 | // Contact support modal event listeners 280 | if (contactFormModal && closeContactFormBtn) { 281 | closeContactFormBtn.addEventListener('click', closeContactFormModal); 282 | 283 | // Add email support button handler 284 | if (openEmailSupportBtn) { 285 | openEmailSupportBtn.addEventListener('click', () => { 286 | const emailLink = generateSupportEmail(); 287 | 288 | // Try to open the email link 289 | try { 290 | window.location.href = emailLink; 291 | } catch (error) { 292 | console.error('Error opening email client:', error); 293 | // Fallback: copy email address to clipboard if available 294 | if (navigator.clipboard) { 295 | navigator.clipboard.writeText('help@techray.on.spiceworks.com').then(() => { 296 | alert('Email address copied to clipboard: help@techray.on.spiceworks.com'); 297 | }).catch(() => { 298 | alert('Please email us at: help@techray.on.spiceworks.com'); 299 | }); 300 | } else { 301 | alert('Please email us at: help@techray.on.spiceworks.com'); 302 | } 303 | } 304 | }); 305 | } 306 | 307 | // Both outside click and Escape key closing functionality removed 308 | // The modal can now only be closed using the X button 309 | } 310 | }); 311 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techcow2/LMSA/490959b2154fdc7ed4bd0e8c252698aacdfe9a45/icon.png -------------------------------------------------------------------------------- /icon22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techcow2/LMSA/490959b2154fdc7ed4bd0e8c252698aacdfe9a45/icon22.png -------------------------------------------------------------------------------- /js/character-gallery-touch-handler.js: -------------------------------------------------------------------------------- 1 | // Touch handler for character gallery 2 | import { characterGalleryContainer, characterGalleryGrid } from './dom-elements.js'; 3 | import { debugLog } from './utils.js'; 4 | 5 | /** 6 | * Initializes touch handling for the character gallery 7 | * Improves touch interaction on mobile devices 8 | */ 9 | export function initializeCharacterGalleryTouchHandler() { 10 | if (!characterGalleryContainer) return; 11 | 12 | debugLog('Initializing character gallery touch handler'); 13 | 14 | // Variables to track touch state 15 | let touchStartY = 0; 16 | let touchStartX = 0; 17 | let touchStartTime = 0; 18 | let isTouchMoving = false; 19 | let touchedElement = null; 20 | let preventNextClick = false; 21 | let lastTouchY = 0; 22 | let scrollVelocity = 0; 23 | let momentumFrame = null; 24 | let velocityHistory = []; // Array to store recent velocity measurements 25 | let lastScrollTop = 0; 26 | let lastTouchTime = 0; 27 | 28 | // Constants for determining tap vs. drag 29 | const DRAG_THRESHOLD_PX = 10; // Pixels of movement to consider a drag 30 | const TAP_THRESHOLD_MS = 200; // Max milliseconds for a tap 31 | const DECELERATION = 0.90; // Momentum scrolling deceleration factor 32 | const MIN_VELOCITY = 0.8; // Minimum velocity to continue momentum scrolling 33 | 34 | // Add touch event listeners to the character gallery container 35 | characterGalleryContainer.addEventListener('touchstart', handleTouchStart, { passive: false }); 36 | characterGalleryContainer.addEventListener('touchmove', handleTouchMove, { passive: false }); 37 | characterGalleryContainer.addEventListener('touchend', handleTouchEnd, { passive: false }); 38 | characterGalleryContainer.addEventListener('touchcancel', handleTouchEnd, { passive: false }); 39 | 40 | // Add a click event listener to handle direct clicks and taps 41 | characterGalleryContainer.addEventListener('click', handleClick, { passive: false }); 42 | 43 | /** 44 | * Handles the touch start event 45 | * @param {TouchEvent} e - The touch event 46 | */ 47 | function handleTouchStart(e) { 48 | if (e.touches.length !== 1) return; // Only handle single touches 49 | 50 | // Cancel any ongoing momentum scrolling 51 | if (momentumFrame) { 52 | cancelAnimationFrame(momentumFrame); 53 | momentumFrame = null; 54 | } 55 | 56 | const touch = e.touches[0]; 57 | touchStartY = touch.clientY; 58 | touchStartX = touch.clientX; 59 | lastTouchY = touchStartY; 60 | touchStartTime = Date.now(); 61 | lastTouchTime = touchStartTime; 62 | isTouchMoving = false; 63 | scrollVelocity = 0; 64 | velocityHistory = []; 65 | lastScrollTop = characterGalleryContainer.scrollTop; 66 | 67 | // Store the touched button if any (exclude the non-interactive indicator) 68 | touchedElement = e.target.closest('.character-card-btn, .character-card, .character-gallery-back-btn, .character-gallery-create-btn'); 69 | 70 | // Don't add touch-active to the indicator since it's not interactive 71 | if (touchedElement && (touchedElement.classList.contains('character-card-indicator') || 72 | touchedElement.closest('.character-card-indicator'))) { 73 | touchedElement = null; 74 | } 75 | 76 | // Add active state to the touched button 77 | if (touchedElement) { 78 | touchedElement.classList.add('touch-active'); 79 | } 80 | 81 | // Prevent default behavior to avoid page scrolling 82 | // This is important for touch scrolling to work properly 83 | e.stopPropagation(); 84 | } 85 | 86 | /** 87 | * Handles the touch move event 88 | * @param {TouchEvent} e - The touch event 89 | */ 90 | function handleTouchMove(e) { 91 | if (e.touches.length !== 1) return; 92 | 93 | const touch = e.touches[0]; 94 | const currentY = touch.clientY; 95 | const currentX = touch.clientX; 96 | const deltaY = touchStartY - currentY; 97 | const deltaX = Math.abs(touchStartX - currentX); 98 | const now = Date.now(); 99 | const timeDelta = now - lastTouchTime; 100 | 101 | // Calculate velocity (pixels per millisecond) 102 | if (timeDelta > 0) { 103 | const instantVelocity = (lastTouchY - currentY) / timeDelta; 104 | velocityHistory.push(instantVelocity); 105 | 106 | // Keep only the last 5 velocity measurements 107 | if (velocityHistory.length > 5) { 108 | velocityHistory.shift(); 109 | } 110 | } 111 | 112 | // If moved more than threshold, consider it a drag 113 | if (Math.abs(deltaY) > DRAG_THRESHOLD_PX || deltaX > DRAG_THRESHOLD_PX) { 114 | isTouchMoving = true; 115 | 116 | // Remove active state from the touched button when dragging 117 | if (touchedElement) { 118 | touchedElement.classList.remove('touch-active'); 119 | } 120 | } 121 | 122 | // Update last touch position and time 123 | lastTouchY = currentY; 124 | lastTouchTime = now; 125 | 126 | // Prevent default behavior to avoid page scrolling 127 | // This is important for touch scrolling to work properly 128 | e.stopPropagation(); 129 | } 130 | 131 | /** 132 | * Handles the touch end event 133 | * @param {TouchEvent} e - The touch event 134 | */ 135 | function handleTouchEnd(e) { 136 | const touchDuration = Date.now() - touchStartTime; 137 | 138 | // Remove active state from the touched button 139 | if (touchedElement) { 140 | touchedElement.classList.remove('touch-active'); 141 | } 142 | 143 | // If it was a short touch and didn't move much, consider it a tap 144 | if (!isTouchMoving && touchDuration < TAP_THRESHOLD_MS && touchedElement) { 145 | // This was a tap - manually trigger the click event 146 | touchedElement.click(); 147 | } else if (isTouchMoving) { 148 | // This was a drag - prevent the next click 149 | preventNextClick = true; 150 | 151 | // Calculate average velocity from recent measurements 152 | if (velocityHistory.length > 0) { 153 | scrollVelocity = velocityHistory.reduce((sum, v) => sum + v, 0) / velocityHistory.length; 154 | 155 | // Amplify the velocity slightly to improve the feel of momentum scrolling 156 | scrollVelocity *= 1.2; 157 | 158 | // Start momentum scrolling if velocity is significant 159 | if (Math.abs(scrollVelocity) > MIN_VELOCITY) { 160 | momentumScroll(); 161 | } 162 | } 163 | 164 | // Reset the prevention after a short delay 165 | setTimeout(() => { 166 | preventNextClick = false; 167 | }, 300); 168 | } 169 | 170 | // Prevent default behavior to avoid page scrolling 171 | // This is important for touch scrolling to work properly 172 | e.stopPropagation(); 173 | } 174 | 175 | /** 176 | * Performs momentum scrolling animation 177 | */ 178 | function momentumScroll() { 179 | // Cancel any existing animation frame 180 | if (momentumFrame) { 181 | cancelAnimationFrame(momentumFrame); 182 | } 183 | 184 | // Apply deceleration to velocity 185 | scrollVelocity *= DECELERATION; 186 | 187 | // Calculate scroll amount based on velocity 188 | const scrollAmount = scrollVelocity * 16; // Assuming ~60fps (16ms per frame) 189 | 190 | // Apply scroll 191 | characterGalleryContainer.scrollTop += scrollAmount; 192 | 193 | // Continue animation if velocity is still significant 194 | if (Math.abs(scrollVelocity) > MIN_VELOCITY) { 195 | momentumFrame = requestAnimationFrame(momentumScroll); 196 | } else { 197 | momentumFrame = null; 198 | 199 | // Ensure we're not in the middle of an inertial scroll 200 | // This helps prevent the scrolling from stopping abruptly 201 | if (Math.abs(scrollVelocity) > 0.1) { 202 | scrollVelocity = 0; 203 | characterGalleryContainer.style.scrollBehavior = 'auto'; 204 | setTimeout(() => { 205 | characterGalleryContainer.style.scrollBehavior = 'smooth'; 206 | }, 50); 207 | } 208 | } 209 | } 210 | 211 | /** 212 | * Handles the click event 213 | * @param {MouseEvent} e - The click event 214 | */ 215 | function handleClick(e) { 216 | if (preventNextClick) { 217 | e.preventDefault(); 218 | e.stopPropagation(); 219 | } 220 | } 221 | 222 | // Add specific touch handling for character cards 223 | if (characterGalleryGrid) { 224 | // Delegate touch events for character cards 225 | characterGalleryGrid.addEventListener('touchstart', function(e) { 226 | const cardElement = e.target.closest('.character-card'); 227 | if (cardElement) { 228 | cardElement.classList.add('touch-active'); 229 | } 230 | 231 | // Don't stop propagation here to allow the container's touch handler to work 232 | }, { passive: false }); 233 | 234 | characterGalleryGrid.addEventListener('touchmove', function(e) { 235 | // If we're scrolling, remove the active state from all cards 236 | if (isTouchMoving) { 237 | const cardElements = characterGalleryGrid.querySelectorAll('.character-card.touch-active'); 238 | cardElements.forEach(card => card.classList.remove('touch-active')); 239 | } 240 | 241 | // Don't stop propagation here to allow the container's touch handler to work 242 | }, { passive: false }); 243 | 244 | characterGalleryGrid.addEventListener('touchend', function(e) { 245 | const cardElements = characterGalleryGrid.querySelectorAll('.character-card.touch-active'); 246 | cardElements.forEach(card => card.classList.remove('touch-active')); 247 | 248 | // Don't stop propagation here to allow the container's touch handler to work 249 | }, { passive: false }); 250 | 251 | characterGalleryGrid.addEventListener('touchcancel', function(e) { 252 | const cardElements = characterGalleryGrid.querySelectorAll('.character-card.touch-active'); 253 | cardElements.forEach(card => card.classList.remove('touch-active')); 254 | 255 | // Don't stop propagation here to allow the container's touch handler to work 256 | }, { passive: false }); 257 | } 258 | 259 | debugLog('Character gallery touch handler initialized with momentum scrolling'); 260 | } 261 | -------------------------------------------------------------------------------- /js/character-modal-touch-handler.js: -------------------------------------------------------------------------------- 1 | // Touch handler for character modal 2 | import { characterModal } from './dom-elements.js'; 3 | import { debugLog } from './utils.js'; 4 | 5 | /** 6 | * Initializes touch handling for the character modal 7 | * Improves touch interaction on mobile devices 8 | */ 9 | export function initializeCharacterModalTouchHandler() { 10 | if (!characterModal) return; 11 | 12 | debugLog('Initializing character modal touch handler'); 13 | 14 | // Get the modal body element 15 | const modalBody = characterModal.querySelector('.character-modal-body'); 16 | if (!modalBody) return; 17 | 18 | // Variables to track touch state 19 | let touchStartY = 0; 20 | let touchStartX = 0; 21 | let lastTouchY = 0; 22 | let scrollVelocity = 0; 23 | let momentumFrame = null; 24 | let velocityHistory = []; // Array to store recent velocity measurements 25 | let lastTouchTime = 0; 26 | 27 | // Constants for momentum scrolling 28 | const DECELERATION = 0.90; // Momentum scrolling deceleration factor 29 | const MIN_VELOCITY = 0.8; // Minimum velocity to continue momentum scrolling 30 | 31 | // Add touch event listeners to the modal body 32 | modalBody.addEventListener('touchstart', handleTouchStart, { passive: true }); 33 | modalBody.addEventListener('touchmove', handleTouchMove, { passive: true }); 34 | modalBody.addEventListener('touchend', handleTouchEnd, { passive: true }); 35 | modalBody.addEventListener('touchcancel', handleTouchEnd, { passive: true }); 36 | 37 | /** 38 | * Handles the touch start event 39 | * @param {TouchEvent} e - The touch event 40 | */ 41 | function handleTouchStart(e) { 42 | if (e.touches.length !== 1) return; // Only handle single touches 43 | 44 | // Cancel any ongoing momentum scrolling 45 | if (momentumFrame) { 46 | cancelAnimationFrame(momentumFrame); 47 | momentumFrame = null; 48 | } 49 | 50 | const touch = e.touches[0]; 51 | touchStartY = touch.clientY; 52 | touchStartX = touch.clientX; 53 | lastTouchY = touchStartY; 54 | lastTouchTime = Date.now(); 55 | scrollVelocity = 0; 56 | velocityHistory = []; 57 | 58 | // Prevent event propagation to allow scrolling 59 | e.stopPropagation(); 60 | } 61 | 62 | /** 63 | * Handles the touch move event 64 | * @param {TouchEvent} e - The touch event 65 | */ 66 | function handleTouchMove(e) { 67 | if (e.touches.length !== 1) return; 68 | 69 | const touch = e.touches[0]; 70 | const currentY = touch.clientY; 71 | const now = Date.now(); 72 | const timeDelta = now - lastTouchTime; 73 | 74 | // Calculate velocity (pixels per millisecond) 75 | if (timeDelta > 0) { 76 | const instantVelocity = (lastTouchY - currentY) / timeDelta; 77 | velocityHistory.push(instantVelocity); 78 | 79 | // Keep only the last 5 velocity measurements 80 | if (velocityHistory.length > 5) { 81 | velocityHistory.shift(); 82 | } 83 | } 84 | 85 | // Update last touch position and time 86 | lastTouchY = currentY; 87 | lastTouchTime = now; 88 | 89 | // Prevent event propagation to allow scrolling 90 | e.stopPropagation(); 91 | } 92 | 93 | /** 94 | * Handles the touch end event 95 | * @param {TouchEvent} e - The touch event 96 | */ 97 | function handleTouchEnd() { 98 | // Calculate average velocity from recent measurements 99 | if (velocityHistory.length > 0) { 100 | scrollVelocity = velocityHistory.reduce((sum, v) => sum + v, 0) / velocityHistory.length; 101 | 102 | // Start momentum scrolling if velocity is significant 103 | if (Math.abs(scrollVelocity) > MIN_VELOCITY) { 104 | momentumScroll(); 105 | } 106 | } 107 | } 108 | 109 | /** 110 | * Performs momentum scrolling animation 111 | */ 112 | function momentumScroll() { 113 | // Cancel any existing animation frame 114 | if (momentumFrame) { 115 | cancelAnimationFrame(momentumFrame); 116 | } 117 | 118 | // Apply deceleration to velocity 119 | scrollVelocity *= DECELERATION; 120 | 121 | // Calculate scroll amount based on velocity 122 | const scrollAmount = scrollVelocity * 16; // Assuming ~60fps (16ms per frame) 123 | 124 | // Apply scroll 125 | modalBody.scrollTop += scrollAmount; 126 | 127 | // Continue animation if velocity is still significant 128 | if (Math.abs(scrollVelocity) > MIN_VELOCITY) { 129 | momentumFrame = requestAnimationFrame(momentumScroll); 130 | } else { 131 | momentumFrame = null; 132 | } 133 | } 134 | 135 | debugLog('Character modal touch handler initialized'); 136 | } 137 | -------------------------------------------------------------------------------- /js/chat-history-touch-handler.js: -------------------------------------------------------------------------------- 1 | // Touch handler for chat history scrolling with optimized performance 2 | import { chatHistory } from './dom-elements.js'; 3 | import { loadChat } from './chat-service.js'; 4 | import { throttle, optimizedAnimation, cancelOptimizedAnimation, addHardwareAcceleration } from './performance-utils.js'; 5 | 6 | /** 7 | * Initializes touch handling for the chat history container 8 | * Allows scrolling by dragging anywhere in the chat history 9 | * while still allowing taps to select chats 10 | */ 11 | export function initializeChatHistoryTouchHandler() { 12 | if (!chatHistory) return; 13 | 14 | // Add hardware acceleration to improve scrolling performance 15 | addHardwareAcceleration(chatHistory); 16 | 17 | // Variables to track touch state 18 | let touchStartY = 0; 19 | let touchStartX = 0; 20 | let touchStartTime = 0; 21 | let isTouchMoving = false; 22 | let touchedElement = null; 23 | let preventNextClick = false; 24 | let lastTouchY = 0; 25 | let scrollVelocity = 0; 26 | let momentumFrame = null; 27 | let velocityHistory = []; // Array to store recent velocity measurements 28 | let lastVelocityUpdate = 0; 29 | 30 | // Constants for determining tap vs. drag 31 | const DRAG_THRESHOLD_PX = 8; // Slightly reduced threshold for more responsive feel 32 | const TAP_THRESHOLD_MS = 200; // Max milliseconds for a tap 33 | const DECELERATION = 0.92; // Slightly increased for smoother deceleration 34 | const MIN_VELOCITY = 0.6; // Slightly reduced for more responsive momentum scrolling 35 | const VELOCITY_UPDATE_INTERVAL = 16; // Only update velocity every 16ms (60fps) 36 | 37 | // Throttled touch move handler to prevent excessive calculations 38 | const throttledTouchMove = throttle(handleTouchMove, 8); // 8ms throttle (~120fps) 39 | 40 | // Add touch event listeners to the chat history container 41 | chatHistory.addEventListener('touchstart', handleTouchStart, { passive: true }); 42 | chatHistory.addEventListener('touchmove', throttledTouchMove, { passive: true }); // Keep as passive to allow native scrolling 43 | chatHistory.addEventListener('touchend', handleTouchEnd, { passive: true }); 44 | 45 | // Add a click event listener to handle direct clicks and taps 46 | chatHistory.addEventListener('click', handleClick, { passive: true }); 47 | 48 | /** 49 | * Handles the touch start event 50 | * @param {TouchEvent} e - The touch event 51 | */ 52 | function handleTouchStart(e) { 53 | if (e.touches.length !== 1) return; // Only handle single touches 54 | 55 | const touch = e.touches[0]; 56 | touchStartY = touch.clientY; 57 | touchStartX = touch.clientX; 58 | lastTouchY = touch.clientY; 59 | touchStartTime = Date.now(); 60 | isTouchMoving = false; 61 | touchedElement = e.target.closest('button'); // Store the touched button if any 62 | 63 | // Stop any ongoing momentum scrolling 64 | if (momentumFrame) { 65 | cancelAnimationFrame(momentumFrame); 66 | momentumFrame = null; 67 | } 68 | } 69 | 70 | /** 71 | * Handles the touch move event 72 | * @param {TouchEvent} e - The touch event 73 | */ 74 | function handleTouchMove(e) { 75 | if (e.touches.length !== 1) return; 76 | 77 | const touch = e.touches[0]; 78 | const deltaY = Math.abs(touch.clientY - touchStartY); 79 | const deltaX = Math.abs(touch.clientX - touchStartX); 80 | 81 | // Only update velocity at a controlled rate to avoid excessive calculations 82 | const now = Date.now(); 83 | if (now - lastVelocityUpdate >= VELOCITY_UPDATE_INTERVAL) { 84 | // Calculate scroll velocity for momentum scrolling 85 | const currentVelocity = touch.clientY - lastTouchY; 86 | 87 | // Only record significant movements to prevent micro-jitters from affecting momentum 88 | // Slightly reduced threshold for more responsive feel 89 | if (Math.abs(currentVelocity) > 0.3) { 90 | velocityHistory.push({ velocity: currentVelocity, time: now }); 91 | 92 | // Keep only the last 7 velocity measurements for better momentum calculation 93 | // Increased from 5 to 7 for smoother momentum 94 | if (velocityHistory.length > 7) { 95 | velocityHistory.shift(); 96 | } 97 | } 98 | 99 | lastTouchY = touch.clientY; 100 | lastVelocityUpdate = now; 101 | } 102 | 103 | // If movement exceeds threshold, mark as dragging 104 | // Use a more responsive threshold check 105 | if (!isTouchMoving && (deltaY > DRAG_THRESHOLD_PX || deltaX > DRAG_THRESHOLD_PX)) { 106 | isTouchMoving = true; 107 | 108 | // Add a subtle haptic feedback when drag starts (if supported) 109 | if (navigator.vibrate && deltaY > DRAG_THRESHOLD_PX * 2) { 110 | try { 111 | navigator.vibrate(10); // Very subtle vibration 112 | } catch (e) { 113 | // Ignore errors if vibration is not supported 114 | } 115 | } 116 | } 117 | 118 | // Let the browser handle the scrolling natively 119 | // We're not manually adjusting scrollTop here for better performance 120 | } 121 | 122 | /** 123 | * Handles the touch end event 124 | * @param {TouchEvent} e - The touch event 125 | */ 126 | function handleTouchEnd() { 127 | const touchDuration = Date.now() - touchStartTime; 128 | 129 | // If it was a short touch and didn't move much, consider it a tap 130 | if (!isTouchMoving && touchDuration < TAP_THRESHOLD_MS && touchedElement) { 131 | // This was a tap - manually trigger the chat loading 132 | const chatId = touchedElement.dataset.chatId; 133 | if (chatId) { 134 | loadChat(chatId); 135 | } 136 | } else if (isTouchMoving) { 137 | // This was a drag - prevent the next click and start momentum scrolling 138 | preventNextClick = true; 139 | 140 | // Start momentum scrolling 141 | startMomentumScroll(scrollVelocity); 142 | 143 | // Reset the prevention after a short delay 144 | setTimeout(() => { 145 | preventNextClick = false; 146 | }, 300); 147 | } 148 | 149 | // Reset touch tracking 150 | touchedElement = null; 151 | isTouchMoving = false; 152 | } 153 | 154 | /** 155 | * Handles click events on the chat history 156 | * @param {MouseEvent} e - The click event 157 | */ 158 | function handleClick(e) { 159 | if (preventNextClick) { 160 | e.preventDefault(); 161 | e.stopPropagation(); 162 | return false; 163 | } 164 | } 165 | 166 | /** 167 | * Starts momentum scrolling with the given initial velocity 168 | * @param {number} initialVelocity - Initial scroll velocity 169 | */ 170 | function startMomentumScroll(initialVelocity) { 171 | // Calculate weighted average velocity from recent history for smoother momentum 172 | let finalVelocity = initialVelocity; 173 | 174 | if (velocityHistory.length > 0) { 175 | // Get the most recent velocity measurements (last 80ms for more responsive feel) 176 | const now = Date.now(); 177 | const recentVelocities = velocityHistory.filter(v => now - v.time < 80); 178 | 179 | if (recentVelocities.length > 0) { 180 | // Calculate weighted average with more recent velocities having higher weight 181 | let totalWeight = 0; 182 | let weightedSum = 0; 183 | 184 | recentVelocities.forEach((entry, index) => { 185 | // More recent entries get higher weight (exponential weighting) 186 | const weight = Math.pow(2, index); 187 | weightedSum += entry.velocity * weight; 188 | totalWeight += weight; 189 | }); 190 | 191 | finalVelocity = weightedSum / totalWeight; 192 | } 193 | } 194 | 195 | // Apply a scaling factor to make the momentum feel natural 196 | // Using a slightly higher factor for more responsive feel 197 | let velocity = finalVelocity * 0.9; 198 | 199 | // Cap the maximum velocity to prevent extremely fast scrolling 200 | const MAX_VELOCITY = 20; // Slightly increased for more responsive feel 201 | if (Math.abs(velocity) > MAX_VELOCITY) { 202 | velocity = velocity > 0 ? MAX_VELOCITY : -MAX_VELOCITY; 203 | } 204 | 205 | // Don't start momentum scrolling if velocity is too low 206 | if (Math.abs(velocity) < MIN_VELOCITY) { 207 | return; 208 | } 209 | 210 | // Use optimizedAnimation from performance-utils.js 211 | function momentumStep() { 212 | if (Math.abs(velocity) < MIN_VELOCITY) { 213 | cancelOptimizedAnimation(momentumFrame); 214 | momentumFrame = null; 215 | return; 216 | } 217 | 218 | // Use transform for smoother scrolling when possible 219 | // This avoids layout thrashing by not modifying scrollTop directly 220 | try { 221 | // Still use scrollTop for compatibility, but in a way that minimizes layout thrashing 222 | chatHistory.scrollTop -= velocity; 223 | } catch (e) { 224 | console.error('Error during momentum scrolling:', e); 225 | } 226 | 227 | // Apply a non-linear deceleration curve for more natural feeling 228 | // Faster at the beginning, slower as it comes to a stop 229 | velocity *= DECELERATION; 230 | 231 | // Apply additional deceleration as the scroll slows down 232 | if (Math.abs(velocity) < 3) { 233 | velocity *= 0.88; // Slightly increased for smoother stop 234 | } 235 | 236 | // Use optimizedAnimation for the next frame 237 | momentumFrame = optimizedAnimation(momentumStep); 238 | } 239 | 240 | // Cancel any existing animation before starting a new one 241 | if (momentumFrame) { 242 | cancelOptimizedAnimation(momentumFrame); 243 | } 244 | 245 | // Start the momentum scrolling animation 246 | momentumFrame = optimizedAnimation(momentumStep); 247 | 248 | // Clear velocity history after starting momentum 249 | velocityHistory = []; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /js/dom-elements.js: -------------------------------------------------------------------------------- 1 | // DOM Elements 2 | export const chatContainer = document.getElementById('chat-container'); 3 | export const messagesContainer = document.getElementById('messages'); 4 | export const chatForm = document.getElementById('chat-form'); 5 | export const userInput = document.getElementById('user-input'); 6 | export const serverIpInput = document.getElementById('server-ip'); 7 | export const serverPortInput = document.getElementById('server-port'); 8 | export const systemPromptInput = document.getElementById('system-prompt'); 9 | export const clearChatButton = document.getElementById('clear-chat'); 10 | export const loadingIndicator = document.getElementById('loading-indicator'); 11 | export const sidebar = document.getElementById('sidebar'); 12 | // New topic button is now in the send context menu 13 | export const newTopicButton = null; 14 | export const newChatButton = document.getElementById('new-chat'); 15 | export const chatHistory = document.getElementById('chat-history'); 16 | export const settingsButton = document.getElementById('settings-btn'); 17 | export const settingsIconButton = document.getElementById('settings-icon-button'); 18 | export const settingsModal = document.getElementById('settings-modal'); 19 | export const closeSettingsButton = document.getElementById('close-settings'); 20 | export const closeSettingsXButton = document.getElementById('close-settings-x'); 21 | export const welcomeMessage = document.getElementById('welcome-message'); 22 | export const sidebarToggle = document.getElementById('sidebar-toggle'); 23 | export const closeSidebarButton = document.getElementById('close-sidebar'); 24 | export const loadedModelDisplay = document.getElementById('loaded-model'); 25 | export const hideThinkingCheckbox = document.getElementById('hide-thinking'); 26 | export const autoGenerateTitlesCheckbox = document.getElementById('auto-generate-titles'); 27 | export const themeToggleCheckbox = document.getElementById('theme-toggle'); 28 | 29 | export const refreshButton = document.getElementById('refresh-button'); 30 | export const modelToggleButton = document.getElementById('model-toggle-button'); 31 | export const helpButton = document.getElementById('help-btn'); 32 | export const whatsNewButton = document.getElementById('whats-new-btn'); 33 | export const aboutButton = document.getElementById('about-btn'); 34 | export const modelButton = document.getElementById('model-btn'); 35 | export const helpIconButton = document.getElementById('help-icon-button'); 36 | 37 | // Character elements 38 | export const charactersButton = document.getElementById('characters-btn'); 39 | export const characterModal = document.getElementById('character-modal'); 40 | export const closeCharacterButton = document.getElementById('close-character'); 41 | 42 | export const createCharacterButton = document.getElementById('create-character-btn'); 43 | export const saveCharacterButton = document.getElementById('save-character'); 44 | export const characterNameInput = document.getElementById('character-name'); 45 | export const characterDescriptionInput = document.getElementById('character-description'); 46 | export const characterPersonalityInput = document.getElementById('character-personality'); 47 | export const characterScenarioInput = document.getElementById('character-scenario'); 48 | 49 | export const characterImageUpload = document.getElementById('character-image-upload'); 50 | export const characterCardUpload = document.getElementById('character-card-upload'); 51 | export const deleteCharacterButton = document.getElementById('delete-character'); 52 | export const characterPreviewImage = document.getElementById('character-preview-image'); 53 | export const characterForm = document.getElementById('character-form'); 54 | export const activeCharacterDisplay = document.getElementById('active-character-display'); 55 | 56 | // Delete character confirmation modal elements 57 | export const deleteCharacterModal = document.getElementById('delete-character-modal'); 58 | export const closeDeleteCharacterModalButton = document.getElementById('close-delete-character-modal'); 59 | export const cancelDeleteCharacterButton = document.getElementById('cancel-delete-character'); 60 | export const confirmDeleteCharacterButton = document.getElementById('confirm-delete-character'); 61 | export const deleteCharacterAvatar = document.getElementById('delete-character-avatar'); 62 | export const deleteCharacterName = document.getElementById('delete-character-name'); 63 | export const deleteCharacterDescription = document.getElementById('delete-character-description'); 64 | 65 | // Import/Export group elements 66 | export const importExportGroupButton = document.getElementById('import-export-group-btn'); 67 | export const importExportContainer = document.getElementById('import-export-container'); 68 | 69 | // Export/Import elements 70 | export const exportChatsButton = document.getElementById('export-chats-btn'); 71 | export const importChatsButton = document.getElementById('import-chats-btn'); 72 | export const importChatsInput = document.getElementById('import-chats-input'); 73 | export const importModal = document.getElementById('import-modal'); 74 | export const cancelImportButton = document.getElementById('cancel-import'); 75 | export const confirmImportButton = document.getElementById('confirm-import'); 76 | export const importStatusContainer = document.getElementById('import-status'); 77 | export const importStatusMessage = document.getElementById('import-status-message'); 78 | 79 | // Import Success modal elements 80 | export const importSuccessModal = document.getElementById('import-success-modal'); 81 | export const importSuccessMessage = document.getElementById('import-success-message'); 82 | export const closeImportSuccessButton = document.getElementById('close-import-success'); 83 | 84 | // Export Confirmation modal elements 85 | export const exportConfirmationModal = document.getElementById('export-confirmation-modal'); 86 | export const exportConfirmationMessage = document.getElementById('export-confirmation-message'); 87 | export const confirmExportButton = document.getElementById('confirm-export'); 88 | export const cancelExportButton = document.getElementById('cancel-export'); 89 | 90 | // Export Success modal elements 91 | export const exportSuccessModal = document.getElementById('export-success-modal'); 92 | export const exportSuccessMessage = document.getElementById('export-success-message'); 93 | export const closeExportSuccessButton = document.getElementById('close-export-success'); 94 | 95 | // File upload elements 96 | export const fileUploadButton = document.getElementById('file-upload-button'); 97 | export const paperclipButton = document.getElementById('paperclip-button'); 98 | export const fileUploadInput = document.getElementById('file-upload-input'); 99 | 100 | // Send and stop buttons 101 | export const sendButton = document.getElementById('send-button'); 102 | export const stopButton = document.getElementById('stop-button'); 103 | 104 | // Confirmation modal elements 105 | export const confirmationModal = document.getElementById('confirmation-modal'); 106 | export const confirmActionButton = document.getElementById('confirm-action'); 107 | export const cancelActionButton = document.getElementById('cancel-action'); 108 | export const confirmationMessage = document.getElementById('confirmation-message'); 109 | 110 | // Context menu elements 111 | export const contextMenu = document.getElementById('context-menu'); 112 | export const copyTextButton = document.getElementById('copy-text'); 113 | export const regenerateTextButton = document.getElementById('regenerate-text'); 114 | 115 | // Send button context menu elements 116 | export const sendContextMenu = document.getElementById('send-context-menu'); 117 | export const newTopicMenuButton = document.getElementById('new-topic-menu-button'); 118 | export const scrollToBottomMenuButton = document.getElementById('scroll-to-bottom-menu-button'); 119 | 120 | // Exit button 121 | export const exitButton = document.getElementById('exit-btn'); 122 | 123 | // Model modal elements 124 | export const modelModal = document.getElementById('model-modal'); 125 | export const closeModelButton = document.getElementById('close-model'); 126 | export const currentModelDisplay = document.getElementById('current-model'); 127 | export const availableModelsList = document.getElementById('available-models-list'); 128 | export const refreshModelsButton = document.getElementById('refresh-models-btn'); 129 | 130 | // Full Model Name modal elements 131 | export const fullModelNameModal = document.getElementById('full-model-name-modal'); 132 | export const closeFullModelNameButton = document.getElementById('close-full-model-name'); 133 | export const fullModelNameDisplay = document.getElementById('full-model-name'); 134 | export const modelHeaderIcon = document.getElementById('model-header-icon'); 135 | 136 | // Character Gallery elements 137 | export const characterGalleryButton = document.getElementById('character-gallery-btn'); 138 | export const characterGalleryContainer = document.getElementById('character-gallery-container'); 139 | export const characterGalleryBackButton = document.getElementById('character-gallery-back-btn'); 140 | export const characterGalleryCreateButton = document.getElementById('character-gallery-create-btn'); 141 | export const characterGalleryImportButton = document.getElementById('character-gallery-import-btn'); 142 | export const characterGalleryImportInput = document.getElementById('character-gallery-import-input'); 143 | export const characterGalleryGrid = document.getElementById('character-gallery-grid'); 144 | export const characterGalleryEmpty = document.getElementById('character-gallery-empty'); 145 | 146 | // Welcome screen buttons 147 | export const welcomeCharacterGalleryBtn = document.getElementById('welcome-character-gallery-btn'); 148 | export const welcomeModelsBtn = document.getElementById('welcome-models-btn'); 149 | export const welcomeNewChatBtn = document.getElementById('welcome-new-chat-btn'); 150 | export const welcomeHelpBtn = document.getElementById('welcome-help-btn'); 151 | 152 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | // Main entry point for the application 2 | import { loadServerSettings, fetchAvailableModels } from './api-service.js'; 3 | import { loadSettings } from './settings-manager.js'; 4 | import { loadChatHistory, loadChat, chatHistoryData } from './chat-service.js'; 5 | import { initializeFileUpload } from './file-upload.js'; 6 | import { initializeEventHandlers } from './event-handlers.js'; 7 | import { hideLoadingIndicatorOnLoad, ensureWelcomeMessagePosition, initializeCollapsibleSections } from './ui-manager.js'; 8 | import { initializeTouchHandlers } from './touch-handlers.js'; 9 | import { initializeChatHistoryTouchHandler } from './chat-history-touch-handler.js'; 10 | import { initializeSettingsModalTouchHandler } from './settings-modal-touch-handler.js'; 11 | import { initializeSidebarTouchHandler } from './sidebar-touch-handler.js'; 12 | import { handleScroll, setDebugEnabled, wasRefreshDueToCodeGeneration, getLastActiveChatId, clearRefreshDueToCodeGenerationFlag } from './utils.js'; 13 | import { initializeExportImport } from './export-import.js'; 14 | import { initializeModelManager } from './model-manager.js'; 15 | import { initializeWhatsNew } from './whats-new.js'; 16 | import { initializeSettingsModal } from './settings-modal-manager.js'; 17 | import { initializeCharacterManager } from './character-manager.js'; 18 | import { initializeCharacterGallery } from './character-gallery.js'; 19 | import { 20 | setupLazyLoading, 21 | addHardwareAcceleration, 22 | getDevicePerformanceLevel, 23 | startMemoryMonitoring, 24 | progressiveComponentLoading 25 | } from './performance-utils.js'; 26 | 27 | import { updateConfirmationModalTheme, updateExportImportModalsTheme } from '../confirmation-modal-fix.js'; 28 | 29 | /** 30 | * Initializes the application with enhanced performance optimizations 31 | */ 32 | function initializeApp() { 33 | // Disable debug logging by default 34 | setDebugEnabled(false); 35 | 36 | console.log('Starting LMSA with enhanced performance optimizations...'); 37 | 38 | // Detect device performance level first 39 | const performanceLevel = getDevicePerformanceLevel(); 40 | console.log(`Detected device performance level: ${performanceLevel}`); 41 | 42 | // Start memory monitoring for automatic cleanup 43 | startMemoryMonitoring(); 44 | 45 | // Initialize Monaco editor cleanup for memory management 46 | import('./monaco-performance.js').then(module => { 47 | module.setupMonacoCleanup(); 48 | }).catch(error => { 49 | console.error('Error setting up Monaco cleanup:', error); 50 | }); 51 | 52 | // Apply performance optimizations to key UI elements 53 | applyPerformanceOptimizations(); 54 | 55 | // Initialize the model banner state based on localStorage 56 | initializeModelBannerState(); 57 | 58 | // Ensure settings modal is hidden on startup 59 | const settingsModal = document.getElementById('settings-modal'); 60 | if (settingsModal) { 61 | settingsModal.classList.add('hidden'); 62 | settingsModal.classList.remove('show'); 63 | } 64 | 65 | // Ensure system prompt overlay is hidden on startup 66 | const systemPromptOverlay = document.getElementById('system-prompt-overlay'); 67 | if (systemPromptOverlay) { 68 | systemPromptOverlay.classList.add('hidden'); 69 | systemPromptOverlay.style.display = 'none'; 70 | } 71 | 72 | // Ensure the About button is properly initialized 73 | const aboutButton = document.getElementById('about-btn'); 74 | if (aboutButton) { 75 | console.log('About button found during initialization'); 76 | } 77 | 78 | /** 79 | * Applies performance optimizations to key UI elements 80 | */ 81 | function applyPerformanceOptimizations() { 82 | // Add hardware acceleration to scrollable containers 83 | const scrollableContainers = [ 84 | document.getElementById('messages'), 85 | document.getElementById('chat-history'), 86 | document.getElementById('sidebar'), 87 | document.getElementById('settings-content-wrapper') 88 | ]; 89 | 90 | scrollableContainers.forEach(container => { 91 | if (container) { 92 | addHardwareAcceleration(container); 93 | } 94 | }); 95 | 96 | // Setup lazy loading for images 97 | setupLazyLoading('img[data-src]'); 98 | 99 | // Add passive event listeners for better touch performance 100 | // This is handled by our touch handlers, but we ensure it's applied 101 | 102 | console.log('Performance optimizations applied'); 103 | } 104 | 105 | // Load critical settings first 106 | loadServerSettings(); // This will also fetch available models 107 | loadSettings(); 108 | loadChatHistory(); 109 | 110 | // Check if refresh was triggered by code generation 111 | if (wasRefreshDueToCodeGeneration()) { 112 | console.log('Refresh was triggered by code generation, restoring previous chat'); 113 | 114 | // Get the last active chat ID 115 | const lastActiveChatId = getLastActiveChatId(); 116 | 117 | // Immediately clear the reload flag to prevent any future accidental reloads 118 | clearRefreshDueToCodeGenerationFlag(); 119 | 120 | // Check if this is a first message reload (special optimization case) 121 | const isFirstMessageReload = localStorage.getItem('isFirstMessageReload') === 'true'; 122 | localStorage.removeItem('isFirstMessageReload'); 123 | 124 | if (lastActiveChatId && chatHistoryData && chatHistoryData[lastActiveChatId]) { 125 | // Load the chat immediately with no delay 126 | loadChat(lastActiveChatId, isFirstMessageReload); 127 | console.log('Restored previous chat:', lastActiveChatId); 128 | 129 | // Force immediate scroll 130 | const messagesContainer = document.getElementById('messages'); 131 | if (messagesContainer) { 132 | messagesContainer.scrollTop = messagesContainer.scrollHeight; 133 | } 134 | } else { 135 | console.log('No valid chat ID found to restore'); 136 | } 137 | } 138 | 139 | // Make sure file upload is initialized before other components 140 | // This ensures file upload handlers are ready when event handlers are set up 141 | try { 142 | console.log('Initializing file upload functionality...'); 143 | initializeFileUpload(); 144 | console.log('File upload initialization complete'); 145 | } catch (error) { 146 | console.error('Error initializing file upload:', error); 147 | } 148 | 149 | hideLoadingIndicatorOnLoad(); 150 | ensureWelcomeMessagePosition(); 151 | 152 | // Initialize critical components first 153 | initializeEventHandlers(); 154 | 155 | // Use progressive loading for non-critical components based on device performance 156 | // (performanceLevel already declared above) 157 | 158 | // Define component initialization functions in order of priority 159 | const componentInitializers = [ 160 | // High priority - touch and interaction 161 | () => initializeTouchHandlers(), 162 | () => initializeChatHistoryTouchHandler(), 163 | () => initializeSettingsModalTouchHandler(), 164 | () => initializeSidebarTouchHandler(), 165 | 166 | // Medium priority - core functionality 167 | () => initializeFileUpload(), 168 | () => initializeModelManager(), 169 | () => initializeCollapsibleSections(), 170 | () => initializeSettingsModal(), 171 | 172 | // Lower priority - additional features 173 | () => initializeExportImport(), 174 | () => initializeWhatsNew(), 175 | () => initializeCharacterManager(), 176 | () => initializeCharacterGallery(), 177 | 178 | // Lowest priority - theme and visual enhancements 179 | () => updateConfirmationModalTheme(), 180 | () => updateExportImportModalsTheme() 181 | ]; 182 | 183 | // Use progressive loading for better performance on slower devices 184 | progressiveComponentLoading(componentInitializers); 185 | 186 | // Initialize scroll button state - ensure it's hidden on startup 187 | const messagesContainer = document.getElementById('messages'); 188 | const scrollButton = document.getElementById('scroll-to-bottom'); 189 | 190 | if (scrollButton) { 191 | // Explicitly hide the button on startup 192 | scrollButton.classList.remove('visible', 'show'); 193 | scrollButton.classList.add('hidden'); 194 | scrollButton.style.opacity = '0'; 195 | scrollButton.style.visibility = 'hidden'; 196 | scrollButton.style.pointerEvents = 'none'; 197 | } 198 | 199 | if (messagesContainer) { 200 | // Force the scroll position to bottom on startup 201 | setTimeout(() => { 202 | messagesContainer.scrollTop = messagesContainer.scrollHeight; 203 | // Then check if the button should be shown (it should remain hidden on startup) 204 | handleScroll(messagesContainer); 205 | }, 100); 206 | 207 | // Add another check after a longer delay to ensure button state is correct 208 | setTimeout(() => { 209 | messagesContainer.scrollTop = messagesContainer.scrollHeight; 210 | handleScroll(messagesContainer); 211 | }, 500); 212 | } 213 | 214 | // Add window resize event listener with debouncing for better performance 215 | let resizeTimer; 216 | window.addEventListener('resize', () => { 217 | // Clear the previous timer 218 | clearTimeout(resizeTimer); 219 | 220 | // Set a new timer to debounce the resize event 221 | resizeTimer = setTimeout(() => { 222 | // Handle welcome message positioning 223 | const welcomeMessage = document.getElementById('welcome-message'); 224 | if (welcomeMessage && welcomeMessage.style.display === 'flex') { 225 | // Ensure welcome message position is updated when window is resized 226 | // This is especially important for small screens like iPhone SE (320x568) 227 | ensureWelcomeMessagePosition(); 228 | 229 | // Additional fix for the icon container on resize 230 | const iconContainer = document.querySelector('.welcome-content .icon-container'); 231 | if (iconContainer) { 232 | // Ensure the icon container is visible after resize 233 | iconContainer.style.position = 'relative'; 234 | iconContainer.style.zIndex = '10'; 235 | iconContainer.style.transform = 'translateY(0)'; 236 | 237 | // Fix for heart icon in footer on resize 238 | const heartIcon = document.querySelector('#welcome-footer i.fa-heart'); 239 | if (heartIcon) { 240 | heartIcon.style.fontSize = '0.7rem'; 241 | heartIcon.style.width = '0.7rem'; 242 | heartIcon.style.height = '0.7rem'; 243 | heartIcon.style.lineHeight = '0.7rem'; 244 | heartIcon.style.maxWidth = '0.7rem'; 245 | heartIcon.style.maxHeight = '0.7rem'; 246 | 247 | // Special case for 344x882 screens 248 | if ((window.innerWidth >= 343 && window.innerWidth <= 345) && 249 | (window.innerHeight >= 880 && window.innerHeight <= 884)) { 250 | heartIcon.style.fontSize = '0.65rem'; 251 | heartIcon.style.width = '0.65rem'; 252 | heartIcon.style.height = '0.65rem'; 253 | } 254 | 255 | // Special case for 320x480 screens 256 | if (window.innerWidth <= 320 && window.innerHeight <= 480) { 257 | heartIcon.style.fontSize = '0.6rem'; 258 | heartIcon.style.width = '0.6rem'; 259 | heartIcon.style.height = '0.6rem'; 260 | } 261 | } 262 | } 263 | } 264 | 265 | // Handle any other resize-related adjustments 266 | const messagesContainer = document.getElementById('messages'); 267 | if (messagesContainer) { 268 | // Recalculate scroll button visibility 269 | handleScroll(messagesContainer); 270 | } 271 | 272 | // Refresh Monaco editors if they exist 273 | if (typeof monaco !== 'undefined') { 274 | try { 275 | const editors = monaco.editor.getEditors(); 276 | editors.forEach(editor => { 277 | editor.layout(); 278 | }); 279 | } catch (e) { 280 | // Ignore errors if Monaco is not fully initialized 281 | } 282 | } 283 | }, 100); // 100ms debounce time for better performance 284 | }); 285 | 286 | // Add event listener to hide loading indicator when the page loads 287 | window.addEventListener('load', hideLoadingIndicatorOnLoad); 288 | } 289 | 290 | /** 291 | * Initializes the model banner state - always hidden 292 | */ 293 | function initializeModelBannerState() { 294 | const loadedModelDisplay = document.getElementById('loaded-model'); 295 | const modelWrapper = document.getElementById('loaded-model-wrapper'); 296 | 297 | console.log('Initializing model banner state - always hidden'); 298 | 299 | // Always hide the banner 300 | if (loadedModelDisplay && modelWrapper) { 301 | // Set the hidden properties 302 | loadedModelDisplay.style.maxHeight = '0'; 303 | loadedModelDisplay.style.opacity = '0'; 304 | loadedModelDisplay.style.transform = 'translateY(-100%)'; 305 | loadedModelDisplay.style.visibility = 'hidden'; 306 | loadedModelDisplay.classList.add('hidden'); 307 | 308 | // Hide the wrapper 309 | modelWrapper.style.display = 'none'; 310 | document.documentElement.style.setProperty('--loaded-model-height', '0px'); 311 | 312 | // Add a class to the body to indicate the banner is hidden 313 | document.body.classList.add('model-banner-hidden-by-user'); 314 | 315 | // Store the preference in localStorage 316 | localStorage.setItem('modelBannerVisible', 'false'); 317 | } 318 | } 319 | 320 | // Initialize the application when the DOM is loaded 321 | document.addEventListener('DOMContentLoaded', initializeApp); 322 | -------------------------------------------------------------------------------- /js/marked-bundle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Direct inclusion of marked.js to avoid any module system conflicts 3 | */ 4 | (function() { 5 | // Create a script element 6 | var script = document.createElement('script'); 7 | 8 | // Set up the load event 9 | script.onload = function() { 10 | console.log('Marked library loaded successfully'); 11 | // Initialize code that depends on marked 12 | if (window.initializeMarkdownRendering) { 13 | window.initializeMarkdownRendering(); 14 | } 15 | }; 16 | 17 | script.onerror = function(e) { 18 | console.error('Failed to load marked.js', e); 19 | }; 20 | 21 | // Use the browser bundle that doesn't use AMD/CommonJS 22 | script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'; 23 | 24 | // Add a timestamp to prevent caching 25 | script.src += '?_=' + new Date().getTime(); 26 | 27 | // Use defer to ensure it loads after Monaco but doesn't block rendering 28 | script.defer = true; 29 | 30 | // Add the script to the document 31 | document.head.appendChild(script); 32 | })(); -------------------------------------------------------------------------------- /js/marked-nomodule.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Custom loader for marked.js that completely bypasses module systems 3 | * This solves conflicts with RequireJS/AMD loaders 4 | */ 5 | (function() { 6 | // Use fetch to get the raw source 7 | fetch('https://cdn.jsdelivr.net/npm/marked@4.0.2/marked.min.js') 8 | .then(response => { 9 | if (!response.ok) { 10 | throw new Error('Network response was not ok'); 11 | } 12 | return response.text(); 13 | }) 14 | .then(sourceCode => { 15 | // Create a function that will run the code in the global scope 16 | // but without triggering AMD detection 17 | try { 18 | // Store original define and require 19 | var originalDefine = window.define; 20 | var originalRequire = window.require; 21 | 22 | // Temporarily remove define and require to prevent AMD detection 23 | window.define = undefined; 24 | window.require = undefined; 25 | 26 | // Set up a CommonJS-like environment 27 | var module = { exports: {} }; 28 | var exports = module.exports; 29 | 30 | // Wrap the source code in a function that provides CommonJS context 31 | var executeMarked = new Function('module', 'exports', sourceCode); 32 | 33 | // Execute the code with mock CommonJS environment 34 | executeMarked(module, exports); 35 | 36 | // Assign marked to the global scope 37 | window.marked = module.exports; 38 | 39 | // Restore define and require 40 | window.define = originalDefine; 41 | window.require = originalRequire; 42 | 43 | console.log('Marked library loaded successfully (bypassing module systems)'); 44 | 45 | // Initialize any code that depends on marked 46 | if (window.initializeMarkdownRendering) { 47 | window.initializeMarkdownRendering(); 48 | } 49 | } catch (error) { 50 | console.error('Error executing marked.js:', error); 51 | } 52 | }) 53 | .catch(error => { 54 | console.error('Error loading marked.js:', error); 55 | }); 56 | })(); -------------------------------------------------------------------------------- /js/monaco-performance.js: -------------------------------------------------------------------------------- 1 | // Monaco Editor performance optimizations for slower devices 2 | import { getDevicePerformanceLevel } from './performance-utils.js'; 3 | 4 | /** 5 | * Gets optimized Monaco editor options based on device performance 6 | * @param {string} language - The programming language 7 | * @param {string} content - The code content 8 | * @returns {Object} - Optimized Monaco editor options 9 | */ 10 | export function getOptimizedMonacoOptions(language, content) { 11 | const performanceLevel = getDevicePerformanceLevel(); 12 | 13 | // Base options for all devices 14 | const baseOptions = { 15 | value: content, 16 | language: language, 17 | readOnly: true, 18 | theme: 'vs-dark', 19 | lineNumbers: 'on', 20 | contextmenu: true, 21 | automaticLayout: true, 22 | wordWrap: 'on', 23 | scrollBeyondLastLine: false 24 | }; 25 | 26 | // Adaptive options based on device performance 27 | if (performanceLevel === 'low') { 28 | return { 29 | ...baseOptions, 30 | // Disable expensive features for low-end devices 31 | minimap: { enabled: false }, 32 | folding: false, 33 | lineDecorationsWidth: 0, 34 | lineNumbersMinChars: 3, 35 | glyphMargin: false, 36 | renderLineHighlight: 'none', 37 | renderWhitespace: 'none', 38 | renderControlCharacters: false, 39 | renderIndentGuides: false, 40 | scrollbar: { 41 | vertical: 'hidden', 42 | horizontal: 'hidden', 43 | verticalScrollbarSize: 0, 44 | horizontalScrollbarSize: 0 45 | }, 46 | // Reduce rendering frequency 47 | smoothScrolling: false, 48 | cursorBlinking: 'solid', 49 | cursorSmoothCaretAnimation: false, 50 | // Disable hover and suggestions for better performance 51 | hover: { enabled: false }, 52 | quickSuggestions: false, 53 | suggestOnTriggerCharacters: false, 54 | acceptSuggestionOnEnter: 'off', 55 | // Reduce validation and syntax highlighting 56 | validate: false, 57 | semanticHighlighting: { enabled: false } 58 | }; 59 | } else if (performanceLevel === 'medium') { 60 | return { 61 | ...baseOptions, 62 | // Moderate optimizations for medium devices 63 | minimap: { enabled: false }, 64 | folding: true, 65 | lineDecorationsWidth: 10, 66 | lineNumbersMinChars: 4, 67 | glyphMargin: false, 68 | renderLineHighlight: 'line', 69 | renderWhitespace: 'none', 70 | renderControlCharacters: false, 71 | renderIndentGuides: true, 72 | scrollbar: { 73 | vertical: 'auto', 74 | horizontal: 'auto', 75 | verticalScrollbarSize: 8, 76 | horizontalScrollbarSize: 8 77 | }, 78 | smoothScrolling: false, 79 | cursorBlinking: 'blink', 80 | cursorSmoothCaretAnimation: false, 81 | hover: { enabled: true, delay: 1000 }, 82 | quickSuggestions: false, 83 | suggestOnTriggerCharacters: false, 84 | acceptSuggestionOnEnter: 'off' 85 | }; 86 | } else { 87 | // High-end devices get full features 88 | return { 89 | ...baseOptions, 90 | minimap: { enabled: false }, // Still disable minimap for code blocks 91 | folding: true, 92 | lineDecorationsWidth: 10, 93 | lineNumbersMinChars: 4, 94 | glyphMargin: true, 95 | renderLineHighlight: 'line', 96 | renderWhitespace: 'selection', 97 | renderControlCharacters: true, 98 | renderIndentGuides: true, 99 | scrollbar: { 100 | vertical: 'auto', 101 | horizontal: 'auto', 102 | verticalScrollbarSize: 12, 103 | horizontalScrollbarSize: 12 104 | }, 105 | smoothScrolling: true, 106 | cursorBlinking: 'blink', 107 | cursorSmoothCaretAnimation: true, 108 | hover: { enabled: true, delay: 500 }, 109 | quickSuggestions: false, // Still disabled for read-only code blocks 110 | suggestOnTriggerCharacters: false, 111 | acceptSuggestionOnEnter: 'off' 112 | }; 113 | } 114 | } 115 | 116 | /** 117 | * Creates a Monaco editor with performance optimizations 118 | * @param {HTMLElement} container - The container element 119 | * @param {string} language - The programming language 120 | * @param {string} content - The code content 121 | * @returns {Promise} - The Monaco editor instance 122 | */ 123 | export async function createOptimizedMonacoEditor(container, language, content) { 124 | if (!window.monaco) { 125 | throw new Error('Monaco editor is not loaded'); 126 | } 127 | 128 | const options = getOptimizedMonacoOptions(language, content); 129 | const performanceLevel = getDevicePerformanceLevel(); 130 | 131 | try { 132 | // Create the editor with optimized options 133 | const editor = window.monaco.editor.create(container, options); 134 | 135 | // Add performance monitoring for low-end devices 136 | if (performanceLevel === 'low') { 137 | // Disable expensive features that might be enabled by default 138 | editor.updateOptions({ 139 | renderValidationDecorations: 'off', 140 | renderLineHighlightOnlyWhenFocus: true 141 | }); 142 | 143 | // Add a cleanup timer for unused editors 144 | setTimeout(() => { 145 | if (!container.isConnected) { 146 | console.log('Disposing disconnected Monaco editor'); 147 | editor.dispose(); 148 | } 149 | }, 30000); // Check after 30 seconds 150 | } 151 | 152 | console.log(`Created optimized Monaco editor for ${performanceLevel} performance device`); 153 | return editor; 154 | 155 | } catch (error) { 156 | console.error('Error creating Monaco editor:', error); 157 | throw error; 158 | } 159 | } 160 | 161 | /** 162 | * Batch processes Monaco editor creation to avoid overwhelming slower devices 163 | * @param {Array} editorConfigs - Array of editor configuration objects 164 | * @returns {Promise} - Array of created editors 165 | */ 166 | export async function batchCreateMonacoEditors(editorConfigs) { 167 | if (!editorConfigs || editorConfigs.length === 0) { 168 | return []; 169 | } 170 | 171 | const performanceLevel = getDevicePerformanceLevel(); 172 | const batchSize = performanceLevel === 'low' ? 1 : performanceLevel === 'medium' ? 2 : 3; 173 | const delay = performanceLevel === 'low' ? 200 : performanceLevel === 'medium' ? 100 : 50; 174 | 175 | const editors = []; 176 | 177 | for (let i = 0; i < editorConfigs.length; i += batchSize) { 178 | const batch = editorConfigs.slice(i, i + batchSize); 179 | 180 | // Process batch in parallel 181 | const batchPromises = batch.map(config => 182 | createOptimizedMonacoEditor(config.container, config.language, config.content) 183 | .catch(error => { 184 | console.error('Error creating editor in batch:', error); 185 | return null; 186 | }) 187 | ); 188 | 189 | const batchResults = await Promise.all(batchPromises); 190 | editors.push(...batchResults.filter(editor => editor !== null)); 191 | 192 | // Add delay between batches for slower devices 193 | if (i + batchSize < editorConfigs.length && delay > 0) { 194 | await new Promise(resolve => setTimeout(resolve, delay)); 195 | } 196 | } 197 | 198 | console.log(`Batch created ${editors.length} Monaco editors for ${performanceLevel} performance device`); 199 | return editors; 200 | } 201 | 202 | /** 203 | * Disposes of Monaco editors that are no longer visible or needed 204 | */ 205 | export function cleanupMonacoEditors() { 206 | if (!window.monaco) return; 207 | 208 | try { 209 | const editors = window.monaco.editor.getEditors(); 210 | let disposedCount = 0; 211 | 212 | editors.forEach(editor => { 213 | const container = editor.getContainerDomNode(); 214 | 215 | // Check if editor is still connected to DOM and visible 216 | if (!container || !container.isConnected) { 217 | editor.dispose(); 218 | disposedCount++; 219 | } else { 220 | // Check if editor is in viewport (for memory optimization) 221 | const rect = container.getBoundingClientRect(); 222 | const isVisible = rect.bottom >= 0 && rect.top <= window.innerHeight; 223 | 224 | if (!isVisible && getDevicePerformanceLevel() === 'low') { 225 | // For low-end devices, dispose editors that are far from viewport 226 | const distanceFromViewport = Math.min( 227 | Math.abs(rect.bottom), 228 | Math.abs(rect.top - window.innerHeight) 229 | ); 230 | 231 | if (distanceFromViewport > window.innerHeight * 2) { 232 | editor.dispose(); 233 | disposedCount++; 234 | } 235 | } 236 | } 237 | }); 238 | 239 | if (disposedCount > 0) { 240 | console.log(`Cleaned up ${disposedCount} Monaco editors`); 241 | } 242 | 243 | } catch (error) { 244 | console.error('Error cleaning up Monaco editors:', error); 245 | } 246 | } 247 | 248 | /** 249 | * Sets up automatic Monaco editor cleanup for memory management 250 | */ 251 | export function setupMonacoCleanup() { 252 | const performanceLevel = getDevicePerformanceLevel(); 253 | 254 | if (performanceLevel === 'low') { 255 | // More frequent cleanup for low-end devices 256 | setInterval(cleanupMonacoEditors, 30000); // Every 30 seconds 257 | } else if (performanceLevel === 'medium') { 258 | // Moderate cleanup for medium devices 259 | setInterval(cleanupMonacoEditors, 60000); // Every minute 260 | } else { 261 | // Less frequent cleanup for high-end devices 262 | setInterval(cleanupMonacoEditors, 120000); // Every 2 minutes 263 | } 264 | 265 | console.log(`Monaco cleanup scheduled for ${performanceLevel} performance device`); 266 | } 267 | -------------------------------------------------------------------------------- /js/reset-app.js: -------------------------------------------------------------------------------- 1 | // Reset App functionality 2 | import { clearAllChats } from './chat-service.js'; 3 | import { debugLog, debugError } from './utils.js'; 4 | import { showConfirmationModal } from './ui-manager.js'; 5 | import { setActionToPerform } from './shared-state.js'; 6 | 7 | /** 8 | * Resets the entire application to its default state 9 | * - Deletes all chats 10 | * - Deletes all saved characters 11 | * - Clears all saved settings including server connection settings 12 | * - Resets the app to its default state 13 | */ 14 | export function resetApp() { 15 | try { 16 | debugLog('Resetting app to default state'); 17 | 18 | // 1. Clear all chats 19 | clearAllChats(); 20 | 21 | // 2. Delete all saved characters 22 | localStorage.removeItem('charactersData'); 23 | localStorage.removeItem('activeCharacterId'); 24 | 25 | // 3. Clear all saved settings 26 | localStorage.removeItem('systemPrompt'); 27 | localStorage.removeItem('isUserCreatedSystemPrompt'); 28 | localStorage.removeItem('temperature'); 29 | localStorage.removeItem('hideThinking'); 30 | localStorage.removeItem('autoGenerateTitles'); 31 | localStorage.removeItem('lightThemeEnabled'); 32 | 33 | 34 | // 4. Clear server connection settings 35 | localStorage.removeItem('serverIp'); 36 | localStorage.removeItem('serverPort'); 37 | 38 | // 5. Clear other app state 39 | localStorage.removeItem('lastActiveChatId'); 40 | localStorage.removeItem('refreshDueToCodeGeneration'); 41 | 42 | // 6. Clear What's New preferences 43 | localStorage.removeItem('whatsNewDontShow'); 44 | localStorage.removeItem('whatsNewSeen'); 45 | 46 | // 7. Reload the page to apply all changes 47 | window.location.reload(); 48 | 49 | debugLog('App reset complete'); 50 | } catch (error) { 51 | debugError('Error resetting app:', error); 52 | } 53 | } 54 | 55 | /** 56 | * Shows the reset app confirmation modal 57 | */ 58 | export function showResetAppConfirmation() { 59 | setActionToPerform('resetApp'); 60 | showConfirmationModal('Are you sure you want to reset the app? This will delete all chats, clear all saved settings (including server connection settings), and delete all saved characters. This action cannot be undone.'); 61 | } 62 | 63 | /** 64 | * Initializes the reset app button event listener 65 | */ 66 | export function initializeResetAppButton() { 67 | const resetAppButton = document.getElementById('reset-app'); 68 | if (resetAppButton) { 69 | resetAppButton.addEventListener('click', showResetAppConfirmation); 70 | debugLog('Reset app button event listener initialized'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /js/settings-modal-touch-handler.js: -------------------------------------------------------------------------------- 1 | // Touch handler for settings modal scrolling 2 | import { settingsModal } from './dom-elements.js'; 3 | import { debugLog } from './utils.js'; 4 | 5 | /** 6 | * Initializes touch handling for the settings modal scrollable areas 7 | * Ensures touch scrolling works properly on touchscreens 8 | */ 9 | export function initializeSettingsModalTouchHandler() { 10 | if (!settingsModal) return; 11 | 12 | // Get the scrollable elements in the settings modal 13 | const settingsContentWrapper = document.getElementById('settings-content-wrapper'); 14 | 15 | // If the settings content wrapper exists, add touch event listeners 16 | if (settingsContentWrapper) { 17 | // Ensure touch events are properly handled 18 | settingsContentWrapper.addEventListener('touchstart', function(e) { 19 | // Allow default behavior for touch events in scrollable areas 20 | e.stopPropagation(); 21 | }, { passive: true }); 22 | 23 | settingsContentWrapper.addEventListener('touchmove', function(e) { 24 | // Allow default behavior for touch events in scrollable areas 25 | e.stopPropagation(); 26 | }, { passive: true }); 27 | 28 | settingsContentWrapper.addEventListener('touchend', function(e) { 29 | // Allow default behavior for touch events in scrollable areas 30 | e.stopPropagation(); 31 | }, { passive: true }); 32 | } 33 | 34 | // Add specific touch event handlers for navigation buttons 35 | const navigationButtons = [ 36 | 'to-prompt-step-btn', 37 | 'back-to-connection-btn', 38 | 'to-options-step-btn', 39 | 'back-to-prompt-btn', 40 | 'to-actions-step-btn', 41 | 'back-to-options-btn' 42 | ]; 43 | 44 | // Get the navigation buttons container 45 | const navButtonsContainer = document.getElementById('settings-navigation-buttons'); 46 | if (navButtonsContainer) { 47 | // Add touch events to the container 48 | navButtonsContainer.addEventListener('touchstart', function(e) { 49 | // Allow default behavior for touch events in the navigation area 50 | e.stopPropagation(); 51 | }, { passive: true }); 52 | 53 | navButtonsContainer.addEventListener('touchmove', function(e) { 54 | // Allow default behavior for touch events in the navigation area 55 | e.stopPropagation(); 56 | }, { passive: true }); 57 | 58 | navButtonsContainer.addEventListener('touchend', function(e) { 59 | // Allow default behavior for touch events in the navigation area 60 | e.stopPropagation(); 61 | }, { passive: true }); 62 | } 63 | 64 | // Add touch event handlers to each navigation button 65 | navigationButtons.forEach(buttonId => { 66 | const button = document.getElementById(buttonId); 67 | if (button) { 68 | // Remove any existing event listeners by cloning and replacing 69 | const newButton = button.cloneNode(true); 70 | button.parentNode.replaceChild(newButton, button); 71 | 72 | // Add click event with stopPropagation 73 | newButton.addEventListener('click', function(e) { 74 | debugLog(`Navigation button ${buttonId} clicked`); 75 | e.stopPropagation(); 76 | }); 77 | 78 | // Add touch events with stopPropagation 79 | newButton.addEventListener('touchstart', function(e) { 80 | debugLog(`Navigation button ${buttonId} touchstart`); 81 | e.stopPropagation(); 82 | }, { passive: false }); 83 | 84 | newButton.addEventListener('touchend', function(e) { 85 | debugLog(`Navigation button ${buttonId} touchend`); 86 | e.preventDefault(); 87 | e.stopPropagation(); 88 | // Trigger a click event to ensure the button action is performed 89 | newButton.click(); 90 | }, { passive: false }); 91 | } 92 | }); 93 | 94 | // Add touch event listeners to each active settings step 95 | settingsModal.addEventListener('click', function() { 96 | // Get all active settings steps 97 | const activeSteps = settingsModal.querySelectorAll('.settings-step.active'); 98 | 99 | // Add touch event listeners to each active step 100 | activeSteps.forEach(step => { 101 | // Ensure touch events are properly handled 102 | step.addEventListener('touchstart', function(e) { 103 | // Allow default behavior for touch events in scrollable areas 104 | e.stopPropagation(); 105 | }, { passive: true }); 106 | 107 | step.addEventListener('touchmove', function(e) { 108 | // Allow default behavior for touch events in scrollable areas 109 | e.stopPropagation(); 110 | }, { passive: true }); 111 | 112 | step.addEventListener('touchend', function(e) { 113 | // Allow default behavior for touch events in scrollable areas 114 | e.stopPropagation(); 115 | }, { passive: true }); 116 | }); 117 | }); 118 | } 119 | -------------------------------------------------------------------------------- /js/shared-state.js: -------------------------------------------------------------------------------- 1 | // Shared state between modules 2 | let actionToPerform = null; 3 | 4 | /** 5 | * Sets the action to perform 6 | * @param {string} action - The action to perform 7 | */ 8 | export function setActionToPerform(action) { 9 | actionToPerform = action; 10 | } 11 | 12 | /** 13 | * Gets the action to perform 14 | * @returns {string} - The action to perform 15 | */ 16 | export function getActionToPerform() { 17 | return actionToPerform; 18 | } 19 | -------------------------------------------------------------------------------- /js/sidebar-touch-handler.js: -------------------------------------------------------------------------------- 1 | // Sidebar touch handler for improved touch scrolling on mobile devices 2 | 3 | /** 4 | * Initializes touch handlers for the sidebar to improve scrolling on mobile devices 5 | */ 6 | export function initializeSidebarTouchHandler() { 7 | const sidebar = document.getElementById('sidebar'); 8 | const chatHistory = document.getElementById('chat-history'); 9 | 10 | if (!sidebar || !chatHistory) { 11 | console.error('Sidebar or chat history elements not found'); 12 | return; 13 | } 14 | 15 | // Variables to track touch state 16 | let touchStartY = 0; 17 | let touchStartX = 0; 18 | let isDragging = false; 19 | let isScrolling = false; 20 | const DRAG_THRESHOLD = 5; // Pixels of movement to consider a drag (reduced for faster detection) 21 | let touchedElement = null; 22 | let scrollStartTime = 0; 23 | let scrollTimeout = null; 24 | 25 | // Immediately apply the no-highlight class to all interactive elements 26 | function applyNoHighlightToAll() { 27 | const interactiveElements = sidebar.querySelectorAll('.menu-item, .section-header'); 28 | interactiveElements.forEach(el => { 29 | el.classList.add('no-touch-highlight'); 30 | }); 31 | } 32 | 33 | // Remove the no-highlight class from all interactive elements 34 | function removeNoHighlightFromAll() { 35 | const interactiveElements = sidebar.querySelectorAll('.no-touch-highlight'); 36 | interactiveElements.forEach(el => { 37 | el.classList.remove('no-touch-highlight'); 38 | }); 39 | } 40 | 41 | // Set scrolling state 42 | function setScrollingState(scrolling) { 43 | isScrolling = scrolling; 44 | 45 | if (scrolling) { 46 | applyNoHighlightToAll(); 47 | 48 | // Clear any existing timeout 49 | if (scrollTimeout) { 50 | clearTimeout(scrollTimeout); 51 | } 52 | } else { 53 | // Set a timeout to remove the no-highlight class after scrolling stops 54 | scrollTimeout = setTimeout(() => { 55 | if (!isDragging) { 56 | removeNoHighlightFromAll(); 57 | } 58 | }, 300); // Wait 300ms after scrolling stops 59 | } 60 | } 61 | 62 | // Add touch event handlers to the sidebar 63 | sidebar.addEventListener('touchstart', function(e) { 64 | // Record the starting position and time 65 | touchStartY = e.touches[0].clientY; 66 | touchStartX = e.touches[0].clientX; 67 | scrollStartTime = Date.now(); 68 | isDragging = false; 69 | touchedElement = e.target; 70 | 71 | // Immediately apply no-highlight to prevent the initial highlight 72 | applyNoHighlightToAll(); 73 | }, { passive: false }); 74 | 75 | sidebar.addEventListener('touchmove', function(e) { 76 | // Calculate distance moved 77 | const touchY = e.touches[0].clientY; 78 | const touchX = e.touches[0].clientX; 79 | const deltaY = Math.abs(touchY - touchStartY); 80 | const deltaX = Math.abs(touchX - touchStartX); 81 | 82 | // If moved more than threshold, consider it a drag/scroll 83 | if (deltaY > DRAG_THRESHOLD || deltaX > DRAG_THRESHOLD) { 84 | isDragging = true; 85 | setScrollingState(true); 86 | } 87 | 88 | // Stop propagation but don't prevent default scrolling 89 | e.stopPropagation(); 90 | }, { passive: true }); 91 | 92 | sidebar.addEventListener('touchend', function(e) { 93 | // Calculate if this was a quick tap or a scroll/drag 94 | const touchDuration = Date.now() - scrollStartTime; 95 | const wasQuickTap = touchDuration < 200 && !isDragging; 96 | 97 | // If this was a quick tap, allow the click to happen 98 | if (wasQuickTap) { 99 | // Only remove no-highlight from the tapped element 100 | if (touchedElement) { 101 | const targetElement = touchedElement.closest('.menu-item, .section-header'); 102 | if (targetElement) { 103 | targetElement.classList.remove('no-touch-highlight'); 104 | } 105 | } 106 | } else { 107 | // This was a scroll/drag, keep no-highlight for a moment 108 | setTimeout(() => { 109 | setScrollingState(false); 110 | }, 100); 111 | } 112 | 113 | // Reset dragging state 114 | isDragging = false; 115 | }, { passive: true }); 116 | 117 | // Handle scroll events to maintain the no-highlight state during scrolling 118 | sidebar.addEventListener('scroll', function() { 119 | setScrollingState(true); 120 | }, { passive: true }); 121 | 122 | // Add specific touch event handlers to the chat history section 123 | chatHistory.addEventListener('touchstart', function(e) { 124 | // Apply no-highlight to all menu items during chat history scrolling 125 | applyNoHighlightToAll(); 126 | }, { passive: true }); 127 | 128 | chatHistory.addEventListener('touchmove', function(e) { 129 | // Keep no-highlight during scrolling 130 | setScrollingState(true); 131 | e.stopPropagation(); 132 | }, { passive: true }); 133 | 134 | chatHistory.addEventListener('touchend', function(e) { 135 | // Reset scrolling state after a delay 136 | setTimeout(() => { 137 | setScrollingState(false); 138 | }, 300); 139 | }, { passive: true }); 140 | 141 | // Add touch event handlers to all menu items 142 | const menuItems = sidebar.querySelectorAll('.menu-item'); 143 | menuItems.forEach(item => { 144 | // Prevent default active state on touchstart 145 | item.addEventListener('touchstart', function(e) { 146 | item.classList.add('no-touch-highlight'); 147 | }, { passive: false }); 148 | 149 | // Remove highlight during move 150 | item.addEventListener('touchmove', function(e) { 151 | item.classList.add('no-touch-highlight'); 152 | e.stopPropagation(); 153 | }, { passive: true }); 154 | }); 155 | 156 | // Add touch event handlers to all section headers 157 | const sectionHeaders = sidebar.querySelectorAll('.section-header'); 158 | sectionHeaders.forEach(header => { 159 | // Prevent default active state on touchstart 160 | header.addEventListener('touchstart', function(e) { 161 | header.classList.add('no-touch-highlight'); 162 | }, { passive: false }); 163 | 164 | // Remove highlight during move 165 | header.addEventListener('touchmove', function(e) { 166 | header.classList.add('no-touch-highlight'); 167 | e.stopPropagation(); 168 | }, { passive: true }); 169 | }); 170 | 171 | console.log('Sidebar touch handlers initialized with improved drag detection'); 172 | } 173 | -------------------------------------------------------------------------------- /js/touch-handlers.js: -------------------------------------------------------------------------------- 1 | // Touch event handlers for mobile devices with performance optimizations 2 | import { messagesContainer, userInput } from './dom-elements.js'; 3 | import { ensureCursorVisible } from './utils.js'; 4 | import { getDevicePerformanceLevel, throttle } from './performance-utils.js'; 5 | 6 | /** 7 | * Initializes touch event handlers with adaptive performance optimizations 8 | */ 9 | export async function initializeTouchHandlers() { 10 | const performanceLevel = getDevicePerformanceLevel(); 11 | console.log(`Initializing touch handlers for ${performanceLevel} performance device`); 12 | 13 | // Create throttled touch handler for low-end devices 14 | const touchMoveHandler = performanceLevel === 'low' 15 | ? throttle(handleTouchMove, 16) // ~60fps for low-end devices 16 | : handleTouchMove; 17 | 18 | function handleTouchMove(e) { 19 | // Check if the touch is within a Monaco editor component 20 | const isMonacoElement = e.target.closest('.monaco-container') !== null || 21 | e.target.closest('.monaco-editor') !== null || 22 | e.target.closest('.monaco-scrollable-element') !== null || 23 | e.target.closest('.lines-content') !== null || 24 | e.target.closest('.view-lines') !== null || 25 | e.target.closest('.view-line') !== null || 26 | e.target.closest('.view-overlays') !== null; 27 | 28 | // Check if the touch is within other scrollable containers 29 | const isScrollableContainer = e.target.closest('#messages') !== null || 30 | e.target.closest('#user-input') !== null || 31 | e.target.closest('#chat-form') !== null || 32 | e.target.closest('#chat-history') !== null || 33 | e.target.closest('#settings-content-wrapper') !== null || 34 | e.target.closest('.settings-step.active') !== null; 35 | 36 | // If not in any scrollable container, prevent default behavior 37 | if (!isMonacoElement && !isScrollableContainer) { 38 | e.preventDefault(); 39 | } 40 | } 41 | 42 | // Prevent default touch behavior except for scrollable containers 43 | document.body.addEventListener('touchmove', touchMoveHandler, { passive: false }); 44 | 45 | // Allow scrolling within the messages container 46 | if (messagesContainer) { 47 | messagesContainer.addEventListener('touchmove', function(e) { 48 | // Check if the touch is within a Monaco editor component 49 | const isMonacoElement = e.target.closest('.monaco-container') !== null || 50 | e.target.closest('.monaco-editor') !== null || 51 | e.target.closest('.monaco-scrollable-element') !== null || 52 | e.target.closest('.lines-content') !== null || 53 | e.target.closest('.view-lines') !== null || 54 | e.target.closest('.view-line') !== null || 55 | e.target.closest('.view-overlays') !== null; 56 | 57 | // If the touch is within a Monaco editor, let the Monaco editor handle it 58 | if (isMonacoElement) { 59 | return; 60 | } 61 | 62 | // For other elements, allow the messages container to scroll 63 | e.stopPropagation(); 64 | }, { passive: true }); 65 | } 66 | 67 | // Allow scrolling within the settings modal scrollable areas 68 | const settingsContentWrapper = document.getElementById('settings-content-wrapper'); 69 | if (settingsContentWrapper) { 70 | settingsContentWrapper.addEventListener('touchmove', function(e) { 71 | e.stopPropagation(); 72 | }, { passive: true }); 73 | } 74 | 75 | // Prevent sidebar toggle when interacting with chat input 76 | if (userInput) { 77 | // Import scrollToBottom function directly to avoid dynamic imports 78 | const { scrollToBottom } = await import('./utils.js'); 79 | 80 | // Consolidated function to handle all input field interactions with performance optimization 81 | const handleInputInteraction = performanceLevel === 'low' 82 | ? throttle(function(e) { 83 | e.stopPropagation(); 84 | 85 | // Ensure messagesContainer exists 86 | if (!messagesContainer) return; 87 | 88 | // Use requestAnimationFrame for smoother scrolling 89 | requestAnimationFrame(() => { 90 | // Force scroll to bottom when user interacts with input field 91 | scrollToBottom(messagesContainer, true); 92 | }); 93 | }, 100) // Throttle to 10fps for low-end devices 94 | : function(e) { 95 | e.stopPropagation(); 96 | 97 | // Ensure messagesContainer exists 98 | if (!messagesContainer) return; 99 | 100 | // Use requestAnimationFrame for smoother scrolling 101 | requestAnimationFrame(() => { 102 | // Force scroll to bottom when user interacts with input field 103 | scrollToBottom(messagesContainer, true); 104 | }); 105 | }; 106 | 107 | // Add event listeners for all interaction types 108 | userInput.addEventListener('focus', handleInputInteraction); 109 | userInput.addEventListener('click', handleInputInteraction); 110 | userInput.addEventListener('touchstart', handleInputInteraction); 111 | 112 | // Also handle keyboard events to ensure scrolling when typing starts 113 | userInput.addEventListener('keydown', function(e) { 114 | // Only trigger on first keypress 115 | if (e.target.dataset.hasTyped !== 'true') { 116 | e.target.dataset.hasTyped = 'true'; 117 | handleInputInteraction(e); 118 | 119 | // Reset the flag after a delay 120 | setTimeout(() => { 121 | delete e.target.dataset.hasTyped; 122 | }, 5000); // Reset after 5 seconds of no typing 123 | } 124 | }); 125 | 126 | // Add a direct scroll handler for mobile devices 127 | userInput.addEventListener('touchmove', function(e) { 128 | // Allow the default behavior for horizontal scrolling 129 | e.stopPropagation(); 130 | }, { passive: true }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /js/whats-new.js: -------------------------------------------------------------------------------- 1 | // Import the checkAndShowWelcomeMessage function 2 | import { checkAndShowWelcomeMessage } from './ui-manager.js'; 3 | import { wasRefreshDueToCodeGeneration, clearRefreshDueToCodeGenerationFlag } from './utils.js'; 4 | 5 | // DOM Elements 6 | const whatsNewModal = document.getElementById('whats-new-modal'); 7 | const closeWhatsNewButton = document.getElementById('close-whats-new'); 8 | const gotItButton = document.getElementById('got-it-whats-new'); 9 | const dontShowAgainToggle = document.getElementById('dont-show-again'); 10 | const versionElement = document.getElementById('whats-new-version'); 11 | 12 | // Local storage keys 13 | const WHATS_NEW_VERSION = '4.9'; // Updated for Characters feature 14 | const WHATS_NEW_SEEN_KEY = 'whatsNewSeen'; 15 | const WHATS_NEW_DONT_SHOW_KEY = 'whatsNewDontShow'; 16 | 17 | 18 | 19 | // Flag to track if the modal has been shown in the current session 20 | let modalShownInCurrentSession = false; 21 | 22 | /** 23 | * Loads the user's preference for the "Don't show again" toggle 24 | */ 25 | function loadPreferences() { 26 | if (dontShowAgainToggle) { 27 | const dontShow = localStorage.getItem(WHATS_NEW_DONT_SHOW_KEY) === 'true'; 28 | dontShowAgainToggle.checked = dontShow; 29 | 30 | // Update toggle appearance based on checked state 31 | updateToggleAppearance(); 32 | } 33 | } 34 | 35 | /** 36 | * Updates the toggle appearance based on its checked state 37 | */ 38 | function updateToggleAppearance() { 39 | if (!dontShowAgainToggle) return; 40 | 41 | const toggleContainer = dontShowAgainToggle.closest('.toggle-container'); 42 | if (!toggleContainer) return; 43 | 44 | const toggleDot = toggleContainer.querySelector('.toggle-dot'); 45 | const toggleDotActive = toggleContainer.querySelector('.toggle-dot-active'); 46 | 47 | if (dontShowAgainToggle.checked) { 48 | if (toggleDotActive) toggleDotActive.style.opacity = '1'; 49 | if (toggleDot) toggleDot.style.opacity = '0'; 50 | } else { 51 | if (toggleDotActive) toggleDotActive.style.opacity = '0'; 52 | if (toggleDot) toggleDot.style.opacity = '1'; 53 | } 54 | } 55 | 56 | /** 57 | * Shows the What's New modal with smooth transition 58 | * @param {boolean} forceShow - If true, shows the modal regardless of user preferences 59 | */ 60 | export function showWhatsNewModal(forceShow = false) { 61 | // If the modal has already been shown in this session and we're not forcing it, don't show it again 62 | if (modalShownInCurrentSession && !forceShow) { 63 | return; 64 | } 65 | 66 | if (whatsNewModal) { 67 | // Load the user's preference for the "Don't show again" toggle 68 | loadPreferences(); 69 | 70 | // Set the version number in the UI 71 | if (versionElement) { 72 | versionElement.textContent = WHATS_NEW_VERSION; 73 | } 74 | 75 | // Fix touch scrolling for the modal 76 | setupTouchScrolling(); 77 | 78 | // Prepare the modal for a smooth entrance 79 | // First remove hidden class to make the modal visible but transparent 80 | whatsNewModal.classList.remove('hidden'); 81 | 82 | // Force a reflow to ensure the transition works 83 | void whatsNewModal.offsetWidth; 84 | 85 | // Add fade-in class to trigger the transition 86 | whatsNewModal.classList.add('fade-in'); 87 | 88 | // Add a simple entrance animation to the modal content 89 | const modalContent = whatsNewModal.querySelector('.modal-content'); 90 | if (modalContent) { 91 | modalContent.style.transform = 'translateY(20px)'; 92 | modalContent.style.opacity = '0'; 93 | 94 | // Force a reflow 95 | void modalContent.offsetWidth; 96 | 97 | // Animate in with a slight delay for a smoother sequence 98 | setTimeout(() => { 99 | modalContent.style.transition = 'transform 0.6s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.6s ease'; 100 | modalContent.style.transform = 'translateY(0)'; 101 | modalContent.style.opacity = '1'; 102 | 103 | // Make all feature items visible immediately 104 | const featureItems = modalContent.querySelectorAll('.feature-item'); 105 | featureItems.forEach(item => { 106 | item.style.opacity = '1'; 107 | item.style.transform = 'translateY(0)'; 108 | }); 109 | }, 50); 110 | } 111 | 112 | // Adjust modal height after modal is visible 113 | setTimeout(adjustModalHeight, 150); 114 | 115 | // Mark that the modal has been shown in this session 116 | modalShownInCurrentSession = true; 117 | } 118 | } 119 | 120 | /** 121 | * Hides the What's New modal with smooth transition 122 | */ 123 | function hideWhatsNewModal() { 124 | if (whatsNewModal) { 125 | const modalContent = whatsNewModal.querySelector('.modal-content'); 126 | if (modalContent) { 127 | // Get all feature items for resetting later 128 | const featureItems = modalContent.querySelectorAll('.feature-item'); 129 | 130 | // Apply exit animation to the content 131 | modalContent.style.transition = 'transform 0.3s ease-in, opacity 0.3s ease-in'; 132 | modalContent.style.transform = 'translateY(20px)'; 133 | modalContent.style.opacity = '0'; 134 | 135 | // Start fading out the entire modal 136 | setTimeout(() => { 137 | whatsNewModal.classList.add('fade-out'); 138 | 139 | // Wait for the transition to complete before hiding 140 | setTimeout(() => { 141 | whatsNewModal.classList.remove('fade-out'); 142 | whatsNewModal.classList.add('hidden'); 143 | 144 | // Reset the content styles for next time 145 | if (modalContent) { 146 | modalContent.style.transition = ''; 147 | modalContent.style.transform = ''; 148 | modalContent.style.opacity = ''; 149 | 150 | // Reset feature item styles 151 | featureItems.forEach(item => { 152 | item.style.transition = ''; 153 | item.style.transform = ''; 154 | item.style.opacity = ''; 155 | }); 156 | } 157 | 158 | // Save the user's preference if they chose not to show again 159 | savePreferences(); 160 | 161 | // Remove resize event listener when modal is hidden 162 | window.removeEventListener('resize', adjustModalHeight); 163 | 164 | // Check if welcome message should be shown 165 | checkAndShowWelcomeMessage(); 166 | }, 300); // Match this with the CSS transition duration 167 | }, 100); // Short delay before starting the fade-out 168 | } else { 169 | // Fallback if modal content is not found 170 | whatsNewModal.classList.add('fade-out'); 171 | setTimeout(() => { 172 | whatsNewModal.classList.remove('fade-out'); 173 | whatsNewModal.classList.add('hidden'); 174 | savePreferences(); 175 | window.removeEventListener('resize', adjustModalHeight); 176 | checkAndShowWelcomeMessage(); 177 | }, 300); 178 | } 179 | } 180 | } 181 | 182 | /** 183 | * Saves the user's preferences to local storage 184 | */ 185 | function savePreferences() { 186 | if (dontShowAgainToggle && dontShowAgainToggle.checked) { 187 | localStorage.setItem(WHATS_NEW_DONT_SHOW_KEY, 'true'); 188 | localStorage.setItem(WHATS_NEW_SEEN_KEY, WHATS_NEW_VERSION); 189 | } else { 190 | localStorage.removeItem(WHATS_NEW_DONT_SHOW_KEY); 191 | localStorage.removeItem(WHATS_NEW_SEEN_KEY); 192 | } 193 | } 194 | 195 | /** 196 | * Checks if the modal should be shown 197 | * @returns {boolean} True if the modal should be shown 198 | */ 199 | function shouldShowModal() { 200 | const dontShow = localStorage.getItem(WHATS_NEW_DONT_SHOW_KEY) === 'true'; 201 | const lastSeenVersion = localStorage.getItem(WHATS_NEW_SEEN_KEY); 202 | 203 | // Check if refresh was triggered by code generation 204 | const isRefreshDueToCode = wasRefreshDueToCodeGeneration(); 205 | 206 | // Don't show if user has opted out 207 | if (dontShow) { 208 | return false; 209 | } 210 | 211 | // Don't show if refresh was triggered by code generation 212 | if (isRefreshDueToCode) { 213 | return false; 214 | } 215 | 216 | // Show if never seen before or if version is different 217 | return !lastSeenVersion || lastSeenVersion !== WHATS_NEW_VERSION; 218 | } 219 | 220 | /** 221 | * Adjusts the modal content to fit all features without scrolling 222 | * Makes the container height dynamic based on content 223 | */ 224 | function adjustModalHeight() { 225 | const featuresContainer = document.querySelector('#whats-new-modal .features-container'); 226 | const modalContent = document.querySelector('#whats-new-modal .modal-content'); 227 | 228 | if (featuresContainer && modalContent) { 229 | // Reset any previously set height to get the natural content height 230 | featuresContainer.style.height = 'auto'; 231 | featuresContainer.style.maxHeight = 'none'; 232 | 233 | // Get the content's natural height 234 | const contentHeight = featuresContainer.scrollHeight; 235 | 236 | // Get the available viewport height 237 | const viewportHeight = window.innerHeight; 238 | 239 | // Get the header and footer heights 240 | const header = modalContent.querySelector('.flex.justify-between.items-center.mb-2'); 241 | const footer = modalContent.querySelector('.flex.justify-between.items-center.pt-2'); 242 | 243 | const headerHeight = header ? header.offsetHeight : 0; 244 | const footerHeight = footer ? footer.offsetHeight : 0; 245 | 246 | // Calculate modal padding 247 | const modalPadding = 24; // 3rem (p-3 class) 248 | 249 | // Calculate the maximum available height for the features container 250 | // Leave some space (10% of viewport) for padding and margins 251 | const maxAvailableHeight = viewportHeight * 0.9 - headerHeight - footerHeight - modalPadding; 252 | 253 | // Set the height to either the content height or the max available height, whichever is smaller 254 | const finalHeight = Math.min(contentHeight, maxAvailableHeight); 255 | 256 | // Apply the calculated height 257 | featuresContainer.style.height = finalHeight + 'px'; 258 | 259 | // If content fits without scrolling, disable scrolling 260 | if (contentHeight <= finalHeight) { 261 | featuresContainer.classList.remove('overflow-y-auto'); 262 | featuresContainer.classList.add('overflow-y-hidden'); 263 | } else { 264 | // Content needs scrolling, ensure scrolling is enabled 265 | featuresContainer.classList.add('overflow-y-auto'); 266 | featuresContainer.classList.remove('overflow-y-hidden'); 267 | } 268 | } 269 | } 270 | 271 | /** 272 | * Sets up proper touch scrolling for the What's New modal 273 | */ 274 | function setupTouchScrolling() { 275 | const featuresContainer = document.querySelector('#whats-new-modal .features-container'); 276 | const modalContent = document.querySelector('#whats-new-modal .modal-content'); 277 | 278 | if (featuresContainer) { 279 | // Remove any conflicting classes that might interfere with native scrolling 280 | featuresContainer.classList.remove('drag-scrollable'); 281 | modalContent?.classList.remove('drag-scrollable'); 282 | 283 | // Ensure proper CSS properties for smooth native touch scrolling 284 | featuresContainer.style.webkitOverflowScrolling = 'touch'; 285 | featuresContainer.style.overflowY = 'auto'; 286 | featuresContainer.style.touchAction = 'pan-y'; 287 | featuresContainer.style.overscrollBehavior = 'contain'; 288 | featuresContainer.style.msOverflowStyle = '-ms-autohiding-scrollbar'; 289 | 290 | // Remove any existing touch event listeners that might conflict 291 | const newFeaturesContainer = featuresContainer.cloneNode(true); 292 | featuresContainer.parentNode.replaceChild(newFeaturesContainer, featuresContainer); 293 | } 294 | } 295 | 296 | /** 297 | * Initializes the What's New modal functionality 298 | */ 299 | export function initializeWhatsNew() { 300 | // Load the user's preference for the "Don't show again" toggle 301 | loadPreferences(); 302 | 303 | // Always show the modal unless the user has opted out 304 | if (shouldShowModal()) { 305 | // Small delay to ensure the app is loaded 306 | setTimeout(() => showWhatsNewModal(), 1000); 307 | } 308 | 309 | // Event listeners 310 | if (closeWhatsNewButton) { 311 | closeWhatsNewButton.addEventListener('click', () => { 312 | hideWhatsNewModal(); 313 | }); 314 | } 315 | 316 | if (gotItButton) { 317 | gotItButton.addEventListener('click', () => { 318 | hideWhatsNewModal(); 319 | }); 320 | } 321 | 322 | // Add event listener for the toggle to update its appearance 323 | if (dontShowAgainToggle) { 324 | dontShowAgainToggle.addEventListener('change', updateToggleAppearance); 325 | } 326 | 327 | // Close modal when pressing Escape key 328 | document.addEventListener('keydown', (e) => { 329 | if (e.key === 'Escape' && whatsNewModal && !whatsNewModal.classList.contains('hidden')) { 330 | hideWhatsNewModal(); 331 | } 332 | }); 333 | 334 | // Close modal when clicking outside 335 | whatsNewModal?.addEventListener('click', (e) => { 336 | if (e.target === whatsNewModal) { 337 | hideWhatsNewModal(); 338 | } 339 | }); 340 | 341 | // Add window resize listener to adjust modal height when window size changes 342 | window.addEventListener('resize', adjustModalHeight); 343 | } -------------------------------------------------------------------------------- /reasoning.js: -------------------------------------------------------------------------------- 1 | // Function to format reasoning output 2 | function formatReasoning(content) { 3 | // Extract the content between tags 4 | const thinkMatch = content.match(/([\s\S]*?)<\/think>/); 5 | if (!thinkMatch) return content; 6 | 7 | const thinkContent = thinkMatch[1].trim(); 8 | 9 | // If think content is empty, don't show reasoning header 10 | if (thinkContent.length === 0) { 11 | return content.replace(/[\s\S]*?<\/think>/g, ''); 12 | } 13 | 14 | // Create the reasoning container 15 | const reasoningHtml = ` 16 |
17 |
18 | 19 | Reasoning Process 20 | [Hide] 21 |
22 |
23 | ${thinkContent.split('\n\n').map(paragraph => { 24 | // Skip empty paragraphs 25 | if (!paragraph.trim()) return ''; 26 | return `
${paragraph.trim()}
`; 27 | }).join('')} 28 |
29 |
30 | `; 31 | 32 | // Replace the original block with the formatted version 33 | return content.replace(/[\s\S]*?<\/think>/, reasoningHtml); 34 | } 35 | 36 | // Function to format special text elements 37 | function formatSpecialText(content) { 38 | // Replace color spans with styled spans 39 | content = content.replace(/([^<]+)<\/span>/g, 40 | (match, color, text) => `${text}`); 41 | 42 | // Format mathematical expressions 43 | content = content.replace(/\\\(([^\)]+)\\\)/g, 44 | (match, expr) => `${expr}`); 45 | 46 | // Format boxed content 47 | content = content.replace(/\\boxed\{([^\}]+)\}/g, 48 | (match, content) => `${content}`); 49 | 50 | return content; 51 | } 52 | 53 | // Add the styles for special formatting 54 | const style = document.createElement('style'); 55 | style.textContent = ` 56 | .math-expr { 57 | font-family: 'Times New Roman', serif; 58 | font-style: italic; 59 | color: #7cb7ff; 60 | } 61 | 62 | .boxed { 63 | border: 2px solid #7cb7ff; 64 | padding: 2px 6px; 65 | border-radius: 4px; 66 | margin: 0 2px; 67 | color: #7cb7ff; 68 | font-weight: bold; 69 | } 70 | 71 | .reasoning-toggle { 72 | font-size: 0.8em; 73 | margin-left: 10px; 74 | cursor: pointer; 75 | color: #7cb7ff; 76 | } 77 | 78 | .reasoning-toggle:hover { 79 | text-decoration: underline; 80 | } 81 | `; 82 | document.head.appendChild(style); 83 | 84 | // Function to handle toggling visibility of individual thinking sections 85 | function setupReasoningToggles() { 86 | // We now use a direct onclick handler in the HTML, so we don't need 87 | // to set up event listeners here anymore. 88 | // This function is kept for backward compatibility. 89 | 90 | // The toggle functionality is now handled by window.toggleReasoningVisibility 91 | // which is defined in utils.js 92 | } 93 | 94 | // Set up event listeners when the DOM is loaded 95 | document.addEventListener('DOMContentLoaded', setupReasoningToggles); 96 | 97 | // Export the formatting functions 98 | window.formatReasoning = formatReasoning; 99 | window.formatSpecialText = formatSpecialText; -------------------------------------------------------------------------------- /splash2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/techcow2/LMSA/490959b2154fdc7ed4bd0e8c252698aacdfe9a45/splash2.png --------------------------------------------------------------------------------