/ lib / main.dart
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  }