diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart index 497f769354..5e4595a1e5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart @@ -102,6 +102,7 @@ class _TypeOptionMenuItem extends StatelessWidget { value.text, fontSize: 14.0, maxLines: 2, + lineHeight: 1.0, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart index 4d8acbbeba..69ab500564 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart @@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget { return Row( children: [ FlowyText( + lineHeight: 1.0, databaseLayoutFromViewLayout(view.layout).layoutName, color: Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart index 050bf4b594..1076b9dba6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/font/font_setting.dart @@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ FlowyText( + lineHeight: 1.0, name, color: theme.colorScheme.onSurface, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart index 6f4e65f2b4..6473485514 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart @@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State { mainAxisSize: MainAxisSize.min, children: [ FlowyText( + lineHeight: 1.0, languageFromLocale(locale), color: theme.colorScheme.onSurface, ), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart index 402243516d..cd5579d7d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_bloc.dart @@ -39,7 +39,8 @@ class ChatBloc extends Bloc { final String chatId; /// The last streaming message id - String lastStreamMessageId = ''; + String answerStreamMessageId = ''; + String questionStreamMessageId = ''; /// Using a temporary map to associate the real message ID with the last streaming message ID. /// @@ -127,21 +128,11 @@ class ChatBloc extends Bloc { ); }, // streaming message - streaming: (Message message) { - final allMessages = _perminentMessages(); - allMessages.insert(0, message); - emit( - state.copyWith( - messages: allMessages, - streamingState: const StreamingState.streaming(), - canSendMessage: false, - ), - ); - }, - finishStreaming: () { + finishAnswerStreaming: () { emit( state.copyWith( streamingState: const StreamingState.done(), + acceptRelatedQuestion: true, canSendMessage: state.sendingState == const SendMessageState.done(), ), @@ -162,9 +153,9 @@ class ChatBloc extends Bloc { // If the streaming is not started, remove the message from the list if (!state.answerStream!.hasStarted) { allMessages.removeWhere( - (element) => element.id == lastStreamMessageId, + (element) => element.id == answerStreamMessageId, ); - lastStreamMessageId = ""; + answerStreamMessageId = ""; } // when stop stream, we will set the answer stream to null. Which means the streaming @@ -189,22 +180,26 @@ class ChatBloc extends Bloc { ), ); }, + startAnswerStreaming: (Message message) { + final allMessages = _perminentMessages(); + allMessages.insert(0, message); + emit( + state.copyWith( + messages: allMessages, + streamingState: const StreamingState.streaming(), + canSendMessage: false, + ), + ); + }, sendMessage: (String message, Map? metadata) async { unawaited(_startStreamingMessage(message, metadata, emit)); final allMessages = _perminentMessages(); - // allMessages.insert( - // 0, - // CustomMessage( - // metadata: OnetimeShotType.sendingMessage.toMap(), - // author: User(id: state.userProfile.id.toString()), - // id: state.userProfile.id.toString(), - // ), - // ); emit( state.copyWith( lastSentMessage: null, messages: allMessages, relatedQuestions: [], + acceptRelatedQuestion: false, sendingState: const SendMessageState.sending(), canSendMessage: false, ), @@ -257,10 +252,17 @@ class ChatBloc extends Bloc { chatMessageCallback: (pb) { if (!isClosed) { // 3 mean message response from AI - if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) { + if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) { temporaryMessageIDMap[pb.messageId.toString()] = - lastStreamMessageId; - lastStreamMessageId = ""; + answerStreamMessageId; + answerStreamMessageId = ""; + } + + // 1 mean message response from User + if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) { + temporaryMessageIDMap[pb.messageId.toString()] = + questionStreamMessageId; + questionStreamMessageId = ""; } final message = _createTextMessage(pb); @@ -270,7 +272,7 @@ class ChatBloc extends Bloc { chatErrorMessageCallback: (err) { if (!isClosed) { Log.error("chat error: ${err.errorMessage}"); - add(const ChatEvent.finishStreaming()); + add(const ChatEvent.finishAnswerStreaming()); } }, latestMessageCallback: (list) { @@ -287,7 +289,7 @@ class ChatBloc extends Bloc { }, finishStreamingCallback: () { if (!isClosed) { - add(const ChatEvent.finishStreaming()); + add(const ChatEvent.finishAnswerStreaming()); // The answer strema will bet set to null after the streaming is finished or canceled. // so if the answer stream is null, we will not get related question. if (state.lastSentMessage != null && state.answerStream != null) { @@ -300,7 +302,9 @@ class ChatBloc extends Bloc { if (!isClosed) { result.fold( (list) { - add(ChatEvent.didReceiveRelatedQuestion(list.items)); + if (state.acceptRelatedQuestion) { + add(ChatEvent.didReceiveRelatedQuestion(list.items)); + } }, (err) { Log.error("Failed to get related question: $err"); @@ -358,16 +362,21 @@ class ChatBloc extends Bloc { } final answerStream = AnswerStream(); + final questionStream = QuestionStream(); add(ChatEvent.didUpdateAnswerStream(answerStream)); final payload = StreamChatPayloadPB( chatId: state.view.id, message: message, messageType: ChatMessageTypePB.User, - textStreamPort: Int64(answerStream.nativePort), + questionStreamPort: Int64(questionStream.nativePort), + answerStreamPort: Int64(answerStream.nativePort), metadata: await metadataPBFromMetadata(metadata), ); + final questionStreamMessage = _createQuestionStreamMessage(questionStream); + add(ChatEvent.receveMessage(questionStreamMessage)); + // Stream message to the server final result = await AIEventStreamMessage(payload).send(); result.fold( @@ -375,13 +384,12 @@ class ChatBloc extends Bloc { if (!isClosed) { add(ChatEvent.finishSending(question)); - final questionMessageId = question.messageId; - final message = _createTextMessage(question); - add(ChatEvent.receveMessage(message)); + // final message = _createTextMessage(question); + // add(ChatEvent.receveMessage(message)); final streamAnswer = - _createStreamMessage(answerStream, questionMessageId); - add(ChatEvent.streaming(streamAnswer)); + _createAnswerStreamMessage(answerStream, question.messageId); + add(ChatEvent.startAnswerStreaming(streamAnswer)); } }, (err) { @@ -404,9 +412,12 @@ class ChatBloc extends Bloc { ); } - Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) { + Message _createAnswerStreamMessage( + AnswerStream stream, + Int64 questionMessageId, + ) { final streamMessageId = (questionMessageId + 1).toString(); - lastStreamMessageId = streamMessageId; + answerStreamMessageId = streamMessageId; return TextMessage( author: User(id: "streamId:${nanoid()}"), @@ -421,6 +432,20 @@ class ChatBloc extends Bloc { ); } + Message _createQuestionStreamMessage(QuestionStream stream) { + questionStreamMessageId = nanoid(); + return TextMessage( + author: User(id: state.userProfile.id.toString()), + metadata: { + "$QuestionStream": stream, + "chatId": chatId, + }, + id: questionStreamMessageId, + createdAt: DateTime.now().millisecondsSinceEpoch, + text: '', + ); + } + Message _createTextMessage(ChatMessagePB message) { String messageId = message.messageId.toString(); @@ -454,9 +479,10 @@ class ChatEvent with _$ChatEvent { _FinishSendMessage; // receive message - const factory ChatEvent.streaming(Message message) = _StreamingMessage; + const factory ChatEvent.startAnswerStreaming(Message message) = + _StartAnswerStreaming; const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage; - const factory ChatEvent.finishStreaming() = _FinishStreamingMessage; + const factory ChatEvent.finishAnswerStreaming() = _FinishAnswerStreaming; // loading messages const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage; @@ -499,6 +525,7 @@ class ChatState with _$ChatState { required bool hasMorePrevMessage, // The related questions that are received after the user message is sent. required List relatedQuestions, + @Default(false) bool acceptRelatedQuestion, // The last user message that is sent to the server. ChatMessagePB? lastSentMessage, AnswerStream? answerStream, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart index 3124e08c77..a82a84f34a 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_message_stream.dart @@ -92,3 +92,102 @@ class AnswerStream { } } } + +class QuestionStream { + QuestionStream() { + _port.handler = _controller.add; + _subscription = _controller.stream.listen( + (event) { + if (event.startsWith("data:")) { + _hasStarted = true; + final newText = event.substring(5); + _text += newText; + if (_onData != null) { + _onData!(_text); + } + } else if (event.startsWith("message_id:")) { + final messageId = event.substring(11); + _onMessageId?.call(messageId); + } else if (event.startsWith("start_index_file:")) { + final indexName = event.substring(17); + _onFileIndexStart?.call(indexName); + } else if (event.startsWith("end_index_file:")) { + final indexName = event.substring(10); + _onFileIndexEnd?.call(indexName); + } else if (event.startsWith("index_file_error:")) { + final indexName = event.substring(16); + _onFileIndexError?.call(indexName); + } else if (event.startsWith("index_start:")) { + _onIndexStart?.call(); + } else if (event.startsWith("index_end:")) { + _onIndexEnd?.call(); + } else if (event.startsWith("done:")) { + _onDone?.call(); + } else if (event.startsWith("error:")) { + _error = event.substring(5); + if (_onError != null) { + _onError!(_error!); + } + } + }, + onError: (error) { + if (_onError != null) { + _onError!(error.toString()); + } + }, + ); + } + + final RawReceivePort _port = RawReceivePort(); + final StreamController _controller = StreamController.broadcast(); + late StreamSubscription _subscription; + bool _hasStarted = false; + String? _error; + String _text = ""; + + // Callbacks + void Function(String text)? _onData; + void Function(String error)? _onError; + void Function(String messageId)? _onMessageId; + void Function(String indexName)? _onFileIndexStart; + void Function(String indexName)? _onFileIndexEnd; + void Function(String indexName)? _onFileIndexError; + void Function()? _onIndexStart; + void Function()? _onIndexEnd; + void Function()? _onDone; + + int get nativePort => _port.sendPort.nativePort; + bool get hasStarted => _hasStarted; + String? get error => _error; + String get text => _text; + + Future dispose() async { + await _controller.close(); + await _subscription.cancel(); + _port.close(); + } + + void listen({ + void Function(String text)? onData, + void Function(String error)? onError, + void Function(String messageId)? onMessageId, + void Function(String indexName)? onFileIndexStart, + void Function(String indexName)? onFileIndexEnd, + void Function(String indexName)? onFileIndexFail, + void Function()? onIndexStart, + void Function()? onIndexEnd, + void Function()? onDone, + }) { + _onData = onData; + _onError = onError; + _onMessageId = onMessageId; + + _onFileIndexStart = onFileIndexStart; + _onFileIndexEnd = onFileIndexEnd; + _onFileIndexError = onFileIndexFail; + + _onIndexStart = onIndexStart; + _onIndexEnd = onIndexEnd; + _onDone = onDone; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart index 41fea4ab22..207d811b8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bloc.dart @@ -1,27 +1,91 @@ -import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'chat_message_service.dart'; - part 'chat_user_message_bloc.freezed.dart'; class ChatUserMessageBloc extends Bloc { ChatUserMessageBloc({ - required Message message, - required String? metadata, + required dynamic message, }) : super( ChatUserMessageState.initial( message, - chatFilesFromMetadataString(metadata), ), ) { on( (event, emit) async { event.when( - initial: () {}, + initial: () { + if (state.stream != null) { + add(ChatUserMessageEvent.updateText(state.stream!.text)); + } + + state.stream?.listen( + onData: (text) { + if (!isClosed) { + add(ChatUserMessageEvent.updateText(text)); + } + }, + onMessageId: (messageId) { + if (!isClosed) { + add(ChatUserMessageEvent.updateMessageId(messageId)); + } + }, + onError: (error) { + if (!isClosed) { + add(ChatUserMessageEvent.receiveError(error.toString())); + } + }, + onFileIndexStart: (indexName) { + Log.debug("index start: $indexName"); + }, + onFileIndexEnd: (indexName) { + Log.info("index end: $indexName"); + }, + onFileIndexFail: (indexName) { + Log.debug("index fail: $indexName"); + }, + onIndexStart: () { + if (!isClosed) { + add( + const ChatUserMessageEvent.updateQuestionState( + QuestionMessageState.indexStart(), + ), + ); + } + }, + onIndexEnd: () { + if (!isClosed) { + add( + const ChatUserMessageEvent.updateQuestionState( + QuestionMessageState.indexEnd(), + ), + ); + } + }, + onDone: () { + if (!isClosed) { + add( + const ChatUserMessageEvent.updateQuestionState( + QuestionMessageState.finish(), + ), + ); + } + }, + ); + }, + updateText: (String text) { + emit(state.copyWith(text: text)); + }, + updateMessageId: (String messageId) { + emit(state.copyWith(messageId: messageId)); + }, + receiveError: (String error) {}, + updateQuestionState: (QuestionMessageState newState) { + emit(state.copyWith(messageState: newState)); + }, ); }, ); @@ -31,18 +95,47 @@ class ChatUserMessageBloc @freezed class ChatUserMessageEvent with _$ChatUserMessageEvent { const factory ChatUserMessageEvent.initial() = Initial; + const factory ChatUserMessageEvent.updateText(String text) = _UpdateText; + const factory ChatUserMessageEvent.updateQuestionState( + QuestionMessageState newState, + ) = _UpdateQuestionState; + const factory ChatUserMessageEvent.updateMessageId(String messageId) = + _UpdateMessageId; + const factory ChatUserMessageEvent.receiveError(String error) = _ReceiveError; } @freezed class ChatUserMessageState with _$ChatUserMessageState { const factory ChatUserMessageState({ - required Message message, - required List files, + required String text, + QuestionStream? stream, + String? messageId, + @Default(QuestionMessageState.finish()) QuestionMessageState messageState, }) = _ChatUserMessageState; factory ChatUserMessageState.initial( - Message message, - List files, + dynamic message, ) => - ChatUserMessageState(message: message, files: files); + ChatUserMessageState( + text: message is String ? message : "", + stream: message is QuestionStream ? message : null, + ); +} + +@freezed +class QuestionMessageState with _$QuestionMessageState { + const factory QuestionMessageState.indexFileStart(String fileName) = + _IndexFileStart; + const factory QuestionMessageState.indexFileEnd(String fileName) = + _IndexFileEnd; + const factory QuestionMessageState.indexFileFail(String fileName) = + _IndexFileFail; + + const factory QuestionMessageState.indexStart() = _IndexStart; + const factory QuestionMessageState.indexEnd() = _IndexEnd; + const factory QuestionMessageState.finish() = _Finish; +} + +extension QuestionMessageStateX on QuestionMessageState { + bool get isFinish => this is _Finish; } diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart new file mode 100644 index 0000000000..542280ca99 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart @@ -0,0 +1,48 @@ +import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_chat_types/flutter_chat_types.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'chat_message_service.dart'; + +part 'chat_user_message_bubble_bloc.freezed.dart'; + +class ChatUserMessageBubbleBloc + extends Bloc { + ChatUserMessageBubbleBloc({ + required Message message, + required String? metadata, + }) : super( + ChatUserMessageBubbleState.initial( + message, + chatFilesFromMetadataString(metadata), + ), + ) { + on( + (event, emit) async { + event.when( + initial: () {}, + ); + }, + ); + } +} + +@freezed +class ChatUserMessageBubbleEvent with _$ChatUserMessageBubbleEvent { + const factory ChatUserMessageBubbleEvent.initial() = Initial; +} + +@freezed +class ChatUserMessageBubbleState with _$ChatUserMessageBubbleState { + const factory ChatUserMessageBubbleState({ + required Message message, + required List files, + }) = _ChatUserMessageBubbleState; + + factory ChatUserMessageBubbleState.initial( + Message message, + List files, + ) => + ChatUserMessageBubbleState(message: message, files: files); +} diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart index a2a0618e41..6ad35d08db 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/chat_page.dart @@ -319,21 +319,22 @@ class _ChatContentPageState extends State<_ChatContentPage> { Widget _buildTextMessage(BuildContext context, TextMessage message) { if (message.author.id == _user.id) { + final stream = message.metadata?["$QuestionStream"]; final metadata = message.metadata?[messageMetadataKey] as String?; - return ChatUserTextMessageWidget( + return ChatUserMessageWidget( + key: ValueKey(message.id), user: message.author, - messageUserId: message.id, - message: message, + message: stream is QuestionStream ? stream : message.text, metadata: metadata, ); } else { final stream = message.metadata?["$AnswerStream"]; final questionId = message.metadata?[messageQuestionIdKey]; final metadata = message.metadata?[messageMetadataKey] as String?; - return ChatAITextMessageWidget( + return ChatAIMessageWidget( user: message.author, messageUserId: message.id, - text: stream is AnswerStream ? stream : message.text, + message: stream is AnswerStream ? stream : message.text, key: ValueKey(message.id), questionId: questionId, chatId: widget.view.id, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart index faf586e3b9..2378379e6e 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/chat_input_action_menu.dart @@ -156,6 +156,7 @@ class _ActionItem extends StatelessWidget { margin: const EdgeInsets.symmetric(horizontal: 6), iconPadding: 10.0, text: FlowyText.regular( + lineHeight: 1.0, item.title, ), onTap: onTap, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart index 77ee0b5e76..9796a28a34 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_metadata.dart @@ -49,6 +49,8 @@ class AIMessageMetadata extends StatelessWidget { child: FlowyText( m.name, fontSize: 14, + lineHeight: 1.0, + overflow: TextOverflow.ellipsis, ), ), onTap: () => onSelectedMetadata(m), diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart index eaf746cae0..ed928eba14 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/ai_text_message.dart @@ -14,12 +14,12 @@ import 'package:flutter_chat_types/flutter_chat_types.dart'; import 'ai_metadata.dart'; -class ChatAITextMessageWidget extends StatelessWidget { - const ChatAITextMessageWidget({ +class ChatAIMessageWidget extends StatelessWidget { + const ChatAIMessageWidget({ super.key, required this.user, required this.messageUserId, - required this.text, + required this.message, required this.questionId, required this.chatId, required this.metadata, @@ -28,7 +28,9 @@ class ChatAITextMessageWidget extends StatelessWidget { final User user; final String messageUserId; - final dynamic text; + + /// message can be a striing or Stream + final dynamic message; final Int64? questionId; final String chatId; final String? metadata; @@ -38,7 +40,7 @@ class ChatAITextMessageWidget extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) => ChatAIMessageBloc( - message: text, + message: message, metadata: metadata, chatId: chatId, questionId: questionId, @@ -59,7 +61,6 @@ class ChatAITextMessageWidget extends StatelessWidget { return FlowyText( LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(), maxLines: 10, - lineHeight: 1.5, ); }, ready: () { diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart index 6f60442a16..06401b76a7 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_message_bubble.dart @@ -1,6 +1,6 @@ import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart'; -import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart'; +import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -32,11 +32,11 @@ class ChatUserMessageBubble extends StatelessWidget { final metadata = message.metadata?[messageMetadataKey] as String?; return BlocProvider( - create: (context) => ChatUserMessageBloc( + create: (context) => ChatUserMessageBubbleBloc( message: message, metadata: metadata, ), - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { return Column( mainAxisSize: MainAxisSize.min, diff --git a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart index 3e37b12dd3..0b2a2efa7d 100644 --- a/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart +++ b/frontend/appflowy_flutter/lib/plugins/ai_chat/presentation/message/user_text_message.dart @@ -1,26 +1,50 @@ +import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_chat_types/flutter_chat_types.dart'; -class ChatUserTextMessageWidget extends StatelessWidget { - const ChatUserTextMessageWidget({ +class ChatUserMessageWidget extends StatelessWidget { + const ChatUserMessageWidget({ super.key, required this.user, - required this.messageUserId, required this.message, required this.metadata, }); final User user; - final String messageUserId; - final TextMessage message; + final dynamic message; final String? metadata; @override Widget build(BuildContext context) { - return TextMessageText( - text: message.text, + return BlocProvider( + create: (context) => ChatUserMessageBloc(message: message) + ..add(const ChatUserMessageEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final List children = []; + children.add( + Flexible( + child: TextMessageText( + text: state.text, + ), + ), + ); + + if (!state.messageState.isFinish) { + children.add(const HSpace(6)); + children.add(const CircularProgressIndicator.adaptive()); + } + + return Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: children, + ); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart index b74f83cebc..0ee31e41f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart @@ -237,6 +237,7 @@ class _BoardColumnHeaderState extends State { leftIcon: FlowySvg(action.icon), text: FlowyText.medium( action.text, + lineHeight: 1.0, overflow: TextOverflow.ellipsis, ), onTap: () { diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart index 6cbf39e9e3..816f553704 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart @@ -173,7 +173,10 @@ class LayoutDateField extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(fieldInfo.name), + text: FlowyText.medium( + fieldInfo.name, + lineHeight: 1.0, + ), onTap: () { onUpdated(fieldInfo.id); popoverMutex.close(); @@ -206,6 +209,7 @@ class LayoutDateField extends StatelessWidget { child: FlowyButton( margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.calendar_settings_layoutDateField.tr(), ), ), @@ -307,6 +311,7 @@ class FirstDayOfWeek extends StatelessWidget { child: FlowyButton( margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0), text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.calendar_settings_firstDayOfWeek.tr(), ), ), @@ -367,7 +372,10 @@ class StartFromButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(title), + text: FlowyText.medium( + title, + lineHeight: 1.0, + ), onTap: () => onTap(dayIndex), rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart index ebf655552c..1040081d51 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart @@ -167,6 +167,7 @@ class _CalculateCellState extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FlowyText( + lineHeight: 1.0, widget.calculation!.calculationType.shortLabel .toUpperCase(), color: Theme.of(context).hintColor, @@ -175,6 +176,7 @@ class _CalculateCellState extends State { if (widget.calculation!.value.isNotEmpty) ...[ const HSpace(8), FlowyText( + lineHeight: 1.0, calculateValue, color: AFThemeExtension.of(context).textColor, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart index eb1a76fe18..872c9bcf52 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart @@ -22,7 +22,11 @@ class CalculationTypeItem extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis), + text: FlowyText.medium( + type.label, + overflow: TextOverflow.ellipsis, + lineHeight: 1.0, + ), onTap: () { onTap(); PopoverContainer.of(context).close(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart index 2e080e8e68..85af9b3934 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/choicechip.dart @@ -39,6 +39,7 @@ class ChoiceChipButton extends StatelessWidget { decoration: decoration, useIntrinsicWidth: true, text: FlowyText( + lineHeight: 1.0, filterInfo.fieldInfo.field.name, color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart index 736fdee63a..7ebe4e9f03 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/condition_button.dart @@ -31,6 +31,7 @@ class ConditionButton extends StatelessWidget { child: FlowyButton( useIntrinsicWidth: true, text: FlowyText( + lineHeight: 1.0, conditionName, fontSize: 10, color: AFThemeExtension.of(context).textColor, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart index 6c5437ef51..d28cc5c263 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/create_filter_list.dart @@ -163,6 +163,7 @@ class GridFilterPropertyCell extends StatelessWidget { return FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( + lineHeight: 1.0, fieldInfo.field.name, color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart index 52c361ab38..145c5aa1d9 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart @@ -87,6 +87,7 @@ class _AddFilterButtonState extends State { height: 28, child: FlowyButton( text: FlowyText( + lineHeight: 1.0, LocaleKeys.grid_settings_addFilter.tr(), color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart index 31ff0e8469..2179c56604 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart @@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget { ), ), text: FlowyText( + lineHeight: 1.0, LocaleKeys.grid_row_newRow.tr(), color: Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index 56c48261f3..d127ef6f91 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -229,6 +229,7 @@ class FieldCellButton extends StatelessWidget { radius: radius, text: FlowyText.medium( field.name, + lineHeight: 1.0, maxLines: maxLines, overflow: TextOverflow.ellipsis, color: AFThemeExtension.of(context).textColor, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index baa62657b8..bf0bf78e08 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -195,6 +195,7 @@ class _CreateFieldButtonState extends State { margin: GridSize.cellContentInsets, radius: BorderRadius.zero, text: FlowyText( + lineHeight: 1.0, LocaleKeys.grid_field_newProperty.tr(), overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart index abdeb90e47..f2c2540764 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/action.dart @@ -51,7 +51,11 @@ class RowActionMenu extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis), + text: FlowyText.medium( + action.text, + overflow: TextOverflow.ellipsis, + lineHeight: 1.0, + ), onTap: () { if (action == RowAction.delete) { NavigatorOkCancelDialog( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart index 69e46a04ff..e85071a971 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart @@ -120,6 +120,7 @@ class GridSortPropertyCell extends StatelessWidget { hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( fieldInfo.name, + lineHeight: 1.0, color: AFThemeExtension.of(context).textColor, ), onTap: onTap, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart index a00bc1002f..4d509b3862 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_choice_button.dart @@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget { useIntrinsicWidth: true, text: FlowyText( text, + lineHeight: 1.0, color: AFThemeExtension.of(context).textColor, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart index 9c853414f1..ef7942ec55 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/desktop/tab_bar_header.dart @@ -222,6 +222,7 @@ class TabBarItemButton extends StatelessWidget { ), text: FlowyText( view.name, + lineHeight: 1.0, fontSize: FontSizes.s11, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart index ad1214fdba..8ff2ccea13 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart @@ -158,6 +158,7 @@ class _EditFieldButton extends StatelessWidget { child: FlowyButton( leftIcon: const FlowySvg(FlowySvgs.edit_s), text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.grid_field_editProperty.tr(), ), onTap: onTap, @@ -193,6 +194,7 @@ class FieldActionCell extends StatelessWidget { disable: !enable, text: FlowyText.medium( action.title(fieldInfo), + lineHeight: 1.0, color: enable ? null : Theme.of(context).disabledColor, ), onHover: (_) => popoverMutex?.close(), @@ -613,6 +615,7 @@ class _SwitchFieldButtonState extends State { }, text: FlowyText.medium( state.field.fieldType.i18n, + lineHeight: 1.0, color: isPrimary ? Theme.of(context).disabledColor : null, ), leftIcon: FlowySvg( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index 84d4c49177..69fe3635a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -75,9 +75,7 @@ class FieldTypeCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium( - fieldType.i18n, - ), + text: FlowyText.medium(fieldType.i18n, lineHeight: 1.0), onTap: () => onSelectField(fieldType), leftIcon: FlowySvg( fieldType.svgData, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart index 0040444bb4..c04bcab92b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart @@ -23,7 +23,10 @@ class DateFormatButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()), + text: FlowyText.medium( + LocaleKeys.grid_field_dateFormat.tr(), + lineHeight: 1.0, + ), onTap: onTap, onHover: onHover, rightIcon: const FlowySvg(FlowySvgs.more_s), @@ -47,7 +50,10 @@ class TimeFormatButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()), + text: FlowyText.medium( + LocaleKeys.grid_field_timeFormat.tr(), + lineHeight: 1.0, + ), onTap: onTap, onHover: onHover, rightIcon: const FlowySvg(FlowySvgs.more_s), @@ -114,7 +120,10 @@ class DateFormatCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(dateFormat.title()), + text: FlowyText.medium( + dateFormat.title(), + lineHeight: 1.0, + ), rightIcon: checkmark, onTap: () => onSelected(dateFormat), ), @@ -199,7 +208,10 @@ class TimeFormatCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(timeFormat.title()), + text: FlowyText.medium( + timeFormat.title(), + lineHeight: 1.0, + ), rightIcon: checkmark, onTap: () => onSelected(timeFormat), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart index 244f38326c..feff29c59e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart @@ -32,6 +32,7 @@ class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory { child: FlowyButton( rightIcon: const FlowySvg(FlowySvgs.more_s), text: FlowyText.medium( + lineHeight: 1.0, typeOption.format.title(), ), ), @@ -167,7 +168,10 @@ class NumberFormatCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(format.title()), + text: FlowyText.medium( + format.title(), + lineHeight: 1.0, + ), onTap: () => onSelected(format), rightIcon: checkmark, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart index 9ca2729cb6..dc1a6ef5c7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart @@ -61,6 +61,7 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { (meta) => meta.databaseId == typeOption.databaseId, ); return FlowyText( + lineHeight: 1.0, databaseMeta == null ? LocaleKeys .grid_relation_relatedDatabasePlaceholder @@ -134,6 +135,7 @@ class _DatabaseList extends StatelessWidget { child: FlowyButton( onTap: () => onSelectDatabase(meta.databaseId), text: FlowyText.medium( + lineHeight: 1.0, meta.databaseName, overflow: TextOverflow.ellipsis, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart index 4c56121890..3c439a0f7a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart @@ -181,6 +181,7 @@ class _AddOptionButton extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.grid_field_addSelectOption.tr(), ), onTap: () { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart index 5df44f4b49..9041b3bc60 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart @@ -107,6 +107,7 @@ class _DeleteTag extends StatelessWidget { height: GridSize.popoverItemHeight, child: FlowyButton( text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.grid_selectOption_deleteTag.tr(), ), leftIcon: const FlowySvg(FlowySvgs.delete_s), @@ -230,6 +231,7 @@ class _SelectOptionColorCell extends StatelessWidget { child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( + lineHeight: 1.0, color.colorName(), color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart index 4cb5b0d9a3..f39a0d83c4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/translate.dart @@ -97,7 +97,12 @@ class SelectLanguageButton extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( height: 30, - child: FlowyButton(text: FlowyText(language)), + child: FlowyButton( + text: FlowyText( + language, + lineHeight: 1.0, + ), + ), ); } } @@ -159,7 +164,10 @@ class LanguageCell extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.medium(languageTypeToLanguage(languageType)), + text: FlowyText.medium( + languageTypeToLanguage(languageType), + lineHeight: 1.0, + ), rightIcon: checkmark, onTap: () => onSelected(languageType), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart index ee021d59d3..af8b60b8db 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/group/database_group.dart @@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget { text: FlowyText.medium( name, color: AFThemeExtension.of(context).textColor, + lineHeight: 1.0, ), leftIcon: icon != null ? FlowySvg( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart index c9f4a796c0..c0cf547a06 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_action.dart @@ -50,7 +50,10 @@ class RowDetailPageDeleteButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()), + text: FlowyText.regular( + LocaleKeys.grid_row_delete.tr(), + lineHeight: 1.0, + ), leftIcon: const FlowySvg(FlowySvgs.trash_m), onTap: () { RowBackendService.deleteRows(viewId, [rowId]); @@ -76,7 +79,10 @@ class RowDetailPageDuplicateButton extends StatelessWidget { return SizedBox( height: GridSize.popoverItemHeight, child: FlowyButton( - text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()), + text: FlowyText.regular( + LocaleKeys.grid_row_duplicate.tr(), + lineHeight: 1.0, + ), leftIcon: const FlowySvg(FlowySvgs.copy_s), onTap: () { RowBackendService.duplicateRow(viewId, rowId); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart index 22dc9c3b47..1fd5b8822e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_banner.dart @@ -220,6 +220,7 @@ class AddEmojiButton extends StatelessWidget { child: FlowyButton( useIntrinsicWidth: true, text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.document_plugins_cover_addIcon.tr(), ), leftIcon: const FlowySvg(FlowySvgs.emoji_s), @@ -242,6 +243,7 @@ class RemoveEmojiButton extends StatelessWidget { child: FlowyButton( useIntrinsicWidth: true, text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.document_plugins_cover_removeIcon.tr(), ), leftIcon: const FlowySvg(FlowySvgs.emoji_s), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 4a6aa78adb..fe08b53ab0 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -294,7 +294,11 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget { return SizedBox( height: 30, child: FlowyButton( - text: FlowyText.medium(text, color: Theme.of(context).hintColor), + text: FlowyText.medium( + text, + lineHeight: 1.0, + color: Theme.of(context).hintColor, + ), hoverColor: AFThemeExtension.of(context).lightGreyHover, leftIcon: RotatedBox( quarterTurns: quarterTurns, @@ -381,6 +385,7 @@ class _CreateRowFieldButtonState extends State { child: FlowyButton( margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6), text: FlowyText.medium( + lineHeight: 1.0, LocaleKeys.grid_field_newProperty.tr(), color: Theme.of(context).hintColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart index b8d1560141..1564559eba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_layout_selector.dart @@ -80,6 +80,7 @@ class DatabaseViewLayoutCell extends StatelessWidget { child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( + lineHeight: 1.0, databaseLayout.layoutName, color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart index 4d01970406..252b702cd6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/database_setting_action.dart @@ -82,6 +82,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction { hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( title(), + lineHeight: 1.0, color: AFThemeExtension.of(context).textColor, ), leftIcon: FlowySvg( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart index a72a30f2ea..f7dd93d9bc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart @@ -150,6 +150,7 @@ class _DatabasePropertyCellState extends State { child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( + lineHeight: 1.0, widget.fieldInfo.name, color: AFThemeExtension.of(context).textColor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart index c56fbd09e9..b4447e1f01 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/selectable_item_list_menu.dart @@ -53,7 +53,10 @@ class SelectableItem extends StatelessWidget { return SizedBox( height: 32, child: FlowyButton( - text: FlowyText.medium(item), + text: FlowyText.medium( + item, + lineHeight: 1.0, + ), rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null, onTap: onTap, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart index 7b45582d5b..b819bb9869 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart @@ -163,7 +163,6 @@ class _FileUploadLocalState extends State<_FileUploadLocal> { fontSize: 16, maxLines: 2, textAlign: TextAlign.center, - lineHeight: 1.5, ), ], ], diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart index 560661d157..087d987262 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart @@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State { size: Size(20, 20), ), text: FlowyText( + lineHeight: 1.0, LocaleKeys.document_plugins_cover_pickFromFiles.tr(), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart index 223db0e0e1..8f05e44185 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/export_tab.dart @@ -161,7 +161,10 @@ class _ExportButton extends StatelessWidget { borderRadius: radius, ), radius: radius, - text: FlowyText(title), + text: FlowyText( + title, + lineHeight: 1.0, + ), leftIcon: FlowySvg(svg), onTap: onTap, ); diff --git a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart index 87610b916b..7fdbab05b9 100644 --- a/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart +++ b/frontend/appflowy_flutter/lib/plugins/shared/share/publish_tab.dart @@ -173,6 +173,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> { ), radius: BorderRadius.circular(10), text: FlowyText.regular( + lineHeight: 1.0, LocaleKeys.shareAction_unPublish.tr(), textAlign: TextAlign.center, ), diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart index b50f95342a..3de35d5537 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart @@ -101,7 +101,10 @@ class _TrashPageState extends State { const Spacer(), IntrinsicWidth( child: FlowyButton( - text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()), + text: FlowyText.medium( + LocaleKeys.trash_restoreAll.tr(), + lineHeight: 1.0, + ), leftIcon: const FlowySvg(FlowySvgs.restore_s), onTap: () { NavigatorAlertDialog( @@ -118,7 +121,10 @@ class _TrashPageState extends State { const HSpace(6), IntrinsicWidth( child: FlowyButton( - text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()), + text: FlowyText.medium( + LocaleKeys.trash_deleteAll.tr(), + lineHeight: 1.0, + ), leftIcon: const FlowySvg(FlowySvgs.delete_s), onTap: () { NavigatorAlertDialog( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart index 8c64563eb3..6cb0e27547 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_more_action_button.dart @@ -257,6 +257,7 @@ class ViewMoreActionTypeWrapper extends CustomActionCell { textBuilder: (onHover) => FlowyText.regular( inner.name, fontSize: 14.0, + lineHeight: 1.0, figmaLineHeight: 18.0, color: inner == ViewMoreActionType.delete && onHover ? Theme.of(context).colorScheme.error diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart index c5e160fb03..a0b4ceacee 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart @@ -204,7 +204,6 @@ class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> { FlowyText.medium( LocaleKeys.sideBar_upgradeToAILocal.tr(), maxLines: 10, - lineHeight: 1.5, ), const VSpace(4), Opacity( @@ -213,7 +212,6 @@ class _UpgradeToAILocalPlanState extends State<_UpgradeToAILocalPlan> { LocaleKeys.sideBar_upgradeToAILocalDesc.tr(), fontSize: 12, maxLines: 10, - lineHeight: 1.5, ), ), ], diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart index dffad1a465..636a33a314 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_manage_data_view.dart @@ -363,7 +363,6 @@ class _CurrentPathState extends State<_CurrentPath> { resetHoverOnRebuild: false, builder: (_, isHovering) => FlowyText.regular( widget.path, - lineHeight: 1.5, maxLines: 2, overflow: TextOverflow.ellipsis, decoration: isHovering ? TextDecoration.underline : null, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart index f2dec9550c..3334450c73 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/pages/settings_plan_comparison_dialog.dart @@ -626,7 +626,6 @@ class _Heading extends StatelessWidget { description!, fontSize: 12, maxLines: 5, - lineHeight: 1.5, ), ), ], diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart index 61a65e0c4b..1ffdd425d6 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text.dart @@ -42,7 +42,7 @@ class FlowyText extends StatelessWidget { this.fontFamily, this.fallbackFontFamily, // https://api.flutter.dev/flutter/painting/TextStyle/height.html - this.lineHeight = 1, + this.lineHeight = 1.5, this.figmaLineHeight, this.withTooltip = false, this.isEmoji = false, @@ -61,7 +61,7 @@ class FlowyText extends StatelessWidget { this.selectable = false, this.fontFamily, this.fallbackFontFamily, - this.lineHeight, + this.lineHeight = 1.5, this.withTooltip = false, this.isEmoji = false, this.strutStyle, @@ -82,7 +82,7 @@ class FlowyText extends StatelessWidget { this.selectable = false, this.fontFamily, this.fallbackFontFamily, - this.lineHeight, + this.lineHeight = 1.5, this.withTooltip = false, this.isEmoji = false, this.strutStyle, @@ -102,7 +102,7 @@ class FlowyText extends StatelessWidget { this.selectable = false, this.fontFamily, this.fallbackFontFamily, - this.lineHeight = 1, + this.lineHeight = 1.5, this.withTooltip = false, this.isEmoji = false, this.strutStyle, @@ -122,7 +122,7 @@ class FlowyText extends StatelessWidget { this.selectable = false, this.fontFamily, this.fallbackFontFamily, - this.lineHeight = 1, + this.lineHeight = 1.5, this.withTooltip = false, this.isEmoji = false, this.strutStyle, diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index c7a37bd290..6bedfe8cc4 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -1510,10 +1510,10 @@ packages: dependency: transitive description: name: pdf - sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0" + sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" url: "https://pub.dev" source: hosted - version: "3.11.0" + version: "3.11.1" pdf_widget_wrapper: dependency: transitive description: @@ -1694,10 +1694,10 @@ packages: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" realtime_client: dependency: transitive description: diff --git a/frontend/rust-lib/flowy-ai/src/ai_manager.rs b/frontend/rust-lib/flowy-ai/src/ai_manager.rs index ee510b36da..1ba15f754d 100644 --- a/frontend/rust-lib/flowy-ai/src/ai_manager.rs +++ b/frontend/rust-lib/flowy-ai/src/ai_manager.rs @@ -144,12 +144,19 @@ impl AIManager { chat_id: &str, message: &str, message_type: ChatMessageType, - text_stream_port: i64, + answer_stream_port: i64, + question_stream_port: i64, metadata: Vec, ) -> Result { let chat = self.get_or_create_chat_instance(chat_id).await?; let question = chat - .stream_chat_message(message, message_type, text_stream_port, metadata) + .stream_chat_message( + message, + message_type, + answer_stream_port, + question_stream_port, + metadata, + ) .await?; Ok(question) } diff --git a/frontend/rust-lib/flowy-ai/src/chat.rs b/frontend/rust-lib/flowy-ai/src/chat.rs index 1e8849828a..6305ac44da 100644 --- a/frontend/rust-lib/flowy-ai/src/chat.rs +++ b/frontend/rust-lib/flowy-ai/src/chat.rs @@ -5,6 +5,7 @@ use crate::entities::{ use crate::middleware::chat_service_mw::AICloudServiceMiddleware; use crate::notification::{make_notification, ChatNotification}; use crate::persistence::{insert_chat_messages, select_chat_messages, ChatMessageTable}; +use crate::stream_message::StreamMessage; use allo_isolate::Isolate; use flowy_ai_pub::cloud::{ ChatCloudService, ChatMessage, ChatMessageMetadata, ChatMessageType, MessageCursor, @@ -81,7 +82,8 @@ impl Chat { &self, message: &str, message_type: ChatMessageType, - text_stream_port: i64, + answer_stream_port: i64, + question_stream_port: i64, metadata: Vec, ) -> Result { if message.len() > 2000 { @@ -93,10 +95,19 @@ impl Chat { .store(false, std::sync::atomic::Ordering::SeqCst); self.stream_buffer.lock().await.clear(); - let stream_buffer = self.stream_buffer.clone(); + let mut question_sink = IsolateSink::new(Isolate::new(question_stream_port)); + let answer_stream_buffer = self.stream_buffer.clone(); let uid = self.user_service.user_id()?; let workspace_id = self.user_service.workspace_id()?; + let _ = question_sink + .send( + StreamMessage::Text { + text: message.to_string(), + } + .to_string(), + ) + .await; let question = self .chat_service .create_question( @@ -112,15 +123,31 @@ impl Chat { FlowyError::server_error() })?; - if self.chat_service.is_local_ai_enabled() { + let _ = question_sink + .send( + StreamMessage::MessageId { + message_id: question.message_id, + } + .to_string(), + ) + .await; + + if self.chat_service.is_local_ai_enabled() && !metadata.is_empty() { + let _ = question_sink + .send(StreamMessage::IndexStart.to_string()) + .await; if let Err(err) = self .chat_service - .index_message_metadata(&self.chat_id, &metadata) + .index_message_metadata(&self.chat_id, &metadata, &mut question_sink) .await { error!("Failed to index file: {}", err); } + let _ = question_sink + .send(StreamMessage::IndexEnd.to_string()) + .await; } + let _ = question_sink.send(StreamMessage::Done.to_string()).await; save_chat_message( self.user_service.sqlite_connection(uid)?, @@ -134,7 +161,7 @@ impl Chat { let cloud_service = self.chat_service.clone(); let user_service = self.user_service.clone(); tokio::spawn(async move { - let mut text_sink = IsolateSink::new(Isolate::new(text_stream_port)); + let mut answer_sink = IsolateSink::new(Isolate::new(answer_stream_port)); match cloud_service .stream_answer(&workspace_id, &chat_id, question_id) .await @@ -149,20 +176,20 @@ impl Chat { } match message { QuestionStreamValue::Answer { value } => { - stream_buffer.lock().await.push_str(&value); - let _ = text_sink.send(format!("data:{}", value)).await; + answer_stream_buffer.lock().await.push_str(&value); + let _ = answer_sink.send(format!("data:{}", value)).await; }, QuestionStreamValue::Metadata { value } => { if let Ok(s) = serde_json::to_string(&value) { - stream_buffer.lock().await.set_metadata(value); - let _ = text_sink.send(format!("metadata:{}", s)).await; + answer_stream_buffer.lock().await.set_metadata(value); + let _ = answer_sink.send(format!("metadata:{}", s)).await; } }, } }, Err(err) => { error!("[Chat] failed to stream answer: {}", err); - let _ = text_sink.send(format!("error:{}", err)).await; + let _ = answer_sink.send(format!("error:{}", err)).await; let pb = ChatMessageErrorPB { chat_id: chat_id.clone(), error_message: err.to_string(), @@ -178,9 +205,9 @@ impl Chat { Err(err) => { error!("[Chat] failed to stream answer: {}", err); if err.is_ai_response_limit_exceeded() { - let _ = text_sink.send("AI_RESPONSE_LIMIT".to_string()).await; + let _ = answer_sink.send("AI_RESPONSE_LIMIT".to_string()).await; } else { - let _ = text_sink.send(format!("error:{}", err)).await; + let _ = answer_sink.send(format!("error:{}", err)).await; } let pb = ChatMessageErrorPB { @@ -195,11 +222,11 @@ impl Chat { } make_notification(&chat_id, ChatNotification::FinishStreaming).send(); - if stream_buffer.lock().await.is_empty() { + if answer_stream_buffer.lock().await.is_empty() { return Ok(()); } - let content = stream_buffer.lock().await.take_content(); - let metadata = stream_buffer.lock().await.take_metadata(); + let content = answer_stream_buffer.lock().await.take_content(); + let metadata = answer_stream_buffer.lock().await.take_metadata(); let answer = cloud_service .create_answer(&workspace_id, &chat_id, &content, question_id, metadata) diff --git a/frontend/rust-lib/flowy-ai/src/entities.rs b/frontend/rust-lib/flowy-ai/src/entities.rs index 3693773177..007d7c1d06 100644 --- a/frontend/rust-lib/flowy-ai/src/entities.rs +++ b/frontend/rust-lib/flowy-ai/src/entities.rs @@ -61,9 +61,12 @@ pub struct StreamChatPayloadPB { pub message_type: ChatMessageTypePB, #[pb(index = 4)] - pub text_stream_port: i64, + pub answer_stream_port: i64, #[pb(index = 5)] + pub question_stream_port: i64, + + #[pb(index = 6)] pub metadata: Vec, } diff --git a/frontend/rust-lib/flowy-ai/src/event_handler.rs b/frontend/rust-lib/flowy-ai/src/event_handler.rs index b18a59504f..26251560b3 100644 --- a/frontend/rust-lib/flowy-ai/src/event_handler.rs +++ b/frontend/rust-lib/flowy-ai/src/event_handler.rs @@ -72,7 +72,8 @@ pub(crate) async fn stream_chat_message_handler( &data.chat_id, &data.message, message_type, - data.text_stream_port, + data.answer_stream_port, + data.question_stream_port, metadata, ) .await; diff --git a/frontend/rust-lib/flowy-ai/src/lib.rs b/frontend/rust-lib/flowy-ai/src/lib.rs index 9ebcbf75bb..be6c743d86 100644 --- a/frontend/rust-lib/flowy-ai/src/lib.rs +++ b/frontend/rust-lib/flowy-ai/src/lib.rs @@ -10,3 +10,4 @@ mod middleware; pub mod notification; mod persistence; mod protobuf; +mod stream_message; diff --git a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs index a3ba55b9a5..2fe6998a0a 100644 --- a/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs +++ b/frontend/rust-lib/flowy-ai/src/local_ai/local_llm_chat.rs @@ -16,11 +16,13 @@ use futures::Sink; use lib_infra::async_trait::async_trait; use std::collections::HashMap; +use crate::stream_message::StreamMessage; +use futures_util::SinkExt; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use serde_json::json; use std::ops::Deref; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::select; use tokio_stream::StreamExt; @@ -339,11 +341,11 @@ impl LocalAIController { .set_bool(APPFLOWY_LOCAL_AI_CHAT_RAG_ENABLED, enabled)?; Ok(enabled) } - pub async fn index_message_metadata( &self, chat_id: &str, metadata_list: &[ChatMessageMetadata], + index_process_sink: &mut (impl Sink + Unpin), ) -> FlowyResult<()> { for metadata in metadata_list { let mut index_metadata = HashMap::new(); @@ -351,52 +353,94 @@ impl LocalAIController { index_metadata.insert("at_name".to_string(), json!(format!("@{}", &metadata.name))); index_metadata.insert("source".to_string(), json!(&metadata.source)); - match &metadata.data.url { - None => match &metadata.data.content_type { - ChatMetadataContentType::Text | ChatMetadataContentType::Markdown => { - if metadata.data.validate() { - if let Err(err) = self - .index_file( - chat_id, - None, - Some(metadata.data.content.clone()), - Some(index_metadata), - ) - .await - { - error!("[AI Plugin] failed to index file: {:?}", err); - } - } - }, - _ => { - error!( - "[AI Plugin] unsupported content type: {:?}", - metadata.data.content_type - ); - }, - }, - Some(url) => { - let file_path = Path::new(url); - if file_path.exists() { - if let Err(err) = self - .index_file( - chat_id, - Some(file_path.to_path_buf()), - None, - Some(index_metadata), - ) - .await - { - error!("[AI Plugin] failed to index file: {:?}", err); - } - } - }, + if let Some(url) = &metadata.data.url { + let file_path = Path::new(url); + if file_path.exists() { + self + .process_index_file( + chat_id, + Some(file_path.to_path_buf()), + None, + metadata, + &index_metadata, + index_process_sink, + ) + .await?; + } + } else if matches!( + metadata.data.content_type, + ChatMetadataContentType::Text | ChatMetadataContentType::Markdown + ) && metadata.data.validate() + { + self + .process_index_file( + chat_id, + None, + Some(metadata.data.content.clone()), + metadata, + &index_metadata, + index_process_sink, + ) + .await?; + } else { + error!( + "[AI Plugin] unsupported content type: {:?}", + metadata.data.content_type + ); } } Ok(()) } + async fn process_index_file( + &self, + chat_id: &str, + file_path: Option, + content: Option, + metadata: &ChatMessageMetadata, + index_metadata: &HashMap, + index_process_sink: &mut (impl Sink + Unpin), + ) -> Result<(), FlowyError> { + let _ = index_process_sink + .send( + StreamMessage::StartIndexFile { + file_name: metadata.name.clone(), + } + .to_string(), + ) + .await; + + let result = self + .index_file(chat_id, file_path, content, Some(index_metadata.clone())) + .await; + match result { + Ok(_) => { + let _ = index_process_sink + .send( + StreamMessage::EndIndexFile { + file_name: metadata.name.clone(), + } + .to_string(), + ) + .await; + }, + Err(err) => { + let _ = index_process_sink + .send( + StreamMessage::IndexFileError { + file_name: metadata.name.clone(), + } + .to_string(), + ) + .await; + error!("[AI Plugin] failed to index file: {:?}", err); + }, + } + + Ok(()) + } + async fn enable_chat_plugin(&self, enabled: bool) -> FlowyResult<()> { info!("[AI Plugin] enable chat plugin: {}", enabled); if enabled { diff --git a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs index b52ef92810..dbab55610d 100644 --- a/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs +++ b/frontend/rust-lib/flowy-ai/src/middleware/chat_service_mw.rs @@ -11,7 +11,7 @@ use flowy_ai_pub::cloud::{ RepeatedRelatedQuestion, StreamAnswer, StreamComplete, }; use flowy_error::{FlowyError, FlowyResult}; -use futures::{stream, StreamExt, TryStreamExt}; +use futures::{stream, Sink, StreamExt, TryStreamExt}; use lib_infra::async_trait::async_trait; use lib_infra::future::FutureResult; @@ -48,10 +48,11 @@ impl AICloudServiceMiddleware { &self, chat_id: &str, metadata_list: &[ChatMessageMetadata], + index_process_sink: &mut (impl Sink + Unpin), ) -> Result<(), FlowyError> { self .local_llm_controller - .index_message_metadata(chat_id, metadata_list) + .index_message_metadata(chat_id, metadata_list, index_process_sink) .await?; Ok(()) } diff --git a/frontend/rust-lib/flowy-ai/src/stream_message.rs b/frontend/rust-lib/flowy-ai/src/stream_message.rs new file mode 100644 index 0000000000..d2b2b14100 --- /dev/null +++ b/frontend/rust-lib/flowy-ai/src/stream_message.rs @@ -0,0 +1,34 @@ +use std::fmt::Display; + +pub enum StreamMessage { + MessageId { message_id: i64 }, + IndexStart, + IndexEnd, + Text { text: String }, + Done, + StartIndexFile { file_name: String }, + EndIndexFile { file_name: String }, + IndexFileError { file_name: String }, +} +impl Display for StreamMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + StreamMessage::MessageId { message_id } => write!(f, "message_id:{}", message_id), + StreamMessage::IndexStart => write!(f, "index_start:"), + StreamMessage::IndexEnd => write!(f, "index_end"), + StreamMessage::Text { text } => { + write!(f, "data:{}", text) + }, + StreamMessage::Done => write!(f, "done:"), + StreamMessage::StartIndexFile { file_name } => { + write!(f, "start_index_file:{}", file_name) + }, + StreamMessage::EndIndexFile { file_name } => { + write!(f, "end_index_file:{}", file_name) + }, + StreamMessage::IndexFileError { file_name } => { + write!(f, "index_file_error:{}", file_name) + }, + } + } +}