diff --git a/mobile/app/lib/components/conversation_thread/conversation.dart b/mobile/app/lib/components/conversation_thread/conversation.dart index 3c883d3..7d79cde 100644 --- a/mobile/app/lib/components/conversation_thread/conversation.dart +++ b/mobile/app/lib/components/conversation_thread/conversation.dart @@ -1,4 +1,4 @@ -import 'package:app/util/server.dart'; +import 'package:app/util/backend.dart'; import 'package:flutter/material.dart'; import 'chat.dart'; @@ -6,19 +6,30 @@ class Conversation with ChangeNotifier { int conversationID; String conversationLabel; String conversationSubLabel; + bool history = true; + // History is disabled by default. This means chats are auto-deleted once the user exits that conversation. + bool conversationContext = true; // The prior prompts and responses are used as additional context for the next prompt and response. + late bool stream = false; // Update the UI with a stream of characters as received from the server, or as one text. List chats = []; Conversation(this.conversationID, this.conversationLabel, this.conversationSubLabel); Conversation.completeConversation(this.conversationID, this.conversationLabel, this.conversationSubLabel, this.chats); + void toggleHistory() => history = !history; + + void toggleConversationContext() => conversationContext = !conversationContext; + + void toggleStream() => stream = !stream; + void add(Chat message) { chats.add(message); notifyListeners(); } void loadChats() { - chats = Backend.getChatsByConversationID(conversationID); + Backend.loadConversation; + chats = Backend.loadedChats; } void setLabel() { diff --git a/mobile/app/lib/components/conversation_thread/conversation_thread_model.dart b/mobile/app/lib/components/conversation_thread/conversation_thread_model.dart index f7fc1d2..c95fa3a 100644 --- a/mobile/app/lib/components/conversation_thread/conversation_thread_model.dart +++ b/mobile/app/lib/components/conversation_thread/conversation_thread_model.dart @@ -1,7 +1,7 @@ import 'package:app/components/conversation_thread/chat_bubble_model.dart'; import 'package:app/components/conversation_thread/chat.dart'; import 'package:app/components/conversation_thread/conversation.dart'; -import 'package:app/util/server.dart'; +import 'package:app/util/backend.dart'; import 'package:app/components/prompt_box/prompt_box_widget.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; diff --git a/mobile/app/lib/components/conversation_thread/conversation_thread_widget.dart b/mobile/app/lib/components/conversation_thread/conversation_thread_widget.dart index 4492a07..a5cf391 100644 --- a/mobile/app/lib/components/conversation_thread/conversation_thread_widget.dart +++ b/mobile/app/lib/components/conversation_thread/conversation_thread_widget.dart @@ -1,5 +1,5 @@ import 'package:app/components/conversation_thread/conversation.dart'; -import 'package:app/util/server.dart'; +import 'package:app/util/backend.dart'; import 'package:provider/provider.dart'; import '/components/conversation_thread/chat_bubble_widget.dart'; diff --git a/mobile/app/lib/components/popup/popup_widget.dart b/mobile/app/lib/components/popup/popup_widget.dart index fdf314b..9ecb668 100644 --- a/mobile/app/lib/components/popup/popup_widget.dart +++ b/mobile/app/lib/components/popup/popup_widget.dart @@ -84,7 +84,7 @@ class _PopupWidgetState extends State { child: Padding( padding: EdgeInsetsDirectional.fromSTEB(0, 0, 12, 0), child: Text( - 'Refine the components modal.', + 'A popup', style: AppTheme.headlineMedium, ), ), @@ -106,12 +106,12 @@ class _PopupWidgetState extends State { ], ), const Divider( - height: 24, + height: 12, thickness: 2, color: Color(0xFFF1F4F8), ), const Text( - 'FlutterFlow is a visual development platform that allows you to easily create beautiful and responsive user interfaces for your mobile and web applications. With its drag-and-drop interface and pre-built components, you can quickly prototype and build your app without writing any code. \nAdditionally, FlutterFlow\'s real-time preview feature allows you to see your changes in real-time and make adjustments on the fly.', + 'Test', style: AppTheme.labelMedium, ), Padding( @@ -147,7 +147,7 @@ class _PopupWidgetState extends State { onPressed: () { print('Button pressed ...'); }, - text: 'Create Task', + text: 'Action', options: FFButtonOptions( width: 130, height: 40, diff --git a/mobile/app/lib/components/prompt_box/prompt_box_widget.dart b/mobile/app/lib/components/prompt_box/prompt_box_widget.dart index ad188df..0a13904 100644 --- a/mobile/app/lib/components/prompt_box/prompt_box_widget.dart +++ b/mobile/app/lib/components/prompt_box/prompt_box_widget.dart @@ -1,4 +1,4 @@ -import 'package:app/util/server.dart'; +import 'package:app/util/backend.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:flutter/material.dart'; @@ -209,7 +209,8 @@ class _PromptBoxWidgetState extends State { validator: _model.textControllerValidator.asValidator(context), textInputAction: TextInputAction.send, // "Send" on keyboard onFieldSubmitted: (text) { - Backend.respondWhenReady(text); // TODO: Connect with multimedia + entry_text = _model.textController.text; + Backend.respondWhenReady(entry_text); // TODO: Connect with multimedia _model.textController?.clear(); // Clear the text field }, ), diff --git a/mobile/app/lib/pages/conversation_widget.dart b/mobile/app/lib/pages/conversation_widget.dart index accc610..26d2e4d 100644 --- a/mobile/app/lib/pages/conversation_widget.dart +++ b/mobile/app/lib/pages/conversation_widget.dart @@ -5,6 +5,7 @@ import '/components/conversation_thread/conversation_thread_widget.dart'; import '/components/model_item/model_item_widget.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:app/theme.dart'; @@ -64,17 +65,9 @@ class _ConversationWidgetState extends State { ), onPressed: () async { print("something pressed in conversation_widget.dart"); - // TODO - // context.goNamed( - // 'ConversationsList', - // extra: { - // kTransitionInfoKey: TransitionInfo( - // hasTransition: true, - // transitionType: PageTransitionType.leftToRight, - // duration: const Duration(milliseconds: 250), - // ), - // }, - // ); + // TODO: Smooth out the animation to prevent the conversations list from loading half-way up the screen. + SystemChannels.textInput.invokeMethod('TextInput.hide'); + Navigator.pop(context); }, ), title: wrapWithModel( diff --git a/mobile/app/lib/pages/conversations_list_widget.dart b/mobile/app/lib/pages/conversations_list_widget.dart index 0eeda67..0a38f42 100644 --- a/mobile/app/lib/pages/conversations_list_widget.dart +++ b/mobile/app/lib/pages/conversations_list_widget.dart @@ -2,11 +2,12 @@ import 'package:app/components/conversation_thread/conversation.dart'; import 'package:app/theme.dart'; import 'package:app/pages/conversation_widget.dart'; -import 'package:app/util/server.dart'; +import 'package:app/util/backend.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:flutter/material.dart'; +import 'package:app/components/options_dialog/options_dialog_widget.dart'; import 'conversations_list_model.dart'; export 'conversations_list_model.dart'; @@ -48,6 +49,20 @@ class _ConversationsListWidgetState extends State { @override Widget build(BuildContext context) { + // Clean conversation list prior to building widgets, to prevent bloating UI with empty conversations. + // Additionally, delete conversations that have history disabled. + for(int i = 0; i < conversations.length; i++) { + if( + conversations[i].conversation.getChats().isEmpty || + !conversations[i].conversation.history + ) { + Backend.conversations.removeAt(i); + conversations.removeAt(i); + Backend.maxConversationID--; // maxConversationID will eventually be deprecated with better ID system. + i--; + } + } + return GestureDetector( onTap: () => _model.unfocusNode.canRequestFocus ? FocusScope.of(context).requestFocus(_model.unfocusNode) @@ -181,6 +196,24 @@ class _ConversationsListWidgetState extends State { ) }, onLongPress: () => { + showModalBottomSheet( + isScrollControlled: true, + backgroundColor: + Colors.transparent, + enableDrag: false, + useSafeArea: true, + context: context, + builder: (context) { + return Padding( + padding: + MediaQuery.viewInsetsOf( + context), + child: const PopupWidget(), + ); + }, + ).then((value) => + safeSetState(() {})), + // TODO: Need to implement. print("opening options") }, @@ -197,7 +230,7 @@ class _ConversationsListWidgetState extends State { right: 20.0, // Adjust positioning as needed child: FloatingActionButton( onPressed: () => { - Backend.loadConversation(Backend.maxConversationID), // TODO: Dynamically get the next ID + Backend.loadConversation(Backend.createConversation()), // TODO: Dynamically get the next ID conversations.add(ConversationWidget(conversation: Backend.getLoadedConversation())), Navigator.push( context, diff --git a/mobile/app/lib/pages/settings_widget.dart b/mobile/app/lib/pages/settings_widget.dart index 5c62546..8876a81 100644 --- a/mobile/app/lib/pages/settings_widget.dart +++ b/mobile/app/lib/pages/settings_widget.dart @@ -1169,6 +1169,7 @@ class _SettingsWidgetState extends State focusColor: Colors.transparent, hoverColor: Colors.transparent, highlightColor: Colors.transparent, + // TODO: Should open in a browser rather than in-app? onTap: () async { await showModalBottomSheet( isScrollControlled: true, diff --git a/mobile/app/lib/util/server.dart b/mobile/app/lib/util/backend.dart similarity index 71% rename from mobile/app/lib/util/server.dart rename to mobile/app/lib/util/backend.dart index 942336b..ccd2f7b 100644 --- a/mobile/app/lib/util/server.dart +++ b/mobile/app/lib/util/backend.dart @@ -11,10 +11,10 @@ import 'model.dart'; // Uses Singleton design pattern to ensure that any server references across the app are using the same instance. class Backend { // SERVER VARIABLES - static final Backend _single_backend = Backend._internal(); + static final Backend singleBackend = Backend._internal(); Backend._internal(); // TODO: One-time process to establish server connection is required for easy setup/maintenance. - static final url = "http://10.0.2.2:11434/api/"; // For emulated device in testing + static const url = "http://10.0.2.2:11434/api/"; // For emulated device in testing //static final url = "http://192.168.1.68:11434/api"; // For local access to home server static String endpoint = ""; static final Map headers = {'Content-Type': 'application/json'}; @@ -27,16 +27,16 @@ class Backend { - // PRIVACY VARIABLES - static bool _historyEnabled = true; - static late int conversationID; - static late int maxConversationID; + // PRIVACY-FOCUSED VARIABLES + // TODO: Reorder based on most recent usage. static List conversations = []; static List loadedChats = []; - + static late Conversation loadedConversation; // RUNTIME & STATUS VARIABLES + static late int conversationID; + static late int maxConversationID; static late Model model; static String prompt = ""; static String response = ""; @@ -50,7 +50,7 @@ class Backend { // loadedChats = conversations[conversationID].chats; - return _single_backend; + return singleBackend; } void init() { @@ -61,31 +61,19 @@ class Backend { model = Model("llama3"); // Primarily used llama3, testing gemma2 // TODO: Dynamically obtain conversations. - conversations.add( - Conversation.completeConversation( - 0, - "Test Conversation", - "Testing Functionality", - [ - (Chat("Hello there! You are part of a prototype and are running locally on my Macbook Air. Please keep your responses brief as every prompt freezes my computer.", true)), - (Chat("Understood! How can I help?", false)), - ] - ) - ); + // conversations.add( + // Conversation.completeConversation( + // 0, + // "Test Conversation", + // "Testing Functionality", + // [ + // (Chat("Hello there! You are part of a prototype and are running locally on my Macbook Air. Please keep your responses brief as every prompt freezes my computer.", true)), + // (Chat("Understood! How can I help?", false)), + // ] + // ) + // ); maxConversationID = conversations.length; - - // conversations.add( - // Conversation.completeConversation( - // 1, - // "Label test", - // "Sub-label Test", - // [ - // (Chat("A different test. Only reply No.", true)), - // (Chat("No.", false)), - // ] - // ) - // ); } @@ -115,21 +103,32 @@ class Backend { // CONVERSATION MANAGEMENT + @Deprecated("") static List getConversationsList() { return conversations; } - static Conversation getLoadedConversation() { return conversations[conversationID]; } + static Conversation getLoadedConversation() { return loadedConversation; } // Load conversation should be called prior to any other server requests. static void loadConversation(id) { conversationID = id; - if(maxConversationID == id) { - conversations.add(Conversation(conversationID, "Conversation $id", "Another AI conversation")); - maxConversationID++; - } - loadedChats = conversations[conversationID].chats; + loadedConversation = conversations[id]; + loadedChats = loadedConversation.chats; } + // TODO: Better conversation ID management is needed. Preferably randomized IDs to not indicate the number of conversations. + static int createConversation() { + int id = conversations.length; + conversations.add(Conversation(id, "Conversation $id", "Another AI conversation")); + return id; + } + + static void deleteConversation(id) { + loadedChats = conversations[conversationID].chats; // TODO: Temporarily saving the chats for "undo" popup. + conversations.removeAt(id); + } + + @Deprecated('Just directly get loadedChats list. Unless multiple chats can be opened simultaneously?') static List getChatsByConversationID(id) { return conversations[id].chats; } @@ -155,7 +154,7 @@ class Backend { // TODO: Allow for different models to be dynamically selected. Map data; - if(_historyEnabled) { + if(conversations[conversationID].conversationContext) { loadedChats = [...loadedChats, Chat(prompt, true)]; endpoint = "chat"; @@ -164,7 +163,7 @@ class Backend { data = { "model": model.name, "messages": messageList, - "stream": false, + "stream": loadedConversation.stream, }; } else { @@ -172,7 +171,7 @@ class Backend { data = { "model": model.name, "prompt": prompt, - "stream": false, + "stream": loadedConversation.stream, }; } @@ -182,6 +181,8 @@ class Backend { // Send the HTTP request to the server. static void httpSendRequest() async { + // TODO: Add AES256 encryption to the prompt/messages thread. + // TODO: Add SHA256 checksum to be sent and verified to prevent/notify in case of data loss. String json = constructPrompt(); // Send the HTTP POST request @@ -193,7 +194,7 @@ class Backend { if (serverResponse.statusCode == 200) { Map re = jsonDecode(serverResponse.body); - if(_historyEnabled) { + if(conversations[conversationID].conversationContext) { response = re["message"]["content"]; loadedChats = [...loadedChats, Chat(response, false)]; } else {