main.dart
1 import 'package:flutter/material.dart'; // This line imports the Material Design package from Flutter, providing UI components. 2 import 'package:flutter_chat_types/flutter_chat_types.dart' as types; // Imports the Flutter Chat Types package with an alias 'types' for easy reference to message types. 3 import 'package:flutter/services.dart'; // Imports Flutter's services library, which includes functionalities like clipboard access. 4 import 'gpt-api.dart'; // Imports a custom API handler for interacting with a GPT-based service. 5 import 'settings.dart'; // Imports the settings page for the application, allowing users to adjust app preferences. 6 import 'status.dart'; // Imports the status page, likely used to display some form of user or application status. 7 import 'prodia-api.dart' as prodia; // Imports a custom Prodia API handler with an alias 'prodia', used for AI image generation. 8 import 'dart:async'; // Imports Dart's asynchronous library, which includes classes like Future and Stream for handling asynchronous operations. 9 10 // A global notifier that tracks the theme mode (dark or light) across the app. 11 final ValueNotifier<bool> isDarkMode = ValueNotifier(true); 12 13 // Global variables to maintain the chat session's state across different pages without persisting data to the device's memory. 14 final List<types.Message> globalMessages = []; // Holds the list of chat messages. 15 String? globalChatId; // Optionally stores a global chat identifier. 16 String? latestImageUrl; // Optionally stores the URL of the most recently generated image for easy access. 17 18 // Defines a class to manage the loading state and microphone access state across the app. 19 class LoadingState extends ChangeNotifier { 20 bool _isLoading = false; // Tracks whether a loading indicator should be shown. 21 bool _isMicOpen = false; // Tracks whether the microphone is in use. 22 23 // Public getters to expose the private state variables. 24 bool get isLoading => _isLoading; 25 bool get isMicOpen => _isMicOpen; 26 27 // Method to initiate loading state, ensuring the microphone is not already in use. 28 void startLoading() { 29 if (!_isMicOpen) { 30 _isLoading = true; 31 _isMicOpen = true; 32 notifyListeners(); // Notifies listeners of state changes to update UI accordingly. 33 } 34 } 35 36 // Method to stop the loading state and indicate the microphone is no longer in use. 37 void stopLoading() { 38 _isLoading = false; 39 _isMicOpen = false; 40 notifyListeners(); // Notifies listeners of state changes to update UI accordingly. 41 } 42 } 43 44 // Instantiates the LoadingState class for global access. 45 final LoadingState loadingState = LoadingState(); 46 47 // The main entry point of the Flutter application. 48 void main() => runApp(const MyApp()); 49 50 // Defines the MyApp widget, which serves as the root of the application. 51 class MyApp extends StatelessWidget { 52 const MyApp({super.key}); // Constructor with an optional key parameter. 53 54 // Builds the MaterialApp widget with a theme that toggles based on the isDarkMode value. 55 @override 56 Widget build(BuildContext context) { 57 return ValueListenableBuilder<bool>( 58 valueListenable: isDarkMode, 59 builder: (context, isDark, _) { 60 return MaterialApp( 61 theme: isDark ? ThemeData.dark() : ThemeData.light(), // Applies dark or light theme based on isDark value. 62 home: const MyHomePage(), // Sets MyHomePage as the default route. 63 ); 64 }, 65 ); 66 } 67 } 68 69 // Defines a stateful widget for the home page of the app. 70 class MyHomePage extends StatefulWidget { 71 const MyHomePage({super.key}); // Constructor with an optional key parameter. 72 73 @override 74 State<MyHomePage> createState() => _MyHomePageState(); // Creates the mutable state for this widget. 75 } 76 77 // The state class associated with MyHomePage, containing the dynamic state of the home page. 78 class _MyHomePageState extends State<MyHomePage> { 79 late types.User user; // Declares a User object for the chat, initialized in initState. 80 int _selectedIndex = 0; // Tracks the currently selected index in the bottom navigation bar. 81 final TextEditingController _textController = TextEditingController(); // Manages the text input field for chat messages. 82 bool _isSendButtonVisible = false; // Controls the visibility of the send button based on text input. 83 String _selectedService = 'chatbot'; // Tracks the currently selected service (e.g., chatbot or AI image generation). 84 85 @override 86 void initState() { 87 super.initState(); 88 user = types.User(id: 'user'); // Initializes the user object with a unique identifier. 89 // Sets the global chat ID for the session, allowing for continuity across app usage. 90 GPTAPI.setChatId(globalChatId); 91 _textController.addListener(_handleTextChange); // Adds a listener to handle changes in the text input field. 92 loadingState.addListener(_handleLoadingStateChange); // Adds a listener to react to changes in the loading state. 93 } 94 95 @override 96 void dispose() { 97 loadingState.removeListener(_handleLoadingStateChange); // Removes the loading state listener upon widget disposal. 98 super.dispose(); 99 } 100 101 // Handles changes in the loading state, showing or hiding a loading dialog accordingly. 102 void _handleLoadingStateChange() { 103 if (loadingState.isLoading) { 104 showDialog( 105 context: context, 106 barrierDismissible: false, 107 builder: (BuildContext context) { 108 return WillPopScope( 109 onWillPop: () async => false, // Prevents dialog dismissal on back press. 110 child: AlertDialog( 111 content: Column( 112 mainAxisSize: MainAxisSize.min, 113 children: [ 114 CircularProgressIndicator(), // Displays a loading spinner. 115 SizedBox(height: 20), // Adds vertical spacing. 116 Text('Processing...'), // Displays a processing message. 117 ], 118 ), 119 ), 120 ); 121 }, 122 ); 123 } else { 124 Navigator.of(context, rootNavigator: true).pop('dialog'); // Dismisses the dialog when loading completes. 125 } 126 } 127 128 // Handles text input changes, updating the send button's visibility based on the input's content. 129 void _handleTextChange() { 130 final text = _textController.text; 131 setState(() { 132 _isSendButtonVisible = text.trim().isNotEmpty; // Shows the send button only if there's non-whitespace text. 133 }); 134 } 135 136 // Adds a message to the chat, either from the user or as a response. 137 void _addMessage(String text, {bool isUserMessage = true}) { 138 final types.TextMessage message = types.TextMessage( 139 author: isUserMessage ? user : types.User(id: 'ai'), // Sets the message author based on the message source. 140 createdAt: DateTime.now().millisecondsSinceEpoch, // Sets the creation time to the current timestamp. 141 id: DateTime.now().toString(), // Generates a unique ID based on the current time. 142 text: text, // Sets the message text. 143 ); 144 145 setState(() { 146 globalMessages.insert(0, message); // Inserts the new message at the beginning of the chat log. 147 }); 148 } 149 150 // Sends a message and handles the response, either by displaying the AI's text response or generating an image. 151 void _sendMessage(types.PartialText message) { 152 final text = message.text; 153 _addMessage(text); // Immediately displays the user's message in the chat. 154 loadingState.startLoading(); // Initiates the loading state. 155 if (_selectedService == 'chatbot') { 156 GPTAPI.sendMessage(text).then((response) { 157 // Handles the plain text response from the AI. 158 _addMessage(response, isUserMessage: false); // Adds the AI's response to the chat. 159 loadingState.stopLoading(); // Stops the loading state once the AI responds. 160 }).catchError((error) { 161 ScaffoldMessenger.of(context).showSnackBar( 162 SnackBar(content: Text('Failed to send message: $error')), // Displays an error message if the send fails. 163 ); 164 loadingState.stopLoading(); // Ensures the loading state is stopped even if an error occurs. 165 }); 166 } else if (_selectedService == 'ai_generation') { 167 prodia.ProdiaAPI.generateImage(text).then((response) { 168 // Handles the response from the image generation API. 169 final types.ImageMessage imageMessage = types.ImageMessage( 170 author: types.User(id: 'ai'), // Sets the AI as the author of the image message. 171 createdAt: DateTime.now().millisecondsSinceEpoch, // Sets the creation time to the current timestamp. 172 id: DateTime.now().toString(), // Generates a unique ID for the message. 173 uri: response['imageUrl'], // Sets the image URL from the API response. 174 width: 512, // Assumes a fixed width for the image. 175 height: 512, // Assumes a fixed height for the image. 176 name: 'AI Generated Image', // Provides a name for the image. 177 size: response['fileSize'], // Sets the image size based on the API response, assuming 'fileSize' is provided. 178 ); 179 latestImageUrl = response['imageUrl']; // Updates the latest image URL for clipboard access. 180 setState(() { 181 globalMessages.insert(0, imageMessage); // Adds the image message to the chat log. 182 }); 183 loadingState.stopLoading(); // Stops the loading state once the image is generated. 184 }).catchError((error) { 185 ScaffoldMessenger.of(context).showSnackBar( 186 SnackBar(content: Text('Failed to generate image: $error')), // Displays an error message if image generation fails. 187 ); 188 loadingState.stopLoading(); // Ensures the loading state is stopped even if an error occurs. 189 }); 190 } 191 _textController.clear(); // Clears the text input field after sending a message. 192 } 193 194 // Builds the UI for the home page, including the app bar, chat log, text input field, and bottom navigation bar. 195 @override 196 Widget build(BuildContext context) { 197 return Scaffold( 198 appBar: AppBar( 199 title: const Text('Chat with AI'), // Sets the title of the app bar. 200 actions: <Widget>[ 201 IconButton( 202 icon: const Icon(Icons.refresh), // Displays a refresh icon. 203 onPressed: _resetChat, // Resets the chat when the icon is pressed. 204 ), 205 ], 206 ), 207 body: Column( 208 children: [ 209 DropdownButton<String>( 210 value: _selectedService, // Binds the selected service to the dropdown. 211 items: <DropdownMenuItem<String>>[ 212 DropdownMenuItem(value: 'chatbot', child: Text('Chatbot')), // Option for the chatbot service. 213 DropdownMenuItem(value: 'ai_generation', child: Text('AI Generation')), // Option for the AI image generation service. 214 ], 215 onChanged: (value) { 216 setState(() { 217 _selectedService = value!; // Updates the selected service when a new option is chosen. 218 }); 219 }, 220 ), 221 Expanded( 222 child: ListView.builder( 223 reverse: true, // Reverses the order of chat messages to start from the bottom. 224 itemCount: globalMessages.length, // Sets the number of items in the list to the number of messages. 225 itemBuilder: (context, index) { 226 final message = globalMessages[index]; // Retrieves the message at the given index. 227 if (message is types.TextMessage) { 228 return GestureDetector( 229 onLongPress: () { 230 Clipboard.setData(ClipboardData(text: message.text)); // Copies the message text to the clipboard on long press. 231 ScaffoldMessenger.of(context).showSnackBar( 232 const SnackBar(content: Text('Text copied to clipboard')), // Notifies the user that the text has been copied. 233 ); 234 }, 235 child: _buildTextMessageBubble(message), // Builds a bubble for the text message. 236 ); 237 } else if (message is types.ImageMessage) { 238 return GestureDetector( 239 onLongPress: () { 240 Clipboard.setData(ClipboardData(text: message.uri)); // Copies the image URL to the clipboard on long press. 241 ScaffoldMessenger.of(context).showSnackBar( 242 const SnackBar(content: Text('Image URL copied to clipboard')), // Notifies the user that the URL has been copied. 243 ); 244 }, 245 child: Image.network(message.uri), // Displays the image from the URL. 246 ); 247 } else { 248 return const SizedBox.shrink(); // Returns an empty widget for unsupported message types. 249 } 250 }, 251 ), 252 ), 253 Padding( 254 padding: const EdgeInsets.all(8.0), // Adds padding around the text input field. 255 child: Row( 256 children: [ 257 Expanded( 258 child: TextField( 259 controller: _textController, // Binds the text controller to the text field. 260 decoration: InputDecoration( 261 hintText: "Type a message", // Displays a hint in the text field. 262 border: OutlineInputBorder( 263 borderRadius: BorderRadius.circular(20), // Sets a rounded border for the text field. 264 ), 265 ), 266 ), 267 ), 268 IconButton( 269 icon: const Icon(Icons.send), // Displays a send icon. 270 onPressed: _isSendButtonVisible 271 ? () => _sendMessage(types.PartialText(text: _textController.text)) // Sends the message when the icon is pressed. 272 : null, // Disables the button when there's no text to send. 273 ), 274 ], 275 ), 276 ), 277 ], 278 ), 279 bottomNavigationBar: BottomNavigationBar( 280 backgroundColor: isDarkMode.value ? Colors.black : Colors.white, // Sets the background color based on the theme mode. 281 selectedItemColor: Colors.red, // Sets the color of the selected item. 282 unselectedItemColor: Colors.grey, // Sets the color of unselected items. 283 items: const <BottomNavigationBarItem>[ 284 BottomNavigationBarItem( 285 icon: Icon(Icons.chat), // Displays a chat icon. 286 label: 'Chat', // Sets the label for the chat item. 287 ), 288 BottomNavigationBarItem( 289 icon: Icon(Icons.assessment), // Displays an assessment icon. 290 label: 'Status', // Sets the label for the status item. 291 ), 292 BottomNavigationBarItem( 293 icon: Icon(Icons.settings), // Displays a settings icon. 294 label: 'Settings', // Sets the label for the settings item. 295 ), 296 ], 297 currentIndex: _selectedIndex, // Binds the selected index to the bottom navigation bar. 298 onTap: _onItemTapped, // Updates the selected index when an item is tapped. 299 ), 300 ); 301 } 302 303 // Resets the chat by clearing the message log and resetting the chat ID. 304 void _resetChat() { 305 setState(() { 306 globalMessages.clear(); // Clears the list of messages. 307 }); 308 GPTAPI.resetChatId(); // Resets the global chat ID. 309 globalChatId = null; // Clears the stored chat ID. 310 _textController.clear(); // Clears the text input field. 311 } 312 313 // Handles taps on the bottom navigation bar items, updating the selected index and navigating accordingly. 314 void _onItemTapped(int index) { 315 setState(() { 316 _selectedIndex = index; // Updates the selected index. 317 }); 318 if (index == 1) { 319 Navigator.pushReplacement( 320 context, MaterialPageRoute(builder: (context) => const StatusPage())); // Navigates to the Status page. 321 } else if (index == 2) { 322 Navigator.pushReplacement(context, 323 MaterialPageRoute(builder: (context) => const SettingsPage())); // Navigates to the Settings page. 324 } 325 } 326 327 // Builds a custom widget for displaying text messages with different background colors for user and AI messages. 328 Widget _buildTextMessageBubble(types.TextMessage message) { 329 bool isUserMessage = message.author.id == user.id; // Determines if the message is from the user. 330 Color bubbleColor = isUserMessage ? Colors.red : Colors.blue; // Sets the bubble color based on the message source. 331 TextAlign textAlign = isUserMessage ? TextAlign.right : TextAlign.left; // Aligns the text based on the message source. 332 333 return Container( 334 padding: const EdgeInsets.all(8), // Adds padding inside the bubble. 335 margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), // Adds margin around the bubble. 336 decoration: BoxDecoration( 337 color: bubbleColor, // Applies the determined background color. 338 borderRadius: BorderRadius.circular(12), // Rounds the corners of the bubble. 339 ), 340 child: Text( 341 message.text, // Displays the message text. 342 style: const TextStyle(color: Colors.white), // Sets the text color to white for contrast. 343 textAlign: textAlign, // Applies the determined text alignment. 344 ), 345 ); 346 } 347 }