diff --git a/mobile/app/lib/components/conversation_thread/chat_bubble_widget.dart b/mobile/app/lib/components/conversation_thread/chat_bubble_widget.dart index fee84cf..2233eb2 100644 --- a/mobile/app/lib/components/conversation_thread/chat_bubble_widget.dart +++ b/mobile/app/lib/components/conversation_thread/chat_bubble_widget.dart @@ -1,3 +1,4 @@ +import 'package:app/theme.dart'; import 'package:flutter/material.dart'; import 'chat.dart'; @@ -26,9 +27,12 @@ class ChatBubbleWidget extends StatelessWidget{ bottomRight: c.role ? const Radius.circular(12.0) : const Radius .circular(0.0), ), - color: c.role ? Colors.blue[200] : Colors.grey[200], + color: c.role ? AppTheme.darkest : AppTheme.dark, + ), + child: Text( + c.content, + style: AppTheme.bodyMedium, ), - child: Text(c.content), ), ], ); diff --git a/mobile/app/lib/components/conversation_thread/conversation.dart b/mobile/app/lib/components/conversation_thread/conversation.dart index ef0667c..3c883d3 100644 --- a/mobile/app/lib/components/conversation_thread/conversation.dart +++ b/mobile/app/lib/components/conversation_thread/conversation.dart @@ -18,7 +18,7 @@ class Conversation with ChangeNotifier { } void loadChats() { - chats = Server.getChatsByConversationID(conversationID); + chats = Backend.getChatsByConversationID(conversationID); } 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 6230189..f7fc1d2 100644 --- a/mobile/app/lib/components/conversation_thread/conversation_thread_model.dart +++ b/mobile/app/lib/components/conversation_thread/conversation_thread_model.dart @@ -20,7 +20,7 @@ class ConversationThreadModel extends FlutterFlowModel @override void initState(BuildContext context) { - conversation = Server.getLoadedConversation(); + conversation = Backend.getLoadedConversation(); promptBoxModel = createModel(context, () => PromptBoxModel()); } 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 8a42f02..4492a07 100644 --- a/mobile/app/lib/components/conversation_thread/conversation_thread_widget.dart +++ b/mobile/app/lib/components/conversation_thread/conversation_thread_widget.dart @@ -53,9 +53,7 @@ class _ConversationThreadWidgetState extends State { return Container( width: double.infinity, height: double.infinity, - decoration: const BoxDecoration( - color: AppTheme.primaryBackground, - ), + child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -65,12 +63,12 @@ class _ConversationThreadWidgetState extends State { listenable: _model.conversation, builder: (BuildContext context, Widget? child) { return ListView.builder( - itemCount: Server.getLoadedConversation().chats.length, + itemCount: Backend.getLoadedConversation().chats.length, padding: const EdgeInsets.fromLTRB(0, 12, 0, 24,), reverse: false, scrollDirection: Axis.vertical, itemBuilder: (BuildContext context, int index) { - return ChatBubbleWidget(c: Server.getLoadedConversation().chats[index]); + return ChatBubbleWidget(c: Backend.getLoadedConversation().chats[index]); }, ); }, @@ -79,7 +77,7 @@ class _ConversationThreadWidgetState extends State { Container( width: double.infinity, decoration: const BoxDecoration( - color: AppTheme.secondaryBackground, + color: AppTheme.medium, boxShadow: [ BoxShadow( blurRadius: 3, @@ -95,74 +93,74 @@ class _ConversationThreadWidgetState extends State { child: Column( mainAxisSize: MainAxisSize.max, children: [ - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsetsDirectional.fromSTEB(0, 12, 0, 0), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // FlutterFlowMediaDisplay( - // path: '', - // imageBuilder: (path) => ClipRRect( - // borderRadius: BorderRadius.circular(8), - // child: CachedNetworkImage( - // fadeInDuration: const Duration(milliseconds: 500), - // fadeOutDuration: - // const Duration(milliseconds: 500), - // imageUrl: path, - // width: 120, - // height: 100, - // fit: BoxFit.cover, - // ), - // ), - // TODO - // videoPlayerBuilder: (path) => - // FlutterFlowVideoPlayer( - // path: path, - // width: 300, - // autoPlay: false, - // looping: true, - // showControls: true, - // allowFullScreen: true, - // allowPlaybackSpeedMenu: false, - // ), - // ), - Align( - alignment: const AlignmentDirectional(-1, -1), - // TODO: Make the delete button conditional upon media upload. - child: FlutterFlowIconButton( - borderColor: - AppTheme.error, - borderRadius: 20, - borderWidth: 2, - buttonSize: 40, - fillColor: AppTheme.primaryBackground, - icon: const Icon( - Icons.delete_outline_rounded, - color: AppTheme.error, - size: 24, - ), - onPressed: () { - print('IconButton pressed ...'); - }, - ), - ), - ] - .divide(const SizedBox(width: 8)) - .addToStart(const SizedBox(width: 16)) - .addToEnd(const SizedBox(width: 16)), - ), - ), - ), - ), - ], - ), + // Row( + // mainAxisSize: MainAxisSize.max, + // children: [ + // Expanded( + // child: Padding( + // padding: const EdgeInsetsDirectional.fromSTEB(0, 12, 0, 0), + // child: SingleChildScrollView( + // scrollDirection: Axis.horizontal, + // child: Row( + // mainAxisSize: MainAxisSize.max, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // FlutterFlowMediaDisplay( + // path: '', + // imageBuilder: (path) => ClipRRect( + // borderRadius: BorderRadius.circular(8), + // child: CachedNetworkImage( + // fadeInDuration: const Duration(milliseconds: 500), + // fadeOutDuration: + // const Duration(milliseconds: 500), + // imageUrl: path, + // width: 120, + // height: 100, + // fit: BoxFit.cover, + // ), + // ), + // // TODO + // videoPlayerBuilder: (path) => + // FlutterFlowVideoPlayer( + // path: path, + // width: 300, + // autoPlay: false, + // looping: true, + // showControls: true, + // allowFullScreen: true, + // allowPlaybackSpeedMenu: false, + // ), + // ), + // Align( + // alignment: const AlignmentDirectional(-1, -1), + // // TODO: Make the delete button conditional upon media upload. + // child: FlutterFlowIconButton( + // borderColor: + // AppTheme.error, + // borderRadius: 20, + // borderWidth: 2, + // buttonSize: 40, + // fillColor: AppTheme.primaryBackground, + // icon: const Icon( + // Icons.delete_outline_rounded, + // color: AppTheme.error, + // size: 24, + // ), + // onPressed: () { + // print('IconButton pressed ...'); + // }, + // ), + // ), + // ] + // .divide(const SizedBox(width: 8)) + // .addToStart(const SizedBox(width: 16)) + // .addToEnd(const SizedBox(width: 16)), + // ), + // ), + // ), + // ), + // ], + // ), Form( key: _model.formKey, autovalidateMode: AutovalidateMode.disabled, diff --git a/mobile/app/lib/components/model_item/model_item_widget.dart b/mobile/app/lib/components/model_item/model_item_widget.dart index 56656f7..71acbd1 100644 --- a/mobile/app/lib/components/model_item/model_item_widget.dart +++ b/mobile/app/lib/components/model_item/model_item_widget.dart @@ -1,3 +1,4 @@ +import 'package:app/components/conversation_thread/conversation.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -9,14 +10,18 @@ import 'model_item_model.dart'; export 'model_item_model.dart'; class ModelItemWidget extends StatefulWidget { - const ModelItemWidget({super.key}); + final Conversation conversation; + const ModelItemWidget({super.key, required this.conversation}); @override - State createState() => _ModelItemWidgetState(); + State createState() => _ModelItemWidgetState(conversation); } class _ModelItemWidgetState extends State { late ModelItemModel _model; + late Conversation conversation; + + _ModelItemWidgetState(this.conversation); @override void setState(VoidCallback callback) { @@ -78,22 +83,22 @@ class _ModelItemWidgetState extends State { ), ), ), - const Expanded( + Expanded( child: Padding( - padding: EdgeInsetsDirectional.fromSTEB(12, 0, 8, 0), + padding: const EdgeInsetsDirectional.fromSTEB(12, 0, 8, 0), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Randy Peterson', + Text( // Label + conversation.conversationLabel, style: AppTheme.bodyMedium, ), Padding( - padding: EdgeInsetsDirectional.fromSTEB(0, 4, 0, 0), - child: Text( - 'name@domainname.com', + padding: const EdgeInsetsDirectional.fromSTEB(0, 4, 0, 0), + child: Text( // Sub-label + conversation.conversationSubLabel, style: AppTheme.bodySmall, ), ), 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 f80e70e..c7a9082 100644 --- a/mobile/app/lib/components/prompt_box/prompt_box_widget.dart +++ b/mobile/app/lib/components/prompt_box/prompt_box_widget.dart @@ -17,6 +17,7 @@ class PromptBoxWidget extends StatefulWidget { class _PromptBoxWidgetState extends State { late PromptBoxModel _model; + late String entry_text; @override void setState(VoidCallback callback) { @@ -69,7 +70,7 @@ class _PromptBoxWidgetState extends State { buttonSize: 40, icon: const Icon( Icons.file_upload_outlined, - color: AppTheme.secondaryText, + color: AppTheme.darkest, size: 30, ), onPressed: () { @@ -88,7 +89,7 @@ class _PromptBoxWidgetState extends State { buttonSize: 40, icon: const Icon( Icons.photo_outlined, - color: AppTheme.secondaryText, + color: AppTheme.darkest, size: 30, ), onPressed: () { @@ -105,13 +106,28 @@ class _PromptBoxWidgetState extends State { borderRadius: 30, borderWidth: 1, buttonSize: 40, - icon: const Icon( - Icons.mic_none, - color: AppTheme.secondaryText, + icon: // Backend.stt.speechToText.isListening ? + // const Icon( + // Icons.mic_none, + // color: AppTheme.error, + // size: 30, + // ) : + const Icon( + Icons.mic_off_outlined, + color: AppTheme.darkest, size: 30, ), onPressed: () { - print('IconButton pressed ...'); + if(Backend.stt.isSpeechEnabled()) { + if(Backend.stt.speechToText.isListening) { + print('De-activating STT.'); + Backend.stt.stopListening(); + } + print('Activating STT.'); + entry_text = Backend.stt.startListening() as String; + } else { + print('Microphone is unavailable for STT'); + } }, ), ), @@ -126,7 +142,7 @@ class _PromptBoxWidgetState extends State { buttonSize: 40, icon: const Icon( Icons.videocam_outlined, - color: AppTheme.secondaryText, + color: AppTheme.darkest, size: 30, ), onPressed: () { @@ -145,7 +161,7 @@ class _PromptBoxWidgetState extends State { buttonSize: 40, icon: const Icon( Icons.camera_alt_outlined, - color: AppTheme.secondaryText, + color: AppTheme.darkest, size: 30, ), onPressed: () { @@ -206,21 +222,28 @@ class _PromptBoxWidgetState extends State { ), filled: true, fillColor: AppTheme.alternate, - suffixIcon: const Icon( - Icons.send, - color: AppTheme.primary, - size: 30, + suffixIcon: GestureDetector( + onTap: () { + // TODO: Connect with multimedia + entry_text = _model.textController.text; + Backend.respondWhenReady(entry_text); + _model.textController?.clear(); // Clear the text field + }, + child: const Icon( + Icons.send, + color: AppTheme.primary, + size: 30, + ), ), ), - style: AppTheme.bodyMedium, + style: AppTheme.displayMedium, textAlign: TextAlign.start, maxLines: 5, minLines: 1, validator: _model.textControllerValidator.asValidator(context), textInputAction: TextInputAction.send, // "Send" on keyboard onFieldSubmitted: (text) { - // TODO: Clear text box - Server.respondWhenReady(text); // TODO: Connect with multimedia + Backend.respondWhenReady(text); // TODO: Connect with multimedia _model.textController?.clear(); // Clear the text field }, ), diff --git a/mobile/app/lib/main.dart b/mobile/app/lib/main.dart index f354c5c..697b98a 100644 --- a/mobile/app/lib/main.dart +++ b/mobile/app/lib/main.dart @@ -28,13 +28,14 @@ BottomNavigationBarItem _buildNavigationItem(int index) { iconData = Icons.settings; break; case 1: - label = 'Home'; - iconData = Icons.home; + label = 'Chats'; + iconData = Icons.chat; break; } return BottomNavigationBarItem( icon: Icon(iconData), label: label, + backgroundColor: AppTheme.lightest, ); } @@ -63,15 +64,15 @@ class MyApp extends ConsumerWidget { final selectedIndex = ref.watch(selectedIndexProvider); return MaterialApp( - title: 'App', + title: 'One AI', home: Scaffold( - backgroundColor: AppTheme.secondaryBackground, + backgroundColor: AppTheme.medium, appBar: AppBar( backgroundColor: AppTheme.secondaryBackground, automaticallyImplyLeading: false, title: const Text( - 'App', - style: AppTheme.headlineLarge, + 'One AI', + style: AppTheme.displayLarge, ), actions: const [], centerTitle: false, @@ -79,6 +80,8 @@ class MyApp extends ConsumerWidget { ), body: _pages[selectedIndex], bottomNavigationBar: BottomNavigationBar( + backgroundColor: AppTheme.darkest, + selectedItemColor: AppTheme.lightest, // Set the number of items to the length of the pages list items: List.generate(_pages.length, (index) => _buildNavigationItem(index)), currentIndex: selectedIndex, diff --git a/mobile/app/lib/pages/conversation_widget.dart b/mobile/app/lib/pages/conversation_widget.dart index d1ebce3..accc610 100644 --- a/mobile/app/lib/pages/conversation_widget.dart +++ b/mobile/app/lib/pages/conversation_widget.dart @@ -59,7 +59,7 @@ class _ConversationWidgetState extends State { buttonSize: 60, icon: const Icon( Icons.arrow_back_rounded, - color: AppTheme.primaryTextColor, + color: AppTheme.lightest, size: 30, ), onPressed: () async { @@ -80,21 +80,16 @@ class _ConversationWidgetState extends State { title: wrapWithModel( model: _model.modelItemModel, updateCallback: () => setState(() {}), - child: const ModelItemWidget(), + child: ModelItemWidget(conversation: conversation,), ), actions: [ Padding( padding: const EdgeInsetsDirectional.fromSTEB(0, 8, 16, 8), // ---------- Options Button child: FlutterFlowIconButton( - borderColor: AppTheme.alternate, - borderRadius: 12, - borderWidth: 2, - buttonSize: 40, - fillColor: AppTheme.primaryBackground, icon: const Icon( Icons.more_vert, - color: AppTheme.primaryTextColor, + color: AppTheme.lightest, size: 24, ), onPressed: () async { diff --git a/mobile/app/lib/pages/conversations_list_widget.dart b/mobile/app/lib/pages/conversations_list_widget.dart index aacf29e..0eeda67 100644 --- a/mobile/app/lib/pages/conversations_list_widget.dart +++ b/mobile/app/lib/pages/conversations_list_widget.dart @@ -6,7 +6,6 @@ import 'package:app/util/server.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'conversations_list_model.dart'; export 'conversations_list_model.dart'; @@ -29,10 +28,10 @@ class _ConversationsListWidgetState extends State { @override void initState() { super.initState(); - Server().init(); + Backend().init(); if(conversations.isEmpty) { - List convoys = Server.conversations; + List convoys = Backend.conversations; for(Conversation c in convoys) { conversations.add(ConversationWidget(conversation: c,)); } @@ -55,138 +54,162 @@ class _ConversationsListWidgetState extends State { : FocusScope.of(context).unfocus(), child: SafeArea( top: true, - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, + child: Stack( children: [ - const Padding( - padding: EdgeInsetsDirectional.fromSTEB(16, 0, 0, 0), - child: Text( - 'Conversations', - style: AppTheme.labelMedium, - ), - ), - Expanded( - child: Align( - alignment: const AlignmentDirectional(0, 0), - child: ListView.builder( - itemCount: conversations.length, - padding: EdgeInsets.zero, - reverse: true, - scrollDirection: Axis.vertical, - itemBuilder: (context, index) { - return Align( - alignment: const AlignmentDirectional(0, 0), - child: InkWell( - splashColor: Colors.transparent, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - highlightColor: Colors.transparent, - // TODO - // onTap: () async { - // context.pushNamed('Conversation'); - // }, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: 100, - height: 100, - decoration: const BoxDecoration(color: AppTheme.secondaryBackground,), - child: const Align( - alignment: AlignmentDirectional(0, 0), - child: Icon( - Icons.settings_outlined, // TODO: Dynamically generate or allow users to color code/tag conversations. - color: AppTheme.secondaryText, - size: 48, + Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Align( + alignment: const AlignmentDirectional(0, 0), + child: ListView.builder( + itemCount: conversations.length, + padding: EdgeInsets.zero, + reverse: true, + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + return Align( + alignment: const AlignmentDirectional(0, 0), + child: InkWell( + splashColor: AppTheme.accent, + focusColor: AppTheme.lightest, + hoverColor: AppTheme.dark, + highlightColor: AppTheme.darkest, + // TODO + // onTap: () async { + // context.pushNamed('Conversation'); + // }, + child: Container( + decoration: const BoxDecoration( + //color: AppTheme.darkest, + border: Border( + top: BorderSide(width: 2.0, color: AppTheme.darkest), + ), ), - ), - ), - Expanded( - child: Align( - alignment: const AlignmentDirectional(-1, 0), - child: Container( - width: 100, - height: 100, - constraints: BoxConstraints( - minWidth: MediaQuery.sizeOf(context).width, - ), - decoration: const BoxDecoration( - color: AppTheme.secondaryBackground, - shape: BoxShape.rectangle, - ), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Align( + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + + children: [ + Container( + width: 100, + height: 100, + decoration: const BoxDecoration(color: AppTheme.lightest,), + child: const Align( + alignment: AlignmentDirectional(0, 0), + child: Icon( + Icons.settings_outlined, // TODO: Dynamically generate or allow users to color code/tag conversations. + color: AppTheme.secondaryText, + size: 48, + ), + ), + ), + Expanded( + child: Align( alignment: const AlignmentDirectional(-1, 0), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Text( - conversations[index].conversation.conversationLabel, - style: AppTheme.headlineLarge, - ), - ], - ), - ), - Align( - alignment: - const AlignmentDirectional(-1, 0), - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Text( - conversations[index].conversation.conversationSubLabel, - style: AppTheme.bodyMedium, + child: Container( + width: 100, + height: 100, + constraints: BoxConstraints( + minWidth: MediaQuery.sizeOf(context).width, + ), + decoration: const BoxDecoration( + color: AppTheme.lightest, + shape: BoxShape.rectangle, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Align( + alignment: const AlignmentDirectional(-1, 0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + conversations[index].conversation.conversationLabel, + style: AppTheme.labelLarge, + ), + ], + ), ), - ), - ], + Align( + alignment: + const AlignmentDirectional(-1, 0), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Text( + conversations[index].conversation.conversationSubLabel, + style: AppTheme.labelSmall, + ), + ), + ], + ), + ), + ], + ), ), ), - ], - ), + ), + Container( + width: 100, + height: 100, + decoration: const BoxDecoration( + color: AppTheme.lightest, + ), + child: const Icon( + Icons.chevron_right_rounded, + color: AppTheme.secondaryText, + size: 48, + ), + ), + ], ), ), + onTap: () => { + Backend.loadConversation(index), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConversationWidget(conversation: Backend.getLoadedConversation(),), + ), + ) + }, + onLongPress: () => { + // TODO: Need to implement. + print("opening options") + }, ), - Container( - width: 100, - height: 100, - decoration: const BoxDecoration( - color: AppTheme.secondaryBackground, - ), - child: const Icon( - Icons.chevron_right_rounded, - color: AppTheme.secondaryText, - size: 48, - ), - ), - ], - ), - onTap: () => { - // TODO: Need to implement. - // Get the conversation object based on the index - // Use Navigator to push a new ConversationWidget with conversation data - Server.loadConversation(index), - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ConversationWidget(conversation: Server.getLoadedConversation(),), - ), - ) - }, - onLongPress: () => { - // TODO: Need to implement. - print("opening options") - }, - ), - ); - } + ); + } + ), + ), + ), + ], + ), + Positioned( + bottom: 20.0, // Adjust positioning as needed + right: 20.0, // Adjust positioning as needed + child: FloatingActionButton( + onPressed: () => { + Backend.loadConversation(Backend.maxConversationID), // TODO: Dynamically get the next ID + conversations.add(ConversationWidget(conversation: Backend.getLoadedConversation())), + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ConversationWidget(conversation: Backend.getLoadedConversation(),), + ), + ), // Handle the button press + }, + backgroundColor: AppTheme.darkest, + child: const Icon( + Icons.bubble_chart, + color: AppTheme.lightest, ), ), ), diff --git a/mobile/app/lib/theme.dart b/mobile/app/lib/theme.dart index 1d38ccd..3818e8f 100644 --- a/mobile/app/lib/theme.dart +++ b/mobile/app/lib/theme.dart @@ -1,37 +1,45 @@ import 'package:flutter/material.dart'; class AppTheme { - static const primary = Colors.deepPurple; - static const secondary = Colors.deepPurpleAccent; - static const tertiary = Colors.white; - static const alternate = Colors.deepPurple; + static const darkest = Color.fromARGB(255, 45, 45, 145); + static const dark = Color.fromARGB(255, 81, 81, 196); + static const medium = Color.fromARGB(255, 107, 107, 201); + static const light = Color.fromARGB(255, 155, 155, 255); + static const lightest = Color.fromARGB(255, 172, 175, 255); + static const accent = Color.fromARGB(255, 255, 113, 0); + static const accent2 = Color.fromARGB(255, 255, 164, 79); - static const accent1 = Colors.amber; - static const accent4 = Colors.amber; + static const primaryBackground = darkest; + static const secondaryBackground = medium; + static const primaryTextColor = darkest; + static const primaryText = TextStyle(color: primaryTextColor, fontSize: 16); + static const secondaryText = dark; - static const primaryBackground = Colors.deepPurpleAccent; - static const secondaryBackground = Colors.deepPurple; + static const accent1 = lightest; + static const accent4 = accent2; + static const primary = dark; + static const secondary = medium; + static const tertiary = light; + static const alternate = lightest; - static const primaryTextColor = Colors.white; - static const primaryText = TextStyle(color: primaryTextColor, fontSize: 12); - static const secondaryText = Colors.white; - - static const bodyLarge = TextStyle(color: Colors.white, fontSize: 24); - static const bodyMedium = TextStyle(color: Colors.white, fontSize: 12); - static const bodySmall = TextStyle(color: Colors.white, fontSize: 10); + static const bodyLarge = TextStyle(color: light, fontSize: 24, fontWeight: FontWeight.bold); + static const bodyMedium = TextStyle(color: lightest, fontSize: 16); + static const bodySmall = TextStyle(color: light, fontSize: 12); static const error = Colors.red; static const info = Colors.grey; - static const displaySmall = TextStyle(color: Colors.white, fontSize: 12); + static const displayLarge = TextStyle(color: darkest, fontSize: 24, fontWeight: FontWeight.bold); + static const displayMedium = TextStyle(color: dark, fontSize: 16, fontWeight: FontWeight.bold); + static const displaySmall = TextStyle(color: lightest, fontSize: 12, fontWeight: FontWeight.bold); - static const labelLarge = TextStyle(color: Colors.black, fontSize: 24); - static const labelMedium = TextStyle(color: Colors.grey, fontSize: 16); - static const labelSmall = TextStyle(color: Colors.grey, fontSize: 12); + static const labelLarge = TextStyle(color: darkest, fontSize: 24, fontWeight: FontWeight.bold); + static const labelMedium = TextStyle(color: dark, fontSize: 16, fontWeight: FontWeight.bold); + static const labelSmall = TextStyle(color: medium, fontSize: 12, fontWeight: FontWeight.bold); - static const headlineLarge = TextStyle(color: Colors.blue, fontWeight: FontWeight.bold); - static const headlineMedium = TextStyle(color: Colors.blue, fontWeight: FontWeight.bold); - static const headlineSmall = TextStyle(color: Colors.blue, fontWeight: FontWeight.bold); + static const headlineLarge = TextStyle(color: medium, fontWeight: FontWeight.bold); + static const headlineMedium = TextStyle(color: light, fontWeight: FontWeight.bold); + static const headlineSmall = TextStyle(color: lightest, fontWeight: FontWeight.bold); static const titleLarge = TextStyle(fontFamily: 'Outfit', letterSpacing: 0, fontSize: 24); static const titleSmall = TextStyle(fontFamily: 'Outfit', letterSpacing: 0, fontSize: 16); diff --git a/mobile/app/lib/util/model.dart b/mobile/app/lib/util/model.dart new file mode 100644 index 0000000..cad0ddf --- /dev/null +++ b/mobile/app/lib/util/model.dart @@ -0,0 +1,5 @@ +class Model { + late String name; + + Model(this.name); +} \ No newline at end of file diff --git a/mobile/app/lib/util/server.dart b/mobile/app/lib/util/server.dart index ff280d0..942336b 100644 --- a/mobile/app/lib/util/server.dart +++ b/mobile/app/lib/util/server.dart @@ -6,11 +6,13 @@ import 'package:app/components/conversation_thread/chat.dart'; import 'package:app/util/speech_to_text.dart'; +import 'model.dart'; + // Uses Singleton design pattern to ensure that any server references across the app are using the same instance. -class Server { +class Backend { // SERVER VARIABLES - static final Server _single_server = Server._internal(); - Server._internal(); + static final Backend _single_backend = 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 final url = "http://192.168.1.68:11434/api"; // For local access to home server @@ -28,30 +30,36 @@ class Server { // PRIVACY VARIABLES static bool _historyEnabled = true; static late int conversationID; + static late int maxConversationID; static List conversations = []; static List loadedChats = []; // RUNTIME & STATUS VARIABLES + static late Model model; static String prompt = ""; static String response = ""; static bool initiated = false; // TODO: Verify all different components of STT, TTS, LLM, etc have been initialized. // TODO: Handshake w/ server to validate identity - factory Server() { + factory Backend() { // Speech-To-Text engine activation. _speechEnabled = stt.isSpeechEnabled(); // loadedChats = conversations[conversationID].chats; - return _single_server; + return _single_backend; } void init() { initiated = true; + // TODO: Dynamically get available models and models that can be pulled. + // TODO: Dynamically get the user's default model + model = Model("llama3"); // Primarily used llama3, testing gemma2 + // TODO: Dynamically obtain conversations. conversations.add( Conversation.completeConversation( @@ -65,6 +73,8 @@ class Server { ) ); + maxConversationID = conversations.length; + // conversations.add( // Conversation.completeConversation( // 1, @@ -81,9 +91,12 @@ class Server { // PRIMARY INTERFACE + @Deprecated("Use STT interface directly and use respondWhenReady().") static Future sttGetResponseWhenReady() async { // TODO: In case on-device STT is unavailable, use server-side STT service. - if (!_speechEnabled) "Unable to process Speech-To-Text on-device."; + if (!_speechEnabled) { + return "Unable to process Speech-To-Text on-device."; + } prompt = stt.getTextWhenReady() as String; conversations[conversationID].add(Chat(prompt, true)); @@ -109,6 +122,11 @@ class Server { // 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; } @@ -144,7 +162,7 @@ class Server { List> messageList = convertMessages(); data = { - "model": "llama3", + "model": model.name, "messages": messageList, "stream": false, }; @@ -152,7 +170,7 @@ class Server { else { endpoint = "generate"; data = { - "model": "llama3", + "model": model.name, "prompt": prompt, "stream": false, }; diff --git a/mobile/app/lib/util/speech_to_text.dart b/mobile/app/lib/util/speech_to_text.dart index 27f9d81..60e3630 100644 --- a/mobile/app/lib/util/speech_to_text.dart +++ b/mobile/app/lib/util/speech_to_text.dart @@ -3,15 +3,15 @@ import 'package:speech_to_text/speech_to_text.dart' as stt; /// Handles standard Speech-To-Text using on-device processing. class SpeechToText { - final stt.SpeechToText _speechToText = stt.SpeechToText(); - bool _speechEnabled = false; + final stt.SpeechToText speechToText = stt.SpeechToText(); + bool speechEnabled = false; String text = ""; // Check if speech recognition is enabled. - bool isSpeechEnabled() => _speechEnabled; + bool isSpeechEnabled() => speechEnabled; Future getTextWhenReady() async { - await _speechToText.stop(); + await speechToText.stop(); return text; } @@ -22,22 +22,22 @@ class SpeechToText { void reset() => text = ""; /// Start a speech recognition session - void _startListening() async => await _speechToText.listen(onResult: _onSpeechResult); + void startListening() async => await speechToText.listen(onResult: onSpeechResult); /// Manually stop the active speech recognition session /// Note that there are also timeouts that each platform enforces /// and the SpeechToText plugin supports setting timeouts on the /// listen method. - void _stopListening() async => await _speechToText.stop(); + void stopListening() async => await speechToText.stop(); SpeechToText() { - _initSpeech(); + initSpeech(); } /// This has to happen only once per app - void _initSpeech() async => _speechEnabled = await _speechToText.initialize(); + void initSpeech() async => speechEnabled = await speechToText.initialize(); /// This is the callback that the SpeechToText plugin calls when /// the platform returns recognized words. - void _onSpeechResult(SpeechRecognitionResult result) => text = result.recognizedWords; + String onSpeechResult(SpeechRecognitionResult result) => (text = result.recognizedWords); } \ No newline at end of file