diff --git a/lib/src/feature/chat/bloc/chat_bloc.dart b/lib/src/feature/chat/bloc/chat_bloc.dart index ae5aee0..419ca0d 100644 --- a/lib/src/feature/chat/bloc/chat_bloc.dart +++ b/lib/src/feature/chat/bloc/chat_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:dart_openai/openai.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:gpt_client/src/feature/chat/bloc/chat_dependencies.dart'; import 'package:gpt_client/src/feature/chat/model/chat_data.dart'; @@ -51,14 +52,16 @@ class ChatBloc extends StreamBloc { yield ChatState.updatedSuccessfully( data: _dependencies.chatRepository.currentData(), ); + } on RequestFailedException catch (e) { + yield ChatState.error( + data: _data, + error: e.message, + ); } on Object catch (e) { yield ChatState.error( data: _data, error: e.toString(), ); - rethrow; - } finally { - yield ChatState.idle(data: _data); } } } diff --git a/lib/src/feature/chat/page/chat_page.dart b/lib/src/feature/chat/page/chat_page.dart index 50520fa..e542877 100644 --- a/lib/src/feature/chat/page/chat_page.dart +++ b/lib/src/feature/chat/page/chat_page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart' as ui; @@ -17,168 +18,213 @@ class ChatPage extends StatefulWidget { } class _ChatPageState extends State { + var _isDialogShowing = false; + @override Widget build(BuildContext context) => ChatScope( - child: BlocBuilder( - builder: (context, state) => Scaffold( - appBar: ChatAppBar( - isTyping: state is ChatStateLoading, - ), - extendBodyBehindAppBar: true, - body: SafeArea( - child: ui.Chat( - messages: state.data.messages, - inputOptions: ui.InputOptions( - sendButtonVisibilityMode: state is ChatStateLoading - ? ui.SendButtonVisibilityMode.hidden - : ui.SendButtonVisibilityMode.editing, + child: BlocListener( + listener: _blocListener, + child: BlocBuilder( + builder: (context, state) { + return Scaffold( + appBar: ChatAppBar( + isTyping: state is ChatStateLoading, ), - onSendPressed: (partialText) => ChatScope.sendMessage( - context, - partialText.text, - ), - emptyState: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Image( - image: const AssetImage('asset/image/ai_logo.png'), - width: 150, - height: 150, - color: Theme.of(context).primaryColor, + extendBodyBehindAppBar: true, + body: SafeArea( + child: ui.Chat( + messages: state.data.messages, + inputOptions: ui.InputOptions( + sendButtonVisibilityMode: state is ChatStateLoading + ? ui.SendButtonVisibilityMode.hidden + : ui.SendButtonVisibilityMode.editing, ), - const Text( - 'Ask your assistant something', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w400, - ), + onSendPressed: (partialText) => ChatScope.sendMessage( + context, + partialText.text, ), - const SizedBox(height: 3), - InkWell( - onTap: () => launchUrl( - Uri.parse('https://openai.com/blog/chatgpt'), - ), - child: Text( - 'More about Chat GPT', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w400, + emptyState: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image( + image: const AssetImage('asset/image/ai_logo.png'), + width: 150, + height: 150, color: Theme.of(context).primaryColor, ), - ), + const Text( + 'Ask your assistant something', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 3), + InkWell( + onTap: () => launchUrl( + Uri.parse('https://openai.com/blog/chatgpt'), + ), + child: Text( + 'More about Chat GPT', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + color: Theme.of(context).primaryColor, + ), + ), + ), + ], ), - ], - ), - user: state.data.user, - scrollPhysics: const BouncingScrollPhysics(), - dateHeaderBuilder: (header) => Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Center( - child: Text( - DateFormat('MMMM d, y').format(header.dateTime), - style: const TextStyle( - color: Color.fromARGB(255, 138, 138, 138), - fontSize: 12, - fontWeight: FontWeight.w500, + user: state.data.user, + scrollPhysics: const BouncingScrollPhysics(), + dateHeaderBuilder: (header) => Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Center( + child: Text( + DateFormat('MMMM d, y').format(header.dateTime), + style: const TextStyle( + color: Color.fromARGB(255, 138, 138, 138), + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), ), ), - ), - ), - theme: ui.DefaultChatTheme( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + theme: ui.DefaultChatTheme( + backgroundColor: + Theme.of(context).scaffoldBackgroundColor, - /// MESSAGES - messageInsetsHorizontal: 10, - messageInsetsVertical: 8, - messageBorderRadius: 14, - primaryColor: Theme.of(context).primaryColor, - secondaryColor: const Color.fromARGB(255, 239, 239, 239), - receivedMessageBodyTextStyle: const TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.3, - ), - sentMessageBodyTextStyle: const TextStyle( - color: Colors.white, - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.3, - ), + /// MESSAGES + messageInsetsHorizontal: 10, + messageInsetsVertical: 8, + messageBorderRadius: 14, + primaryColor: Theme.of(context).primaryColor, + secondaryColor: const Color.fromARGB(255, 239, 239, 239), + receivedMessageBodyTextStyle: const TextStyle( + color: Colors.black, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.3, + ), + sentMessageBodyTextStyle: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.3, + ), - /// INPUT - inputBorderRadius: BorderRadius.zero, - inputBackgroundColor: Colors.white, - inputTextColor: Colors.black, - inputTextCursorColor: Theme.of(context).primaryColor, - inputPadding: EdgeInsets.only( - left: 10, - right: 10, - top: 7, - bottom: - MediaQuery.of(context).viewInsets.bottom != 0 ? 7 : 30, - ), - inputTextStyle: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - height: 1.3, - ), - inputContainerDecoration: const BoxDecoration( - border: Border( - top: BorderSide( - color: Color.fromARGB(255, 204, 204, 204), - width: 0.5, + /// INPUT + inputBorderRadius: BorderRadius.zero, + inputBackgroundColor: Colors.white, + inputTextColor: Colors.black, + inputTextCursorColor: Theme.of(context).primaryColor, + inputPadding: EdgeInsets.only( + left: 10, + right: 10, + top: 7, + bottom: MediaQuery.of(context).viewInsets.bottom != 0 + ? 7 + : 30, ), - ), - ), - inputTextDecoration: const InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(14)), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(14)), - borderSide: BorderSide( - color: Color.fromARGB(255, 204, 204, 204), - width: 0.5, + inputTextStyle: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + height: 1.3, ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(14)), - borderSide: BorderSide( - color: Color.fromARGB(255, 204, 204, 204), - width: 0.5, + inputContainerDecoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: Color.fromARGB(255, 204, 204, 204), + width: 0.5, + ), + ), + ), + inputTextDecoration: const InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(14)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(14)), + borderSide: BorderSide( + color: Color.fromARGB(255, 204, 204, 204), + width: 0.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(14)), + borderSide: BorderSide( + color: Color.fromARGB(255, 204, 204, 204), + width: 0.5, + ), + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 13, + vertical: 8, + ), + isCollapsed: true, ), - ), - contentPadding: EdgeInsets.symmetric( - horizontal: 13, - vertical: 8, - ), - isCollapsed: true, - ), - /// SEND BUTTON - sendButtonMargin: EdgeInsets.zero, - sendButtonIcon: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(14)), - color: Theme.of(context).primaryColor, - ), - padding: const EdgeInsets.all(2), - child: Transform.rotate( - angle: 1.5708, - child: const Icon( - Icons.arrow_back_rounded, - color: Colors.white, - size: 19, + /// SEND BUTTON + sendButtonMargin: EdgeInsets.zero, + sendButtonIcon: Container( + decoration: BoxDecoration( + borderRadius: + const BorderRadius.all(Radius.circular(14)), + color: Theme.of(context).primaryColor, + ), + padding: const EdgeInsets.all(2), + child: Transform.rotate( + angle: 1.5708, + child: const Icon( + Icons.arrow_back_rounded, + color: Colors.white, + size: 19, + ), + ), ), ), ), ), - ), - ), + ); + }, ), ), ); + + void _blocListener(BuildContext context, ChatState state) { + if (!_isDialogShowing && state is ChatStateError) { + Future.delayed( + Duration.zero, + () => _showErrorDialog(context, state.error), + ); + } + } + + Future _showErrorDialog(BuildContext context, String error) async { + _isDialogShowing = true; + await showCupertinoModalPopup( + context: context, + builder: (context) => CupertinoAlertDialog( + title: const Text('An error has occurred'), + content: Text(error), + actions: [ + CupertinoDialogAction( + isDefaultAction: true, + onPressed: () => Navigator.pop(context), + child: Text( + 'Okay', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 17, + color: Theme.of(context).primaryColor, + ), + ), + ), + ], + ), + ); + _isDialogShowing = false; + } } diff --git a/lib/src/feature/chat/repository/chat_repository.dart b/lib/src/feature/chat/repository/chat_repository.dart index 25c3a06..c2844b1 100644 --- a/lib/src/feature/chat/repository/chat_repository.dart +++ b/lib/src/feature/chat/repository/chat_repository.dart @@ -1,7 +1,7 @@ import 'package:dart_openai/openai.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart' as types; -import 'package:gpt_client/src/feature/chat/model/open_ai_message.dart'; import 'package:gpt_client/src/feature/chat/model/chat_data.dart'; +import 'package:gpt_client/src/feature/chat/model/open_ai_message.dart'; import 'package:gpt_client/src/feature/chat/repository/chat_repository_dependencies.dart'; import 'package:gpt_client/src/feature/chat/repository/chat_repository_interface.dart'; import 'package:uuid/uuid.dart'; diff --git a/lib/src/feature/chat/widget/chat_app_bar.dart b/lib/src/feature/chat/widget/chat_app_bar.dart index 0601afc..b884ac7 100644 --- a/lib/src/feature/chat/widget/chat_app_bar.dart +++ b/lib/src/feature/chat/widget/chat_app_bar.dart @@ -102,22 +102,47 @@ class ChatAppBar extends StatelessWidget implements PreferredSizeWidget { showCupertinoModalPopup( context: pageContext, builder: (context) => CupertinoActionSheet( - message: const Text('Are you want to clear history?'), + message: const Text( + 'Are you want to clear history?', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 15, + ), + ), cancelButton: CupertinoActionSheetAction( isDefaultAction: true, onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + child: Text( + 'Cancel', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 17, + color: Theme.of(context).primaryColor, + ), + ), ), actions: [ CupertinoActionSheetAction( isDestructiveAction: true, - onPressed: () { - Navigator.pop(context); - ChatScope.clearMessage(pageContext); - }, - child: const Text('Clear'), + onPressed: () => clearMessagesAndPopDialog(context, pageContext), + child: const Text( + 'Clear', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 17, + color: CupertinoColors.systemRed, + ), + ), ) ], ), ); + + void clearMessagesAndPopDialog( + BuildContext context, + BuildContext pageContext, + ) { + Navigator.pop(context); + ChatScope.clearMessages(pageContext); + } } diff --git a/lib/src/feature/chat/widget/scope/chat_scope.dart b/lib/src/feature/chat/widget/scope/chat_scope.dart index 12d1e39..ce10020 100644 --- a/lib/src/feature/chat/widget/scope/chat_scope.dart +++ b/lib/src/feature/chat/widget/scope/chat_scope.dart @@ -28,7 +28,7 @@ class ChatScope extends StatelessWidget { _scope.add(context, ChatEvent.sendMessage(text)); } - static void clearMessage(BuildContext context) { + static void clearMessages(BuildContext context) { _scope.add(context, const ChatEvent.clearMessages()); }