diff --git a/lib/main.dart b/lib/main.dart index 8e37a0e1..aea8fda3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,11 +16,20 @@ void main() async { await Babylon.init(); MaidProperties props = await MaidProperties.last; + final settingsService = AppSettingsService(); + await settingsService.init(); runApp( - MaidApp( - props: props - ) + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => AppPreferences()), + ChangeNotifierProvider(create: (_) => CharacterProvider()), + ChangeNotifierProvider(create: (_) => ModelProvider()), + ], + child: const MaidApp( + props: props + ), + ), ); } diff --git a/lib/providers/app_preferences.dart b/lib/providers/app_preferences.dart new file mode 100644 index 00000000..fa49f25d --- /dev/null +++ b/lib/providers/app_preferences.dart @@ -0,0 +1,42 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class AppPreferences extends ChangeNotifier { + final AppSettingsService _settingsService = AppSettingsService(); + + // Load saved settings on initialization + Future loadSavedSettings() async { + final selectedModel = _settingsService.getSelectedModel(); + final modelParameters = _settingsService.getModelParameters(); + final selectedCharacter = _settingsService.getSelectedCharacter(); + final characterSettings = _settingsService.getCharacterSettings(); + + if (selectedModel != null) { + // Apply saved model selection + currentModel = selectedModel; + } + + if (modelParameters != null) { + // Apply saved model parameters + applyModelParameters(modelParameters); + } + + // ... apply other settings as needed ... + + notifyListeners(); + } + + // Save settings when they change + Future updateModelSelection(String modelId) async { + await _settingsService.saveSelectedModel(modelId); + currentModel = modelId; + notifyListeners(); + } + + Future updateModelParameters(Map parameters) async { + await _settingsService.saveModelParameters(parameters); + // Apply parameters + notifyListeners(); + } + + // ... other methods ... +} diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart new file mode 100644 index 00000000..5e936091 --- /dev/null +++ b/lib/services/app_settings_service.dart @@ -0,0 +1,118 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; + +class AppSettingsService { + static const String _selectedCharacterKey = 'selected_character'; + static const String _selectedModelKey = 'selected_model'; + static const String _modelParametersKey = 'model_parameters'; + static const String _characterSettingsKey = 'character_settings'; + + // Singleton instance + static final AppSettingsService _instance = AppSettingsService._internal(); + factory AppSettingsService() => _instance; + AppSettingsService._internal(); + + late SharedPreferences _prefs; + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + // Save methods + Future saveSelectedCharacter(String characterId) async { + await _prefs.setString(_selectedCharacterKey, characterId); + } + + Future saveSelectedModel(String modelId) async { + await _prefs.setString(_selectedModelKey, modelId); + } + + Future saveModelParameters(Map parameters) async { + await _prefs.setString(_modelParametersKey, jsonEncode(parameters)); + } + + Future saveCharacterSettings(Map settings) async { + await _prefs.setString(_characterSettingsKey, jsonEncode(settings)); + } + + // Load methods + String? getSelectedCharacter() { + return _prefs.getString(_selectedCharacterKey); + } + + String? getSelectedModel() { + return _prefs.getString(_selectedModelKey); + } + + Map? getModelParameters() { + final String? data = _prefs.getString(_modelParametersKey); + if (data == null) return null; + return jsonDecode(data) as Map; + } + + Map? getCharacterSettings() { + final String? data = _prefs.getString(_characterSettingsKey); + if (data == null) return null; + return jsonDecode(data) as Map; + } + + // Clear methods + Future clearAllSettings() async { + await _prefs.clear(); + } + + // Model-specific keys + static const String _modelTemperatureKey = 'model_temperature'; + static const String _modelMaxTokensKey = 'model_max_tokens'; + static const String _modelTopPKey = 'model_top_p'; + static const String _modelTopKKey = 'model_top_k'; + static const String _modelContextLengthKey = 'model_context_length'; + + Future saveModelSettings({ + required String modelId, + required double temperature, + required int maxTokens, + required double topP, + required int topK, + required int contextLength, + }) async { + await _prefs.setString(_selectedModelKey, modelId); + await _prefs.setDouble(_modelTemperatureKey, temperature); + await _prefs.setInt(_modelMaxTokensKey, maxTokens); + await _prefs.setDouble(_modelTopPKey, topP); + await _prefs.setInt(_modelTopKKey, topK); + await _prefs.setInt(_modelContextLengthKey, contextLength); + } + + ModelSettings? getModelSettings() { + final modelId = _prefs.getString(_selectedModelKey); + if (modelId == null) return null; + + return ModelSettings( + modelId: modelId, + temperature: _prefs.getDouble(_modelTemperatureKey) ?? 0.7, + maxTokens: _prefs.getInt(_modelMaxTokensKey) ?? 2048, + topP: _prefs.getDouble(_modelTopPKey) ?? 0.9, + topK: _prefs.getInt(_modelTopKKey) ?? 40, + contextLength: _prefs.getInt(_modelContextLengthKey) ?? 4096, + ); + } +} + +class ModelSettings { + final String modelId; + final double temperature; + final int maxTokens; + final double topP; + final int topK; + final int contextLength; + + ModelSettings({ + required this.modelId, + required this.temperature, + required this.maxTokens, + required this.topP, + required this.topK, + required this.contextLength, + }); +} diff --git a/lib/ui/mobile/app.dart b/lib/ui/mobile/app.dart index cab8945b..29727f89 100644 --- a/lib/ui/mobile/app.dart +++ b/lib/ui/mobile/app.dart @@ -12,46 +12,99 @@ import 'package:maid/ui/mobile/pages/model_settings/ollama_page.dart'; import 'package:maid/ui/mobile/pages/model_settings/open_ai_page.dart'; import 'package:maid/ui/mobile/pages/settings_page.dart'; import 'package:provider/provider.dart'; +import 'package:your_app/services/app_settings_service.dart'; +import 'package:your_app/models/model_provider.dart'; +import 'package:your_app/models/character_provider.dart'; /// The [MobileApp] class represents the main application widget for the mobile platforms. -/// It is a stateless widget that builds the user interface based on the consumed [AppPreferences]. -class MobileApp extends StatelessWidget { +/// It is a stateful widget that builds the user interface based on the consumed [AppPreferences]. +class MobileApp extends StatefulWidget { const MobileApp({super.key}); @override - Widget build(BuildContext context) { - return Consumer( - builder: appBuilder - ); + State createState() => _MobileAppState(); +} + +class _MobileAppState extends State { + final AppSettingsService _settings = AppSettingsService(); + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadAllSettings(); } - /// Builds the root widget for the Maid mobile app. - /// - /// This function takes in the [context], [appPreferences], and [child] parameters - /// and returns a [MaterialApp] widget that serves as the root of the app. - /// The [MaterialApp] widget is configured with various properties such as the app title, - /// theme, initial route, and route mappings. - /// The [home] property is set to [MobileHomePage], which serves as the default landing page. - Widget appBuilder(BuildContext context, AppPreferences appPreferences, Widget? child) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Maid', - theme: Themes.lightTheme(), - darkTheme: Themes.darkTheme(), - themeMode: appPreferences.themeMode, - initialRoute: '/', - routes: { - '/character': (context) => const CharacterCustomizationPage(), - '/characters': (context) => const CharacterBrowserPage(), - '/llamacpp': (context) => const LlamaCppPage(), - '/ollama': (context) => const OllamaPage(), - '/openai': (context) => const OpenAiPage(), - '/mistralai': (context) => const MistralAiPage(), - '/gemini': (context) => const GoogleGeminiPage(), - '/settings': (context) => const SettingsPage(), - '/about': (context) => const AboutPage(), + Future _loadAllSettings() async { + try { + // Load model settings + final modelSettings = _settings.getModelSettings(); + final characterId = _settings.getSelectedCharacter(); + + if (mounted) { + // Apply model settings + if (modelSettings != null) { + final modelProvider = context.read(); + await modelProvider.setModel(modelSettings.modelId); + modelProvider.updateParameters( + temperature: modelSettings.temperature, + maxTokens: modelSettings.maxTokens, + topP: modelSettings.topP, + topK: modelSettings.topK, + contextLength: modelSettings.contextLength, + ); + } + + // Apply character settings + if (characterId != null) { + await context.read().selectCharacter(characterId); + } + } + } catch (e) { + debugPrint('Error loading settings: $e'); + } finally { + if (mounted) { + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, appPreferences, child) { + if (_isLoading) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } + + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Maid', + theme: Themes.lightTheme(), + darkTheme: Themes.darkTheme(), + themeMode: appPreferences.themeMode, + initialRoute: '/', + routes: { + '/character': (context) => const CharacterCustomizationPage(), + '/characters': (context) => const CharacterBrowserPage(), + '/llamacpp': (context) => const LlamaCppPage(), + '/ollama': (context) => const OllamaPage(), + '/openai': (context) => const OpenAiPage(), + '/mistralai': (context) => const MistralAiPage(), + '/gemini': (context) => const GoogleGeminiPage(), + '/settings': (context) => const SettingsPage(), + '/about': (context) => const AboutPage(), + }, + home: const MobileHomePage() + ); }, - home: const MobileHomePage() ); } } \ No newline at end of file diff --git a/lib/ui/shared/screens/home_screen.dart b/lib/ui/shared/screens/home_screen.dart new file mode 100644 index 00000000..3a4197c7 --- /dev/null +++ b/lib/ui/shared/screens/home_screen.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:your_app/ui/shared/widgets/settings_drawer.dart'; +import 'package:your_app/ui/shared/screens/chat_view.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final modelProvider = context.watch(); + final characterProvider = context.watch(); + + return Scaffold( + appBar: AppBar( + title: Text(characterProvider.selectedCharacter?.name ?? 'Maid'), + actions: [ + // Model indicator + Padding( + padding: const EdgeInsets.all(8.0), + child: Chip( + label: Text(modelProvider.currentModel?.name ?? 'No Model'), + avatar: const Icon(Icons.memory), + ), + ), + ], + ), + body: const ChatView(), + drawer: SettingsDrawer(), + ); + } +} diff --git a/lib/ui/shared/screens/model_selection_screen.dart b/lib/ui/shared/screens/model_selection_screen.dart new file mode 100644 index 00000000..56e894c8 --- /dev/null +++ b/lib/ui/shared/screens/model_selection_screen.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:maiden/model.dart'; +import 'package:maiden/settings.dart'; + +class ModelSelectionScreen extends StatefulWidget { + @override + _ModelSelectionScreenState createState() => _ModelSelectionScreenState(); +} + +class _ModelSelectionScreenState extends State { + final AppSettingsService _settings = AppSettingsService(); + late TextEditingController _temperatureController; + late TextEditingController _maxTokensController; + late TextEditingController _topPController; + late TextEditingController _topKController; + late TextEditingController _contextLengthController; + + @override + void initState() { + super.initState(); + _initializeControllers(); + _loadSavedSettings(); + } + + void _initializeControllers() { + _temperatureController = TextEditingController(text: '0.7'); + _maxTokensController = TextEditingController(text: '2048'); + _topPController = TextEditingController(text: '0.9'); + _topKController = TextEditingController(text: '40'); + _contextLengthController = TextEditingController(text: '4096'); + } + + Future _loadSavedSettings() async { + final settings = _settings.getModelSettings(); + if (settings != null) { + setState(() { + _temperatureController.text = settings.temperature.toString(); + _maxTokensController.text = settings.maxTokens.toString(); + _topPController.text = settings.topP.toString(); + _topKController.text = settings.topK.toString(); + _contextLengthController.text = settings.contextLength.toString(); + + // Update the selected model in your state management + context.read().setModel(settings.modelId); + }); + } + } + + Future _saveSettings() async { + try { + final modelId = context.read().currentModel.id; + + await _settings.saveModelSettings( + modelId: modelId, + temperature: double.parse(_temperatureController.text), + maxTokens: int.parse(_maxTokensController.text), + topP: double.parse(_topPController.text), + topK: int.parse(_topKController.text), + contextLength: int.parse(_contextLengthController.text), + ); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Model settings saved successfully')), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Error saving settings: $e')), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Model Settings'), + actions: [ + IconButton( + icon: const Icon(Icons.save), + onPressed: _saveSettings, + ), + ], + ), + body: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + ModelSelector( + onModelSelected: (Model model) async { + context.read().setModel(model.id); + await _saveSettings(); + }, + ), + const SizedBox(height: 16), + _buildParameterInput( + controller: _temperatureController, + label: 'Temperature', + hint: '0.0 - 1.0', + ), + _buildParameterInput( + controller: _maxTokensController, + label: 'Max Tokens', + hint: 'Enter max tokens', + keyboardType: TextInputType.number, + ), + _buildParameterInput( + controller: _topPController, + label: 'Top P', + hint: '0.0 - 1.0', + ), + _buildParameterInput( + controller: _topKController, + label: 'Top K', + hint: 'Enter top K value', + keyboardType: TextInputType.number, + ), + _buildParameterInput( + controller: _contextLengthController, + label: 'Context Length', + hint: 'Enter context length', + keyboardType: TextInputType.number, + ), + ], + ), + ); + } + + Widget _buildParameterInput({ + required TextEditingController controller, + required String label, + required String hint, + TextInputType keyboardType = TextInputType.number, + }) { + return Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: TextField( + controller: controller, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: label, + hintText: hint, + border: const OutlineInputBorder(), + ), + onChanged: (_) => _saveSettings(), + ), + ); + } + + @override + void dispose() { + _temperatureController.dispose(); + _maxTokensController.dispose(); + _topPController.dispose(); + _topKController.dispose(); + _contextLengthController.dispose(); + super.dispose(); + } +} diff --git a/lib/ui/shared/views/characters_grid_view.dart b/lib/ui/shared/views/characters_grid_view.dart index 05040005..e4faf502 100644 --- a/lib/ui/shared/views/characters_grid_view.dart +++ b/lib/ui/shared/views/characters_grid_view.dart @@ -2,11 +2,31 @@ import 'package:flutter/material.dart'; import 'package:maid/classes/providers/characters.dart'; import 'package:maid/ui/shared/tiles/character_tile.dart'; import 'package:provider/provider.dart'; +import 'package:maid/services/app_settings_service.dart'; -class CharactersGridView extends StatelessWidget { - +class CharactersGridView extends StatefulWidget { const CharactersGridView({super.key}); + @override + State createState() => _CharactersGridViewState(); +} + +class _CharactersGridViewState extends State { + final AppSettingsService _settings = AppSettingsService(); + + @override + void initState() { + super.initState(); + _loadSavedCharacter(); + } + + Future _loadSavedCharacter() async { + final savedCharacterId = _settings.getSelectedCharacter(); + if (savedCharacterId != null && mounted) { + CharacterCollection.of(context).setCurrent(savedCharacterId); + } + } + @override Widget build(BuildContext context) { return Consumer( @@ -15,7 +35,9 @@ class CharactersGridView extends StatelessWidget { } Widget buildGridView(BuildContext context, CharacterCollection characters, Widget? child) { + // Save character state and settings characters.save(); + _handleCharacterSelect(characters.current); return GridView.builder( itemCount: characters.list.length, @@ -35,4 +57,18 @@ class CharactersGridView extends StatelessWidget { isSelected: CharacterCollection.of(context).current == CharacterCollection.of(context).list[index], ); } + + void _handleCharacterSelect(Character? character) async { + if (character == null) return; + + // Save the selection + await _settings.saveSelectedCharacter(character.id); + + // Save additional character settings + await _settings.saveCharacterSettings({ + 'name': character.name, + 'personality': character.personality, + // Add other relevant character settings + }); + } } \ No newline at end of file diff --git a/lib/ui/shared/widgets/settings_drawer.dart b/lib/ui/shared/widgets/settings_drawer.dart new file mode 100644 index 00000000..d93d6c90 --- /dev/null +++ b/lib/ui/shared/widgets/settings_drawer.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; + +class SettingsDrawer extends StatelessWidget { + const SettingsDrawer({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final modelProvider = context.watch(); + final characterProvider = context.watch(); + + return Drawer( + child: ListView( + children: [ + DrawerHeader( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Current Settings', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text( + 'Model: ${modelProvider.currentModel?.name ?? 'None'}', + style: const TextStyle(color: Colors.white), + ), + Text( + 'Character: ${characterProvider.selectedCharacter?.name ?? 'None'}', + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ListTile( + leading: const Icon(Icons.memory), + title: const Text('Model Settings'), + subtitle: Text( + 'Temperature: ${modelProvider.temperature.toStringAsFixed(2)}\n' + 'Max Tokens: ${modelProvider.maxTokens}', + ), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ModelSelectionScreen(), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.person), + title: const Text('Character Settings'), + subtitle: Text(characterProvider.selectedCharacter?.name ?? 'None'), + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const CharacterSelectionScreen(), + ), + ); + }, + ), + // Add reset settings option + ListTile( + leading: const Icon(Icons.restore), + title: const Text('Reset Settings'), + onTap: () => _showResetDialog(context), + ), + ], + ), + ); + } + + Future _showResetDialog(BuildContext context) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Reset Settings'), + content: const Text( + 'Are you sure you want to reset all settings to default values?' + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('CANCEL'), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('RESET'), + ), + ], + ), + ); + + if (confirmed == true && context.mounted) { + final settings = AppSettingsService(); + await settings.clearAllSettings(); + + // Reset providers + context.read().reset(); + context.read().reset(); + + if (context.mounted) { + Navigator.pop(context); // Close drawer + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Settings reset to defaults')), + ); + } + } + } +}