diff --git a/data/lib/data_sources/chat_completion_data_source.dart b/data/lib/data_sources/chat_completion_data_source.dart index ea7f572..9d01d4e 100644 --- a/data/lib/data_sources/chat_completion_data_source.dart +++ b/data/lib/data_sources/chat_completion_data_source.dart @@ -2,11 +2,12 @@ import 'dart:convert'; import 'package:domain/entities/function_tool.dart'; import 'package:domain/entities/message.dart'; import 'package:http/http.dart' as http; +import '../models/chat_chunk.dart'; import '../utils/api_constants.dart'; import '../utils/network_exceptions.dart'; abstract class ChatCompletionsDataSource { - Stream sendMessages(List messages, List functionTools); + Stream sendMessages(List messages, List functionTools); } class ChatRemoteDataSourceImpl implements ChatCompletionsDataSource { @@ -15,7 +16,7 @@ class ChatRemoteDataSourceImpl implements ChatCompletionsDataSource { ChatRemoteDataSourceImpl({required this.client}); @override - Stream sendMessages(List messages, + Stream sendMessages(List messages, List functionTools) async* { final request = http.Request('POST', Uri.parse(ApiConstants.chatCompletionsEndpoint)) ..headers.addAll({ @@ -58,12 +59,11 @@ class ChatRemoteDataSourceImpl implements ChatCompletionsDataSource { try { final data = jsonDecode(jsonString); - final content = data['choices'][0]['delta']['content'] ?? ''; - if (content.isNotEmpty) { - yield content; - } - } catch (_) { + final chatChunk = ChatChunk.fromJson(data); + yield chatChunk; + } catch (e) { // Handle JSON parsing errors + print('Error decoding chat completion JSON: $e. $jsonString'); continue; } } diff --git a/data/lib/models/chat_chunk.dart b/data/lib/models/chat_chunk.dart new file mode 100644 index 0000000..a723e62 --- /dev/null +++ b/data/lib/models/chat_chunk.dart @@ -0,0 +1,65 @@ +import 'package:domain/entities/message.dart'; + +class ChatChunk { + String? id; + String? object; + int? created; + String? model; + String? systemFingerprint; + List? choices; + + ChatChunk( + {this.id, + this.object, + this.created, + this.model, + this.systemFingerprint, + this.choices}); + + ChatChunk.fromJson(Map json) { + id = json['id']; + object = json['object']; + created = json['created']; + model = json['model']; + systemFingerprint = json['system_fingerprint']; + if (json['choices'] != null) { + choices = []; + json['choices'].forEach((v) { + choices!.add(new Choices.fromJson(v)); + }); + } + } +} + +class Choices { + int? index; + Delta? delta; + String? logprobs; + String? finishReason; + + Choices({this.index, this.delta, this.logprobs, this.finishReason}); + + Choices.fromJson(Map json) { + index = json['index']; + delta = json['delta'] != null ? new Delta.fromJson(json['delta']) : null; + logprobs = json['logprobs']; + finishReason = json['finish_reason']; + } +} + +class Delta { + String? content; + List? toolCalls; + + Delta({this.content, this.toolCalls}); + + Delta.fromJson(Map json) { + content = json['content']; + if (json['tool_calls'] != null) { + toolCalls = []; + json['tool_calls'].forEach((v) { + toolCalls!.add(ToolCalls.fromJson(v)); + }); + } + } +} diff --git a/data/lib/models/chat_completion.dart b/data/lib/models/chat_completion.dart deleted file mode 100644 index e7bd328..0000000 --- a/data/lib/models/chat_completion.dart +++ /dev/null @@ -1,86 +0,0 @@ -class ChatCompletion { - String? id; - String? object; - int? created; - String? model; - String? systemFingerprint; - List? choices; - - ChatCompletion( - {this.id, - this.object, - this.created, - this.model, - this.systemFingerprint, - this.choices}); - - ChatCompletion.fromJson(Map json) { - id = json['id']; - object = json['object']; - created = json['created']; - model = json['model']; - systemFingerprint = json['system_fingerprint']; - if (json['choices'] != null) { - choices = []; - json['choices'].forEach((v) { - choices!.add(new Choices.fromJson(v)); - }); - } - } - - Map toJson() { - final Map data = new Map(); - data['id'] = this.id; - data['object'] = this.object; - data['created'] = this.created; - data['model'] = this.model; - data['system_fingerprint'] = this.systemFingerprint; - if (this.choices != null) { - data['choices'] = this.choices!.map((v) => v.toJson()).toList(); - } - return data; - } -} - -class Choices { - int? index; - Delta? delta; - Null? logprobs; - Null? finishReason; - - Choices({this.index, this.delta, this.logprobs, this.finishReason}); - - Choices.fromJson(Map json) { - index = json['index']; - delta = json['delta'] != null ? new Delta.fromJson(json['delta']) : null; - logprobs = json['logprobs']; - finishReason = json['finish_reason']; - } - - Map toJson() { - final Map data = new Map(); - data['index'] = this.index; - if (this.delta != null) { - data['delta'] = this.delta!.toJson(); - } - data['logprobs'] = this.logprobs; - data['finish_reason'] = this.finishReason; - return data; - } -} - -class Delta { - String? content; - - Delta({this.content}); - - Delta.fromJson(Map json) { - content = json['content']; - } - - Map toJson() { - final Map data = new Map(); - data['content'] = this.content; - return data; - } -} \ No newline at end of file diff --git a/data/lib/repositories/chat_repository_impl.dart b/data/lib/repositories/chat_repository_impl.dart index 084bdc7..9b782fe 100644 --- a/data/lib/repositories/chat_repository_impl.dart +++ b/data/lib/repositories/chat_repository_impl.dart @@ -1,7 +1,10 @@ +import 'dart:async'; + import 'package:domain/entities/function_tool.dart'; import 'package:domain/entities/message.dart'; import 'package:domain/repositories_abstract/chat_repository.dart'; import '../data_sources/chat_completion_data_source.dart'; +import '../models/chat_chunk.dart'; class ChatRepositoryImp implements ChatRepository { final ChatCompletionsDataSource chatCompletionsDataSource; @@ -9,7 +12,47 @@ class ChatRepositoryImp implements ChatRepository { ChatRepositoryImp({required this.chatCompletionsDataSource}); @override - Stream sendMessages(List messages, List functionTools) { - return chatCompletionsDataSource.sendMessages(messages, functionTools); + Stream sendMessages( + List messages, List functionTools) { + // Create a StreamController to accumulate and emit the content as strings + final StreamController controller = StreamController(); + + // Call the original sendMessages method that returns Stream + final chatChunks = + chatCompletionsDataSource.sendMessages(messages, functionTools); + + Message buffer = Message(role: "assistant"); + // Listen to the incoming stream of ChatChunks + chatChunks.listen((ChatChunk chunk) { + // Accumulate or extract the content from the ChatChunk and add it to the stream + final token = chunk.choices?.first.delta?.content; + if (token != null) { + buffer.content = (buffer.content ?? "") + token; + } + + final toolCalls = chunk.choices?.first.delta?.toolCalls; + if (toolCalls != null) { + if (buffer.toolCalls == null) { + buffer.toolCalls = toolCalls; + } else { + final newArguments = toolCalls.first.function?.arguments; + if (newArguments != null) { + final currentArg = + buffer.toolCalls?.first.function?.arguments ?? ""; + buffer.toolCalls?.first.function?.arguments = + currentArg + newArguments; + } + } + } + + controller.add(buffer); // Emit the content to the stream + }, onError: (error) { + controller.addError(error); // Handle errors + }, onDone: () { + controller.close(); // Close the stream when done + }); + + // Return the stream of accumulated content as strings + return controller.stream; } } diff --git a/domain/lib/entities/message.dart b/domain/lib/entities/message.dart index 510dd43..4b7580c 100644 --- a/domain/lib/entities/message.dart +++ b/domain/lib/entities/message.dart @@ -1,22 +1,78 @@ class Message { - final String role; - String content; + String? role; + String? content; + List? toolCalls; + String? tool_call_id; - Message({required this.role, required this.content}); + Message({this.role, this.content, this.toolCalls, this.tool_call_id}); // Factory constructor for creating a new Message instance from JSON - factory Message.fromJson(Map json) { - return Message( - role: json['role'], - content: json['content'], - ); + Message.fromJson(Map json) { + role = json['role'] ?? 'user'; + content = json['content']; + if (json['tool_calls'] != null) { + toolCalls = []; + json['tool_calls'].forEach((v) { + toolCalls!.add(ToolCalls.fromJson(v)); + }); + } + tool_call_id = json['tool_call_id']; } // Method for converting a Message instance to JSON format Map toJson() { - return { - 'role': role, - 'content': content, - }; + final Map data = {}; + data['role'] = role; + data['content'] = content; + if (toolCalls != null) { + data['tool_calls'] = toolCalls!.map((v) => v.toJson()).toList(); + } + data['tool_call_id'] = tool_call_id; + return data; + } +} + +class ToolCalls { + String? id; + String? type; + FunctionCall? function; + + ToolCalls({this.id, this.type, this.function}); + + ToolCalls.fromJson(Map json) { + id = json['id']; + type = json['type']; + function = json['function'] != null + ? FunctionCall.fromJson(json['function']) + : null; + } + + Map toJson() { + final Map data = {}; + data['id'] = id; + data['type'] = type; + if (function != null) { + data['function'] = function!.toJson(); + } + return data; + } +} + +class FunctionCall { + String? name; + String? arguments; + + FunctionCall({this.name, this.arguments}); + + FunctionCall.fromJson(Map json) { + name = json['name']; + arguments = json['arguments']; + } + + Map toJson() { + final Map data = {}; + data['name'] = name; + data['arguments'] = arguments; + return data; } } \ No newline at end of file diff --git a/domain/lib/repositories_abstract/chat_repository.dart b/domain/lib/repositories_abstract/chat_repository.dart index 2be0ab0..8e24c52 100644 --- a/domain/lib/repositories_abstract/chat_repository.dart +++ b/domain/lib/repositories_abstract/chat_repository.dart @@ -2,6 +2,6 @@ import '../entities/function_tool.dart'; import '../entities/message.dart'; abstract class ChatRepository { - Stream sendMessages(List messages, + Stream sendMessages(List messages, List functionTools); } diff --git a/domain/lib/usecases/chat_session_usecase.dart b/domain/lib/usecases/chat_session_usecase.dart index 609c3fd..6f3820a 100644 --- a/domain/lib/usecases/chat_session_usecase.dart +++ b/domain/lib/usecases/chat_session_usecase.dart @@ -40,13 +40,11 @@ class ChatSessionUseCase { } // Add a new method to update the last assistant message - void updateLastAssistantMessage(ChatSession session, String token) { + void updateLastAssistantMessage(ChatSession session, Message message) { // Find the last message that is from the assistant - print(session.messages.last.content); for (var i = session.messages.length - 1; i >= 0; i--) { if (session.messages[i].role == 'assistant') { - final previousContent = session.messages[i].content; - session.messages[i] = Message(role: 'assistant', content: previousContent + token); + session.messages[i] = message; break; } } diff --git a/domain/lib/usecases/chat_usecase.dart b/domain/lib/usecases/chat_usecase.dart index 24fc88b..f9aae57 100644 --- a/domain/lib/usecases/chat_usecase.dart +++ b/domain/lib/usecases/chat_usecase.dart @@ -16,7 +16,7 @@ class ChatUseCase { content: "You are an expert assistant specialized in composite materials. Your role is to provide accurate and detailed answers to questions related to composite material properties, design, calculations, and analysis."); - Stream sendMessages(List messages) { + Stream sendMessages(List messages) { final chatHistory = [systemMessage] + messages; final functionTools = functionToolsRepository.getAllFunctionTools(); return chatRepository.sendMessages( diff --git a/lib/presentation/chat/viewModels/chat_view_model.dart b/lib/presentation/chat/viewModels/chat_view_model.dart index 219dc3c..55a2f32 100644 --- a/lib/presentation/chat/viewModels/chat_view_model.dart +++ b/lib/presentation/chat/viewModels/chat_view_model.dart @@ -25,6 +25,13 @@ class ChatViewModel extends ChangeNotifier { ChatSession? get selectedSession => _selectedSession; + List defaultQuestions = [ + "What is SwiftComp?", + "Calculate lamina engineering constants", + "What is the upper bound of Young's modulus for composites?", + "How to use SwiftComp?", + ]; + ChatViewModel({ required ChatUseCase chatUseCase, required ChatSessionUseCase chatSessionUseCase, @@ -84,40 +91,68 @@ class ChatViewModel extends ChangeNotifier { scrollToBottom(); } - Future sendMessage() async { - if (!isUserInputEmpty() && selectedSession != null) { - setLoading(true); - final userMessage = Message(role: 'user', content: _controller.text); - _chatSessionUseCase.addMessageToSession(selectedSession!, userMessage); - _controller.clear(); - scrollToBottom(); - notifyListeners(); - - try { - await _chatUseCase.sendMessages(selectedSession!.messages).listen( - (token) { - final bool isLastMessageAssist = _chatSessionUseCase - .isLastMessageAssistInSession(selectedSession!); - if (isLastMessageAssist) { - _chatSessionUseCase.updateLastAssistantMessage( - selectedSession!, token); - } else { - _chatSessionUseCase.addMessageToSession( - selectedSession!, - Message(role: "assistant", content: token), - ); - } - scrollToBottom(); - notifyListeners(); - }, onDone: () { - setLoading(false); - }); - } catch (error) { - setLoading(false); + Future sendCurrentUserMessage() async { + sendUserMessage(_controller.text); + } + + Future sendUserMessage(String content) async { + if (content.isEmpty || selectedSession == null) return; + setLoading(true); + final userMessage = Message(role: 'user', content: content); + _chatSessionUseCase.addMessageToSession(selectedSession!, userMessage); + _controller.clear(); + scrollToBottom(); + notifyListeners(); + _sendMessages(); + } + + Future _sendMessages() async { + try { + await _chatUseCase.sendMessages(selectedSession!.messages).listen( + (message) { + final bool isLastMessageAssist = _chatSessionUseCase + .isLastMessageAssistInSession(selectedSession!); + if (isLastMessageAssist) { + _chatSessionUseCase.updateLastAssistantMessage( + selectedSession!, message); + } else { + _chatSessionUseCase.addMessageToSession(selectedSession!, message); + } + scrollToBottom(); + notifyListeners(); + }, onDone: () { + checkFunctionCall(); + }); + } catch (error) { + setLoading(false); + } + } + + void checkFunctionCall() { + final lastMessage = selectedSession?.messages.last; + final tool = lastMessage?.toolCalls?.first; + if (tool != null) { + final functionName = tool.function?.name; + if (functionName == "calculate_lamina_engineering_constants") { + // TODO: Use internal or external tools to calculation the result... + _chatSessionUseCase.addMessageToSession( + selectedSession!, + Message( + role: "tool", + content: + "{ \"E_1\": 4431.314623338257, \"E_2\": 4431.314623338257, \"G_12\": 36144.57831325301, \"nu_12\": 0.7725258493353028, \"eta_1_12\": 0.10339734121122615, \"eta_2_12\": 0.10339734121122582, \"Q\": [ [ 43000.50301810866, 40500.50301810865, -70422.5352112676 ], [ 40500.50301810865, 43000.50301810863, -70422.53521126758 ], [ -70422.53521126762, -70422.53521126759, 154929.57746478872 ] ], \"S\": [ [ 0.00022566666666666666, -0.00017433333333333333, 2.3333333333333373e-05 ], [ -0.00017433333333333333, 0.00022566666666666669, 2.3333333333333295e-05 ], [ 2.333333333333337e-05, 2.3333333333333295e-05, 2.7666666666666667e-05 ] ] }", + tool_call_id: tool.id + ) + ); + } + _sendMessages(); + } else { + setLoading(false); } } + bool isUserMessage(Message message) { return message.role == 'user'; } @@ -127,9 +162,21 @@ class ChatViewModel extends ChangeNotifier { final shouldAppendDot = _isLoading && isLastMessage; - final assistantMessageContent = - shouldAppendDot ? message.content + " ●" : message.content; - return assistantMessageContent; + var content = message.content ?? ""; + + final function = message.toolCalls?.first.function; + if (function != null) { + final name = message.toolCalls?.first.function?.name; + final arguments = message.toolCalls?.first.function?.arguments; + content = content + + '\n\n' + + "Call function: $name" + + '\n\n' + + "Use parameters:\n\n $arguments"; + } + + final assistantContent = shouldAppendDot ? content + " ●" : content; + return assistantContent; } void _onUserInputChanged() { @@ -139,4 +186,10 @@ class ChatViewModel extends ChangeNotifier { bool isUserInputEmpty() { return controller.text.isEmpty; } + + void onDefaultQuestionsTapped(int index) { + // Handle the card tap event, e.g., navigate to another screen + final question = defaultQuestions[index]; + sendUserMessage(question); + } } diff --git a/lib/presentation/chat/views/chat_message_list.dart b/lib/presentation/chat/views/chat_message_list.dart index 3b0fa17..61771dc 100644 --- a/lib/presentation/chat/views/chat_message_list.dart +++ b/lib/presentation/chat/views/chat_message_list.dart @@ -16,52 +16,92 @@ class ChatMessageList extends StatelessWidget { return Center(child: Text('No session selected.')); } + final messages = selectedSession.messages; + return Column( children: [ Expanded( - child: ListView.builder( - controller: chatViewModel.scrollController, - itemCount: selectedSession.messages.length, - itemBuilder: (context, index) { - final message = selectedSession.messages[index]; - final isUserMessage = chatViewModel.isUserMessage(message); - - if (isUserMessage) { - return Align( - alignment: Alignment.centerRight, - child: Container( - margin: - EdgeInsets.symmetric(vertical: 5.0, horizontal: 10.0), - padding: - EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), - decoration: BoxDecoration( - color: Colors.grey[300], - borderRadius: BorderRadius.circular(20.0), + child: messages.isEmpty + ? Column( + children: [ + // Optional: Add a logo or banner at the top like the second image + Padding( + padding: const EdgeInsets.all(16.0), + child: Icon( + Icons.chat_bubble_outline, + size: 60, + color: Colors.grey, + ), + ), + Text( + "Ask a question to get started!", + style: TextStyle(fontSize: 18, color: Colors.grey), ), - child: Text( - message.content, - style: TextStyle( - fontSize: 15, - color: Colors.black, + SizedBox(height: 20), + // Default questions displayed as cards or buttons + Expanded( + child: GridView.count( + crossAxisCount: 2, + // You can adjust the number of columns + crossAxisSpacing: 10, + mainAxisSpacing: 10, + padding: EdgeInsets.all(16), + children: List.generate(chatViewModel.defaultQuestions.length, (index) { + return InkWell( + onTap: () { + chatViewModel.onDefaultQuestionsTapped(index); + }, + child: _buildDefaultQuestionCard(chatViewModel.defaultQuestions[index]), + ); + }), ), ), - ), - ); - } else { - final assistantMessageContent = - chatViewModel.assistantMessageContent(message); - return Align( - alignment: Alignment.centerLeft, - child: Container( - padding: - EdgeInsets.symmetric(vertical: 10.0, horizontal: 15.0), - child: - MarkdownWithMath(markdownData: assistantMessageContent), - ), - ); - } - }, - ), + ], + ) + : ListView.builder( + controller: chatViewModel.scrollController, + itemCount: selectedSession.messages.length, + itemBuilder: (context, index) { + final message = selectedSession.messages[index]; + switch (message.role) { + case "user": + return Align( + alignment: Alignment.centerRight, + child: Container( + margin: EdgeInsets.symmetric( + vertical: 5.0, horizontal: 10.0), + padding: EdgeInsets.symmetric( + vertical: 10.0, horizontal: 15.0), + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(20.0), + ), + child: Text( + message.content ?? "", + style: TextStyle( + fontSize: 15, + color: Colors.black, + ), + ), + ), + ); + case "assistant": + final assistantMessageContent = + chatViewModel.assistantMessageContent(message); + return Align( + alignment: Alignment.centerLeft, + child: Container( + padding: EdgeInsets.symmetric( + vertical: 10.0, horizontal: 15.0), + child: MarkdownWithMath( + markdownData: assistantMessageContent), + ), + ); + default: + return Container(); + } + }, + ), ), Padding( padding: const EdgeInsets.all(8.0), @@ -82,7 +122,7 @@ class ChatMessageList extends StatelessWidget { onPressed: chatViewModel.isUserInputEmpty() ? null : () async { - await chatViewModel.sendMessage(); + await chatViewModel.sendCurrentUserMessage(); }, ), ], @@ -92,3 +132,23 @@ class ChatMessageList extends StatelessWidget { ); } } + +// A helper function to create default question cards +Widget _buildDefaultQuestionCard(String question) { + return Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Text( + question, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16, color: Colors.black), + ), + ), + ), + ); +} diff --git a/lib/presentation/chat/views/markdown_with_math.dart b/lib/presentation/chat/views/markdown_with_math.dart index deb3025..f084175 100644 --- a/lib/presentation/chat/views/markdown_with_math.dart +++ b/lib/presentation/chat/views/markdown_with_math.dart @@ -16,8 +16,10 @@ class MarkdownWithMath extends StatelessWidget { data: markdownData, selectable: true, // For better user experience - builders: {'h5': InlineMathBuilder(), 'h4': NewlineMathBuilder()}, - inlineSyntaxes: [MathInlineSyntax(), MathNewlineSyntax()], + // builders: {'h5': InlineMathBuilder(), 'h4': NewlineMathBuilder()}, + // inlineSyntaxes: [MathInlineSyntax(), MathNewlineSyntax()], + builders: {'h4': NewlineMathBuilder()}, + inlineSyntaxes: [MathNewlineSyntax(), MathInlineSyntax()], // Custom inline syntax for inline math styleSheet: MarkdownStyleSheet(), ); @@ -31,9 +33,8 @@ class MathInlineSyntax extends md.InlineSyntax { @override bool onMatch(md.InlineParser parser, Match match) { final mathExpression = match.group(1) ?? ""; - // print("Inline equation:" + mathExpression); - // h4 is used for math equations. If there is h4 element, it will be render by math - final element = md.Element.text("h5", mathExpression); + print("Inline equation detected:" + mathExpression); + final element = md.Element.text("h4", mathExpression); parser.addNode(element); // Add the custom math node return true; } @@ -59,17 +60,19 @@ class NewlineMathBuilder extends MarkdownElementBuilder { @override Widget visitText(md.Text text, TextStyle? preferredStyle) { final mathExpression = text.text; - // print("Math equation detected: $mathExpression"); - return Math.tex(mathExpression, textStyle: preferredStyle); + // print("New line Math builder: $mathExpression"); + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Math.tex(mathExpression, textStyle: preferredStyle)); } } -class InlineMathBuilder extends MarkdownElementBuilder { - @override - Widget visitText(md.Text text, TextStyle? preferredStyle) { - final mathExpression = text.text; - // print("Math equation detected: $mathExpression"); - return Math.tex(mathExpression, - mathStyle: MathStyle.text, textStyle: preferredStyle); - } -} +// class InlineMathBuilder extends MarkdownElementBuilder { +// @override +// Widget visitText(md.Text text, TextStyle? preferredStyle) { +// final mathExpression = text.text; +// print("Inline Math builder: $mathExpression"); +// return Math.tex(mathExpression, +// mathStyle: MathStyle.display, textStyle: preferredStyle); +// } +// } diff --git a/pubspec.lock b/pubspec.lock index 5cca5a2..8c4ece2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -372,10 +372,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7 + sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78 url: "https://pub.dev" source: hosted - version: "0.7.3+1" + version: "0.7.3+2" flutter_math_fork: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 8076e0a..f229dbd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -47,7 +47,7 @@ dependencies: provider: ^6.0.2 flutter_progress_hud: ^2.0.2 file: ^6.1.4 - flutter_markdown: ^0.7.3+1 + flutter_markdown: ^0.7.3+2 get_it: ^8.0.0 amplify_flutter: ^1.8.0